Repository navigation
bootstrap.py, validate-family.py and siblings.py: a root or holder whose .git this user cannot read is refused git-repo-unreadable, exit 2, not a traceback or not-a-git-repo, and the doctor's members row names it once, never drift (#1148) - #1172
brettheap wants to merge 16 commits into
Conversation
…ose own `.git` this user cannot read is named, `.git: cannot be read (<reason>)`, so the doctor says INVALID and not DRIFTED, and both pinned validators refuse it by name, exit 2, with no traceback (#1146) The case: an assembly root whose `.git` is a link into a folder at mode 000. On main 038dafb the doctor said DRIFTED (2 leg(s) not at the pin), its `legs` row said `no gitlink recorded` of each leg, and the root's own `scripts/validate-manifest.py` and `scripts/validate-pins.py` ended in a `PermissionError` traceback out of `find_repo_root` (3.12), or refused `not-a-git-repo` (3.14-like shim). - validate-pins.py and validate-manifest.py (pinned copies): each `.git` `find_repo_root` would ask is asked first with the file's own `_is_a`, and one whose `stat` is refused is `git-repo-unreadable`, exit 2, in the file's `_unreadable_file` words. validate-pins.py, which reads the gitlinks through git, also asks the first `.git` that is there what it asks a leg's (#968): a gitfile it cannot open, a git directory whose `HEAD` it cannot reach. - shape-doctor.py: `root_git_read` asks the same; the `legs` row says the gate's line once, NEXT this doctor again, WHY the gate's advice, each leg keeps it in `gitlink_cannot_be_read`, which `drifted_reasons` does not count; `placement` says it, n/a. - tests/test_root_git_unreadable.py: 35 rows, mode and stand-in. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… legs' `.git`, since a root whose own `.git` is refused too is #1146's case, which the `legs` row names once; the new file's one-path stand-in answers `is_file` and `is_dir` as 3.14 and puts `sys.path` back (#1146) `test_a_mount_whose_git_cannot_be_reached_is_not_one_without_a_git` used `refused_stat(monkeypatch, ".git")`, which refuses every `.git` by name, the root's own among them. Its case is a mount this user can list and not search, so it now refuses `spec/.git` and `code/.git` alone, by path (`as_python_314`), and says the same lines it always did, on base and head. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… audits a leg whose own repository git reads, and gives the root's line only to a leg git cannot read through it; each `legs` entry carries every key, null; the tests pin both gates' walk (#1146) r1147's S2. `placement` declined every leg once the root's `.git` could not be read, so a leg with a `.git` directory of its own lost the path in the wrong leg the base reported, and the verdict read `INVALID (pins)` where the ladder's answer is `MISPLACED (1 path)`. `audit_leg` now gives `root_git_read`'s line to a leg whose `.git` is there and leads to a repository git names no top level for (`gitlink_cannot_be_read`, and its `state`); a leg git reads is audited, a leg with no `.git` of its own keeps git's answer, and a leg refused for its own reason keeps that. The row says the root's line once, `n/a`, only where it is every leg's. N7. The `legs` row's entries for an unreadable root carry `pin`, `populated`, `head`, `own_git` and `unborn`, each null, beside `gitlink`, so a reader of `--json` finds every key a leg's entry has. tests/test_root_git_unreadable.py, 35 rows to 45: - S1: per gate, a relative `--root .` run from `contracts/`, and the walk stopping at a readable gitfile with a `.git` above it refused; - N1: a gitfile ending in CR LF, and one naming an unprintable folder; - N2: `placement`'s `--json` entries for the root's line; - N3: `validate-pins.py`'s git-directory step under a subfolder `--root`; - S2: a leg audited beside one read through the root (MISPLACED), a leg with no `.git` of its own, and an in-process row on every platform. The stand-in row that refuses every `.git` now expects each leg's own line in `placement`. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…as root, only a leg git names no repository for is given the root's line, and each `legs` entry for an unreadable root is held whole, nulls included (#1146) The rule `audit_leg` follows, and the `legs` entries' keys (r1147's N7), were held only by the mode cases, which skip on Windows and as root. One row puts git's answers in by hand beside a refused root `.git`: the code leg, whose `.git` is there and git names no top level for, is given the root's line; the spec leg keeps git's answer where git answers from another repository, or where it has no `.git` of its own. The stand-in row that refuses every `.git` now compares each `legs` entry whole. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and the doctor's members row, with a new test file not yet run Work in progress, stacked on #1147 (b75babe). Code, smoke-measured by a probe on 3.12 and a 3.14-like shim: - templates/assembly-root/scripts/bootstrap.py, templates/family-root/ scripts/validate-family.py and bootstrap.py: `_refuse_an_unreadable_root` asks each `.git` find_repo_root would ask, then what validate-pins.py asks of the first that is there (a gitfile it cannot open, a git directory whose HEAD it cannot reach); `git-repo-unreadable`, exit 2, in each tool's own words. - templates/family-root/scripts/siblings.py: the stat step only, as validate-manifest.py asks it; `member_unreadable` takes a `code`. - shape-doctor.py: `members` says a holder's unreadable `.git` once (`members_git_unread_row`), and `drifted_reasons` counts no member that carries `gitlink_cannot_be_read`. tests/test_pinned_root_git_unreadable.py is NEW and HAS NOT BEEN RUN: it may fail. No mutants, no gate yet. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, from a folder under one whose name holds an ESC, is spelled by each of the four tools, so dropping `spelled` from siblings.py's own call is seen (#1148) The 55th mutant, `S-spelled-removed`, survived the file as it stood: its only unprintable row was the git readers' gitfile row. The new row walks from `<ESC folder>/root/contracts` with the root's `.git` refused, as Python 3.14's `exists()` answers it, and expects the whole path spelled. Base: 4 failed (`not-a-git-repo`); head: 4 passed, 3.12 and the 3.14 shim. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n the holder as its root whole, so `_refuse_an_unreadable_root` asks nothing there and a row's `.git` refused by name is a member's again, `member-unreadable` (#1148) Lane 1's gate of #1148 failed 6 of 53 here, 53 of 53 on #1147's head b75babe: `REFUSED git-repo-unreadable: .git: cannot be read (Permission denied)` where each row expects `REFUSED member-unreadable: members/IRRS/.git: ...`. The cause is the fixture, not the tool. `refused_stat(monkeypatch, ".git")` refuses the `stat` of EVERY path named `.git`, the holder's own with the member's. `gate()` stubbed only `find_repo_root`, the one asker of the holder's `.git` on base (validate-family.py:4193 there). #1148's `_run_checks` asks `_refuse_an_unreadable_root(start)` first (validate-family.py:4258), which the stub did not cover, so it met the holder's refused `.git` and answered for it, named `.git`, relative to the holder. The six rows are the ones that run `main` in-process after that refusal; the rows that call one function alone never reach `_run_checks`. In a real run the walk asks only `start` and the folders above it, never a member: the mode rows (`member-000`, `members-000`, `variant-000`, `staged-removal-000`, ...) run the whole gate in a subprocess, the new check included, and passed on #1148's head before this change. `gate()` now gives the holder as the root whole: `find_repo_root` answers it and `_refuse_an_unreadable_root` asks nothing. No assertion changes. The holder's own `.git` stays held by tests/test_pinned_root_git_unreadable.py (73 passed). Mutants, in a clone: the stub removed is killed by `test_a_path_whose_stat_is_refused_is_refused_by_name[member-checkout]` (and the five others); validate-family's root check walking from `members/IRRS` is killed by `test_a_folder_this_user_cannot_search_is_refused_by_name[member-000]`. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideImplementationAdd root
Make the doctor report an unreadable holder
TestsAdd coverage for unreadable root repositories across the pinned callers and doctor, and adapt the gate fixture to distinguish holder-level from member-level
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-1, 2026-10-22T21:23:31Z) Independent review r1172 of #1172 at
Lane 1 runs the fix round in a later window. Review of #1172 (r1172, lane openRepoShape-1): partial, cut short by STOPVerdict: NOT READY, as far as I got. I found no MUST. There is one SHOULD: a key line in all three git readers has no test that catches a change to it. It needs one cheap test row. Coverage is incomplete; see the last section. State at stop: no runs are live. Each mutant was restored and its blob checked against HEAD. Your three points1. f1148's stub (commit
|
…tfile that names no directory stops the walk, so a `.git` above it that cannot be read is never asked, and each git reader's `return` there is now held (#1148) Review r1 of #1172 (issuecomment-6351621763) found one SHOULD: the line in each of the three git readers that returns when `.git` is a gitfile not starting with `gitdir: ` had no test that caught a change to it (templates/assembly-root/scripts/bootstrap.py, templates/family-root/scripts/ validate-family.py and templates/family-root/scripts/bootstrap.py, each in `_refuse_an_unreadable_root`). `find_repo_root` stops at that `.git`, so the question must stop there too. Its three mutants, `return` made `continue`, all survived the 73 rows. New row `test_the_walk_stops_at_a_gitfile_that_names_no_directory`, over GIT_READERS (3 ids): the root's `.git` is a gitfile reading `no gitdir`, the parent's `.git` is refused with `as_python_314`, and `_refuse_an_unreadable_root(root)` answers None. A tool that went on to the parent would raise over a `.git` that `find_repo_root` never reaches. It sits beside `test_the_walk_stops_at_a_git_that_is_a_gitfile`, which holds the gitfile that does name a directory. Mutants, in a scratch clone of this tree, the new file run with `-x` on 3.12.3, the file put back in a `finally`: - A-gitfile-naming-nothing-continues KILLED by `[assembly-bootstrap]` - V-gitfile-naming-nothing-continues KILLED by `[validate-family]` - F-gitfile-naming-nothing-continues KILLED by `[family-bootstrap]` The file now has 76 rows: 76 passed on 3.12.3 and 76 passed under the 3.14-like shim; with the file laid over #1147's head `b75babee`, 74 failed and 2 passed on both. The new row fails there on the name base lacks (`_refuse_an_unreadable_root`, AttributeError), as the other 33 do. Also tests/test_validate_family_unreadable.py 53 passed, and tests/test_path_guard_hygiene.py with tests/test_repo_hygiene.py 149 passed. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-1, 2026-10-23T08:09:41Z) Independent re-review r1172b of #1172 at Review r1172b of #1172 at
|
| 3.12.3 | 3.14-like shim (mine, r1172b/shim314) |
|
|---|---|---|
| base | 74 failed, 2 passed |
74 failed, 2 passed |
| head | 76 passed |
76 passed |
- The new row's 3 ids fail on base and pass at head, on both Pythons. On base each fails with
test_pinned_root_git_unreadable.py:452: AttributeError: module 'pinned_root_git_bootstrap' has no attribute '_refuse_an_unreadable_root', and the same for the other two tools. - The three mutants that survived r1172, run in
r1172b/clone-headwith-xon the whole file. Each file was restored in afinally, and its blob matched HEAD's after:Each one fails withA-gitfile-naming-nothing-continues KILLED [assembly-bootstrap] 1 failed, 45 passed restored=True V-gitfile-naming-nothing-continues KILLED [validate-family] 1 failed, 47 passed restored=True F-gitfile-naming-nothing-continues KILLED [family-bootstrap] 1 failed, 46 passed restored=TrueREFUSED git-repo-unreadable: <parent>/.git: cannot be read (Permission denied). That is the wrong refusal the row exists to catch, and the counts match the body's word for word. 3/3 killed.
2. NIT-2 and NIT-4
- NIT-2 is done. The body now says the other family rows were ok and that "
placementisn/afor a holder". r1172's probe files (r1172/probe-{base,head}-{312,314}.jsonl) agree: in all 6 states, on both sides and both Pythons,manifest-kinds,shape-currencyandagent-filesareokandplacementisn/a. - NIT-4 is done. I checked the new Known-limits sentence myself on 3.12, with a searchable
.gitwhoseHEADis at 000. Each tool ran from the root, and base and head gave the same answers:- the assembly bootstrap:
REFUSED pin-gitlink-absent: no gitlink (mode 160000) recorded at 'spec', thenbootstrap REFUSED: pins (exit 2); validate-family.py:REFUSED gitlinks-unreadable, exit 2;- the family bootstrap:
FETCH INCOMPLETE (git exit 128) using credential source 'none':andfatal: not a git repository, with theSHAPE_LEGS_TOKENadvice, exit 1; - with
--no-fetch --skip-members:family bootstrap ok, exit 0.
- the assembly bootstrap:
3. Reading the diff adversarially
The row is sound. _is_a uses path.stat(), which as_python_314 patches, so a continue really does reach the parent's refused .git. Nothing in it depends on the Python version, and it runs as root and on Windows. Leaving out siblings is right: siblings' stop at any .git that is there is held by test_the_walk_stops_at_a_git_that_is_a_gitfile[siblings].
4. The body's claims
- Rows and counts: 76 rows; base 74 failed and 2 passed, head 76 passed, both Pythons. Confirmed.
- The 2 base passes are the two named controls. Confirmed.
- Why base fails: 36 rows on the
AttributeErrorand 38 on base's own answers. Confirmed: 74 tb lines, 36 of themAttributeError. - Hygiene:
test_path_guard_hygiene.pywithtest_repo_hygiene.pygave149 passedat head. Confirmed. - The regression file:
test_validate_family_unreadable.pygave53 passedat5aa0f116. Confirmed. - Gate arithmetic: the counts add up.
pinis 237 + (234+3) + 237 + 234 = 945;sdis 445+3 = 448;bigis 8037+39 = 8076;newis 255+3 = 258. - Closing form: exactly one,
Closes #1148. A regex finds no other close, fix or resolve form before any#N.- Title: 246 characters.
- The outside-v1.2.0 ruling is named.
- The body ends with the lane line and the generated footer.
closingIssuesReferencesis[]while the base is notmain, which is expected for a stacked PR.
- Stacking is right:
- shape-doctor.py, validate-pins.py and validate-manifest.py: a root whose own
.gitthis user cannot read is named.git: cannot be read (<reason>), so the doctor never reads it as DRIFTED, and both pinned validators refuse it, exit 2 (#1146) #1147 is OPEN, its headfix/1146-root-git-unreadable-not-driftis atb75babee…889, and its base ismain; - the remote ref
fix/1146-…is atb75babee…889, and the remote reffix/1148-…is at5aa0f116…5; merge-base 5aa0f116 b75babeeisb75babee, with 4 commits on top.
- shape-doctor.py, validate-pins.py and validate-manifest.py: a root whose own
NIT findings
NIT-A: the row holds return against continue, but not against a fall-through
- Where:
tests/test_pinned_root_git_unreadable.py:449-451, againstbootstrap.py:510-511,validate-family.py:568-569andfamily/bootstrap.py:447-448. - Evidence:
returnmadepasssurvives the whole file in all three readers (76 passed, restored=True ×3).- With that mutant, the code parses
b"no gitdir\n"[8:], which isb'r', as a git directory. Where<root>/r/HEADis refused, it gives the wrong refusalREFUSED git-repo-unreadable: r: cannot be read (Permission denied). - The case is contrived, so this is only a NIT.
- With that mutant, the code parses
- Fix: add
root.resolve() / "r" / "HEAD": deniedto that row'sas_python_314dict.- Measured in a scratch copy of the row (
r1172b/clone-head/tests/test_r1172b_probe_strong_row.py), it gives3 passedat head. - It kills all three
passmutants (1 failed …) and still kills the threecontinuemutants.
- Measured in a scratch copy of the row (
NIT-B: two stale phrases in the body's Tests section
- The regression table's header labels
`3a389565` (head), but the head is5aa0f116. Fix: say "3a389565(the fix)", or add a5aa0f116column with 53 passed, which I measured. - The "Which head" bullet puts "The fix that followed touches only
tests/test_validate_family_unreadable.py" after the5aa0f116sentence, so it reads as if3a389565came after5aa0f116. Fix: move it before that sentence.
What I did not cover, and two slips
-
Not re-run: the
pin,docs,sdandbiggate calls, thenewcall as a whole, and r1172's other 43 mutants. The diff is a single test file that nothing imports, so those results stand as the body states them. -
Load rule slips:
- two of my pytest calls started while the container held 4 pytest processes: the first mutant run (about 07:57Z), and the strengthened-row run (about 08:07Z, after my wait loop timed out);
/workspacestood at 12G free during the last runs.
All of my runs have ended.
State
l1-r1172is clean at5aa0f116.l1-r1172-baseis untouched:b75babee, with r1172's two untracked files.- My scratch is
/workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1172b/:- logs:
logs/, mutant results:mut/res-*.jsonl; - clones
clone-headandclone-base, each with untracked probe files only and no tracked diff.
- logs:
Lane: openreposhape-1 (openRepoShape-1)
…w pins path-twice's wording of whose file it puts back, the index's or HEAD's as the index holds none, a row pins the value check's reading of HEAD's where the deletion of `.gitmodules` is staged, and `_listed_as_git_reads`'s docstring says it returns nothing where the work tree's file is on disk (#1181) (#1208) It follows #1124 (for #1077), which landed as `dea964a`; W1181r restacked it onto main with only its own change (#1208 issuecomment-6371837537). It is the follow-up of RV1124b's review of #1124 (see #1150 for the neighbouring follow-ups from the first review). ## What changed - `tests/test_validate_family_index_gitmodules.py` - **PT7 (NIT 1).** In `test_1077_path_twice_records_its_put_back_for_the_next_finding`, after the existing `` `{put_back}` `` assertion, a second assertion pins the whole put-back wording: `back first from {GONE[how][0]}, `{put_back}`, so that every other section stays`. It rides on the two existing rows, `[deleted]` (the index's) and `[deletion-staged]` (HEAD's, as the index holds none). The row's docstring says path-twice names whose file it puts back. - **S1c (NIT 2).** A new row, `test_1077_a_value_head_gives_where_the_deletion_is_staged_puts_head_s_back`. `update = !true` is committed in `members/IRRS`'s section and the deletion of `.gitmodules` is staged, so the index holds no file and git reads HEAD's. The row checks the premise (`git submodule status` exits 128), then that `found_alike(holder, *MODES)` returns the one finding as the issue gives it, whole. It types the two lines in order, runs `git add -- .gitmodules` and a plain `git commit`, and checks that `!true` is not in HEAD, `head_paths` is `{ROW, OTHER_ROW}`, `git status --porcelain` is empty, and `ends_well` passes (the gate in every mode, and a plain clone). - `templates/family-root/scripts/validate-family.py` (NIT 3): the docstring of `_listed_as_git_reads` only. It opens "Where the work tree's .gitmodules is gone from disk, ..." and says it returns `(b'', '')` where the work tree's file is on disk. The script has no pinned copy and no checked-in digest. Its AST is equal to `8dfa32bd7`'s once that one docstring is dropped (sha256 `63f4b9d5...` there, `30abb4ea...` here). ## Counts - The rows file has 16 rows: 15 old and 1 new. PT7's assertion adds no row. - On this head: 16 passed. - Gate, 14 files, one pytest run on this head: **777 passed** (exit 0). The files are the rows file, `tests/test_repo_hygiene.py`, the eight other test files #1124 changed (`test_family_add_refusals`, `test_family_gitmodules_values`, `test_family_hand_mount_git_rm_lines`, `test_family_holder_bootstrap_texts`, `test_family_holder_remedies_compose`, `test_family_unnamed_gitlink`, `test_validate_family_remedies_typed`, `test_validate_family_unnamed_mounts`), the three more that name the functions and codes it touched (`test_family_add_spelling_leftovers`, `test_family_bootstrap_gate_order`, `test_validate_family_typed_states`), and `test_pinned_copy_text.py`. - The same 14 files on a scratch merge commit of `origin/main` (`961640c68`) and this head: **778 passed** (exit 0). The one extra id is a new row in `test_repo_hygiene.py` on `main`. ## Mutants Each mutant was applied to `validate-family.py` in a scratch worktree of this head, run against the new rows file (16 rows) and a copy of #1124's original 15-row file (31 rows together), and restored with its sha256 checked (`30abb4ea...` before and after). | Mutant | Edit | New file (16 rows) | #1124's 15 old rows | |---|---|---|---| | PT7 | path-twice's `f"from {whose}, ` written `f"from the index's, ` | 1 failed: `[deletion-staged]`; `[deleted]` passes | 15 passed (survives) | | S1c | `_listed_as_git_reads` reads `spec = f":{GITMODULES}"` alone, with no fallback to HEAD's | 1 failed: the new S1c row | 15 passed (survives) | ## Merge-trees `git merge-tree --write-tree` of each ref against this head was clean: - `origin/main` (`961640c68`) - #1124's head (`8dfa32bd7`) - #1166's head (`5102b4294`) and #1172's head (`5aa0f116d`), the two other open PRs that touch `validate-family.py`; and the head of #1172's base branch (`b75babee3`) No other open PR touches `tests/test_validate_family_index_gitmodules.py`. Closes #1181 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ory is inside the root's own the root's line ahead of `mount_read`'s refusal of that directory, so a root's `.git` this user cannot read is said once again, and a leg's own `.git` directory keeps its line (#1146) Merged with main at 50869f3, #1200's `mount_read` asks each leg's git directory, and `audit_leg` returned on its refusal, `code: .git/modules/code: cannot be read (<reason>)`, before #1147's `root_line` was reached. Where the root's own `.git` cannot be read, the legs' git directories under it cannot either, so 5 of the 47 rows of tests/test_root_git_unreadable.py failed on the `placement` reason: the row named a symptom once per leg instead of the root's line once (lane 3's note on #1147). `audit_leg` now gives the root's line first, where `root_line` is set, `mount_read` refused the leg, and `read_through_the_root` says the leg's git directory is inside the root's own: under the root's `.git`, or under the directory a root's gitfile names, which is where git keeps a submodule's for a root that is a linked worktree or a submodule itself (measured: `../../main/.git/worktrees/wt/modules/spec` and `../../.git/modules/root/modules/spec`). Every other refusal keeps `mount_read`'s line: a mount, a `.git` or a gitfile this user cannot read, and a leg's own `.git` directory, whose repository is not the root's. With the root's `.git` readable, `root_line` is None and #1200's refusal stands. validate-pins.py already refuses the root before any leg, and validate-manifest.py asks no leg's git directory, so neither changes. tests/test_root_git_unreadable.py: 3 rows, in process, on every platform and as root, with every `HEAD`'s `stat` refused: the root's line once for legs under the root's `.git` and under the directory its gitfile names, and the control, a leg's own `.git` directory keeping `spec/.git: cannot be read`. Each fails on the merge. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…py: an `id:`, a date or a `repository:` ending in a line feed is refused by name, and a pin's `source_repository:` that is no path or `owner/repo` name is named as the pin's (#1139) (#1166) The value patterns the issue names refuse a value that ends in a line feed, by name, as #1126 (#1017) makes `COMMIT_RE`, `SHA256_RE` and `COMMIT_40` do: - `DATE_RE` and `QUALIFIED_RE` in `validate-manifest.py` and `validate-family.py`, `MEMBER_RE` in `scripts/family.py`, `DATE_RE` in `scripts/shape_advisory.py` and `REPOSITORY_RE` in `update-shape.py`; - `PROJECT_ID_RE`, read with `fullmatch` where the two validators and `family.py add` read an id from a file. A shape pin's `source_repository:` that is neither a path on this machine nor an `owner/repo` name is refused in the pin's name, not `--upstream`'s. Values without a line feed keep their outcome, and no refusal code is added or renamed. Two inputs do move from one existing code to another (see "Behaviour changed", below). **What this PR does not cover, and the title does not claim:** - the naming-policy patterns that `scripts/repo_shape.py` reads from `contracts/repository-naming.yaml`, which `validate-repository-naming.py` still reads with `.match`; - `update-upstream-not-a-repo`, which still says `--upstream` when a pin names a folder that exists and is not a repository. Both are follow-ups, with the other findings of the independent review, under "Follow-ups (not in this PR)". This does not need #1126 and does not touch its files or its test file. Measured on main `038dafba` (Python 3.12.3, git 2.43.0) by a probe of each tool, before any change. Each value is written in the file as a YAML double-quoted `"...\n"`, which the reader takes as the value and a line feed. | Read from | Value | On main | |---|---|---| | `project.yaml` (`validate-manifest.py`) | `id: "atlas\n"` | only `FINDING manifest-topic: topic is 'xf-project-atlas', expected 'xf-project-atlas\n' derived from id 'atlas\n'`, exit 1 | | `project.yaml` | `elected_on: "2026-09-02\n"` | `manifest ok`, exit 0 | | `project.yaml` | a leg's `repository: "testorg/Atlas-spec\n"` | `manifest ok`, exit 0 | | `family.yaml` (`validate-family.py`) | `id: "inkrouter\n"` | `family ok: InkRouter (inkrouter` and a line break, exit 0 | | `family.yaml` | `created_on: "2026-09-04\n"` | `family ok`, exit 0 | | `family.yaml` | a member's `repository: "InkRouter/IRRS\n"` | `FINDING member-repository-name: ... whose name is 'IRRS\n'`, exit 1 | | a member's `project.yaml`, read by `family.py add` | `id: "irqs\n"` | mounted and committed, exit 0, with the row `id: "irqs\n"`; the holder's gate then said `family ok` | | `contracts/shape-pin.yaml`, `update-shape.py check` with no `--upstream` | `source_repository: "opensoft/openRepoShape\n"` | `REFUSED unsafe-value: --upstream is 'opensoft/openRepoShape\n'`, exit 2 | | the same | `source_repository: "opensoft/open Repo"` | `REFUSED update-upstream-unresolvable: --upstream 'opensoft/open Repo' is neither ...`, exit 2 | | the same | `source_repository: "-opensoft/openRepoShape"` | `REFUSED unsafe-value: --upstream is '-opensoft/openRepoShape'`, exit 2 | | an adoption plan (`adopt-project.py check`) | `id: "northwind\n"` | `FINDING plan-manifest-invalid: id is 'northwind\n', which the project's own validate-manifest.py refuses: manifest-topic: ...`, exit 1; `execute` refused it, exit 2, before any leg | In-process on the same main: `repo_shape.PROJECT_ID_RE.match('atlas\n')`, `shape_advisory.DATE_RE.match('2026-09-02\n')` and `REPOSITORY_RE.match('a/b\n')` each matched, and `date.fromisoformat('2026-09-02\n')` raised `ValueError`. - **`DATE_RE` and `QUALIFIED_RE`** in `templates/assembly-root/scripts/validate-manifest.py` and `templates/family-root/scripts/validate-family.py` end in `\Z`. So do `MEMBER_RE` in `scripts/family.py` (the third spelling `tests/test_family_member_name.py` holds equal to the two `QUALIFIED_RE`), `DATE_RE` in `scripts/shape_advisory.py` and `REPOSITORY_RE` in `update-shape.py`. - **`PROJECT_ID_RE` keeps its `$`.** Every id refusal prints its text (`must match ^[a-z0-9][a-z0-9-]*$`, pinned in six test files), the validators run against each project's own copy of `repo_shape.py`, and its definition sits inside #1126's hunk. The three callers that read an id from a file ask it with `fullmatch` instead: `validate-manifest.py` (`manifest-id`), `validate-family.py` (`family-id`) and `family.py`'s reading of a member's `project.yaml` (`member-bad-id`, which `add` now says before anything is mounted). - **`update-shape.py`.** `open_upstream(spec, pin_path=None)` takes the pin the value was read from, which `prepare` passes when no `--upstream` was given. Then `update-upstream-unresolvable` says ``<root>/contracts/shape-pin.yaml: `source_repository:` is '...', which is neither a path on this machine nor an `owner/repo` name``, with a remedy to write the pin's field or pass `--upstream`, and `checked_value` names the same field. **Code kept:** `update-upstream-unresolvable`, the existing code for a value that is not an `owner/repo` name. A line feed now reaches it, since the pattern no longer takes one. Before, `checked_value` refused it as `unsafe-value`. - **One existing test line:** `tests/test_family_init_values.py` finds validate-family's `DATE_RE` line by its text, which now ends `\Z`. - **One existing stand-in:** the `temporaries` fixture in `tests/test_update_shape_unreadable.py` stands in for `open_upstream` with `def opening(spec)`, which the new two-argument call refused with `TypeError: ... takes 1 positional argument but 2 were given`. It now takes `pin_path=None` and hands it on to the real `open_upstream`, so `test_a_read_refused_in_prepare_closes_the_upstream[check|apply]` asks what it asked before. - **The rows** are in a new file of their own, `tests/test_value_pattern_line_break.py`. - **A second guard, in a second commit (`5102b4294`, a test only, no production file).** The first guard holds the four pattern names this PR ends in `\Z`, and nothing held `PROJECT_ID_RE`, which keeps its `$`. `test_every_reader_of_the_id_pattern_is_in_the_table` reads every tracked `.py` outside `tests/` with `ast` (it runs none) and holds each call of a method of `PROJECT_ID_RE` (`PROJECT_ID_RE.x(...)` or `repo_shape.PROJECT_ID_RE.x(...)`) to the table `ID_READERS`: - the three `fullmatch` readers of an id from a file (`validate-manifest.py`, `validate-family.py`, `scripts/family.py`); - the modules that still `match` it, each with its reason: argv `--id` in `scaffold-project.py` and `scripts/family.py`, where `checked_value` refuses a line feed after the pattern; argv `--id` in `adopt-project.py`, which the project's own `validate-manifest.py` refuses (`adopt-manifest-invalid`, below; `ID_READERS` still gives it the `checked_value` reason); and `adopt-project.py`'s two `plan-bad-id` readers, of the plan's `id:` and of the id derived from `legs.assembly`, which a line feed never passes (below). A new reader of `PROJECT_ID_RE`, `fullmatch` included, fails it by file and method until it is listed in `ID_READERS` with its reason. The table is exact both ways, so a reader taken away fails it too. **Left as they are, and why:** - the argv readers of `PROJECT_ID_RE` (`--id` in `scaffold-project.py`, `family.py init` and `adopt-project.py`). The first two hold the id to `checked_value` after the pattern, which refuses a line feed as `unsafe-value` naming `--id`, and `tests/test_derived_id_refusals.py` pins that answer for them. `adopt-project.py` never calls `checked_value` on `--id`: `plan` with `--id` set to `northwind` and a line feed exits 2 with `adopt-manifest-invalid`, in the words of the project's own `validate-manifest.py` (`manifest-id`), and writes no plan (lane 5's re-check, issuecomment-6371408915, NIT 2; measured on `plan` only); - `adopt-project.py`'s reading of a plan's `id:`: `check` and `execute` hold it to the project's own `validate-manifest.py`, which refused it on main (above) and now names it in `manifest-id`'s words. It is the same value, refused under the same code, `plan-manifest-invalid`, before any leg exists. The id derived from `legs.assembly` is read with `.match` too, and a line feed in `legs.assembly` is `unsafe-value` before that id is asked (the review measured both, on main and here alike). This keeps the open PRs on `adopt-project.py` (24 at 12:02Z) clear; - `PIN_ARG_RE`, and `NAME_RE`/`REF_RE`/`PATH_RE` in `setup-project.py`, as the issue says; - `update-upstream-not-a-repo` still says `--upstream`. A pin's value reaches it only when it names a path that exists, not through the pattern. **Behaviour changed, stated plainly, and what the issue's Verify means by "No refusal code changes":** the issue's Verify line reads as no code added or renamed, and none is. Two inputs do move from one existing code to another, exit 2 both before and after: - `update-shape.py --upstream "opensoft/openRepoShape<LF>"` was `unsafe-value` and is now `update-upstream-unresolvable`, still naming `--upstream`; - a pin's `source_repository:` that ends in a line feed, with no `--upstream`, was `unsafe-value` naming `--upstream` and is now `update-upstream-unresolvable` naming the pin's field. No test pinned either old answer: every gate below passed. `family.py add --member` is stripped before `MEMBER_RE`, so `add` answers as before. **Not changed, seen while measuring:** with `--upstream` passed, `update-shape.py` does not read the pin's `source_repository:` through `REPOSITORY_RE` at all, and it keeps the value as the upstream's repository name. `validate-family.py` does not hold a member row's `id:` to `PROJECT_ID_RE`; it compares the row with the member's manifest. Neither is a `$` question. Both are under "Follow-ups (not in this PR)". All in `tests/test_value_pattern_line_break.py`, run against main's production code: the five production files swapped in place from `origin/main` (`961640c68`), then put back with their sha256 matching (`dirty=5` in the run's START line is those five files). On the head `5102b4294` (30 rows) the result is **20 failed, 10 passed in 23.12s, git 2.43.0**. The 10 that pass are baselines: the seven `test_a_pattern_takes_its_value` rows, the spelling-table guard and the two double-quoted-without-a-line-feed rows. The first 19 are the first commit's rows (19 failed, 10 passed in 22.12s on its 29 rows at `038dafba`). The independent review reproduced them from main's bytes of the same five files (19 failed, 10 passed in 26.78s, the same 19 ids). The 20th, `test_every_reader_of_the_id_pattern_is_in_the_table`, is the second commit's: it fails on main because the three readers of an id from a file are `.match` there, not `.fullmatch`. - `test_a_pattern_refuses_its_value_and_a_line_feed[manifest-DATE_RE]` - `test_a_pattern_refuses_its_value_and_a_line_feed[manifest-QUALIFIED_RE]` - `test_a_pattern_refuses_its_value_and_a_line_feed[family-DATE_RE]` - `test_a_pattern_refuses_its_value_and_a_line_feed[family-QUALIFIED_RE]` - `test_a_pattern_refuses_its_value_and_a_line_feed[family.py-MEMBER_RE]` - `test_a_pattern_refuses_its_value_and_a_line_feed[shape_advisory-DATE_RE]` - `test_a_pattern_refuses_its_value_and_a_line_feed[update-shape-REPOSITORY_RE]` - `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[id]` - `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[elected_on]` - `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[leg-repository]` - `test_adopt_check_names_a_plan_id_that_ends_in_a_line_break_as_an_id` - `test_a_family_value_that_ends_in_a_line_break_is_a_finding[id]` - `test_a_family_value_that_ends_in_a_line_break_is_a_finding[created_on]` - `test_a_family_value_that_ends_in_a_line_break_is_a_finding[member-repository]` - `test_add_refuses_a_member_whose_id_ends_in_a_line_break` - `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]` - `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[space]` - `test_a_pins_source_checked_value_refuses_is_named_as_the_pins` - `test_an_upstream_passed_with_a_line_feed_is_named_as_passed` - `test_every_reader_of_the_id_pattern_is_in_the_table` **13 of 13 killed.** Each mutant was applied alone to one file of the branch's production bytes (the same bytes as the first commit and as the head, which adds a test only), the rows were run, and the file was put back with its sha256 checked (`restored=yes` for all 13). Each run is `tests/test_value_pattern_line_break.py` plus `tests/test_family_member_name.py::test_every_spelling_of_the_member_name_rule_is_the_same_pattern`, git 2.43.0. | Mutant | File | Killed by | |---|---|---| | M1 `DATE_RE` back to `$` | `validate-manifest.py` | 2: `test_a_pattern_refuses_its_value_and_a_line_feed[manifest-DATE_RE]`, `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[elected_on]` | | M2 `QUALIFIED_RE` back to `$` | `validate-manifest.py` | 3: `...[manifest-QUALIFIED_RE]`, `...[leg-repository]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` | | M3 the id read with `.match` | `validate-manifest.py` | 2: `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[id]`, `test_adopt_check_names_a_plan_id_that_ends_in_a_line_break_as_an_id` | | M4 `DATE_RE` back to `$` | `validate-family.py` | 2: `...[family-DATE_RE]`, `test_a_family_value_that_ends_in_a_line_break_is_a_finding[created_on]` | | M5 `QUALIFIED_RE` back to `$` | `validate-family.py` | 3: `...[family-QUALIFIED_RE]`, `...[member-repository]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` | | M6 the id read with `.match` | `validate-family.py` | 1: `test_a_family_value_that_ends_in_a_line_break_is_a_finding[id]` | | M7 `MEMBER_RE` back to `$` | `scripts/family.py` | 2: `...[family.py-MEMBER_RE]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` | | M8 a member's id read with `.match` | `scripts/family.py` | 1: `test_add_refuses_a_member_whose_id_ends_in_a_line_break` | | M9 `DATE_RE` back to `$` | `scripts/shape_advisory.py` | 1: `...[shape_advisory-DATE_RE]` (its pattern row alone: `date.fromisoformat` refuses the line feed after the pattern, so no tool row sees it) | | M10 `REPOSITORY_RE` back to `$` | `update-shape.py` | 3: `...[update-shape-REPOSITORY_RE]`, `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]`, `test_an_upstream_passed_with_a_line_feed_is_named_as_passed` | | M11 `prepare` passes no pin | `update-shape.py` | 3: `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]` and `[space]`, `test_a_pins_source_checked_value_refuses_is_named_as_the_pins` | | M12 `checked_value` names `--upstream` again | `update-shape.py` | 1: `test_a_pins_source_checked_value_refuses_is_named_as_the_pins` | | M13 the pin's refusal sentence removed | `update-shape.py` | 2: `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]` and `[space]` | No mutant is added for the stand-in in `tests/test_update_shape_unreadable.py`: it is a test change. The independent review re-ran M3, M8, M11 and M12, and one mutant of its own (X1: `prepare` names the pin even when `--upstream` is passed). All five were killed, and each file was put back with its sha256 checked. The other nine of the 13 are this writer's runs alone. **The second commit's guard: 6 of 6 probes caught.** Each was applied alone to one tracked file at `5102b4294`, `test_every_reader_of_the_id_pattern_is_in_the_table` was run, and the file was put back with its sha256 checked (`restored=yes` for all six). The first commit's guard, `test_every_spelling_of_a_value_pattern_is_in_the_table`, was run with each plant too: it passes every one of them, which is the gap the second guard closes. | Probe | File | The new guard | The first guard | |---|---|---|---| | P1 a new `PROJECT_ID_RE.match(value)` reader appended | `update-shape.py` | failed: `('update-shape.py', 'match'): 1` is more than the table holds | passed | | P2 a new `repo_shape.PROJECT_ID_RE.search(value)` reader appended | `scripts/shape_advisory.py` | failed | passed | | P3 `fullmatch` back to `match` | `templates/assembly-root/scripts/validate-manifest.py` | failed | passed | | P4 `fullmatch` back to `match` | `templates/family-root/scripts/validate-family.py` | failed | passed | | P5 `fullmatch` back to `match` | `scripts/family.py` | failed | passed | | P6 main's bytes of those three files together (the 20th row above) | the three | failed | not run | **The second commit `5102b4294` (dirty=0)**, Python 3.12.3, with the same stub `gh`, bogus token and no global git config: | Run | Git | Result | |---|---|---| | `tests/test_value_pattern_line_break.py` | 2.43.0 | **30 passed** in 23.00s | | `tests/test_value_pattern_line_break.py` | 2.47.1 | **30 passed** in 23.30s | | `tests/test_pinned_copy_text.py`, `tests/test_path_guard_hygiene.py`, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py` | 2.43.0 | **306 passed** in 61.54s | That commit changes only the test file (62 lines added, no assert that joins conditions with `and` or `or`, ASCII), so the first commit's runs below are not repeated: they hold the production files and every other test file, and those are unchanged. CI on `5102b4294`, and an independent look at the second commit, were not done when this was written. **The independent review of the first commit** (rv1166c, READY, read-only) reports these gates on `2593952c9`, all passing: the stand-in gate's 6 files (298 passed), the new file on 2.47.1 (29 passed), the five hygiene files (306 passed), 15 more test files that name a changed function, pattern or code (1611 passed, 19 skipped) and `tests/test_shape_doctor.py` (445 passed, 3 skipped). It also read CI on that head at about 20:04Z: `tests` (the full suite on 3.14), `tests-git-bash`, `package` and SonarCloud all passed. **The first commit's gates ran on `2593952c` (dirty=0)**, Python 3.12.3, with the real `gh` unreachable (a stub first on `PATH`, a bogus token) and no global git config: | Run | Git | Result | |---|---|---| | the files that held every failure and setup error of the shards below: `tests/test_family.py`, `tests/test_update_shape_pr_base.py`, `tests/test_update_shape_unreadable.py`, `tests/test_update_shape_header_comments.py`, `tests/test_update_shape_messages.py` and the new `tests/test_value_pattern_line_break.py` | 2.43.0 | **298 passed** in 343.60s | | `tests/test_value_pattern_line_break.py` | 2.47.1 | **29 passed** in 21.31s | | `tests/test_pinned_copy_text.py`, `tests/test_path_guard_hygiene.py`, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py` | 2.43.0 | **306 passed** in 69.12s | **The two shards, on the uncommitted tree** holding the same production bytes as the first commit (the 7 files' sha256 checked before and after), git 2.43.0: the validators' test files, the `REPOSITORY_RE`/`source_repository` files and the `update-shape.py`, `shape_advisory.py` and `family.py` files, 125 in all. - shard 1 (62 files): 4 failed, 5290 passed, 13 skipped in 56 min; - shard 2 (63 files): 3 failed, 4 setup errors, 3761 passed, 29 skipped in 50 min. Every failure and setup error is in the five existing files of the first row of the table above, which all pass on the first commit: - 2 were real: `test_a_read_refused_in_prepare_closes_the_upstream[check|apply]`, the `opening(spec)` stand-in under the two-argument call. The stand-in is fixed in the first commit. - The other 5 failures and the 4 setup errors were `update-conflict` on `scripts/validate-family.py` ("not a verbatim copy ... at the pinned commit"). This is an artifact of an uncommitted tree: `tests/test_family.py`'s `upstream_clone` commits the working tree into its clone, while `family.py init` pins the holder at the real `HEAD`, so an uncommitted `templates/family-root/` file is never verbatim at its pin. On the first commit they pass. Every other file of the two shards passed on these production bytes, so the shards were not re-run. `git merge-tree --write-tree <other> 5102b42` (git 2.43.0) at 2026-10-23T12:08Z, after re-fetching main (still `961640c6`; read again at 12:10Z, just before the push) and re-listing the open PRs at 12:02Z (55 open). Main has moved from the `038dafba` that this body measured to `961640c6` by one file, `docs/release-notes/v1.2.0.md`. Each open PR that touches one of this PR's 8 paths, at its current head, plus #1126: | Against | Head | Shares | Result | |---|---|---|---| | origin/main | `961640c6` | | clean, tree `4613729516ff` | | #1098 | `dad7961a` | `scripts/family.py` | clean, tree `4d1ff9517588` | | #1117 | `10dc011c` | `scripts/family.py` | `scripts/family.py` merges clean; conflict in `docs/handbook.html` only, which this PR does not touch (see below) | | #1124 | `8dfa32bd` | `validate-family.py` | clean, tree `cb2ec1073646` | | #1126 | `4c1f80cf` | none (its `scripts/repo_shape.py` and tests) | clean, tree `e82c8945ac37` | | #1133 | `a7bfee1b` | `update-shape.py` | clean, tree `a710870d1ffc` | | #1144 | `89486f69` | `scripts/shape_advisory.py` | clean, tree `2d3e3e1ef43d` | | #1147 | `b75babee` | `validate-manifest.py` | clean, tree `a82ac3b47a3f` | | #1159 | `66e68f94` | `scripts/shape_advisory.py` | clean, tree `4abeb7b38665` | | #1164 | `74096eb4` | `scripts/family.py` | clean, tree `ee285e1cb8cf` | | #1172 | `5aa0f116` | `validate-family.py` | clean, tree `aa32d41e6e2c` | | #1187 | `cc8a87ee` | `update-shape.py`, `tests/test_update_shape_unreadable.py` | clean, tree `25711eb1cac4` | | #1201 | `eac177af` | `scripts/family.py` | `scripts/family.py` merges clean; conflict in `docs/handbook.html` only (see below) | - #1117 and #1201 conflict in `docs/handbook.html` alone. #1201 is stacked on #1117 (their merge-base is #1117's head `10dc011c`). Each of them against origin/main alone gives the same conflict in the same file, so it is theirs: #1117's, from its older base `6da757cb`, which #1201 inherits. - #1187 and #1201 are new since the first merge-trees. #1187 shares `tests/test_update_shape_unreadable.py` with this PR; its diff adds or removes no line that names `open_upstream`, `def opening` or `REPOSITORY_RE`, so it brings no one-argument stand-in for the two-argument call. That was read for all 55 open heads at 12:12Z; only this PR's own lines name them. - No other open PR adds or removes a call of a method of `PROJECT_ID_RE`, which the second guard counts: the lines each of the 55 open heads adds or removes outside `tests/`, `docs/` and `*.md` were read at 12:04Z. Only this PR's own name it, and #1201's one new `MEMBER_RE.fullmatch` is another pattern. So a sibling landing first does not move the guard's table. Each is a candidate for its own issue. They come from the independent review (rv1166c, READY at `2593952c9`, issuecomment-6352173084), which measured each of them on main and at the head unless the item says it did not. The review's finding numbers are in brackets. - **The naming-policy patterns [1].** `scripts/repo_shape.py` reads the families, the roles and the topic pattern of `contracts/repository-naming.yaml` with `.match` (`family["_re"].match(name)`, `family["_split_re"].match(name)`, `role["_re"].match(name)`, and `topic_pattern`, which defaults to `^xf-project-[a-z0-9-]+$`). `validate-repository-naming.py --project project.yaml` runs in the root template's `Makefile` and `validate.yml`. At the head, and on main, it exits 0 on a leg `repository: "testorg/Atlas\n"` and prints the line feed inside its table; `-spec` and `-code` the same. The project's gate as a whole still fails, because `validate-manifest.py` now refuses that manifest. - **`update-upstream-not-a-repo` with a pin [3].** With no `--upstream`, a pin whose `source_repository:` names a folder that exists and is no repository (the review measured `contracts` and `scripts/repo_shape.py`) is `REFUSED update-upstream-not-a-repo: --upstream contracts exists but is not a git repository`, exit 2, on main and here. `pin_path` is in scope at `update-shape.py:686-687`, so that refusal can name the pin as `update-upstream-unresolvable` now does. - **A pin's line feed when `--upstream` is passed [9].** `update-shape.py check --upstream <clone>` over a pin whose `source_repository:` ends in a line feed exits 0, `nothing to do`. The value is kept as `upstream.repository`, which reaches the README shape line's check, `apply`'s commit subject and the pull request words (`update-shape.py:2525`, `3017` and `5121`). The review measured `check`, not `apply`. It belongs with the previous item. - **A member row's `id:` [4].** On main, `add` of a member whose `project.yaml` reads `id: "irqs\n"` commits the row `id: "irqs\n"`, and the holder's `validate-family.py` then says `family ok`. Under mutant M8 (this branch with main's `.match` in `add`) the same row is written, and the gate still says `family ok`. By code reading, not measured end to end: `validate-family.py` compares a row's id with the member manifest's by `str` equality (`member-id-mismatch`, `validate-family.py:1608`), and `_check_member_ids_unique` does the same, so neither asks `PROJECT_ID_RE` of a row. This PR makes `add` refuse such a member, so no new holder gets such a row. - **Values that no pattern reads [8].** The review's sweep found these `manifest ok` or `family ok`, exit 0, with a trailing line feed, at the head and on main alike: - in `project.yaml`: `name`, a spec or code leg's `path`, `shape.repository` and `shape.digests.digest_algorithm`; - in `family.yaml`: `org`, the family's own `repository`, `tracking_branch`, and the shape's and each member pin's `digest_algorithm`. No `^...$` pattern reads them, so they are not this PR's. A leg's `path` is checked only by `_leg_path_findings`, which asks no pattern, and the family's `tracking_branch` is refused in `project.yaml` (`manifest-tracking-branch`, U+000A) but not in `family.yaml`. `shape.commit` and `tree_sha256` with a line feed are also `manifest ok`; that is #1126's. The first commit's message says "the three readers of an id from a file ask it with `fullmatch` (`manifest-id`, `family-id`, `member-bad-id`)". That is true of those three, but it reads as if there were no others [2]. What the review measured instead: - `adopt-project.py` still reads the id derived from `legs.assembly` (`:6780`) and the plan's `id:` (`:6791`), both `plan-bad-id`, with `.match`; - neither lets a line feed through. A plan `id: "northwind\n"` is `plan-manifest-invalid`, now in `manifest-id`'s words. A plan with no `id:` and `legs.assembly: "NorthWind\n"` is `REFUSED unsafe-value: legs.assembly is 'NorthWind\n'`, exit 2, before the derived id is asked, on main and here alike. So it is the wording alone that overstates. The second commit's `ID_READERS` lists both readers with that reason. The title's pin clause is the narrower one (the pin's name only where the value is no path and no `owner/repo` name), and two inputs change from `unsafe-value` to `update-upstream-unresolvable` (see "Behaviour changed", above). Review: rv1166 and rv1166c, lane 5's independent opus reviewers, at `2593952c` (issuecomment-6352173084); then rc1166b, lane 5's independent sonnet re-checker, which wrote none of #1166: READY at `5102b429`, H0 of this landing (issuecomment-6371408915), with no MUST or SHOULD; its NITs 1 and 2 are corrected in the body. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 5102b42 passed tests, tests-git-bash and SonarCloud; the head 5c8afef is a clean update-branch merge of main 93d2931, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 3c89065 over main 11560eb: the family_init_values, update_shape_unreadable, value_pattern_line_break, repo_hygiene, agents_md_codes, cli_reference sets, 365 passed, 19 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 #1139 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o a leg whose mount `mount_read` got into, so a mount this user can search and not list keeps `spec: cannot be listed` as the text says, and a row pins it (#1146) Fix round for rv1147c's S1 on #1147 (lane 5's re-review at a2fc1da). With the spec mount at mode 0100 or 0300, `mount_read` refuses the listing, and `read_through_the_root` can still read `spec/.git` because the mount can be searched, so `audit_leg` returned True and gave the leg the root's line (`.git: cannot be read`) where the text says a mount keeps `mount_read`'s (`spec: cannot be listed`). Only the text of the `placement` row changed (the verdict is INVALID (pins) either way), and the leg's own line came on the run after the root was mended. `audit_leg` now asks `populated` before it asks `read_through_the_root`. `populated` is True only where `mount_read` got as far as the mount's `.git`, so a mount it could not stat or list is never given the root's line, and every other refusal keeps `mount_read`'s as before. The comment beside it now says so: "what mends the root's mends it" holds only for a leg whose mount was read. The row `test_a_mount_that_cannot_be_listed_keeps_its_own_line` (in process, on every platform and as root: every `HEAD` refused and the spec mount's listing refused) fails at a2fc1da and passes with the word. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
#1147's final head 95ea564 is update-branch's merge of main 801a7d6 into 4d299dd, its S1 fix (`placement` gives the root's line only to a leg whose mount `mount_read` got into). It brings that fix and main. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 6 days and 18 hours by commenting |
|



What
The four other pinned callers of
find_repo_root, which #1146 left as they were (its Known limits): the assembly root'sscripts/bootstrap.py, and the family holder'svalidate-family.py,bootstrap.pyandsiblings.py. Also the doctor'smembersrow over a holder whose own.gitcannot be read.Was stacked on #1147 (base
fix/1146-root-git-unreadable-not-drift, whose head wasb75babeewhen this was measured and reviewed). #1147 has landed, as its squash51a9c5f1on main, and this branch was restacked onto main by real merges, with its own change unchanged (see "Restacked after #1147 landed"). This PR is outside v1.2.0 by Brett Heap's ruling of 2026-10-21T10:32:04Z ("leave the follow-ups out of v1.2.0"). It lands after the tag, on a later word.Measured in a scaffold's recursive clone and a family holder, with base
b75babee(#1147's head then) and this head, on Python 3.12.3. It was also measured under a 3.14-likesitecustomize, in whichPath.is_file,is_dir,existsandis_symlinkanswer False to anyOSError. Each tool ran from the root, asmakeruns it..gita link into a folder at 000.gita directory at 000bootstrap.py, basePermissionErrortraceback, exit 1. Shim:REFUSED not-a-git-repo: no .git found at or above <root>, exit 2no gitlink recorded; leaving as isof each leg. Thenpinsrefusesgit-repo-unreadableandbootstrap REFUSED, exit 2bootstrap okbootstrap.py, headREFUSED git-repo-unreadable: .git: cannot be read (Permission denied)and thechmodcure, exit 2, no traceback, nothing on stdout, both Pythonsbootstrap okvalidate-family.py(and--no-members), basenot-a-git-repo, exit 2REFUSED gitlinks-unreadableover git'snot a git repository, with the remedy to askgit status, exit 2family okvalidate-family.py(and--no-members), headREFUSED git-repo-unreadable: .git: cannot be read (Permission denied), exit 2, both Pythonsfamily okbootstrap.py, basenot-a-git-repo, exit 2, with or without--no-fetch --skip-membersFETCH INCOMPLETE (git exit 128)with the advice to setSHAPE_LEGS_TOKEN, each member fails its pins,family bootstrap FAILED, exit 1. With--no-fetch --skip-members:family bootstrap ok, exit 0, over a run that read nothingfamily bootstrap okbootstrap.py, headREFUSED git-repo-unreadable: .git: cannot be read (Permission denied), exit 2, both Pythons, with or without the flagsfamily bootstrap oksiblings.py(plain and--dry-run), basenot-a-git-repo, exit 2siblings ok, exit 0; the holder's branch shown?siblings oksiblings.py(plain and--dry-run), headREFUSED git-repo-unreadable: .git: cannot be read (Permission denied), exit 2, both Pythonssiblings okINVALID (family). Thefamilyrow sayscrashed ... PermissionError, andmemberssays.git: cannot be readthroughrun_checks' generic handler. Shim:DRIFTED (a member is not at its pin),memberssayschecked out at ?DRIFTED (a member is not at its pin)on both Pythons;familyrefusesgitlinks-unreadableCOMPLIANTINVALID (family), both Pythons.members: FINDING.git: cannot be read (Permission denied), so no gitlink is read and no member is compared with its pin, NEXT this doctor again, WHY thechmodcure.family:the project's own scripts/validate-family.py refuses (exit 2): REFUSED git-repo-unreadable: .git: cannot be read (Permission denied)COMPLIANTThe doctor's other family rows (
manifest-kinds,shape-currency,agent-files) were ok in every state, on both sides.placementis unchanged (placementisn/afor a holder).Lane 1's ruling. The family bootstrap's new refusal under
--no-fetch --skip-membersover a.gitdirectory at 000 is ACCEPTED. Base saidfamily bootstrap okthere, over a run that read nothing. Head refusesgit-repo-unreadable, exit 2, as the other tools that read the holder's repository through git do.The gate's regression, fixed in the test's fixture. Lane 1's gate found
tests/test_validate_family_unreadable.pyfailing 6 of 53 at05e6ab0f, where base passes all 53.REFUSED member-unreadable: members/IRRS/.git: cannot be read (Permission denied), and gotREFUSED git-repo-unreadable: .git: cannot be read (Permission denied).validate-family.py'smainin process afterrefused_stat(monkeypatch, ".git"). That call refuses thestatof every path named.git: the holder's own as well as the member's.find_repo_rootasked the holder's.git, and the file'sgate()stubs it to answer the holder._run_checksasks_refuse_an_unreadable_root(start)first. The stub did not cover it, so it met the holder's refused.gitand refused it by its own name,.git.gate()now gives the holder as the whole root:find_repo_rootanswers it, and_refuse_an_unreadable_rootasks nothing. No assertion changed.member-000,members-000,variant-000,staged-removal-000) run the whole gate in a subprocess, the new check included. They passed at05e6ab0ftoo.mainin process after refusing.gitby name. Their.gitrows call one function alone (checkout_tracking_branch,unfetchable,run_member_target,fetch_members,_print_pinned_row), and the gate passed them.Why
Each pinned copy carries its own
_refuse_an_unreadable_root(start), not a newerfind_repo_root.scripts/repo_shape.pyis a shape copy too, and a pinned copy may not rely on a newer one (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). That is why shape-doctor.py: a shape root whose own.gitis a link into a folder this user cannot search reads DRIFTED, its legs as having no gitlink, and both of its validators crash with PermissionError #1146 put the walk in each validator.find_repo_root(start), withstart = args.root or parents[1]..gitthatfind_repo_rootwould ask, in the same order, with the file's own_is_a..gitthat answers absent (nothing there, a link to nothing or to itself) is passed over, asexists()passes it.--root .from a folder inside the root walks up from there..gitthat is there, a directory or a gitfile, so a.gitabove the root is never asked.spelled, templates/assembly-root/scripts/bootstrap.py and templates/family-root/scripts/bootstrap.py:mainprints the manifest'snamewith a plainprintbefore any step runs, soname: "a\ud83db"(a lone surrogate) endsmake bootstrapin aUnicodeEncodeErrortraceback, exit 1 (make: 2), and the validator that names the problem is never reached; a valid name the stdout cannot encode ends the same way #391).One code,
git-repo-unreadable, exit 2. It is the codevalidate-pins.pyandvalidate-manifest.pyrefuse the same root by (shape-doctor.py: a shape root whose own.gitis a link into a folder this user cannot search reads DRIFTED, its legs as having no gitlink, and both of its validators crash with PermissionError #1146), and the sibling ofnot-a-git-repo. Each remedy is in the tool's own words:_unreadable(..., ROOT_READ);validate-family.py:_unreadablegainsreads=PATHS_READ;unreadable(..., ROOT_UNDONE);siblings.py:member_unreadablegainscode="member-unreadable"and is passedcode=ROOT_UNREADABLE. Its advice keeps its own "(and on every directory above it)", without "inside the root", since its clones sit outside the holder (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).The three that read the root's repository through git also ask what
validate-pins.pyasks. Those are both bootstraps andvalidate-family.py. Each asks of the first.gitthat is there:HEADit cannot reach.The gitfile's line has CR LF stripped, and the name is
normpath'd. On base, git's answers there wereno gitlink recorded,gitlinks-unreadableovernot a git repository, andFETCH INCOMPLETEwith the advice to set a credential: each a claim about something else.siblings.pyasks thestatalone, asvalidate-manifest.pydoes. It reads the holder's files, and.gitmodulesas a file, and runs git in the holder only for the branch its layout table shows. With a.gitdirectory at 000 it still places every member, measured on both sides.The doctor: one finding, in the
membersrow, and no member is drift.check_memberscallsroot_git_read(ctx.root)(shape-doctor.py: a shape root whose own.gitis a link into a folder this user cannot search reads DRIFTED, its legs as having no gitlink, and both of its validators crash with PermissionError #1146's) after the empty-members return, and returnsmembers_git_unread_row.gitlink_cannot_be_read, so a reader of--jsongets noKeyError.drifted_reasonscounts no entry that carries it, and still counts a member beside it that it could read and is off its pin.Not touched:
scripts/repo_shape.py(find_repo_rootstays on the hygiene test'sBENIGNlist asPINNED);KNOWN_OPEN(no entry for these sites); siblings'submodule_urls, which is 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 slice B's.Tests
tests/test_pinned_root_git_unreadable.py, new, has 76 rows.CANNOT_RUN_AS_ROOT), and restore the mode after:.gitlinked into a folder at 000, for each tool;.gitat 000 and at 444, for each git reader;INVALID (family),membersheld whole, neverchecked out at ?.refused_stat,refusing,as_python_314):.gitabove the--rootgiven, named whole; the same under a folder whose name holds an ESC, the whole path spelled; a relative--root .run fromcontracts/; a.gitabove the root never asked; a stop at a readable gitfile, and at a gitfile that names no directory (added after review r1:test_the_walk_stops_at_a_gitfile_that_names_no_directory, one id per git reader);.gitis or names: a git directory'sHEAD, a gitfile that cannot be opened, a git directory outside the root and inside it, a gitfile ending in CR LF, an unprintable folder (POSIX only);siblings.pyasks only thestat;membersrow in process, both causes, anddrifted_reasonswith a readable member beside an unread one.COMPLIANT, each tool goes on past the question. Five roots that answer absent or that git answers for: no tool refuses.siblings.pyover a.gitdirectory at 000 still places the members.Base vs head. Base is
b75babeewith the new file laid over it.b75babee5aa0f116The 2 rows that pass on base are controls:
test_siblings_still_places_the_members_of_a_holder_whose_git_is_at_000andtest_roots_whose_git_can_be_read_are_unchanged.The other rows fail on base for two reasons:
not-a-git-repo(link at 000, every tool, in process,--root contracts,--root ., and the ESC folder);no gitlink recorded,gitlinks-unreadable,gitmodules-unreadableandFETCH INCOMPLETE(the git readers at 000, 444 and through a gitfile);DRIFTED (a member is not at its pin)and amembersrow that is ok, not FINDING;drifted_reasonscounting the unread entries;_refuse_an_unreadable_root,AttributeError): the 33 before review r1, and the 3 ids oftest_the_walk_stops_at_a_gitfile_that_names_no_directory.Mutants: 58 of 58 killed (the 55 below, and the 3 review r1 added). Each was applied once to a scratch clone of the head (never the worktree), the new file was run there with
-xon 3.12, and the file was put back in afinally. The clones were checked clean after.continuemadereturnon absent; no.resolve(); the finalreturnremoved; CR not stripped; nonormpath; the git directory named as.git; theHEADstep removed; relative naming removed or named against the candidate; theValueErrorfallback removed;spelledremoved (F); the code changed; thereadsor what text dropped.siblings.py(S): the call removed; thereturnremoved; an absent.gitstopping the walk; no resolve; the relative naming and its fallback;spelledremoved; the code dropped or ignored; the what text dropped.root_git_readincheck_members; the drift filter removed; the null keys dropped;whyornextNone; status ok; the reason changed; the line not carried.A-,V-andF-gitfile-naming-nothing-continues: in each git reader, thereturnmadecontinuewhere.gitis a gitfile that does not start withgitdir:. The 73 rows had no row that held that line.test_the_walk_stops_at_a_gitfile_that_names_no_directorywas added for them: the root's.gitis a gitfile readingno gitdir, the parent's.gitis refused withas_python_314, and_refuse_an_unreadable_root(root)answers None. Run in a scratch clone of this tree, the file with-xon 3.12.3, each mutant is KILLED by its own tool's id:[assembly-bootstrap],[validate-family],[family-bootstrap](1 failed, 45 passed,1 failed, 47 passed,1 failed, 46 passed), restored in afinally. The review's other 43 mutants were not run by it and were not run again here.S-spelled-removedsurvived those (69 passed), because the only unprintable row was the git readers'. The rowtest_a_root_git_named_whole_is_spelledwas added for it, over all four tools, and its[siblings]id kills it.The regression in
tests/test_validate_family_unreadable.py, on 3.12.3:b75babee(base)05e6ab0f(before the fix)3a389565(the fix; 53 passed again at the head5aa0f116, measured by r1172b)test_a_path_whose_stat_is_refused_is_refused_by_nameat[member-checkout],[member-checkout-pins],[unregistered-member-checkout],[staged-removal-checkout]and[variant-checkout], andtest_a_stray_whose_rm_takes_no_f_asks_nothing_of_its_git.finally:test_a_path_whose_stat_is_refused_is_refused_by_name[member-checkout], and by the other five, as before the fix.validate-family.py's check walking frommembers/IRRSinstead of the start. Killed by the mode rowtest_a_folder_this_user_cannot_search_is_refused_by_name[member-000]. A root check that reached a member's.gitis still seen with the stub in place.3a389565: 73 passed on 3.12.3, and 73 passed under the shim. At5aa0f116, with review r1's 3 rows: 76 passed on 3.12.3, and 76 passed under the shim;tests/test_validate_family_unreadable.py, 53 passed.The gate, all five calls, on 3.12.3.
--collect-only; the count of ids equals pytest's own "collected" line). The ids were then run in shards of at most 300 ids, each under 9 minutes, as a person runs them: nogh, a stubghfirst on PATH,GIT_SSH_COMMAND=false.05e6ab0f. The fix that followed,3a389565, touches onlytests/test_validate_family_unreadable.py. After review r1 thenewcall ran again at5aa0f116(the head, which adds 3 rows to the new file and nothing else).big-22shard holds that file, and no other test imports it.newandbig-22ran again at3a389565, and the other shards stand.tests/test_family_hand_mount_git_rm_lines.py, which names the file in its docstring, also ran again at3a389565: 29 passed.test_path_guard_hygiene,test_root_git_unreadable,test_pinned_root_git_unreadable,test_doctor_unreadable_pin,test_doctor_unreadable_paths05e6ab0f, 255 again at3a389565, and 258 passed at5aa0f116(3 rows added)test_scaffold_leg_path_collisionsand the doc trio (test_agents_md_codes,test_cli_reference,test_repo_hygiene)test_pinned_copy_text,test_shape_pin_*,test_update_shape*(22 files)test_update_shape_add_symlink.py:832), 237 passed, 234 passed; 0 failedtest_shape_doctor.py, in 4 shardsvalidate-family.py,siblings.pyorbootstrap.py, and 6 that read themembersrow or its keysbig-22at3a389565(300 passed). At05e6ab0f,big-22failed 6 of 300: the regression above. The other 26 shards passedThe 39 skips in
bigare this bench's:The new file was also run in full under the 3.14-like shim: 73 passed, at
05e6ab0fand again at3a389565, and 76 passed at5aa0f116.tests/test_path_guard_hygiene.pywithtests/test_repo_hygiene.py: 149 passed at5aa0f116. Thepin,docs,sdandbigcalls were not run again after review r1, since the only file it changes is the new test file, which no other test imports.Pinned copy
The four files are in
scripts/shape_materialize.py'sCOPIED_VERBATIM(the assembly bootstrap) andFAMILY_COPIED_VERBATIM(the three holder files). No file is added to or taken from a list, and no digest of them is recorded in the tree, so there is no pin-record edit, as in #1147. Measured against a standard one commit past the pin of a project and a holder scaffolded from base:update-shape.py check, project:upstream-changed scripts/bootstrap.py,unchangedfor the other 10;update-shape.py check, holder:upstream-changedforscripts/validate-family.py,scripts/bootstrap.pyandscripts/siblings.py,unchangedfor the other 7;COMPLIANT, SHAPE BEHIND (1 upstream-changed, 0 upstream-added)for the project and(3 upstream-changed, 0 upstream-added)for the holder;INVALID (family). Themembersrow names.git: cannot be read (Permission denied)once, with the cure. Until the holder runsupdate-shape.py apply, thefamilyrow is the old copy's answer:crashed before reaching a verdict (exit 1): PermissionErrorfor the link,REFUSED gitlinks-unreadablefor the directory. The doctor takesPYTHONPATHout of the validator's child (shape-doctor.py: after #238 the doctor runs a root's OWN validator with the operator'sPYTHONPATHstill in the child's environment, so for an operator who exports one that reaches arepo_shape.pythemanifestrow of a root that lost its ownscripts/repo_shape.pysaysok ... passesagain, the false pass #238 removed, where the project's own gate in CI fails onModuleNotFoundError: No module named 'repo_shape'#306), so the shim does not reach it, and the row is the same on both.Merge with #1147's new head
#1147's head was then
a2fc1da8: main with #1200 merged in (569ea056), thenplacementgiving the root's line ahead of #1200's refusal of a leg's git directory. It is merged here as2eabec8e("Merge #1146's branch into #1148's branch"), parents5aa0f116anda2fc1da8, with no conflict.git merge-treegave treec473b237, and the merge commit has that tree. Four files changed on both sides since the merge baseb75babee(shape-doctor.py, bothbootstrap.pyfiles andvalidate-family.py), and git merged each with no conflict. Compared line by line, per file, the merge adds to this PR's 7 files exactly the linesa2fc1da8brings, and keeps exactly the lines this PR added: 902 added and 26 removed, as before.The
membersrow and #1200 agree. #1200 names a member whose own git directory cannot be read, throughmount_readinmember_state. This PR's check comes first incheck_members: with the holder's own.gitunreadable,root_git_readanswers and the row returns before any member is read. So the two never meet, and the order is the onea2fc1da8givesplacement: the root's line first. On the merge:.gitlinked into a locked folder, or a directory at 000, the row is a FINDING:.git: cannot be read (Permission denied), so no gitlink is read and no member is compared with its pin, said once. The member's entry has every key null, plusgitlink_cannot_be_read, and the verdict isINVALID (family). shape-doctor.py: a leg or member whose git directory this user cannot read is named in the gate's words,spec: .git/modules/spec: cannot be read (<reason>), so thelegsandmembersrows never count it as drift,placementnever calls it "not a git repository", and no HEAD is read for it (#1174) #1200's line for.git/modules/members/IRRS, unreadable under that.gittoo, is not said;r--, the row is shape-doctor.py: a leg or member whose git directory this user cannot read is named in the gate's words,spec: .git/modules/spec: cannot be read (<reason>), so thelegsandmembersrows never count it as drift,placementnever calls it "not a git repository", and no HEAD is read for it (#1174) #1200's:members/IRRS: .git/modules/members/IRRS: cannot be read (Permission denied), so no HEAD in it is compared with the pin. The entry hasmount_cannot_be_readandheadnull, and the verdict isINVALID (family);drifted_reasonsis[]for both rows, and each row'swhyis the advice for its cause.Tests on
2eabec8e, in the probe environment, on Python 3.12.3 and git 2.43.0:tests/test_pinned_root_git_unreadable.py,tests/test_validate_family_unreadable.pytests/test_root_git_unreadable.py,tests/test_doctor_unreadable_paths.pytest_path_guard_hygiene,test_no_auto_maintenance,test_scaffold_leg_path_collisionstest_no_auto_maintenance.py:222: the session began withGIT_CONFIG_GLOBALset)membersrows of both PRs and the readable control, with each row loggedThat is 532 ids: 530 passed and 2 skipped. Lane 1's gate on
a2fc1da8covers #1147's side: 1,018 passed and 5 skipped. Thepin,docs,sdandbigcalls above were not run again on the merge.Restacked after #1147 landed
#1147 landed as
51a9c5f1on main, the squash of its final head95ea5646. That head is update-branch's merge of main801a7d6dinto4d299dd3, #1147's last fix:placementgives the root's line only to a leg whose mountmount_readgot into, so a mount this user can search and not list keepsspec: cannot be listed. The squash's tree,2d6db67c, equals95ea5646's. The branch was restacked from2eabec8eby real merges, with no rebase, squash, amend or force-push:81bc91f8merges95ea5646. No conflict. Its tree,f48d7e9e, is the onegit merge-tree --write-treegave beforehand. It brings4d299dd3(shape-doctor.py, and a row intests/test_root_git_unreadable.py) and main's 18 commits aftera2fc1da8.ba81fd7dmerges the squash51a9c5f1, with this branch's side kept for shape-doctor.py, validate-pins.py and validate-manifest.py: a root whose own.gitthis user cannot read is named.git: cannot be read (<reason>), so the doctor never reads it as DRIFTED, and both pinned validators refuse it, exit 2 (#1146) #1147's files. Those are the five files ofgit diff --name-only 51a9c5f1^ 51a9c5f1:shape-doctor.py,validate-pins.py,validate-manifest.py,tests/test_doctor_unreadable_paths.pyandtests/test_root_git_unreadable.py.git ls-files -uwas empty, andgit write-treegavef48d7e9e, the tree of81bc91f8: the merge changes no file.origin/mainwas still51a9c5f1.The PR's own change is unchanged. The
+and-lines ofgit diff -U0 b75babee 5aa0f116(the delta r1172b found READY) and ofgit diff -U0 51a9c5f1 ba81fd7dare the same 928 lines, in the same 7 files and the same order: 902 added and 26 removed. Both have sha2569eef7395.Tests on
ba81fd7d, in the probe environment, on Python 3.12.3 and git 2.43.0:tests/test_pinned_root_git_unreadable.py,tests/test_validate_family_unreadable.pytests/test_root_git_unreadable.py,tests/test_doctor_unreadable_paths.py,tests/test_doctor_unreadable_pin.pytest_scaffold_leg_path_collisions,test_path_guard_hygiene,test_no_auto_maintenancetest_no_auto_maintenance.py:222: the session began withGIT_CONFIG_GLOBALset)test_agents_md_codes,test_cli_reference,test_repo_hygienetests/test_shape_doctor.py, 449 ids in 4 shardstest_shape_doctor.py:10516: the PowerShell line, printed only on a Windows host), 0 failedThat is 1,205 ids: 1,200 passed and 5 skipped, none failed. The
pinandbigcalls above were not run again on the restack.Known limits
.gitmodulesthis user cannot read, as through a link into a folder it cannot search, isgitmodules-unreadable, never no section forremove's rollback or no url formake siblings(#1110) #1164 (the sibling branchfix/1110-family-unsearchable-gitmodules) also changestemplates/family-root/scripts/siblings.py. Lane 1's merge-tree shows a content conflict in the docstring's exit-2 paragraph and inmember_unreadable. Whichever lands second keeps one form of each.chmodcure says "every directory above it". In the issue's case the line names.git, which is a link, and the folder to mend is the one it leads into.chmod u+rx .gitfollows the link and is refused. These are each tool's existing words, as shape-doctor.py: a shape root whose own.gitis a link into a folder this user cannot search reads DRIFTED, its legs as having no gitlink, and both of its validators crash with PermissionError #1146 left the gates'.siblings.pyover a.gitdirectory at 000 still places every member, by design: it asks only thestat.validate-family.pyandmake bootstraprefuse that holder..gitthis user can search whoseHEADis at mode 000 is not asked by this PR's four tools. The question is thestatofHEAD, and a searchable directory answers it. Review r1 measured the same answers on base and head, on both Pythons, each a claim about something else: the assembly bootstrap saysREFUSED pin-gitlink-absent: no gitlink (mode 160000) recorded at 'spec'; the holder's bootstrap saysFETCH INCOMPLETE (git exit 128) using credential source 'none'with the advice to setSHAPE_LEGS_TOKEN, and exits 0 with--no-fetch --skip-members;validate-family.pyrefusesgitlinks-unreadableover git'snot a git repository. This PR leaves that case as it was.HEADthis user cannot open, under.git/modules/<leg>or in a.gitdirectory, is refusedpin-submodule-unreadableby name, where it endedpin-commit-unresolvable(#1222) #1239 (for validate-pins.py and shape-doctor.py: a leg's git-directoryHEADfile at mode 000 passes the gate'sstatof HEAD and endspin-commit-unresolvable, and the doctor reads the leg DRIFTED, where both should name the file this user cannot open #1222) changes the assembly root's answer there; this PR does not. It is not on main at51a9c5f1. It makesvalidate-pins.py's_refuse_an_unreadable_git_diropen a git directory'sHEAD, and shape-doctor.py, validate-pins.py and validate-manifest.py: a root whose own.gitthis user cannot read is named.git: cannot be read (<reason>), so the doctor never reads it as DRIFTED, and both pinned validators refuse it, exit 2 (#1146) #1147's_refuse_an_unreadable_rootin that file calls it for the root's own.git. Once validate-pins.py: a leg git directory'sHEADthis user cannot open, under.git/modules/<leg>or in a.gitdirectory, is refusedpin-submodule-unreadableby name, where it endedpin-commit-unresolvable(#1222) #1239 lands, the pins validator, whosepin-gitlink-absentis the assembly bootstrap's answer above, opens the root'sHEADfirst, so that answer becomes agit-repo-unreadablerefusal by name (lane 5's note, docs/release-notes/v1.0.0.md: the v1.0.0 release notes, a draft for Brett to amend before he tags, saying what 1.0 contains and promises, how to pin it by commit, what changed on the way grouped by the five tracking areas, and every known limit as the input that triggers it and what the person sees (#472) #473 issuecomment-6724142443, read from the code, not run). validate-pins.py: a leg git directory'sHEADthis user cannot open, under.git/modules/<leg>or in a.gitdirectory, is refusedpin-submodule-unreadableby name, where it endedpin-commit-unresolvable(#1222) #1239 changes onlyvalidate-pins.pyand its tests. The family gate,validate-family.py, and the holder'sbootstrap.pyandsiblings.pycarry their own walk, so for the family holder the sentence above holds either way.statof.git, the open of a gitfile, thestatof the git directory'sHEAD. A git directory this user can search but whose index it cannot read is still git's answer.Not in this PR
scripts/repo_shape.py'sfind_repo_root.submodule_urls).membersrow for an unread holder.githaswithout_working_clone: []beside entries whoseworking_cloneis null (shape-doctor.py:11241atba81fd7d). A--jsonreader is told no member lacks a working clone, about a question the row did not ask. Review r1's NIT 3. The follow-up isNonethere, or computing it: a working clone sits beside the holder, outside its.git._refuse_an_unreadable_rootpass on base, by driving them throughmainand asserting the code is not in stderr (review r1's NIT 1). 36 of the 74 rows that fail on base fail on that name alone.Closes #1148
Lane: openreposhape-1 (openRepoShape-1)
🤖 Generated with Claude Code
Summary by Sourcery
Refuse unreadable repository roots consistently across pinned family tools and report them accurately in the shape doctor's members check.
New Features:
git-repo-unreadableerror and exit code 2.membersrow once without treating its members as drift.Bug Fixes:
Enhancements:
Tests: