Skip to content

bootstrap.py: a missing tracking branch is not made while any refs/heads/ branch, packed or loose, folds to it, and a twin folds in NFC first, so an NFD name moves no leg, and the way out renames the twin, HEAD's twin included (#962) - #984

Merged
brettheap merged 7 commits into
mainfrom
fix/962-create-twins
Oct 19, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

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

Follows #948 (#896), which landed as ebbd9fa4.

Head f1cceddf: 414e9e0a (round 0, on #948's head 45415296), 5a435acd (the merge of origin/main ebbd9fa4, #948's squash) and f1cceddf (fix round 1, on rv984's review of 5a435acd). The squash is not an ancestor of this branch, so the merge conflicted in bootstrap.py in seven hunks, and this branch's side was taken whole: main's bootstrap.py (blob e31385cd) is 45415296's byte for byte, so nothing of main's was dropped. Against ebbd9fa4 the branch is three files, templates/assembly-root/scripts/bootstrap.py, tests/test_bootstrap_casefold_twin.py (three tests changed) and the new tests/test_bootstrap_create_twins.py (+737, -68). git merge-tree --write-tree of the head against origin/main c4dd19dc is clean. One file of templates/ changes.

A pinned copy: every project's bootstrap.py takes an upstream-changed row, and re-pins it.

What #948 left (rv896's review of #948, filed as #962)

#948 leaves a leg alone when HEAD's own ref is a TWIN of the tracking branch, a branch that folds to it on a disk that folds case (Main for main). Two things were not covered:

  1. Only HEAD's ref was asked. create_at_pin asked about HEAD's ref, so a leg DETACHED AT THE PIN over a packed Main, and a leg on an unborn Main beside a packed MAIN, ran checkout -b main <pin> with no refusal. On a disk that folds case that writes the loose file Main and main are both read from. What such a disk then does to the packed Main is reasoned from git's files backend (rv984), and not measured on APFS or NTFS: the packed entry stays, for-each-ref goes on listing it at its old sha while rev-parse Main reads the new file main, and a later git branch -D Main unlinks that file. The commit is still in a ref; its name is shadowed, and the state is fragile.
  2. The fold was casefold() alone. APFS also reads the NFC and the NFD spelling of a name as one file, so a leg on the NFD spelling of caf and U+00E9 is a twin that _folded_twin did not see: bootstrap fast-forwarded the NFC branch under HEAD's feet and the index was left at the old commit (D NEW-CONTRACT.md;M README.md), the same harm as bootstrap.py: on a disk that folds case, a leg on Main whose tracking branch main is behind the pin is moved under its own HEAD, and bootstrap says it is on main now over an index that undoes the pin #896's.

Item 1: every spelling that folds, before checkout -b

create_at_pin asks git for-each-ref refs/heads/ (packed refs are listed too) for a branch that is a twin of the tracking branch (_folded_spelling), just before the checkout -b and after every refusal that was already there. If one is found, _say_spelling leaves the leg and prints three lines: where the leg is and the pin's sha, why it is left, and the way out. Nothing is moved and nothing is made.

Item 2: NFC, then casefold()

_fold(name) is unicodedata.normalize("NFC", name).casefold(), used by _folded_twin and so by all three step (a) paths, and by item 1. A name held as surrogate escapes (#704) passes through normalize as the text it is and raises nothing (a test). NFKC is not used: APFS does not fold compatibility forms, and main with U+00B2 stays no twin of main2 (two rows, and the NFKC mutant is killed).

_twin_words words the refusal by what the twin differs in. A twin that is one under casefold() alone prints #896's words byte for byte (only in case; a disk that folds case (macOS, Windows)). Any other says it differs only in case or Unicode form, and that a disk that folds case or Unicode forms (macOS folds both, Windows folds case) reads the two as one, because Windows folds case only and the words must not say it folds Unicode forms.

Fix round 1: the way out renames the twin (rv984's SHOULD 1, and #948's HEAD-twin keep line)

This replaces round 0's _kept_elsewhere, a departure from the brief that rv984 found breaks #962's Goal, and lane 5 ruled to follow the Goal. Round 0 spared a twin whose commit was an ancestor of the pin, or was held by another branch, a tag, a remote-tracking ref or the stash, and its way out kept the commits on another branch and ran again. rv984 measured that in each case that made main, a folding disk would read Main as the pin: the Goal says bootstrap never makes, with checkout -b, a ref that a folding disk reads as a branch that already exists, and these cases broke its letter. The loop the sparing avoided is real only for a way out that leaves the twin where it is: #948's keep line ("keep any commit ... check out the pin") leaves Main in place, so the next run refuses again. A way out that REMOVES the twin ends it.

What it costs: several twins are named one at a time, each rename one run. A name is chosen when the line prints; if something makes it first, git refuses the rename and the person picks another. The cost of the old sparing, a shadowed name on a folding disk, is gone with it.

Before and after, the four states (git 2.43.0, the pasted line run through /bin/sh)

At 5a435acd and at f1cceddf. A packed Main holding a commit, main missing:

detached over a packed Main, at 5a435acd:
  [spec] spec: branch main does not exist (the pin is <pin>), and the leg has the branch Main, which differs from main only in case
          ... the leg is left as it is
          keep the commits Main holds on a branch whose name differs from main by more than case: `git -C spec branch <another name> Main`, and run bootstrap again.
at f1cceddf, the third line is:
          rename Main to a name that differs from main by more than case, its commits staying on it: `git -C spec branch -m Main Main-kept`, and run bootstrap again.
  run 2 after the pasted line: [spec] spec: branch main created at <pin> - at the pin
  after: HEAD -> refs/heads/main, heads: Main-kept (the commit), main (the pin)

the same, the commit held by a tag (rv984's SHOULD 1 case):
  at 5a435acd: [spec] spec: branch main created at <pin> - at the pin, with Main left in packed-refs
  at f1cceddf: the refusal above, and `Main-kept` after the pasted rename

HEAD on the packed Main (#948's keep line):
  at 5a435acd: "not moving the checkout off it. Keep any commit made there on a branch whose name differs from main by more than case, check out the pin in the leg, and run bootstrap again."
  at f1cceddf: the rename line, then HEAD -> refs/heads/Main-kept; run 2: branch main created at <pin>, "the leg was on Main-kept, which keeps its commits; it is on main now"

HEAD on an unborn Main beside a packed MAIN:
  at f1cceddf: run 1 names Main (rename Main Main-kept; HEAD goes with it); run 2 names MAIN (rename MAIN MAIN-kept); run 3 makes main, "the leg was on Main-kept, a branch with no commit yet"

The NFD twin of an NFC tracking branch, T7, is as in round 0: at 45415296 bootstrap fast-forwarded caf and U+00E9 under HEAD (D NEW-CONTRACT.md;M README.md); at this head the leg is left, switch is printed, and nothing moves.

Tests

tests/test_bootstrap_create_twins.py, 31 tests. It uses #948's emulation and helpers (test_bootstrap_casefold_twin), main_of (test_bootstrap_leg_moves), head_bytes (test_bootstrap_non_utf8_branch) and pasted (test_bootstrap_pasteable_lines); the lines are compared exactly, and every way out is run as printed through /bin/sh.

  1. A leg detached at the pin over a real spelling Main, packed and loose (2): the lines exact, nothing moved, the rename run as printed, the next run makes main, and the commit is on Main-kept.
  2. A leg ON a packed Main with main missing: the rename moves HEAD, and the next run makes main and says the leg was on Main-kept.
  3. An unborn Main beside a packed MAIN: each twin named in turn and renamed, the third run makes main.
  4. Two twins, MAIN and Main: the second rename is Main-kept-2, not Main-kept, which differs from MAIN-kept only in case.
  5. A twin is a reason to leave however its commit is held (5 rows: the pin alone, another branch, a tag, a remote-tracking ref, the stash).
  6. mains is no twin: main is made, as it was.
  7. The NFD T7 test, and a missing branch beside its NFD spelling, left and renamed.
  8. _kept_name (4 rows, and the NFC/NFD one), and a shell reads each word of the rename as the bytes of its names (4 rows, a byte that is not UTF-8 and a dash-led path included).
  9. _folded_twin over Unicode forms (8 rows, with main and U+00B2 against main2, and U+2460 against 1, expecting False), and the fold raises nothing on a name that is not UTF-8.

At 5a435acd, with these test files copied in: 23 failed, 40 passed. 20 of the 31 tests of the new file fail there (cases 1 to 5, the NFD missing-branch test, the five _kept_name tests and the four shell rows) and the three changed tests of the casefold file fail (the unborn twin, the packed twin, the non-UTF-8 lines). The 11 that pass there pass by design: mains, the NFD T7 test, the eight _folded_twin rows (the two NFKC rows guard a mutant, not a base fault) and the fold test. At this head: 63 passed.

Mutants (each in a scratch tree, against the new file and the casefold file; none survives)

mutant failed
the check dropped from create_at_pin (item 1) 10
_fold: casefold() alone, no NFC 6
_twin_words: always the case words 2
_say_spelling names the branch, not the twin 14
_fold: NFKC in place of NFC (rv984's O1) 2 (the two new rows)
the printed branch -m is a plain branch (a copy, not a rename) 17
the rename's two arguments swapped 17
_kept_name never loops (always -kept) 4
_kept_name compares byte for byte, with no fold 3
_kept_name folds the taken names but not the candidate 3
the HEAD-twin line says the leg has the branch for the checkout is on 4
the HEAD-twin check dropped from create_at_pin 4
the sparing put back (a twin another ref holds is left alone) 5
the new name not _paste_worded (spelled(shlex.quote(...))) 2
the twin not _paste_worded 2
_heads lists loose refs only 13

Round 0's four mutants of _kept_elsewhere (the ancestor rule, the holders, heads-only holders, the first twin) are gone with the code.

Complexity, and what is not touched

Cognitive complexity (S3776, counted from the ast): create_at_pin 14, fast_forward_to_pin 15 (unchanged), place_at_pin 6, _folded_spelling 3, _folded_twin 3, _heads 1, _kept_name 1, _twin_words 1, _say_spelling 0, _say_twin 0. New lines are within 79 columns and ASCII; the docstrings spell the Unicode forms (cafe and U+0301, caf and U+00E9). _paste_word (#973 edits it) and the NOT INITIALIZED line (#958 moves it) are not touched.

Runs

At the tree of f1cceddf (the head), git 2.43.0: the new file, test_bootstrap_casefold_twin.py, test_bootstrap_pasteable_lines.py, test_bootstrap_leg_moves.py, test_repo_hygiene.py, test_agents_md_codes.py, test_cli_reference.py and test_pinned_copy_text.py, 392 passed. git 2.47.1: the new file and the casefold file, 63 passed. Output that is not a twin's: a differential of 27 states (the tracking branch missing, at the pin, behind it, ahead of it, by HEAD on main, detached at the pin, detached elsewhere, on feature, on an unborn feature, on a ref outside refs/heads/, on mains), the leg and the printed lines read after each run: byte for byte the same at ebbd9fa4 and at this head. The rest of the suite is left to CI's tests job.

Merge checks

git merge-tree --write-tree of this head against: origin/main c4dd19dc clean; fix/961-paste-words (#973, b2887cf5) clean; fix/430-d-assembly-root-guards (#958, 0ef875c0) and slice D's round 1 (wip/l5-430d-r1-20261018, b49f34e1) one conflict each, the import block of bootstrap.py (import unicodedata here against from collections.abc import Callable, Iterator there). The resolution keeps import unicodedata and slice D's from collections.abc import Callable, Iterator, and does not keep both collections.abc lines, which would be a duplicate import (rv984's NIT 2). Round 0's scratch merges of those two (95 and 222 passed) were not re-run for round 1, whose changes lie below the import block.

Known limits

  • A real APFS or NTFS disk is not available in CI: the shadowing a folding disk does to a packed twin is reasoned from git's files backend, not measured. The tests emulate the fold with a symlink, and run the real names where the disk keeps case.
  • Several twins are named one at a time, and each rename takes one run.

Closes #962

🤖 Generated with Claude Code

Brett Heap and others added 6 commits October 18, 2026 15:02
…racking branch only in case (`Main` for `main`) is left where it is, so on a disk that folds case `branch -f main` no longer moves the ref HEAD stands on (#896)

The assembly root's pinned bootstrap.py compared HEAD's ref with
`refs/heads/<branch>` byte for byte, in `fast_forward_to_pin` (T7),
`place_at_pin` (P9) and `create_at_pin`. A disk that folds case (macOS APFS,
Windows NTFS, by default) reads `refs/heads/Main` and `refs/heads/main` as one
loose file, so `git switch Main` in a leg whose branch is `main` puts HEAD on
the file `branch -f main <pin>` moves. T7 called that leg "on another branch",
moved `main` under HEAD, found HEAD already at the pin on `checkout main`,
printed `fast-forwarded` and `it is on main now`, and left the index and the
work tree at the old commit: the next commit reverted the pin's changes.

`_folded_twin(current, branch)` says whether HEAD's ref is a TWIN of the
branch: under `refs/heads/`, equal under `str.casefold()` and different in
bytes. A detached HEAD and a ref outside `refs/heads/` are never one. Each of
the three paths asks it first and, for a twin, `_say_twin` prints three lines
and no ref moves or is made: where the leg is, why (a disk that folds case
reads the two as one branch, and what that could do), and the way out. T7 and
P9 give `git -C <leg> switch <branch>`, printed as `move_back_to_pin` prints
its command; on a disk that folds case it re-spells HEAD and moves no file,
and the next run takes the on-branch `merge --ff-only` or the no-op checkout.
For a branch that is not there that command has nothing to switch to on a disk
that keeps case, so the line says to keep the commits on a branch whose name
differs by more than case, check out the pin and run bootstrap again: on a disk
that folds case, `checkout -b main` writes the file `Main` reads and hides the
commits `Main` holds, packed or not, behind it. The three path functions take
the leg's `path` for the command, and each grows by one `if` (cognitive
complexity 14, 6 and 12). Every other state prints what it printed.

tests/test_bootstrap_casefold_twin.py emulates a disk that folds case with a
symlink `.git/refs/heads/Main -> main` (CI is Linux only) and holds each path,
the way out run as printed, a disk that keeps case with a real `Main`, the
unchanged states, and the helper's names, a surrogate-escaped one included.

bootstrap.py is a pinned copy: every project's takes an `upstream-changed` row
and re-pins it.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ame the commits "a branch spelled like main" holds, and the docstrings say what making a branch hides, so no line over an unborn twin is false (#896)

Part 1 of the fix round on #948, from rv896's review (issuecomment-6252823490),
which found the change READY with four NITs that need nothing from #944.

NIT 1. `TWIN_RISKS["make"]` said making the branch could hide "the commits
Main holds"; an unborn twin holds none. It now says "the commits a branch
spelled like main holds", which is true of a packed twin, an unborn twin and a
third spelling beside either.

NIT 2. The missing-branch refusal told the person to check out the pin and
never printed it. Its first line now reads `branch main does not exist (the
pin is <sha12>), and the checkout is on Main, ...`. No refusal #896 did not
add is changed. The packed-twin test takes the sha from the printed line to
run the way out.

NIT 3. Two `_folded_twin` rows, `refs/notes/Main` and `refs/tags/xMain`
against `main`, both false. Each has 11 characters before `Main`, as
`refs/heads/` does, so a mutant that drops the `refs/heads/` guard and keeps
the 11-character slice answered true for both and survived the 24 tests; the
two rows kill it.

NIT 4. The module docstring says what each act could do: a branch MOVED as
`main` could be the ref HEAD stands on, with the index left at the old commit,
and one MADE as `main` could hide the commits the twin holds behind the new
ref. `_say_twin`'s docstring no longer says "packed or not": a loose `Main` on
a disk that folds case makes `refs/heads/main` resolve, so the missing-branch
path never runs for it.

The `switch` line and the docstrings that compare it with `move_back_to_pin`
are not touched: part 2 switches them to #944's `_paste_word` once #944 lands.
Cognitive complexity is unchanged: place_at_pin 6, fast_forward_to_pin 15,
create_at_pin 13, _folded_twin 3, _say_twin 1. (The first commit's message says
14, 6 and 12; those were a hand count that missed `why or []`.)

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…h and the branch with `_paste_word`, so a branch that is not UTF-8 pastes into a shell as the bytes it is (#896)

Part 2 of the fix round on #948, from rv896's SHOULD 1, now that #944 (#932)
has landed as 450279c and `_paste_word` is on main. The first commit's merge
of origin/main is the commit before this one.

`_say_twin` built its command with `spelled(shlex.quote(...))`, the form #932
replaced. For a branch named with a byte that is not UTF-8 that printed
`git -C spec switch 'a\udcffb'`, and `/bin/sh` reads that word as the eight
characters it holds, so the pasted command named no branch. The command is now
`git -C {_paste_word(path)} switch {_paste_word(branch)}`, as #944's hand-over
gave it: a name that prints as it is stays `shlex.quote`d, and one with a byte
that is not UTF-8 is a `printf` that prints the bytes. `_say_twin`'s docstring
and `test_the_way_out_is_quoted_for_a_shell_...` say so, where they said the
command was printed as `move_back_to_pin` printed it before #944.

Tests: one end-to-end case with a tracking branch `a<0xff>b` and HEAD on its
twin `A<0xff>b` (T7, `--branch`): bootstrap leaves the leg, the printed
command is `git -C spec switch "$(printf -- 'a\377b')"`, run through
`/bin/sh -c` as printed it puts HEAD on `refs/heads/a<0xff>b` byte for byte,
and the next run fast-forwards it. Five unit rows read the two words of the
command back through `/bin/sh` and compare them with the bytes of the names
(an ordinary pair, a path with the byte, a branch with the byte, both with a
leading dash, a path with a percent). Dropping `_paste_word` from the path,
from the branch, or restoring the old form each fails 3, 3 and 5 of the 32.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ds/` branch, packed or loose, that folds to it holds a commit nothing else holds, and a twin folds in NFC first, so no commit or leg is lost to an NFD name or a spelling HEAD is not on (#962)

Stacked on #948 (#896), whose rule is kept: a twin is only ever left in place,
never moved, and every output that is not a twin's is byte for byte what it
was. rv896's review of #948 found what #948 leaves.

Item 1, every spelling that folds. `create_at_pin` asked only about HEAD's own
ref (`_folded_twin`), so a leg DETACHED AT THE PIN over a packed `Main`, and a
leg on an unborn `Main` beside a packed `MAIN` (whose #948 way out, checking
out the pin, then ran the same checkout), ran `checkout -b main <pin>`. On a
disk that folds case that wrote the loose file `Main` and `MAIN` are read
from, and the commit they held was in no ref. Before the checkout,
`_spelling_at_risk` now asks `git for-each-ref refs/heads/`, which lists the
packed refs too, for a twin of the branch that holds a commit nothing else
would hold, and `_say_spelling` leaves the leg, naming it, with the pin's sha
and the way out: keep the commits on a branch whose name differs from the
branch by more than case or Unicode form (`git -C <leg> branch <another name>
<spelling>`), and run bootstrap again. `_kept_elsewhere` spares a twin whose
commit is the pin's ancestor, which the new branch holds, or which another
branch, a tag or a remote-tracking ref holds, so a kept commit is no reason to
refuse and the way out, and #948's way out, end in `main` made. The check is
after every refusal that was already there.

Item 2, NFC and NFD. `_fold` is NFC, then `casefold()`, in `_folded_twin` and
in item 1. APFS reads the NFC and the NFD spelling of a name as one file, so a
leg on the NFD spelling of `caf` and U+00E9 was moved under its own HEAD as
#896's was (`D NEW-CONTRACT.md;M README.md`). A twin that is one under
`casefold()` alone prints #896's words byte for byte; any other says it
differs `only in case or Unicode form` and that `a disk that folds case or
Unicode forms (macOS folds both, Windows folds case)` reads the two as one.

Cognitive complexity: create_at_pin 14, fast_forward_to_pin 15 (unchanged),
place_at_pin 6, the new helpers 4 or less.

tests/test_bootstrap_create_twins.py: the three states on the emulation of
#948's tests, the way out run as printed, real names on a disk that keeps case
(packed and loose), a twin that holds nothing the new branch hides, a twin
named among two, the NFD T7 run through `/bin/sh`, a missing branch beside an
NFD spelling, and the helper's rows. 17 tests; 10 fail on #948's head and 7
pass there by design. bootstrap.py is a pinned copy: every project re-pins it.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
#948 landed as ebbd9fa (tree 67a22e1 = main 3db58c4 + 4541529). The squash is
not an ancestor of this branch, so git saw both sides add #948's text and
bootstrap.py conflicted in seven hunks. This branch's side is taken whole: main's
bootstrap.py (blob e31385c) is 4541529's byte for byte, and this branch is
4541529 plus #962. The result differs from origin/main in two files only,
templates/assembly-root/scripts/bootstrap.py and tests/test_bootstrap_create_twins.py.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-18T20:09Z) @codex review

@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates bootstrap branch creation to avoid hiding commits held only by case- or Unicode-equivalent refs, using NFC-plus-casefold twin detection, preservation-aware risk checks, clearer platform-specific refusals, and extensive regression tests.

Sequence diagram for twin-aware bootstrap branch creation

sequenceDiagram
    participant Bootstrap
    participant Git
    participant Leg

    Bootstrap->>Git: for-each-ref refs/heads/
    Git-->>Bootstrap: packed and loose branch spellings
    Bootstrap->>Bootstrap: _folded_twin
    Bootstrap->>Git: for-each-ref --contains tip
    Git-->>Bootstrap: refs holding the commit
    alt risky twin found
        Bootstrap->>Leg: _say_spelling
        Bootstrap-->>Leg: leave branch and commits unchanged
    else no commit would be hidden
        Bootstrap->>Git: checkout -b branch pin
        Git-->>Bootstrap: branch created at pin
    end
Loading

Flow diagram for safe tracking-branch creation

flowchart TD
    A[create_at_pin] --> B[_spelling_at_risk]
    B --> C[for-each-ref refs/heads/]
    C --> D{Twin under _fold?}
    D -->|No| C
    D -->|Yes| E[_kept_elsewhere]
    E -->|Ancestor or held by another ref| C
    E -->|Commit would be hidden| F[_say_spelling]
    F --> G[Leave leg unchanged]
    C -->|No risky spelling| H[checkout -b branch pin]
Loading

File-Level Changes

Change Details Files
Prevent creation of a missing tracking branch when any case- or Unicode-equivalent branch would have its unique commits hidden.
  • Enumerate packed and loose refs under refs/heads/.
  • Identify the first risky twin whose tip is not retained by the pin, another branch, a tag, or a remote-tracking ref.
  • Leave the leg unchanged and print an actionable command to rename/preserve the risky spelling before retrying.
templates/assembly-root/scripts/bootstrap.py
Extend branch-twin detection and refusal messaging to normalize Unicode names before case folding.
  • Add NFC normalization followed by casefolding, including safe handling of surrogate-escaped names.
  • Differentiate case-only twins from twins differing in Unicode form and report platform-specific filesystem behavior.
  • Reuse shared refusal formatting for HEAD twins and unrelated risky spellings.
templates/assembly-root/scripts/bootstrap.py
Add comprehensive regression coverage for risky spellings, Unicode twins, preservation rules, and exact user-facing output.
  • Test packed and loose case variants, unborn branches, multiple twins, and commits retained elsewhere.
  • Test NFC/NFD branch handling, non-UTF-8 names, exact refusal text, and successful retry workflows.
  • Validate helper behavior across Unicode forms and ensure no branch, HEAD, index, or commit is moved when refusing.
tests/test_bootstrap_create_twins.py

Assessment against linked issues

Issue Objective Addressed Explanation
#962 Before creating a missing branch, inspect packed and loose branches under refs/heads/ and refuse if any name folds to the requested branch, naming the existing spelling. ❌ The PR checks all such branches, but refuses only when the branch's commit would otherwise be left in no ref. If the commit is an ancestor of the pin or is held by another branch, tag, or remote-tracking ref, it creates the requested branch despite the colliding spelling. The issue calls for refusing whenever any branch name folds to the requested name.
#962 Treat NFC and NFD spellings as twins by normalizing names to NFC before casefolding. ✅
#962 Preserve the existing rule that a twin branch is left in place rather than moved. ✅

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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-18T20:13:24.121295Z 5a435ac Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a435acdf7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

then `str.casefold()`ed (#962). APFS reads the NFC and the NFD spelling
of a name as one file, and a surrogate escape (a name that is not UTF-8,
#704) passes through `unicodedata.normalize` as the text it is."""
return unicodedata.normalize("NFC", name).casefold()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Normalize again after case folding

When a ref name combines case and canonical-form differences, case folding can itself produce a decomposed result: for example, Ϊ́ and ΐ fold here to ΐ and ΐ, respectively, which compare unequal even though they are canonically equivalent and alias on a case- and normalization-folding APFS volume. _folded_twin therefore misses the twin, allowing create_at_pin or fast_forward_to_pin to hide or move the aliased ref and potentially strand its commit or leave a stale index; normalize the case-folded result as well. templates/assembly-root/AGENTS-shape.mdL100-L102

Useful? React with 👍 / 👎.

@brettheap

Copy link
Copy Markdown
Contributor Author

Independent review of #984 (#962) at 5a435acd, for lane openRepoShape-5: READY

No MUST. #984 fixes everything #962 measured: probe (b), probe (c) and the NFD twin all do their harm at 45415296 and are left alone at the head. Every output that is not a twin's, and every case-only twin's, prints byte for byte what main ebbd9fa4 prints. There is one design question for you (SHOULD 1), on the _kept_elsewhere departure.

Findings

SHOULD 1 (decision). _kept_elsewhere makes the very ref #962's Goal forbids, and the loop it avoids has another way out.

  • Where: templates/assembly-root/scripts/bootstrap.py, in _kept_elsewhere and _spelling_at_risk.

  • What I measured at the head. I set up a packed Main holding a commit M that is not in the pin's history, with HEAD detached at the pin and no main:

    What else holds M Result
    only a tag bootstrap makes main
    only a remote-tracking ref bootstrap makes main
    only another branch bootstrap makes main
    only refs/stash bootstrap makes main
    nothing refused

    In each case that makes main, emulating the fold afterwards makes Main read as the pin. The Goal says bootstrap never makes, with checkout -b, a ref that a folding disk reads as a branch that already exists, so these cases break its letter.

  • The loop is real, but only for a way out that leaves the twin in place. bootstrap.py: a leg whose HEAD is on a branch that differs from the tracking branch only in case (Main for main) is left where it is, so on a disk that folds case branch -f main no longer moves the ref HEAD stands on (#896) #948's keep line ("keep any commit … check out the pin") leaves Main where it is, so the next run refuses again.

  • A different way out ends the loop and meets the Goal: git -C <leg> branch -m <spelling> <another name>. The twin disappears and its commits stay on the renamed branch. If HEAD is on the twin, git moves HEAD with the rename, and the next run takes the ordinary "the leg was on <another name>, which keeps its commits" path.

  • The cost is more than "a name" (reasoned, not run on APFS). On a real folding disk the twin's packed entry stays in packed-refs. for-each-ref would still list Main at its packed sha, while rev-parse Main and log Main read the new loose main. A later git branch -D Main would unlink the file main.

  • refs/stash is a weak holder. It is spared like the others, but a git stash drop removes it.

  • Recommendation: refuse every folded spelling under refs/heads/, give the branch -m way out, and amend bootstrap.py: a leg whose HEAD is on a branch that differs from the tracking branch only in case (Main for main) is left where it is, so on a disk that folds case branch -f main no longer moves the ref HEAD stands on (#896) #948's HEAD-twin keep line the same way. Alternatively, rule the departure accepted in the PR. The PR is still strictly better than base in every case I ran.

NIT 1. One mutant survives.

  • O1: NFKC in place of NFC in _fold. The new file and the casefold file still pass (49 passed).
  • Effect: under O1, main² becomes a twin of main2, and ① of 1; at the head neither is. APFS does not fold compatibility forms.
  • Fix: one _folded_twin row for main² against main2, expecting False, kills it.

NIT 2. The conflict with #958's round-1 WIP (b49f34e1) is the expected import-line conflict only. Keep import unicodedata and slice D's from collections.abc import Callable, Iterator. Do not keep both collections.abc lines, which would be a duplicate import.

NIT 3. A point about my own #948 probe. Its "no ref contains M" result came from the symlink emulation, which lists the twin as a loose ref. On a real APFS the packed entry would still be listed (reasoned above), so the harm is a shadowed name and a fragile state rather than an unreachable commit. The PR's "in no ref" wording inherits that overstatement.

Checked and true

  1. The merge 5a435acd (check 1).

  2. The probes, at 45415296 and at the head, run with bootstrap.py: a leg whose HEAD is on a branch that differs from the tracking branch only in case (Main for main) is left where it is, so on a disk that folds case branch -f main no longer moves the ref HEAD stands on (#896) #948's fixtures (check 2):

  3. bootstrap.py: a leg whose HEAD is on a branch that differs from the tracking branch only in case (Main for main) is left where it is, so on a disk that folds case branch -f main no longer moves the ref HEAD stands on (#896) #948's rules (check 4).

  4. Mutants (check 5): 7 run, 6 killed.

    Mutant Result
    w962's: check dropped 5 failed
    w962's: ancestor rule dropped 1 failed
    w962's: no NFC 5 failed
    Mine: ancestor arguments reversed 5 failed
    Mine: way out spelled instead of _paste_word 1 failed
    Mine: the twin counted as its own holder 5 failed
    Mine: NFKC survives (NIT 1)
  5. Runs at 5a435acd (check 6):

    • git 2.43.0: the new file, the casefold file, the pasteable file, test_bootstrap_leg_moves.py and the trio: 291 passed, 0 skipped. test_pinned_copy_text.py also passes.
    • git 2.47.1: the new file: 17 passed.
    • Base check: the new file against 45415296: 10 failed and 7 passed, as the body says.
  6. Merges (check 7):

  7. Sonar-style checks and the pinned copy (check 8).

    • Complexity by hand: create_at_pin 14, _spelling_at_risk 5 (the body says 4; the listed or "" counts), _kept_elsewhere 2, _twin_words 1, _say_twin 1.
    • Duplication: no repeated run of 6 or more lines; two 4-line runs.
    • Asserts: none joins conditions with and.
    • Line length: added lines are at most 79 columns. The added bootstrap.py lines are ASCII.
    • Imports: the only new import is the standard library's unicodedata; the repo_shape import set is unchanged.

Could not verify

  • A real APFS or NTFS disk: the packed-ref shadowing behaviour described in SHOULD 1 and NIT 3 is reasoned from git's files backend, not measured.
  • git 2.55 (CI's version), and Sonar's own analyser.
  • I kept within the 25-minute budget, so the wider bootstrap test files were not run.

Checklist candidates

  1. The rename way out (branch -m) for every folded spelling, including bootstrap.py: a leg whose HEAD is on a branch that differs from the tracking branch only in case (Main for main) is left where it is, so on a disk that folds case branch -f main no longer moves the ref HEAD stands on (#896) #948's HEAD-twin keep line (SHOULD 1).
  2. A _folded_twin row for main² against main2, expecting False (NIT 1).
  3. Exclude refs/stash as a holder if _kept_elsewhere stays.
  4. The <another name> placeholder makes the way-out line not pasteable as printed (w962's own limit).

…eads/` branch, packed or loose, folds to it, whatever else holds its commits, and the way out renames the twin with `branch -m`, HEAD's twin included (#962)

Fix round 1 on #984, on rv984's review of 5a435ac (READY; SHOULD 1 was a
decision, and lane 5 ruled: follow #962's Goal).

SHOULD 1. `_kept_elsewhere`'s sparing is dropped with `_spelling_at_risk`.
`_folded_spelling` returns the first `refs/heads/` ref, packed or loose, that
folds (NFC, then casefold) to the branch, whether or not a tag, a
remote-tracking ref, another branch, the stash or the pin holds its commits:
the ref `checkout -b` would make is one a folding disk reads as a branch that
is already there. The way out renames the twin, `git -C <leg> branch -m
<twin> <name>`, the path and both names through `_paste_word`, and
`_kept_name` chooses a name that is not there: `<twin>-kept`, then `-kept-2`
and on, the first no branch of the leg folds to, so the line can be pasted
as printed.

#948's HEAD-twin keep line (the missing branch, HEAD on a twin) is
`_say_spelling` with `where` = `the checkout is on`: its first two lines are
byte for byte #948's, and the third is the rename. git moves HEAD with the
rename of the branch HEAD is on, an unborn one included, so the next run
says `the leg was on <name>, which keeps its commits` and makes the branch.
`_say_twin` keeps T7 and P9; `_say_keep` is back to the one-parameter text
main had before #948, which no twin line uses now.

NIT 1. `_folded_twin` rows for `main` and U+00B2 against `main2`, and U+2460
against `1`, both False: NFKC would fold them, so the NFKC mutant is
killed.

NIT 3. The docstrings and the tests say what a real folding disk does with a
packed twin (the name is shadowed and the state is fragile; reasoned from
git's files backend, not measured on APFS or NTFS), not that the commit is in
no ref.

Tests: tests/test_bootstrap_create_twins.py, 31 tests (20 fail on 5a435ac,
11 pass there by design), and 3 changed tests in test_bootstrap_casefold_twin.py.
bootstrap.py is a pinned copy: every project's `bootstrap.py` takes an
`upstream-changed` row, and re-pins it.

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

Copy link
Copy Markdown

@brettheap brettheap changed the title bootstrap.py: a missing tracking branch is not made while a refs/heads/ branch, packed or loose, that folds to it holds a commit nothing else holds, and a twin folds in NFC first, so an NFD name loses no commit and moves no leg (#962) bootstrap.py: a missing tracking branch is not made while any refs/heads/ branch, packed or loose, folds to it, and a twin folds in NFC first, so an NFD name moves no leg, and the way out renames the twin, HEAD's twin included (#962) Oct 19, 2026
@brettheap

Copy link
Copy Markdown
Contributor Author

Fix round 1 on #984 (#962): rv984's SHOULD 1 (lane 5's ruling: refuse every folded spelling, and the way out renames the twin), #948's HEAD-twin keep line, NIT 1 and NIT 3, one commit f1cceddf on 5a435acd, pushed

rv984 found #984 READY at 5a435acd (#984 issuecomment-6254685548). SHOULD 1 was a decision: round 0's _kept_elsewhere made the very ref #962's Goal forbids. Lane 5 ruled to follow the Goal. The push is a fast-forward of fix/962-create-twins from 5a435acd to f1cceddf; nothing was forced. The change is three files: templates/assembly-root/scripts/bootstrap.py, tests/test_bootstrap_create_twins.py and tests/test_bootstrap_casefold_twin.py (three tests changed).

SHOULD 1: no sparing, and the way out renames the twin

  • Dropped: _kept_elsewhere and _spelling_at_risk. _folded_spelling(sub, branch) returns the first refs/heads/ ref, packed or loose, that folds (NFC, then casefold) to the branch, whether or not a tag, a remote-tracking ref, another branch, the stash or the pin holds its commits. _heads(sub) is the shared listing.
  • The way out: git -C <leg> branch -m <twin> <name>, the path and both names through _paste_word.
  • A concrete name, so the line can be pasted as printed: _kept_name(sub, twin) is <twin>-kept, then -kept-2 and on, the first that no branch of the leg folds to. Main-kept is taken when main-kept is there, since on a folding disk they are one file. I chose the concrete name over the <another name> placeholder, as the brief allowed.
  • Why the loop is gone: it came from a way out that left the twin in place. The rename removes it, so the next run finds no twin and makes main.
  • The state rv984 measured, at 5a435acd (git 2.43.0): a packed Main whose commit a tag holds, main missing, HEAD detached at the pin: branch main created at <pin>. At f1cceddf: the refusal, the rename pasted through /bin/sh (git -C spec branch -m Main Main-kept), and the next run says created, with Main-kept holding the commit. The same for another branch, a remote-tracking ref, the stash and the pin alone (five rows).

#948's HEAD-twin keep line

NIT 1: the NFKC mutant

Two _folded_twin rows, main and U+00B2 against main2, and U+2460 against 1, both expecting False (NFKC would fold them; APFS does not). The NFKC mutant: 2 failed, killed (it survived the 49 tests of round 0).

NIT 2: no change

Against #958 (0ef875c0) and slice D's round 1 (b49f34e1), merge-tree gives one conflict each, the import block of bootstrap.py. The resolution keeps import unicodedata and slice D's from collections.abc import Callable, Iterator; it does not keep both collections.abc lines, which would be a duplicate import. The body says so.

NIT 3: what a folding disk really does

The module docstring, _folded_spelling's docstring, the test file's docstrings and the body no longer say the commit ends "in no ref". They say what rv984 reasoned from git's files backend: the packed Main entry stays, for-each-ref goes on listing it at its old sha while rev-parse Main reads the new file main, and a later git branch -D Main unlinks that file: a shadowed name and a fragile state. This is not measured on APFS or NTFS, and the body says so.

Evidence

  • New and changed tests: tests/test_bootstrap_create_twins.py is 31 tests. At 5a435acd, with the two test files copied in: 23 failed, 40 passed. 20 of the new file's 31 fail there; the three changed casefold tests fail (the unborn twin, the packed twin, the non-UTF-8 lines). The 11 that pass there pass by design: mains, the NFD T7 test, the eight _folded_twin rows and the fold test. At the head: 63 passed (the new file and the casefold file).
  • Mutants, 16, none survives: the check dropped (10 failed), no NFC (6), always the case words (2), names the branch not the twin (14), NFKC (2), branch for branch -m (17), the rename's arguments swapped (17), _kept_name never loops (4), compares byte for byte (3), the candidate not folded (3), the HEAD-twin line says the leg has the branch (4), the HEAD-twin check dropped (4), the sparing put back (5), the new name not _paste_worded (2), the twin not _paste_worded (2), _heads loose refs only (13). Round 0's four mutants of _kept_elsewhere are gone with the code.
  • git 2.43.0, at the tree of f1cceddf: the new file, the casefold file, test_bootstrap_pasteable_lines.py, test_bootstrap_leg_moves.py, test_repo_hygiene.py, test_agents_md_codes.py, test_cli_reference.py and test_pinned_copy_text.py: 392 passed in 1:53. git 2.47.1: the new file and the casefold file: 63 passed. The rest of the suite is left to CI's tests job.
  • Output that is not a twin's: a differential of 28 states (4 tracking-branch states by 7 HEAD states, one not a state) against main ebbd9fa4: 27 real states, the printed lines and the leg afterwards, byte for byte the same. Nothing printed for a non-twin differs.
  • Merges (git merge-tree --write-tree): origin/main c4dd19dc clean; bootstrap.py: the paste words it prints quote a leading =, are made of the bytes git is handed (UTF-8 on Windows), and the NOT INITIALIZED command names a leg's path as one word after a --, with ./ before a : (#961) #973 (b2887cf5) clean; the assembly root's validate-manifest.py and bootstrap.py: a path this user cannot read is named, never read as absent, so a scripts/ nobody may search no longer SKIPs every validator into bootstrap ok (#430, slice D) #958 and slice D's WIP one import-line conflict each (NIT 2).
  • Cognitive complexity (the ast counter): fast_forward_to_pin 15 (unchanged), place_at_pin 6, create_at_pin 14, _folded_spelling 3, _heads 1, _kept_name 1, _say_spelling 0, _say_twin 0. New lines are within 79 columns and the new bootstrap.py lines are ASCII.

Not changed

_paste_word and the NOT INITIALIZED line. T7's and P9's twin lines. Every output that is not a twin's. The body (body.md) is updated: the _kept_elsewhere heading is now the rename way out.

@brettheap

Copy link
Copy Markdown
Contributor Author

Re-check of #984 (#962)'s fix round 1 at f1cceddf, for lane openRepoShape-5: READY

I found no MUST or SHOULD. Lane 5's ruling on SHOULD 1 is carried out as ruled. There are two NITs, both small enough for a follow-up.

Findings, most severe first

  • NIT 1. _kept_name can print a rename git refuses. templates/assembly-root/scripts/bootstrap.py:875-888 (the loop is at :885).
    • It only checks whether a branch folds to the same name. It does not check for a branch stored under the chosen name.
    • Scenario: the leg is detached at the pin, Main is packed and holds a commit, and a branch Main-kept/x exists.
    • Probe at the head: the printed line is git -C spec branch -m Main Main-kept. Pasted through /bin/sh it exits 128 with error: 'refs/heads/Main-kept/x' exists; cannot create 'refs/heads/Main-kept'. The next run prints the same command again, so the person has to pick a name themselves.
    • On a folding disk, a branch main-kept/x would block the rename the same way.
    • This is false against the docstring's "so the line that says to rename it can be pasted as it is". It is rare, and the line does state the rule, so the person can recover.
    • Fix: also skip a candidate when any held name starts with _fold(name) + "/", and add a row for it.
  • NIT 2. No test has a twin whose name contains a slash. My mutant O3 survives with 63 of 63 passing.
    • O3 makes _folded_spelling (:871) return ref.rsplit("/", 1)[-1] instead of ref[len("refs/heads/"):].
    • The real code is right. I probed --branch team/main with a packed Team/main: it is refused, the printed git -C spec branch -m Team/main Team/main-kept works, the next run makes team/main, and Team/main-kept holds the commit.
    • Only a test row is missing.

Checked and true

  1. SHOULD 1, as ruled.
    • I re-ran rv984's eight state probes at the head: (b), (c), NFD, and a twin whose commit only a tag, a remote-tracking ref, another branch, the stash or nothing holds. Every one is refused and none makes main. At 5a435acd the four holder cases made main.
    • The test rows also cover the pin itself.
    • The branch -m line, run through /bin/sh, leaves the commit on Main-kept, and the next run makes main. I also checked this in a leg where Main was checked out in a linked worktree: git moved that worktree's HEAD to Main-kept.
    • git check-ref-format --branch accepts Main-kept, Team/main-kept, the NFD café-kept, a@-kept, v1.0-kept and Main-kept-2.
    • By reasoning: adding -kept to a valid name cannot break a ref-format rule, and the fold of <twin>-kept is the fold of the branch plus -kept, so it is never the branch.
  2. bootstrap.py: a leg whose HEAD is on a branch that differs from the tracking branch only in case (Main for main) is left where it is, so on a disk that folds case branch -f main no longer moves the ref HEAD stands on (#896) #948's HEAD-twin keep line.
    • A function-level comparison with main (ebbd9fa4, whose bootstrap.py is the same as a19d69cb's): 1,920 calls where create_at_pin runs on a case twin. The first two lines are byte for byte main's in all of them, and the third is the rename line every time.
    • The unborn-twin tests pass on 2.43 and on 2.47.1: the rename moves HEAD, and the next run prints the ordinary "the leg was on Main-kept, a branch with no commit yet" (or "which keeps its commits" for a twin with commits).
    • A HEAD twin with a dirty work tree is renamed, and then hits main's ordinary "could not create branch 'main'... leaving it on Main-kept" refusal. That is the same as for any other branch.
  3. The three NITs from my rv984 review.
    • The NFKC mutant is killed (2 failed, the two new rows).
    • The import note is in the body: keep import unicodedata and slice D's Callable, Iterator line, and do not keep both.
    • "Shadowed name, fragile state" is true by reasoning from git's files backend: checkout -b writes a loose file, which the folding disk also reads for Main; the packed entry stays; and branch -D Main would unlink main and drop the packed entry.
  4. Unchanged outputs.
    • 47,616 calls to place_at_pin, fast_forward_to_pin and create_at_pin in states that are not a twin's, plus case twins on place and fast-forward: every one is byte for byte main's. Only 4 states that are NFD twins and nothing else were skipped.
    • _say_keep prints main's text, because main's default kept="a branch" gives the same string. Its three callers (:719, :1352, :1358) pass no second argument.
  5. Mutants. I re-ran three of w962's: _kept_name never loops (4 failed), the HEAD-twin where wrong (4), sparing put back (5). These match w962's counts. Of my own, O1 (check the twin only when HEAD is detached) failed 1 and O2 (_kept_name with casefold alone) failed 1. O3 survives (NIT 2).
  6. Test runs at f1cceddf, through run9.sh.
    • git 2.43.0: the brief's six files, 278 passed. Adding test_pinned_copy_text.py and test_bootstrap_leg_moves.py, 114 passed. 392 in all, which matches the writer.
    • git 2.47.1: the new file, 31 passed. The casefold file, 32 passed. 63 in all, which matches.
  7. Merges.
  8. Sonar-style checks.
    • Cognitive complexity: fast_forward_to_pin 15 (same as main), create_at_pin 14 (main 13), place_at_pin 6, _folded_spelling 3, _heads 1, _kept_name 1 (2 if the set comprehension is counted), _say_spelling 0, _say_twin 0.
    • The new lines are ASCII and within 79 columns.
    • The imports from repo_shape are the same seven names as before.
    • The "refs/heads/" literal was already repeated 5 times on main, so the 9 now add to an existing duplication rather than creating a new one.

Could not verify

@brettheap
brettheap marked this pull request as ready for review October 19, 2026 14:12
@sourcery-ai

sourcery-ai Bot commented Oct 19, 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 2 days and 5 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 2a78ae1 into main Oct 19, 2026
5 checks passed
@brettheap
brettheap deleted the fix/962-create-twins branch October 19, 2026 14:13
brettheap pushed a commit that referenced this pull request Oct 19, 2026
…ot-guards

Two resolutions, both mechanical:

- templates/assembly-root/scripts/bootstrap.py, the one conflict: the
  import block. Every module either side imports is kept, once: #984's
  `import unicodedata`, and this branch's ONE
  `from collections.abc import Callable, Iterator`; main's
  `from collections.abc import Iterator` is dropped, since this branch's
  line imports `Iterator` too. With this branch's `import errno` and
  `import stat`, the block is main's in alphabetical order; no name is
  imported twice and none is lost.
- templates/assembly-root/scripts/validate-manifest.py, merged without a
  conflict marker but defining `_shown` twice: this branch's
  `_shown(typed)` is deleted and #981's `_shown(path)` is kept. Both of
  this branch's calls pass a `str`, which #981's `str(path)` leaves as it
  is, so each prints the same bytes.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 19, 2026
…branch stored under it, `Main-kept` over `Main-kept/x`, which git refuses, so the pasted line works and the next run makes the tracking branch (#1000) (#1006)

Follow-up of #962 (landed as #984, `2a78ae1a`): the two NITs of rv984b's READY re-check (#984 issuecomment-6266764236), filed as #1000. One commit, `9b8c643f`, on `2a78ae1a`. Two files change: `templates/assembly-root/scripts/bootstrap.py` and `tests/test_bootstrap_create_twins.py` (+177, -10).

A pinned copy: every project's `bootstrap.py` takes an `upstream-changed` row, and re-pins it. No digest of the template is kept anywhere, and #984 handled it the same way.

The line step (a) prints to rename a twin, `git -C <leg> branch -m <twin> <name>`, can be pasted as it is. `_kept_name`'s docstring says so (#962), and #984 chose the concrete `<twin>-kept` name to keep that promise. It broke when a branch is stored UNDER the chosen name.

`_kept_name` skipped `<twin>-kept` only when a branch of the leg FOLDED to it. With the leg detached at the pin, `Main` packed and holding a commit, and a branch `Main-kept/x`, the line was `git -C spec branch -m Main Main-kept`, which git refuses: exit 128, `error: 'refs/heads/Main-kept/x' exists; cannot create 'refs/heads/Main-kept'`, `fatal: branch rename failed`. The next run printed the same line again, so the person had to find a name alone. On a disk that folds case, `main-kept/x` blocks `Main-kept` the same way.

- **`_taken(held, name)`** (new, `bootstrap.py`): the name is taken when a held branch, folded, is `_fold(name)` or starts with `_fold(name) + "/"`. `_kept_name` loops `while _taken(held, name)`: `<twin>-kept`, then `-kept-2` and on. `Main-kept-2` is printed, the pasted line works, and the next run makes the tracking branch.
- **The docstrings** of `_taken` and `_kept_name` state the rule, why a branch that is a directory PREFIX of the name needs no check, and what the helper cannot see (below).
- **Item 2 (#1000): a test row for a twin whose name has a slash.** `--branch team/main` with a packed `Team/main`, detached over it and ON it. rv984b's mutant O3 (`_folded_spelling` returning `ref.rsplit("/", 1)[-1]` instead of `ref[len("refs/heads/"):]`) survived 63 of 63 tests; the rows kill it, and kill the same cut at the HEAD-twin site (O3b).
- **13 tests added**, the file is 44 tests: seven `_kept_name` rows (a branch under the name; under it in another case; deeper under it; under each of the first two; a name that only starts with it is no bar; a branch under another name that holds it is no bar; a twin with a slash), two Unicode rows (NFD under an NFC name, NFC under an NFD name), two end-to-end rows (`Main-kept/x` loose and packed: `Main-kept-2` printed, pasted through `/bin/sh`, the next run makes `main`) and the two slash rows. Helpers: `missing` and `created` take a branch, and `twin_with_a_commit`. Cognitive complexity: `_taken` 1, `_kept_name` 1, `create_at_pin` 14 and `fast_forward_to_pin` 15 (both unchanged). New lines are within 79 columns, and the new `bootstrap.py` lines are ASCII.

Probed on git 2.43.0 and 2.47.1 in throw-away repositories (`gp/gitfacts*.sh`):

- **Handled, exit 128:** a branch stored under the name, loose or packed, at any depth, for a twin with a slash too, and with the reftable backend.
- **Not refused, so not skipped:** a name that only starts with the candidate (`Main-kept-x`), a tag, a remote-tracking ref or the stash of that name, and an unborn HEAD twin, whose rename only rewrites HEAD and checks nothing.
- **A branch that is a directory prefix of the name** (`team` for `Team/main-kept`) is not handled, on purpose: the name has every directory its twin has, and the twin folds to the tracking branch, so git will not make that prefix beside the twin (`cannot lock ref 'refs/heads/Team'`), and a held `team` stops `checkout -b team/main` itself, packed or loose. No `-kept` name helps.
- **Not handled, and said in `_kept_name`'s docstring:** a stale reflog directory under the name, which git refuses (`directory not empty`, exit 128), `for-each-ref` does not list, and `branch -D` removes with the branch, so only a ref file removed by hand leaves one; and a twin whose last path component is 246 bytes or more, whose `-kept` name is refused at the file-name limit.

A real repository from the test scaffold, local bare remotes, the printed line pasted through `/bin/sh`. The leg is detached at the pin, `Main` packed with a commit, `Main-kept/x` loose or packed, `main` missing.

    at 2a78ae1:
      run 1 prints  rename Main to a name that differs from main by more than case, its commits staying on it: `git -C spec branch -m Main Main-kept`, and run bootstrap again.
      pasted        exit 128  error: 'refs/heads/Main-kept/x' exists; cannot create 'refs/heads/Main-kept' / fatal: branch rename failed
      runs 2 and 3 print the same line again; heads stay Main, Main-kept/x.
    with this change:
      run 1 prints  ... `git -C spec branch -m Main Main-kept-2`, and run bootstrap again.
      pasted        exit 0
      run 2 prints  [spec] spec: branch main created at <pin> - at the pin
      heads after   Main-kept-2 (the commit), Main-kept/x, main (the pin)

    HEAD unborn on Main, Main-kept/x held:
      at 2a78ae1   prints Main-kept and the rename works (exit 0, HEAD rewritten); run 2 makes main.
      with this     prints Main-kept-2, also works; run 2 makes main, "the leg was on Main-kept-2, a branch with no commit yet".

The final test file copied onto `2a78ae1a`: **9 failed, 35 passed (44 tests).** The 9: the five `_kept_name` rows with a branch under the name (under it; under it in another case; deeper; under each of the first two; a twin with a slash), the two Unicode rows and the two end-to-end rows. The 4 new tests that pass there pass by design: the two no-bar rows, and the two slash rows, which pin code that was already right and fail only for O3 and O3b.

| mutant | failed |
|---|---|
| O3: `_folded_spelling` returns `ref.rsplit("/", 1)[-1]` | 1 (the slash row, detached) |
| O3b: the same cut at the HEAD-twin site | 1 (the slash row, on the twin) |
| the new check removed (`return folded in held`) | 9 |
| the `"/"` dropped (a plain `startswith(folded)`) | 1 (a name that only starts with it) |
| `folded + "/" in h` for `startswith` | 1 (a branch under another name that holds it) |
| the check on the unfolded name | 8 |
| `all` for `any` (guarded against an empty set; unguarded it hangs) | 9 |
| the equality dropped | 4 |
| the immediate parent only | 1 (deeper) |
| the first path component only | 1 (a twin with a slash) |
| the name `casefold`ed, not `_fold`ed | 1 (NFC under, NFD name) |
| the held names not folded | 9 |
| the loop dropped (`while False`) | 13 |

At the committed head `9b8c643f`, through `run9.sh` (gh stub first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`):

- **git 2.43.0** (`/usr/bin/git`): the create-twins, casefold, `test_bootstrap_pasteable_lines.py`, `test_bootstrap_leg_moves.py` and `test_pinned_copy_text.py` files and the trio (`test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`): **405 passed**.
- **git 2.47.1** (exactly `$Q/v115/w764/git247/bin/git`, first on PATH): the create-twins and casefold files: **76 passed**.
- The rest of the suite is left to CI's `tests` job.

`git merge-tree --write-tree` of the head against: origin/main `04923f10` clean (tree `b2486824`); `fix/430-d-assembly-root-guards` (#958, `cf8d5667`) clean (tree `12cb36fb`); `fix/961-paste-words` (#973, `b2887cf5`) clean (tree `de0bb093`). Main has moved on since `2a78ae1a` and has not touched `bootstrap.py` or the twin tests, so the branch needs no merge and no new run.

Lane: openreposhape-5 (openRepoShape-5)

Review: rv984b, lane 5's independent reviewer, not the writer (w1000): READY at `9b8c643f`, the head that lands (issuecomment-6276993923), with no MUST or SHOULD. Its three NITs are words only (two in `_taken`'s docstring, one in the body). CI (Brett Heap's ruling 27): H0 is the head 9b8c643 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 2a78ae1; and the local gate passed on its predicted squash tree 3d2f109 over main 46cf924: the bootstrap_create_twins, repo_hygiene, agents_md_codes, cli_reference sets, 214 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green".

Closes #1000

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 20, 2026
…s user cannot read is named, never read as absent, so a `scripts/` nobody may search no longer SKIPs every validator into `bootstrap ok` (#430, slice D) (#958)

# Title

the assembly root's validate-manifest.py and bootstrap.py: a path this user cannot read is named, never read as absent, so a `scripts/` nobody may search no longer SKIPs every validator into `bootstrap ok` (#430, slice D)

# Body

Part of #430 (slice D of lane 1's plan, #430 issuecomment-6251069236). A v1.2.0 DRAFT.

Head `cf8d5667`, on `50178305` (the v1.1.1 release commit): the change (`df1bb186`); a pure move in `bootstrap.py` (`0ef875c0`) that clears a merge conflict with lane 5's #896 branch; the merge of origin/main `ebbd9fa4`, with #896's twin refusals (#948) in it (`bb58c441`, clean); fix round 1 on lane 4's RV958 review (`b420171f`); and the merge of origin/main `2a78ae1a`, with #984 (#962) in it (`cf8d5667`), which keeps #981's `_shown` in `validate-manifest.py` and one `collections.abc` import line in `bootstrap.py`. Files: `templates/assembly-root/scripts/validate-manifest.py`, `templates/assembly-root/scripts/bootstrap.py` and a new `tests/test_assembly_root_unreadable_paths.py`.

Every guard below asked `Path.is_file()`, `Path.exists()` or `os.path.lexists`. Up to Python 3.13 the first two raise a refused `stat` (`PermissionError`) out of the run: a traceback, exit 1, which is a finding's code. From 3.14 they answer False to any `OSError`, and `os.path.lexists` always has. So a path this user cannot read was taken for one that is not there.

Measured at `50178305` in a scaffold's recursive clone (local bare remotes, a stub `gh`), on Python 3.12.3 with the mode taken away, and under a `sitecustomize` shim that answers as 3.14's `is_file`, `is_dir`, `exists` and `is_symlink` do. A `project.yaml` or a pin file is reached through a symlink into a folder nobody may enter, since no tool finds a root it cannot search.

| Cell | 3.12 | 3.14-like | Now, on both |
|---|---|---|---|
| validate-manifest.py, `project.yaml` behind such a folder | traceback, exit 1 | `REFUSED manifest-missing`, exit 2 | `REFUSED yaml-unreadable: project.yaml: cannot be read (Permission denied)` and its remedy, exit 2 |
| validate-manifest.py, a declared referent's pin file behind such a folder | traceback, exit 1 | `FINDING naming-referent-missing`, exit 1 | `REFUSED yaml-unreadable: contracts/<pin>: cannot be read (Permission denied)`, exit 2 |
| bootstrap.py, `project.yaml` behind such a folder | traceback, exit 1 | no leg placed, no validator run, `bootstrap ok`, exit 0 | `REFUSED yaml-unreadable: project.yaml: ...`, exit 2, before step (a) |
| bootstrap.py run with `--root`, `scripts/` at 000 | traceback in step (b), exit 1 | all three validators `SKIPPED`, `bootstrap ok`, exit 0 | `REFUSED validator-unreadable: scripts/validate-repository-naming.py: cannot be read (Permission denied)`, exit 2, before any validator runs |
| bootstrap.py, `governance/` at 000 | traceback in step (c), exit 1 | `authority is not wallet-carried in this org`, exit 0 | step (c) heads it `register, if one is there:`, names it as `cannot be read`, with the cure, and claims nothing from it; exit 0 |
| bootstrap.py, `spec/` at 000 | traceback in step (a), exit 1 | `[spec] spec: NOT INITIALIZED`, then pins refuses, exit 2 | `[spec] spec: spec/.git: cannot be read (Permission denied), so the leg is left as it is`, with the cure; pins refuses; exit 2 |
| bootstrap.py, the spec leg's `sequencer/` at 000 over a `todo` | the leg is put on `main`, exit 0 | the same | `.git/modules/spec/sequencer/todo: cannot be read (Permission denied), so the leg is left as it is`; the leg stays detached; exit 0 |
| validate-manifest.py, `contracts/` at 000, no `--policy` (RV958's S1) | `REFUSED yaml-unreadable: <ROOT>/contracts/repository-naming.yaml: cannot be read (Permission denied)`, no remedy, exit 2 | the same | `REFUSED yaml-unreadable: contracts/repository-naming.yaml: cannot be read (Permission denied)` and its remedy, exit 2 |
| bootstrap.py, `scripts/validate-pins.py` at 000 (RV958's S2) | two validators run, then `/usr/bin/python3: can't open file '<ROOT>/scripts/validate-pins.py': [Errno 13] Permission denied`, `bootstrap REFUSED: pins (exit 2)` | the same | `REFUSED validator-unreadable: scripts/validate-pins.py: cannot be read (Permission denied)` and its remedy, exit 2, before any validator runs |
| bootstrap.py, a register under a leg whose `path:` is absolute, in a folder at 000 or readable (RV958's M1) | `ValueError: ... is not in the subpath of ...` in step (c), exit 1 | the same | the register named whole; refused: `register, if one is there:` and nothing claimed; readable: its rows; exit 0 |

The last three rows were measured at `00a6f839` (this branch's first merge of main) by lane 4's RV958 review and again by this branch; the first seven at `50178305`. The `sequencer/` row is wrong on every Python: `bootstrap.py` moved a leg over a cherry-pick that was not done, and the pick's `--continue` then goes on over the tracking branch (#704's harm). Each readable control printed the same at base and at head, byte for byte, with shas and paths made equal.

| Site at `50178305` | Class | What changed |
|---|---|---|
| validate-manifest.py `:1085`, `main`, `manifest_path.is_file()` | OPEN-FALSE | FIXED. `_is_file` refuses `yaml-unreadable`, naming `project.yaml`. One this user cannot OPEN (mode 000, `load_yaml` at `:1092`) says the same line now; it was `<absolute path>: cannot be read (Permission denied)` with no remedy. |
| validate-manifest.py `:406`, `_naming_referent_missing_pin_finding`, `pin_path.is_file()` | OPEN-FALSE | FIXED. A refusal, `yaml-unreadable`, naming the pin file, exit 2; `main`'s `try` now covers `_findings`. |
| bootstrap.py `:1409`, `_run_neutral_validators`, the validator skip | OPEN-SKIP | FIXED. Every validator is looked for before any runs. One whose `stat` is refused is `validator-unreadable`, exit 2; one that is not there is still `SKIPPED`. |
| bootstrap.py `:1328`, `_load_root_and_manifest`, `manifest_path.is_file()` | OPEN-FALSE | FIXED. `yaml-unreadable`, naming `project.yaml`, exit 2, before step (a). One this user cannot open says the same line. |
| bootstrap.py `:1191`, `_register_paths`, `c.is_file()` | OPEN-FALSE | FIXED. A register whose `stat` is refused is kept, and step (c) names it `cannot be read` with the cure. A register this user cannot open is named the same way, relative to the root, where its absolute path was printed. |
| bootstrap.py `:1056`, `checkout_tracking_branch`, `(sub / ".git").exists()` | OPEN-FALSE | FIXED. Step (a) names the leg's `.git` and leaves the leg as it is (`_initialized`). |
| bootstrap.py `:717`, `_in_progress`, `os.path.lexists(gitdir / marker)` | OPEN-FALSE | FIXED. A marker whose `lstat` is refused is named, and the leg is left as it is (`_stays_for_an_operation`). |
| bootstrap.py `:750`, `_sequence_in_progress`, `os.path.lexists(todo)` | OPEN-FALSE | FIXED. The same. |
| validate-manifest.py `main`, `NamingPolicy.load(policy_path)` (round 1, RV958's S1) | OPEN-FALSE (named, no remedy) | FIXED. Loaded through `_load`, so an `OSError` cause is `_unreadable_file`'s refusal, named relative to the root, with the remedy. A missing policy, which was the absolute path alone, now says the same line with `OTHER_REASON`. |
| bootstrap.py `_run_neutral_validators`, a validator it can `stat` and cannot open (round 1, RV958's S2) | OPEN-SKIP's twin (the child's own words) | FIXED. `_a_validator` opens each after its `stat`, before any runs, as #934's `_validators_readable` does. |
| bootstrap.py `_report_one_register`, `relative_to(root)` (round 1, RV958's M1) | a traceback for an absolute leg path | FIXED. The header goes through `_named`, whole where the path is not under the root. |

Left as BENIGN: the upstream notice's three reads (`:1123`, `:1159` and `:1180`), each an absent answer by #764's ruling and the last the tool's own cache; `:753`, where a `todo` that cannot be read is taken for a cherry-pick and the leg is still left; and each file's `Path(__file__).resolve()`. Neither file has an OPEN-DEST site.

- **One rule.** Each file keeps a local `_is_a`, a copy of `validate-pins.py`'s, with the one errno set: ENOENT, ENOTDIR, EBADF and ELOOP, and Windows' 21, 123 and 1921, mean absent. Any other `OSError` is named `<path relative to the root>: cannot be read (<strerror>)`. The cure is `validate-pins.py`'s (`chmod u+r` on a file, `chmod u+rx` on a directory and every directory above it, or fix who owns it), or its `OTHER_REASON` for a cause that is not a permission. Nothing new is imported from `repo_shape.py` (#370, #610), and `scripts/repo_shape.py` is untouched. bootstrap.py's `_is_a` takes `link=True` for the two `lexists` sites, which ask with `lstat`.
- **Refusals.** `yaml-unreadable`, the id `load_yaml` and `validate-pins.py` already give such a file, for `project.yaml` in both files and for the referent's pin file. One new id, `validator-unreadable`, for a validator bootstrap cannot reach or open. Each remedy says what the file is read for, then the cure, then `Then run the same command again.`
- **A referent's pin file is a refusal, not a finding.** The check cannot run, so the run is refused, exit 2, as `validate-pins.py` refuses the same file.
- **Steps (a) and (c) still never fail the run.** A leg or a register this user cannot read is a line in its step, and the step goes on. In a normal run `validate-pins.py` refuses such a leg (`pin-submodule-unreadable`), so the run ends `bootstrap REFUSED: pins (exit 2)`. Under `--skip-validators` the run ends `bootstrap ok`, as it did for a leg NOT INITIALIZED.
- **A register whose `stat` is refused is kept, and not said to be there.** Whether one is there cannot be told, so step (c) does not say the degrade line, and heads it `register, if one is there: <leg>/governance/review-authority/register.yaml` (RV958's N1). A register whose `stat` works and whose open is refused keeps `register:`.
- **The open as well as the stat.** A validator is opened, and closed unread, before any runs (RV958's S2). A `project.yaml`, the naming policy (RV958's S1) or a register this user cannot open is read off the refusal's `__cause__`, as `validate-pins.py`'s `_load` reads it (#712), so the two causes say one line. Every other `load_yaml` refusal keeps its words; `tests/test_manifest_validator.py`'s not-UTF-8 case, which pins the absolute path, passes.
- **Every path is spelled.** bootstrap.py names each through `spelled` (#391); validate-manifest.py's `_unreadable_file` through a local `_shown`, validate-pins.py's (RV958's N3). A path not under the root (a leg's absolute `path:`, a `--policy` given elsewhere) is named whole.
- **Readable paths print what they printed**, which lane 1's slice A (`fix/430-a-bump-leg-unsearchable`) relies on when it runs `validate-manifest.py`.

Both files are pinned copies (`COPIED_VERBATIM`), so each project takes one `upstream-changed` row for each. No `--help` text changes, so `docs/cli.md` is unchanged. `AGENTS.md`, `templates/assembly-root/AGENTS-shape.md`, the template README and README's degrade rule stay true.

`tests/test_assembly_root_unreadable_paths.py`, 71 tests (46 in round 0, 25 added in round 1), on `conftest.scaffolded` through the `project` fixture.

- Each refusal is asked twice and must say the same, exactly, stdout and stderr: as a project runs the tool, with the mode taken away and put back in a `finally` (`mode_of`; skipped on Windows and as root), and in-process under `refused_stat` from `tests/test_bump_leg_unopenable.py`, which answers as 3.14 does on any Python. `failing` from `tests/test_validate_pins_named_and_spelled.py` gives an I/O error from a `stat`, an open or a read, whose advice is `OTHER_REASON`, and a refused `lstat`.
- Round 1 adds: the naming policy (mode 000 with no `--policy`; an I/O error; a `--policy` outside the root whose name holds an ESC); a validator at mode 000 and one whose open fails; a register under an absolute leg path, readable and refused; a leg in a folder at 000 whose name holds an ESC, inside and outside the root; a leg path holding a NUL; the relative branch of validate-manifest.py's spelling; and, for the survivors RV958 named, an I/O-error row at every site that reads the cause, a directory where `project.yaml`, a validator or a register goes, and anything at a leg's name for `_found`.
- The controls: a referent's pin file that is a dangling link (ENOENT) or a link to itself (ELOOP) is still `naming-referent-missing`; a validator that is not there, or a directory in its place, is still `SKIPPED`; a directory where `project.yaml` goes is still `manifest-missing` to the gate and no manifest to `bootstrap.py`; a directory where a register goes is no register.
- The errno set: each file's `_is_a` answers False to the seven absent causes and to a name no file can have (a NUL, a lone surrogate), and raises EACCES, EPERM, EIO and ENAMETOOLONG.

**At origin/main** (`ebbd9fa4`; both scripts as main has them; the new file kept): 64 of the 71 do not pass (38 fail, and 26 error at setup because main has no `_is_a`). The 7 that pass there by design are the controls: `test_a_referent_pin_that_is_not_there_is_still_missing[dangling-link]` and `[link-to-itself]`, `test_a_validator_that_is_not_there_is_still_skipped[taken-away]` and `[a-directory-in-its-place]`, `test_a_manifest_that_is_a_directory_is_no_manifest_to_either_tool`, `test_only_a_file_is_a_register_where_the_stat_works[a-directory]` and `test_a_leg_path_holding_a_nul_is_not_initialized`.

**At `00a6f839`**, this branch's first merge of main (`2f5cabf5`), before round 1: 14 fail, 57 pass. The 14 are round 1's eleven new cells for its changes and the three cells whose step (c) header N1 changed. The 14 new cells that pass there do so by design: each pins behaviour the branch already had and kills a survivor (see Mutants).

**Round 1:** 27 mutants, one string each, run against the new file at the round's code on the earlier merge (`b220336b`'s scripts; #948's merge changes none of the mutated lines, and each mutant's text is found once at the head): 27 killed, 0 survived. B* and V* are RV958's thirteen survivors (B2, B3, B5, B6, B7, B8, B10, B11, B15, V1, V2, V4, V5) as this branch reconstructs them, since RV958 did not publish its mutant texts, with B5b for the second branch of `_named`; K* are further guards of the same kind; R* are one or more for each change of the round.

| Mutant | What it changes | Result |
|---|---|---|
| B2 | bootstrap.py `_is_a` without its `except ValueError` | killed: 3 test(s): test_a_leg_path_holding_a_nul_is_not_initialized, test_a_name_no_file_can_have_is_absent[bootstrap.py-a-lone-surrogate], test_a_name_no_file_can_have_is_absent[bootstrap.py-a-nul] |
| V1 | validate-manifest.py `_is_a` without its `except ValueError` | killed: 2 test(s): test_a_name_no_file_can_have_is_absent[validate-manifest.py-a-lone-surrogate], test_a_name_no_file_can_have_is_absent[validate-manifest.py-a-nul] |
| B5 | bootstrap.py `_named`, the relative branch unspelled | killed: 1 test(s): test_a_leg_in_a_folder_nobody_may_search_is_named_and_spelled[inside-the-root] |
| B5b | bootstrap.py `_named`, the whole-path branch unspelled | killed: 1 test(s): test_a_leg_in_a_folder_nobody_may_search_is_named_and_spelled[outside-the-root] |
| B6 | bootstrap.py `_named`, out of the root: `path.name` for the whole path | killed: 2 test(s): test_a_leg_in_a_folder_nobody_may_search_is_named_and_spelled[outside-the-root], test_a_register_under_a_leg_path_outside_the_root_is_read |
| V5 | validate-manifest.py `_unreadable_file`, out of the root: `path.name` | killed: 1 test(s): test_a_naming_policy_the_gate_cannot_open_is_named_on_every_python[given-outside-the-root-with-an-esc] |
| B7 | bootstrap.py `_is_file` takes only `PermissionError` | killed: 2 test(s): test_a_manifest_bootstrap_cannot_stat_or_open_is_the_same_refusal[stat-io-error], test_a_validator_whose_stat_is_refused_stops_step_b_before_any_runs[stat-io-error] |
| B8 | bootstrap.py `_a_register` takes only `PermissionError` | killed: 1 test(s): test_a_register_whose_stat_is_refused_is_not_dropped[io-error] |
| B10 | bootstrap.py `_load_manifest` maps only a `PermissionError` cause | killed: 1 test(s): test_a_manifest_bootstrap_cannot_stat_or_open_is_the_same_refusal[open-io-error] |
| B11 | bootstrap.py `_load_register_rows` names only a `PermissionError` cause | killed: 1 test(s): test_a_register_that_cannot_be_opened_claims_nothing_on_every_python |
| V4 | validate-manifest.py `_load` maps only a `PermissionError` cause | killed: 1 test(s): test_a_naming_policy_the_gate_cannot_open_is_named_on_every_python[io-error] |
| B3 | bootstrap.py `_is_file` takes anything for a file | killed: 2 test(s): test_a_manifest_that_is_a_directory_is_no_manifest_to_either_tool, test_a_validator_that_is_not_there_is_still_skipped[a-directory-in-its-place] |
| B15 | bootstrap.py `_a_register` takes anything for a register | killed: 1 test(s): test_only_a_file_is_a_register_where_the_stat_works[a-directory] |
| V2 | validate-manifest.py `_is_file` takes anything for a file | killed: 1 test(s): test_a_manifest_that_is_a_directory_is_no_manifest_to_either_tool |
| K1 | bootstrap.py `_is_a` ignores `kind` | killed: 3 test(s): test_a_manifest_that_is_a_directory_is_no_manifest_to_either_tool, test_a_validator_that_is_not_there_is_still_skipped[a-directory-in-its-place], test_only_a_file_is_a_register_where_the_stat_works[a-directory] |
| K2 | validate-manifest.py `_is_a` ignores `kind` | killed: 1 test(s): test_a_manifest_that_is_a_directory_is_no_manifest_to_either_tool |
| K3 | bootstrap.py `_found` asks for a regular file | killed: 2 test(s): test_anything_at_a_name_in_a_leg_is_found[a-directory], test_anything_at_a_name_in_a_leg_is_found[a-link-to-nothing-asked-of-the-name] |
| K4 | bootstrap.py `_found` drops `link` | killed: 3 test(s): test_a_marker_whose_lstat_is_refused_keeps_the_leg_on_every_python[a-merge], test_a_marker_whose_lstat_is_refused_keeps_the_leg_on_every_python[a-sequence], test_anything_at_a_name_in_a_leg_is_found[a-link-to-nothing-asked-of-the-name] |
| K5 | bootstrap.py `_found` takes only `PermissionError` | killed: 1 test(s): test_a_leg_whose_stat_is_refused_is_not_uninitialized[io-error] |
| R1 | M1: the header back to `relative_to(root)` | killed: 2 test(s): test_a_leg_in_a_folder_nobody_may_search_is_named_and_spelled[outside-the-root], test_a_register_under_a_leg_path_outside_the_root_is_read |
| R2 | N1: a refused `stat` headed `register` again | killed: 6 test(s): test_a_leg_bootstrap_cannot_search_is_left_as_it_is, test_a_leg_in_a_folder_nobody_may_search_is_named_and_spelled[inside-the-root], test_a_leg_in_a_folder_nobody_may_search_is_named_and_spelled[outside-the-root], test_a_register_bootstrap_cannot_read_is_named_not_absent[a-folder-nobody-may-enter], test_a_register_whose_stat_is_refused_is_not_dropped[io-error], test_a_register_whose_stat_is_refused_is_not_dropped[permission] |
| R3 | S2: `_a_validator` opens nothing | killed: 2 test(s): test_a_validator_at_mode_000_is_refused_before_any_runs, test_a_validator_whose_stat_is_refused_stops_step_b_before_any_runs[open-io-error] |
| R4 | S2: `_a_validator` takes only `PermissionError` | killed: 1 test(s): test_a_validator_whose_stat_is_refused_stops_step_b_before_any_runs[open-io-error] |
| R5 | S1: the policy back to `NamingPolicy.load(policy_path)` | killed: 3 test(s): test_a_naming_policy_the_gate_cannot_open_is_named_on_every_python[given-outside-the-root-with-an-esc], test_a_naming_policy_the_gate_cannot_open_is_named_on_every_python[io-error], test_a_naming_policy_the_gate_cannot_reach_is_refused_by_name |
| R6 | N3: `_shown` returns the name as it is | killed: 2 test(s): test_a_name_that_does_not_print_is_spelled_under_the_root_too, test_a_naming_policy_the_gate_cannot_open_is_named_on_every_python[given-outside-the-root-with-an-esc] |
| R7 | N3: the relative branch unspelled | killed: 1 test(s): test_a_name_that_does_not_print_is_spelled_under_the_root_too |
| R8 | N3: the whole-path branch unspelled | killed: 1 test(s): test_a_naming_policy_the_gate_cannot_open_is_named_on_every_python[given-outside-the-root-with-an-esc] |

**Round 0:** 18 mutants, one string each, run against the new file at `df1bb186` (the move commit after it changes no line). All were killed; none survived.

| Mutant | Caught by |
|---|---|
| M1 validate-manifest.py `:1085`: `_is_file` back to `manifest_path.is_file()` | killed: 2 test(s): test_a_manifest_the_gate_cannot_reach_or_open_is_refused_by_name[behind-a-folder-nobody-may-enter], test_a_manifest_whose_stat_is_refused_is_the_same_refusal |
| M2 validate-manifest.py: `_load` back to `load_yaml` | killed: 1 test(s): test_a_manifest_the_gate_cannot_reach_or_open_is_refused_by_name[at-mode-000] |
| M3 validate-manifest.py `:406`: `_is_file` back to `pin_path.is_file()` | killed: 4 test(s): test_a_contracts_folder_the_gate_cannot_search_names_the_pin, test_a_referent_pin_the_gate_cannot_reach_is_refused_not_missing, test_a_referent_pin_whose_stat_is_refused_is_refused_on_every_python[io-error], test_a_referent_pin_whose_stat_is_refused_is_refused_on_every_python[permission] |
| M4 validate-manifest.py: `_findings` back outside `main`'s `try` | killed: 4 test(s): test_a_contracts_folder_the_gate_cannot_search_names_the_pin, test_a_referent_pin_the_gate_cannot_reach_is_refused_not_missing, test_a_referent_pin_whose_stat_is_refused_is_refused_on_every_python[io-error], test_a_referent_pin_whose_stat_is_refused_is_refused_on_every_python[permission] |
| M5 bootstrap.py `:1328`: `_is_file` back to `manifest_path.is_file()` | killed: 2 test(s): test_a_manifest_bootstrap_cannot_reach_or_open_is_refused_first[behind-a-folder-nobody-may-enter], test_a_manifest_bootstrap_cannot_stat_is_the_same_refusal |
| M6 bootstrap.py: `_load_manifest` back to `load_yaml` | killed: 1 test(s): test_a_manifest_bootstrap_cannot_reach_or_open_is_refused_first[at-mode-000] |
| M7 bootstrap.py `:1409`: the look-ahead back to `(root / argv_[0]).is_file()` | killed: 2 test(s): test_a_scripts_folder_bootstrap_cannot_search_is_refused_not_skipped, test_a_validator_whose_stat_is_refused_stops_step_b_before_any_runs |
| M8 bootstrap.py `:1056`: `_found` back to `(sub / ".git").exists()` | killed: 3 test(s): test_a_leg_bootstrap_cannot_search_is_left_as_it_is, test_a_leg_whose_stat_is_refused_is_not_uninitialized[io-error], test_a_leg_whose_stat_is_refused_is_not_uninitialized[permission] |
| M9 bootstrap.py `:717`: `_found(..., link=True)` back to `os.path.lexists` | killed: 1 test(s): test_a_marker_whose_lstat_is_refused_keeps_the_leg_on_every_python[a-merge] |
| M10 bootstrap.py `:750`: the same for `sequencer/todo` | killed: 2 test(s): test_a_marker_whose_lstat_is_refused_keeps_the_leg_on_every_python[a-sequence], test_an_operation_bootstrap_cannot_see_keeps_the_leg_where_it_is |
| M11 bootstrap.py `:1191`: `_a_register` back to `c.is_file()` | killed: 3 test(s): test_a_leg_bootstrap_cannot_search_is_left_as_it_is, test_a_register_bootstrap_cannot_read_is_named_not_absent[a-folder-nobody-may-enter], test_a_register_whose_stat_is_refused_is_not_dropped |
| M12 bootstrap.py: a register's open refusal said as before (`unread = False`) | killed: 3 test(s): test_a_leg_bootstrap_cannot_search_is_left_as_it_is, test_a_register_bootstrap_cannot_read_is_named_not_absent[a-folder-nobody-may-enter], test_a_register_bootstrap_cannot_read_is_named_not_absent[at-mode-000] |
| M13 validate-manifest.py: ELOOP dropped from `_ABSENT_ERRNOS` | killed: 2 test(s): test_a_name_that_is_not_there_is_absent[validate-manifest.py-ELOOP], test_a_referent_pin_that_is_not_there_is_still_missing[link-to-itself] |
| M14 validate-manifest.py: EACCES added to `_ABSENT_ERRNOS` | killed: 6 test(s): test_a_contracts_folder_the_gate_cannot_search_names_the_pin, test_a_manifest_the_gate_cannot_reach_or_open_is_refused_by_name[behind-a-folder-nobody-may-enter], test_a_manifest_whose_stat_is_refused_is_the_same_refusal, test_a_referent_pin_the_gate_cannot_reach_is_refused_not_missing, test_a_referent_pin_whose_stat_is_refused_is_refused_on_every_python[permission], test_every_other_refusal_is_raised[validate-manifest.py-EACCES] |
| M15 bootstrap.py: ELOOP dropped from `_ABSENT_ERRNOS` | killed: 1 test(s): test_a_name_that_is_not_there_is_absent[bootstrap.py-ELOOP] |
| M16 bootstrap.py: EACCES added to `_ABSENT_ERRNOS` | killed: 12 test(s): test_a_leg_bootstrap_cannot_search_is_left_as_it_is, test_a_leg_whose_stat_is_refused_is_not_uninitialized[permission], test_a_manifest_bootstrap_cannot_reach_or_open_is_refused_first[behind-a-folder-nobody-may-enter], test_a_manifest_bootstrap_cannot_stat_is_the_same_refusal, test_a_marker_whose_lstat_is_refused_keeps_the_leg_on_every_python[a-merge], test_a_marker_whose_lstat_is_refused_keeps_the_leg_on_every_python[a-sequence], test_a_register_bootstrap_cannot_read_is_named_not_absent[a-folder-nobody-may-enter], test_a_register_whose_stat_is_refused_is_not_dropped, test_a_scripts_folder_bootstrap_cannot_search_is_refused_not_skipped, test_a_validator_whose_stat_is_refused_stops_step_b_before_any_runs, test_an_operation_bootstrap_cannot_see_keeps_the_leg_where_it_is, test_every_other_refusal_is_raised[bootstrap.py-EACCES] |
| M17 bootstrap.py: `_cure` gives the `chmod` advice for every cause | killed: 1 test(s): test_a_leg_whose_stat_is_refused_is_not_uninitialized[io-error] |
| M18 validate-manifest.py: the `chmod` advice for every cause | killed: 1 test(s): test_a_referent_pin_whose_stat_is_refused_is_refused_on_every_python[io-error] |

Each run is `run9s.sh` with `SHARDS=3` (three `run9.sh` processes: the gh stub first on `PATH`, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, a blank git identity), one run at a time, at head `b420171f`.

- **git 2.43.0, the first brief's whole list, 134 files: 9656 passed, 25 skipped (longest shard 40 min), rc 0.** 2026-10-19T11:37:42Z to 2026-10-19T12:17:59Z. The list is every test file that names the assembly root's `bootstrap.py` or `validate-manifest.py`, or runs a scaffolded root's `make bootstrap` or its validators, with #948's new `test_bootstrap_casefold_twin.py`, and `test_pinned_copy_text.py`, `test_bom_reads.py`, `test_validator_child_encoding.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py` and `test_cli_reference.py`.
  The 25 skips: 8 of `test_leg_path_yaml_scalars.py` (empty parameter sets, and the templates read the value back); 6 of `test_doctor_legs_members_next.py` (git 2.43 dies on a `shallow` of -2**31, and crashes on `submodule init`; CI's 2.55 does neither); 3 reftable cases of `test_bootstrap_ref_store.py` (git 2.45 and later); 3 NTFS junction cases of `test_update_shape_add_symlink.py`, 3 PowerShell lines of `test_shape_doctor.py` and 1 of `test_stdout_encoding.py` (Windows only); 1 of `test_bump_leg.py` (a disk that ignores case).
- **git 2.47.1 (a build first on `PATH`), 24 files: 1764 passed (longest shard 4 min), rc 0.** 2026-10-18T20:07:50Z to 2026-10-18T20:11:58Z. Files: `test_agents_md_codes.py`, `test_assembly_root_unreadable_paths.py`, `test_bom_reads.py`, `test_bootstrap_after_bump.py`, `test_bootstrap_branch_bytes.py`, `test_bootstrap_casefold_twin.py`, `test_bootstrap_leg_moves.py`, `test_bootstrap_non_utf8_branch.py`, `test_bootstrap_pasteable_lines.py`, `test_bootstrap_placement_texts.py`, `test_bootstrap_ref_store.py`, `test_bootstrap_refusal_exit.py`, `test_bootstrap_spelling.py`, `test_bootstrap_upstream_notice.py`, `test_bootstrap_without_git.py`, `test_bump_leg_unopenable.py`, `test_bump_leg_unreadable.py`, `test_cli_reference.py`, `test_manifest_validator.py`, `test_naming_policy.py`, `test_pinned_copy_text.py`, `test_repo_hygiene.py`, `test_validate_pins_named_and_spelled.py`, `test_validator_child_encoding.py`.
- A first 2.43.0 run of the same list at the same head, on 2026-10-18, was stopped from outside at 20:31Z (a signal; its shards had reached 52%, 29% and 41% with no failure); the run above is its re-run.
- Before #948 landed, the same list less `test_bootstrap_casefold_twin.py` (23 files), on git 2.47.1 at the round's commit on the earlier merge (`b220336b`): 1713 passed, rc 0.

<details><summary>The 134 files of the git 2.43.0 run</summary>

`test_adopt_attributes_eol_block.py`, `test_adopt_clone_template.py`, `test_adopt_e2e.py`, `test_adopt_git_reads.py`, `test_adopt_ignored_shape_files.py`, `test_adopt_manifest_values.py`, `test_adopt_mount_ignored.py`, `test_adopt_nested_attributes.py`, `test_adopt_origin_repository.py`, `test_adopt_pins.py`, `test_adopt_plan_yaml_quoting.py`, `test_adopt_reencoded_files.py`, `test_adopt_relative_submodule_url.py`, `test_adopt_root_manifest.py`, `test_adopt_root_symlinks.py`, `test_adopt_shape_pin_and_work_dir.py`, `test_adopt_spec_only.py`, `test_adopt_submodules.py`, `test_adopt_verify.py`, `test_agents_md_codes.py`, `test_assembly_root_unreadable_paths.py`, `test_bom_reads.py`, `test_bootstrap_after_bump.py`, `test_bootstrap_branch_bytes.py`, `test_bootstrap_casefold_twin.py`, `test_bootstrap_leg_moves.py`, `test_bootstrap_non_utf8_branch.py`, `test_bootstrap_pasteable_lines.py`, `test_bootstrap_placement_texts.py`, `test_bootstrap_ref_store.py`, `test_bootstrap_refusal_exit.py`, `test_bootstrap_spelling.py`, `test_bootstrap_upstream_notice.py`, `test_bootstrap_without_git.py`, `test_bump_leg.py`, `test_bump_leg_branch_and_push_line.py`, `test_bump_leg_dry_run_tracking.py`, `test_bump_leg_dry_run_truth.py`, `test_bump_leg_paste_lines.py`, `test_bump_leg_rollback.py`, `test_bump_leg_unopenable.py`, `test_bump_leg_unreadable.py`, `test_chain_remedy_callers.py`, `test_cli_reference.py`, `test_doctor_gitlink_repairs.py`, `test_doctor_gitmodules_not_utf8.py`, `test_doctor_legs_members_next.py`, `test_doctor_machine_row.py`, `test_doctor_members_spelled.py`, `test_doctor_next_is_a_command.py`, `test_doctor_no_lazy_fetch.py`, `test_doctor_non_ascii_paths.py`, `test_doctor_partial_clone.py`, `test_doctor_paste_through_git.py`, `test_doctor_pin_restore_merges.py`, `test_doctor_plan_overrides.py`, `test_doctor_recorded_answers.py`, `test_doctor_unfinished_merge.py`, `test_doctor_unreadable_pin.py`, `test_doctor_validator_rows.py`, `test_doctor_validator_site.py`, `test_doctor_validator_verdicts.py`, `test_family.py`, `test_family_bootstrap_gate_order.py`, `test_family_bump_remove_rollback.py`, `test_family_gitmodules_values.py`, `test_family_head_lost_gitmodules.py`, `test_family_holder_bootstrap_texts.py`, `test_family_makefile_siblings.py`, `test_family_member_name.py`, `test_family_members_not_a_list.py`, `test_family_siblings_placement.py`, `test_git_output_decoding.py`, `test_leg_path_yaml_scalars.py`, `test_legs_not_a_list.py`, `test_legs_readers_not_a_list.py`, `test_manifest_validator.py`, `test_naming_policy.py`, `test_non_list_project_fields.py`, `test_non_utf8_oserror_sites.py`, `test_park_resume_targets.py`, `test_pin_source_env_collisions.py`, `test_pinned_copy_text.py`, `test_pins_child_kind_and_siblings_root_spelling.py`, `test_push_branch_refspec.py`, `test_reference_default.py`, `test_repo_hygiene.py`, `test_scaffold_e2e.py`, `test_scaffold_free_text_values.py`, `test_scaffold_ignored_shape_files.py`, `test_scaffold_leg_path_collisions.py`, `test_scaffold_manifest_values.py`, `test_scaffold_mount_ignored.py`, `test_scaffold_pin_and_reuse.py`, `test_scaffold_referent_chain_links.py`, `test_setup_by_hand_commands.py`, `test_setup_project_py.py`, `test_setup_sh.py`, `test_shape_doctor.py`, `test_siblings_family_name.py`, `test_siblings_inputs.py`, `test_stdout_encoding.py`, `test_template_quoted_values.py`, `test_template_text_placeholders.py`, `test_template_values_single_pass.py`, `test_update_shape.py`, `test_update_shape_add.py`, `test_update_shape_add_ignored.py`, `test_update_shape_add_not_a_directory.py`, `test_update_shape_add_symlink.py`, `test_update_shape_apply_lines_and_headers.py`, `test_update_shape_branch_any_directory.py`, `test_update_shape_hardlink.py`, `test_update_shape_header_comments.py`, `test_update_shape_kept_helper.py`, `test_update_shape_landing.py`, `test_update_shape_merged_away.py`, `test_update_shape_messages.py`, `test_update_shape_next_quoting.py`, `test_update_shape_partial_rollback.py`, `test_update_shape_paste_lines.py`, `test_update_shape_pr_base.py`, `test_update_shape_rewrite_symlink.py`, `test_validate_family_branch_step.py`, `test_validate_family_member_path_glob.py`, `test_validate_family_remedies_typed.py`, `test_validate_family_typed_states.py`, `test_validate_pins_lockstep_lines.py`, `test_validate_pins_named_and_spelled.py`, `test_validate_workflow_gitmodules_urls.py`, `test_validator_child_encoding.py`, `test_windows_paths.py`, `test_yaml_indicator_openers.py`, `test_yaml_reader_spaces.py`

</details>

The branch merged main twice. Most recently, `cf8d5667` merges origin/main `2a78ae1a` (#984, landed) into round 1's head `b420171f`: its one conflict, the import block of `bootstrap.py`, keeps #984's `import unicodedata` and this branch's `from collections.abc import Callable, Iterator`, and the branch's own `_shown` in `validate-manifest.py` is deleted in favour of #981's `_shown(path)` (the account of that merge is on the PR). Before that, `bb58c441` merges origin/main `ebbd9fa4` (#948, #896's twin refusals, landed) into `0ef875c0` with no conflict (`bootstrap.py` auto-merged), and the round's commit `b420171f` sits on it. The round was first written on an earlier merge of main (`00a6f839`, `2f5cabf5`); that merge was replaced, unpushed, once #948 landed. The round's commit was reapplied unchanged (the same `git patch-id`). Its tree is the one `git merge-tree` predicted for the earlier round commit with `ebbd9fa4` (`9f4c2991`).

`git merge-tree --write-tree` of head `b420171f`, each ref fetched with `gc.auto=0` and `maintenance.auto=false` just before:

- origin/main `c7a3ff16`, re-fetched on 2026-10-19 after the head was pushed: clean as text (tree `977f0980`), and NOT merged here: the landing pilot refreshes the head under the fast-refresh ruling. Since `ebbd9fa4`, main's #981 (`ca6a0b40`) changes `validate-manifest.py`, and its own `_shown(path)` (`text = str(path)`, then `isprintable` or `repr`) lands beside this branch's `_shown(typed)`: the merged file defines `_shown` twice. The two give the same answer for every string this branch passes, the later definition wins, and the new file passes on the merged tree (71 passed, `$Q/v118/w430d/r1/m981.log`), but the refresh should delete this branch's `_shown` and keep #981's. Main's other changes in the files near this slice are to `validate-pins.py` and `tests/conftest.py`, which this branch does not touch.
- #948 at `45415296`: clean (tree `9f4c2991`, the head's own).
- #973 at `b2887cf5` (`fix/961-paste-words`): clean. #973 does not yet change the NOT INITIALIZED line; when it does, the new file's `not_initialized` helper is the one place to follow it.
- Each clean merge's `bootstrap.py` parses, with no top-level name defined twice.
- #962's `wip/l5-962-create-twins-20261018` at `414e9e0a`: conflicts in `bootstrap.py`, 8 hunks. 7 of them are with main alone: #962 is stacked on #948's head before #948 was squashed, and `git merge-tree` of origin/main with it gives the same 7. The 8th is this branch's only one, the import lines: `from collections.abc import Callable, Iterator` here against #962's `import unicodedata` and `from collections.abc import Iterator`. Taking both resolves it.

- No duplicated block: the new file shares no run of five code lines with any test file but an import block, reuses `as_a_descendant_by_chain`, `link_tree`, `refused_stat`, `failing`, `os_error`, `mode_of`, `CANNOT_RUN_AS_ROOT` and the cure constants instead of copying them, and puts its own repeated lines in helpers (`refused`, `left_as_it_is`, `nothing_claimed`, `not_initialized`, `refusing`, `leg_path`, `up_to_step_b`, `skipping_validators`, `a_register`).
- No assert joins conditions with `and`.
- Cognitive complexity, counted by hand: `checkout_tracking_branch` 13, unchanged; `_load_register_rows` 11; `_in_progress` 8; `_run_neutral_validators` 7; `_report_one_register` 6; `_register_paths` 4; `read_authority` 4; `validate-manifest.py`'s `main` 9 and `_load` 3; every other new function 2 or less.
- Every added line is within 79 columns.

Round 0's first two candidates (the naming policy named by its absolute path, and a validator the child cannot open) are fixed in round 1, on RV958's S1 and S2.

- **The assembly root's `validate-repository-naming.py` names a naming policy it cannot read by its absolute path, with no remedy.** Measured with `contracts/` at 000 in a full `bootstrap.py` run with round 1's change: `REFUSED yaml-unreadable: <ROOT>/contracts/repository-naming.yaml: cannot be read (Permission denied)`, while `validate-manifest.py`'s refusal just after it names `contracts/repository-naming.yaml` with the remedy. Not this slice's file.
- **`OTHER_REASON` tells a validator's I/O error to "take away one nobody wants".** A validator taken away is `SKIPPED`. #934 gives bump-leg.py a `git checkout --` line instead. These are validate-pins.py's words, kept as the plan rule asks.
- **The cure's "(and on every directory above it inside the root)"** reads oddly for a folder outside the root (an absolute leg path, a `--policy` given elsewhere). validate-pins.py's words, as RV958's N2 found for a symlinked `project.yaml`.
- **Seen in the code, not measured:** validate-manifest.py prints `project.yaml`'s absolute path raw in `manifest-missing`, `manifest-unreadable` and `<n> finding(s) in <path>`, where `validate-pins.py` and `bootstrap.py` spell every path they print (#391, #712).

Part of #430

Review: RV958, lane 4's independent reviewer, not the writer (w430d): READY at `7e87e05f` (issuecomment-6278103469), the reviewed head, after its READY on round 1 at `cf8d5667` (issuecomment-6271860975); the delta was a merge of main and one test-only commit for CI. CI (Brett Heap's ruling 27): H0 is the head 7c06c57 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 051176c; and the local gate passed on its predicted squash tree 194700b over main 051176c: the adopt_reencoded_files, assembly_root_unreadable_paths, repo_hygiene, agents_md_codes, cli_reference sets, 294 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green".


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

Labels

None yet

Projects

None yet

1 participant