Repository navigation
shape_materialize.py and the pinned AGENTS-shape.md: where the root keeps a GNUmakefile or a makefile, which GNU make reads first, the Makefile collision follow-up no longer says the shape's make targets arrive with the merge, and the pinned file says make reads the first of the three and the block reaches it only once that file holds it (#1048) - #1105
Conversation
…eeps a `GNUmakefile` or a `makefile`, which GNU make reads first, the Makefile collision follow-up no longer says the shape's make targets arrive with the merge, and the pinned file says `make` reads the first of the three and the block reaches it only once that file holds it (#1048) r1046 measured it in a clone that keeps all three names: after `cat shape/Makefile >> Makefile`, `make -n bootstrap` and `make contracts-dir` at the root still exit 2, "No rule to make target", for GNU make reads `GNUmakefile`, then `makefile`, then `Makefile`, and only that one. - `collision_follow_up` takes `reads_first`, the file GNU make reads before the shape's `Makefile`, and `_predict_collisions` reads it from the plan's root paths as `_makefile_follow_up` does. Where it is set, the follow-up names the file and says the targets reach `make` only once it holds them or includes a Makefile that does. Where the root keeps only the source's `Makefile`, the sentence is what it was, byte for byte. - `templates/assembly-root/AGENTS-shape.md` says `make contracts-dir` prints the block wherever `make` reads the shape's Makefile, names the three files in the order the code reads them, and says a kept `GNUmakefile` or `makefile`, beside a `Makefile` or instead of one, is the one `make` reads. The pin of every copy is rendered from these bytes at adopt and scaffold time, so no digest is stored in this repository; each project re-pins through `update-shape.py` as for any template change. - `tests/test_pinned_copy_text.py` holds the new text, and `tests/test_adopt_collision_make_reads.py` holds both sentences to `make -n` in a real adoption's clone, for each kept name. The `_next_steps` NOTE in adopt-project.py, the third box of #1048, is left to #1047. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Reviewer's GuideData structuresDefine the shared GNU make filename precedence tuple that the collision logic and pinned-text tests rely on.
ImplementationUse the filename precedence when predicting Makefile collisions and describing when the shape’s targets and bootstrap command become available.
DocumentationUpdate the pinned adoption guidance to explain GNU make’s file precedence and when the shape’s Makefile is read.
TestsKeep the pinned guidance text synchronized with the template’s updated explanation.
Exercise collision follow-ups, pinned wording, and real adoption behavior across kept makefile names and include scenarios.
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
RV1105: independent review of #1105 at One SHOULD: the new collision follow-up adds a claim, "and not with that merge alone", that is untrue where the kept What I measuredGNU make 4.3 (
"Only that one" also holds. With a The three texts, rendered for the same ten roots on a scratch merge of main
Pins.
FindingsSHOULD-1: "and not with that merge alone" is untrue where the file A
A The clause before it, "only once The same over-claim appears in four other places:
Fix:
Mutant R11 below drops the clause and shows which rows pin it now. NIT-1: wording drift among the three texts once #1121 lands.
All three name the same file, so nothing is wrong. Pick one verb and one phrase in whichever PR lands second. NIT-2: the script-aside tail, where The tail says: "neither #1121's NOTE says "then run NIT-3: the template's "and never the shape's Makefile". This is false where the source keeps a The case is rare. The rest of the sentence ("until that file holds the block, or includes a Makefile that does") is true for every root. Fix, if the template is touched again: "and not the shape's Makefile unless that file includes it". NIT-4: dead code and a stale docstring.
Fix in a follow-up: delete the method and that sentence. NIT-5: NIT-6: two template lines still say NIT-7: the order of landing against #1121. GitHub links only #1048 to this PR, but #1048's third box (the NIT-8 (existing before this PR): stale follow-ups after a hand edit. Writer's claims
The 12 fail for the right reason:
On main, the 3 clone rows still show MutantsI ran my own on the new guard, on the scratch merge. Each mutant was run against
Merge-tree
#1104 is the only one of them that touches Gate (scratch merge commit of main
|
| files | passed | failed | skipped |
|---|---|---|---|
test_adopt_collision_make_reads.py (new) |
27 | 0 | 0 |
test_pinned_copy_text.py, test_adopt_make_reads_first.py, test_adopt_contracts_dir.py, test_adopt_printed_sentences.py, test_repo_hygiene.py, test_scaffold_leg_path_collisions.py |
412 | 0 | 0 |
test_adopt_e2e.py |
27 | 0 | 0 |
test_shape_doctor.py, 4 shards (118 + 110 + 117 + 103 collected) |
445 | 0 | 3 (PowerShell rows, Windows host only) |
| total | 911 | 0 | 3 |
On a second scratch merge, main + #1105 + #1121, the new file, #1121's test_adopt_next_make_reads_first.py and test_pinned_copy_text.py gave 152 passed and 0 failed.
CI on 5a4de7f7 is green: tests, tests-git-bash, package and SonarCloud.
Lane: openreposhape-4 (openRepoShape-4)
…NU make reads first ends in `include Makefile`, the Makefile collision follow-up no longer says the merge alone does not bring the shape's make targets, its script-aside tail names only what the script's merge lets run, and the pinned file says `make` reads the shape's Makefile there only where that file includes it (#1048) #1105's review (RV1105) measured it in a real adoption: where the kept `GNUmakefile` or `makefile` ends in `include Makefile`, `make -n bootstrap` at the root exits 2 before the merge and 0 after `cat shape/Makefile >> Makefile` alone, so "and not with that merge alone" was untrue there. - `collision_follow_up`, where `reads_first` is set: says the targets reach `make` only once that file "has them, or includes a Makefile that does", and no more (SHOULD-1). It uses the words of `execute`'s NEXT (#1047): "has them", and "what the shape's `make bootstrap` runs" (NIT-1). Where `scripts/bootstrap.py` went aside too, it says neither command runs the shape's bootstrap until the script is merged, and then `python3 scripts/bootstrap.py` runs it, and no longer reads as if `make bootstrap` runs it after both merges (NIT-2). Where the root keeps only the source's `Makefile`, the sentence is what it was, byte for byte. - Its docstring, the `SHAPE_MAKE_TARGETS` comment and the `ADOPTED_SENTENCES` comment say the merge alone brings the targets only where that file includes `Makefile`. The docstring's opening says what is so: `plan` predicts the follow-ups, the split commit carries the plan's, `execute` names its collisions by path only, and `Materialized.follow_ups()` has no caller (NIT-4). - `templates/assembly-root/AGENTS-shape.md`: `make` at the root reads the first of the kept files, "and not the shape's Makefile unless that file includes it" (NIT-3). `ADOPTED_SENTENCES` holds the new text. No digest of the file is stored in this repository. - `tests/test_adopt_collision_make_reads.py`: the rows hold the new text, two rows hold the script-aside tail where `make` reads another file, and two rows in a real adoption's clone, for a `GNUmakefile` and a `makefile` that end in `include Makefile`, hold that the merge alone brings the shape's recipe and that the split message says nothing untrue of it. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
RV1105b: independent review of #1105 at I re-checked the fix round ( What I measuredGNU make 4.3 (
The shape's copy's
The three texts on a scratch merge of main
W1048b's claims
The 17 fail for the right reason:
#1111's rule. #1105 adds no MutantsI wrote my own, on the changed sentences, on the scratch merge
Merge-tree
#1118 conflicts in Gate (scratch merge commit
|
| files | passed | failed | skipped |
|---|---|---|---|
test_adopt_collision_make_reads.py, test_pinned_copy_text.py, test_adopt_make_reads_first.py, test_adopt_contracts_dir.py, test_adopt_printed_sentences.py, test_repo_hygiene.py, test_scaffold_leg_path_collisions.py |
443 | 0 | 0 |
test_adopt_e2e.py (pins the branch with no reads_first) |
27 | 0 | 0 |
test_shape_doctor.py, 4 shards by node id (118 + 110 + 117 + 103 collected) |
445 | 0 | 3 (PowerShell rows, Windows host only) |
| total | 915 | 0 | 3 |
On the triple merge 9f8d9060f (main + #1105 + #1121), the new file, #1121's test_adopt_next_make_reads_first.py and test_pinned_copy_text.py gave 158 passed and 0 failed.
Findings
NIT-1: the comments still say the merge alone brings the targets "only where that file includes Makefile". That is untrue where the file is a symlink to Makefile.
- RV1105's SHOULD-1 named this case: "or is a link to it".
- I measured it. With
Makefile+GNUmakefile -> Makefile(and the same withmakefile),make bootstrapexits 2 before the merge. After the merge alone it runs the shape's recipe, andmake contracts-dirprints the path. The file includes nothing. - The printed follow-up is true there: through the link,
X"has them". - The places are:
- the
SHAPE_MAKE_TARGETScomment; - the
collision_follow_updocstring ("exits 2 with the sameNo rulewhere it does not"); - the
ADOPTED_SENTENCEScomment; - the test module's docstring.
- the
Fix, in a follow-up: "only where that file includes Makefile or is a link to it".
NIT-2 (older than this PR): the branch with no reads_first still says "neither make bootstrap nor python3 scripts/bootstrap.py runs the shape's bootstrap before both merges are made". That is untrue of python3 scripts/bootstrap.py.
- I measured it. In a root that keeps only
Makefileand has the script aside, merging the script alone makespython3 scripts/bootstrap.pyprintSHAPE-BOOT, whilemake bootstrapstill exits 2. - This is the same reasoning W1048b used to keep "before both merges" out of the other branch.
- The text is main's (adopt-project.py: the tracking branch defaults to the source's default branch, topics are set before the pull request, and NEXT names scripts/bootstrap.py when the shape's Makefile went aside (#479, #480, #481) #493's F2), and this round kept it byte for byte on purpose.
test_adopt_e2e.pypins it.
Fix, in a follow-up: "... so python3 scripts/bootstrap.py runs the shape's bootstrap once the script is merged, and make bootstrap once both merges are made". Move the pin with it.
NIT-3: two docstrings are left with claims that are not true on main.
_predict_collisions, a docstring this PR adds a paragraph to, still says the wording comes from "the same functionexecutereports its actual collisions through". This is the claim NIT-4 removed fromcollision_follow_up.collision_follow_up's docstring says the follow-up uses "the wordsexecute's NEXT uses (adopt-project.py:execute's NEXT printsmake bootstrapwhere GNU make at the root reads a keptmakefileorGNUmakefile, which has nobootstraprule #1047)". The test module says "The words are those ofexecute's NEXT". On main today, NEXT's NOTE still says "arrive only once the follow-up merges the two;python3 scripts/bootstrap.pyis whatmake bootstrapruns". The words come with adopt-project.py:execute's NEXT printspython3 scripts/bootstrap.pywhere GNU make at the root reads a keptGNUmakefileormakefile, and its NOTE names that file and what it takes formake bootstrapto run there (#1047) #1121, so the claim is true only once adopt-project.py:execute's NEXT printspython3 scripts/bootstrap.pywhere GNU make at the root reads a keptGNUmakefileormakefile, and its NOTE names that file and what it takes formake bootstrapto run there (#1047) #1121 lands. RV1105's NIT-7 already asks for adopt-project.py:execute's NEXT printspython3 scripts/bootstrap.pywhere GNU make at the root reads a keptGNUmakefileormakefile, and its NOTE names that file and what it takes formake bootstrapto run there (#1047) #1121 to land first, and that makes this moot.- Smaller: "
Materialized.follow_ups()would say this of them". It passes noreads_first, so it would not say this.
Fix: reword the _predict_collisions sentence as NIT-4 did, and land #1121 first.
NIT-4: no row ties the template's new clause to what make does. Mutant K9 puts both the template and ADOPTED_SENTENCES back to "and never the shape's Makefile" together, and all 118 rows pass. The clone rows already show make reading the shape's Makefile through include Makefile where the source keeps makefile or GNUmakefile alone. Fix, optional: in that branch, also assert that the pinned file does not say "never the shape's Makefile".
NIT-5 (cosmetic):
- The template paragraph was not rewrapped: it leaves a short line, "does. A SCAFFOLDED".
- The new test's docstring breaks mid-sentence: "The tail says what the / script's merge does".
Every byte change to the template makes each project re-pin, so leave the template one for the next template change.
Lane: openreposhape-4 (openRepoShape-4)
… 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>
…py` where GNU make at the root reads a kept `GNUmakefile` or `makefile`, and its NOTE names that file and what it takes for `make bootstrap` to run there (#1047) (#1121) GNU make reads the first it finds of `GNUmakefile`, `makefile` and `Makefile`, and only that one. Where the source keeps `GNUmakefile` or `makefile` in the root, `make bootstrap` at the root of the clone reads the source's file, not the shape's `Makefile`, and exits 2 with "No rule to make target 'bootstrap'". `execute`'s NEXT printed it there anyway. This PR also covers the third box of #1048 (see #1048): the `_next_steps` NOTE that said the shape's targets "arrive only once the follow-up merges the two", which is untrue where GNU make reads a kept `GNUmakefile` or `makefile`. #1105 covers that issue's other two boxes, and leaves this one to this PR. ## Measured GNU Make 4.3 on Linux, a disk that keeps case. One directory per set of files, each file with a `which` target: | files | `make which` reads | `make -n bootstrap` | |---|---|---| | `GNUmakefile` | `GNUmakefile` | exit 2, no rule | | `makefile` | `makefile` | exit 2 | | `Makefile` (with a `bootstrap` rule) | `Makefile` | exit 0 | | `GNUmakefile` + `makefile` | `GNUmakefile` | exit 2 | | `GNUmakefile` + `Makefile` | `GNUmakefile` | exit 2 | | `makefile` + `Makefile` | `makefile` | exit 2 | | all three | `GNUmakefile` | exit 2 | A `GNUmakefile` that holds `include Makefile` runs the shape's rule (exit 0), and so does `make -f Makefile bootstrap`. What NEXT printed on main, from a real `plan`, `check` and `execute --local-remote-dir`, with the printed command run in a recursive clone of the split ("S" is the source's own `scripts/bootstrap.py`, kept with `scripts/`): | the root keeps | NEXT on main | in the clone | |---|---|---| | none | `make bootstrap` | exit 0 | | `Makefile` | `python3 scripts/bootstrap.py`, #480's NOTE | exit 0 | | `makefile`, `GNUmakefile`, or both | `make bootstrap` | **exit 2, no rule** | | `Makefile` + `makefile`, `Makefile` + `GNUmakefile`, all three | `python3 scripts/bootstrap.py`, and a NOTE saying the targets arrive once the two `Makefile`s are merged | the command exits 0; `make bootstrap` exits 2, and still does after `cat shape/Makefile >> Makefile` | | S + `GNUmakefile`, S + `makefile`, S + `Makefile` + `GNUmakefile` | no command; the NOTE says to merge, "and then run `make bootstrap`" | `make bootstrap` exits 2 | ## What changed `adopt-project.py`, `_next_steps`: - It takes `surviving=`, the paths the plan's entries send to the root (`paths_for["root"]`, passed by `cmd_execute`). - It asks them for a kept `GNUmakefile` or `makefile` in GNU make's order, with `_root_keeps`, as `_makefile_follow_up` asks them. So NEXT names the same file as the plan's `CONTRACTS_DIR` follow-up. - Where it finds one, the command is `python3 scripts/bootstrap.py`. The NOTE names the file in the follow-up's words, and says `make bootstrap` and the shape's other make targets run there only once that file has them, or includes a Makefile that does: - the shape's `Makefile` is the root's: "NOTE the shape's Makefile is the root's `Makefile`, but GNU make at the root reads the source's `GNUmakefile`, the first of `GNUmakefile`, `makefile` and `Makefile` it finds, so `make bootstrap` and the shape's other make targets run there only once `GNUmakefile` has them, or includes a Makefile that does; `python3 scripts/bootstrap.py` is what the shape's `make bootstrap` runs." - the source also keeps its own `Makefile`: "NOTE the root keeps its own Makefile and the shape's is shape/Makefile, and GNU make at the root reads ..." (the same clause, and the same ending). - the source's own script went aside too: the merges first, as before, "and then run `python3 scripts/bootstrap.py`; GNU make at the root reads ...". - A root that keeps neither name gets the block it got before, byte for byte. That includes `MAKEFILE`, which GNU make never reads where case is kept. ## Tests `tests/test_adopt_next_make_reads_first.py`, new, 40 tests (38, and the 2 of the fix round below): - 13 rows word for word: none, `Makefile`, `makefile`, `GNUmakefile`, `makefile`+`GNUmakefile`, `Makefile`+`makefile`, `Makefile`+`GNUmakefile`, all three, S, S+`Makefile`, S+`GNUmakefile`, S+`makefile`, S+`Makefile`+`GNUmakefile`. - 3 rows that keep `make bootstrap`: `MAKEFILE`; longer names (`makefile.am`, `Makefile.in`, `GNUmakefile.bak`); and the names below the root. - 13 rows in which NEXT names the same file as `_makefile_follow_up`, for the same root paths. - 9 end to end rows, one for each row that keeps `GNUmakefile` or `makefile`. Each is a real `execute`, and its clone is made by pasting NEXT's own clone line. In that clone: - NEXT's lines are the row's; - `make which` prints the file the NOTE names; - `make -n bootstrap` has no rule; - the printed command exits 0 and ends `bootstrap ok`. For S rows, it does so once the shape's script is merged, as the NOTE says; - where a `Makefile` is kept, the follow-up's merge alone still leaves no rule; - an `include Makefile` in the file GNU make reads makes `make -n bootstrap` run the shape's recipe. On main's `adopt-project.py`, the first 38 all fail. The 9 end to end rows fail on what NEXT prints. The unit rows that guard the unchanged cases fail there only for want of the `surviving=` keyword. M1 below shows the same rows passing and every row that keeps `GNUmakefile` or `makefile` failing. On this head, all 40 pass. Mutants of the change, each restored and checked by sha256. Each run is the new file plus the five `next` tests of `test_adopt_e2e.py` (43 tests). Every mutant is killed: | mutant | failed | |---|---| | M1 `first` never found | 27 | | M2 `makefile` asked before `GNUmakefile` | 6 | | M3 the command ignores `first` | 6 | | M4 the branch for a `Makefile` kept beside `first` dropped | 6 | | M5 the branch for a script gone aside ignores `first` | 9 | | M6 the lookup folds case | 7, two of them existing `test_adopt_e2e.py` rows | | M7 `cmd_execute` passes no paths | 9, the end to end rows | Gate on this head, named files only, one basetemp per run. Each run had `EMAIL` set, `GIT_CONFIG_GLOBAL` an empty file, `GIT_CONFIG_NOSYSTEM=1`, a stub `gh` first on PATH, `GH_TOKEN=bogus` and an empty `GH_CONFIG_DIR`: - the new file, `test_adopt_make_reads_first`, `test_adopt_e2e`: 98 passed, with the new file at 38 tests (at `c04bb710`; the fix round's gate is below). - `test_adopt_github_exits`, `test_adopt_printed_sentences`, `test_repo_hygiene`, `test_readme_windows_quoting_sentence`, `test_cli_reference`, `test_agents_md_codes`: 291 passed. - `test_scaffold_leg_path_collisions`: 104 passed. - `test_adopt_contracts_dir`, `test_adopt_verify`, `test_adopt_pins`, `test_adopt_root_manifest`, `test_adopt_ignored_shape_files`, `test_adopt_nested_attributes`, `test_adopt_shape_pin_and_work_dir`: 378 passed. - `test_shape_doctor.py` in 4 shards: 112 + 112 + 112 + 109 = 445 passed, and 3 skipped (the PowerShell line, which is printed only on a Windows host). ## Fix round 1 (RV1121's SHOULD-1) RV1121 found the code right and one test missing: nothing held `cmd_execute` to handing `_next_steps(surviving=)` only the root's paths. Its mutant X4, which passes `paths_for["root"] + paths_for["code"] + paths_for["drop"]`, kept all 43 tests green. The case is a real one: the `root-project-makefile` rule sends a kept `GNUmakefile` to the root, and its reason says that moving a Makefile that only builds the code to the code leg "is a one-line edit of the plan" (`contracts/path-classification.yaml`, lines 236-237). Two end to end rows, `test_next_keeps_make_bootstrap_where_the_plan_sends_the_makefile_elsewhere[code]` and `[drop]`. Each keeps `GNUmakefile` in the source, edits the plan to send it to `leg: code` (then `leg: drop`), runs a real `execute`, and pastes NEXT's own clone line. Each asserts that NEXT's lines after the clone are ` make bootstrap` and nothing else, that the clone's root has no `GNUmakefile`, and that `make -n bootstrap` there exits 0 and runs `python3 scripts/bootstrap.py`. The `execute`, paste and clone steps of the nine existing end to end rows moved into one helper, `executed_and_cloned`, which the two new rows share, so no block of the test is written twice. Those rows assert what they asserted. The file now holds 40 tests (38 and 2). - **X4, on this head's `adopt-project.py` with the one line changed:** the new file and the five `next` rows of `test_adopt_e2e.py` (45 selected): 2 failed, 43 passed. Both new rows fail on NEXT's lines, which are `python3 scripts/bootstrap.py` and the NOTE "the shape's Makefile is the root's `Makefile`, but GNU make at the root reads the source's `GNUmakefile`, ...", naming a file the root does not have. The file was restored and its sha256 read `60bad59a...` again. - **On main's `adopt-project.py`:** the two rows pass, since main printed `make bootstrap` there. They hold the unchanged case against the new lookup, and X4 is the mutant that tells them apart. - **On this head `1c5173758`**, same environment as above: the new file 40 passed, `test_adopt_e2e.py` 27 passed, `test_repo_hygiene.py` 100 passed. - **Merge:** `git merge-tree --write-tree` of this head with main at `823624d93`: clean (tree `73ddafb2`). RV1121's four NITs are not in this PR; the lane files them. ## Merge - `git merge-tree --write-tree` of this head with main at `6da757cbe`, where the branch starts: clean (tree `21955cc7`). Main has since moved to `7cd66902a`, which changes `scaffold-project.py`, its test and the docs, and none of this branch's files. The merge with it is clean too (tree `ec4e6c8e`). - With #1105's head `5a4de7f76`: clean (tree `3394d19d`). #1105's `adopt-project.py` change is in `_predict_collisions`, apart from `_next_steps`. On that merge, built in a `git clone --shared` checkout, `test_adopt_next_make_reads_first`, #1105's `test_adopt_collision_make_reads` and `test_pinned_copy_text` gave 152 passed. - With the other open pull requests that change `adopt-project.py` (#1120 `c55e55e3`, #1119 `bdcf9952`, #1118 `c60066cb`, #1106 `504a1dd6`, #1091 `160de118`): each clean. ## Not in this PR - Once #1105 lands, the lookup "which of `GNUmakefile` and `makefile` the root keeps first" is written three times: in `_makefile_follow_up`, `_predict_collisions` and `_next_steps`. Each is the same two lines over `GNU_MAKE_READS` and `_root_keeps`. They are kept apart here so this PR and #1105 touch disjoint lines; one helper could serve all three after both land. - `templates/assembly-root/AGENTS-shape.md` line 19 ("`git clone --recurse-submodules` this repository and run `make bootstrap`") has the same gap in an adopted root that keeps `GNUmakefile` or `makefile`. It is pinned, and #1105 changes that file, so it is left. - On a disk that ignores case (#1049), a kept `makefile` is reported as a `Makefile` collision, and NEXT's NOTE says "the root keeps its own Makefile". The new clause is true there too, since GNU make reads that file and merging into it brings the targets. The spelling is #1049's. - A kept directory named `GNUmakefile/` or `makefile/` is named as the source's file. GNU make stops on it with "Is a directory". `_makefile_follow_up` does the same. Closes #1047 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s message spells a path byte that is not UTF-8 as `_shown` does, an escape, and no longer says it names it with a `?` in its place, which #900 replaced (#1018) (#1104) `env_commit`'s docstring in `scripts/shape_materialize.py` says what the split commit's message does with a `root` or `drop` path that holds a byte that is not UTF-8, as the code does since #900. It said the message names such a path "with a `?` in place of that byte, a path that does not exist". That was true before #900, and it is not true now. A reader of the docstring is told about a `caf?.md` the tool no longer writes. Found by lane 1's writer on #900 and filed as #1018. Lane 5 took it outside v1.2.0, under Brett Heap's ruling of 2026-10-21T10:32:04Z. Measured on main `2a119f281` and again on `16f9a0e81` (main after #1087), with a small script that calls: `adopt-project.py`'s own `_split_message` and `_shown`, then `env_commit` and a real `git commit`, read back with `cat -A`. - The issue's claim held: the docstring (about line 2103 when filed, line 2268 on `2a119f281`, line 2349 on this branch) still said the message names the byte with a `?`. - The code does not. `_shown("caf\udce9.md")` is `` `caf\udce9.md` ``, and the split message for a `root` and a `drop` entry named that holds no lone surrogate. Through `env_commit` and `git cat-file commit HEAD`, `cat -A`: ``` DROPPED (in no leg and no longer here):$ `caf\udce9.md`$ STAYS IN THE ASSEMBLY ROOT:$ README.md$ `caf\udce9.md`$ ``` - What stays true of `env_commit` itself: a message that holds the lone surrogate, handed to it directly, is still written as `?`. `cat -A` reads `Name: caf?.md$`. So the `errors="replace"` paragraph is kept, and what changes is who it describes. - #1087 (main `16f9a0e81`) changed `scripts/shape_materialize.py` by 128 lines, none of them in `env_commit`, `COMMIT_COMMAND` or the message, and changed nothing in `adopt-project.py` (0 diff lines). The rebase onto it was clean and the docstring needed no other change. One file, `env_commit`'s docstring, in two paragraphs: the `errors="replace"` paragraph (round 1, 11 lines added and 7 removed) and the sentence above it (round 2, below; 3 added and 2 removed). Together 14 added and 9 removed against main `6da757cb`. The `errors="replace"` paragraph now says: - encoding, a character UTF-8 cannot carry is written as `?`, and nothing raises after both legs are pushed, as before; - `adopt-project.py` refuses a lone surrogate in a path it extracts (`entry_path_problem`), and a `root` or `drop` entry that holds one is listed in the split message but never raw: the message names each path as `plan`'s report does (`_shown`, #900), so the byte 0xE9 reads as an escape with no surrogate in it, `` `caf\\udce9.md` ``, the path the source holds; - written raw it would be a `?`, `caf?.md`, a path that does not exist, which is what `env_commit` still writes for a lone surrogate it is handed. The backslash is doubled in the source, as the neighbouring docstrings write `\\udcc3`. A single `\u` in a non-raw docstring would be an escape that makes a lone surrogate. Measured: the docstring holds no character from U+D800 to U+DFFF and holds the six characters `\udce9`. The longest docstring line is 78 characters. **The code is unchanged.** A script compares the AST of `scripts/shape_materialize.py` with every docstring blanked: the branch's file is identical to `origin/main`'s after the rebase onto `16f9a0e81` (round 1), and the merge of this head into `origin/main` `6da757cb` is identical to `6da757cb`'s file (round 2). **No pin moves.** `scripts/shape_materialize.py` is not a pinned copy. `COPIED_VERBATIM`, `COPIED_FROM_SHAPE`, `FAMILY_COPIED_VERBATIM` and `FAMILY_COPIED_FROM_SHAPE` do not list it, and `templates/assembly-root/scripts/validate-pins.py` says of it "which no project carries". So no `upstream-changed` row, no digest, no assembly copy and no `tests/test_pinned_copy_text.py` entry changes. **No other text pins these words.** Searched in `tests/`, `README.md`, `docs/`, `docs/handbook.html`, `AGENTS.md` and `templates/`: no test reads `env_commit`'s docstring (`__doc__`, `getdoc`), and none of them holds "in place of that byte" or "a path that does not exist" for this. The other places that name the `?` are about this function's behaviour or about history, and are still true: `_split_message`'s docstring says `caf\\udce9.md` "was `caf?.md`" (before #900), `_refused_by_policy` already says the message "lists it as `_shown` does (#900)", and `tests/test_adopt_spelled_names.py` quotes the message as it was on `c7a3ff16`. The `?` in `argv_text`, `trailer_line` and the commit-trailer text is about a `--trailer`, not the split message. No rows and no mutants: the change is a docstring, the code is the same AST, and no test pins the docstring. So there is no row to fail on main and nothing to mutate. All with `$Q/v110/run9.sh`, `EMAIL=service@opensoft.dev`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`, the stub `gh`, a fake token, git 2.43.0 (`/usr/bin/git`). Two shards, run before the rebase, at the `2a119f281` base with this one-file change in the tree. Together they are every `tests/*.py` that names `shape_materialize` or `env_commit` (86 files, including `tests/test_adopt_spelled_names.py` and `tests/test_adopt_entry_paths.py`, which run `env_commit` and the #900 message) and `tests/test_agents_md_codes.py`: | run | files | result | |---|---|---| | shard A | 43 | 2873 passed, 4 skipped in 1707.51s, rc=0 | | shard B | 44 | 4611 passed, 31 skipped in 2462.01s, rc=0 | After the rebase onto `16f9a0e81`, on the committed head `e32b2703d733f6c60c1b2cba2d62d164d93d7b9a` (`dirty=0`): the trio, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py` and `tests/test_cli_reference.py`, **170 passed in 55.67s**, rc=0. The docstring-only change needed no new shards: the shards' counts are runs at the `2a119f281` base, and #1087 did not touch `env_commit`. Round 2, on the committed head `de89aa81b1533e32f1007f991aeae05cd915b36d` (`dirty=0`): the same trio, **170 passed in 61.77s**, rc=0. The round 2 commit is one docstring sentence, so it needed no new shards either. git 2.47.1: no run. The change adds no test file of its own, so there is none to run on it. `git merge-tree --write-tree HEAD <other>` with HEAD at `de89aa81b1533e32f1007f991aeae05cd915b36d`, after a re-fetch of `origin main` with `-c gc.auto=0 -c maintenance.auto=false` (round 2, about 14:03Z on 2026-10-21): | against | result | |---|---| | `origin/main` `6da757cbe9a4eb66d60c34f38dcc2b332d175e8b` | clean, rc 0, tree `15c11b6c959fc4c7540105d5d62e3f961c8e27fd` | | #1105 `fix/1048-makefile-follow-up-gnu-read-order` `5a4de7f763b5a3c32adce9614a3a539508416eb4`, an open PR that touches `scripts/shape_materialize.py` | clean, rc 0, tree `db05aead67052643942e35a9689d9dd4a0873fda` | | #1118 `fix/1089-named-link-skip-tests` `c60066cbeec18b8b6c926e254ed4bee6b9a486c2`, the other open PR that touches `scripts/shape_materialize.py` | clean, rc 0, tree `a40df3b63c32c2a3d8f4c40587b094940c8d6567` | Read with `gh pr list --state open --json number,headRefName,headRefOid,files`: 16 open PRs, three of them (this one, #1105 and #1118) touch `scripts/shape_materialize.py`, the only file this change touches. The other 13 (#1120, #1119, #1117, #1111, #1109, #1107, #1106, #1102, #1101, #1098, #1097, #1091, #1088) touch no file this change touches. The same command with HEAD at the reviewed head `e32b2703` gives rv1104's trees: `de31d857` against `6da757cb` and `195ffe19` against #1105. The round 1 merge against #1090 is moot: #1090 has landed in `6da757cb`. Held as a draft until the v1.2.0 tag; it is now on the v1.2.1 milestone. - **NIT 1, `scripts/shape_materialize.py` lines 2340-2343, a line round 1 did not change.** The paragraph above the reworded one said the adoption's split message "names each entry path as the source spells it". Since #900 that is true of a printable path alone: `_split_message` lists any other through `_shown`, as the escape the next paragraph describes. It now says "as the source spells it (a printable one; see below)". One new commit on top of `e32b2703`, `de89aa81b1533e32f1007f991aeae05cd915b36d`, 3 lines added and 2 removed, docstring only, pushed as a fast-forward (no rebase, the PR is open). - Measured: the AST with docstrings blanked is identical to `e32b2703`'s and, merged into `6da757cb`, to `6da757cb`'s. The docstring holds no lone surrogate, still holds the six characters `\udce9`, and its longest line is 78 characters. - Searched `tests/`, `README.md`, `docs/`, the handbook mirror, `AGENTS.md`, `templates/` and the scripts for "names each entry path", "as the source spells" and the new words: only `tests/test_adopt_entry_paths.py` line 233 matches, a test's docstring about `execute` handing each path on as the source spells it, which is another sentence and pins nothing here. - **NIT 2, this body.** It named main as `16f9a0e81` and #1090 as the one open PR that touches the file. #1090 has landed. Main is `6da757cb`, and the merge-trees above are redone against it, against #1105 and against #1118, a draft that opened since and also touches the file. Review: rv1104-1107, lane 5's independent opus reviewer, which wrote none of #1104: READY at `de89aa81`, H0 of this landing (issuecomment-6318111413), with no finding; NIT 1 and NIT 2 of the round before are fixed. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 de89aa8 passed tests, tests-git-bash and SonarCloud; the head 92285d4 is a clean update-branch merge of main b66d8f0, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 6bd2128 over main b66d8f0: 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 #1018 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
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 21 hours by commenting |
|
…e out the trees that keep case twins, which check names and execute refuses as plan-case-twins (#1161) `test_the_sentences_are_true_of_what_make_does_in_the_clone` (#1105) ran a real adoption at `[Makefile+makefile]` and `[all-three]`, and `test_where_the_file_make_reads_includes_makefile_the_merge_alone_is_enough` (#1105) at `[makefile]`: sources that keep `Makefile` and `makefile` side by side in the root, each with its own blob. Since #1161 `check` names that set of case twins as `plan-case-twins` and `execute` refuses it before any leg, so there is no clone to run `make` in: at the merge 47478d5 all three failed at `adopt_with_legs_mounted_away`'s `check` (`FINDING plan-case-twins: ... `Makefile` and `makefile``, `assert 1 == 0`). Both parametrizes now leave out the rows `keeps_twins` (from `test_adopt_make_reads_first`) names, as 7d2f651 did in `test_adopt_next_make_reads_first.py`, and each docstring says why, and that `test_execute_refuses_makefile_beside_makefile_before_any_leg` pins the refusal for those trees. Every other row is as it was: the clone test runs 5 of CLONES, the include test its `[GNUmakefile]` row, and the file's other functions, which call `_predict_collisions` and `follow_ups_for` and run no `check`, all their rows, `[Makefile+makefile]` and `[all-three]` among them. Measured on CPython 3.12, the whole file: at 47478d5 3 failed, 28 passed; with this change 28 passed, the same 28 node ids. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…`makefile/` is tested, README says `make` at the root reads a kept `GNUmakefile` or `makefile` instead, and a docstring says the shape's Makefile alone holds `CONTRACTS_DIR` (#1061) (#1123) #1046 (#977) left three follow-ups, which r1046 named as NIT-C, NIT-D and NIT-E. None is a regression. A kept directory entry `makefile/` had no test: dropping the `rstrip("/")` in `twins`, in `_makefile_follow_up`, left every test passing, though without it `twins` is empty and `twins[0]` raises in `plan`. README did not say that `make` at the root reads a kept `GNUmakefile` or `makefile` instead of the shape's `Makefile`. And the docstring of `contracts_from_code_leg` said "`CONTRACTS_DIR` is the root Makefile's alone", which predates #977. - (a) `ANSWERED` in `tests/test_adopt_make_reads_first.py` gains a `("makefile/", "root")` row, id `makefile-directory`, which expects the line a kept `makefile` file gets. GNU make 4.3 stops on a directory with `makefile: Is a directory.`, so the case is contrived, and the comment above `ANSWERED` says so. - (b) README's section "Adopting an existing repository" now ends the sentence on `shape/Makefile` with "which holds the export until a person merges the two; `make` at the root reads a kept `GNUmakefile` or `makefile` instead." - README.md is at its cap, so the paragraph is tightened and stays at 12 lines, with README.md at 1482 lines. Three phrasings are shorter, and no fact is lost, only the word "honest". - The handbook's `adopt` section says nothing of `CONTRACTS_DIR`, so only its marker moves, to the section's new blob (`a7154cc3`), and `SENTENCE` in the test follows the new text. - (c) `contracts_from_code_leg`'s docstring now says `CONTRACTS_DIR` is the shape's Makefile's alone: the root's `Makefile`, or, where the source keeps a `Makefile` of its own, `shape/Makefile`. `adopt-project.py` changes only in that docstring. - Measured, with mutants of `twins` in `_makefile_follow_up`, each in a scratch clone and restored by sha256: - both `rstrip("/")` dropped (R10): on main the test file gives 32 passed and 1 failed, README's sentence and nothing to do with the mutant; on head it gives 1 failed and 33 passed, `[makefile-directory]`, with `IndexError: list index out of range`; - `rstrip("/")` dropped from the filter only: 1 failed, 33 passed, the same `IndexError`; - `rstrip("/")` dropped from the element only: 1 failed, 33 passed, the line names `makefile/` where it names `makefile`. - So R10 survived the 32 rows on main, and the new row kills it and both halves of it. `tests/test_adopt_make_reads_first.py` has 33 tests on main and 34 here. - Known limits: - The directory row pins a line that is not true for a directory, since make stops with "Is a directory", so the row works as a crash guard. - The new docstring and README clause leave out the disk that ignores case, where a kept `makefile` (or `MAKEFILE`, or the directory `makefile/`) is the `Makefile` path and `place` writes the shape's copy to `shape/Makefile`. - Not in this PR: #1048 covers the same gap in `collision_follow_up`, `AGENTS-shape.md` and the NOTE in `adopt-project.py`. `adopt_makefile_block`'s docstring in `scripts/shape_materialize.py` says the block is appended "to the assembly root's Makefile", the same older claim. It is filed as #1135, since #1048 and its PR #1105 do not hold it. Review: lane 1's independent reviewer r1123 found `5c3908f8` READY on condition, a complete review, with no MUST, two SHOULDs and three NITs (#1123 issuecomment-6319888568). The SHOULDs were a hand-off of `adopt_makefile_block`'s docstring that nobody held, and a stale "Merge with main" section. Lane 1 met both in the body: the docstring is filed as #1135, and the section names main `823624d9`. NIT-1 is softened in the body, and NIT-2 and NIT-3 are the known limits above. On the merge with `823624d9` (tree `a317e482`) the review measured README.md at 1489 lines, exactly at its cap of 1489, the `adopt` handbook marker unchanged at `a7154cc3`, and the tests passing. It ran real adoptions and `/usr/bin/make` (GNU Make 4.3) to show the README sentence is true. The new row is the only one that kills R10 and both of its halves, and 7 of 8 mutants were killed (the writer's three, re-measured, and five of the review's own). The survivor, `twins` sorted with `reverse=True`, changes no row, because no row keeps two names that differ only in case, and it is outside this change's scope. Tests, under a stub `gh` with a bogus token: at `5c3908f8`, with main `6da757cb` merged in, the gate in the PR body gave 1,151 passed and 3 skipped over named files, in three groups: - `tests/test_adopt_make_reads_first.py`, `tests/test_adopt_contracts_dir.py` and `tests/test_adopt_printed_sentences.py`: 122 passed; - `tests/test_repo_hygiene.py`, `tests/test_readme_sections.py`, `tests/test_readme_windows_quoting_sentence.py`, `tests/test_family_docs_one_spelling.py`, `tests/test_setup_by_hand_commands.py`, `tests/test_update_add_codes_documented.py`, `tests/test_doctor_plan_overrides.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py`, `tests/test_pinned_copy_text.py` and `tests/test_scaffold_leg_path_collisions.py`: 584 passed; - `tests/test_shape_doctor.py` in 4 shards: 445 passed and 3 skipped, the PowerShell line, which runs on Windows only. The review reproduced the same numbers on the merge with `823624d9`. It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its H0 `5c3908f8` is green on Linux and SonarCloud. The branch was never rebased. After H0, the only commit is update-branch's clean merge of main, giving d22439a, which is green on SonarCloud and Git Bash. Lane 1's gate passed on its tree: 8 pytest calls over `tests/test_adopt_make_reads_first.py`, `tests/test_adopt_next_make_reads_first.py`, `tests/test_adopt_contracts_dir.py`, `tests/test_adopt_printed_sentences.py`, `tests/test_repo_hygiene.py`, `tests/test_readme_sections.py`, `tests/test_readme_windows_quoting_sentence.py`, `tests/test_family_docs_one_spelling.py`, `tests/test_setup_by_hand_commands.py`, `tests/test_update_add_codes_documented.py`, `tests/test_doctor_plan_overrides.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py`, `tests/test_pinned_copy_text.py`, `tests/test_scaffold_leg_path_collisions.py`, `tests/test_path_guard_hygiene.py`, `tests/test_no_auto_maintenance.py` and `tests/test_shape_doctor.py` in 4 shards: 1,383 passed, 5 skipped. Closes #1061 Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on_follow_up` keeps #1105's `reads_first` lines and #1207's script-aside tail, each naming `python` on Windows, and four pins in #1105's test file move with them Lane 4's #1105 (#1048) landed as 1212295, and its `collision_follow_up` conflicted with this branch's, as the body's "Landing order" said it would. `adopt-project.py` and `templates/assembly-root/AGENTS-shape.md` merged with no conflict. The resolution keeps both sides' behaviour and does none of #1240's unification: #1105's second `GNU_MAKE_READS`, its `_predict_collisions` lookup and its collision line stay, and so do this branch's `_make_reads_first`, `_make_reads_first_clause` and NEXT. scripts/shape_materialize.py, three hunks, all in `collision_follow_up`: - Docstring, the NIT-4 sentence: both sides rewrote the same sentence for the same finding. Main's is kept, since it also names `Materialized.follow_ups()`, which #1240 item G changes. - Docstring, the Makefile paragraphs: main's heading and its "BUT GNU MAKE READS" paragraph, with this branch's `BOOTSTRAP_COMMAND` and NIT-2 sentences in the first, and one sentence in the second saying both of its commands are named as `BOOTSTRAP_COMMAND` names them. - The returns: #1105's per-branch `arrive`, `joint` and `meanwhile` stay, and its `until` becomes a per-branch `then`. With no `reads_first`, the line is this branch's, byte for byte. With one, it is #1105's, byte for byte, but the command a person runs is `BOOTSTRAP_COMMAND` in `meanwhile` and in "and then ... runs it" (#1240 item C, for #1105's line). tests/test_adopt_collision_make_reads.py (#1105's): `MERGE_IS_ENOUGH`, `MEANWHILE`, `BOTH_MERGES` and `SCRIPT_MERGED` move to those words, through a new `COMMAND`. Before the move exactly the 11 rows that use them failed, and the other 242 rows of the six adopt and pin files passed. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>



Two sentences said a kept
Makefileis what holds the shape's block back. GNU make reads the first ofGNUmakefile,makefileandMakefileit finds, and only that one, so where the root also keepsGNUmakefileormakefile,makereads that file, and reaches the mergedMakefileonly where that file includes it. Closes #1048.Measured (a real
execute, thenmake -nin the clone, for each kept name; r1046's NIT-B, repeated; the include rows are RV1105's SHOULD-1):makereadscat shape/Makefile >> Makefile,make -n bootstrapMakefileMakefileMakefile+makefilemakefileMakefile+GNUmakefileGNUmakefileGNUmakefileMakefile+ amakefileorGNUmakefilethat ends ininclude Makefilepython3 scripts/bootstrap.py(exit 2 before the merge)makefileorGNUmakefilealoneMakefileis the root's, andmake contracts-dirhas no rule until that file holds the block or includesMakefileWhat changed
scripts/shape_materialize.py,collision_follow_up: takesreads_first, the file GNU make reads before the shape'sMakefile. Where it is set, the Makefile's follow-up says GNU make reads that file, the first of the three it finds, and that the shape's make targets reachmakethere only once that file has them, or includes a Makefile that does; until thenpython3 scripts/bootstrap.pyis what the shape'smake bootstrapruns. Wherescripts/bootstrap.pywent aside too, it says neither command runs the shape's bootstrap until the script is merged, and thenpython3 scripts/bootstrap.pyruns it. Where the root keeps only the source'sMakefile, the sentence is what it was, byte for byte.GNU_MAKE_READSjoinsSHAPE_MAKE_TARGETS.adopt-project.py,_predict_collisions: reads the file from the plan's root paths as_makefile_follow_updoes, so theCONTRACTS_DIRline and the collision line of one plan name the same file.templates/assembly-root/AGENTS-shape.md: saysmake contracts-dirprints the block wherevermakereads the shape's Makefile, names the three files in the order the code reads them, and says that where aGNUmakefileormakefileis kept, beside aMakefileor instead of one,makereads it, and not the shape's Makefile unless that file includes it. It is one verbatim copy: the digest in every project'scontracts/shape-pin.yamlis rendered from its bytes at adopt and scaffold time, and no digest ortree_sha256of it is stored in this repository, so there is no pin to move here; each project re-pins throughupdate-shape.pyas for any template change.tests/test_pinned_copy_text.py:ADOPTED_SENTENCESholds the new text.tests/test_adopt_collision_make_reads.py(new, 31 rows): the follow-up per kept set, the two lines of one plan naming one file, no collision line where noMakefileis kept, the two name tuples equal, the pinned file naming every file the code reads first, the script-aside tail wheremakereads another file, and, in a real adoption's clone,make -nheld to both sentences for each kept name and for a keptGNUmakefileormakefilethat ends ininclude Makefile.Fix round 1 (RV1105's review,
b28f40de0on top of5a4de7f76)make"only onceXholds them, or includes a Makefile that does, and not with that merge alone". The last clause is untrue whereXalready ends ininclude Makefile: the merge alone brings them. The clause is gone, and the docstring paragraph, theSHAPE_MAKE_TARGETScomment and theADOPTED_SENTENCEScomment now say the merge alone brings the targets only where that file includesMakefile.execute's NEXT printspython3 scripts/bootstrap.pywhere GNU make at the root reads a keptGNUmakefileormakefile, and its NOTE names that file and what it takes formake bootstrapto run there (#1047) #1121's NOTE: "has them", and "is what the shape'smake bootstrapruns"._next_stepsis not touched.reads_firstis set, said "neithermake bootstrapnorpython3 scripts/bootstrap.pyruns the shape's bootstrap before both merges are made", which reads as ifmake bootstrapruns it after them. It now says neither runs it until the script is merged, and thenpython3 scripts/bootstrap.pyruns it. I did not keep "before both merges": whereXis given the targets itself, as the sentence before invites,make bootstrapruns the shape's bootstrap once the script is merged, with noMakefilemerge at all.ADOPTED_SENTENCESchanges with it. No other copy or regex intests/pins that sentence (git grepfor "never the shape" and "reads the first of them").collision_follow_updocstring no longer saysexecutereports through it. It saysplanpredicts the follow-ups and stores them infollow_ups:, the split commit carries those,executenames its collisions by path only (itsbesidelines and the "WRITTEN BESIDE" list), andMaterialized.follow_ups()has no caller. The method stays.Rows of this round:
tests/test_adopt_collision_make_reads.pyandtests/test_pinned_copy_text.py, 118 rows. With this round's test files on5a4de7f76's code, 17 fail and 101 pass; onb28f40de0all 118 pass. The 17 fail for the right reason:5a4de7f76Makefile+makefile,Makefile+GNUmakefile, all three,Makefile+GNUmakefile+scripts)GNUmakefile,makefile)GNUmakefile,makefile)make -n bootstrapexits 2 before the merge and 0 after it alone (both asserted first, and passing), then the split message holds "not with that merge alone"test_a_scaffolded_root_is_told_its_makefile_exports_noneMutants of this round, each run against the 118 rows on
b28f40de0, each file restored and its sha256 checked:not in messageinverted toin messagereturncode == 0invertedbootstraptarget runs" put backGate on
b28f40de0:test_adopt_collision_make_reads,test_pinned_copy_text,test_adopt_make_reads_first,test_adopt_contracts_dir,test_adopt_printed_sentences,test_adopt_e2e,test_repo_hygiene,test_scaffold_leg_path_collisionstest_shape_doctor, 8 shards (62 + 56 + 54 + 56 + 60 + 57 + 58 + 45 collected)Merge-tree:
b28f40de0against main4ed7c7bf5is clean, and so is it against each open PR that touches these files: #1104, #1106, #1118, #1119, #1120, #1121 (1c5173758), #1122, #1123 and #1125.Round 0 (
5a4de7f76, main6da757cbemerged in): on main's code (plus the constant alone), 12 of the then 27 rows failed and 15 passed; the 30 test files that run the changed code or read the template gave 2109 passed, 0 failed and 4 skipped (3 PowerShell rows intest_shape_doctor.py, 1 MedxEHR row intest_adopt_plan.pythat needs a cloneSHAPE_MEDXEHR_CLONEnames); nine mutants each failed 1 to 11 of the then 114 rows.Not here
Makefileholds the shape's block back, and leave out a keptmakefileorGNUmakefile, which GNU make reads first #1048, the_next_stepsNOTE inadopt-project.py, is covered by the change adopt-project.py:execute's NEXT printsmake bootstrapwhere GNU make at the root reads a keptmakefileorGNUmakefile, which has nobootstraprule #1047 makes to it: see adopt-project.py:execute's NEXT printsmake bootstrapwhere GNU make at the root reads a keptmakefileorGNUmakefile, which has nobootstraprule #1047 and adopt-project.py:execute's NEXT printspython3 scripts/bootstrap.pywhere GNU make at the root reads a keptGNUmakefileormakefile, and its NOTE names that file and what it takes formake bootstrapto run there (#1047) #1121.templates/assembly-root/AGENTS-shape.mdline 19 and the five-commands table saymake bootstrapwithout this caveat. They say nothing about a kept file, so they are outside shape_materialize.py and the pinned AGENTS-shape.md: two sentences say a keptMakefileholds the shape's block back, and leave out a keptmakefileorGNUmakefile, which GNU make reads first #1048's two sentences.Lane 4 lands nothing outside v1.2.0 until after the v1.2.0 tag, on Brett's later word, so this is a draft.
🤖 Generated with Claude Code
Lane: openreposhape-4 (openRepoShape-4)
Summary by Sourcery
Correct adoption guidance and validation for roots that keep multiple makefile names.
Bug Fixes:
Enhancements:
Documentation:
Tests: