Skip to content

shape_materialize.py: env_commit's docstring says the split commit'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

Merged
brettheap merged 3 commits into
mainfrom
fix/1018-env-commit-docstring
Nov 8, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-5 (openRepoShape-5)
Claim: #1018 (comment)

Goal

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.

What still held on main

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.

What changed

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:

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.

Rows and mutants

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.

Runs

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.

Merge-trees

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.

Round 2 (rv1104's two NITs)

Closes #1018

🤖 Generated with Claude Code

Summary by Sourcery

Update env_commit documentation to accurately describe how non-UTF-8 path bytes appear in split commit messages.

Enhancements:

  • Clarify env_commit documentation to accurately describe escaped path rendering for non-UTF-8 bytes in split commit messages while preserving the distinction from raw message encoding.

Documentation:

  • Correct the env_commit docstring to reflect current _shown behavior and distinguish escaped paths from ? replacement during commit encoding.

…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)

`env_commit`'s docstring said a `root` or `drop` entry holding a byte that is not UTF-8 is listed in the split commit's message "with a `?` in place of that byte, a path that does not exist". That was the behaviour before #900 (`caf?.md`). Since #900, `adopt-project.py`'s `_split_message` lists each path as `plan`'s report names it (`_shown`), so the byte 0xE9 reads `caf\udce9.md` between two backticks, with no surrogate in the message and no `?` in it.

The docstring now says that, and keeps what stays true of `env_commit`: it writes a lone surrogate it is handed as `?` and raises nothing after both legs are pushed. Measured on main 2a119f2 with `cat -A` through `env_commit` and a real commit: the split message for `caf\udce9.md` reads `  `caf\udce9.md`$`, and a message holding the surrogate itself reads `Name: caf?.md$`.

The docstring is the only change: the module's code is the same AST with its docstrings blanked. `scripts/shape_materialize.py` is no pinned copy (no project carries it, `FAMILY_COPIED_VERBATIM` and the assembly copies do not list it), so no pin or digest moves.

Closes #1018

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

sourcery-ai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Implementation

Correct env_commit’s docstring to distinguish _shown’s escaped paths in split messages from ? replacement when a raw lone surrogate is encoded.

  • scripts/shape_materialize.py L2448-2450, L2456-2466

Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

Independent review of #1104 (#1018) at e32b2703, for lane openRepoShape-5: READY

Findings: no MUST, no SHOULD.

Checked and true

  1. What the code does. My own probe, $Q/v118/rv1104-1107/probe1104.py, ran offline (GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null). It calls adopt-project.py's own _split_message and _shown, then the head's env_commit into a real commit, read back with git cat-file commit HEAD.

    • Same result on git 2.43.0 and 2.47.1. git --version was printed under the same PATH env_commit used.
    • The split message:
      • _shown('caf\udce9.md') gives `caf\udce9.md`.
      • The drop entry and the root entry are each stored as the line `caf\udce9.md`.
      • The stored message holds no ?, no raw 0xE9 and no ED B3 A9. The message string holds no surrogate.
      • A surrogate outside the not-UTF-8 range, x\ud800.md, is escaped as well.
    • A message holding the lone surrogate, handed straight to env_commit, is stored as Name: caf?.md and Name: x?.md. Python's encode('utf-8', 'replace') gives b'caf?.md'.
    • On the predicted merge with 6da757cb (git 2.43) the probe gives the same results.
    • The docstring at run time holds no character from U+D800 to U+DFFF. It holds the six characters \udce9 and the span caf\udce9.md . Its longest line is 77 characters.
    • The module compiles under -W error. The added lines are ASCII, at most 75 wide.
    • Read from the code:
      • _listed_lines writes every spec, code, drop and root path through _shown, and follow-ups go through _shown too.
      • A surrogate is not printable, so it is always escaped.
      • entry_path_problem refuses one in an extracted path, through _misread and _refused_by_policy.
      • So "never raw" holds.
  2. Only the docstring changed.

    • With every docstring blanked, the module's AST at the head equals 16f9a0e81's (both dumps 383658 characters). The only docstring that differs is env_commit's.
    • The predicted merge against 6da757cb gives the same result (384424 each).
    • The diff is 1 file, 11 lines added and 7 removed. The merge differs from 6da757cb by exactly those lines.
  3. Nothing pins the old or new words.

    • I grepped the tree for 9 phrases from the old and new text. They appear only in the docstring itself.
    • Other hits for "a path that does not exist" and "never raw" are unrelated sentences, for example adopt-project.py:2041 and tests/test_windows_paths.py:18.
    • No test reads env_commit.__doc__ or uses getdoc.
    • The file is not a pinned copy:
      • COPIED_FROM_SHAPE lists repo_shape.py, validate-repository-naming.py and contracts/repository-naming.yaml.
      • COPIED_VERBATIM, FAMILY_COPIED_VERBATIM and FAMILY_COPIED_FROM_SHAPE do not list it.
      • No templates/*/scripts/shape_materialize.py exists.
      • validate-pins.py:2139 says "which no project carries".
    • The issue's premise that it is a pinned copy is wrong. The writer read it correctly.
  4. Merges (git merge-tree --write-tree):

    against result
    6da757cb clean, tree de31d85748568d1f4e1b56e2a131be3e8e82359d
    16f9a0e81 clean, tree eabbf449 (the head's own tree)
    shape_materialize.py, adopt-project.py and bump-leg.py: a tag a clone holds without its commit is one it cannot answer for, check says a commit it cannot ask was not asked, and a pinned product YAML 1.1 or 1.2 reads as no text is quoted (#912) #1090's old head d5389eb6 clean, tree 1f39bd41 (the body's figure)
    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 5a4de7f7 clean, tree 195ffe19
    tests: the bump-leg.py and family.py reachability rows tag a descendant of the commit --to names, so a tag above it reads as one that reaches it and a tag below it keeps the plain words, and --points-at for --contains no longer survives (#1019) #1107 clean, tree f3d4db3b
  5. Title and body.

  6. Run: the trio on the predicted merge 8167bccd (e32b2703 + 6da757cb), git 2.43, through run9.sh: 170 passed in 52.98s, rc=0.

  7. Sonar (SonarCloud read): gate OK, 0.0% new duplication, 0 open issues.

Could not verify

  • The shard results (2873 passed, 4 skipped; 4611 passed, 31 skipped) and the 170 at e32b2703 itself. I read them in the writer's logs (each ends rc=0) but did not re-run them.
  • The list of 8 open PRs at the time the body was written. I saw only today's 12.
  • git 2.55 (CI).
  • Which head Sonar analysed.

(Posted by lane openRepoShape-5's coordinator. rv1104-1107 is lane 5's independent opus reviewer; it wrote none of #1104. The PR stays a DRAFT until the v1.2.0 tag; its NITs are taken before it leaves draft.)

Lane: openreposhape-5 (openRepoShape-5)

… names an entry path as the source spells it only where the path is printable, since #900 spells any other as an escape, which the next paragraph says (#1018)

rv1104's NIT 1. The paragraph above the one the first commit reworded still 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 path through `_shown`, as the escape the next paragraph describes. The sentence now says "(a printable one; see below)".

Docstring only: the module's AST with its docstrings blanked is the same as the previous commit's. No test, README, doc, handbook or AGENTS.md text holds these words.

Part of #1018

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

Copy link
Copy Markdown
Contributor Author

Re-check of #1104 (#1018) at de89aa81, for lane openRepoShape-5: READY

Findings: none. NIT 1 and NIT 2 are both fixed, and what the fixes say is true.

What changed and what I checked

Could not verify

  • The trio's 170 passed at de89aa81. I did not re-run it: the change is one docstring sentence and the AST is identical. Round 1 measured 170 on the merge.
  • git 2.55 (CI).

(Posted by lane openRepoShape-5's coordinator. rv1104-1107 is lane 5's independent opus reviewer; it wrote none of #1104. The PR stays a DRAFT until the v1.2.0 tag.)

Lane: openreposhape-5 (openRepoShape-5)

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

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

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

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

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

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

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

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

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

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

**`scripts/shape_materialize.py`**

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes #1100

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

sonarqubecloud Bot commented Nov 8, 2026

Copy link
Copy Markdown

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

sourcery-ai Bot commented Nov 8, 2026

Copy link
Copy Markdown

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

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

@brettheap
brettheap merged commit 0d5704b into main Nov 8, 2026
5 checks passed
@brettheap
brettheap deleted the fix/1018-env-commit-docstring branch November 8, 2026 15:03
brettheap pushed a commit that referenced this pull request Nov 8, 2026
Main 0d5704b is five commits past #1144's squash b1f45fa (#1169, #1223,
#1166, #1133, #1104). This is the third of the three merges that restack
#1159 after #1144 landed.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Nov 8, 2026
…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 or includes a Makefile that does (#1048) (#1105)

Two sentences said a kept `Makefile` is what holds the shape's block back. GNU make reads the first of `GNUmakefile`, `makefile` and `Makefile` it finds, and only that one, so where the root also keeps `GNUmakefile` or `makefile`, `make` reads that file, and reaches the merged `Makefile` only where that file includes it.

**Measured** (a real `execute`, then `make -n` in the clone, for each kept name; r1046's NIT-B, repeated; the include rows are RV1105's SHOULD-1):

| the source keeps | file `make` reads | after `cat shape/Makefile >> Makefile`, `make -n bootstrap` |
|---|---|---|
| `Makefile` | `Makefile` | exit 0 |
| `Makefile` + `makefile` | `makefile` | exit 2, "No rule to make target 'bootstrap'" |
| `Makefile` + `GNUmakefile` | `GNUmakefile` | exit 2, same |
| all three | `GNUmakefile` | exit 2, same |
| `Makefile` + a `makefile` or `GNUmakefile` that ends in `include Makefile` | that file | exit 0, runs `python3 scripts/bootstrap.py` (exit 2 before the merge) |
| `makefile` or `GNUmakefile` alone | that file | no merge to make: the shape's `Makefile` is the root's, and `make contracts-dir` has no rule until that file holds the block or includes `Makefile` |

**What changed**
- `scripts/shape_materialize.py`, `collision_follow_up`: takes `reads_first`, the file GNU make reads before the shape's `Makefile`. 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 reach `make` there only once that file has them, or includes a Makefile that does; until then `python3 scripts/bootstrap.py` is what the shape's `make bootstrap` runs. 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. Where the root keeps only the source's `Makefile`, the sentence is what it was, byte for byte. `GNU_MAKE_READS` joins `SHAPE_MAKE_TARGETS`.
- `adopt-project.py`, `_predict_collisions`: reads the file from the plan's root paths as `_makefile_follow_up` does, so the `CONTRACTS_DIR` line and the collision line of one plan name the same file.
- `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 that where a `GNUmakefile` or `makefile` is kept, beside a `Makefile` or instead of one, `make` reads it, and not the shape's Makefile unless that file includes it. It is one verbatim copy: the digest in every project's `contracts/shape-pin.yaml` is rendered from its bytes at adopt and scaffold time, and no digest or `tree_sha256` of it is stored in this repository, so there is no pin to move here; each project re-pins through `update-shape.py` as for any template change.
- `tests/test_pinned_copy_text.py`: `ADOPTED_SENTENCES` holds 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 no `Makefile` is kept, the two name tuples equal, the pinned file naming every file the code reads first, the script-aside tail where `make` reads another file, and, in a real adoption's clone, `make -n` held to both sentences for each kept name and for a kept `GNUmakefile` or `makefile` that ends in `include Makefile`.

**Fix round 1** (RV1105's review, `b28f40de0` on top of `5a4de7f76`)
- **SHOULD-1:** the follow-up said the targets reach `make` "only once `X` holds them, or includes a Makefile that does, and not with that merge alone". The last clause is untrue where `X` already ends in `include Makefile`: the merge alone brings them. The clause is gone, and the docstring paragraph, the `SHAPE_MAKE_TARGETS` comment and the `ADOPTED_SENTENCES` comment now say the merge alone brings the targets only where that file includes `Makefile`.
- **NIT-1 (and RV1121's NIT-3):** the follow-up now uses the words of #1121's NOTE: "has them", and "is what the shape's `make bootstrap` runs". `_next_steps` is not touched.
- **NIT-2:** the script-aside tail, where `reads_first` is set, said "neither `make bootstrap` nor `python3 scripts/bootstrap.py` runs the shape's bootstrap before both merges are made", which reads as if `make bootstrap` runs it after them. It now says neither runs it until the script is merged, and then `python3 scripts/bootstrap.py` runs it. I did not keep "before both merges": where `X` is given the targets itself, as the sentence before invites, `make bootstrap` runs the shape's bootstrap once the script is merged, with no `Makefile` merge at all.
- **NIT-3:** the template's "and never the shape's Makefile" is now ", and not the shape's Makefile unless that file includes it". `ADOPTED_SENTENCES` changes with it. No other copy or regex in `tests/` pins that sentence (`git grep` for "never the shape" and "reads the first of them").
- **NIT-4:** the `collision_follow_up` docstring no longer says `execute` reports through it. It says `plan` predicts the follow-ups and stores them in `follow_ups:`, the split commit carries those, `execute` names its collisions by path only (its `beside` lines and the "WRITTEN BESIDE" list), and `Materialized.follow_ups()` has no caller. The method stays.
- NITs 5 to 8 are left to the lane.

**Rows of this round:** `tests/test_adopt_collision_make_reads.py` and `tests/test_pinned_copy_text.py`, 118 rows. With this round's test files on `5a4de7f76`'s code, 17 fail and 101 pass; on `b28f40de0` all 118 pass. The 17 fail for the right reason:

| rows | why they fail on `5a4de7f76` |
|---|---|
| 4 collision rows (`Makefile+makefile`, `Makefile+GNUmakefile`, all three, `Makefile+GNUmakefile+scripts`) | the line holds "not with that merge alone" |
| 2 new script-tail rows (`GNUmakefile`, `makefile`) | the line holds "before both merges are made" |
| 2 new include rows in a real clone (`GNUmakefile`, `makefile`) | `make -n bootstrap` exits 2 before the merge and 0 after it alone (both asserted first, and passing), then the split message holds "not with that merge alone" |
| 7 clone rows, the pinned-file row and `test_a_scaffolded_root_is_told_its_makefile_exports_none` | the pinned file still says "and never the shape's Makefile" |

**Mutants of this round**, each run against the 118 rows on `b28f40de0`, each file restored and its sha256 checked:

| id | mutant | rows failing | result |
|---|---|---|---|
| MA | ", and not with that merge alone" put back | 6 | killed |
| MB | the script-aside tail put back ("before both merges are made") | 3 | killed |
| MC | the template's "and never the shape's Makefile" put back | 9 | killed |
| MD1 | include rows: `not in message` inverted to `in message` | 2 | killed |
| MD2 | include rows: after the merge, `returncode == 0` inverted | 2 | killed |
| ME | "holds them" put back | 6 | killed |
| MF | "the shape's `bootstrap` target runs" put back | 5 | killed |

**Gate** on `b28f40de0`:

| files | passed | failed | skipped |
|---|---|---|---|
| `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_collisions` | 470 | 0 | 0 |
| `test_shape_doctor`, 8 shards (62 + 56 + 54 + 56 + 60 + 57 + 58 + 45 collected) | 445 | 0 | 3 (PowerShell rows, Windows host only) |
| **total** | **915** | **0** | **3** |

**Merge-tree:** `b28f40de0` against main `4ed7c7bf5` is 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`, main `6da757cbe` merged 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 in `test_shape_doctor.py`, 1 MedxEHR row in `test_adopt_plan.py` that needs a clone `SHAPE_MEDXEHR_CLONE` names); nine mutants each failed 1 to 11 of the then 114 rows.

**Not here**
- The third box of #1048, the `_next_steps` NOTE in `adopt-project.py`, is covered by the change #1047 makes to it: see #1047 and #1121.
- `templates/assembly-root/AGENTS-shape.md` line 19 and the five-commands table say `make bootstrap` without this caveat. They say nothing about a kept file, so they are outside #1048's two sentences.

Closes #1048

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

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

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

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

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

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

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

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

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

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

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

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

The checks the issue's Verify lists:

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes #1238

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

shape_materialize.py: env_commit's docstring still says the split message names a byte that is not UTF-8 with a ?, which #900 replaces with an escape

1 participant