Skip to content

bump-leg.py: a tag a leg with no origin holds without its commit, or through a tag it lacks, is told so, a tag of a tree or a blob it lacks is refused by name, and rows hold YAML_NOT_TEXT_RE's exponents of two digits (#1103) - #1131

Merged
brettheap merged 5 commits into
mainfrom
fix/1103-exponent-row
Nov 8, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Goal

The three things rv1090's review of #1090 (#912) left (#1090 issuecomment-6316009648): a row that holds YAML_NOT_TEXT_RE's exponents of two or more digits, a true refusal where a leg with no origin holds a tag without its commit, and no else that nothing reaches. This is #1103. Brett Heap's ruling of 2026-10-21T10:32:04Z held it until the v1.2.0 tag, and it is now on the v1.2.1 milestone.

Three commits: 9d14c29a on main 6da757cb, a merge of main 038dafba (clean), and fix round 1 a4beec9e, which answers rv1131's review (#1131 issuecomment-6321276436, READY with two NITs): see ## Fix round 1. Together they change scripts/bump-leg.py, tests/test_neutral_pin_entry_one_spelling.py and tests/test_bump_leg_reachability_words.py. The sections down to ## Runs are the first commit's, as rv1131 reviewed them.

What held on main

Each claim was measured on main 6da757cb by name; the issue's lines are at d5389eb6. All three held, at the same names.

  • The SHOULD held. YAML_NOT_TEXT_RE (scripts/shape_materialize.py:538-543) has two exponent groups, (?:[eE][-+][0-9]+)? and (?:[eE][-+]?[0-9]+)?, and every exponent in YAML_FLOATS (tests/test_neutral_pin_entry_one_spelling.py:92) has one digit.
    • rv1090's R2 changes [0-9]+ to [0-9] in both groups.
    • Applied to main, R2 passes all 208 rows of the spelling file, test_scaffold_pin_and_reuse.py and test_adopt_pins.py that -k "spell or entry or name or rule or stock or every" selects.
    • The head before this change quotes .5e-10, .5E+10, .5_e-10, .5e10 and .05E-12 correctly, so this is a missing row, not a defect.
  • NIT 1 held. _unreachable_refusal (scripts/bump-leg.py:1239) was called by the probe on a leg that holds the tag v1's object but not its commit.
    • With has_origin=False and either fetched, it said bump-leg-commit-not-on-remote: <tag> is not in the leg's own object store, and the leg's mount, <leg>, has no remote named origin to fetch it from, .... That is untrue: cat-file -t says tag.
    • With has_origin=True it said opensoft/openGlass has no branch containing <tag>, which is true.
  • NIT 2 held. A line trace of peeled_target (scripts/bump-leg.py:979) ran over a commit, a tag, a tag of a tag, a tree, a blob, a tag of either, a tag and a tag of a tag held without their commit, and an id the leg lacks.
    • names a {kind} was reached by the tree and the blob, and is an annotated tag of a {names} by the tag of a tree and the tag of a blob.
    • else: what = "is an annotated tag that names no commit" was reached by nothing.

What changed

  • YAML_FLOATS gains .5e-10, .5E+10, .5_e-10 and .5e10, as the issue asks.
    • It also gains .5.5e-10. All four of those reach the second pattern (\.[0-9_]+), so a cut to the first group's exponent alone still passed them. Only the first pattern, YAML 1.1's as written, takes .5.5e-10. Each group is now held on its own (R2a and R2b below).
    • The list's comment no longer says --pin admits every name in it: .e+5, .5e+3 and .5E+10 hold a +, which PIN_ARG_RE refuses, so it says a plain scalar can spell them.
    • Each new name runs through the two rows that take YAML_FLOATS: double-quoted and read back, and double-quoted by the rule alone. That is 10 new rows.
  • _unreachable_refusal: where the leg has no origin and object_kind(id) is tag, the refusal keeps its code, bump-leg-commit-not-on-remote. Its words are now:
    • <tag> is an annotated tag the leg's own object store holds without the commit it names, and the leg's mount, <leg>, has no remote named origin to fetch that commit from, so whether a branch of <repository> reaches it is not known
    • Remediation: check the tag -- the commit it names belongs to the LEG, not to the assembly root -- and push that commit to a branch of the leg first if it is not pushed yet; give <leg> back a remote named origin, ... (an em dash in the tool, as in the words beside it).
    • An id the leg does not hold keeps its words, and so does every case with an origin.
    • Fix round 1 gives two rarer held tags this branch caught their own true words: a tag through a tag the leg lacks, and a tag of a tree or a blob.
    • No other text pins the old sentence. grep of tests/, README.md, docs/, the handbook and AGENTS.md finds it only in bump-leg.py, and AGENTS.md's codes table is unchanged.
  • peeled_target: the three-way if/elif/else is now one expression: is an annotated tag of a {names} for a tag, names a {kind} otherwise, with a comment saying why nothing else is left. The words for a tree, a blob and a tag of either are unchanged, by the same trace on the head. shape_materialize.not_a_commit, which commit_on_forge uses, still says "an annotated tag that names no commit"; it is not touched.

Rows, failing on main

  • tests/test_bump_leg_reachability_words.py, test_a_tag_a_leg_with_no_origin_holds_alone_is_told_it_is_held.
    • The setup: a tag of a commit pushed nowhere is made in a clone of the leg's remote, and only the tag object goes into the leg (git hash-object -t tag -w). The leg's origin is renamed to upstream, then both runs go through assert_one_refusal.
    • It asserts the note and the two lines exactly, through told_no_origin, and that the old sentence is not in stderr.
    • It reuses the file's private_remote, without_origin, runner and origin_back.
  • On main's code (the two test files copied onto 6da757cb, git 2.43): 1 failed, 159 passed. The one failure is that row, which got main's REFUSED bump-leg-commit-not-on-remote: <tag> is not in the leg's own object store.
    • The 10 float rows pass on main, as they should, because main's rule is right. They are there to kill R2, below.
  • No assert joins conditions with and or or.

Mutants

Each mutant was applied alone to a scratch clone holding this commit's files, then restored and compared with the worktree.

Mutant Rows run Result
R2: [0-9]+ to [0-9] in both exponent groups the spelling file killed: 10 failed, 115 passed (all 10 new rows)
R2 on main's rows the 208 rows above survives: 208 passed
R2a: the first group only the spelling file killed: 2 failed (.5.5e-10, both rows)
R2b: the second group only the spelling file killed: 4 failed (.5_e-10 and .5e10, both rows)
N1: the held-tag branch removed, so the old words answer -k "no_origin or held" in the reachability file killed: 1 failed, 3 passed

Runs

All runs go through lane 5's run9.sh: a stub gh first on PATH, a bogus token, every LANES_* variable cleared, EMAIL=service@opensoft.dev, GIT_SSH_COMMAND=false and GIT_CONFIG_GLOBAL=/dev/null.

  • The two changed test files on the head:
    • git 2.43: 160 passed;
    • git 2.47.1 (a git 2.47.1 build first on PATH; git --version printed git version 2.47.1): 160 passed.
  • git 2.43, every test file that runs bump-leg.py. The commit changes production code in scripts/bump-leg.py only. scripts/shape_materialize.py is unchanged: only its test rows were added, and those ran on both gits above. So the related-file runs take the test files that name bump-leg or bump_leg, and the files that name only shape_materialize are not needed.
    • The set is grep -rl -E 'bump-leg|bump_leg' tests --include='*.py': 56 files, sorted and split into two halves. It holds tests/test_bump_leg_reachability_words.py again, and tests/test_repo_hygiene.py and tests/test_agents_md_codes.py.
    • shard a, 28 files: 1038 passed, 1 skipped, 0 failed;
    • shard b, 28 files: 2324 passed, 1 skipped, 0 failed;
    • together 3362 passed, 2 skipped, 0 failed. Both skips are rows for another host: one needs a disk that ignores case (macOS, Windows), in tests/test_bump_leg.py, and one is Windows' own stdout default, in tests/test_stdout_encoding.py.
  • git 2.43, the trio tests/test_repo_hygiene.py, tests/test_agents_md_codes.py and tests/test_cli_reference.py: 170 passed.

Fix round 1

Commit a4beec9e, on the merge of main 038dafba into the branch (ae830e2b, clean, tree 10c2372fe578). It answers rv1131's review (#1131 issuecomment-6321276436).

What changed

  • NIT 1: the no-origin words for a held tag were untrue in two rarer cases. The branch keyed on object_kind(id) == "tag" alone, so every held tag peeled_target cannot peel was told the leg holds it "without the commit it names". Each case now reads the tag chain from the tags' own headers, git cat-file tag's type and object lines, one tag at a time while the leg holds the next (tag_chain_end, new). The peel (<id>^{}) says nothing where the leg lacks an object on the way.
    • (a) A tag through a tag the leg lacks, with the commit at the end held or not: still bump-leg-commit-not-on-remote, now in these words:
      • <tag> is an annotated tag the leg's own object store holds without a tag it leads through, <inner tag>, and the leg's mount, <leg>, has no remote named origin to fetch that tag from, so what that tag names, and whether a branch of <repository> reaches it, are not known
      • Remediation: check the tag -- the tags it leads through, and what they name, belong to the LEG, not to the assembly root -- and push them to the leg first if they are not pushed yet, and any commit they name to a branch of it; give <leg> back a remote named origin, ... (an em dash in the tool, as in the words beside it).
    • (b) A tag, or a tag of a tag, of a tree or a blob the leg lacks names no commit, so peeled_target now refuses it bump-leg-target-not-a-commit, in the words a tag of a held tree or blob already had: --to <tag> is an annotated tag of a tree in the leg (<repository>), not a commit. That is with or without an origin, since no fetch makes a tree a commit.
      • Measured first: for the same tag with its tree or blob held, main and 9d14c29a both refuse it bump-leg-target-not-a-commit in those words. The new refusal is byte for byte the same as main's for the same tag id held with its tree or blob, in all 6 (leg, id) pairs of (b) below.
      • With an origin, such a tag was refused bump-leg-commit-not-on-remote, " has no branch containing ", with "check the commit". That is the one with-origin answer this round changes.
    • The issue's own case is as it was: a tag held without its commit, and a tag of a tag with both tags held, keep the head's words, byte for byte.
    • Neither code is added or removed. Neither code, nor any changed sentence, appears in README.md, docs/, the handbook or AGENTS.md; tests/test_agents_md_codes.py and tests/test_cli_reference.py pass.
    • peeled_target and _unreachable_refusal keep their docstrings' accounts true: the adopt-project.py and shape_materialize.py: a tag whose target the clone lacks is called a tag that names no commit, a recorded tag id is asked only of a clone still in place, and a pinned product only PyYAML misreads is written bare #912 paragraph now says a tag held without THE COMMIT it names, and each function says what (a) and (b) are now told.
    • A cycle of tags made with refs/replace/ is never walked: git's own peel never leaves one (measured, git 2.43: rev-parse <tag>^{} was still running at a 10 s timeout), and the walk is reached only where the peel failed.
  • NIT 2: mutant M5 survived. .5.5E-10 is added to YAML_FLOATS: only the first exponent group takes it, and only with its E. The list's comment says so.

Words compared with main's and the head's tool

The probe called peeled_target and _unreachable_refusal from main 038dafba, the head 9d14c29a and this head on 15 legs x 10 ids x 4 (has_origin, fetched), as rv1131's did, with legs for (a) and (b) added. Each id was asked as typed (raw) and, where peeled_target returned, as it returned (flow). That is 1578 answers from main and the head, and 1554 from this head, where 6 (leg, id) pairs are refused before the verdict.

Pair The same Differ
9d14c29a vs this head 1532 of 1578 46: (a) 16, (b) 30, and nothing else
main vs this head 1492 of 1578 86: those 46, the issue's case (12) and 28 raw asks
  • (a): 4 legs, has_origin=False only. Each holds the outer tag without the inner one: a tag of a tag of a commit, with the commit or without it, and a tag of a tag of a tree, with the tree or without it.
  • (b): 6 (leg, id) pairs, peeled_target and the four verdicts each: a tag of a tree and a tag of a blob, each alone and under a held tag of it, and those two tags of a tag, all held without the tree or blob.
  • The 28 raw asks ask _unreachable_refusal directly about a held tag of a tree or blob, which the flow never does: peeled_target refuses it first, with the tree or blob held or not. They keep the head's words.
  • Every other answer, every id with an origin but (b)'s, every missing id and every held commit keeps main's words.

Rows, failing on 9d14c29a and on main

In tests/test_bump_leg_reachability_words.py, in the file's style, through assert_one_refusal (both runs, exit 2, the same words, nothing written). They reuse private_remote, without_origin, runner, told_no_origin, refusal and origin_back. Two helpers, pushed_nowhere and written_into_the_leg, are split out of held_without_its_commit, which keeps what it did.

  • test_a_tag_through_a_tag_the_leg_lacks_is_told_which_tag_it_lacks[commit-held, commit-missing]: (a). The leg holds only the outer tag; the commit is the leg's own pin, or one pushed nowhere. The note and the two lines are asserted exactly, and "without the commit it names" is not in stderr.
  • test_a_tag_of_a_tree_or_a_blob_the_leg_lacks_is_refused_by_name[...]: (b), 6 rows. The kinds are a tag of a tree, a tag of a blob, and a tag of a tag of a tree with both tags held. Each runs with the leg's origin and without it. The tree and the blob are pushed nowhere, so no fetch brings them, and the row checks that afterwards. The two lines are asserted exactly, with the note where there is no origin.
  • On 9d14c29a's bump-leg.py: 8 failed, 59 passed (git 2.43; the rows of tests/test_bump_leg_reachability_words.py and tests/test_bump_leg_dry_run_truth.py). The 8 are exactly the 8 new rows: the 2 of (a) and the 6 of (b). The old file was put back by sha256 afterwards.
  • On main 038dafba's bump-leg.py: 9 failed, 58 passed (the same two files, git 2.43). The 9 are those 8 and the issue's row, test_a_tag_a_leg_with_no_origin_holds_alone_is_told_it_is_held. The old file was put back by sha256 afterwards.
  • .5.5E-10 adds 2 rows, one to each test that takes YAML_FLOATS. They pass on main and on the head, as they should: the rule is right, and they are there to kill M5.
  • No assert joins conditions with and or or. Added lines are at most 79 characters wide. The only non-ASCII character is the em dash in the tool's own words.

Mutants

Each was applied alone in the worktree, at this commit. Its rows were run, then the file was restored and checked against the commit by sha256. For bump-leg.py the rows are tests/test_bump_leg_reachability_words.py and tests/test_bump_leg_dry_run_truth.py; for shape_materialize.py, the spelling file.

Mutant Result Rows that failed
M5: shape_materialize.py, the first exponent group's [eE] cut to [e] killed: 2 failed, 125 passed .5.5E-10, in test_a_name_yaml_takes_for_no_text_is_double_quoted and test_the_rule_quotes_it_whatever_the_projects_reader_reads
NM1: names = names or tag_chain_end(submodule, target)[0] cut to names = names, so the header is never read where the peel says nothing killed: 6 failed, 61 passed the 6 rows of (b)
NM2: the walk's tag = named step replaced by a second return, so the chain is never followed past its first tag killed: 2 failed, 65 passed the 2 tag of a tag of a tree rows of (b)
NM3: (a)'s branch made dead, if names == "tag": to "tags" killed: 2 failed, 65 passed the 2 rows of (a)
NM4: a blob not refused, ("tree", "blob") to ("tree",) killed: 4 failed, 63 passed the 2 tag of a blob rows of (b), and tag of a blob for local-remote-dir and origin in test_a_tree_a_blob_or_a_tag_of_either_is_refused_by_name_in_both_runs
NM5: (a)'s note names the outer tag, {named} to {commit} killed: 2 failed, 65 passed the 2 rows of (a)
NM6: the held-tag words given with an origin too, not has_origin and cut killed: 2 failed, 65 passed test_a_tag_the_leg_holds_without_its_commit_is_not_on_the_remote, for local-remote-dir and origin
NM7: the header key read is tag, not type killed: 8 failed, 59 passed the 8 new rows

No mutant survived. After each, scripts/shape_materialize.py and scripts/bump-leg.py were restored from the commit and their sha256 checked: a37e3d83fbcda475... and d6822093b1aa4b0e... both matched, with the worktree clean.

Runs

All go through lane 5's run9.sh, as above, at a4beec9e.

  • The two changed test files (tests/test_neutral_pin_entry_one_spelling.py and tests/test_bump_leg_reachability_words.py): 170 passed, which is the first commit's 160 and this round's 10 new rows.
    • git 2.43: 170 passed;
    • git 2.47.1 (a git 2.47.1 build first on PATH; git --version printed git version 2.47.1): 170 passed.
  • git 2.43, the path guard and the trio: tests/test_path_guard_hygiene.py, tests/test_repo_hygiene.py, tests/test_agents_md_codes.py and tests/test_cli_reference.py: 219 passed.
  • git 2.43, every test file that runs bump-leg.py, picked as above (grep -rl -E 'bump-leg|bump_leg' tests --include='*.py'): 57 files, which are the 56 files of the first commit's set and tests/test_path_guard_hygiene.py, which main gained since. Sorted and split in two:
    • shard a, 29 files: 1094 passed, 1 skipped, 0 failed;
    • shard b, 28 files: 2325 passed, 1 skipped, 0 failed;
    • together 3419 passed, 2 skipped, 0 failed. The two skips are the same two rows for another host as before: tests/test_bump_leg.py (a disk that ignores case) and tests/test_stdout_encoding.py (Windows' own stdout default).
  • Every run in this section was on a4beec9e itself, committed, with the worktree clean (dirty=0 in each log's START line).

Merge-trees

Refreshed for fix round 1 (rv1131's body NIT). git merge-tree --write-tree of the head a4beec9e was run against origin/main, re-fetched (-c gc.auto=0 -c maintenance.auto=false), and against each open PR that touches these files:

Against Head Result
origin/main, re-fetched at 15:16Z on 2026-10-22 038dafba9dbe clean, tree f571322318b1, the head's own tree: the head holds main by its merge
#1107 fix/1019-points-at-row (DRAFT) ee48b5fe9693 clean, tree 7383abd73770
#1157 fix/1100-refspec-followups (DRAFT) a35a97ea52f4 clean, tree de32065b7371

NIT round

rc1131 re-checked fix round 1 at a4beec9e and found it READY with four NITs (#1131, issuecomment-6347256815). The commit 55c30f20f005 mends the first three, in scripts/bump-leg.py and tests/test_bump_leg_reachability_words.py. NIT 4, the title, is not in a commit. Nothing the tool does changes: the production edit is a comprehension that gives the same dict and a longer docstring.

What changed

  • NIT 1, SonarCloud python:S7494 at bump-leg.py:999. In tag_chain_end, dict(line.split(" ", 1) for line in lines if " " in line) is now {key: value for key, sep, value in (line.partition(" ") for line in lines) if sep}, the form rc1131 gave.
    • Same dict: tools/equiv2.py compares the two on every list of up to two lines of up to five characters, and every list of up to three lines of up to three characters, over a, b, a space and a tab (2478716 distinct lists: the script runs the two ranges one after the other, and the 7311 lists of up to two lines of up to three characters sit in both, so it makes 2486027 comparisons, as rc1131's count of the union shows), and on a real tag header with a continuation line, a line with no space and a type line with a trailing space: equal in all. The commit message says "every list of up to three lines"; the coverage is the one in this sentence (equiv.py's first pass, 3864591 lists, drew its three-line lists from a prefix of the strings).
    • The rule: SonarQube's local analysis of a standalone file holding both forms (project opensoft_openRepoShape, python, MAIN) reports one issue, python:S7494 MINOR, at the line of the old dict(...) form, and none at the comprehension.
    • SonarCloud's own analysis, read through the SonarQube API (rc1131's reading, issuecomment-6352247887, posted 2026-10-22T22:00:11Z; lane 5 read the same two answers again between 12:05Z and 12:08Z on 2026-10-23): for PR 1131 the API lists exactly one issue, python:S7494 at scripts/bump-leg.py:999 (MINOR, created 2026-10-22T13:31:04Z), and its status is CLOSED, so there are 0 open issues. The quality gate is OK on all five conditions (new reliability, security and maintainability ratings 1, new duplicated lines density 0.0, new security hotspots reviewed 100.0). The API gives no commit, so it does not say which analysis it read: it shows the issue closed, not that the analysis was of 55c30f20.
  • NIT 2, the walk of two headers. TAGS_OF_NO_COMMIT has a fourth kind, tag of a tag of a tag of a tree: three tags, all held, and the tree pushed nowhere, run with and without an origin like the other kinds. Its values are now (what, depth), and the row makes depth - 1 tags over the first one (outer1-w1131, outer2-w1131) where it made the one outer-w1131 for the second kind; no other row reads the dict.
  • NIT 3, tag_chain_end's docstring. It now says ("tag", <id>) is also what it returns "through a tag whose header misstates what it names (type tag over an <id> the leg holds as something else, which only a malformed tag has)".

Rows, failing on main

Both rows of the new kind (test_a_tag_of_a_tree_or_a_blob_the_leg_lacks_is_refused_by_name[tag of a tag of a tag of a tree-origin] and [...-no-origin]) fail on main's bump-leg.py (961640c6) and pass on the head's before this commit (a4beec9e's). The two test files that hold rows for peeled_target (tests/test_bump_leg_reachability_words.py and tests/test_bump_leg_dry_run_truth.py) run 69 rows:

bump-leg.py from Result Failing rows
main 961640c6 11 failed, 58 passed the 9 rows of fix round 1, and the 2 new rows
a4beec9e (this PR before the commit) 69 passed none: the new rows hold on the code as it was
55c30f20 69 passed none

With main's file the new rows fail on the code of the refusal, as the other rows of the kind do: the row expects REFUSED bump-leg-target-not-a-commit, and the run with an origin prints bump-leg-commit-not-on-remote: testorg/Atlas-spec has no branch containing <the tag> (main's peeled_target returns the tag as typed where its peel cannot be read, because the leg lacks the tree, and the run goes on to the reachability verdict), and the run without one prints the git fetch origin ... has no remote named origin note and goes the same way.

Mutants

Each was applied alone to scripts/bump-leg.py, the 69 rows run on git 2.43.0, and the file restored from HEAD; all 11 restores matched (bl=1a7a69d38f737dce, dirty=0). The mutants are in tools/mutate.py.

Mutant Result Rows that failed
NX1 (rc1131's): the walk reads at most two headers, while True: as for _ in range(2): killed: 2 failed, 67 passed the 2 rows of the new kind: the walk stops at tag, and the refusal is bump-leg-commit-not-on-remote, not bump-leg-target-not-a-commit
NX0: the walk reads one header killed: 4 failed the 2 rows of the third kind and the 2 of the new one
NM2 (fix round 1's): tag = named replaced by a return killed: 4 failed the same 4 rows
NX2 (rc1131's): no check that the leg holds the next tag killed: 2 failed the 2 rows of test_a_tag_through_a_tag_the_leg_lacks_is_told_which_tag_it_lacks
C3: the comprehension's key: value as value: key killed: 10 failed the rows that read a tag's header
C6: the comprehension keeps the lines with no space (if not sep) killed: 10 failed the same 10 rows
NX3: the walk reads at most three headers survives: 69 passed none
C1: the if sep filter dropped survives: 69 passed none
C2: rpartition for partition survives: 69 passed none
  • NX3 survives by construction. A walk bounded at three headers gives the code's answer on every chain of up to three tags, and the longest chain the rows hold is three. Any bound survives a chain of that length, so a row of four tags would only move the survivor to a bound of four. NX1 is the mutant the review named, and it is killed.
  • C1 and C2 are equivalent for every header git writes. The type and object lines each carry exactly one space and each appears once, and nothing reads another key. C1 keeps a line with no space as a key with an empty value, which .get("type", "") and .get("object", "") already say of a missing key. C2 splits at the last space, which for a line with one space is the first. Where they could differ is a hand-forged header that repeats a bare type line after type tag; no row forges one.

Runs

Every run went through run9.sh with EMAIL=service@opensoft.dev, on 55c30f20 itself, committed, with the worktree clean (dirty=0 in each log's START line). Logs are in $Q/v124/n1131/, where $Q is the lane's scratchpad.

Run git Result
tests/test_bump_leg_reachability_words.py, tests/test_neutral_pin_entry_one_spelling.py, tests/test_bump_leg_dry_run_truth.py, tests/test_path_guard_hygiene.py, tests/test_agents_md_codes.py, tests/test_cli_reference.py, tests/test_pinned_copy_text.py (g243.log) 2.43.0 (/usr/bin/git) 402 passed
the first three of those files (g247.log) 2.47.1 (first on PATH; git --version printed git version 2.47.1) 196 passed
  • The 57 files that run bump-leg.py, which fix round 1 ran in two shards (3419 passed), were not run again: the edit is inside tag_chain_end, and the rows that reach it are the two files above, which ran in full. CI's tests job on the pushed head is the full-suite run.
  • tests/test_path_guard_hygiene.py passes: the commit adds no is_file, is_dir, exists, is_symlink or os.path guard.
  • No added line is wider than 79 characters, none is non-ASCII, and no added assert joins conditions with and or or.

Merge-trees at 55c30f20

git merge-tree --write-tree of the head against origin/main, re-fetched (-c gc.auto=0 -c maintenance.auto=false), and against each open PR that touches one of this PR's three files. Of the 41 open PRs listed at 20:09Z (gh pr list --json number,headRefName,headRefOid,files), only #1157 and #1107, besides this one, touch them.

Against Head Result
origin/main, re-fetched before the push 961640c681b5 clean, tree 4d7968be5547: the head's tree and docs/release-notes/v1.2.0.md, the one file main gained since 038dafba (#1155)
#1157 fix/1100-refspec-followups (DRAFT) 368b87e9be87 (re-measured at 2026-10-23T12:06:38Z, after #1157's NIT round, which changes only tests/test_family_bump_fetch_refspecs.py; baa19972a271 gave the same clean merge with tree bd6ebc5cd3af, and a35a97ea52f4 when this round began) clean, tree e036c02d6a12
#1107 fix/1019-points-at-row (DRAFT) ee48b5fe9693 clean, tree a72145c05633

After the second re-check

rc1131 found this round READY at 55c30f20f (#1131, issuecomment-6352247887), with two NITs of its own, both in this body and neither in the code. This body now answers both: the count of lists in NIT 1's equivalence check (2478716 distinct lists, 2486027 comparisons), and SonarCloud's own analysis (python:S7494 CLOSED, 0 open issues, gate OK, no commit named by the API). The one other change is the #1157 row of the merge-trees table, which moved when #1157's NIT round added a commit. The head 55c30f20f is unchanged.

Closes #1103

Claim: #1103 (comment)

Lane: openreposhape-5 (openRepoShape-5)

🤖 Generated with Claude Code

Summary by Sourcery

Improve bump-leg validation and diagnostics for incomplete annotated tags while expanding coverage for YAML float spellings with multi-digit exponents.

Bug Fixes:

  • Correct bump-leg diagnostics for tags held without their commits, tags that lead through missing tags, and tags of unavailable trees or blobs.
  • Reject tags of trees or blobs by name instead of treating them as unreachable commits.

Enhancements:

  • Clarify tag-chain handling and refusal messages for missing objects and malformed or nested annotated tags.

Tests:

  • Expand reachability tests for missing commits, nested tag chains, and tags of trees or blobs.
  • Add YAML spelling cases with multi-digit exponents to cover both float-matching patterns.

… told it is held, `peeled_target` loses the `else` nothing reaches, and rows hold shape_materialize.py's `YAML_NOT_TEXT_RE` exponents of two digits (#1103)

rv1090's review of #1090 (#912) left one SHOULD and two NITs, measured on
main 6da757c:

- No row held an exponent of two or more digits. With `[0-9]+` cut to
  `[0-9]` in both exponents of `YAML_NOT_TEXT_RE` (rv1090's R2), every
  spelling and entry row still passed, and `.5e-10`, `.05E-12` and
  `.5_e-10` would be written bare, which PyYAML reads as numbers.
  `YAML_FLOATS` gains `.5e-10`, `.5E+10`, `.5_e-10`, `.5e10` and
  `.5.5e-10`, the last because only the pattern as written takes it, so
  each exponent is held on its own.
- In a leg with no `origin`, a tag id the leg holds without its commit
  (which `peeled_target` gives the verdict as typed since #912) was told
  it "is not in the leg's own object store". The tag is: it is now told
  the leg holds the tag and not the commit it names, under the same
  code, `bump-leg-commit-not-on-remote`.
- `peeled_target`'s `else: "is an annotated tag that names no commit"`
  was reached by nothing: a tag that names a commit, or whose peel cannot
  be read, has returned, and `^{}` peels through every tag. It is gone.
  `shape_materialize.not_a_commit` keeps the words, for `commit_on_forge`.

No refusal code changes, and none is added.

Closes #1103

Lane: openreposhape-5 (openRepoShape-5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Implementation

Trace tag headers when peeling is unavailable so refusals distinguish missing commits, missing intermediate tags, and tags of trees or blobs, while simplifying the unreachable else branch.

  • scripts/bump-leg.py L979-1009, L1043-1058, L1376-1402 (+3 small edits)

Tests

Add regression coverage for the corrected no-origin tag diagnostics, missing links and non-commit tag targets, and exercise the YAML exponent patterns with multi-digit exponents.

  • tests/test_bump_leg_reachability_words.py L20-23, L45, L541-722
  • tests/test_neutral_pin_entry_one_spelling.py L87-102

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

Independent review of #1131 (#1103) at 9d14c29a, for lane openRepoShape-5: READY

The PR does what #1103 asks. It changes no refusal code, no output moved that should not have, every number I re-ran matches, and every merge is clean against current main 4ed7c7bf. I found two NITs and one stale passage in the body. None blocks.

Findings

NIT 1: the new no-origin words are still untrue in two rarer cases the same branch catches. scripts/bump-leg.py:1292

  • Why: the branch is chosen on object_kind(id) == "tag" alone. So every held tag object that peeled_target cannot peel gets "holds without the commit it names", not only a tag of a missing commit.
  • I ran both cases end to end through bump-leg.py, both the dry run and the real run, exit 2, in a leg whose origin was renamed ($Q/v118/rv1131/e2e-head/):
    • Outer tag held, inner tag missing, commit held: the leg holds the outer tag and the commit, but not the tag in between. The head says the store holds the tag "without the commit it names". That is false: the commit is there, and what is missing is the inner tag.
    • A tag of a tree or a blob, held without the tree or blob (also a tag of a tag of a tree): the head says "without the commit it names" and "push that commit". That is false: the tag names a tree or a blob, not a commit.
  • No regression: main said "is not in the leg's own object store" in both cases, which was also false. The issue's own case is now true.
  • Possible fix: word it from the tag's own header (git cat-file tag <id> shows type, and you can walk on while each named object is held). Or use a sentence that is true in all four cases, such as "an object it names, through every tag, is not held there". It could also go on a checklist as a known limit.

NIT 2: a regex mutant of my own survives. It is the same kind of gap the issue's SHOULD closes. tests/test_neutral_pin_entry_one_spelling.py:97-100

  • Mutant M5: change [eE] to [e] in YAML_NOT_TEXT_RE's first exponent group. All 125 rows of the spelling file pass.
  • Why it survives: every row with a capital E is also taken by the second pattern. No row holds a capital-E exponent that only the first pattern takes, such as .5.5E-10 or .E+5.
  • What it would ship: .5.5E-10 written bare.
  • Impact is low: only a reader that follows YAML 1.1's pattern as written reads that as a float. PyYAML 6.0.3 already reads .5.5e-10 as text.
  • Fix: add .5.5E-10 to YAML_FLOATS. I did not run that fix.

NIT (body, for the coordinator): main moved after the body was written.

Checked and found true

Head and files

  • The head is 9d14c29a, one commit on 6da757cb, three files.
  • bump-leg.py's sha256 is 6a54d9f177ce2834..., as the writer's notes say.
  • None of the three files, nor shape_materialize.py, differs between 6da757cb and 4ed7c7bf.

NIT 1 (brief item 1)

  • Words compared with main's tool: I called peeled_target and _unreachable_refusal on main and on the head, over 9 legs x 9 ids x 4 combinations of (has_origin, fetched). 726 of 764 answers are byte for byte the same.
  • Every one of the 38 differences has has_origin=False and an id that is a tag object the leg cannot peel. Every case with an origin, every missing id, and every held commit keeps main's words.
  • fetched: the new branch gives the same words for fetched True and False. Through fetch_leg, has_origin=False only happens after a failed fetch, so fetched=True never reaches it.
  • The code: it stays bump-leg-commit-not-on-remote.
  • Cases that do not reach the branch:
    • A lightweight tag name (--to rv-lw) is refused earlier, bump-leg-target-not-a-commit ... is not a 40-hex commit, the same on main and the head.
    • A tag whose commit the leg holds is peeled and gets the unchanged bump-leg-commit-local-only words.
  • The two true cases, end to end: a tag held without its commit, and a tag of a tag with both tags held but not the commit. Both get the new words, as the PR intends.
  • Old sentence: git grep finds it only for the missing-id case (bump-leg.py:1306, the existing row at tests/test_bump_leg_reachability_words.py:439) and in the new row's docstring and negative assert. Nothing in README, docs/, the handbook, AGENTS.md or the templates pins it.

NIT 2 (brief item 2)

  • All 72 peeled_target answers are the same on main and the head.
  • The writer's line trace, re-run on main: names a {kind} is reached by the tree and the blob, the tag-of line by the tag of a tree and the tag of a blob, and the else by nothing.
  • shape_materialize.not_a_commit is untouched.
  • Cognitive complexity by S3776 (limit 15): peeled_target is 12, was 14 (rv1090 also counted 14). _unreachable_refusal is 10, was 8.

The SHOULD (brief item 3)

  • Patterns: .5.5e-10 is taken only by the first pattern. .5e-10, .5E+10, .5_e-10 and .5e10 are all taken by the second; .5_e-10 and .5e10 only by the second.
  • The --pin claim: PIN_ARG_RE refuses exactly .e+5, .5e+3 and .5E+10, the three names with a +. PLAIN_SCALAR_RE takes all 25 names, so the list's new comment is true.
  • Mutants: each was applied alone, then restored and checked by sha256 (a37e3d83... and 6a54d9f1..., clean every time).
Mutant Result Rows that failed
R2 (both groups) killed: 10 failed, 115 passed all 5 new names
R2a (first group) killed: 2 failed .5.5e-10
R2b (second group) killed: 4 failed .5_e-10, .5e10
M2 (first group's sign - only) killed: 2 failed .e+5
M3 (second group's sign required) killed: 6 failed .5e3, .5E3, .5e10
M5 (first group lower-case e only) survives: 125 passed none (NIT 2)
M6 (second group lower-case e only) killed: 2 failed .5E3
R2 on main's own rows survives: 208 passed, 132 deselected matches the body
NB1 (new branch made dead) killed: 1 failed, 58 passed the new row
NB2 (new branch taken with an origin too) killed: 2 failed the held-tag rows with an origin
NB3 (peeled_target's condition flipped) killed: 8 failed the NOT_COMMITS rows
NB4 ("that commit" changed to "it") killed: 1 failed the new row

All runs went through run9.sh with EMAIL set.

Runs (brief item 4)

Run git Result
The two changed files at the head 2.43 160 passed
The two changed files at the head 2.47.1 (checked: the build is first on PATH inside the run) 160 passed
The two files on the predicted merge with main 4ed7c7bf (tree 887b4140) 2.43 160 passed
The two files on the predicted merge of main, the head and #1107 (tree 6c4791d1) 2.43 168 passed (#1107 adds 8 rows)
The trio at the head 2.43 170 passed
The writer's shard a, 28 files, re-run 2.43 1038 passed, 1 skipped (the case-insensitive-disk row, test_bump_leg.py:697)
The head's two test files on main's code 2.43 1 failed, 159 passed; the failure is the new row, with main's old words

Merges

Sonar-style checks

  • No added assert joins conditions with and or or.
  • Added lines are at most 79 characters wide. The only non-ASCII character is the em dash the tool's words beside it already use.
  • SonarCloud on PR 1131: quality gate OK, new duplication 0.0%, no open issues.

Title and body (brief item 5)

  • The title is 224 characters and true.
  • closingIssuesReferences is [1103], and the only closing keyword is Closes #1103.
  • The bump-leg test set has 56 files, the same list as the writer's two shards.
  • Other body claims re-measured and true: the 10 new rows, the N1-style kill, the line numbers 538-543, :92, :979 and :1239 on main, and the head quoting all five long-exponent names.

Not verified

  • Shard b (2324 passed, 1 skipped) and so the 3362 total: not re-run, because the host load was 50-70 and the 30-minute budget was spent.
  • The writer's 391a4c89 tree itself: I ran the two files on the three-way merge with current main, not on that tree.
  • The tests CI job: it was still IN_PROGRESS when I read the checks. tests-git-bash, package and SonarCloud were green.
  • A refs/replace object that changes a peeled object's type: not probed.
  • The .5.5E-10 fix proposed in NIT 2: not run.

Checklist candidates

  • peeled_target and _unreachable_refusal: read a held tag's type header, so a tag of a tree or blob held without its target is refused bump-leg-target-not-a-commit by name, and a missing inner tag is named as such (NIT 1).
  • YAML_FLOATS: add a capital-E exponent that only the first pattern takes (NIT 2, mutant M5).

Scratch left

  • Worktrees: all five rv1131-* worktrees were removed with git worktree remove; --force only on head and main, for my untracked probe file.
  • Refs: all five refs/l5rv/rv1131-* were deleted.
  • Objects: three unreferenced commit objects for the predicted merges (a86903b2, 01850969, 1e9bfc3d) remain in the shared object store, with no refs.
  • Notes, logs, fixtures and probe tools: /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/rv1131/
  • Basetemps (10 directories):
    • /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v110/bt/rv1131-basefail
    • /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v110/bt/rv1131-e2e
    • /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v110/bt/rv1131-mut
    • /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v110/bt/rv1131-own243
    • /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v110/bt/rv1131-own247
    • /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v110/bt/rv1131-pm
    • /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v110/bt/rv1131-pm3
    • /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v110/bt/rv1131-r2main
    • /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v110/bt/rv1131-sharda
    • /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v110/bt/rv1131-trio
  • Running: nothing of mine. Every background run and monitor has ended.

Lane: openreposhape-5 (openRepoShape-5)

…that tag, and a tag of a tree or a blob the leg lacks is refused `bump-leg-target-not-a-commit`, read from the tags' headers; `.5.5E-10` holds the first exponent's `E` (#1103)

Fix round 1 of #1131, after rv1131's review (READY, two NITs).

NIT 1. The no-`origin` words for a held tag the leg cannot peel keyed on
`object_kind(id) == "tag"` alone, so two rarer cases got "without the
commit it names" too, and it was untrue of both:
- (a) a tag of a tag whose inner tag the leg lacks, with the commit held
  or not. It is now told which tag it leads through that the leg lacks,
  and that what that tag names is not known there, under the same
  `bump-leg-commit-not-on-remote`.
- (b) a tag, or a tag of a tag, of a tree or a blob the leg lacks. It
  names no commit, so `peeled_target` now refuses it
  `bump-leg-target-not-a-commit`, in the words a tag of a held tree or
  blob already had (byte for byte, measured), with or without an
  `origin`, since no fetch makes a tree a commit.
`tag_chain_end` reads the chain from each held tag's `type` and `object`
header lines, where the peel cannot be read. The issue's own case, and
every other id, keeps its words: a probe of main, 9d14c29 and this head
over 15 legs and 10 ids found head-vs-new differences only in (a) and (b).

NIT 2. Mutant M5 (`[eE]` to `[e]` in YAML_NOT_TEXT_RE's first exponent
group) survived: no row held a capital-E exponent only that group takes.
`.5.5E-10` is added to YAML_FLOATS.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

Re-check of #1131 (#1103) for lane openRepoShape-5

READY at a4beec9e, fix round 1. It sits on ae830e2b, the merge of main 038dafba into the reviewed head 9d14c29a.

Both of rv1131's NITs are met, and I measured each:

  • (a) and (b) now print true sentences, by name.
  • The 8 new rows fail on 9d14c29a's bump-leg.py, and 9 rows fail on main's.
  • The three body mutants I re-ran are all killed.
  • My re-run of the three-way probe matches the body's counts exactly.

I found four NITs. None blocks. Two are worth a small commit: the new SonarCloud issue, and a mutant of mine that survives because no row holds a chain of three tags.

Findings

NIT 1: the round adds one open SonarCloud issue, python:S7494, at scripts/bump-leg.py:999.

  • The line: header = dict(line.split(" ", 1) for line in lines if " " in line), in the new tag_chain_end. Sonar says "Replace dict constructor call with a dictionary comprehension." It is MINOR, created 2026-10-22T13:31:04Z, the fix commit's time.
  • Measured:
    • The SonarQube API for PR 1131 lists exactly one OPEN issue, this one. The bot's comment (issuecomment-6342383079) says "1 New issue".
    • The quality gate is OK.
    • rv1131 found no open issues at 9d14c29a.
  • Fix: header = {key: value for key, sep, value in (line.partition(" ") for line in lines) if sep}. I checked it gives the same dict as the current line on a header with a continuation line and a line with no space. I did not run the rows on it.

NIT 2: no row holds a chain of three tags, so a walk cut to two headers survives.

  • Mutant NX1 (mine): while True: becomes for _ in range(2):, with return names, named after the loop, so tag_chain_end reads at most two headers.
    • I ran the rows of tests/test_bump_leg_reachability_words.py and tests/test_bump_leg_dry_run_truth.py on git 2.43: 67 passed, so the mutant survives.
    • The file was then restored, and its sha256 checked (d6822093b1aa4b0e..., dirty=0).
  • NX1 is not equivalent. I built a leg that holds three tags of a tree, r3 -> r2 -> r1 -> tree, without the tree, and asked peeled_target, then _unreachable_refusal, in fetch_leg's order (probe2.py):
    • The head refuses --to <r3> with bump-leg-target-not-a-commit: --to <r3> is an annotated tag of a tree in the leg (opensoft/openGlass), not a commit.
    • NX1, with no origin, says <r3> is an annotated tag the leg's own object store holds without a tag it leads through, <r1>, .... That is false: the leg holds r1.
    • NX1, with an origin, says opensoft/openGlass has no branch containing <r3>.
  • The code itself is right on chains of three tags, by the same probe:
    • With t3 and t2 held and t1 lacked, the head names t1, with the commit held or not.
    • With t3 and t1 held and t2 lacked, it names t2.
    • The tree chain above is refused by name.
    • Main and 9d14c29a gave the old words in all five cases.
  • Fix: add a fourth kind to TAGS_OF_NO_COMMIT, "tag of a tag of a tag of a tree", with all three tags held and the tree pushed nowhere. Under NX1 it would get bump-leg-commit-not-on-remote instead of bump-leg-target-not-a-commit.

NIT 3: tag_chain_end returns ("tag", <id>) for an <id> the leg holds as something other than a tag. Its docstring does not say so. Only a malformed tag gets there.

  • Reproduction: a tag object whose header says type tag but names a commit the leg holds, written with git hash-object -t tag -w --stdin. Git 2.43 accepts it, and git fsck says bad tag pointer.
  • Measured ($Q/v123/rc1131/forged/):
    • The peel fails: object ... is a tag, not a commit, then missing object referenced by.
    • tag_chain_end returns ('tag', <commit>), and object_kind of that commit is commit.
    • The no-origin refusal then says the leg holds the tag "without a tag it leads through, ". The leg holds that commit.
  • No regression: main says "is not in the leg's own object store", and 9d14c29a says "without the commit it names". Both are untrue here too.
  • Optional fix: the docstring could add "or a tag whose header misstates what it names". Or (a)'s branch could require object_kind(submodule, named) is None.

NIT 4 (for the coordinator): the PR's title on GitHub is still the first commit's.

Checked and found true

$Q is /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad. Every run went through run9.sh with EMAIL set.

Head and main

  • origin fix/1103-exponent-row = refs/pull/1131/head = a4beec9ef3da (ls-remote at 17:23Z). origin/main is 038dafba (17:23Z).
  • The tree of ae830e2b, 10c2372fe578, equals git merge-tree --write-tree 9d14c29a 038dafba. So the merge is clean and adds nothing of its own.
  • The fix commit changes only the three files.

rv1131's NIT 1: (a) and (b), by name

  • End to end, both runs (the new rows, through assert_one_refusal):
    • On 9d14c29a's file, both (a) rows got REFUSED bump-leg-commit-not-on-remote: <id> is an annotated tag the leg's own object store holds without the commit it names.
    • On main's file, (a) got ... is not in the leg's own object store.
    • On the same two files, the (b) rows with an origin got REFUSED bump-leg-commit-not-on-remote: testorg/Atlas-spec has no branch containing <tag>.
    • Every (b) row on either file still exited 2 in both runs. So no case that main applies is now refused: main refused these too, under the other code.
    • On the head, all 8 rows pass with the new words.
  • The three-way probe, re-run. I used the writer's probe_drive3.py and probe_one.py, copied unchanged (sha256 checked against the writer's), on git 2.43.
    • It gave 1578 answers from main and from 9d14c29a, and 1554 from the head.
    • Head vs 9d14c29a: 1532 are the same and 46 differ.
      • (a) accounts for 16: 4 legs, has_origin=False only, raw and flow.
      • (b) accounts for 30: 6 (leg, id) pairs, each with peeled_target and 4 flow verdicts.
    • Main vs head: 1492 are the same and 86 differ.
      • 46 are those above.
      • The other 40 are the issue's case (12) and 28 raw asks.
      • All 40 have has_origin=False, and in all 40 the head gives 9d14c29a's answer.
    • My classified file is the writer's, line for line, but for the tag ids.
  • (b)'s new refusal is byte for byte main's for the same id on the leg that holds everything (full), in all 6 pairs.
  • A refs/replace/ cycle (tag A replaced by a tag of A): git rev-parse -q --verify A^{} was still running at 10 s on git 2.43 and on git 2.47.1. So the peel never fails on a cycle, and the walk, which runs only after a failed peel, never meets one. The body measured 2.43 only.
  • No pinned text: git grep finds neither code, nor leads through, without the commit it names or is an annotated tag of a, in README.md, docs/, AGENTS.md or templates/.

Rows

  • On 9d14c29a's bump-leg.py: 8 failed, 59 passed. The 8 are exactly the new rows.
  • On main's bump-leg.py: 9 failed, 58 passed. The 9 are those 8 and test_a_tag_a_leg_with_no_origin_holds_alone_is_told_it_is_held.
  • Both ran the rows of the two files above on git 2.43. Each old file was swapped in alone, then restored by sha256.
  • .5.5E-10:
    • Only the first exponent group of YAML_NOT_TEXT_RE takes it (first True, second False).
    • PIN_ARG_RE refuses it and PLAIN_SCALAR_RE takes it, as the list's comment says.
  • The added lines: 218 in all, at most 78 characters wide. The only non-ASCII character is U+2014, in the tool's own words. No added assert joins conditions with and or or.

Mutants. Each was applied alone in my scratch worktree. The file was then restored, and checked by sha256 (bump-leg.py d6822093b1aa4b0e..., shape_materialize.py a37e3d83fbcda475..., dirty=0 every time).

Mutant Rows run Result Rows that failed
M5 (body): first exponent group [eE] to [e] the spelling file killed: 2 failed, 125 passed .5.5E-10, in both tests that take YAML_FLOATS
NM2 (body): the walk's tag = named replaced by a return the two files above killed: 2 failed, 65 passed the 2 tag of a tag of a tree rows of (b)
NM5 (body): (a) names the outer tag, {named} to {commit} the two files above killed: 2 failed, 65 passed the 2 rows of (a)
NX2 (mine): the walk steps on without asking whether the leg holds the next tag the two files above killed: 2 failed, 65 passed the 2 rows of (a)
NX1 (mine): the walk reads at most two headers the two files above survives: 67 passed none (NIT 2)

The three body mutants killed the same rows, in the same counts, as the body says.

Gates at a4beec9e, committed and clean (dirty=0 in each START line):

Run git Result
tests/test_bump_leg_reachability_words.py, tests/test_neutral_pin_entry_one_spelling.py, tests/test_path_guard_hygiene.py 2.43 219 passed
the two changed test files 2.47.1 (first on PATH; no other git on run9's PATH) 170 passed
  • CI on a4beec9e: tests (Linux, 1h7m53s), tests-git-bash, package and SonarCloud Code Analysis all succeeded.

Merges

The body as posted (17:25:48Z)

  • It is $Q/v121/f1131c/body.md, byte for byte, through the Lane line. After that come the attribution line and Sourcery's summary.
  • body-guard exits 0 on the source. The only closing keyword is Closes #1103, and closingIssuesReferences is [1103].
  • Claims I re-measured and found true:
    • the tree 10c2372fe578;
    • every probe count, and (b)'s byte equality;
    • the 8 and 9 failing rows;
    • the M5, NM2 and NM5 kills;
    • the 170 rows passing on both gits;
    • the three merge trees, and "the other 34 of the 37";
    • the hunk ranges: git diff -U0 puts them at main's lines 978-1040 and 1252-1286, inside the body's 976-1043 and 1250-1289.

Not verified

  • The 57-file shards (the writer's 1094 and 2325 passed) were not re-run. CI's full Linux tests job passed at this head.
  • Chains of three tags and the malformed tag were asked of peeled_target and _unreachable_refusal in fetch_leg's order, not through the command line.
  • NM1, NM3, NM4, NM6 and NM7 were not re-run. Their numbers are the writer's.

Scratch left

  • Worktree: /workspace/projects/openRepoShape-worktrees/rc1131-recheck, detached at a4beec9e, clean. Left in place.
  • Objects: tree objects from my merge-trees are in the shared object store, with no refs.
  • Notes, logs, probes, fixtures and tools: /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v123/rc1131/
  • Basetemps:
    • /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v110/bt/rc1131-g243
    • /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v110/bt/rc1131-g247
    • /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v110/bt/rc1131-mut
  • Running: nothing of mine.

Lane: openreposhape-5 (openRepoShape-5)

@brettheap brettheap changed the title bump-leg.py: a tag a leg with no origin holds without its commit is told it is held, peeled_target loses the else nothing reaches, and rows hold shape_materialize.py's YAML_NOT_TEXT_RE exponents of two digits (#1103) bump-leg.py: a tag a leg with no origin holds without its commit, or through a tag it lacks, is told so, a tag of a tree or a blob it lacks is refused by name, and rows hold YAML_NOT_TEXT_RE's exponents of two digits (#1103) Oct 22, 2026
…n and its docstring covers a malformed tag; test_bump_leg_reachability_words.py: three held tags to a tree the leg lacks are refused by name, which a two-header walk fails (#1103)

#1131's re-check (rc1131, issuecomment-6347256815: READY, 4 NITs) found three
things to mend in the fix round, and this commit mends them.

NIT 1, SonarCloud python:S7494 at bump-leg.py:999: the dict constructor over
a generator is a dict comprehension. It gives the same dict as the line it
replaces, on every list of up to three lines made of two letters, a space and
a tab, and on a real tag header with a continuation line.

NIT 2: no row held a chain of three tags, so a walk cut to two headers
(`while True:` read as `for _ in range(2):`) survived. `TAGS_OF_NO_COMMIT`
has a fourth kind, a tag of a tag of a tag of a tree, all three tags held and
the tree pushed nowhere, in both runs, with and without an `origin`.

NIT 3: the docstring of `tag_chain_end` says it returns `("tag", <id>)` also
for a tag whose header misstates what it names, which a malformed tag has.

Nothing the tool does changes.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

Re-check of #1131 (#1103) NIT round for lane openRepoShape-5

READY at 55c30f20f (parent a4beec9e). The commit changes scripts/bump-leg.py and tests/test_bump_leg_reachability_words.py and nothing else. rc1131's NITs 1 to 3 (issuecomment-6347256815) are met. NIT 4, the title, is met too: the live title is the 228-character one the writer proposed. I found two NITs of my own. Neither blocks.

Findings

NIT 1: the body's "(2486027 lists)" counts comparisons, not lists.

  • The body says tools/equiv2.py compares the old and new header dict "on every list of up to two lines of up to five characters, and every list of up to three lines of up to three characters ... (2486027 lists)". The script counts 7311 of those lists twice, because the lists of up to two lines of up to three characters sit in both passes.
  • I counted the union: 2478716 distinct lists. 2478716 plus 7311 is 2486027.
  • The claim "equal in all" is true (see "Checked and true"). Only the number is off.
  • Ask: say "2478716 distinct lists", or "2486027 comparisons".

NIT 2: the body's "Not yet measured: SonarCloud's own analysis" can now be replaced.

  • The SonarQube API for PR 1131 lists python:S7494 at scripts/bump-leg.py:999 (created 2026-10-22T13:31:04Z) as CLOSED. It lists 0 open issues, and the quality gate is OK.
  • The API gives no commit, so I cannot say which analysis I read.

Checked and true

NIT 1 (the comprehension)

  • git diff a4beec9e 55c30f20f -- scripts/bump-leg.py shows the dict constructor replaced by {key: value for key, sep, value in (line.partition(" ") for line in lines) if sep}, plus the docstring.
  • My own probe (tools/equiv_rc.py, not the writer's) found the old and new header dicts equal on:
    • 3820909 distinct lists (up to 3 lines of up to 3 characters, over a, b, space, tab and an accented letter);
    • the writer's stated domain, 2478716 distinct lists;
    • 1000000 random lists with longer strings and the separators splitlines() cuts on.
  • It also ran the real tag_chain_end of a4beec9e and of 55c30f20f on 26 real and forged tag objects in a scratch repo: 0 differ. They include a continuation line, a line with no space, a trailing-space type, duplicate keys, CRLF, upper-case ids, and tags of trees, blobs and missing objects at depth 1 to 3.

The commit message's overclaim

  • The commit says the dict is the same "on every list of up to three lines made of two letters, a space and a tab". The body names that sentence and gives the real coverage.
  • That coverage is right. equiv.py's three-line lists came from strings[:200], cut by islice at 2,000,000. 1864591 plus 2,000,000 is its 3864591.

NIT 3 (the docstring)

  • A forged tag whose header says type tag over a commit or a tree the leg holds makes tag_chain_end return ("tag", <that id>), in both versions. That is what the new docstring says.
  • Plain git hash-object -t tag -w (no --literally) accepts it. git fsck then reports bad tag pointer.

NIT 2 (the three-tag row)

  • The new kind is tag of a tag of a tag of a tree, run with and without an origin. It makes outer2 -> outer1 -> named -> tree, all three tags held.
  • The values are (what, depth), and git grep shows no other reader of TAGS_OF_NO_COMMIT.
  • Each run put one bump-leg.py under the head's two test files, tests/test_bump_leg_reachability_words.py and tests/test_bump_leg_dry_run_truth.py (69 rows), on git 2.43.0. The file was restored and sha-checked each time.
bump-leg.py Result Failing rows
main 961640c68 11 failed, 58 passed the 9 rows of fix round 1 and the 2 new rows
a4beec9e 69 passed none
NX1 (while True: as for _ in range(2):, return names, named after the loop) killed: 2 failed, 67 passed the 2 rows of the new kind
NX0 (range(1)) killed: 4 failed, 65 passed the 2 rows of tag of a tag of a tree and the 2 of the new kind
NX3 (range(3)) survives: 69 passed none, as the body says
  • On main the new rows fail on the refusal. With an origin the run prints REFUSED bump-leg-commit-not-on-remote: testorg/Atlas-spec has no branch containing <tag>. Without one it prints the git fetch origin ... has no remote named origin note. Neither contains REFUSED bump-leg-target-not-a-commit. This is as the body says.
  • NX1 fails the same two rows with the same words.

Runs (run9.sh, EMAIL set, dirty=0 in each START line, committed head)

git Files Result
2.43.0 tests/test_bump_leg_reachability_words.py, tests/test_neutral_pin_entry_one_spelling.py, tests/test_bump_leg_dry_run_truth.py, tests/test_path_guard_hygiene.py 245 passed
2.47.1 (printed in the log) the same four 245 passed
  • The hygiene file has 49 rows. So the first three files give 196 on 2.47.1, which is the body's figure. I derived that by subtraction and did not run the three alone.

Added lines and merges

Body and commit

  • The live body equals the writer's body-post.md.
  • body-guard exits 0 through the Lane line.
  • closingIssuesReferences is [1103].
  • The title is 228 characters and the commit subject 248, both ASCII.

Not measured

  • Mutants C1, C2, C3, C6, NM2 and NX2. C3 was running when the LAST STEP stopped me, and C6 never started.
  • The body's 402 passed (7 files) on git 2.43. I ran 4 of the 7.
  • The 57-file shards.
  • CI on the head.
  • Which commit SonarCloud analysed.

Lane: openreposhape-5 (openRepoShape-5)

@brettheap brettheap added this to the v1.2.1 milestone Oct 23, 2026
brettheap added a commit that referenced this pull request Oct 23, 2026
… leaves out a pattern like `refs/heads*` that puts a branch under another name, its clauses move into `_left_out`, and the docstrings name the clashes left and the way back (#1100) (#1157)

#1100 holds five follow-ups from RV1087's review of #1087 (issuecomment-6314349196). This PR does all five:

1. Take the clause-4 fix for a pattern such as `refs/heads*`, and name the clash that is left, `+refs/pull/7/head:refs/remotes/origin/main`, in the three places that list clashes.
2. Add the `WHICH_CLAUSE` row that kills R16.
3. In `bump-leg.py`, name the unforced exception and the way back, `git fetch --force origin`.
4. Write down the accepted cost where the cost is described.
5. Move `origin_fetch_refspecs`' four clauses into a helper, `_left_out`.

Fix round 1 (`baa19972a`) answers the independent review rv1157/rv1157b (issuecomment-6347113460: NOT READY, 2 SHOULD, 4 NIT, no BLOCKER). It does all six. See "Fix round 1" below.

The NIT round (`368b87e9b`) answers rc1157's re-check of fix round 1 (issuecomment-6352215076: READY, 3 NITs, all in this body). It adds one test row, which kills F2, the survivor of fix round 1, and corrects three statements in this body: the reason F2 was left, the longest line's length and the #1131 merge-tree line. The scripts are unchanged. See "NIT round, after the re-check of fix round 1" below.

Measured by a probe on git 2.43 and git 2.47.1, with local bare remotes only. The two gits gave identical tables. For each case the probe ran:
- the repository's own `git fetch origin`;
- the tools' fetch, `git fetch -q --prune --no-prune-tags --refmap= <origin|path> <refspecs>`, with the refspecs from main's `origin_fetch_refspecs`, once through `origin` and once through a path;
- the repository's own fetch again ("own-after").

Where own-after failed, it then ran `git fetch --force origin`, a plain fetch, and the tools' fetch followed by the repository's own fetch ("again"). Branches are `main`, `x` and `release/x` at three unrelated commits unless the case says otherwise.

```
case                                                    own  main o/p  own-after  force, plain, again
C25  +refs/heads*:refs/remotes/origin/h*, branch h/x    0    128/128   0/0        -
C18b +refs/pull/7/head:refs/remotes/origin/main alone   0    128/128   0/0        -
C02  release/* onto origin/*, unforced                  0    0/0       1/1        0 (ref back), 0, 1
C02c same, x descends from release/x                    0    0/0       1/1        0 (ref back), 0, 1
C05u release/x onto origin/x, unforced                  0    0/0       1/1        0 (ref back), 0, 1
C20u tags onto origin/*, unforced, branch and tag x     0    0/0       1/1        0 (ref back), 0, 1
C32  heads/* onto origin/*-a, branches q and q-a        0    0/0       0/0        -
C33  feat-* onto origin/feat-*-old, feat-1, feat-1-old  0    0/0       0/0        -
```

These match the issue's numbers. C32 and C33 exit 0 because #1087 leaves them out. The own-after failure is `! [rejected] release/x -> origin/x (non-fast-forward)`. C02b, where `release/x` descends from `x`, gives 0. So the condition is that the kept commit does not descend from the branch's, not that the two commits are unrelated. The old paragraph's words "not in line" were wrong for C02c, and the new text corrects them.

Main has since moved to `961640c6` (#1155, which touched only `docs/release-notes/v1.2.0.md`). The four files this PR changes are byte-identical there, so nothing above changes.

**`scripts/shape_materialize.py`**

- **Clause 4** (`gives_a_branch_another_name`) now reads a source that is not under `refs/heads/` as naming branches when it is a pattern whose part before the `*` is a start of `refs/heads/`. This is how `can_name_a_tag` reads `refs/*` for tags:

  ```
  fixed, star, _ = source.partition("*")
  if not source.startswith(HEADS_PREFIX):
      return bool(star) and HEADS_PREFIX.startswith(fixed)
  ```

  Such a pattern's `*` also takes what the pattern leaves of `refs/heads/`. So no destination under `refs/remotes/origin/` gives a branch its own name, and every such refspec is left out. The only pattern that keeps the names, `refs/heads*:refs/remotes/origin*`, writes outside `origin/`, and clause 1 leaves it out first. The shorter starts (`refs/`, `*`) can name a tag, and clause 2 leaves them out first.
- **The fix was taken only after measuring it.** A pure-function differential of main against the branch ran over 39,385 configs: 43 sources x 31 destinations, forced and unforced, negatives, pairs, doubles, and each one beside the first refspec.
  - The branch carries nothing that main does not.
  - It newly leaves out 207 distinct refspecs. Every one has a pattern source whose part before the `*` starts `refs/heads/` but neither `refs/heads/` nor `refs/tags/` (`refs/h*` to `refs/heads*`).
  - The helper alone (main plus `_left_out`) gives main's output on all 39,385 configs.
  - The fix alone and the fix plus the helper give the same output as each other on all 39,385.
- **The probe of all 53 cases**, on both gits, changed no outcome outside that class: exits, own-after, force, plain and again are identical for the 46 cases outside the class, C03u, C17u and C32o included. The seven cases in the class changed:

  ```
  case                                                      own  main o/p  branch o/p  own-after (branch)
  C25   refs/heads* onto origin/h*, branch h/x              0    128/128   0/0         0/0
  C25u  same, unforced                                      0    128/128   0/0         1/1; --force 0, plain 0
  C25n  refs/heads* onto origin/h*, no clash                0    0/0       0/0         0/0 (pruned, written back)
  C25g  default glob + refs/heads* onto origin/h*           0    0/0       0/0         0/0 (pruned, written back)
  C25b  default glob + refs/he* onto origin/he*, heads/x    128  128/128   0/0         128/128
  C25c  refs/he* onto origin/he*, branch heads/x            0    128/128   0/0         0/0
  C25h  glob + refs/h* onto origin/hh/*, ref refs/hidden/1  0    0/0       0/0         0/0 (pruned, written back)
  ```

  Wherever a tools' fetch exited 0, every remote branch's `refs/remotes/origin/<name>` held that branch's commit (all cases, both rules, both ways, both gits). A fetch that exited 128 changed no ref.
- **`_left_out(source, destination)`** holds the four clauses, in their order and with their comments. `origin_fetch_refspecs` now asks it once. The SonarQube analyzer (`python:S3776`, `analyze_code_snippet`) raises nothing on an excerpt of the four functions as committed, with their docstrings dropped. With four more `if ...: continue` in the loop it reports "from 17 to the 15 allowed", so `origin_fetch_refspecs` measures 9 (it was 15). By hand, `_left_out` is 3 and `gives_a_branch_another_name` is 2.
- **The docstrings:**
  - `gives_a_branch_another_name` no longer ends "A source not under `refs/heads/` names no branch". It names the pattern and points to WHAT CAN STILL CLASH.
  - The A BRANCH UNDER ANOTHER NAME paragraph names the pattern.
  - A new THE COST, ACCEPTED paragraph says that, after either tool runs, an unforced refspec's own fetch exits 1 on every run until `git fetch --force origin`. Where the refspec was carried, the tools' fetch exited 128 in those same states and left the ref alone. No commit is lost, and main already behaves this way for unforced tags onto the glob.
  - WHAT CAN STILL CLASH now names any carried refspec whose source names no branch, onto a name a branch has: the pull requests in `origin/pr/` and C18b's single name.
  - Fix round 1 changes three of these paragraphs and adds one (below).

**`scripts/bump-leg.py`**, docstrings only. An AST comparison with the docstrings blanked shows the code is identical to main's, at both heads.
- The #1066 paragraph of `_leg_fetch_refspecs` names the `refs/heads*` pattern. It adds NOT WHERE THE LEG'S REFSPEC WAS WRITTEN WITHOUT ITS `+`, with the way back, `git fetch --force origin`.
- "Two ways remain" now names the second way as a source that names no branch, onto a name a branch has, in a folder or onto a single name (C18b).

**`tests/test_family_bump_fetch_refspecs.py`**
- New `WHICH_CLAUSE` rows (both tools' tests ask them):
  - `another-name-suffix`, `+refs/heads/*:refs/remotes/origin/*-old`, left out (item 2);
  - `another-name-heads-pattern`, `+refs/heads*:refs/remotes/origin/h*`, left out;
  - `another-name-heads-pattern-shorter`, `refs/he*:refs/remotes/origin/he*`, left out;
  - `carried-not-refs-heads`, `+refs/headsx/*:refs/remotes/origin/headsx/*`, carried;
  - fix round 1: `another-name-heads-pattern-refs-h` and `another-name-heads-pattern-suffix` (below).
- New `BRANCH_UNDER_ANOTHER_NAME` row `heads-pattern-into-a-folder`, `+refs/heads*:refs/remotes/origin/h*`, with `x-w1066` and `h/x-w1066`. It runs end to end through `family.py bump` and through `bump-leg.py` via `origin` and via `--local-remote-dir`, with the clash premise first.
- Fix round 1: `NO_BRANCH_A_PATTERN_TAKES` and its end-to-end rows (below).
- NIT round: `carried-refs-h-not-a-pattern`, `+refs/h:refs/remotes/origin/hy`, carried (below).

**`tests/test_bump_leg_fetch_refspecs.py`**: one docstring sentence, and in fix round 1 the leg's `NO_BRANCH_A_PATTERN_TAKES` rows.

`scripts/family.py` is unchanged. Its `_origin_fetch_refspecs` paragraph "A mount whose refspec git cannot satisfy" gives examples and claims no complete list, and it stays true.

One new commit, `baa19972a`, on `a35a97ea5`. No merge of main was needed: main's one new commit touches none of these files. The scripts' code is unchanged: `astsame.py` (AST with docstrings blanked) gives SAME CODE for `shape_materialize.py` against `a35a97ea5` and for `bump-leg.py` against main.

**SHOULD 1 (finding 1): two mutants of the fix survived the rows.** Two `WHICH_CLAUSE` rows are added. Both tools' tests ask them (`test_family_bump_fetch_refspecs.py` holds the table, and `test_bump_leg_fetch_refspecs.py` imports it):
- `another-name-heads-pattern-refs-h`, `+refs/h*:refs/remotes/origin/hh/*`, left out. This is the class's lower edge, the shortest start of `refs/heads/` that no earlier clause meets.
- `another-name-heads-pattern-suffix`, `+refs/heads*x:refs/remotes/origin/h*x`, left out. Git accepts it and maps branch `x` onto `origin/h/x` and `ax` onto `origin/h/ax` (probe section B, both gits).

On the pure function (`tools/rows.py`, `logs/rows.log`):
```
row                                     main     head      B1        B9        F2
+refs/h*:refs/remotes/origin/hh/*       carried  left out  carried   left out  left out
+refs/heads*x:refs/remotes/origin/h*x   carried  left out  left out  carried   left out
```
So each new row fails on main. B1 is killed by `-refs-h` and B9 by `-suffix`. F2 was the one survivor of fix round 1. The NIT round's row kills it (see "NIT round" and Mutants).

**SHOULD 2 (finding 2): leaving the class out changes the answer from pin to refuse** for a commit that only a non-branch ref reaches, where the pattern takes that ref too. The three places that said nothing is lost now say so:
- `shape_materialize.py`, `origin_fetch_refspecs`: LEFT OUT BY EITHER OF THE LAST TWO, NO BRANCH IS LOST TO THE ANSWER, followed by a new paragraph, BUT A COMMIT ONLY A REF THAT IS NO BRANCH REACHES IS REFUSED. It covers the pull requests onto the branches' names (left out since #946, refused on main too) and, since #1100, a ref of another namespace that a pattern from a start of `refs/heads/` takes. It says that every run of either tool refuses such a commit alike. It also says that a refspec whose source is that namespace itself is carried, and the commit is pinned through it. This is finding 2(b): one remote ref, two answers by refspec.
- `shape_materialize.py`, `_left_out`'s comment: a ref that is no branch, which a pattern takes too, no longer answers for one, so a commit only it reaches is refused, as one only a tag reaches is.
- `bump-leg.py`, the #1066 paragraph: "so no branch is lost to the answer", and a new paragraph, A COMMIT ONLY A REF OF ANOTHER NAMESPACE THE PATTERN TAKES REACHES IS REFUSED NOW (#1100), with the review's `heads-x-in` case and its `heads-x-out` control.

New end-to-end rows pin the refusal, `NO_BRANCH_A_PATTERN_TAKES` (`+refs/heads*:refs/remotes/origin/h*` with `refs/heads-archive/old-w1100`, and `+refs/h*:refs/remotes/origin/hh/*` with `refs/hidden/old-w1100`):
- `test_a_ref_a_heads_pattern_takes_answers_for_no_branch` runs `family.py bump --dry-run` and `bump`. Both refuse `member-to-local-only` in the plain words, with nothing in the holder moved and no ref of the mount moved.
- `test_a_ref_a_heads_pattern_takes_answers_for_no_branch_of_the_leg` runs `bump-leg.py` via `origin` and via `--local-remote-dir`. Both runs refuse `bump-leg-commit-local-only` in the plain words, with nothing in the root moved and no ref of the leg moved.
- Each first asserts its premises: the ref reaches the commit, no branch of the remote does, and no remote-tracking ref does. All 6 rows fail on main, where both runs pin (Rows failing on main).

The docstrings' claims, measured by `tools/claims.py` (the tools' own code from an exported tree: `fetch_leg` end to end, then `family.py`'s reader and `_on_a_remote_branch`). It ran on main `961640c6` and on `baa19972a`, on git 2.43 and 2.47.1. The two gits gave identical output on each tree (`logs/claims-{main,head}-{243,247}.log`):
```
case              refspec beside the glob                                ref holding C           main    head
C25h-in           +refs/h*:refs/remotes/origin/hh/*                      refs/hidden/1           PIN     REFUSE
C25h-unforced     refs/h*:refs/remotes/origin/hh/*                       refs/hidden/1           PIN     REFUSE
heads-x-in        +refs/heads*:refs/remotes/origin/h*                    refs/heads-archive/old  PIN     REFUSE
heads-x-unforced  refs/heads*:refs/remotes/origin/h*                     refs/heads-archive/old  PIN     REFUSE
hidden-out        +refs/hidden/*:refs/remotes/origin/hidden/*            refs/hidden/1           PIN     PIN
heads-x-out       +refs/heads-archive/*:refs/remotes/origin/h-archive/*  refs/heads-archive/old  PIN     PIN
pulls-out         +refs/pull/*/head:refs/remotes/origin/pr/*             refs/pull/7/head        PIN     PIN
pulls-onto-glob   +refs/pull/*/head:refs/remotes/origin/*                refs/pull/7/head        REFUSE  REFUSE
```
- `family.py`'s answer equals `bump-leg.py`'s in every row, and so do the two readers' lists.
- On the head, the tools' fetch exits 0 and prunes the class ref (`origin/hh/idden/1`, `origin/h-archive/old`). The repository's next own `git fetch origin` exits 0 and writes it back.

**NIT 1 (finding 3), `bump-leg.py`'s tag paragraph.** It now says that such a ref is pruned or holds the branch of that name, and that the leg's own next fetch writes it back. It adds NOT WHERE THE LEG WROTE THAT REFSPEC WITHOUT ITS `+` and the tag's commit does not descend from the branch's: then the leg's own fetch exits 1 on that run and every one after it, until `git fetch --force origin`. Measured (probe section C, both gits, main and head alike):
```
case       refspec                              own  tool  own-after  --force (ref back)  plain
C20        +refs/tags/*:refs/remotes/origin/*   0    0     0,0        -                   -     (origin/x back at the tag)
C20u       refs/tags/*:refs/remotes/origin/*    0    0     1,1        0 (back)            0     ! [rejected] x -> origin/x (non-fast-forward)
C20u-desc  same, the tag's commit a child of x  0    0     0,0        -                   -
```

**NIT 2 (finding 4).** The 109-character line is reflowed. The longest line this PR adds, over `961640c6..368b87e` in the four files, is 78 characters (`test_bump_leg_fetch_refspecs.py`'s `assert contains(bare_of(remotes), made, ref), f"premise: {ref} reaches it"`). In the scripts the longest added lines are 75 (`bump-leg.py`) and 76 (`shape_materialize.py`). `bump-leg.py`'s longest line, 81, is main's. This body said 79 until the re-check measured 78.

**NIT 3 (finding 5), the overclaim in `gives_a_branch_another_name`.** It no longer says each such refspec puts a branch under another name. Where the part before the `*` is all of `refs/heads` and the destination's `*` comes right after a `/`, git refuses the name it gives each branch, and the fetch writes no branch. The docstring now says this, and says that the pattern still takes the refs of another namespace, pointing to the new paragraph. Probe section B, both gits: `+refs/heads*:refs/remotes/origin/h/*` with branches `main`, `x` and `ax` exits 0, says `error: * Ignoring funny ref 'refs/remotes/origin/h//x' locally` (and the same for `h//main` and `h//ax`), and writes only `origin/h/foo/x` for `refs/headsfoo/x`.

**NIT 4 (finding 6), the stale merge-tree list.** Merge-trees were re-run at `baa19972a` against main and every open PR touching these four files (Merge-trees).

One new commit, `368b87e9b`, on `baa19972a`, in `tests/test_family_bump_fetch_refspecs.py` only: one `WHICH_CLAUSE` row, a sentence in the table's comment and one in its test's docstring. The scripts are byte for byte `baa19972a`'s (`git diff --name-only baa1997 368b87e` names that one file). The row is `pytest.param("+refs/h:refs/remotes/origin/hy", True, id="carried-refs-h-not-a-pattern")`. `WHICH_CLAUSE` is one table, which `tests/test_bump_leg_fetch_refspecs.py` imports and asks of `_leg_fetch_refspecs`, so the one row is two test ids, one in each file, and both tools' tests ask it.

**NIT 1 (finding 1): the reason given for leaving F2 was wrong, and the row it said could not be added can be.** The first body said F2 differs from this head only on a source that is no pattern and is a start of `refs/heads/` (`refs/`, `refs/h`, ... `refs/heads`), that no such full name can be a ref beside the remote's branches, and that a row for it would pin an arbitrary answer. Measured by `tools/claims.py` (the tools' own code from an exported tree: `fetch_leg` end to end, then `family.py`'s reader and `_on_a_remote_branch`), on main `961640c6`, on this head and on this head with F2 applied, on git 2.43 and 2.47.1 (`logs/claims-{main,head,f2}-{243,247}.log`; the two gits gave identical output on each tree):

```
case                   refspec beside the glob                     ref holding C   main    head    F2
refs-h-no-pattern      +refs/h:refs/remotes/origin/hy              refs/h          PIN     PIN     REFUSE
refs-head-no-pattern   +refs/head:refs/remotes/origin/hz           refs/head       PIN     PIN     REFUSE
refs-heads-no-pattern  +refs/heads:refs/remotes/origin/hx          refs/heads      the remote cannot hold refs/heads and refs/heads/main
hidden-out             +refs/hidden/*:refs/remotes/origin/hidden/* refs/hidden/1   PIN     PIN     PIN
C25h-in                +refs/h*:refs/remotes/origin/hh/*           refs/hidden/1   PIN     REFUSE  REFUSE
```

- Only `refs/heads` itself cannot be such a ref: `git update-ref` refuses it with "cannot process 'refs/heads/main' and 'refs/heads' at the same time" (exit 128, no ref made). `refs/h` and `refs/head` are made beside `refs/heads/main` with exit 0.
- With `+refs/h:refs/remotes/origin/hy` and a commit C that only `refs/h` reaches, main's code and this head's carry the refspec and PIN C. Under F2 both tools leave it out and REFUSE C (`bump-leg-commit-local-only`, `family_answer=False`): the tools' fetch prunes `origin/hy`, which the repository's own fetch had written, and the next own fetch writes it back.
- So this head's answer is not arbitrary. It is the rule ANY OTHER SOURCE NAMES NO BRANCH, the same as `hidden-out`'s: a source that is no pattern takes the ref of its name. Where the remote has no ref of that name, git completes the name instead: with a branch named `refs/h` (the ref `refs/heads/refs/h`) beside `hy`, the tools' fetch line exits 128 with `Cannot fetch both refs/heads/hy and refs/heads/refs/h to refs/remotes/origin/hy`, on git 2.43.0 and 2.47.1, on main as here. That is the WHAT CAN STILL CLASH shape (lane 5's re-check, issuecomment-6371455922, NIT 1).
- On the pure function (`tools/rows.py`, `logs/rows.log`, `origin_fetch_refspecs` alone):

```
row                                       main      head      B1        B9        F2
+refs/h:refs/remotes/origin/hy            carried   carried   carried   carried   left out
+refs/head:refs/remotes/origin/hz         carried   carried   carried   carried   left out
+refs/h*:refs/remotes/origin/hh/*         carried   left out  carried   left out  left out
+refs/heads*x:refs/remotes/origin/h*x     carried   left out  left out  carried   left out
```

- **The new row passes on main by design,** as `another-name-suffix` does, because main's clause 4 (`gives_a_branch_another_name`) answers `False` for every source not under `refs/heads/` (`961640c6`, `scripts/shape_materialize.py` line 1704), and `refs/h` meets no earlier clause. It passes on this head. It kills F2: with F2 applied to `scripts/shape_materialize.py`, `-k carried-refs-h-not-a-pattern` over both test files gives 2 failed (`test_which_clause_leaves_each_refspec_out[carried-refs-h-not-a-pattern]` and `test_which_clause_leaves_each_refspec_out_of_the_legs_fetch[carried-refs-h-not-a-pattern]`), and over the 90 rows of Mutants, 2 failed and 88 passed. The runs are in `logs/mut-summary.log`: the row alone, 2 passed on main's code (`logs/mut-ROW-main-code.log`), 2 passed on this head (`logs/mut-ROW-head.log`), 2 failed under F2 (`logs/mut-ROW-F2.log`).
- The Mutants section below replaces the F2 sentence with this result.

**NIT 2 (finding 2): the line length.** Fix round 1's NIT 2 sentence now says 78, and names the scripts' 75 and 76 (above). The new commit adds lines of at most 76 characters, all ASCII, and no assert that joins conditions with `and` or `or`.

**NIT 3 (finding 3): the #1131 merge-tree line.** #1131's head is `55c30f20` now (its NIT round), not `a4beec9e`. `git merge-tree --write-tree 55c30f2 368b87e` exits 0 with tree `e036c02d6a12`; at `baa19972a` it gave `bd6ebc5cd3af`, as the re-check measured. See Merge-trees.

Main's production code (`961640c6`: `scripts/shape_materialize.py` and `scripts/bump-leg.py`) was put in the worktree under this head's two changed test files. Then the committed files were put back, with sha256 and dirty=0 checked (`logs/mut-MAIN-code-head-tests.log`). The result is **17 failed, 106 passed** at `baa19972a`, and **17 failed, 108 passed** at `368b87e9b` (`logs/mut-MAIN-code-head-tests.log`, 125 rows): the NIT round's 2 rows pass on main, by design. The failures are exactly the rows the fix decides:

- `WHICH_CLAUSE` `[another-name-heads-pattern]`, `[-shorter]`, `[-refs-h]` and `[-suffix]`, in both files: 8. Each is carried on main ("Left contains one more item").
- `test_a_branch_put_on_another_branchs_name_fails_no_fetch[heads-pattern-into-a-folder]` (member), and `..._of_the_leg[origin-heads-pattern-into-a-folder]` and `[local-remote-dir-heads-pattern-into-a-folder]` (leg): 3. The fetch exits 128 on main, so the runs give the failed-fetch note.
- Fix round 1's `NO_BRANCH_A_PATTERN_TAKES` rows: `test_a_ref_a_heads_pattern_takes_answers_for_no_branch[heads-pattern]` and `[refs-h-pattern]` (member), and `..._of_the_leg[origin-heads-pattern]`, `[origin-refs-h-pattern]`, `[local-remote-dir-heads-pattern]` and `[local-remote-dir-refs-h-pattern]` (leg): 6. On main the pattern is carried, so the real run pins the commit instead of refusing it.

The first round's 7 are among these. Fix round 1 adds 10: `-refs-h` and `-suffix` in both files, and the 6 end-to-end rows. `another-name-suffix` and `carried-not-refs-heads` pass on main by design. They kill mutants (below).

NIT round, on `368b87e9b` with git 2.43, through `tools/mut.sh` (`logs/mut-summary.log`). The scripts are `baa19972a`'s, so these are fix round 1's mutants over the NIT round's tests. Each mutant was applied alone to the committed `scripts/shape_materialize.py`. The runs covered both changed test files with `-k "which_clause or another_branchs_name or heads_pattern_takes"` (90 rows, 35 deselected). After each run the committed files were put back, and their sha256 and dirty=0 were checked: 9 of 9 restores in the script matched (`shape_materialize.py` `16c6ec378c36...`, `bump-leg.py` `8f4af9c5e78c...`).

```
mutant                                                        failed  result
main's code (961640c) under these two files, all 125 rows     17     the rows above (108 passed)
F2 the fix's `bool(star) and` dropped                           2     killed (carried-refs-h-not-a-pattern x2)
B1 the class drawn from `refs/he` up (`refs/h*` carried)        5     killed (-refs-h x2, refs-h-pattern x3)
B9 the fix only where the `*` ends the source                   2     killed (-suffix x2)
B4 the fix undone (`return False` in its place)                17     killed (every row the fix decides)
```

F2 is killed now. At `baa19972a`, with 88 rows, it survived (88 passed); B1 (5 failed), B9 (2) and B4 (17) gave the same counts as here. The earlier sentence said F2 differs from this head only on a source that is no pattern and is a start of `refs/heads/`, and that no such full name can be a ref beside the branches. The first half holds: over the 39,385-config corpus F2 differs from this head only there (`refs/`, `refs/h`, ... `refs/heads`). The second half held for `refs/heads` alone. `refs/h` and `refs/head` are refs a remote can hold beside its branches, and the head carries `+refs/h:refs/remotes/origin/hy` and pins a commit only that ref reaches, where F2 refuses it (NIT round, NIT 1). The row `carried-refs-h-not-a-pattern` holds that answer.

At `a35a97ea5`, whose code this head keeps, the first round measured these over `WHICH_CLAUSE` in both files (66 rows then):

```
mutant                                                failed  result
R16 clause 4 compares only the part before the `*`      2     killed (another-name-suffix x2); on main's own rows it survives (102 passed)
R17 clause 4 compares only the part after the `*`       8     killed (the four another-name rows x2)
F3  only `refs/heads*` itself, not shorter starts       2     killed (another-name-heads-pattern-shorter x2)
F4  any pattern starting `refs/h` (too broad)           2     killed (carried-not-refs-heads x2)
F5  every pattern not under refs/heads/ left out        6     killed (carried pulls, not-refs-tags, not-refs-heads x2)
H1  _left_out's OUTSIDE clause removed                  2     killed
H2  _left_out's tag clause removed                     14     killed
H3  _left_out's branches'-glob clause removed           2     killed (branches-glob-pull-requests x2)
H4  _left_out's last clause removed                    14     killed
H5  `if _left_out(...)` negated                        60     killed
```

Every run went through `run9.sh` on the commit `368b87e9b` (dirty=0 in each START line): gh stub first on PATH, a bogus token, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`, and `EMAIL` set.

- git 2.43, the three files that name the functions (`test_family_bump_fetch_refspecs.py`, `test_bump_leg_fetch_refspecs.py`, `test_bump_leg_reachability_words.py`): **159 passed** (`logs/g243.log`; 157 at `baa19972a`, and the NIT round's 2 rows).
- git 2.47.1 (a git 2.47.1 build first on PATH; `git --version` printed `git version 2.47.1` in the log): the same three files: **159 passed** (`logs/g247.log`).
- git 2.43, `test_path_guard_hygiene.py`, `test_pinned_copy_text.py` and the trio (`test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`): **306 passed** (`logs/hyg243.log`, the same count as at `baa19972a`).

`git merge-tree --write-tree <target> 368b87e`, run at 2026-10-23T12:06:38Z against main and the head of every open PR that touches one of the four files (55 open PRs listed at 12:06:22Z; `logs/merge-trees.log`). Each exits 0 with no conflict:
- `origin/main` `961640c6`: tree `19c8442d96b0`;
- #1104 `de89aa81` (`shape_materialize.py`): tree `b76c36be588a`;
- #1105 `b28f40de` (`shape_materialize.py`): tree `5f2a8b3706e4`;
- #1118 `2307036a` (`shape_materialize.py`): tree `0c21c6b39607`;
- #1131 `55c30f20` (`bump-leg.py`; its head moved from `a4beec9e` in its NIT round): tree `e036c02d6a12`;
- #1160 `0068f5af` (`shape_materialize.py`): tree `bce90eb65c6c`.

The head's own tree is `351a9b73cda8`. Main is still `961640c6`: its one commit past `038dafba`, #1155, touches only `docs/release-notes/v1.2.0.md`.

- C18b, a source that names no branch onto one branch's name, still clashes: own 0, tools 128. Which names the remote's branches have is not known from the refspecs. Leaving out every such refspec would also leave out the pull requests, which #946 carries on purpose. The docstrings now say so. The coordinator adds it and C25 to #1027's list.
- Leaving out the `refs/h*` class prunes what such a refspec kept until the repository's next own fetch, as for #1066's cases (C25n, C25g, C25h). An unforced one held under a branch's name costs the non-fast-forward of THE COST, ACCEPTED (C25u).
- Leaving it out also changes the answer for a commit that only a non-branch ref the pattern takes reaches, from pin to refuse (fix round 1, SHOULD 2). This is the rule #613 and #1037 keep for a commit only a tag reaches: a bump pins only what a branch reaches. Every run gives the same answer, so a dry run and the real run after it never differ. Pinning such a commit instead would be a design question, not part of this fix.

Review: lane 5's independent review (issuecomment-6347113460) and its re-check, READY at `baa19972` (issuecomment-6352215076); then rc1157b, lane 5's independent sonnet re-checker, which wrote none of #1157: READY at `368b87e9`, the head that lands (issuecomment-6371455922), with no MUST or SHOULD; its wording NIT is corrected in the body. CI (Brett Heap's ruling 27): H0 is the head 368b87e itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 038dafb; and the local gate passed on its predicted squash tree 44b3b5c over main b055bb7: the bump_leg_fetch_refspecs, family_bump_fetch_refspecs, repo_hygiene, agents_md_codes, cli_reference sets, 296 passed. Landed on Brett Heap's word of 2026-10-23T13:10:26Z, relayed to every lane by lane 3 (#473 issuecomment-6365697123): "merge v1.2.1 PRs once reviewed and green".

Closes #1100

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Nov 8, 2026

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review November 8, 2026 15:10
@sourcery-ai

sourcery-ai Bot commented Nov 8, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 6 days and 6 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 53bd317 into main Nov 8, 2026
5 checks passed
@brettheap
brettheap deleted the fix/1103-exponent-row branch November 8, 2026 15:12
brettheap added a commit that referenced this pull request Nov 8, 2026
…ject.py`'s argv `--id` the reason that holds (`match`, then validate-manifest.py refuses a line feed), a row proves it, and the guard says a new reader needs a row, `fullmatch` too (#1243)

`tests/test_value_pattern_line_break.py` says only what is true of the readers of `PROJECT_ID_RE`: no row of `ID_READERS` gives a reason that does not hold for the tool it names, and the guard `test_every_reader_of_the_id_pattern_is_in_the_table` tells the author of a new reader, in its docstrings and in its failure message, that the one way to pass is a row in `ID_READERS`, whichever method the reader calls.

This is #1229, the follow-up of #1166 (#1139): rc1166b's NIT 1, NIT 2 and NIT 4 (#1166 issuecomment-6371408915). Wording, one assert message and one row, in the test file only. No production file changes.

**Restacked onto main after #1166 landed** as `105b5da` (2026-11-08T14:49Z). This branch was built on #1166's head `5102b429` (branch `fix/1139-anchored-patterns`), with one commit, `f92338ef6`. The restack merges, never rebases:

1. #1166's final head, `5c8afefd` (its update-branch refresh).
2. Its squash `105b5da` on main, keeping THIS branch's side (`ours`) for `tests/test_value_pattern_line_break.py`. That file is the one add/add conflict, and the squash's side would drop every #1229 edit without a marker.
3. Main, which had not moved past the squash, so this merge was a no-op.

- **The head:** `b80214b7f`. Its diff against main is the one file, 56 lines added and 16 removed.
- **Checked by lane 5's `restack3.sh`:** its changed lines equal `f92338ef6`'s against that commit's base, and `git merge-tree --write-tree` with main is clean.

1. **A false reason (rc1166b NIT 2).** The module docstring's "LEFT AS THEY ARE" bullet and the `ID_READERS` rows said every argv `--id` of `scaffold-project.py`, `family.py init` and `adopt-project.py` "holds the id to `checked_value` after the pattern". That holds for the first two, and `tests/test_derived_id_refusals.py`'s `TOOLS` pins it for those two only. `adopt-project.py` never calls `checked_value` on `--id`: `_refuse_bad_id` asks `PROJECT_ID_RE.match(project_id)` of the id `plan` takes, passed or derived, and `args.id` reaches no `checked_value` call (`git grep -n 'args\.id\b' -- adopt-project.py`; its calls are for `--project`, the tracking branch, `--pin-owner`, the leg paths and `--org`). A line feed is still refused, by another door, the project's own `validate-manifest.py`.
2. **"`fullmatch` is enough" (rc1166b NIT 1).** Both docstrings said a new reader "asks with `fullmatch`, or it is listed in `ID_READERS`". The guard is `assert id_pattern_calls() == expected`, a dict equality over every (module, method), exact both ways, so a new `fullmatch` reader fails it too, and so does a listed reader taken away. The assert had no message, so the author of a sibling PR saw only a dict diff.
3. **The guard reads spellings (rc1166b NIT 4, optional).** `_ID = PROJECT_ID_RE` then `_ID.match(value)` passes it, and so does `from repo_shape import PROJECT_ID_RE as X`. Nothing in #1166 says that is false, and nothing says it.

All in `tests/test_value_pattern_line_break.py`.

- **Item 1.** The module docstring's bullet names `scaffold-project.py` and `family.py init` only, with `TOOLS` as the proof, and a new bullet says `adopt-project.py`'s argv `--id` is NOT held to `checked_value`: `plan` asks it with `match` and calls `checked_value` on `--project` and never on the id, so `northwind\n` passes the pattern, and the project's own `validate-manifest.py` refuses it as `adopt-manifest-invalid` naming `--id`, in `manifest-id`'s words, exit 2, no plan written (the row below, "measured for `plan` only"). The `adopt-project.py` row of `ID_READERS` begins with that reason in place of "argv `--id`, as above". The `scripts/family.py` and `scaffold-project.py` rows are as they were.
- **Item 1's optional row.** `test_adopt_plan_names_a_passed_id_that_ends_in_a_line_break_as_an_id` runs `adopt-project.py plan --id` with a trailing line feed (conftest's `write_plan` over its `source_repo`, one `plan` run of about a second) and asserts exit 2, the first stderr line `REFUSED adopt-manifest-invalid: --id is 'northwind\n', which the project's own validate-manifest.py refuses: manifest-id: id is 'northwind\n'; it must match ^[a-z0-9][a-z0-9-]*$.`, and that no plan file was written. It also imports `write_plan` from `conftest`. `--id` and `--id=...` gave the same lines when measured.
- **Item 2.** Both docstrings drop "it asks with `fullmatch`, or": a new reader, `fullmatch` included, is listed in `ID_READERS` with the reason a line feed cannot reach it, or the test fails; the table is exact both ways, so a reader taken away loses its row. The comment above `ID_READERS` says a reader added later needs its row whichever method it calls.
- **Item 3.** The assert carries a message: "`ID_READERS` lists every call of a method of `PROJECT_ID_RE` outside `tests/`, `fullmatch` included, by module and method. Add, change or drop the row of the module and method the diff below shows, and give a `match` or `search` reader the reason a line feed cannot reach it." Pytest prints it above the dict diff.
- **Item 4 (aliases).** Left as it is, and said: both docstrings say the guard reads spellings (a call whose receiver is the name `PROJECT_ID_RE` or an attribute of that name) and stops there, with the two aliases named as outside it. S1 to S3 below measure exactly that.

`git grep -n 'checked_value' -- tests/test_value_pattern_line_break.py` now shows the `checked_value` reason only against `scaffold-project.py` and `scripts/family.py` (lines 43, 171, 173); the module docstring's `adopt-project.py` bullet and the `adopt-project.py` row name `validate-manifest.py` and `adopt-manifest-invalid` for it, and say `adopt-project.py` is not held to `checked_value`.

**The new row fails on main (`50869f384`).** The test file is #1166's, so it was copied from this head onto a detached worktree at main, and only the new row was run (git 2.43.0): it fails, because on main the manifest validator's `id` pattern still takes `northwind` and a line feed (`$` matches before the line feed), and the line feed is refused only through the topic:

```
E  AssertionError: REFUSED adopt-manifest-invalid: --id is 'northwind\n', which the project's own validate-manifest.py refuses: manifest-topic: topic is 'xf-project-northwind', expected 'xf-project-northwind\n' derived from id 'northwind\n'.
E  -  manifest-id: id is 'northwind\n'; it must match ^[a-z0-9][a-z0-9-]*$.
E  +  manifest-topic: topic is 'xf-project-northwind', expected 'xf-project-northwind\n' derived from id 'northwind\n'.
```

So the row holds the claim that `manifest-id` is the refusal, which is #1166's own change to `validate-manifest.py`, and it passes at #1166's head plus this commit (31 passed below). That is why this PR was stacked on #1166 until #1166 landed.

Each applied alone to a scratch worktree (detached at `f92338ef6`), run, restored by writing the original bytes back, and the sha256 compared (`restored=True` each time, `git status` clean after each). Production files only; the test file is not touched by a plant. Node ids: the guard is `test_every_reader_of_the_id_pattern_is_in_the_table`, the row is the new one above. Baseline before any plant: both pass (2 passed).

| plant | file | what | result |
| --- | --- | --- | --- |
| G1 | `update-shape.py` | `def _rc_probe(value): return PROJECT_ID_RE.fullmatch(value)` appended | guard fails, its message names `ID_READERS`; `Left contains 1 more item: {('update-shape.py', 'fullmatch'): 1}` |
| G2 | `update-shape.py` | the same with `.match` | guard fails, message names `ID_READERS` |
| G3 | `update-shape.py` | the same with `.search` | guard fails, message names `ID_READERS` |
| G4 | `adopt-project.py` | `if False:` in place of the plan-`id:` test at line 6791 | guard fails, message names `ID_READERS`; `{('adopt-project.py', 'match'): 2} != {('adopt-project.py', 'match'): 3}` |
| R1 | `adopt-project.py` | `fullmatch` in place of `match` at line 5140 (`_refuse_bad_id`) | guard fails (names `ID_READERS`) and the row fails: `REFUSED adopt-bad-id: --id 'northwind\n' must match ^[a-z0-9][a-z0-9-]*$` |
| R2 | `adopt-project.py` | `checked_value('--id', project_id)` added before line 5140 | row fails: `REFUSED unsafe-value: --id is 'northwind\n', which is not a value this tool will put on a git or gh command line` |
| R3 | `adopt-project.py` | `if False:` in place of `if values is not None:` in `_refuse_unholdable_values`, so no manifest check | row fails: the plan is written, exit 0 |
| S1 | `update-shape.py` | `_ID = PROJECT_ID_RE` then `_ID.match(value)` | guard PASSES (1 passed): an alias is outside it, as the docstrings now say |
| S2 | `update-shape.py` | `from repo_shape import PROJECT_ID_RE as X` then `X.match(value)` | guard PASSES (1 passed) |
| S3 | `update-shape.py` | `PROJECT_ID_RE.match(value)` in a comment and in a string | guard PASSES (1 passed): not counted, as intended |

No mutant survives: G1 to G4 and R1 to R3 each fail where they should. R2 shows the new row also fails if `adopt-project.py` ever does hold the id to `checked_value`, which makes the module docstring's sentence false, so whoever adds that call is sent to the wording.

All through the lane's runner `run9.sh` (stub `gh` first on PATH, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`, `EMAIL` exported), on the committed head `f92338ef6` with `dirty=0`, Python 3.12.3, `git --version` printed into each log.

- `tests/test_value_pattern_line_break.py`: 31 passed on git 2.43.0 (25.78 s) and 31 passed on git 2.47.1 (31.21 s), the 30 of #1166's head plus the new row.
- The file plus the hygiene five (`test_pinned_copy_text.py`, `test_path_guard_hygiene.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`): 337 passed on git 2.43.0 (111.39 s) and 337 passed on git 2.47.1 (264.87 s), that is 31 and the five's 306.
- `git diff --check` is clean. The diff touches only the test file, adds no line over 79 columns (the file's own width) and no non-ASCII line, and the file has no `assert` that joins conditions with `and` or `or` (an `ast` scan for a `BoolOp` in any assert: none).

**At the restack (2026-11-08):** main is `105b5daf9`, #1166's squash, and the head is `b80214b7f`. `git merge-tree --write-tree origin/main HEAD` is clean, and the diff against main is the test file alone. The rest of this section is from the first push.

Main `50869f384` (re-fetched just before the push), this head `f92338ef6`:

- `git merge-tree --write-tree origin/main HEAD`: clean (exit 0).
- `git merge-tree --write-tree 5102b42 HEAD` (#1166's head): clean (exit 0).
- For the stack as a whole, `git merge-tree --write-tree origin/main 5102b42` (#1166 alone with main): clean (exit 0). The merge base of main and #1166's head is `038dafba9`.

Of the 51 open PRs (`gh pr list --json number,headRefName,headRefOid,files`), only #1166 touches `tests/test_value_pattern_line_break.py`. Not checked: whether a PR that touches `adopt-project.py` (26 do) adds or removes a call of a method of `PROJECT_ID_RE`; that is the guard's own job once #1166 lands, and #1166's body found none among the open PRs. A PR that makes `plan` hold `--id` to `checked_value` would fail the new row (R2), which is the row doing its work.

Review: rv1243, lane 5's independent sonnet reviewer, which wrote none of #1243: READY at `f92338ef`, H0 of this landing before its restack (issuecomment-6385593825), with no MUST; its one SHOULD (the body's restack sentence) is fixed in the body, and its four NITs are optional. The restack's changed lines equal the reviewed ones, and lane 3 placed it on v1.2.1 right after #1131 (#473 issuecomment-6723002632). CI (Brett Heap's ruling 27): H0 f92338e passed tests, tests-git-bash and SonarCloud; the head b80214b merges #1166's final head, then #1166's squash 105b5da (the branch's own side for #1166's files), then main, with tests, tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 372f767 over main 5926ccc: the value_pattern_line_break, repo_hygiene, agents_md_codes, cli_reference sets, 202 passed. Landed on Brett Heap's word of 2026-10-23T13:10:26Z, relayed to every lane by lane 3 (#473 issuecomment-6365697123): "merge v1.2.1 PRs once reviewed and green".

Closes #1229

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Nov 8, 2026
…says a full name that is no pattern takes the one ref git completes it to, which may be a branch, where it said any other source names no branch (#1238) (#1244)

#1238 is a wording NIT that the review of #1235 (#1230) found in the code: NIT 4 of rv1235's first review, and NIT 2 of its re-check. The docstring of `gives_a_branch_another_name` in `scripts/shape_materialize.py` ended "ANY OTHER SOURCE NAMES NO BRANCH", but git completes a source that is a full name and no pattern, and completes `refs/h` to the branch `refs/heads/refs/h` where the remote has one. The sentence now says what git does with such a source, as an open list ("such as"), and where the clash is. A reader of the docstring is no longer told that a source outside `refs/heads/` is never a branch.

Wording only, in one docstring of one file: 9 lines added, 3 removed. No code, no row, no other file. The file's syntax tree with every docstring dropped is the same as main's.

The sentence is at `scripts/shape_materialize.py:1720-1723`, the end of the docstring of `gives_a_branch_another_name` (line 1671):

```
1720:     BRANCH REACHES). A shorter start, such as `refs/*` or `*`, can name a
1721:     tag, and that clause has left it out first. ANY OTHER SOURCE NAMES NO
1722:     BRANCH, though a refspec from one can still put a ref on a branch's
1723:     name: `origin_fetch_refspecs` says where (WHAT CAN STILL CLASH).
```

It is true of a source in another namespace such as `refs/pull/7/head`. It is not true of every other source. A source that is a full name and no pattern takes the one ref git completes the name to on the remote: for `refs/h`, `refs/h` itself, the branch `refs/heads/refs/h`, the tag `refs/tags/refs/h`, `refs/refs/h` or `refs/remotes/refs/h`, and where several exist git takes the exact ref over a tag over a branch. Where the remote has the branch, the source is a branch. The code is right: `gives_a_branch_another_name("refs/h", ...)` is False, because the source has no `*` and does not start with `refs/heads/`, so the refspec is carried. What fails afterwards is the clash the docstring's own next clause names. Only the words were untrue.

Measured on this branch's base, with git 2.43.0 (`/usr/bin/git`) and git 2.47.1, which gave the same answers on every row. Local bare remotes, `GIT_CONFIG_GLOBAL=/dev/null`, `GIT_CONFIG_NOSYSTEM=1`, `GIT_SSH_COMMAND=false`. rv1235's probe scripts were run again here (copied unchanged, new output folders), and their logs are identical to rv1235's. The fetch was `+refs/heads/*:refs/remotes/origin/*` plus the refspec of the row, and the remote had `main` and `hy`:

```
the remote has                    refspec                          result
branch refs/heads/refs/h          +refs/h:refs/remotes/origin/hy   128 Cannot fetch both refs/heads/hy and refs/heads/refs/h to refs/remotes/origin/hy
tag refs/tags/refs/h              +refs/h:refs/remotes/origin/hy   128 Cannot fetch both refs/heads/hy and refs/tags/refs/h to refs/remotes/origin/hy
exact ref refs/h (update-ref)     +refs/h:refs/remotes/origin/hy   128 Cannot fetch both refs/heads/hy and refs/h to refs/remotes/origin/hy
refs/refs/h or refs/remotes/refs/h  +refs/h:refs/remotes/origin/hy 128, the same message naming that ref
each of those five refs, alone    onto origin/not-a-branch         0, holds that ref's commit
branch refs/heads/refs/h          onto origin/refs/h               0 (the branch's own name: no clash)
refs/pull/7/head                  onto origin/main                 128 Cannot fetch both refs/heads/main and refs/pull/7/head to refs/remotes/origin/main
branch refs/heads/main            onto origin/main                 0
```

So a name with no `*` can be a branch that its spelling does not show, and the clash is a ref that is not that branch put on a branch's name, which is what the second half of the sentence has to say. "ANY OTHER" was wrong in a second way: a carried refspec from a source that IS the branch does not clash onto that branch's own name.

The docstring's last sentence, now lines 1721-1729:

```
    tag, and that clause has left it out first. ANY OTHER SOURCE TAKES NO
    BRANCH BY A PATTERN, and a full name that is no pattern, such as
    `refs/h`, takes the one ref git completes it to on the remote: that ref
    itself, or one such as the branch `refs/heads/refs/h` or the tag
    `refs/tags/refs/h` (measured, git 2.43 and 2.47.1). Such a name can
    stand for a branch its spelling does not show, so it is not left out
    here. A refspec from any such source can still put a ref on a branch's
    name, if the ref is not that branch itself: `origin_fetch_refspecs`
    says where (WHAT CAN STILL CLASH).
```

- **"Takes no branch by a pattern"** holds for every source this sentence covers. Git matches a pattern against each ref's whole name, and a ref under `refs/heads/` starts with the part before the `*` only where that part is a start of `refs/heads/` or is under it, the two cases the function reads before this sentence.
- **The list is open** ("one such as"): it names the exact ref, the branch and the tag, and the rows above show two more completions (`refs/refs/h`, `refs/remotes/refs/h`), so no reader takes the three as all.
- **"So it is not left out here"** is what the code does: `gives_a_branch_another_name` is False for `refs/h` onto `origin/hy`, onto `origin/refs/h`, for `refs/pull/7/head` onto `origin/main`, and for the pattern `refs/pull/*/head`, and `origin_fetch_refspecs(["+refs/h:refs/remotes/origin/hy"])` returns it carried. It is True for `refs/heads/main` onto `origin/hy`, as before.
- **"If the ref is not that branch itself"** is the condition the rows show: the clash is two different refs on one name, a branch's.

The checks the issue's Verify lists:

- **The diff is only that docstring.** The diff has one hunk, inside the docstring, which spans lines 1672-1724 on main and 1672-1730 here. The file's tree with every docstring dropped (module, class and function: 168 each side) is equal on main (`50869f384`) and at this head under `python3 -I`, `ast.dump`: 380120 characters of dump on both sides. A control that renames `gives_a_branch_another_name` on the head side gives 380121 and DIFFERENT, and the same trees with the docstrings kept differ (520956 against 521366), so the check can fail. Python 3.12.3. The check compares two dumps made by one interpreter and asserts no text that depends on the version.
- **`git grep -n 'ANY OTHER SOURCE NAMES NO$' -- scripts/`** finds nothing at this head. On main it finds `shape_materialize.py:1721`. With any case and no line end, `git grep -n -i 'other source names no'` also finds nothing here, and nothing but that line on main.
- **No added line is over 79 columns or holds a non-ASCII character.** 9 lines are added, the longest is 75 columns, and none has a tab or trailing space. The file's own width is unchanged: 20 lines over 79 columns (longest 118) and 90 lines with a non-ASCII character, as on main, none of them mine.
- **No test, `README.md`, `docs/` page or `AGENTS.md` line pins the sentence.** The issue's grep holds at this head.

The issue asks whether `shape_materialize.py:1874` (now 1880, in the docstring of `origin_fetch_refspecs`) and `bump-leg.py:1165` (in `_leg_fetch_refspecs`) want the same change. Both paragraphs were read, and both are left as they are:

```
shape_materialize.py:1880:     WHAT CAN STILL CLASH is a refspec carried whose source names no branch,
bump-leg.py:1165:     whose source names no branch, onto a name a branch of the remote has
```

1. **They define a shape by its examples and say nothing about what git completes a name to.** Each goes on at once to the pull requests (`origin/pr/` where a branch `pr/7` is, and `+refs/pull/7/head:refs/remotes/origin/main` beside a branch `main`), and neither names `refs/h`. They were not the sentence that told a reader a source is never a branch.
2. **"Names no branch" is spelling there, as it is in the sentence the function's docstring opens with.** "A SOURCE UNDER `refs/heads/` NAMES BRANCHES" is true by spelling too: `can_name_a_tag`'s docstring measures that `+refs/heads/zz` took the tag `refs/tags/refs/heads/zz` where the remote had no branch `zz`. The sentence changed here now says outright that the spelling of a name with no `*` does not show what it stands for, so the docstring that defines the class carries the caveat the two borrowers need.
3. **The mechanism they state is the one that fails `refs/h`:** "wherever a name it writes is a branch's". The `origin_fetch_refspecs` paragraph ends with why such a refspec is carried at all: "Which names the remote's branches have is not known from its refspecs, and left out, what such a refspec keeps would be pruned on every run". That holds for `refs/h` word for word.
4. **The cost of leaving them is one reading:** a reader who takes "names no branch" in git's sense meets the single case `refs/h` beside a remote branch `refs/heads/refs/h`. If the review wants a word there, it is a few words in each place, and no open PR touches either paragraph (merge-trees below), so it can be a fix round.

None of either. No row is added or changed and no behaviour moved: the syntax tree outside the docstring is main's, so there is nothing for main to fail and nothing to mutate. `gives_a_branch_another_name` returns what it returned for every case in the table above (called in process at this head; the syntax tree outside the docstrings is main's).

All through the lane's runner `run9.sh` (stub `gh` first on PATH, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`), on the committed head `36ec589a6` with the tree clean (`dirty=0`), as one run of seven files at a time: `tests/test_pinned_copy_text.py`, `tests/test_path_guard_hygiene.py`, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py` (the hygiene five), `tests/test_family_bump_fetch_refspecs.py` and `tests/test_bump_leg_fetch_refspecs.py`.

- **The seven files, git 2.43.0** (`/usr/bin/git`): 504 passed in 469.44s (47 `test_agents_md_codes.py`, 68 `test_bump_leg_fetch_refspecs.py`, 23 `test_cli_reference.py`, 57 `test_family_bump_fetch_refspecs.py`, 121 `test_path_guard_hygiene.py`, 87 `test_pinned_copy_text.py`, 101 `test_repo_hygiene.py`; the counts are from `--collect-only -q` on the same head and add up to 504). Log `v131/n1238/gate-g243.log`.
- **The seven files, git 2.47.1** (first on PATH, `git --version` printed into the log): 504 passed in 714.00s. Log `v131/n1238/gate-g247.log`.
- **`-k carried-refs-h-not-a-pattern`** over `tests/test_family_bump_fetch_refspecs.py` and `tests/test_bump_leg_fetch_refspecs.py`: 2 passed, 123 deselected, on 2.43.0 and on 2.47.1 (`run-k-g243.log`, `run-k-g247.log`).
- Both runs start with `dirty=0`. Python 3.12.3 (CI runs 3.14); nothing asserts text that depends on the version.

The measured git behaviour above comes from the probe scripts, not the test files. `-k carried-refs-h-not-a-pattern` over the two fetch-refspec files is part of those runs.

`git merge-tree --write-tree origin/main HEAD` exits 0, with tree `9d874bcf2caf`, equal to this head's tree, against main `50869f384` (the merge base). Against the head of each open PR that touches `scripts/shape_materialize.py` or `scripts/bump-leg.py` (read from `gh pr list --state open --limit 100 --json number,headRefName,headRefOid,files`, 52 open PRs at the last read, 7 of them touch one of the two files), `git merge-tree --write-tree HEAD <head>` exits 0 and the merged tree still holds the new sentence and not the old one:

```
#1223 f50b472  exit 0   #1207 6fa1edd  exit 0   #1160 0068f5a  exit 0   #1131 55c30f2  exit 0
#1118 2307036  exit 0   #1105 b28f40d  exit 0   #1104 de89aa8  exit 0
```

Main moved once while this was gated, to `4829418f0` (`adopt-project.py`, `docs/cli.md` and a new test file: none of the files this change or its runs touch), and `git merge-tree --write-tree origin/main HEAD` against it also exits 0.

None of their hunks is within 100 lines of the sentence changed here.

Review: rv1244 and rv1244b, lane 5's independent sonnet reviewers, which wrote none of #1244: READY at `36ec589a6`, H0 of this landing (issuecomment-6723280477), with no MUST or SHOULD; its four NITs are optional (two wordings, a body note and a follow-up). CI (Brett Heap's ruling 27): H0 is the head 473959f itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 9729315; and the local gate passed on its predicted squash tree fb798f3 over main 9252ef2: the repo_hygiene, agents_md_codes, cli_reference sets, 171 passed. Landed on Brett Heap's word of 2026-10-23T13:10:26Z, relayed to every lane by lane 3 (#473 issuecomment-6365697123): "merge v1.2.1 PRs once reviewed and green".

Closes #1238

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant