Repository navigation
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
Conversation
…`, `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>
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)_is_a or a reasoned allowlist covers it, and a project.yaml linked into an unsearchable folder is yaml-unreadable (#430)
Reviewer's GuideThis 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 Sequence diagram for unreadable linked project manifest handlingsequenceDiagram
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
Flow diagram for repository path-guard hygiene enforcementflowchart 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"]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-1, 2026-10-21T13:35:34Z) r1111, lane 1's independent reviewer (not the writer), reviewed #1111 at 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 (
|
| 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.
- Where:
tests/test_path_guard_hygiene.py:335-340,:417-421,:460-461and:468-469. The code isshape-doctor.py:11476(check_the_way_in),:11599(check_the_other_way_in),:11008(origin_org) and:10977(repo_local_origin_name), andscripts/shape_advisory.py:495and:779. - Why the reason is false: each site asks
(root / ".git").exists(), which follows a link. The ROOT tag says "bylstat, or a rootmainsearched first". Neither protects a.gitthat is a link into a folder this user cannot search. That is exactly the class KNOWN_OPEN already lists forcheck_what_is_here, whose loop asks.gittoo. - Measured: a root
Atlas/with.git -> ../locked/repo/.gitandlocked/at 000, run with the head doctor:- Control: "the way in" says the repository is ADOPTED, NEXT
adopt-project.py plan. - 3.12:
FINDING the way in .git: cannot be read (Permission denied). - 3.14-like shim (my sitecustomize; real 3.14 is unverified):
the way in there is no repository here to adopt: a new project is SCAFFOLDED, which creates three repositories and asks firstandNEXT .../setup.sh --org <your-org> --project Atlas. "The other way in" and the Triad advisory rows vanish. - So the doctor sends a person with an existing repository to scaffold three new ones.
- Control: "the way in" says the repository is ADOPTED, NEXT
- Fix:
- Move these six to KNOWN_OPEN under 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 and add them to 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. Otherwise 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 sayscopy-missingandupdate-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 closes with them untracked. - Update the body's counts: BENIGN becomes 172 entries (176 sites) and ROOT becomes 20 (21).
family.py _members_on_disk top.is_dir()(also ROOT) follows a link too. Its effect is loud later, at the add, but its reason is inaccurate.
- Move these six to KNOWN_OPEN under 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 and add them to 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. Otherwise Every
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 Falseplaced beforeexcept OSError; - X18:
except Exception: return False; - X6:
or exc.errno == errno.EACCESbeside 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,Exceptionand a bareexcept:; - refuse any
errnoorwinerrorcomparison that is notin <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
-
N1. The scanner misses some spellings (probe file
/workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1111/probes/fn_probe.py):import os as _os; _os.path.isfile(X3 survived);P = os.path; P.isfile;getattr(p, "is_file")(),getattr(os.path, "exists")andoperator.methodcaller;os.path.ismount,is_mount,is_fifo,is_junction, andgetattr(os.path, "isjunction", ...);os.access(..., F_OK);- a hand-written
try: stat() except OSError: return False.
None of these occurs in shipped code today. The isjunction lookups at
shape-doctor.py:10305andsiblings.py:782and:1352are Windows-only. The body's "covers each spelling" overstates this.What the scanner does catch:
- aliases from
from os.path import,import os.path as,from os import pathandposixpath/genericpath; - path variables, chained calls, lambdas, comprehensions and conditional expressions;
- module-level guards, default arguments and decorators.
-
N2. A
class _is_aorasync def _is_agets the helper exemption but no parity check (X15 survived). -
N3. The stale check and the count logic have no self-test. Both of these survive: T2 (stale check disabled) and T3 (
if limit or ..., so one entry silences any count). The live tree does catch both cases (X2, X9, X17, W4), but no self-test pins the logic. -
N4. The body's merge claim names main
7e00834c; main is now6da757cb. My merge with it is clean. -
N5. Older wording that the new row now shows:
- The
manifestrow's WHY foryaml-unreadabletalks of "a merge conflict resolved, a hand edit". - The chmod cure says "every directory above it inside the root", but the locked folder here is outside the root.
- Both are wording from shape-doctor.py calls an assembly root whose
project.yamldoes not parse (a merge conflict) NOT A SHAPE ROOT, exit 2, and its NEXT line tells the reader to adopt that assembly root in place while the Triad row calls it a single repository #205 and shape-doctor.py: for a pin or acontracts/this user cannot read, thelegs,manifest kindsandshape currencyrows disagree with validate-pins.py's named refusal, andmanifest kindsnames agit checkout HEAD --that discards work #847, not from this PR.
- The
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 asalias; - X4: a guard inside the helper;
- X5, X6 and X18: EACCES read as absent;
- X12:
except PermissionErrorin_to_load, so an EIOstatwould traceback; low; - X15: a class named
_is_a; - T2: stale check disabled;
- T3: count logic loosened;
- T6: the KNOWN_OPEN owner check loosened; trivial.
- X3: an
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):
templates/family-root/scripts/validate-family.py_on_disk;src/openreposhape/cli.pydoctor;scripts/repo_shape.pyfind_repo_root: PINNED, and no issue tracks it after EveryPath.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 sayscopy-missingandupdate-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 closes;shape-doctor.pydetached: callers not traced;update-shape.pyRollback.write_added: loud failure, loose OWN tag;scripts/family.py_members_on_disk: loud failure, reason inaccurate.
Not benign (✗): all six M1 entries. Of these:
check_the_way_inandcheck_the_other_way_inwere measured, and so were shape_advisory's_readandrepository_names(the Triad row vanishes);origin_organdrepo_local_origin_nameare the same class and were not measured one by one.
Not finished
- 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. origin_organdrepo_local_origin_namewere not measured one by one.detached's callers were not traced.- 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>
|
(lane openRepoShape-1, 2026-10-21T15:30:43Z) r1111b, lane 1's independent reviewer (not the writer), re-checked #1111 at 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 (
|
| 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 |
- Scanning with head's module:
16f9a0e8,6da757cband823624d9each have 188 sites in 18 of 25 files, with the one unlistedContext.__init__ path.is_file(). So scaffold-project.py: the--reuse-empty-repohelp, 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 abovevisibility: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 add no guard.- Head and the merged tree each have 187 sites, none unlisted, none stale, none in a helper.
- The six-file list on the merged tree, 305 passed, is the 297 list round 1 could not run.
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 |
- In the full doctor report under the shim,
namingreadsAtlasand notAtlas, Orbit, and the other-way-in and Triad rows are gone. - On 3.12 the report shows
FINDING … .git: cannot be read (Permission denied)fornaming, the way in and the other way in. - Every KNOWN_OPEN reason is true. The 11 entries match 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 site for site, by name, by call and by line (11476, 11599, 11008, 10977, 495, 779).
_members_on_disk's new ROOT reason is true for 3.14. With aMembersormemberslink into a 000 folder, the shim gives exactly the readable control's answer (a symlink, moved aside, or not judged). On 3.12,top.is_dir()raisesPermissionError. That is loud, not the EveryPath.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 sayscopy-missingandupdate-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 "absent" class.
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 throughglobal, function-localimport os as,q = os.pathandfrom os import path as, and AnnAssign. - Rebinding and shadowing: names are tracked across the whole file and never dropped. A parameter named
path, a localo = Thing(), or a localexistsgets 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.
-
Where: body line 26, and the same claim in the test's
_os_path_namesdocstring,tests/test_path_guard_hygiene.py:677-681("in no shipped tool today"). -
Evidence: three shipped sites ask
getattr(os.path, "isjunction", lambda _: False), which answers False to anyOSErroron Windows:shape-doctor.py:10305(unclonable);templates/family-root/scripts/siblings.py:822(not_a_clone) and:1393(family_link), as numbered on the merged tree.
Each sits beside an allowlisted DELIBERATE
is_symlink/islink, so each is benign in context. -
Fix:
- Body: "…except three Windows-only
getattr(os.path, "isjunction", …)lookups (shape-doctor.pyunclonable,siblings.pynot_a_cloneandfamily_link), each beside an allowlistedis_symlink/islink." - The docstring can change with 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 follow-up.
- Body: "…except three Windows-only
NIT
-
The S1 class is still open by other one-line routes.
- Probed with
…/r2/probes/parity_probe.pyand as mutants. These pass parity:or isinstance(exc, PermissionError)(Z1);- a dead
return Falsebefore theraise(Z2). The bare-raisecheck at:1072-1076asks only that araiseexists; _ABSENT_ERRNOS += (errno.EACCES,)(Z3)._constantsat:970-980reads 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 bytest_validate_pins_unreadable_workflows_dir.pyand others (55 failed), and Z3 bytest_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).
- Probed with
-
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
fororwithtarget, a default parameter or a class attribute, or viaimportlib/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. -
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 KeyErrorin 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".
-
Self-test gaps (each of these test mutants survives):
- R2: the alias fixpoint cut to one pass;
- R3: AnnAssign no longer followed (
:701); - R7:
_helperswithoutClassDef(:1112-1120). Z10, a class_is_awith no guard, would then pass; - R13:
_membersskipping an element it can't read (:1002). Head refuses a by-nameEACCES(Z15 killed); nothing pins that.
Fix: a SPELLINGS line
typed: Any = os.path, a_helpersself-test, and a parity row withfrom errno import EACCES. -
The KNOWN_OPEN owner check is a tautology. At
:859, every entry usesFOLLOW_UP, so T6 (assert owner) and Z11 (FOLLOW_UP = "#1101") both survive. A typo in the constant at:123passes. 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. -
The
HELPERcomment "as slice A wrote it" (:1160) is inexact. Slice A'sscripts/bump-leg.py_is_ahas its set inline and no_absent;HELPERhas adopt-project/family.py's shape. -
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.
-
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_aor a reasoned allowlist covers it, and aproject.yamllinked into an unsearchable folder isyaml-unreadable(#430) #1111 has no milestone. EveryPath.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 sayscopy-missingandupdate-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-repohelp, 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 abovevisibility: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.
- Killed: R1 (no
- 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/holdsmutants.py,six.sh,members.sh,alias_probe.py,parity_probe.pyandmissforms.py;logs/holdsgate_merged.log,mutants.logandz*_slice*.log.
Lane: openreposhape-1 (openRepoShape-1)
|
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 |
…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>
|
|
(lane openRepoShape-1, 2026-10-21T17:17:47Z) r1111c, lane 1's independent reviewer (not the writer), re-checked #1111's third round at
Lane 1's landing chain runs on Verdict: READY. I found no MUST or SHOULD. Head 1. The delta: pass
2. Each entry's reason: passThe two
The 3. The run and the bite proof: pass
4. The PR body: pass
FindingsMUST: none. NIT 1: NIT 2: carried over from round 2 and not new. The open NITs are the Everything is in Lane: openreposhape-1 (openRepoShape-1) |
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…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>
….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>
…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>
…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>
…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>
…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>
…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>
…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>
…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>



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()andis_symlink()guards onto a local_is_a(is_ain the doctor). From Python 3.14 those methods answer False to anyOSError, andos.path.isfile,isdir,exists,lexistsandislinkalways have. The helper treats ENOENT, ENOTDIR, EBADF and ELOOP (and Windows' 21, 123 and 1921) as absent and raises every otherOSError, so the caller refuses it by name:<path>: cannot be read (<reason>), exit 2.scripts/bump-leg.pyupdate-shape.pyshape-doctor.py,scripts/shape_advisory.pyvalidate-manifest.pyandbootstrap.pyvalidate-family.pysiblings.pyandbootstrap.pyscripts/family.pyadopt-project.pyThis 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 inshape-doctor.py.The hygiene test
tests/test_repo_hygiene.py'sSHIPPEDlists, and every*.pyat the top level, inscripts/, intemplates/**/scripts/and insrc/openreposhape/. That is 25 files today; the glob adds none thatSHIPPEDlacks.astand finds every call of the four methods and the fiveos.pathtests. It catches:os.path,posixpath,ntpathorgenericpath, or throughimport os as o(o.path),import os.path as p,from os import pathorp = os.path;from os.path import lexists);filter(os.path.exists, ...),map(Path.is_dir, ...)).getattr(p, "is_file"),operator.methodcaller), the other questions of the same kind (os.path.ismount,is_fifo,os.accesswithF_OK), and a hand-writtenstatwhoseOSErroris False. It also misses an alias bound by tuple unpacking, a walrus, afororwithtarget, a default parameter, a class attribute,importliborsys.modules, ando = os; o.path.isfileorfrom os.path import *. A guard bound once at module level (EX = os.path.exists) is flagged at the binding, but its later calls are not. Shipped code has none of these except three Windows-onlygetattr(os.path, "isjunction", ...)lookups:shape-doctor.pyunclonable, andtemplates/family-root/scripts/siblings.pynot_a_cloneandfamily_link. Each sits beside an allowlistedis_symlinkorislink, so each is benign in context. The test's_os_path_namesdocstring still says "in no shipped tool today"; it changes with a follow-up.BENIGNorKNOWN_OPEN, as often as its function makes it. Those lists key each entry by file, enclosing function and the call's own text, never by line, with a count. The failure namesfile:lineand the function, and says what to do: use the file's_is_a, or copy validate-pins.py's where the file has none (repo_shape.py'sRefusalgives every refusal built without a remediation the assembly root's own (run git submodule update --init --recursive, then python3 scripts/validate-pins.py ... contracts/<leg>-pin.yaml), so in a family holderfamily.py addover an unparsable.gitmodules(git-failed),bumpon afamily.yamlof the wrong kind, and the holder's ownvalidate-family.pyon a shortpin.commiteach tell the operator to run a file no holder carries and to move a leg pin a holder does not have #370, Pinned shape copies: AGENTS-shape.md'sCONTRACTS_DIRsentence is false for every scaffolded root, and the chain WARNING offers remedies the scaffold lacks (one shape bump before the v1.1.0 tag) #610). Where the site is benign, it says to add an entry with a tag and a reason.statand nothing else. No entry may name one.TAGS, or with no reason.statorlstat.The parity test
_is_aandis_ain 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 eachgetattr(<exc>, "winerror", None) in <set>, through the module constant, aserrno.<NAME>names and numbers. A set it cannot read that way fails; it is not guessed at.statto itsraise:OSErrorandValueError. A handler forPermissionError,Exception, or a bareexcept:fails;except OSErrormust hold a bareraise;errnoandwinerrormay be read only asin <set>. An== errno.EACCESbeside the set fails.async defby a helper's name fails.*ABSENT_ERRNOSor*ABSENT_WINERRORSmust be the one set. There are 24, two per helper.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'svalidate-pins.py,validate-manifest.pyandbootstrap.py, and the family root'svalidate-family.py,bootstrap.pyandsiblings.py.What the scan finds
On main
4ed7c7bfthe scan finds 191 bare guards in 18 of the 25 files: the 188 of16f9a0e8,6da757cband823624d9, and three that #1091 added toadopt-project.py. None is inside a helper.BENIGN, in 175 entries. Each carries the plan's own reason for leaving a site out (lane 1's site table at5017830) and a line of its own:os.pathcall 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)stat,mount_reador git asked the path or its folder, or inside atrythat keeps theOSErrorlstat, 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 itscripts/repo_shape.py'sfind_repo_root, which the plan left (pinned, low)checknames what the clone's.githolds as git keeps refs, and asks a pin's file through the links the root keeps, so a link into.gitor onto a pin's file is refused before any leg or adopted with the copy beside it (#904) #1091's three areOWN. None needs 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._PlannedRoot._no_directoryand_PlannedRoot._into_gitaskself._git().is_dir(...)._git()returns_CloneGit,check's model in memory of the names git makes in the clone's own.git(adopt-project.pycheck: a root-kept symlink into the clone's.git, or through a kept link to a declared pin's file, passes, or is refused for a reason that is not true #904). Itsis_diris a set lookup, not the disk. Measured in a folder with no.git, with everystat,lstat, listing andos.pathtest patched to raise: it answers True, False and None as its sets say, and asks the disk nothing._clone_ref_storeasks(probe / CLONE_GIT_DIR / "reftable").is_dir()ofgit init's throwaway, amkdtempof this user's (0700) in the run's own work directory. A work directory at 0500 or 0600 stops at themkdtempbefore the guard is asked, andref_storeanswers git's defaults. The one way to a refusedstatthere is a git template holdingreftableas a link into a folder at 000. Then 3.12 raises intoref_store, which answers git's defaults, and the shim reads False. Both answer(False, True)on git 2.43.0, as the readable control does.KNOWN_OPEN. These are real sites this slice found and did not change. Each is left to the follow-up issue 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, which is not in v1.2.0. In each, a name in the root that is a link into a folder this user cannot search is still taken for nothing there. Every "under the shim" result below was measured with a 3.14-likesitecustomize.scripts/family.py_gitmodules_namesPermissionErrorraised out ofremove; under the shim(None, []), soremove --dry-runsays no section is removed. Slice G left it for this slice to flag. A change needs a refusal threaded throughremoveand its rollback.scripts/family.py_sections_giving[]under the shimtemplates/family-root/scripts/siblings.pysubmodule_urlsshape-doctor.pycheck_agent_filesabsentin theagent filesnote, which never moves the verdictshape-doctor.pycheck_what_is_hereshape-doctor.pycheck_the_way_in.gitlinked that way: under the shim theway-inrow says there is no repository to adopt, and sends the person tosetup.shto scaffold three new ones; on 3.12 the row names.git: cannot be readshape-doctor.pycheck_the_other_way_in.git: theway-in-scaffoldrow is left outshape-doctor.pyorigin_org.git, measured alone:Noneunder the shim, the answer for a folder that is no repository ('acme'in the readable control); 3.12 raisesPermissionErrorshape-doctor.pyrepo_local_origin_name.git, measured alone:Noneunder the shim ('Orbit'in the control), so thenamingrow classifiesAtlasalone, notAtlas, Orbit; 3.12 raisesscripts/shape_advisory.py_read.git:read()saysnot-a-repositoryunder the shim (advisein the control), so the Triad advisory row is left outscripts/shape_advisory.pyrepository_names.git, measured alone:['Atlas']under the shim (['Atlas', 'Orbit']in the control); 3.12 raisesThe site changed here: the doctor's own manifest
shape-doctor.py'sContext.__init__askedpath.is_file()of the root'sproject.yamlandfamily.yaml. Slice C's root check inmainasks only whether the root can be searched, so aproject.yamlthat is a link into a folder this user cannot search got past it.It is now asked with
is_a, and a refusedstatis loaded (Context._to_load).load_yamlthen refuses it as it refuses a file it cannot open, and themanifestrow names it, as it already did for aproject.yamlat mode 000 (#205).Measured on a root holding
project.yaml -> ../locked/project.yamlwithlocked/at 000, Python 3.12.3, and the 3.14-like shim:16f9a0e8)PermissionErrortraceback atContext.__init__, exit 1kind 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 1NOT A SHAPE ROOT, exit 2, withadopt-project.py plan --source ...as the NEXT, over a projectThe
manifest kindsrow names the same path with thechmodcure, in validate-pins.py's words. Where thestatsucceeds,is_a(path, stat.S_ISREG)answers asis_file()did, so every other root reads as it did. An I/O error from thestatis 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:
manifestrow's WHY foryaml-unreadablespeaks of "a merge conflict resolved, a hand edit";chmodcure 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 archiveof 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.scripts/bump-leg.py_validators_readableback topath.is_file()scripts/bump-leg.py:1474errno.EACCESadded tovalidate-pins.py's_ABSENT_ERRNOSupdate-shape.py's_ABSENT_WINERRORSpath.is_file()inupdate-shape.py's_extend_the_eol_blocktaken outContext.__init__back topath.is_file()import os as _osif not path.exists(): return Falseinside bump-leg's_is_aexcept PermissionError: return Falsebeforeexcept OSErrorcatches PermissionErroror exc.errno == errno.EACCESbeside the setreads exc.errno other than as in <set>_is_aholding a bare guardexcept Exception: return Falsebeforeexcept OSErrorcatches Exceptionerrnoread other thaninno longer refusedimport os as ono longer followedp = os.pathno longer followedasync defby a helper's name read as a functionexcept PermissionErrorin_to_load(r1111's X12)checknames what the clone's.githolds as git keeps refs, and asks a pin's file through the links the root keeps, so a link into.gitor onto a pin's file is refused before any leg or adopted with the copy beside it (#904) #1091's sites: 3 of 3 bite. Each new entry was taken out alone, on the merge with main4ed7c7bf. Each run gave 1 failed and 48 passed._PlannedRoot._no_directoryadopt-project.py, namingadopt-project.py:10785_PlannedRoot._into_gitadopt-project.py:10817_clone_ref_storeadopt-project.py:11113==, anasync defand a class.Tests
tests/test_path_guard_hygiene.py, new: 49 tests.refused_statand a refusedread_text, for a permission and for an I/O error), on every Python and as root;4ed7c7bf, with the new file laid over it), 45 pass and 4 fail. The 4 are the doctor's unlistedContext.__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.d286ae9a. The head holds main4ed7c7bfthrough the plain merge8bc4d1b2, with treefc0731ea; the branch's first merge,03c16cb9, took6da757cb. Against main, the head changes onlyshape-doctor.pyand the new file. The gate ran on the head itself, with Python 3.12.3, git 2.43.0, a stubghfirst onPATH,GH_TOKENbogus, an emptyGH_CONFIG_DIR,GIT_CONFIG_GLOBAL=/dev/nullandGIT_SSH_COMMAND=false:tests/test_repo_hygiene.py,tests/test_cli_reference.py,tests/test_agents_md_codes.py,tests/test_doctor_unreadable_paths.pyandtests/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.--helptext changes, sodocs/cli.mdis unchanged. No pinned copy changes, so no project or holder gets anupstream-changedrow.Closes #430
Lane: openreposhape-1 (openRepoShape-1)
🤖 Generated with Claude Code