Skip to content

tests/test_path_guard_hygiene.py and shape-doctor.py: a bare is_file/is_dir/exists guard in a shipped tool fails by file and line unless its _is_a or a reasoned allowlist covers it, and a project.yaml linked into an unsearchable folder is yaml-unreadable (#430) - #1111

Merged
brettheap merged 6 commits into
mainfrom
fix/430-path-guard-hygiene
Oct 21, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

What

Slice Z of lane 1's plan for #430 (issuecomment-6251069236), the slice that lands last. Slices A to H moved each shipped tool's Path.is_file(), is_dir(), exists() and is_symlink() guards onto a local _is_a (is_a in the doctor). From Python 3.14 those methods answer False to any OSError, and os.path.isfile, isdir, exists, lexists and islink always have. The helper treats ENOENT, ENOTDIR, EBADF and ELOOP (and Windows' 21, 123 and 1921) as absent and raises every other OSError, so the caller refuses it by name: <path>: cannot be read (<reason>), exit 2.

Slice Files PR
A scripts/bump-leg.py #934
B update-shape.py #976
C shape-doctor.py, scripts/shape_advisory.py #975
D the assembly root's validate-manifest.py and bootstrap.py #958
E the family root's validate-family.py #956
F the family root's siblings.py and bootstrap.py #951
G scripts/family.py #955
H adopt-project.py #970

This PR adds tests/test_path_guard_hygiene.py, which stops a new bare guard from bringing the old answer back. It also changes one site the scan found in shape-doctor.py.

The hygiene test

The parity test

  • Where the sets come from. Each top-level _is_a and is_a in a shipped tool has its sets read from the AST. That covers the twelve the slices wrote, and any added later. The test follows the helper through every module function it calls (_absent, _mode_of, _says_absent, refused_read). It reads each <exc>.errno in <set> and each getattr(<exc>, "winerror", None) in <set>, through the module constant, as errno.<NAME> names and numbers. A set it cannot read that way fails; it is not guessed at.
  • What each helper must hold. Every set must be {ENOENT, ENOTDIR, EBADF, ELOOP} and {21, 123, 1921}. On the path from its stat to its raise:
    • it may catch only OSError and ValueError. A handler for PermissionError, Exception, or a bare except: fails;
    • every except OSError must hold a bare raise;
    • errno and winerror may be read only as in <set>. An == errno.EACCES beside the set fails.
  • Helpers must be plain functions. A class or an async def by a helper's name fails.
  • Each slice's helper must still be there. That way none is held to nothing because it was renamed.
  • The constants. Every module constant named *ABSENT_ERRNOS or *ABSENT_WINERRORS must be the one set. There are 24, two per helper.
  • Results on this head. All twelve helpers hold the set: adopt-project.py, update-shape.py, shape-doctor.py (is_a), scripts/bump-leg.py, scripts/family.py, scripts/shape_advisory.py, and the assembly root's validate-pins.py, validate-manifest.py and bootstrap.py, and the family root's validate-family.py, bootstrap.py and siblings.py.

What the scan finds

On main 4ed7c7bf the scan finds 191 bare guards in 18 of the 25 files: the 188 of 16f9a0e8, 6da757cb and 823624d9, and three that #1091 added to adopt-project.py. None is inside a helper.

  • 1 is the real site below, changed here. On this head the scan finds 190.
  • 179 are on BENIGN, in 175 entries. Each carries the plan's own reason for leaving a site out (lane 1's site table at 5017830) and a line of its own:
Tag Entries (sites) What it means
OWN 57 (58) the tool's own checkout of the standard, its templates or package, its cache, a temporary or work directory it made, or what this run wrote
DELIBERATE 63 (65) a deliberate os.path call or guard whose False the code relies on and says why: wording, advice, family.py's members-spelling twins, or a check a by-name refusal follows (#608, #609, #798, #735, #784, #793, #764)
ASKED 25 (25) asked only after the stat helper, a stat, mount_read or git asked the path or its folder, or inside a try that keeps the OSError
ROOT 20 (21) a name directly in the root: asked by lstat, or where a link at the name is left out or handled as a link first, in a root the tool refuses by name first where it cannot search it
DEST 9 (9) the plan's OPEN-DEST, loud: a destination asked before a create or a clone, which then fails by name
PINNED 1 (1) scripts/repo_shape.py's find_repo_root, which the plan left (pinned, low)
Site What happens
scripts/family.py _gitmodules_names measured in process: on 3.12 a PermissionError raised out of remove; under the shim (None, []), so remove --dry-run says no section is removed. Slice G left it for this slice to flag. A change needs a refusal threaded through remove and its rollback.
scripts/family.py _sections_giving the same file: [] under the shim
templates/family-root/scripts/siblings.py submodule_urls the same, in a pinned copy, which this slice does not edit
shape-doctor.py check_agent_files an agent file linked that way is absent in the agent files note, which never moves the verdict
shape-doctor.py check_what_is_here the same, for the NOT A SHAPE ROOT row's facts
shape-doctor.py check_the_way_in a .git linked that way: under the shim the way-in row says there is no repository to adopt, and sends the person to setup.sh to scaffold three new ones; on 3.12 the row names .git: cannot be read
shape-doctor.py check_the_other_way_in the same .git: the way-in-scaffold row is left out
shape-doctor.py origin_org the same .git, measured alone: None under the shim, the answer for a folder that is no repository ('acme' in the readable control); 3.12 raises PermissionError
shape-doctor.py repo_local_origin_name the same .git, measured alone: None under the shim ('Orbit' in the control), so the naming row classifies Atlas alone, not Atlas, Orbit; 3.12 raises
scripts/shape_advisory.py _read the same .git: read() says not-a-repository under the shim (advise in the control), so the Triad advisory row is left out
scripts/shape_advisory.py repository_names the same .git, measured alone: ['Atlas'] under the shim (['Atlas', 'Orbit'] in the control); 3.12 raises

The site changed here: the doctor's own manifest

shape-doctor.py's Context.__init__ asked path.is_file() of the root's project.yaml and family.yaml. Slice C's root check in main asks only whether the root can be searched, so a project.yaml that is a link into a folder this user cannot search got past it.

It is now asked with is_a, and a refused stat is loaded (Context._to_load). load_yaml then refuses it as it refuses a file it cannot open, and the manifest row names it, as it already did for a project.yaml at mode 000 (#205).

Measured on a root holding project.yaml -> ../locked/project.yaml with locked/ at 000, Python 3.12.3, and the 3.14-like shim:

Before (16f9a0e8) After
3.12 PermissionError traceback at Context.__init__, exit 1 kind assembly root (project.yaml, which does not load), FINDING manifest ... REFUSED yaml-unreadable: .../project.yaml: cannot be read (Permission denied), INVALID (manifest, manifest-kinds), exit 1
3.14-like NOT A SHAPE ROOT, exit 2, with adopt-project.py plan --source ... as the NEXT, over a project byte for byte the 3.12 report above

The manifest kinds row names the same path with the chmod cure, in validate-pins.py's words. Where the stat succeeds, is_a(path, stat.S_ISREG) answers as is_file() did, so every other root reads as it did. An I/O error from the stat is loaded and named the same way.

Out of scope here (r1111's N5). The row this case now shows carries two older pieces of wording, from #205 and #847, which this PR leaves alone:

  • the manifest row's WHY for yaml-unreadable speaks of "a merge conflict resolved, a hand edit";
  • the chmod cure says "every directory above it inside the root", where the locked folder lies outside the root.

Mutants

Each mutant was applied once to a scratch copy of the head (git archive of the merge with main, with the new test file laid over it), and the new file was run there. The file was then put back from its bytes and checked by sha256. The worktree was never changed.

  • Round 1: 5 of 5 killed.
Mutant Fails
M1: scripts/bump-leg.py _validators_readable back to path.is_file() the hygiene test for that file, naming scripts/bump-leg.py:1474
M2: errno.EACCES added to validate-pins.py's _ABSENT_ERRNOS both parity tests
M3: 1921 dropped from update-shape.py's _ABSENT_WINERRORS both parity tests
M4: the allowlisted path.is_file() in update-shape.py's _extend_the_eol_block taken out the stale-entry test
M5: Context.__init__ back to path.is_file() the doctor tests
  • Round 2: 17 of 17 killed.
Mutant Fails
r1111's X3: a guard through import os as _os the hygiene test for that file
r1111's X4: if not path.exists(): return False inside bump-leg's _is_a the hygiene test (a guard in the helper)
r1111's X5: except PermissionError: return False before except OSError parity: catches PermissionError
r1111's X6: or exc.errno == errno.EACCES beside the set parity: reads exc.errno other than as in <set>
r1111's X15: a class _is_a holding a bare guard hygiene and parity
r1111's X18: except Exception: return False before except OSError parity: catches Exception
r1111's T2: the stale check disabled the count self-test
r1111's T3: one entry silences any count the count self-test
round 1's W1 and W4 hygiene, and the stale-entry test
Y1: the handler rule loosened the parity self-test
Y2: an errno read other than in no longer refused the parity self-test
Y3: import os as o no longer followed the scanner self-test
Y4: p = os.path no longer followed the scanner self-test
Y5: the helper exemption put back the scanner self-test
Y6: a class or async def by a helper's name read as a function the parity self-test
Y7: except PermissionError in _to_load (r1111's X12) the doctor test's I/O-error row
Entry taken out Fails
B1: _PlannedRoot._no_directory the hygiene test for adopt-project.py, naming adopt-project.py:10785
B2: _PlannedRoot._into_git the same test, naming adopt-project.py:10817
B3: _clone_ref_store the same test, naming adopt-project.py:11113
  • The file's self-tests. Slice A's helper passes the parity test, and eleven one-line changes to it are each named: three to its sets, the re-raise, a constant's name, three handlers, ==, an async def and a class.

Tests

  • tests/test_path_guard_hygiene.py, new: 49 tests.
    • 25 hygiene rows, one per tool.
    • The stale-entry, tag, scanner, count and parity tests.
    • Five tests for the doctor change:
      • two in process (refused_stat and a refused read_text, for a permission and for an I/O error), on every Python and as root;
      • two controls, nothing there and a directory there, which still read as no manifest;
      • one mode case run as a person runs the doctor, skipped on Windows and as root.
  • Against main's tools (main 4ed7c7bf, with the new file laid over it), 45 pass and 4 fail. The 4 are the doctor's unlisted Context.__init__ guard and its three rows: the two in-process rows, the I/O-error row among them, and the mode case. On this head all 49 pass.
  • The gate on head d286ae9a. The head holds main 4ed7c7bf through the plain merge 8bc4d1b2, with tree fc0731ea; the branch's first merge, 03c16cb9, took 6da757cb. Against main, the head changes only shape-doctor.py and the new file. The gate ran on the head itself, with Python 3.12.3, git 2.43.0, a stub gh first on PATH, GH_TOKEN bogus, an empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null and GIT_SSH_COMMAND=false:
    • the new file, tests/test_repo_hygiene.py, tests/test_cli_reference.py, tests/test_agents_md_codes.py, tests/test_doctor_unreadable_paths.py and tests/test_doctor_unreadable_pin.py: 305 passed;
    • tests/test_shape_doctor.py, 448 tests in four shards: 445 passed and 3 skipped (118 passed; 110 passed; 117 passed; 100 passed and 3 skipped);
    • tests/test_scaffold_leg_path_collisions.py: 104 passed.
  • No --help text changes, so docs/cli.md is unchanged. No pinned copy changes, so no project or holder gets an upstream-changed row.

Closes #430

Lane: openreposhape-1 (openRepoShape-1)

🤖 Generated with Claude Code

Brett Heap and others added 2 commits October 21, 2026 12:50
…`, `is_dir`, `exists`, `is_symlink` or `os.path` guard in a shipped tool fails by file and line unless it is in its file's `_is_a` or on a reasoned allowlist, every `_is_a` holds the one absent errno set, and a `project.yaml` linked into a folder nobody may search is the `manifest` row's `yaml-unreadable`, never NOT A SHAPE ROOT (#430)

Slice Z of lane 1's plan for #430, after slices A to H (#934, #976,
#975, #958, #956, #951, #955, #970).

- tests/test_path_guard_hygiene.py, new, 41 tests.
  - The hygiene test reads every shipped tool with `ast` (SHIPPED, and
    every *.py at the top, in scripts/, templates/**/scripts/ and
    src/openreposhape/: 25 files). Each call of the four Path methods
    and the five os.path tests, however spelled or passed, must be in
    the file's own `_is_a`/`is_a`, or on BENIGN (178 entries, 182
    sites, each tagged with the plan's reason and a line of its own) or
    KNOWN_OPEN (5 real sites left to FOLLOW-UP-430Z, out of v1.2.0).
    Keys are file, function and the call's text, never a line. A stale
    entry fails.
  - The parity test reads each helper's absent errnos and Windows
    errors out of the AST, through the module constants and the
    functions it calls, holds all twelve (and any later) to {ENOENT,
    ENOTDIR, EBADF, ELOOP} and {21, 123, 1921}, and asks that every
    `except OSError` on the way re-raises.
- shape-doctor.py: Context.__init__ asks the root's manifests with
  `is_a`, and loads one whose `stat` is refused (`_to_load`), so
  `load_yaml` names it as it names one at mode 000 (#205). A
  project.yaml linked into a folder at 000 was a PermissionError
  traceback on 3.12 and NOT A SHAPE ROOT with an adopt NEXT on 3.14;
  it is now INVALID (manifest), exit 1, the same on both.

Bite proofs in a scratch copy, each file put back and checked by
sha256: a bare guard in bump-leg.py, EACCES added to validate-pins.py's
set, 1921 dropped from update-shape.py's, an allowlisted guard taken
out, and the doctor change reverted each fail the test meant for them.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re left to #1110, the follow-up lane 1 filed, in place of the `FOLLOW-UP-430Z` placeholder (#430)

The follow-up is out of v1.2.0 (Brett Heap, 2026-10-21T10:32Z). The new
file passes: 41 passed.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap brettheap changed the title tests/test_path_guard_hygiene.py and shape-doctor.py: a bare is_file, is_dir, exists, is_symlink or os.path guard in a shipped tool fails by file and line unless it is in its _is_a or on a reasoned allowlist, every _is_a holds one absent errno set, and a project.yaml linked into an unsearchable folder is yaml-unreadable (#430) tests/test_path_guard_hygiene.py and shape-doctor.py: a bare is_file/is_dir/exists guard in a shipped tool fails by file and line unless its _is_a or a reasoned allowlist covers it, and a project.yaml linked into an unsearchable folder is yaml-unreadable (#430) Oct 21, 2026
@sourcery-ai

sourcery-ai Bot commented Oct 21, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR closes the path-guard migration by adding repository-wide AST hygiene and helper-parity enforcement, while fixing shape-doctor so an unreadable linked project manifest is classified and reported as yaml-unreadable rather than silently absent or crashing.

Sequence diagram for unreadable linked project manifest handling

sequenceDiagram
    participant Doctor as shape-doctor
    participant Context
    participant Guard as is_a
    participant FS as Filesystem
    participant Loader as load_yaml

    Doctor->>Context: __init__(root)
    Context->>Context: _to_load(project.yaml)
    Context->>Guard: is_a(path, stat.S_ISREG)
    Guard->>FS: stat(project.yaml)
    FS-->>Guard: OSError
    Guard-->>Context: raises OSError
    Context->>Context: _to_load catches OSError
    Context->>Loader: _load(project.yaml)
    Loader-->>Context: yaml-unreadable refusal
    Context-->>Doctor: manifest row names project.yaml
Loading

Flow diagram for repository path-guard hygiene enforcement

flowchart TD
    Files["Shipped Python files"] --> Scan["AST scan for bare path guards"]
    Scan --> Allow{In _is_a/is_a or allowlisted?}
    Allow -- No --> Failure["Fail with file, line, function, and remedy"]
    Allow -- Yes --> Entries["Validate allowlist entry"]
    Entries --> Stale{Entry still matches call and count?}
    Stale -- No --> Failure
    Stale -- Yes --> Tags["Require valid tag and reason"]
    Tags --> Pass["Hygiene checks pass"]
Loading

File-Level Changes

Change Details Files
Adds AST-based hygiene and parity coverage to enforce safe path-guard usage across all shipped tools.
  • Scans shipped Python tools for bare Path and os.path existence/type guards, including aliases, multiline calls, and function references.
  • Requires each guard to use the file-local stat helper or a documented, tagged allowlist entry; detects stale, duplicate-count, untagged, and unreasonable entries.
  • Verifies every _is_a/is_a helper and absent-error constant uses ENOENT, ENOTDIR, EBADF, ELOOP plus Windows errors 21, 123, and 1921, while re-raising other OSErrors.
  • Tests the scanner and parity logic with mutation-style fixtures and records known-open follow-up sites.
tests/test_path_guard_hygiene.py
Routes shape-doctor manifest discovery through refusal-aware loading so unreadable manifests are reported instead of treated as absent or raising an uncaught error.
  • Replaces the direct project.yaml/family.yaml is_file() filter with _to_load.
  • Uses is_a(..., stat.S_ISREG) for normal checks and treats refused stats as candidates for _load, preserving yaml-unreadable reporting.
  • Adds regression coverage for refused stats and reads, absent or directory manifests, and symlinks into unsearchable directories.
shape-doctor.py
tests/test_path_guard_hygiene.py

Assessment against linked issues

Issue Objective Addressed Explanation
#430 Ensure unreadable paths are refused by name rather than treated as absent, and do not let unreadability silently skip required checks. ❌ The PR reports that five known sites remain unresolved, including scripts/family.py paths that can make remove --dry-run report that no section is removed and a pinned siblings.py path that can make cloning use a default URL. The hygiene test records these as KNOWN_OPEN rather than fixing them. The diff itself fixes only the doctor's manifest-loading case.
#430 Provide one shared path-classification helper for the shipped tools and pinned copies. ❌ The PR uses separate local _is_a/is_a helpers in twelve files and explicitly says there is no shared helper. Its parity test keeps those copies consistent, but does not implement the issue's requested single helper in scripts/repo_shape.py.
#430 Treat only FileNotFoundError and NotADirectoryError as absence; propagate other OSError results so callers can refuse them. ❌ The PR's helpers also classify EBADF, ELOOP, and Windows errors 21, 123, and 1921 as absent, contrary to the issue's requirement that any error other than file-not-found or not-a-directory is not an answer.

Possibly linked issues


Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-21T13:35:34Z) r1111, lane 1's independent reviewer (not the writer), reviewed #1111 at 9cb946be: NOT READY, and PARTIAL because the usage window forced a checkpoint. One MUST (M1: six BENIGN "ROOT" entries are open #430 sites; they move to KNOWN_OPEN and #1110) and three SHOULDs go to a fix round when the window has room. The merge with main 6da757cb is clean, and every test passed on the merged tree: hygiene 41, doctor 445 (3 skipped), path collisions 104. The review follows in full.


Verdict: NOT READY. This review is PARTIAL: the unfinished checks are listed at the end. One MUST blocks the merge. Everything else passes, including the merge with main.

Merge with main (6da757cb)

  • git merge-tree --write-tree 9cb946be origin/main is clean, tree 14e6405b. I built it as a scratch commit (84d543ee) in a --shared scratch clone and ran it in the probe environment.
  • tests/test_path_guard_hygiene.py: 41 passed.
  • tests/test_shape_doctor.py in 4 shards: 118, 110, 117, and 100 passed with 3 skipped. That is 445 passed and 3 skipped of 448.
  • tests/test_scaffold_leg_path_collisions.py: 104 passed.
  • Scanning each tree with the head test module:
Tree Bare guards Files Unlisted Stale
base 16f9a0e8 188 18 1 (shape-doctor.py:1369 Context.__init__ path.is_file()) –
main 6da757cb 188 18 the same 1 –
merged 187 18 0 0

So #1075 and #1090 add no guard.

MUST

M1. Six BENIGN "ROOT" entries are open #430 sites with a false reason, and they are in neither KNOWN_OPEN nor #1110.

SHOULD

S1. The parity test can be beaten by a helper that reads EACCES as absent. These mutants of scripts/bump-leg.py's _is_a all pass the 41 tests:

  • X5: except PermissionError: return False placed before except OSError;
  • X18: except Exception: return False;
  • X6: or exc.errno == errno.EACCES beside the set.

The test only reads in <set> comparisons (:944-955), and reraises only asks that a bare raise occurs somewhere in each except OSError (:956-959). The body's "must end in a bare raise" overstates this.

Fix:

  • refuse any other handler on the way except ValueError, including OSError subclasses, Exception and a bare except:;
  • refuse any errno or winerror comparison that is not in <set>.

S2. A bare guard inside a helper is exempt (X4 survived). if not path.exists(): return False added at the top of bump-leg's _is_a passes, because in_a_helper (:714-717) exempts the helper's whole body. Today 0 of the 187 sites sit in a helper, so the fix costs nothing: drop the exemption, or assert there is none.

S3. #1110 misquotes one site. It gives check_agent_files as (root / name).exists(), but the code at shape-doctor.py:9895, and KNOWN_OPEN, have (ctx.root / name).is_file(). The other four match. Fix: edit #1110's body.

NIT

Body claims

Claim Result
25 files; the glob adds none that SHIPPED lacks true
188 bare guards in 18 of 25 files on 16f9a0e8; none in a helper true
182 sites on BENIGN in 178 entries; tag table (54/55, 63/65, 25/25, 26/27, 9/9, 1/1) true as counts; the six ROOT entries are misclassified (M1)
5 on KNOWN_OPEN, left to #1110, not in v1.2.0 true (#1110 has no milestone); S3 is a misquote
_gitmodules_names: 3.12 raises, shim gives (None, []) true (in-process; _sections_giving gives [])
17 written spellings, no stat/lstat true
12 helpers, 24 constants true
"every except OSError must end in a bare raise" false as stated (S1)
Doctor before and after, 3.12 and shim, shim byte for byte true (cmp identical); adopt NEXT appears when the root is a git repository
manifest kinds row gives the chmod cure true
Bite proofs M1, M4 and M5 true (W1 and W4 killed; X10 and X13 cover M5)
Bite proofs M2 and M3 true in kind (X7 and X8 killed)
41 tests true
test_shape_doctor.py 445 passed, 3 skipped true (on the merged tree)
297-passed gate unverified
No --help change, no pinned copy changed true
Only Closes #430 true: no other keyword before an issue number; closingIssuesReferences = [430]
Docs (README, docs/, AGENTS.md, templates/) say nothing now untrue true (README: NOT A SHAPE ROOT means "neither manifest is there")

Doctor, base against head

The root holds project.yaml -> ../locked/project.yaml with locked/ at 000, run with /usr/bin/python3 3.12.

Doctor 3.12 3.14-like shim
base shape-doctor.py (sha adb2e864) PermissionError at line 1369 in __init__, exit 1 NOT A SHAPE ROOT, exit 2
head INVALID (manifest, manifest-kinds), exit 1, REFUSED yaml-unreadable ... cannot be read (Permission denied) byte-identical to 3.12

Head's new tests run against base's doctor: the in-process test and the mode test fail; the two controls pass.

Mutants: 16 killed of 26

  • Killed: X1, X2, X7, X8, X9, X10, X11, X13, X14, X16, X17, T1, T4, T5, W1, W4.
  • Survived:
    • X3: an import os as alias;
    • X4: a guard inside the helper;
    • X5, X6 and X18: EACCES read as absent;
    • X12: except PermissionError in _to_load, so an EIO stat would traceback; low;
    • X15: a class named _is_a;
    • T2: stale check disabled;
    • T3: count logic loosened;
    • T6: the KNOWN_OPEN owner check loosened; trivial.

Every mutant ran in the scratch clone headc and was put back. The sha256 of each file matched its HEAD blob, for example bump-leg 60a30b03, doctor fe84e73a and the test file a30876a1. My worktree is untouched at 9cb946be.

BENIGN spot check (32 entries)

Benign (✓, 25):

  • adopt-project.py: _top_level, _not_a_whole_clone (both asked after _is_there), Source.clone, _why_out_cannot_be_written, _holds_something, _create_leg_remote.
  • scaffold-project.py: local_remote_is_empty.
  • scripts/family.py: _landing, _moved_or_kept, _checked_index_unlocked.
  • scripts/shape_materialize.py: copy_in_the_way, _leg_path_lines.
  • setup-project.py: family_manifest_here, family_next_command.
  • shape-doctor.py: gate_line, shape_file_state, sibling_slot, same_repository_here, members_verdict.
  • update-shape.py: _extend_the_eol_block.
  • templates/assembly-root/scripts/bootstrap.py: shape_upstream_notice.
  • templates/assembly-root/scripts/validate-pins.py: _has_own_gate.
  • templates/family-root/scripts/bootstrap.py: gitmodules_sections.
  • templates/family-root/scripts/siblings.py: holds_git, same_directory.

Benign in effect, but the stated reason is loose or unread (~, 6):

Not benign (✗): all six M1 entries. Of these:

  • check_the_way_in and check_the_other_way_in were measured, and so were shape_advisory's _read and repository_names (the Triad row vanishes);
  • origin_org and repo_local_origin_name are the same class and were not measured one by one.

Not finished

  1. The writer's 297-passed gate (the new file, test_repo_hygiene, test_cli_reference, test_agents_md_codes, test_doctor_unreadable_paths, test_doctor_unreadable_pin) was not re-run on head or on the merged tree.
  2. origin_org and repo_local_origin_name were not measured one by one.
  3. detached's callers were not traced.
  4. Python 3.14 itself was not run: every 3.14 statement above comes from my shim.

Scratch probes and logs are in /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1111/ (probes/, run_merged.log, mutants.log, w/).

Lane: openreposhape-1 (openRepoShape-1)

…` that follows a link, is six open sites left to #1110, a helper may catch only `OSError` and `ValueError` and read `errno` only as `in <set>`, a guard inside a helper fails, and `import os as o`, `p = os.path` and the counts are held too (r1111's M1, S1, S2, N1-N4) (#430)

r1111's review of #1111 at 9cb946b, fix round f1111, on the merge of
main 6da757c (03c16cb).

- M1: shape-doctor.py's check_the_way_in, check_the_other_way_in,
  origin_org and repo_local_origin_name, and shape_advisory.py's _read
  and repository_names, move from BENIGN (ROOT) to KNOWN_OPEN under
  #1110: `(root / ".git").exists()` follows a `.git` linked into a
  folder this user cannot search. Measured alone under a 3.14-like
  shim: origin_org None (control 'acme'), repo_local_origin_name None
  (control 'Orbit'), repository_names ['Atlas'] (control ['Atlas',
  'Orbit']); 3.12 raises. ROOT's text and _members_on_disk's reason
  say how a link there is handled. BENIGN is 172 entries (176 sites),
  ROOT 20 (21), KNOWN_OPEN 11.
- S1: on the way from the helper's stat to its raise, a handler for
  anything but OSError and ValueError, an `except OSError` with no bare
  raise, and an errno or winerror read other than `in <set>` fail.
- S2: no helper exemption: a guard inside a helper fails, and no entry
  may name one.
- N1: `import os as o` (o.path), and `p = os.path` to any depth.
- N2: a class or async def by a helper's name fails parity.
- N3: self-tests for the counts and the stale check.
- The doctor's in-process test gains an I/O-error row (r1111's X12).

49 tests. Mutants: r1111's X3, X4, X5, X6, X15, X18, T2 and T3, round
1's W1 and W4, and Y1-Y7, 17 of 17 killed.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-21T15:30:43Z) r1111b, lane 1's independent reviewer (not the writer), re-checked #1111 at b4484647: READY on condition. Lane 1 has now met the condition. Every round-1 finding is resolved. The merge with main 823624d9 is clean, and every test passed on it: hygiene 49; the 297 list 305; doctor 445 (3 skipped); path collisions 104. The condition was SHOULD 1, the body's false "No shipped tool has one today". Lane 1 has edited the body: it now names the three Windows-only isjunction lookups, and it adds NIT 2's misses to the "not caught" list. NITs 1 and 3-6 go to a follow-up issue outside v1.2.0. The review follows in full.


Verdict: READY on condition. The condition is one sentence in the PR body (SHOULD 1 below): an edit to the body only, needing no commit and no new gate run. No MUST. Every round-1 finding is resolved, except T6 (a NIT) and a remainder of S1's class that the full suite catches anyway.

Merging with current main (823624d9)

  • I fetched main: it is 823624d9. git merge-tree --write-tree b4484647 823624d9 is clean, giving tree 14a95d88. I built it as scratch commit b1b8cd5f in a shared scratch clone. It differs from main only in shape-doctor.py and tests/test_path_guard_hygiene.py.
  • All runs used the probe environment, with Python 3.12.3 and git 2.43.0:
Run Result
tests/test_path_guard_hygiene.py 49 passed
tests/test_shape_doctor.py, 4 shards 118, 110, 117, and 100 with 3 skipped: 445 passed, 3 skipped of 448
tests/test_scaffold_leg_path_collisions.py 104 passed
the new file, test_repo_hygiene, test_cli_reference, test_agents_md_codes, test_doctor_unreadable_paths, test_doctor_unreadable_pin 305 passed
the head test file over main 823624d9's tools 45 passed, 4 failed: the shape-doctor.py hygiene row, both in-process rows and the mode case, as the body says

Round-1 findings

Finding Status Evidence
M1 Resolved Counts on head and merged: BENIGN 172 entries (176 sites); tags OWN 54 (55), DELIBERATE 63 (65), ASKED 25 (25), ROOT 20 (21), DEST 9 (9), PINNED 1 (1); KNOWN_OPEN 11. Each of the six moved sites was measured (below).
S1 Resolved as asked X5, X6 and X18 are killed. The parity check passes all 12 real helpers with no problem on head AND on the merged tree. 24 constants. The class S1 named still has other routes: NIT 1.
S2 Resolved X4 is killed (unlisted names the guard as inside the helper). No entry names a helper.
S3 Resolved #1110 now quotes (ctx.root / name).is_file().
N1 Resolved X3 is killed. Probe below.
N2 Resolved X15 is killed by the hygiene test and by parity.
N3 Resolved T2 and T3 are killed by the new count self-test.
N4 Resolved 03c16cb9 has tree 14e6405b and parents 9cb946be and 6da757cb, as the body says. Main has since moved to 823624d9, and that merge is clean too.
N5 Resolved The body names it as out of scope.

M1, the six moved sites. I set up a root Atlas/ with .git -> ../locked/repo/.git and origin git@github.com:acme/Orbit.git, then ran each site with the folder readable and with locked/ at 000:

Site Readable control 3.12, locked 3.14-like shim, locked
origin_org acme raises PermissionError None
repo_local_origin_name Orbit raises None
repository_names ['Atlas','Orbit'] raises ['Atlas']
read() advise not-answered not-a-repository
check_the_way_in adopt raises "no repository here to adopt", NEXT setup.sh … --project Atlas
check_the_other_way_in a row raises None

N1, alias tracking (probe file …/r2/probes/alias_probe.py):

  • Caught: import os as o, p = os.path, a chain three names deep, a reverse-order chain through global, function-local import os as, q = os.path and from os import path as, and AnnAssign.
  • Rebinding and shadowing: names are tracked across the whole file and never dropped. A parameter named path, a local o = Thing(), or a local exists gets over-flagged, which is the safe direction.
  • Still missed: see NIT 2.

New findings at b4484647

MUST

None.

SHOULD

1. The body's "What it does not catch" ends "No shipped tool has one today". That is false.

NIT

  1. The S1 class is still open by other one-line routes.

    • Probed with …/r2/probes/parity_probe.py and as mutants. These pass parity:
      • or isinstance(exc, PermissionError) (Z1);
      • a dead return False before the raise (Z2). The bare-raise check at :1072-1076 asks only that a raise exists;
      • _ABSENT_ERRNOS += (errno.EACCES,) (Z3). _constants at :970-980 reads only top-level Assign and AnnAssign;
      • exc.args[0] ==, exc.strerror ==, contextlib.suppress(PermissionError), a method or lambda the walk doesn't follow, and _is_a = _lenient.
    • Each slice's own tests kill the three I ran: Z1 by test_bump_leg_unopenable.py (25 failed), Z2 by test_validate_pins_unreadable_workflows_dir.py and others (55 failed), and Z3 by test_doctor_unreadable_paths.py (3 failed). So this is defence in depth only.
    • Fix (follow-up): a behavioural parity test. Load each of the 12 helpers, raise each absent errno and winerror (expect False), and raise EACCES, EPERM and EIO (expect the same exception back).
  2. Scanner misses not on the "not caught" list. All were measured; none is in shipped code (…/r2/probes/missforms.py):

    • o = os; o.path.isfile (Z6), from os.path import * (Z7);
    • an alias bound by tuple unpacking, walrus, a for or with target, a default parameter or a class attribute, or via importlib/sys.modules.

    A guard bound once at module level (EX = os.path.exists) is flagged at the binding, but its later calls are free. Fix: add these to the body's list.

  3. The parity rules refuse some correct helpers:

    • errno not in (:1077-1078);
    • raise exc;
    • a frozenset({...}) constant (:991-992);
    • import errno as E;
    • an except (FileNotFoundError, NotADirectoryError);
    • an unrelated except KeyError in any module function the helper calls (:1068).

    A copy of validate-pins' helper, which the failure message prescribes, passes. Note: the handler rule checks every handler in every followed function, not only those "on the path".

  4. Self-test gaps (each of these test mutants survives):

    • R2: the alias fixpoint cut to one pass;
    • R3: AnnAssign no longer followed (:701);
    • R7: _helpers without ClassDef (:1112-1120). Z10, a class _is_a with no guard, would then pass;
    • R13: _members skipping an element it can't read (:1002). Head refuses a by-name EACCES (Z15 killed); nothing pins that.

    Fix: a SPELLINGS line typed: Any = os.path, a _helpers self-test, and a parity row with from errno import EACCES.

  5. The KNOWN_OPEN owner check is a tautology. At :859, every entry uses FOLLOW_UP, so T6 (assert owner) and Z11 (FOLLOW_UP = "#1101") both survive. A typo in the constant at :123 passes. Fix, if wanted: assert re.fullmatch(r"#\d+", FOLLOW_UP) plus a note that family.py, the family root's siblings.py, shape-doctor.py and shape_advisory.py: a name in the root that is a link into a folder this user cannot search is still read as absent at twelve sites (follow-up of #430 slice Z) #1110's body mirrors KNOWN_OPEN.

  6. The HELPER comment "as slice A wrote it" (:1160) is inexact. Slice A's scripts/bump-leg.py _is_a has its set inline and no _absent; HELPER has adopt-project/family.py's shape.

  7. family.py, the family root's siblings.py, shape-doctor.py and shape_advisory.py: a name in the root that is a link into a folder this user cannot search is still read as absent at twelve sites (follow-up of #430 slice Z) #1110's title still says "at five sites"; its body says eleven. That is lane 1's to fix.

  8. tests/test_path_guard_hygiene.py and shape-doctor.py: a bare is_file/is_dir/exists guard in a shipped tool fails by file and line unless its _is_a or a reasoned allowlist covers it, and a project.yaml linked into an unsearchable folder is yaml-unreadable (#430) #1111 has no milestone. Every Path.is_file() / is_dir() / exists() guard in the shipped tools answers False to any OSError on Python 3.14 (3.12 and 3.13 raise PermissionError), so a directory this user cannot search reads as ABSENT: validate-pins.py skips its workflow @<sha> check, update-shape.py says copy-missing and update-pin-missing ("adopt the project") for files that are there, the doctor says DRIFTED (5 copy-missing), and bump-leg.py's remaining guards, and every other tool's, skip or misreport instead of refusing by name #430, scaffold-project.py: the --reuse-empty-repo help, its docs and each remedy that sends a person into that re-run say ssh, a credential helper or askpass can still make it wait, and how to keep it from waiting (#915) #1101 and templates/assembly-root/project.yaml and templates/family-root/family.yaml: the comment above visibility: says it is the visibility the run asked for, and that a repository the run reuses keeps its own, where it said "this is all three repositories' visibility" (#913) #1102 carry v1.2.0.

The I/O-error row is realistic and effective. The doctor's is_a calls path.stat(), which the row patches to raise OSError(EIO). is_a re-raises it, and only _to_load's except OSError turns it into a loaded manifest. Z14 (only EACCES loaded) is killed, and only that row fails.

No self-test only tests itself: the count, scanner and parity self-tests call the same unlisted, stale_entries, guards_in and parity_problems the real tests use.

Mutants: 19 killed of 30, run against the hygiene file on head

  • Round 1's survivors re-run: 8 of 9 killed.
    • Killed: X3, X4, X5, X6, X15, X18, T2, T3.
    • Survived: T6.
  • Mine: 11 of 21 killed.
    • Killed: R1 (no import os as), R14, R15 (no bare-raise rule), R16 (no handler rule), Z4 (a KNOWN_OPEN site fixed, entry left in, caught as stale), Z5 (a count over), Z8, Z9, Z10, Z14, Z15.
    • Survived: R2, R3, R7, R13, Z1, Z2, Z3, Z6, Z7, Z11. Z1, Z2 and Z3 are killed by their slices' tests in the full suite.
  • Every file was restored and its sha256 matched the HEAD blob (30 of 30). The scratch clone and my worktree are clean at b4484647.

Body claims

Claim Result
25 files; the glob adds none true
188 sites in 18 of 25 on 6da757cb and 16f9a0e8; none in a helper; head 187 true; 823624d9 is also 188
BENIGN 176 sites in 172 entries; tag table true
KNOWN_OPEN 11, each effect, #1110 not in v1.2.0 true; all six new effects measured; #1110 has no milestone
"What it does not catch" list items true; "No shipped tool has one today" false (SHOULD 1); incomplete (NIT 2)
19 spellings, no stat/lstat; count self-test true
parity follows _absent, _mode_of, _says_absent, refused_read true
handler, bare-raise and in <set> rules; class or async refused true as the code does them
24 constants; 12 helpers hold the set true, on head and merged
doctor before and after (3.12 and shim, byte for byte) true, re-measured on 823624d9 against merged
an I/O error is loaded and named true (Z14 killed)
11 parity self-test changes; 49 tests; 5 doctor tests true
against main's tools, 45 pass and 4 fail true on 823624d9
gate: 305; 445 passed and 3 skipped; 104 true on the merge with 823624d9
this round, 17 of 17 killed 8 re-run, all killed; Y1, Y3 and Y7 covered by my R16, R1 and Z14; the rest not re-run
merge claim (N4) true
no --help change, no pinned copy true; the files are shape-doctor.py and the test file
only Closes #430 true; closingIssuesReferences = [430]; no keyword in any branch commit message
N5 out of scope true

Probes, logs and mutants are in /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1111/r2/:

  • probes/ holds mutants.py, six.sh, members.sh, alias_probe.py, parity_probe.py and missforms.py;
  • logs/ holds gate_merged.log, mutants.log and z*_slice*.log.

Lane: openreposhape-1 (openRepoShape-1)

@sourcery-ai

sourcery-ai Bot commented Oct 21, 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 8 hours by commenting @sourcery-ai review. Upgrade to get a review now.

…opt-project.py` are `OWN`, since `_PlannedRoot`'s two ask `_CloneGit`, the run's model in memory of the clone's `.git`, and not the disk, and `_clone_ref_store`'s asks `git init`'s own throwaway (#430)

Lane 1's landing chain gated the predicted squash over main `4ed7c7bf`
and failed only the `adopt-project.py` hygiene row: #1091 added three
bare guards no entry covered. This merges main `4ed7c7bf` and gives each
an entry.

- `_PlannedRoot._no_directory` and `_PlannedRoot._into_git` ask
  `self._git().is_dir(...)`. `_git()` returns `_CloneGit`, whose
  `is_dir` looks the name up in sets of the names git makes in the
  clone's own `.git`. Measured with every stat, lstat, listing and
  `os.path` test patched to raise, in a folder with no `.git`: it
  answers True, False and None as its sets say and asks the disk
  nothing.
- `_clone_ref_store` asks `.git/reftable` of `git init`'s throwaway, a
  `mkdtemp` of this user's (0700) in the run's own work directory. A
  work directory at 0500 or 0600 stops at the `mkdtemp` first. A git
  template holding `reftable` as a link into a folder at 000 is the one
  way to a refused stat there: 3.12 raises it into `ref_store`, which
  answers git's defaults, and a 3.14-like shim reads False; both answer
  `(False, True)`, as the readable control does, on git 2.43.0.

BENIGN now holds 179 sites in 175 entries (OWN 57 (58)); the head has
190 bare guards in 18 of 25 files, main `4ed7c7bf` 191.

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

Copy link
Copy Markdown

@brettheap

brettheap commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-21T17:17:47Z) r1111c, lane 1's independent reviewer (not the writer), re-checked #1111's third round at d286ae9a: READY. No MUST and no SHOULD.

Lane 1's landing chain runs on d286ae9a next. The review follows in full.


Verdict: READY. I found no MUST or SHOULD. Head d286ae9a is what the PR reports. My worktree is still clean at d286ae9a, and I made no commit, no push and no GitHub write.

1. The delta: pass

  • git diff b4484647 d286ae9a -- tests/test_path_guard_hygiene.py is 1 file changed, 12 insertions(+): exactly three new BENIGN entries, all tagged OWN.
    • They are _PlannedRoot._into_git, _PlannedRoot._no_directory and _clone_ref_store.
    • They sit in ASCII sort order beside their neighbours.
  • git diff 4ed7c7bf d286ae9a --stat names only shape-doctor.py (21 lines) and tests/test_path_guard_hygiene.py (1325 lines).
  • The history is as reported:
    • 8bc4d1b2 has parents b4484647 and 4ed7c7bf, and its tree is fc0731ea, the predicted one.
    • d286ae9a has parent 8bc4d1b2.
    • The head's test-file blob is 070f5597, as f1111c said.

2. Each entry's reason: pass

The two self._git().is_dir(...) sites (adopt-project.py:10785 and :10817).

  • _git() at :10575 is annotated -> _CloneGit and returns _CloneGit.of(...).
  • _CloneGit.is_dir (:10300 region) is if self._key(path) in self._dirs: return True, then None if self.unread(path) else False.
    • unread is a set test plus a scan of an in-memory dict. It makes no stat, no Path call and no os call.
  • I re-ran clonegit_probe.py from my scratch with stat, lstat, scandir, listdir, the os.path tests and Path.stat/lstat/is_dir/exists all patched to raise.
    • The probe found no .git in its working folder, and disk questions asked while patched: [].
    • .git/refs gave True, .git/nothing gave False and .git/objects/ab gave None.
    • _no_directory and _into_git returned their normal messages.
    • Log: /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1111/r3/logs/clonegit_probe.log.

_clone_ref_store's (probe / CLONE_GIT_DIR / "reftable").is_dir() (:11113).

  • probe = Path(tempfile.mkdtemp(prefix="refs-probe-", dir=work_root)), followed by git init -q.
  • The only caller is _SymlinkProbe.ref_store (:11152). It maps OSError to GIT_DEFAULT_REFS = (False, True), which is the same answer as a False guard.
  • My trace on a work directory I made (refstore_trace.py):
    • The guard's path was <work>/refs-probe-8kw370zc/.git/reftable.
    • Its parent's parent is the work directory, and the probe folder is mode 0o700, owned by this user.
    • The .git inside it is a real directory owned by this user.
    • The answer was (False, True) and nothing was left behind.
  • With the work directory at 0500 and at 0600, _clone_ref_store raised PermissionError at the mkdtemp, so the guard was never reached. ref_store() gave (False, True) both times.
  • So the entry's reason is true.

The OWN tags fit the TAGS text at tests/test_path_guard_hygiene.py:127, "a temporary or work directory it made", for _clone_ref_store. For the two _CloneGit sites the fit is loose: see NIT 1.

3. The run and the bite proof: pass

  • On d286ae9a, in the probe environment (stub gh, bogus token, empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null, GIT_SSH_COMMAND=false, EMAIL, env -u LANES_LANE, one basetemp), tests/test_path_guard_hygiene.py gave 49 passed in 9.27s.
  • Bite proof, in a git archive copy of the head, removing each new entry on its own:
    • B1 (_no_directory): 1 failed, 48 passed; the failure was the adopt-project.py row, naming adopt-project.py:10785.
    • B2 (_into_git): the same, naming adopt-project.py:10817.
    • B3 (_clone_ref_store): the same, naming adopt-project.py:11113.
    • After each run I restored the file. Its sha256 was 7fbefb44…1ca86 every time, equal to the original and to the worktree's file.
  • Also run over main 4ed7c7bf's tools with the head's test file: 45 passed, 4 failed. The 4 are the shape-doctor.py row, two in-process rows and the mode case, as the body says. The adopt-project.py row passes there.
  • f1111c's gate logs read as reported: 305, 445 passed with 3 skipped, and 104. I did not re-run them. The tested blob equals the head's blob, and the merge tree matches the one the gate predicted.

4. The PR body: pass

  • Head and state: the PR is on d286ae9a, open and mergeable, with milestone v1.2.0. That resolves r2's NIT 8, the missing milestone.
  • Counts: I re-measured them with the head's own test module (logs/counts_head.log, logs/commit_counts.log).
Count Body Measured
25 files scanned, the glob adds none yes 25, none added
Guards on main 4ed7c7bf 191 191
Guards on head 190 190
Guards on 16f9a0e8, 6da757cb, 823624d9 188 188
BENIGN 175 entries, 179 sites 175 entries, 179 sites
KNOWN_OPEN 11 11
OWN 57 (58) 57 (58)
DELIBERATE 63 (65) 63 (65)
ASKED 25 (25) 25 (25)
ROOT 20 (21) 20 (21)
DEST 9 (9) 9 (9)
PINNED 1 (1) 1 (1)
  • Nothing is unlisted or stale, no guard is in a helper, 12 helpers pass parity, and there are 24 absent-set constants.
  • git diff 823624d9 4ed7c7bf adds exactly the three guards named, so 188 + 3 = 191.
  • Closing keywords:
    • Closes #430 (line 161) is the only closing keyword.
    • The one other match, "a merge conflict resolved" on line 97, has no issue reference after it.
    • closingIssuesReferences is [430] alone.
    • No branch commit message from 4ed7c7bf..d286ae9a carries a closing keyword.
  • Round 2's SHOULD 1: the "No shipped tool has one today" sentence is fixed. The body now names the three Windows-only isjunction lookups. It also adds r2's NIT 2 miss forms (tuple unpacking, walrus, from os.path import *, and the others).
  • Hygiene: no operator email address, no literal escapes, and the only non-ASCII character is the robot emoji in the required attribution line.

Findings

MUST: none.
SHOULD: none.

NIT 1: tests/test_path_guard_hygiene.py:186 and :190. OWN is the nearest tag for an in-memory model, but its TAGS text names disk things. The reasons do say "not the disk", and no tag fits better. A follow-up could add an in-memory tag, or widen OWN. It needs no change for this PR.

NIT 2: carried over from round 2 and not new. The open NITs are the FOLLOW_UP tautology (:859), the "as slice A wrote it" comment (:1160), #1110's "five sites" title, and the _os_path_names docstring. The body already says the docstring changes with the follow-up.

Everything is in /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1111/r3/: probes/ holds bite.py, refstore_trace.py and the copied f1111c probes, and logs/ holds pr-body.md and the run logs.

Lane: openreposhape-1 (openRepoShape-1)

@brettheap
brettheap merged commit 038dafb into main Oct 21, 2026
5 checks passed
@brettheap
brettheap deleted the fix/430-path-guard-hygiene branch October 21, 2026 18:42
brettheap pushed a commit that referenced this pull request Oct 21, 2026
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 21, 2026
…w `os.path.lexists(mount / adopt.GITMODULES)` in `unregistered_gitlinks` is on `BENIGN` as asked after git listed the leg's index, so the path-guard test passes with #1111 merged, and the docstring paragraph on a recursive clone is wrapped to 79 (#1093)

Review round 2 (RV1109: SHOULD-2 and NIT-6), on top of the merge of
origin/main at 038dafb (#1111).

- SHOULD-2: with #1111 merged, `test_no_bare_guard_outside_a_stat_helper_or_the_allowlist[shape-doctor.py]`
  failed on the bare guard at `unregistered_gitlinks`. The entry sits in
  `BENIGN` with the table's alphabetical order, tagged ASKED: the guard is
  asked only after `.gitmodules` is among the paths git listed in the leg's
  index (`leg_tracked_files`), by `lstat`, in a mount `mount_read` read.
- NIT-6: the `unregistered_gitlinks` docstring paragraph that began at 88
  characters is rewrapped to 75. With docstrings stripped, `ast` shows
  shape-doctor.py identical to the merge.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 21, 2026
….fsmonitor=false`, and the rows r1119b named pass on main: the clone-env row appends to the environment's count, and the ref-probe row fails only the probe's `git init`

r1119b's second review of #1119, after the merge of main 038dafb (#1091, #1111):

- MUST-1: `test_the_clone_takes_a_parent_gits_settings_and_no_other_repository`
  built its `GIT_CONFIG_COUNT` from `{}`, which
  `test_no_test_sets_a_git_config_count_of_its_own` refuses. It now appends
  to `os.environ`, and asserts the last entry kept and no `attr.tree`.
- MUST-2: main's `test_check_says_where_git_would_not_make_the_ref_probe`
  set `GIT_DEFAULT_HASH=bogus`, which now stops the split's own clone
  first (`adopt-git-unasked`, exit 1). A stub `git` first on PATH fails
  only `git init` of a `refs-probe-*` directory, with git's words; skipped
  on Windows.
- SHOULD-A: `_throwaway_made` returns `-c core.fsmonitor=false` beside
  `core.hooksPath`, as the rehearsal and `_stored_as_written` give theirs.
  A counting fsmonitor program ran 8 times in `_resolved`'s two
  repositories in one `check`; now 0, in the global config and in
  `GIT_CONFIG_COUNT`. New rows: that one and its control.
- NIT-A: a row with `--work-dir` inside the source, whose config rewrites
  the url (kills R8).
- NIT-B: a row with that rewrite handed down by a parent git in
  `GIT_CONFIG_PARAMETERS`, which `_answers` leaves out (kills A18).
- NIT-C: `_split_clone_asked`'s docstring says a shallow source makes the
  clone run the `uploadpack.packObjectsHook` of protected config, as
  `execute`'s clones do.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 21, 2026
brettheap pushed a commit that referenced this pull request Oct 22, 2026
…its own, so #1111's hygiene test passes, path-twice says "back first from" the file it puts back, and two rows pin the value's write-back and path-twice's recorded put-back (#1077)

Fix round 1 on #1124, from RV1124's review at 672845c.

S1. `_listed_as_git_reads` opened with a bare `os.path.lexists(root /
GITMODULES)`, which #1111's `test_path_guard_hygiene.py` fails by file
and line. Its one caller, `_check_gitmodules_values`, asks it only where
the work tree's file is gone, and `_gitmodules_put_back`, which the
function calls next, asks the same `lexists` (it is on the test's
allowlist), so the guard is dropped.

N3. Two behaviours had no row; mutants A and B survived all 439 rows of
the PR's nine files. A row for each, from RV1124's probes P10 and P7:
- the value's write-back reads the file git reads, not HEAD's (A);
- path-twice records its put-back, so `gitmodules-path-unnamed` after it
  prints none and a plain commit keeps neither `second` nor
  `members/Old` in HEAD (B), where the work tree's file is deleted or
  its deletion is staged.

N4. Path-twice said "back from HEAD's, as the index holds none first":
"first" sat on the wrong phrase. It says "back first from {whose}".

N5. `_gitlink_out_first`'s docstring said its text ends "then commit
.gitmodules"; every branch stages now, and it says which words each
branch ends with.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 23, 2026
…git dies on read the `.gitmodules` git reads, the index's where the work tree's is gone, and every section's lines end `git add -- .gitmodules`, so a plain `git commit` after them takes the section out of HEAD (#1077) (#1124)

## What this does

RV1069's review of #1069 (the PR for #1036) left four follow-ups, recorded in #1077's body (NIT 1, NIT 2) and in issuecomment-6303768948 (NIT 3, NIT 4). Each was measured on main before this change. The only script changed is `templates/family-root/scripts/validate-family.py`, which has no pinned copy.

- **NIT 1, `gitmodules-path-twice` read the work tree's `.gitmodules` alone.** A second section for `members/IRRS`, staged in the index's file with the work tree's deleted, was `family ok` in every mode, and a clean clone of the commit after it failed its update and its gate.
  - It reads the sections as git reads them now (`_sections_as_git_reads`), as `gitmodules-path-unnamed` does since #1036.
  - Where the work tree's file is gone, it names the line that puts back the file git reads first. That put-back is recorded, so no later finding puts the file back over the hand edit.
  - It ends "then stage and commit .gitmodules".
- **NIT 2, the value check read the work tree's file and HEAD's, never the index's.** An `update = !true` staged in the index's file alone, in a named member's section, was `family ok` while git's own `submodule status` exited 128.
  - It reads the file git reads (`_listed_as_git_reads`), and says "the index's file, which git reads here" where that is the one read.
  - Its lines put that file back, not HEAD's. They end "then stage", or "then stage and commit" where HEAD's gives the value too.
  - Where HEAD's file alone gives the value and the work tree's is gone, it says "Commit the index's .gitmodules, which git reads here and does not give it", with no line, so a fix staged there is kept.
  - `member-gitmodules-absent` then puts the file back itself: `_check_gitlinks_registered` asks `Report.put_back` alone.
  - Git is asked in the order it was before (the work tree's file, then HEAD's, then the one read in its place), so with no git to run, `git-not-runnable` names what it named.
- **NIT 3, a plain `git commit` after a section's lines.** Where HEAD's file held the section, `gitmodules-path-unnamed`'s lines ended "then commit .gitmodules" and staged nothing. A plain `git commit` after them left the section in HEAD, and a clone's gate failed. Every branch (in `members/`, outside it, and in another case) now prints `git add -- .gitmodules` after the keys, and says "then stage and commit" where HEAD holds the section.
- **NIT 4, a test gap.** No row covered the case-variant branch's stage line where HEAD holds no section of the name. RV1069's probe `test_r2_stray_not_in_head` is that row now.

## New rows: `tests/test_validate_family_index_gitmodules.py`

Every row types only the printed lines and does by hand only what the text says in words, then makes a plain `git commit`. The gate and a plain clone then pass.

- N1: a second section in the index's file alone.
- N2: a value in a named member's section, in the index's file alone.
- N2b: HEAD's file gives the value, and the index's is mended.
- N2c: as N2b, with OTHER's section out of both files, so `member-gitmodules-absent` puts the index's file back first.
- N3: a section HEAD holds (a stale one, or `tools/x` with its gitlink), with the file on disk, deleted, or its deletion staged.
- N4: the stray `ghost` (RV1069's probe), on disk and deleted.
- Fix round 1: path-twice's put-back is recorded, so `gitmodules-path-unnamed` after it prints none (RV1124's P7, on `deleted` and `deletion-staged`); and the value's url is written back from the file git reads, not HEAD's (RV1124's P10). See the fix-round section below.

**Before and after.** On main's script the file gives 10 failed and 2 passed. The 2 are N4's rows, which pass there by design (NIT 4 is a test gap) and kill the case-variant mutant M8 below. On this head it gives 12 passed. After fix round 1 the file has 15 rows: on main's script it gives 13 failed and 2 passed (the 2 are N4's), and on this head 15 passed.

## Older rows changed, and why

Each one pinned the behaviour #1077 names as the defect.

- **"then commit" with no stage line (NIT 3).**
  - `SECTION_REMEDY` and a new `STAGE` constant (`test_validate_family_unnamed_mounts.py`), and its rows (b) and (c), plus the two-sections row.
  - `THEN_COMMIT` and `section_first`, which always ends with the stage line now (`test_family_unnamed_gitlink.py`), and its two-sections and control rows.
  - Two `removal(...)` assertions in `test_validate_family_remedies_typed.py`.
  - `ghost_out`, R1's replace anchor and R6 in `test_family_hand_mount_git_rm_lines.py`.
  - Each section in these rows is committed, so HEAD holds it: a plain commit after the old lines left it there.
- **Path-twice's ending (NIT 1 and NIT 3).** `test_family_add_refusals.py`'s path-twice row ends "then stage and commit": the commit records the index's file, not the work tree's hand edit.
- **HEAD's file put back where git reads the index's (NIT 2).**
  - `taken_out(put_back=...)` and the two gone-file value rows in `test_family_gitmodules_values.py`. These now read `HERE_AND_HEAD`; one is renamed `..._put_it_back_once`, since its old name said HEAD's file is put back.
  - The first line of `test_round1_nit_6` in `test_family_holder_remedies_compose.py`.
  - The value row in `test_family_holder_bootstrap_texts.py`, renamed `test_a_value_finding_that_put_the_file_back_is_not_put_back_over`.
  - These are now `git checkout -- .gitmodules`, the index's file.
- **#1036's R5 control (`test_1036_control_a_value_git_dies_on_keeps_what_it_read`).**
  - Round 1 put back HEAD's file, which dropped IRQS's section that only the index's file gave.
  - The value's lines put back the index's file now, so the commit after round 1 records that section, as it does with the file on disk. The next run names it, in every mode.
  - The row now types round 2 as well, then a plain `git commit`; nothing is left, and the gate and a plain clone pass.
  - What the control pins is unchanged: where git reads a file that gives a value git dies on, `gitmodules-path-unnamed` reads nothing from it.

## Gates

Every run used `/usr/bin/python3 -m pytest -p no:cacheprovider` with:
- `EMAIL` set;
- `GIT_ALLOW_PROTOCOL`, `GIT_SSH_COMMAND` and the `LANES_*` variables unset;
- an empty `GIT_CONFIG_GLOBAL` and `GIT_CONFIG_NOSYSTEM=1`;
- test author and committer identities;
- a stub `gh` first on PATH, `GH_TOKEN=bogus` and an empty `GH_CONFIG_DIR`.

- **Targeted set: 78 files.**
  - The files that name `gitmodules-path-twice`, `gitmodules-value-refused`, `gitmodules-path-unnamed` or `validate-family` in the head's tree, plus every test file that imports from a test module this changes.
  - This includes `test_pinned_copy_text`, `test_repo_hygiene`, `test_cli_reference` and `test_agents_md_codes`.
  - Result: 0 failed in all 78 files, 4390 passed and 34 skipped.
  - Shards 0 to 4 and 6 to 9 (70 files) were run at `0cd1c3f5`, the three WIP commits this head squashes (the same tree, and the same sha256 of the script): 3379 passed, 25 skipped.
  - Shard 5 (8 files) was run last, at this head (`672845cc`): 1011 passed, 9 skipped, 0 failed. Two earlier runs of it were cut by checkpoints, at 49% and at 28%, with 0 failures both times. Its files are `test_doctor_legs_members_next.py`, `test_family_add_spelling_leftovers.py`, `test_family_docs_one_spelling.py`, `test_family_makefile_siblings.py`, `test_family_remove_gitmodules.py`, `test_shape_doctor.py`, `test_update_shape_kept_helper.py` and `test_validate_family_unreadable.py`.
  - The PR's CI `tests` job runs the full suite.
- **Older rows.** The 33 rows the change moved, which failed on the new script before their update, now pass with the new rows file (45 passed).

## Mutants

Each mutant was run against the new rows file and the older rows that pin the changed paths. The file was restored after each one, and its sha256 checked against the head's.

| Mutant | Rows that failed |
|---|---|
| M1: the value check reads the work tree's file alone | 8 (N2, N2b, N2c, two gitmodules_values rows, round1 NIT 6, the bootstrap_texts row, R5) |
| M2: path-twice reads the work tree's file alone | 1 (N1) |
| M3: no "commit the index's" branch where HEAD's alone gives the value | 2 (N2b, N2c) |
| M4: the value's put-back is HEAD's, as before | 6 |
| M5: the value's text does not say to stage the file | 1 (N2) |
| M6: no stage line in `members/` | 7 |
| M7: no stage line outside `members/` | 3 |
| M8: no stage line in the case-variant branch (NIT 4) | 2 (N4 on disk, deleted) |
| M9: "then commit" where HEAD holds the section | 10 |
| M10: path-twice names no put-back | 1 (N1) |
| M11: path-twice ends "then commit" | 3 |
| M12: `member-gitmodules-absent` puts nothing back after any value finding | 1 (N2c) |
| M13: the file git reads is asked before HEAD's | 3 (the without_git rows) |

## Merge-trees

`git merge-tree --write-tree` against `origin/main` 823624d is clean, and was clean against 6da757c, the base the gates ran on. Main has since taken #1101 and #1102; neither touches a file of this PR. No open PR touches these files. #1109, the closest in subject, merges clean too.

## Follow-up, not in this PR

RV1124 measured the suspected counterpart of NIT 3 (its N1) and two more older defects (N2 and N6). All three are older than this PR and self-correcting or known limits; the lane files them as the follow-up #1150, and none is fixed here.

- N1: the work tree's `.gitmodules` is on disk, and both it and the index's give a value HEAD's does not. The value's lines say nothing of staging, so a plain `git commit` after them records the index's value; the next run names it in HEAD. Main prints the same text and gives the same result.
- N2: an index blob with a line git cannot parse after a value is `family ok` (`_blob_listed` returns nothing on git's exit 128).
- N6: an unmerged `.gitmodules` (stages 1 to 3), which v1.1.1's release notes list as a known limit.

## Fix round 1: RV1124's review of `672845cc` (S1, N3, N4, N5)

RV1124's review of this PR at `672845cc` found one SHOULD and six NITs. This round fixes S1, N3, N4 and N5, in one commit on top of it (`8dfa32bd`), with no amend and no force. N1, N2 and N6 are left to the follow-up #1150.

- **S1 (SHOULD): #1111's `test_path_guard_hygiene.py` failed the new guard.** `_listed_as_git_reads` opened with a bare `os.path.lexists(root / GITMODULES)`. Its one caller, `_check_gitmodules_values`, asks it only where the work tree's file is gone (`if present else _listed_as_git_reads(root)`), and `_gitmodules_put_back`, which the function calls next, asks the same `lexists` and is on the test's allowlist. The guard is dropped; the function is `spec = _gitmodules_put_back(root)[0]` and the blob read. On the scratch merge of main `038dafba9` and this head, `test_path_guard_hygiene.py` gives 49 passed, with no failure for this file and no stale `BENIGN` entry.
- **N3: two behaviours had no row.** Mutants A and B survived all 439 rows of the PR's nine files. Two rows, written from RV1124's probes P10 and P7:
  - `test_1077_a_url_git_dies_on_is_written_back_as_the_file_git_reads_gave_it` (P10): the index's file gives a url that is not HEAD's, then a `url` with no value; the line that writes the url back takes it from the index's file.
  - `test_1077_path_twice_records_its_put_back_for_the_next_finding` (P7, on `deleted` and `deletion-staged`): HEAD's file holds `second` and a stale `members/Old`; path-twice's put-back is recorded, so `gitmodules-path-unnamed` after it prints none, and typed in order then a plain `git commit`, HEAD loses both.
- **N4: path-twice's wording.** "back from HEAD's, as the index holds none first" put "first" on the wrong phrase. It says "back first from {whose}, `{put_back}`" now. Only `TWICE_PUT_BACK` in the new rows file pinned the old phrase.
- **N5: a stale docstring.** `_gitlink_out_first` said its text ends "then commit .gitmodules"; it now says it ends as `_section_then` does, "then stage and commit" or "then stage".

**Rows.** The new rows file has 15 rows. On main's script (`6da757cb`) it gives 13 failed and 2 passed (the 2 are N4's, by design). On `672845cc` it gives 14 passed and 1 failed: the first row (`test_1077_a_second_section_the_index_alone_gives_is_path_twice`) fails on N4's old wording. The P7 and P10 rows pass on `672845cc`, whose code was right; they kill the mutants below. On this head, 15 passed.

**Mutants.** Each ran in its own worktree over the new rows file, and the script was restored after each and its sha256 checked.

| Mutant | Rows that failed |
|---|---|
| A: the value's write-back from HEAD's where the work tree's file is gone | 1 (the P10 row: the last line wrote the HEAD url, not the staged one) |
| B: path-twice names its put-back without recording it (`_file_put_back(root, None)`) | 2 (the P7 rows: `gitmodules-path-unnamed` printed the put-back again) |

**Gate.** On a scratch merge of main `038dafba9` and `2d4cb64b`, whose tree is this head's: the usual environment (`EMAIL`, an empty `GIT_CONFIG_GLOBAL`, `GIT_CONFIG_NOSYSTEM=1`, test identities, a stub `gh` first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`). 0 failed, 3458 passed and 27 skipped in all.
- `test_path_guard_hygiene.py`: 49 passed.
- The 64 files of every `tests/test_family*.py` and `tests/test_validate_family*.py` (the PR's nine among them), plus `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`, `test_pinned_copy_text.py` and `test_doctor_validator_rows.py`: 2964 passed and 24 skipped, in four shards (685; 997 and 20 skipped; 557 and 4 skipped; 725). Those four shards were cut by a checkpoint and re-run on the same merge with the same lists.
- `test_shape_doctor.py`, which runs `validate-family.py`, in 8 shards by test id, each list non-empty: 445 passed and 3 skipped.

**Merge-trees.** `git merge-tree --write-tree origin/main HEAD` at `8dfa32bd` against main `038dafba9` is clean (tree `23c7194f0b6a08f56b93bdc80a05924e90e703a4`).

Closes #1077

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 23, 2026
…eps at a pin's file reached through a kept link, and holds that `check` renames the path the walk reaches, not the file kept in it, so rv1091's R6 no longer survives (#1116) (#1140)

One row that holds the remedy `check` gives where a kept link leads a pin's walk to a directory, so that rv1091's mutant R6 (NIT 4 of its review of #1091, issuecomment-6316443939) is killed. This is a test-only change: `tests/test_adopt_root_symlinks.py` gains one row, and no production file, refusal code or sentence changes. It is a follow-up to #1091 (#904), so Brett Heap's ruling of 2026-10-21T10:32:04Z left it out of v1.2.0, and it is on the v1.2.1 milestone.

- #1091 landed as `4ed7c7bf` and what it ships is right; only this row was missing.
- `_kept_pin_finding` words a kept pin's finding through a kept link with `_through_the_kept_link(link, reached, entry)`. Its remedy says to send the link to a leg or `drop` it, "or rename `<reached>` in the source and re-run `plan`", where `reached` is the file the root walk reaches.
- R6 hands that "rename" the kept row's own path (`path`) in place of `reached` (line 778: `link, reached, entry` becomes `link, path, entry`).
- No row put a directory at a pin's reached file (rv1091's NIT 4), so in each of them `path` and `reached` are the same string and R6 passed all of them. With R6 applied alone, the two adopt-root files give `1 failed, 326 passed, 3 skipped`, and the one failure is the new row, so main's 326 other rows do not notice it.

`test_a_directory_at_a_pins_file_through_a_link_names_what_it_reaches`, at `tests/test_adopt_root_symlinks.py:1173`, 30 added lines, in the file's style and on its helpers (`planned`, `declare`, `check`, `held_pins`, `symlink_findings`, `refused_by`, `execute_args`, `no_leg_was_created`, `ticked`):

- the plan keeps `contracts -> governance`, keeps `governance/`, and puts a directory at `governance/openglass-pin.yaml` holding `notes.md`; a pin is declared on `openGlass`;
- `check` exits 1, with no symlink finding;
- its one `plan-root-holds-pin` line names the file `governance/openglass-pin.yaml/notes.md` kept under `governance/`, inside `contracts/openglass-pin.yaml` through the symlink `contracts` to `governance`, and its remedy renames `governance/openglass-pin.yaml`, the path the walk reaches, not the file kept in it;
- `execute` refuses with `plan-root-holds-pin` and the same line before any leg.

No assert joins conditions with `and` or `or`, the added lines are at most 76 wide (the file's widest is 80) and ASCII. The docstring says that before #1091 `check` passed this plan, measured on main `823624d9`.

R6 alone, in a scratch tree at this head: `adopt-project.py` sha256 `0c1c1f3d7abf41136a61d7d4651f13fef5d3642a3162c024e921429b88bd7c1f` before, `9a921c6a9261234a68ef930e478421dca2d7135ad8674b357f2f55f46f773068` with R6, and restored to the first (checked), tree clean.

| Where | Result |
|---|---|
| the row, with R6 | `1 failed, 208 deselected`: `assert held_pins(checked) == [line]` at `:1196`, the finding reading "rename `governance/openglass-pin.yaml/notes.md`" |
| both adopt-root files, with R6, git 2.43 | `1 failed, 326 passed, 3 skipped`; the failed id is the new row alone |
| the row, on `823624d9`'s `adopt-project.py` (before #1091) | `1 failed, 208 deselected`: `assert 0 == 1`, `check` said `plan ok` |

All through `run9.sh` (stub `gh`, bogus token, `GIT_SSH_COMMAND=false`, no global git config), at head `2b6db57a3`:

| Git | Files | Result | END |
|---|---|---|---|
| 2.43.0 | `tests/test_adopt_root_symlinks.py` and `tests/test_adopt_root_shape_paths.py` | 327 passed, 3 skipped (the reftable rows at 1956, 2011 and 2562) | 2026-10-21T16:21:00Z rc=0 |
| 2.43.0 | `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py` and `tests/test_cli_reference.py` | 170 passed | 2026-10-21T16:23:02Z rc=0 |
| 2.47.1 (`git version 2.47.1`) | `tests/test_adopt_root_symlinks.py` | 209 passed, no skips | 2026-10-21T16:31:33Z rc=0 |

The 2.43 count is 327 with the row, which is the 326 other rows plus it. `tests/test_agents_md_codes.py` passes, as no refusal code changed.

- `origin/main` is `4ed7c7bf`, unmoved when fetched at the start, mid-way and before the push. `git merge-tree --write-tree origin/main HEAD` is clean, tree `d675b07f29edb9a061c3bc542dc2a76334d1e1a0`, the head's own tree: the branch differs from main by this one file, 30 insertions.
- None of the 22 open PRs touched `tests/test_adopt_root_symlinks.py` when this was written.
- **NIT round 1, 2026-10-22:**
  - origin/main `038dafba9dbedd9405b316cc749a04dffe02bb36` (#1111) is merged in as `e7623ed98`, cleanly, with tree `8a5cf7b1`, as rv1140 predicted.
  - The head `94712c3bc076142c16e48af70c218e6e958bd005` merges with main at its own tree, `f8faecc8cbcefbb87453f28647baf7a636ee1825`.
  - Of the 28 open PRs, #1119 (head `bba168bd`) now edits `tests/test_adopt_root_symlinks.py`. Its merge-tree with this head is clean, tree `81a0397437484f52b52789a86a2b5da43f712dae`.
- As a check on the nearest neighbour, #1118 (`c60066cbe`, which edits `tests/test_adopt_root_shape_paths.py` and was cut at `6da757cb`) conflicts in that file with `origin/main` alone, and with this branch the same: that is #1118's, not this row's.

This round is one merge of main and one new commit, after rv1140's review: READY at `2b6db57a`, with NITs (#1140 issuecomment-6341111415).

- **The row now keeps two files** under `governance/openglass-pin.yaml/`: `README.md` and `notes.md`.
  - Git's order puts `README.md` first, so the one `plan-root-holds-pin` line names `governance/openglass-pin.yaml/README.md`. The sections above describe the first commit's one-file row.
  - This kills rv1140's mutant M3, which changes `rows[0]` to `rows[-1]` at `adopt-project.py:770`. M3 survived the one-file row.
  - The docstring names the sort order.
- **Mutants, the row alone,** each applied alone and restored with its sha256 checked:
  - M3: killed (it survived before);
  - R6: still killed;
  - rv1140's M1, M2 and M4 to M7: all killed.
- **Runs at `94712c3bc`:**
  - git 2.43.0, `tests/test_adopt_root_symlinks.py` and `tests/test_path_guard_hygiene.py`: **255 passed, 3 skipped**. The skips are the reftable rows.
  - git 2.47.1, the same files: **258 passed**.

Review: lane 5's independent re-check, by a reviewer that wrote none of #1140: READY at `94712c3b`, the head that lands (issuecomment-6344746584), with no MUST, SHOULD or NIT. It checked the delta from `2b6db57a`, which rv1140 passed (issuecomment-6341111415). CI (Brett Heap's ruling 27): H0 is the head 94712c3 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 038dafb; and the local gate passed on its predicted squash tree 19f483e over main dea964a: the adopt_root_symlinks, repo_hygiene, agents_md_codes, cli_reference sets, 377 passed, 3 skipped. Landed on Brett Heap's word of 2026-10-23T13:10:26Z, relayed to every lane by lane 3 (#473 issuecomment-6365697123): "merge v1.2.1 PRs once reviewed and green".

Closes #1116

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 23, 2026
…ST and asked with its `stat` refused, the scan follows every binding #1111 left out, and parity takes the correct spellings it refused (#1134) (#1149)

`tests/test_path_guard_hygiene.py` takes the follow-ups #1134 collected from the reviews of #1111 (#430's slice Z). It is test-only: no shipped tool and no `--help` text changes.

- A behaviour test asks each stat helper `_helpers` finds (the twelve, and any added later). Each is loaded by its AST, with the top-level statements it needs, in a namespace of its own, because none of the twelve modules imports without side effects.
- With `os.stat` and `os.lstat` refused for one name, each helper answers an absent path False and raises on any other refusal.
- The scan follows every binding #1111 left out, and the parity test takes the correct spellings it refused.
- The test's own messages are spelled with `ast.unparse`, which is the same on every Python. `ast.dump`'s text changed in Python 3.13, which failed CI's 3.14 run at `cd935163`.

Review: lane 1's independent reviewer found `ccbd0eb0` READY on condition, with two SHOULDs and five NITs (#1149 issuecomment-6336443343), then found the fix round's `cd935163` READY (issuecomment-6337584692). Lane 1 reviewed the one later commit, `fa67fac7`, which changes only how the test spells its messages and the assertions on them (issuecomment-6339538077).

Tests, under a stub `gh` with a bogus token: at `fa67fac7` the hygiene file gives 121 passed and the doc trio 170 passed. At `cd935163` the doctor's 4 shards gave 445 passed and 3 skipped, and the leg path collisions file 104 passed. Mutants: 143 of 143 killed.

It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its reviewed head `fa67fac7` is green on Linux and SonarCloud. The branch merged main three times and was never rebased; the last merge is update-branch's, clean, giving 1c893a0, which is green on SonarCloud and Git Bash. Lane 1's gate passed on its tree: 2 pytest calls over `tests/test_path_guard_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py`, `tests/test_repo_hygiene.py`: 292 passed.

Closes #1134

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 23, 2026
…40` end in `\Z`, not `$`, so a pin's `commit:` or `tree_sha256:` that ends in a line break is refused as not 40 or 64 hex by every validator and `adopt-project.py` (#1017) (#1126)

`scripts/repo_shape.py` ends `COMMIT_RE` and `SHA256_RE` in `$`, and every caller reads them with `.match`. In Python `$` also matches just before a trailing line feed, so a pin's `commit:` or `tree_sha256:` that ends in a line break passed as well formed. This makes both patterns refuse it, wherever a value is decided to be valid, and changes nothing else about them.

Measured on main (`2a119f28`, then re-checked on `6da757cbe`; the use sites of both patterns are the same on `823624d93`, and no new copy of either pattern was added). Python 3.12.3:

| pattern | value | `.match` on main |
|---|---|---|
| `COMMIT_RE` / `SHA256_RE` | 40 / 64 hex | a match |
| same | hex and a line feed | a match (the defect) |
| same | hex and a carriage return and line feed | None |
| same | hex and a space | None |

A line feed reaches the patterns through YAML: the double-quoted escape `"<hex>\n"` and a block scalar both read as the hex and a line feed. On main:

- `validate-manifest.py`: `shape.commit` and `shape.digests.tree_sha256` ending in a line feed -> `manifest ok`, exit 0.
- `validate-family.py`: `shape.digests.tree_sha256` in `family.yaml` ending in a line feed passed (`family ok`, exit 0). `shape.commit` did not pass: it exited 1 with `shape-pin-manifest-disagree`, because `contracts/shape-pin.yaml` and `family.yaml`'s `shape.commit` differ only by the line feed, so the same 40 hex is printed on both sides; the finding that names the commit as not 40 hex, `family-shape-commit`, was not raised. A member pin's `tree_sha256` ended in a line feed gave `member-digest-mismatch`, not the malformed finding, and a member pin's `commit` gave `member-commit-unresolvable`, not `member-pin-commit`.
- `validate-pins.py`: a leg pin's commit gave exit 2 `pin-commit-unresolvable`, not `pin-tag-only`; a leg digest gave `pin-digest-mismatch`, not `pin-digest-malformed`; a shape pin's commit gave only `shape-pin-manifest-disagree`, not `shape-pin-commit`.
- `adopt-project.py check` (`_pin_row_problem`): the row read as well formed, with no `plan-pin-malformed`.
- `templates/assembly-root/scripts/bootstrap.py` holds a copy of the commit pattern, `COMMIT_40`, with the same defect: `shape_upstream_notice` printed its notice for a pin ending in a line feed.
- `shape-doctor.py` `pin_commit` returned `"<hex>\n"` for such a leg pin.

Checked and not affected: `bump-leg.py --to` and `family.py --at/--to` go through `checked_value` (a `fullmatch` that already refuses a line feed); `bump-leg.py` line 2212 strips first; `update-shape.py` line 558 reads `git_out`, which is stripped; `shape-doctor`'s `recorded_gitlink` splits fields; `src/openreposhape/standard.py` uses `fullmatch`. The `@(?P<sha>[0-9a-fA-F]{40})` patterns in `bump-leg.py` and `validate-pins.py` are unanchored workflow-ref patterns.

- `scripts/repo_shape.py`: `COMMIT_RE` and `SHA256_RE` end in `\Z`, not `$` (comment added). `\Z` matches only at the very end of the value, so each caller's `.match` refuses a line feed as it already refused a carriage return and line feed, and a space. 40 and 64 digits, either case, are unchanged.
- `templates/assembly-root/scripts/bootstrap.py`: `COMMIT_40` likewise (comment added).
- Nothing else in production changes. No README, doc, handbook or `AGENTS.md` text pins the old pattern bytes (grepped), and `tests/test_pinned_copy_text.py` and every `tests/test_bootstrap*.py` pass unchanged (shard tB and shard tA).
- Rows:
  - new `tests/test_pin_pattern_line_break.py` (46 rows: 42 at first, and 4 added in fix round 1, below): the three patterns x case, length, after-suffix (line feed, carriage return and line feed, space, two line feeds, carriage return, tab) and before-prefix; the holder's gate (four rows and a baseline); the bootstrap upstream notice (two rows); the doctor's `pin_commit` (two rows);
  - `tests/test_pin_validator.py`: 3 rows (leg commit, leg digest, shape-pin commit);
  - `tests/test_manifest_validator.py`: 2 rows (`shape.commit`, shape digest) and a helper `shape_of`;
  - `tests/test_adopt_pins.py`: 2 `malformed()` cases (`commit-line-break`, `digest-line-break`);
  - `tests/test_shape_doctor.py`: 1 `ROOT_PIN_FAULTS` param (`a-commit-ending-in-a-line-break`).

Run with the two production files of main checked out, on a head with the same tree as this one (git 2.43.0, `EMAIL=service@opensoft.dev`):

- the new file, `test_pin_validator.py`, `test_manifest_validator.py`, `test_adopt_pins.py`: **16 failed, 307 passed** in 294.83s. The 16:
  - new file (9): `line-feed-COMMIT_RE`, `line-feed-SHA256_RE`, `line-feed-bootstrap-COMMIT_40`, `test_a_member_pin_commit_that_ends_in_a_line_break_is_refused`, `test_a_member_pin_digest_that_ends_in_a_line_break_is_malformed`, `test_a_family_shape_commit_that_ends_in_a_line_break_is_a_finding`, `test_a_family_shape_digest_that_ends_in_a_line_break_is_a_finding`, `test_the_upstream_notice_is_silent_over_a_pin_that_ends_in_a_line_break`, `test_the_doctor_reads_a_pins_commit_as_the_validators_do[40-hex-and-a-line-feed]`;
  - `test_pin_validator.py` (3): a commit, a digest and a shape-pin commit that end in a line break;
  - `test_manifest_validator.py` (2): a shape commit and a shape digest;
  - `test_adopt_pins.py` (2): `plan-pin-malformed-commit-line-break`, `plan-pin-malformed-digest-line-break`;
- `test_shape_doctor.py -k a_root_pin_that_does_not_read`: **1 failed, 12 passed, 436 deselected** in 57.84s (`a-commit-ending-in-a-line-break`).

With the change the same files pass (see the runs).

| mutant | failed | passed |
|---|---|---|
| M1: both `repo_shape` patterns back to `$` | 11 | 35 |
| M2: only `COMMIT_RE` back to `$` | 7 | 39 |
| M3: only `SHA256_RE` back to `$` | 4 | 42 |
| M4: only the bootstrap `COMMIT_40` back to `$` | 2 | 44 |

Re-measured in fix round 2, at `4c1f80cf`, on the 46-row file. The first round measured its 42 rows: 7/35, 4/38, 3/39 and 2/40. M1 is M2 and M3 together.

Each is killed by at least one row, and each pattern is killed by a row of its own. Every mutant was undone: the production files are byte for byte (`cmp`) their backups of this change.

git 2.43.0 (`/usr/bin/git`):

- shard tA, 35 files: **2782 passed, 8 skipped** in 4138.49s, rc=0;
- shard tB, 39 files: **2630 passed, 6 skipped** in 4532.76s, rc=0;
- trio (`tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py`): **170 passed** in 37.48s, rc=0.

The two shards are 74 targeted files: every file that names a pin code (`pin-tag-only`, `pin-digest-*`, `shape-pin-*`, `manifest-shape-*`, `family-shape-*`, `member-pin-*`, `update-pin-*`, `neutral-pin-*`, `plan-pin-*`, `not 40/64 hex`) or one of `COMMIT_RE`, `SHA256_RE`, `COMMIT_40`, every `tests/test_bootstrap*.py` (13 of 13), `test_shape_doctor.py`, 6 of the 19 `test_update_shape*.py` files, `test_family.py`, `test_scaffold_e2e.py`, and the pin tests. They replaced a wide run (226 importer files, about 27k tests) that was killed by PID under load (load average 72 on 32 cores, 2% in 11 minutes); that run is not evidence. The new file and the four edited files are all inside the shards. The other 13 `test_update_shape*.py` files, which name no pin code, ran as a third run (480 passed, 3 skipped in 441.06s, rc=0), and `tests/test_scaffold_leg_path_collisions.py` as a fourth (104 passed in 4.35s, rc=0). No full suite was run.

git 2.47.1 (`git --version` printed first in each log):

- the new file, `test_pin_validator.py`, `test_manifest_validator.py`, `test_adopt_pins.py`: **323 passed** in 208.37s, rc=0.
- `test_shape_doctor.py -k a_root_pin_that_does_not_read`: **13 passed, 436 deselected** in 52.85s, rc=0.

Refreshed after the fix round, against the re-fetched origin/main:

- **Fix round 2, 2026-10-22:**
  - origin/main `038dafba9dbedd9405b316cc749a04dffe02bb36` (#1111) is merged into the branch as `c7da27dc2`, cleanly.
  - The head `4c1f80cf4db6215d8861bd43cd57f72df957d82a` merges with main at its own tree, `9884a99aeba2a55e486a355ae3483d8c932b4f68`: the seven files of this PR.
  - Of the 28 open pull requests, #1125 (head `2a53a8e6`) still touches `tests/test_adopt_pins.py`, and its merge-tree with this head is clean, tree `a026a192669a86abfaed59fbcce2fd1179bb0894`.
  - The bullets below are fix round 1's.
- `git merge-tree --write-tree origin/main HEAD` against `4ed7c7bf581dceb75f816f30f76b776c2b8917f0` (merge base `6da757cbe`), with this PR's head `03b2b97b9d3cccc42b4703cb2f7c978693488266`: clean, rc=0, tree `95d242489e6a8d6ea2be8c454a5e6655922d5712`. The difference between that tree and origin/main is exactly the seven files of this PR.
- The branch stays on `6da757cbe`. Since then three PRs landed. #1101 and #1102 touched `README.md`, `docs/cli.md`, `docs/handbook.html`, `scaffold-project.py`, `templates/assembly-root/project.yaml`, `templates/family-root/family.yaml`, `tests/readme-cap.d/915.txt`, `tests/test_scaffold_reuse_seeded_legs.py` and `tests/test_visibility_field_words.py`. #1091 (`4ed7c7bf5`) touched `adopt-project.py`, `tests/test_adopt_root_shape_paths.py` and `tests/test_adopt_root_symlinks.py`. None is a file of this PR, and `adopt-project.py` holds the same 8 uses of `COMMIT_RE` and `SHA256_RE` as before.
- Of the 23 open pull requests (listed with `gh pr list --state open --json number,headRefName,headRefOid,files`, this one included), one other touches a file of this PR: #1125 (head `2a53a8e64a0cce17ab84cd1bf5949bcb6fbd4484`) touches `tests/test_adopt_pins.py`. `git merge-tree --write-tree 2a53a8e HEAD` is clean, rc=0, tree `5fbd1eaafdf94004ead7142ae9132dc79dc4d1e5`.

One new commit on top, `03b2b97b9d3cccc42b4703cb2f7c978693488266`, after the independent review of `ae30dbd0e0eecfb7092c5ef257da2367d114fc62` (not ready on one must). It changes `tests/test_pin_pattern_line_break.py` and nothing else; no production file changes.

- **Composite asserts (SonarCloud `python:S9073`, the two issues at lines 164 and 173).** `member_pin` and `holder_shape` ended in `assert commit and digest, text`. Each value now has its own assert, with a message that names it (`fixture drift: no member pin commit`, `no member pin tree_sha256`, `no shape commit`, `no shape tree_sha256`).
- **Two callers had no row of their own,** so a `.rstrip("\n")` before the match in either survived every row. The new rows (the file now has 46):
  - `test_a_neutral_pin_commit_that_ends_in_a_line_break_is_refused`: the neutral product's pin in `validate-pins.py`, exit 2, `REFUSED neutral-pin-tag-only`. On main the commit passed `COMMIT_RE` and the run exited 1 with `neutral-pin-source-unreadable`, git refusing the name;
  - `test_a_neutral_pin_digest_that_ends_in_a_line_break_is_malformed`: the same pin's `tree_sha256`, exit 1, `neutral-pin-digest-malformed`. On main it was recomputed and compared (`neutral-pin-digest-mismatch`);
  - `test_a_recorded_source_commit_that_ends_in_a_line_break_is_a_finding`, two rows (`submodules`, `answered-paths`): a record row's `source_commit:` in `validate-manifest.py`, exit 1, `manifest-submodule-commit` and `manifest-answered-commit`. On main: `manifest ok`.
- **The four rows fail on main.** The new file with main's two production files checked out: **13 failed, 33 passed** (the nine of the first round, and these four).
- **Mutants,** each applied alone to the committed head, the file restored afterwards (sha256 `94f125c5f5e4b8a0` for `validate-pins.py` and `260c4742fe422597` for `validate-manifest.py`, the same as before the mutant, and clean against HEAD; `repo_shape.py` and `bootstrap.py` unchanged at `623af12131447f95` and `4102d375f4c9bc38`). The new file only:

  - S1, `.rstrip("\n")` on the neutral pin's commit before the match (`validate-pins.py` line 1790): **1 failed, 45 passed**, killed by the neutral-commit row;
  - S1b, the same on the neutral pin's recorded digest before the match (line 1800): **1 failed, 45 passed**, killed by the neutral-digest row;
  - S2, `.rstrip("\n")` on `source_commit` before the match (`validate-manifest.py` line 1264): **2 failed, 44 passed**, killed by both `source_commit` rows;
  - before this round S1 and S2 survived.
- **Runs, at head `03b2b97b9d3cccc42b4703cb2f7c978693488266`, worktree clean at each START:**
  - git 2.43.0: the new file, `tests/test_pin_validator.py` and `tests/test_manifest_validator.py`: **175 passed** in 58.03s, rc=0;
  - git 2.43.0: the trio: **170 passed** in 39.29s, rc=0;
  - git 2.43.0, extra: `tests/test_neutral_pin_validator.py` (the new rows use its helpers): **39 passed** in 99.62s, rc=0;
  - git 2.47.1 (`git --version` printed first): the new file: **46 passed** in 19.32s, rc=0.
  - The shards, the other git 2.47.1 runs and the extras above ran at `ae30dbd0e`; the two production files and the four other test files are the same at this head.

This round is one merge of main and one new commit, after rc1126's re-check: READY at `03b2b97b`, with two prose NITs (#1126 issuecomment-6340973564).

- **Merge of main:** `038dafba` (#1111), as `c7da27dc2`, with no conflict.
- **NIT 1:** `4c1f80cf4` changes only the module docstring of `tests/test_pin_pattern_line_break.py`. It now says what main did:
  - `validate-manifest.py` passed both a `shape.commit` and a `shape.digests.tree_sha256` that end in a line feed;
  - `validate-family.py` passed only the digest. Its commit gave `shape-pin-manifest-disagree` with the same hex on both sides, because `contracts/shape-pin.yaml` holds the commit without the line feed.
  - No code or row changes.
- **NIT 2:** the Mutants table above is re-measured on the 46-row file.
- **Runs at `4c1f80cf4`, git 2.43.0:**
  - the file and `tests/test_path_guard_hygiene.py`: **95 passed** (46 + 49);
  - with main's two production files checked out: **13 failed, 33 passed**, as before.

Other `^...$` patterns are used with `.match` on values read from files or arguments: `PROJECT_ID_RE`, `DATE_RE` and `QUALIFIED_RE` (`validate-manifest.py`, `validate-family.py`), `REPOSITORY_RE` (`update-shape.py`), `PIN_ARG_RE` (`shape_materialize.py`), and `NAME_RE`, `REF_RE` and `PATH_RE` (`setup-project.py`). They have the same `$` property and are outside the two patterns this issue names, so they are left for #1139, filed from this PR's review.

Review: lane 5's independent re-check, by a reviewer that wrote none of #1126: READY at `4c1f80cf`, the head that lands (issuecomment-6344730185), with no MUST, SHOULD or NIT. It checked the delta from `03b2b97b`, which rc1126 passed (issuecomment-6340973564). CI (Brett Heap's ruling 27): H0 is the head 4c1f80c itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 038dafb; and the local gate passed on its predicted squash tree df676e7 over main 8679c80: the adopt_pins, manifest_validator, pin_pattern_line_break, pin_validator, shape_doctor, repo_hygiene, agents_md_codes, cli_reference sets, 944 passed, 3 skipped. Landed on Brett Heap's word of 2026-10-23T13:10:26Z, relayed to every lane by lane 3 (#473 issuecomment-6365697123): "merge v1.2.1 PRs once reviewed and green".

Closes #1017

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Nov 8, 2026
…s message spells a path byte that is not UTF-8 as `_shown` does, an escape, and no longer says it names it with a `?` in its place, which #900 replaced (#1018) (#1104)

`env_commit`'s docstring in `scripts/shape_materialize.py` says what the split commit's message does with a `root` or `drop` path that holds a byte that is not UTF-8, as the code does since #900. It said the message names such a path "with a `?` in place of that byte, a path that does not exist". That was true before #900, and it is not true now. A reader of the docstring is told about a `caf?.md` the tool no longer writes.

Found by lane 1's writer on #900 and filed as #1018. Lane 5 took it outside v1.2.0, under Brett Heap's ruling of 2026-10-21T10:32:04Z.

Measured on main `2a119f281` and again on `16f9a0e81` (main after #1087), with a small script that calls: `adopt-project.py`'s own `_split_message` and `_shown`, then `env_commit` and a real `git commit`, read back with `cat -A`.

- The issue's claim held: the docstring (about line 2103 when filed, line 2268 on `2a119f281`, line 2349 on this branch) still said the message names the byte with a `?`.
- The code does not. `_shown("caf\udce9.md")` is `` `caf\udce9.md` ``, and the split message for a `root` and a `drop` entry named that holds no lone surrogate. Through `env_commit` and `git cat-file commit HEAD`, `cat -A`:

  ```
  DROPPED (in no leg and no longer here):$
    `caf\udce9.md`$
  STAYS IN THE ASSEMBLY ROOT:$
    README.md$
    `caf\udce9.md`$
  ```
- What stays true of `env_commit` itself: a message that holds the lone surrogate, handed to it directly, is still written as `?`. `cat -A` reads `Name: caf?.md$`. So the `errors="replace"` paragraph is kept, and what changes is who it describes.
- #1087 (main `16f9a0e81`) changed `scripts/shape_materialize.py` by 128 lines, none of them in `env_commit`, `COMMIT_COMMAND` or the message, and changed nothing in `adopt-project.py` (0 diff lines). The rebase onto it was clean and the docstring needed no other change.

One file, `env_commit`'s docstring, in two paragraphs: the `errors="replace"` paragraph (round 1, 11 lines added and 7 removed) and the sentence above it (round 2, below; 3 added and 2 removed). Together 14 added and 9 removed against main `6da757cb`. The `errors="replace"` paragraph now says:

- encoding, a character UTF-8 cannot carry is written as `?`, and nothing raises after both legs are pushed, as before;
- `adopt-project.py` refuses a lone surrogate in a path it extracts (`entry_path_problem`), and a `root` or `drop` entry that holds one is listed in the split message but never raw: the message names each path as `plan`'s report does (`_shown`, #900), so the byte 0xE9 reads as an escape with no surrogate in it, `` `caf\\udce9.md` ``, the path the source holds;
- written raw it would be a `?`, `caf?.md`, a path that does not exist, which is what `env_commit` still writes for a lone surrogate it is handed.

The backslash is doubled in the source, as the neighbouring docstrings write `\\udcc3`. A single `\u` in a non-raw docstring would be an escape that makes a lone surrogate. Measured: the docstring holds no character from U+D800 to U+DFFF and holds the six characters `\udce9`. The longest docstring line is 78 characters.

**The code is unchanged.** A script compares the AST of `scripts/shape_materialize.py` with every docstring blanked: the branch's file is identical to `origin/main`'s after the rebase onto `16f9a0e81` (round 1), and the merge of this head into `origin/main` `6da757cb` is identical to `6da757cb`'s file (round 2).

**No pin moves.** `scripts/shape_materialize.py` is not a pinned copy. `COPIED_VERBATIM`, `COPIED_FROM_SHAPE`, `FAMILY_COPIED_VERBATIM` and `FAMILY_COPIED_FROM_SHAPE` do not list it, and `templates/assembly-root/scripts/validate-pins.py` says of it "which no project carries". So no `upstream-changed` row, no digest, no assembly copy and no `tests/test_pinned_copy_text.py` entry changes.

**No other text pins these words.** Searched in `tests/`, `README.md`, `docs/`, `docs/handbook.html`, `AGENTS.md` and `templates/`: no test reads `env_commit`'s docstring (`__doc__`, `getdoc`), and none of them holds "in place of that byte" or "a path that does not exist" for this. The other places that name the `?` are about this function's behaviour or about history, and are still true: `_split_message`'s docstring says `caf\\udce9.md` "was `caf?.md`" (before #900), `_refused_by_policy` already says the message "lists it as `_shown` does (#900)", and `tests/test_adopt_spelled_names.py` quotes the message as it was on `c7a3ff16`. The `?` in `argv_text`, `trailer_line` and the commit-trailer text is about a `--trailer`, not the split message.

No rows and no mutants: the change is a docstring, the code is the same AST, and no test pins the docstring. So there is no row to fail on main and nothing to mutate.

All with `$Q/v110/run9.sh`, `EMAIL=service@opensoft.dev`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`, the stub `gh`, a fake token, git 2.43.0 (`/usr/bin/git`).

Two shards, run before the rebase, at the `2a119f281` base with this one-file change in the tree. Together they are every `tests/*.py` that names `shape_materialize` or `env_commit` (86 files, including `tests/test_adopt_spelled_names.py` and `tests/test_adopt_entry_paths.py`, which run `env_commit` and the #900 message) and `tests/test_agents_md_codes.py`:

| run | files | result |
|---|---|---|
| shard A | 43 | 2873 passed, 4 skipped in 1707.51s, rc=0 |
| shard B | 44 | 4611 passed, 31 skipped in 2462.01s, rc=0 |

After the rebase onto `16f9a0e81`, on the committed head `e32b2703d733f6c60c1b2cba2d62d164d93d7b9a` (`dirty=0`): the trio, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py` and `tests/test_cli_reference.py`, **170 passed in 55.67s**, rc=0. The docstring-only change needed no new shards: the shards' counts are runs at the `2a119f281` base, and #1087 did not touch `env_commit`.

Round 2, on the committed head `de89aa81b1533e32f1007f991aeae05cd915b36d` (`dirty=0`): the same trio, **170 passed in 61.77s**, rc=0. The round 2 commit is one docstring sentence, so it needed no new shards either.

git 2.47.1: no run. The change adds no test file of its own, so there is none to run on it.

`git merge-tree --write-tree HEAD <other>` with HEAD at `de89aa81b1533e32f1007f991aeae05cd915b36d`, after a re-fetch of `origin main` with `-c gc.auto=0 -c maintenance.auto=false` (round 2, about 14:03Z on 2026-10-21):

| against | result |
|---|---|
| `origin/main` `6da757cbe9a4eb66d60c34f38dcc2b332d175e8b` | clean, rc 0, tree `15c11b6c959fc4c7540105d5d62e3f961c8e27fd` |
| #1105 `fix/1048-makefile-follow-up-gnu-read-order` `5a4de7f763b5a3c32adce9614a3a539508416eb4`, an open PR that touches `scripts/shape_materialize.py` | clean, rc 0, tree `db05aead67052643942e35a9689d9dd4a0873fda` |
| #1118 `fix/1089-named-link-skip-tests` `c60066cbeec18b8b6c926e254ed4bee6b9a486c2`, the other open PR that touches `scripts/shape_materialize.py` | clean, rc 0, tree `a40df3b63c32c2a3d8f4c40587b094940c8d6567` |

Read with `gh pr list --state open --json number,headRefName,headRefOid,files`: 16 open PRs, three of them (this one, #1105 and #1118) touch `scripts/shape_materialize.py`, the only file this change touches. The other 13 (#1120, #1119, #1117, #1111, #1109, #1107, #1106, #1102, #1101, #1098, #1097, #1091, #1088) touch no file this change touches.

The same command with HEAD at the reviewed head `e32b2703` gives rv1104's trees: `de31d857` against `6da757cb` and `195ffe19` against #1105. The round 1 merge against #1090 is moot: #1090 has landed in `6da757cb`.

Held as a draft until the v1.2.0 tag; it is now on the v1.2.1 milestone.

- **NIT 1, `scripts/shape_materialize.py` lines 2340-2343, a line round 1 did not change.** The paragraph above the reworded one said the adoption's split message "names each entry path as the source spells it". Since #900 that is true of a printable path alone: `_split_message` lists any other through `_shown`, as the escape the next paragraph describes. It now says "as the source spells it (a printable one; see below)". One new commit on top of `e32b2703`, `de89aa81b1533e32f1007f991aeae05cd915b36d`, 3 lines added and 2 removed, docstring only, pushed as a fast-forward (no rebase, the PR is open).
  - Measured: the AST with docstrings blanked is identical to `e32b2703`'s and, merged into `6da757cb`, to `6da757cb`'s. The docstring holds no lone surrogate, still holds the six characters `\udce9`, and its longest line is 78 characters.
  - Searched `tests/`, `README.md`, `docs/`, the handbook mirror, `AGENTS.md`, `templates/` and the scripts for "names each entry path", "as the source spells" and the new words: only `tests/test_adopt_entry_paths.py` line 233 matches, a test's docstring about `execute` handing each path on as the source spells it, which is another sentence and pins nothing here.
- **NIT 2, this body.** It named main as `16f9a0e81` and #1090 as the one open PR that touches the file. #1090 has landed. Main is `6da757cb`, and the merge-trees above are redone against it, against #1105 and against #1118, a draft that opened since and also touches the file.

Review: rv1104-1107, lane 5's independent opus reviewer, which wrote none of #1104: READY at `de89aa81`, H0 of this landing (issuecomment-6318111413), with no finding; NIT 1 and NIT 2 of the round before are fixed. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 de89aa8 passed tests, tests-git-bash and SonarCloud; the head 92285d4 is a clean update-branch merge of main b66d8f0, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 6bd2128 over main b66d8f0: the repo_hygiene, agents_md_codes, cli_reference sets, 171 passed. Landed on Brett Heap's word of 2026-10-23T13:10:26Z, relayed to every lane by lane 3 (#473 issuecomment-6365697123): "merge v1.2.1 PRs once reviewed and green".

Closes #1018

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Nov 8, 2026
…r through a tag it lacks, is told so, a tag of a tree or a blob it lacks is refused by name, and rows hold `YAML_NOT_TEXT_RE`'s exponents of two digits (#1103) (#1131)

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

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

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

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

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

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

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

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

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

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

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

### What changed

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

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

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

| Pair | The same | Differ |
| --- | --- | --- |
| `9d14c29a` vs this head | 1532 of 1578 | 46: (a) 16, (b) 30, and nothing else |
| main vs this head | 1492 of 1578 | 86: those 46, the issue's case (12) and 28 `raw` asks |

- **(a):** 4 legs, `has_origin=False` only. Each holds the outer tag without the inner one: a tag of a tag of a commit, with the commit or without it, and a tag of a tag of a tree, with the tree or without it.
- **(b):** 6 (leg, id) pairs, `peeled_target` and the four verdicts each: a tag of a tree and a tag of a blob, each alone and under a held tag of it, and those two tags of a tag, all held without the tree or blob.
- **The 28 `raw` asks** ask `_unreachable_refusal` directly about a held tag of a tree or blob, which the flow never does: `peeled_target` refuses it first, with the tree or blob held or not. They keep the head's words.
- **Every other answer**, every id with an `origin` but (b)'s, every missing id and every held commit keeps main's words.

### Rows, failing on `9d14c29a` and on main

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

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

### Mutants

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

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

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

### Runs

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

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

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

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

- #1107 touches `tests/test_bump_leg_reachability_words.py`. It gives `made_in_the_leg` an optional `on=` and adds rows after `test_a_tag_of_a_tag_the_two_share_reaches_it`.
  - The rows here call neither `made_in_the_leg` nor anything #1107 changes. The merged file parses, and no name in it is defined twice.
- #1157 touches `scripts/bump-leg.py`, `scripts/shape_materialize.py` and two refspec test files, so it shares only `scripts/bump-leg.py` with this PR.
  - Its two hunks there are inside `_leg_fetch_refspecs`, at lines 1109 to 1146 of main's file. This PR's hunks in that file are at main's lines 976 to 1043 (`tag_chain_end`, new, and `peeled_target`) and 1250 to 1289 (`_unreachable_refusal`), so the two do not meet and the merge is clean.
- None of the other 34 of the 37 open PRs touches these three files.
- The branch is no longer behind main: the merge `ae830e2b` brought in every file main gained since `6da757cb` (among them #1111's `tests/test_path_guard_hygiene.py`). None of them is one of these three files, and the runs above stand on the head's own tree.

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

### What changed

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

### Rows, failing on main

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

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

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

### Mutants

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

| Mutant | Result | Rows that failed |
| --- | --- | --- |
| NX1 (rc1131's): the walk reads at most two headers, `while True:` as `for _ in range(2):` | **killed**: 2 failed, 67 passed | the 2 rows of the new kind: the walk stops at `tag`, and the refusal is `bump-leg-commit-not-on-remote`, not `bump-leg-target-not-a-commit` |
| NX0: the walk reads one header | killed: 4 failed | the 2 rows of the third kind and the 2 of the new one |
| NM2 (fix round 1's): `tag = named` replaced by a return | killed: 4 failed | the same 4 rows |
| NX2 (rc1131's): no check that the leg holds the next tag | killed: 2 failed | the 2 rows of `test_a_tag_through_a_tag_the_leg_lacks_is_told_which_tag_it_lacks` |
| C3: the comprehension's `key: value` as `value: key` | killed: 10 failed | the rows that read a tag's header |
| C6: the comprehension keeps the lines with no space (`if not sep`) | killed: 10 failed | the same 10 rows |
| NX3: the walk reads at most three headers | **survives**: 69 passed | none |
| C1: the `if sep` filter dropped | survives: 69 passed | none |
| C2: `rpartition` for `partition` | survives: 69 passed | none |

- **NX3 survives by construction.** A walk bounded at three headers gives the code's answer on every chain of up to three tags, and the longest chain the rows hold is three. Any bound survives a chain of that length, so a row of four tags would only move the survivor to a bound of four. NX1 is the mutant the review named, and it is killed.
- **C1 and C2 are equivalent for every header git writes.** The `type` and `object` lines each carry exactly one space and each appears once, and nothing reads another key. C1 keeps a line with no space as a key with an empty value, which `.get("type", "")` and `.get("object", "")` already say of a missing key. C2 splits at the last space, which for a line with one space is the first. Where they could differ is a hand-forged header that repeats a bare `type` line after `type tag`; no row forges one.

### Runs

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

| Run | git | Result |
| --- | --- | --- |
| `tests/test_bump_leg_reachability_words.py`, `tests/test_neutral_pin_entry_one_spelling.py`, `tests/test_bump_leg_dry_run_truth.py`, `tests/test_path_guard_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py`, `tests/test_pinned_copy_text.py` (`g243.log`) | 2.43.0 (`/usr/bin/git`) | 402 passed |
| the first three of those files (`g247.log`) | 2.47.1 (first on PATH; `git --version` printed `git version 2.47.1`) | 196 passed |

- The 57 files that run `bump-leg.py`, which fix round 1 ran in two shards (3419 passed), were not run again: the edit is inside `tag_chain_end`, and the rows that reach it are the two files above, which ran in full. CI's `tests` job on the pushed head is the full-suite run.
- `tests/test_path_guard_hygiene.py` passes: the commit adds no `is_file`, `is_dir`, `exists`, `is_symlink` or `os.path` guard.
- No added line is wider than 79 characters, none is non-ASCII, and no added assert joins conditions with `and` or `or`.

### Merge-trees at `55c30f20`

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

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

- This commit's hunks in `scripts/bump-leg.py` are at lines 979 to 1003, in `tag_chain_end`; #1157's against main are in `_leg_fetch_refspecs`, from line 1105. They do not meet.
- The other 38 of the 41 touch none of the three files.

### After the second re-check

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

Review: lane 5's independent re-check, by a reviewer that wrote none of #1131: READY at `55c30f20` (issuecomment-6352247887), with rc1131's NITs 1 to 3 fixed. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 55c30f2 passed tests, tests-git-bash and SonarCloud; the head db8533a is a clean update-branch merge of main 0d5704b, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 8fe39a5 over main 0d5704b: the bump_leg_reachability_words, neutral_pin_entry_one_spelling, repo_hygiene, agents_md_codes, cli_reference sets, 351 passed. Landed on Brett Heap's word of 2026-10-23T13:10:26Z, relayed to every lane by lane 3 (#473 issuecomment-6365697123): "merge v1.2.1 PRs once reviewed and green".

Closes #1103

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