Repository navigation
family.py and the family root's siblings.py: a holder's .gitmodules this user cannot read, as through a link into a folder it cannot search, is gitmodules-unreadable, never no section for remove's rollback or no url for make siblings (#1110) - #1164
brettheap wants to merge 7 commits into
Conversation
… and the family root's siblings.py submodule_urls, with its new test file Checkpoint at the coordinator's word (usage window). Work in progress. Done in this commit: - scripts/family.py: `_gitmodules_on_disk` asks the work tree's `.gitmodules` with `_is_a` and `os.access`; `_gitmodules_names` and `_sections_giving` use it, so a file this user may not read is `gitmodules-unreadable` (`.gitmodules: cannot be read (<reason>)`, exit 2), never "no section". `PinRollback` treats that code as it treats a store it cannot read (`READABLE_ONCE_CURED`): mount made again, update typed after the cure, never "only from its remote". - templates/family-root/scripts/siblings.py: `submodule_urls` asks the same way and raises `gitmodules-unreadable` (`path_unreadable`); the placement and `--make` callers refuse with exit 2 before anything is cloned, fetched or run. - tests/test_path_guard_hygiene.py: the three KNOWN_OPEN entries out. - tests/test_family_gitmodules_unreadable.py: 33 rows. Head: 33 passed. Base 038dafb (3.12.3 and a 3.14-like shim): 25 failed, 8 passed (the 8 are controls and the remove-order guard). Not yet done: mutants, the gate, the pin measurement on the base-made holder, pr-body.md. No test is known to fail at this commit. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR hardens all three targeted Sequence diagram for unreadable .gitmodules refusal in sibling operationssequenceDiagram
participant User
participant Siblings as siblings.py
participant Gitmodules as .gitmodules
participant Refusal
participant Members as Member operations
User->>Siblings: dispatch_all() or _run_placement()
Siblings->>Gitmodules: submodule_urls()
Gitmodules-->>Siblings: stat/access failure
Siblings->>Refusal: path_unreadable(gitmodules-unreadable)
Refusal-->>Siblings: exit 2 with remediation
Siblings-->>User: refuse operation
Note over Members: No clone, fetch, or member command runs
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-1, 2026-10-22T20:24:56Z) Independent review r1164 of #1164 at r1164: partial review of PR #1164 (head
|
…it could not read before the update, so the commands typed after the cure bring the member back (r1164 MUST-1, #1110) `_file_by_hand` returned only the index entry where the current bytes could not be read. After a pre-commit hook made `.gitmodules` a link into a folder at mode 000 (or the file at 000), the work tree kept the copy `git rm` wrote, without the member's section, so the typed `git submodule update` said "No url found". Unknown bytes are now not taken for the recorded ones: `git checkout -- .gitmodules` is typed where the index held them. Tests: the link row now gives the mode back, unsets core.hooksPath, types the commands and checks the member is back on main; a new row covers a hook that only runs `chmod 000 .gitmodules`. Both fail at 4352ac2. Part of #1110. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o readers and siblings.py's submodule_urls, even where its read is refused (r1164 SHOULD-1, #1110) Control rows: a directory at `.gitmodules` gives `(None, [])`, `[]` and `{}`, as `is_file()` always answered, and with `os.access` refusing its read it still does. They pass at 038dafb and at head, and kill r1164's mutants M10 and M21 (`S_ISREG` changed to any mode), which survived. Part of #1110. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bject`, so `remove` names a .gitmodules at mode 000 `gitmodules-unreadable` whatever git's stat cache says (r1164 SHOULD-2, #1110) Where `git ls-files -m` had to read the file, could not and listed it as modified, `_stages_as` quoted the failed `git hash-object` ("a git or gh command failed"); where git's cache spared the read, `_gitmodules_names` gave `gitmodules-unreadable`. Both now give `gitmodules-unreadable`. A link is still `gitmodules-unstaged`'s first. `_stages_as` takes the verb, and the remedy says what `add` or `remove` reads the file for (`GITMODULES_READ_FOR`). Tests: both stat-cache states made on purpose (`core.trustctime` off, the mtime set long ago and the index refreshed; stale: the mtime moved again), each asserted through `git ls-files -m`, dry run and real; and an in-process `add` row over an assume-unchanged copy it may not open. Part of #1110. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d with its cure in the sentence, as a store it cannot read is (r1164 NIT-1, #1110) `_undo_work_tree` appends to the refusal's detail, which the sentence prints with no remediation: for a permission, "`chmod u+r` on it, and `chmod u+rx` on every directory above it, lets git read it" (no "inside the holder": a link can lead out of it); for another cause, OTHER_CURE. Part of #1110. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…NREAD_CODE`, so it is not read for `GITMODULES_UNREADABLE`, the remediation `_submodule_paths` gives (r1164 NIT-4, #1110) A rename only: the value, every refusal and every sentence are as they were. The constant's comment says which of the two is which. Part of #1110. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-1, 2026-10-23T03:07:15Z) Independent review r1164b of #1164 at r1164b: independent review of PR #1164 at head Verdict: NOT READY, on one SHOULD. No MUST.r1164's MUST and its other six findings are fixed, and I measured each one. Every claim in the body that I checked holds, apart from one gap: the rollback change also changes what r1164's findings, re-checked at head
The body's claims I measured
SHOULD-1: the rollback change also changes
|
| Mutant | What it changes | 3.12 rows failed | Shim rows failed |
|---|---|---|---|
| A1 | _gitmodules_on_disk: _is_a back to is_file() |
5 | 10 |
| A2 | _gitmodules_on_disk: no os.access |
9 | – |
| A3 | _gitmodules_on_disk: except PermissionError only |
2 | 2 |
| A4 | where="" dropped |
9 | – |
| A5 | remedy always remove's |
1 | – |
| A6 | _stages_as does not ask _gitmodules_on_disk |
2 | – |
| A7 | _stages_as does not pass the verb on |
1 | – |
| A8 | _gitmodules_names back to is_file() |
9 | 9 |
| A9 | _sections_giving back to is_file() |
6 | 6 |
| A10 | no cure appended in _undo_work_tree |
1 | – |
| A11 | cure asks exc, not its cause |
1 | – |
| A12 | mount made again for a store only | 1 | – |
| A18 | S_ISREG changed to _any_mode |
– | 2 |
| A20 | _gitmodules_on_disk asked before lstat (the order guard) |
– | 2 |
| A22 | OSError taken as absent |
– | 19 |
| B1 | submodule_urls: _is_a back to is_file() |
– | 9 |
| B3 | submodule_urls: except PermissionError only |
– | 1 |
| B4 | S_ISREG changed to _anything |
– | 1 |
| B13 | OSError taken as no urls |
– | 10 |
A dash means that mutant did not run on that Python before LAST STEP.
Not covered (LAST STEP)
- Mutants not run (16): A13 to A17, A19, A21, A23, B2 and B6 to B12. The 3.12 run of B1, B3, B4 and B13 did not happen either.
- bootstrap.py, and the family root's validate-family.py, bootstrap.py and siblings.py: a root whose own
.gitthis user cannot read is a PermissionError traceback on 3.12, ornot-a-git-repowhereexists()answers False, not refused by name #1148 merge: I did not re-rungit merge-tree --write-tree --name-only 3162b523b 3a389565a, nor the "merges withb75babee3cleanly" claim. - The pin claims: a base-made holder passing its own
validate-family.py, andupdate-shape.py checknaming onlysiblings.py. - The wider gate: the 132-file family set, the four
test_shape_doctor.pyshards andtest_scaffold_leg_path_collisions.py. None was re-run; I ran only the 171-rowsiblings.pyset. - Rows that skip on Windows and as root.
- Load rule: I once started a 9-second probe run while the container showed 4 pytest processes. Every runner waits on that count now.
State
The review checkout /workspace/projects/openRepoShape-worktrees/l1-r1164 is detached at 3162b523b with an empty git status --short. No run of mine is live. The only mutants.py process running belongs to r1189.
Everything is in /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1164b:
- probe file:
test_r1164b_probe.py - shim:
shim314/sitecustomize.py - probe environment:
penv - mutant driver:
mutants.py - logs:
logs/(probe-*.txt,mut-p1.txt,mut-s1.txt,g1.txttog6.txt,base-*.rows,head-*.rows,prev-312.txt)
The scratch clones base, probe and prev hold untracked probe files only.
Lane: openreposhape-1 (openRepoShape-1)
…dex's copy after the entry, under `bump` and under `remove`, so `_file_by_hand`'s change holds for the manifest too (r1164b SHOULD-1, #1110) `PinRollback._file_by_hand` serves every recorded file, so taking bytes it cannot read for not the recorded ones also changed what `bump` and `remove` type for an unreadable `family.yaml`: `git checkout -- family.yaml` follows the index entry. No row held it. A `pre-commit` hook runs `chmod 000 family.yaml` and refuses; with the mode given back and the typed commands run, `family.yaml` holds its recorded bytes and `git status --porcelain` is empty. Base 038dafb: both rows fail at the bytes assertion (31 failed, 11 passed in the file) on Python 3.12.3 and on the 3.14-like shim. Head: 42 passed on both. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
|
(lane openRepoShape-1, 2026-10-23T12:09:42Z) Independent re-check r1164c of #1164 at Lane 1 applies both NITs to the body. r1164c: independent review of PR #1164 at Verdict: READY on what I covered (provisional). No MUST, no SHOULD; two NITs.r1164b's SHOULD and both NITs are fixed, and I measured each. The verdict is provisional because 12 of the 16 mutants were not run, and the new file was not run on either merged tree. Part 1: the fix round
Part 2
NITs
Not covered (STOP)
State
Lane: openreposhape-1 (openRepoShape-1) |
|
(lane openRepoShape-1, 2026-10-23T17:59:46Z) #1164 is READY at r1164c (issuecomment-6364558859) was a provisional READY. It stopped at lane 1's STOP with 12 of r1164b's 16 mutants unrun, the 3.12 runs of B1, B3, B4 and B13 unrun, and the new file run on neither merged tree. Lane 1 ran all of that from the main loop, with r1164c's
r1164c found no MUST and no SHOULD, and both of its NITs are already applied to the body. So #1164 counts READY at Lane: openreposhape-1 (openRepoShape-1) |
…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>



Part of #1110. This is its slice B:
scripts/family.py's_gitmodules_names,_sections_givingand_stages_as, and the family root'sscripts/siblings.pysubmodule_urls. The issue stays open for its other sites.It 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"), and lands after the tag, on a later word.
What
Each of the three readers asked
Path.is_file()of the holder's.gitmodules. Python 3.9 to 3.13 raise a refusedstat(PermissionError); from 3.14is_file()answers False to anyOSError.Measured with a family holder whose
.gitmodulesis a link into a folder at mode 000: at base038dafba9on Python 3.12.3 and on a 3.14-like shim (asitecustomizethat givespathlib3.14's answers), and at head on both. The control, the same link into a folder at 755, reads the file through the link at base and at head, on both.family.py remove(as a person runs it), dry run and real, link into 000 or 755gitmodules-unstaged, exit 2: git lists the link as modified, so this refusal comes before either readerfamily.py remove(as a person runs it), dry run and real,.gitmodulesa file at mode 000, git's stat cache stale (git ls-files -mlists it)git hash-objectgitmodules-unreadable, exit 2, nothing writtengit ls-files -mdoes not list it)git config --file .gitmodules --listgitmodules-unreadable, exit 2, nothing written_gitmodules_names/_sections_giving/_store_git_uses/_unrestorable/_checked_one_section/_checked_mount_readable(in process)PermissionError(None, [])/[]/None/ "no section of .gitmodules gives git members/IRRS" /None/Truegitmodules-unreadable,.gitmodules: cannot be read (Permission denied)remove's rollback (apre-commithook links.gitmodulesinto 000 and refuses the commit)members/IRRSleft gone, nothing typed.gitmodulesindex entry, then onlymkdir members/IRRS.gitmodulesindex entry,git checkout -- .gitmodules,git -c protocol.allow=never submodule update --init --no-fetch -- members/IRRS,git -C members/IRRS checkout -q main --. Typed as printed once the mode is given back, they put the member back onmainand leavegit statuscleanremove's rollback (apre-commithook that only runschmod 000 .gitmodules)mkdir members/IRRSmkdir members/IRRSbump's andremove's rollback (apre-commithook that only runschmod 000 family.yamland refuses the commit):PinRollback._file_by_handservesfamily.yamltoofamily.yamlindex entry. With the mode given back,family.yamlstays the copy the verb had rewritten, andgit statuslistsM family.yamlgit checkout -- family.yaml. With the mode given back,family.yamlholds its recorded bytes andgit status --porcelainis emptysiblings.py --dry-run/family.py siblings --dry-rungitmodules-unreadable, exit 2siblings.py(real run)CLONE FAILED, exit 1gitmodules-unreadable, exit 2, nothing cloned beside the holdersiblings.py --make help --dry-run, no clonesmake helpran in 0 of 2 membersgitmodules-unreadable, exit 2PinRollback._file_by_hand(scripts/family.py:8236-8256) serves every file the rollback records:bump'sfamily.yaml, andremove's.gitmodulesandfamily.yaml. Taking bytes it cannot read for not the recorded ones, which the two.gitmodulesrollback rows above need, therefore also changesbumpandremovefor an unreadablefamily.yaml:git checkout -- family.yamlis typed after the index entry, and with the mode given backfamily.yamlcomes back as recorded, where at base it stayed the copy the verb had rewritten (the last row above). It is the same fix at a second file, and one row holds it under each verb (r1164b's SHOULD-1).In process, with the
statof a.gitmodulesgit still reads as the index's refused: at baseremove --dry-runsaid no section is removed, exit 0, and the real run was refused only by its rollback's record, asmember-unreadable. At head both aregitmodules-unreadable, exit 2, before any git command that writes, and the holder is left as it was.The refusal names the file and the cause. A permission gets the
chmodcure; any other cause gets none:Why
gitmodules-unreadableis the codeadd's_submodule_pathsand the holder's ownvalidate-family.pyalready give this file, so the cure follows the cause at each site this slice changes (not inadd's read or the holder's validator's, which keep their own remediation: see Known limits). Infamily.pythe constant isGITMODULES_UNREAD_CODE, so it is not read forGITMODULES_UNREADABLE, which is the remediation_submodule_pathsgives a file git cannot parse._stages_as, whichremoveandaddask before either reader, now asks_gitmodules_on_diskoncelstatsays the file is regular, beforegit hash-object. Without it, a file at mode 000 was refused two ways: bygit hash-object's failure where git had to read the file to compare it, and asgitmodules-unreadablewhere git's cache spared the read. A link is stillgitmodules-unstaged's first. The remedy says what the verb reads the file for (GITMODULES_READ_FOR), soaddis not told whatremovedoes._is_aasks. Nothing at the name is still no file, as it always was, and so is a directory. A refusedstatis refused by name.os.accesstoo, after_checked_mount_readable'sHEADprecedent. Without it, a.gitmodulesat mode 000 whosestatsucceeds is still read as giving no url insiblings.py(git's read fails, and that failure reads as no url) and, in the rollback, as no store.where=""): a link can lead out of the holder.READABLE_ONCE_CURED = (STORE_UNREADABLE, GITMODULES_UNREAD_CODE)), inPinRollback._undo_work_treeand_work_tree_by_hand. The mount is made again, empty, and the update is typed after the cure; it is never "comes back only from its remote". Met inside the rollback, the refusal's detail carries its cure, as a store's does, since the sentence prints no remediation._file_by_handtyped only the index entry where it could not read the current.gitmodules. The hook's copy is the onegit rmwrote, without the member's section, so the update typed after the cure saidNo url found for submodule path 'members/IRRS'. Nowgit checkout -- .gitmodulesis typed where the index held the recorded bytes; where they were there already, it writes the same bytes. Once cured, the checkout comes back from the storegit rmkeeps, with no network._file_by_handserves every recorded file, so the same change reachesfamily.yamlunderbumpandremove:git checkout -- family.yamlfollows the index entry, and the file comes back as recorded (see What).gitmodules-unstagedstill refuses a person's linked.gitmodulesfirst.siblings.py:path_unreadable(code, ...)is new andmember_unreadabledelegates to it._run_placementanddispatch_allcatch the refusal and return 2 before anything is cloned, fetched or run. The docstring's exit-2 paragraph names the case.Tests
New
tests/test_family_gitmodules_unreadable.py, 42 rows. Each site has a control (nothing there, a directory there, or a link this user can read); in-process rows for a refusedstat, an I/O error and a refusedos.access, which run on every Python and platform and as root; real links into a folder at 000, or a file at 000, skipped on Windows and as root; and, forfamily.yaml, apre-commithook that only takes its mode off, one row run underbumpand underremove, skipped the same way.74096eb47: 42 passed on 3.12.3, and 42 passed on the 3.14-like shim (it loaded in 14family.pyand 6siblings.pysubprocesses);038dafba9: 31 failed, 11 passed, on 3.12.3 and on the shim alike, the same 31: the 29 of the previous head and the new row under each verb. Both new rows fail on behaviour, atassert manifest.read_bytes() == recordedafter the typed commands ran. On the shim no row reached the network: the one row that tries a clone,test_make_siblings_refuses_a_gitmodules_linked_into_a_locked_folder[real], stopped three times attransport 'https' not allowed.test_a_persons_remove_over_a_linked_gitmodules_is_refused_first.4352ac2fd, 7 of the 40 fail. Five are this round's behaviour: the two rollback rows typed by hand (the typedgit submodule updateexits 128), the stale-cacheremoverow, the in-processaddrow and the rollback sentence's cure. Two fail only on the renamed constant.git diff --stat 3162b523b 74096eb47names that test file alone, soscripts/family.pyandsiblings.pyare as they were.tests/test_path_guard_hygiene.py. The threeKNOWN_OPENentries for these sites are taken out. Before that, its stale-entry check failed on exactly those three ("0 found, 1 listed"). At4352ac2fd: 49 passed (hygiene); at3162b523band at74096eb47: 107 passed, 2 skipped withtests/test_no_auto_maintenance.py(call G2 below).Mutants, each run against the new file in a scratch clone at head, each file restored after: review r1164's 22, and 7 more for this round. Run at
3162b523b: 29 of 29 killed, no survivors, none "not applied"; the clone was restored after each (git statusof the clone shows only the untracked probe file). The two that survived at4352ac2fd, M10 and M21, are killed now by the directory rows (r1164 SHOULD-1).One more for r1164b's SHOULD-1, run at
74096eb47: A21,_file_by_handkeeping the base behaviour forfamily.yamlonly (same = name != GITMODULES), against the new file withtests/test_family_py_unreadable.pyandtests/test_family_bump_remove_rollback.py(148 rows). It is killed, on 3.12.3 and on the shim: 2 rows failed, exactly the new row underbumpand underremove, and the other 146 pass with it, so no earlier row held that case. The same 148 rows with no mutant: 148 passed on both. The clone'sscripts/family.pyblob equals HEAD's after it. The code M1-M29 mutate is unchanged at74096eb47, so that is 30 of 30 killed, no survivors.3162b523b; A21 at74096eb47)_gitmodules_on_disk:_is_aback to.is_file()_gitmodules_on_disk:os.accessbranch removed_gitmodules_on_disk:OSErrortaken as absentwhere=""dropped_undo_work_treeback to== STORE_UNREADABLE_work_tree_by_handback to!= STORE_UNREADABLEREADABLE_ONCE_CUREDwithout the.gitmodulescodemember-unreadableexcept OSErrornarrowed toPermissionErrorS_ISREGchanged to any mode (survived at4352ac2fd)_gitmodules_namesback to.is_file()_sections_givingback to.is_file()siblings.pysubmodule_urls:_is_aback to.is_file()submodule_urls:os.accessbranch removedsubmodule_urls:OSErrortaken as{}submodule_urls:except OSErrornarrowed toPermissionErrorpath_unreadable: advice alwaysOTHER_REASONpath_unreadable: the code ignored_run_placement: refusal exit 2 changed to 1dispatch_all: refusal exit 2 changed to 1submodule_urls:S_ISREGchanged to any kind (survived at4352ac2fd)_undo_work_tree: the mount not made again for any refusal_file_by_hand: bytes it cannot read back toreturn typed74096eb47, with thefamily.yamlrows)_stages_as:_gitmodules_on_disknot asked_stages_as: the verb not passed on_gitmodules_on_disk: the remedy alwaysremove's_undo_work_tree: no cure appended to the detail (NIT-1)_undo_work_tree: the permission cure changed toOTHER_CURE(NIT-1)_file_by_hand: bytes it cannot read taken as the same74096eb47, with thefamily.yamlrows)_file_by_hand: base behaviour kept forfamily.yamlonly[bump]and[remove]), on 3.12.3 and on the shimThe pin. A family holder made at base
038dafba9passes its ownscripts/validate-family.py(exit 0, 10 copied shape files match).update-shape.py checkagainst head names exactlyscripts/siblings.pyasupstream-changed, with the other 9 unchanged (measured at4352ac2fd; this round does not touchsiblings.py). Every name head'ssiblings.pyimports fromrepo_shape(10) andbootstrap(6) is in a base holder's copies, sotests/test_update_shape_kept_helper.pyneeds no change.The gate on Python 3.12.3 (no full suite):
74096eb473162b523b4352ac2fdtests/test_family_gitmodules_unreadable.py(the new file)tests/test_family_py_unreadable.pyandtests/test_family_bump_remove_rollback.py_file_by_handchange)tests/test_path_guard_hygiene.pyandtests/test_no_auto_maintenance.py(a new row setscore.trustctimelocally)test_no_auto_maintenance.pyneed a session that began withoutGIT_CONFIG_GLOBAL, and the probe environment sets it to/dev/null)test_agents_md_codes,test_cli_reference,test_repo_hygiene)--helpchanged)test_family_member_branch_not_utf8,test_family_add_move_back_sections,test_family_refuse_up_front,test_family_remove_gitmodules,test_family_add_spelling_leftovers,test_family_add_spellings_remedies,test_refusal_default_remediation)scripts/family.pyis unchanged since3162b523btests/test_scaffold_leg_path_collisions.pytests/test_shape_doctor.py, 4 shards of 112 idsfamily.py,siblings.pyorfamily-root(132 files, 8274 ids, 28 shards of 300)Known limits
PinRollback._undo_work_tree's leg loop names a member's own.gitmodulesthat cannot be read by its reason, then still raises it asmember-store-gone, which says the leg "comes back only from its remote". In practice it cannot be reached: git has just written that file. It is out of this slice..gitmodulesgives a permission the step's own cure, which says "on every directory above it inside the holder", even where the file is a link out of the holder (the folder at 000 is outside it). The person reads which path was refused from the sentence. It is 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's wording for every rollback step, and out of this slice..gitmodulesrefused for a cause other than a permission (OTHER_CURE) has no row: there,_bytesfails first and the step that would append it is not tried, so it is reached only in a race.addover a.gitmodulesat mode 000 is refused by_submodule_pathsfirst, in both stat-cache states, asgitmodules-unreadablewith its "mend the file by hand" remediation, not thechmodcure (measured). The holder's own validator does the same:templates/family-root/scripts/validate-family.py's_gitmodules_listed(lines 1895-1907) gives a permission failure the same code,gitmodules-unreadable, with "Mend the file by hand" (git config -f .gitmodules --listsays where it breaks), not thechmodcure; measured for a file at mode 000 and for a link into a folder at 000, each exit 2 (r1164b's NIT-2). Those areadd's and the validator's own sites of 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, not this slice's; the in-processaddrow here holds_stages_as's wording only.fix/1148-pinned-find-repo-root-refuses(PR bootstrap.py, validate-family.py and siblings.py: a root or holder whose.gitthis user cannot read is refusedgit-repo-unreadable, exit 2, not a traceback ornot-a-git-repo, and the doctor'smembersrow names it once, never drift (#1148) #1172 for bootstrap.py, and the family root's validate-family.py, bootstrap.py and siblings.py: a root whose own.gitthis user cannot read is a PermissionError traceback on 3.12, ornot-a-git-repowhereexists()answers False, not refused by name #1148, now at5aa0f116;3a389565ais an older head of it; stacked onfix/1146-root-git-unreadable-not-driftatb75babee3, which is off038dafba9), changes the sametemplates/family-root/scripts/siblings.py, so the second of the two to land will need a merge there.git merge-tree --write-tree --name-only 74096eb47 5aa0f116gives a content conflict in that file alone (r1164c;3162b523bwith3a389565agave the same); this head merges withb75babee3cleanly. The overlap is the docstring's exit-2 paragraph andmember_unreadable, to which bootstrap.py, and the family root's validate-family.py, bootstrap.py and siblings.py: a root whose own.gitthis user cannot read is a PermissionError traceback on 3.12, ornot-a-git-repowhereexists()answers False, not refused by name #1148 gives acode=keyword where this branch addspath_unreadable(code, ...). The second to land should keep one of the two forms.Not in this PR
shape-doctor.pysitesKNOWN_OPENstill lists 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,check_agent_filesandcheck_what_is_here,add's_submodule_paths, the holder'svalidate-family.py_gitmodules_listed, and 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 other sites. Each is its own slice.Lane: openreposhape-1 (openRepoShape-1)
🤖 Generated with Claude Code