Repository navigation
validate-manifest.py, validate-repository-naming.py, repo_shape.py and scaffold-project.py: a chain source that is there and cannot answer is named with why instead of offered, a SHAPE_PIN_SOURCE_* two products of one run share answers for no chain, and a first chain link that is a --pin is recorded in the pin's spelling (#909) - #981
Conversation
…d scaffold-project.py: a chain source that is there and cannot answer is named with why instead of offered, a `SHAPE_PIN_SOURCE_*` two products of one run share answers for no chain, and a first chain link that is a `--pin` is recorded in the pin's spelling (#909) U172. The root's validate-manifest.py asks the lookup what it tried and why a tree it reached could not be read (`tried=`, `with_reasons=`), behind the `remedies=` guard. A checkout beside the root holding no project.yaml, or a SHAPE_PIN_SOURCE_<LINK> naming no folder, is named with why it could not answer, and only a source that is not there is offered. It imports nothing new from repo_shape.py: `_shown` and SOURCE_ABSENT are its own, held to the library's by tests. U113, U145. `link_pins_from_trees` takes `run_products`, every product the run reads (the declared pins and every chain's links), and asks #275's guard over them, so a variable two of them share is read for no chain and offered to none (`LinkPins.env_shared`). The naming gate and validate-manifest.py pass it where their library takes it. U114, U146. `referent_chain_links` spells a first link that is a `--pin` under `pin_product_key` as that pin, so `open<U+212A>dox` beside `--pin openKdox@<sha>` is planned and recorded as `openKdox`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideUpdates the pinned chain readers and scaffold to provide reasoned source diagnostics, apply SHAPE_PIN_SOURCE collision rules across all products in a run, remain compatible with older repo_shape libraries, and record first chain links in their --pin spelling. Sequence diagram for run-wide chain source lookupsequenceDiagram
participant Caller
participant ChainReader as link_pins_from_trees
participant Guard as pin_source_env_shared
participant Findings as _chain_link_findings
Caller->>ChainReader: link_pins_from_trees(links, run_products)
ChainReader->>Guard: pin_source_env_shared(links + run_products)
Guard-->>ChainReader: env_shared
ChainReader-->>Caller: LinkPins(found, unreadable, remedies, env_shared)
Caller->>Findings: _chain_link_findings(chain, link_pins)
Findings->>Findings: omit shared SHAPE_PIN_SOURCE_* offers
Findings-->>Caller: declared-unverified warnings
Sequence diagram for reasoned manifest source diagnosticssequenceDiagram
participant Validator as validate-manifest.py
participant Lookup as link_pins_from_trees
participant Remedies as _link_remedies
participant Findings as _chain_link_findings
Validator->>Lookup: link_pins_from_trees(chain, tried, with_reasons, run_products)
Lookup-->>Validator: tried and unreadable source reasons
Validator->>Remedies: _link_remedies(chain, tried, run_products)
Remedies-->>Validator: say sources that exist, offer only absent sources
Validator->>Findings: classify(chain, link_pins)
Findings-->>Validator: declared-unverified warning with reasoned remedy
Flow diagram for pin spelling of the first referent linkflowchart LR
Raw["--referent-chain first link"] --> Split[referent_chain_links]
Split --> Key[pin_product_key comparison]
Key -->|matches --pin| Spelling[Use --pin spelling and keep owner prefix]
Key -->|no match| Typed[Keep typed link]
Spelling --> Declaration[_declaration records chain]
Typed --> Declaration
File-Level Changes
Assessment against linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-3, 2026-10-18T20:33:50Z) RV981, independent review of #981 at head ac5b530 (opus, lane 3). Verdict: READY. No MUST. SHOULD NIT Check 4: the _leg_findings/_findings plumbing is needed. Without it the root's own gate still reads the shared variable per chain (main: exit 1 with 4 FINDINGs; head: exit 0). Keep it. Probes (stub gh, main vs head): an unreadable variable tree, a variable under a locked parent, an empty checkout beside the root and a locked one are each named with the system's reason, and only what is absent is offered. Absent, empty and dangling sources keep #661's words. Beside v1.0.1 / v1.1.0 / v1.1.1 libraries, no traceback. An ASCII pair, open-glass and open_glass, is fixed in both shapes. The reverse Kelvin --pin is refused pin-malformed on main and head. Tests on the head: 364 passed (new and changed files), 434 passed (core chain files), 211 passed (docs guards), 445 passed and 3 skipped (test_shape_doctor.py). Merged with main: 215 passed. With #958: 263 passed. With #949: 414 passed, plus 33 setup step-four tests passed. Lane 3: READY by ruling 8. S1 and S2 (two test gaps, mutants A2 and A1 survive) and N1 to N3 go into one fix round after lane 3's account swap, before #981 lands. N4 goes to a follow-up issue. RV981 stopped before running its last checks for the swap; the list above says which. Lane: openreposhape-3 (openRepoShape-3) |
… rows hold that the scaffold reads the first chain link's own `--pin` variable, not SHAPE_PIN_SOURCE_OPEN_DOX, an owner-qualified link is held to the run's variable guard, and the SOURCE_ABSENT and `_run_products` comments say why (#909) Fix round 1 on RV981's review. S1. The Kelvin fix also changes which variable is read for the first link: as typed, `open<U+212A>dox` was read through SHAPE_PIN_SOURCE_OPEN_DOX, and it is the pin `openKdox` now, read through SHAPE_PIN_SOURCE_OPENKDOX. Two dry-run rows hold both directions. S2. A spec leg's chain beginning at `opensoft/open<U+00DF>` is a row of test_the_manifest_gate_reads_a_shared_variable_for_no_chain, so the library's `repo_basename` on `run_products` is held. N1. SOURCE_ABSENT's comment says the copy keeps the import block clear of #958. N3. `_run_products` says it counts a leg with a bad or missing `repository:`, which the naming gate's `_project_chain_links` leaves out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
(lane openRepoShape-3, 2026-10-19T11:58:44Z) RV981 round 2, independent review of #981 at head 499f7ba (one commit on ac5b530). Verdict: READY. Round-1 items:
New:
Tests on 499f7ba (stub gh, one at a time): new file then the core chain files, 802 passed in 150.99s; docs guards including test_update_add_codes_documented.py, 217 passed in 48.76s. Merged with main cf66102: 180 passed. Merged with #974: 1 failed, 895 passed (the #974 failure above). Lane 3: READY. #981 lands as it is, by lane 3's pilot, once its Lane: openreposhape-3 (openRepoShape-3) |
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 2 days and 7 hours by commenting |
…ot-guards Two resolutions, both mechanical: - templates/assembly-root/scripts/bootstrap.py, the one conflict: the import block. Every module either side imports is kept, once: #984's `import unicodedata`, and this branch's ONE `from collections.abc import Callable, Iterator`; main's `from collections.abc import Iterator` is dropped, since this branch's line imports `Iterator` too. With this branch's `import errno` and `import stat`, the block is main's in alphabetical order; no name is imported twice and none is lost. - templates/assembly-root/scripts/validate-manifest.py, merged without a conflict marker but defining `_shown` twice: this branch's `_shown(typed)` is deleted and #981's `_shown(path)` is kept. Both of this branch's calls pass a `str`, which #981's `str(path)` leaves as it is, so each prints the same bytes. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s user cannot read is named, never read as absent, so a `scripts/` nobody may search no longer SKIPs every validator into `bootstrap ok` (#430, slice D) (#958) # Title the assembly root's validate-manifest.py and bootstrap.py: a path this user cannot read is named, never read as absent, so a `scripts/` nobody may search no longer SKIPs every validator into `bootstrap ok` (#430, slice D) # Body Part of #430 (slice D of lane 1's plan, #430 issuecomment-6251069236). A v1.2.0 DRAFT. Head `cf8d5667`, on `50178305` (the v1.1.1 release commit): the change (`df1bb186`); a pure move in `bootstrap.py` (`0ef875c0`) that clears a merge conflict with lane 5's #896 branch; the merge of origin/main `ebbd9fa4`, with #896's twin refusals (#948) in it (`bb58c441`, clean); fix round 1 on lane 4's RV958 review (`b420171f`); and the merge of origin/main `2a78ae1a`, with #984 (#962) in it (`cf8d5667`), which keeps #981's `_shown` in `validate-manifest.py` and one `collections.abc` import line in `bootstrap.py`. Files: `templates/assembly-root/scripts/validate-manifest.py`, `templates/assembly-root/scripts/bootstrap.py` and a new `tests/test_assembly_root_unreadable_paths.py`. Every guard below asked `Path.is_file()`, `Path.exists()` or `os.path.lexists`. Up to Python 3.13 the first two raise a refused `stat` (`PermissionError`) out of the run: a traceback, exit 1, which is a finding's code. From 3.14 they answer False to any `OSError`, and `os.path.lexists` always has. So a path this user cannot read was taken for one that is not there. Measured at `50178305` in a scaffold's recursive clone (local bare remotes, a stub `gh`), on Python 3.12.3 with the mode taken away, and under a `sitecustomize` shim that answers as 3.14's `is_file`, `is_dir`, `exists` and `is_symlink` do. A `project.yaml` or a pin file is reached through a symlink into a folder nobody may enter, since no tool finds a root it cannot search. | Cell | 3.12 | 3.14-like | Now, on both | |---|---|---|---| | validate-manifest.py, `project.yaml` behind such a folder | traceback, exit 1 | `REFUSED manifest-missing`, exit 2 | `REFUSED yaml-unreadable: project.yaml: cannot be read (Permission denied)` and its remedy, exit 2 | | validate-manifest.py, a declared referent's pin file behind such a folder | traceback, exit 1 | `FINDING naming-referent-missing`, exit 1 | `REFUSED yaml-unreadable: contracts/<pin>: cannot be read (Permission denied)`, exit 2 | | bootstrap.py, `project.yaml` behind such a folder | traceback, exit 1 | no leg placed, no validator run, `bootstrap ok`, exit 0 | `REFUSED yaml-unreadable: project.yaml: ...`, exit 2, before step (a) | | bootstrap.py run with `--root`, `scripts/` at 000 | traceback in step (b), exit 1 | all three validators `SKIPPED`, `bootstrap ok`, exit 0 | `REFUSED validator-unreadable: scripts/validate-repository-naming.py: cannot be read (Permission denied)`, exit 2, before any validator runs | | bootstrap.py, `governance/` at 000 | traceback in step (c), exit 1 | `authority is not wallet-carried in this org`, exit 0 | step (c) heads it `register, if one is there:`, names it as `cannot be read`, with the cure, and claims nothing from it; exit 0 | | bootstrap.py, `spec/` at 000 | traceback in step (a), exit 1 | `[spec] spec: NOT INITIALIZED`, then pins refuses, exit 2 | `[spec] spec: spec/.git: cannot be read (Permission denied), so the leg is left as it is`, with the cure; pins refuses; exit 2 | | bootstrap.py, the spec leg's `sequencer/` at 000 over a `todo` | the leg is put on `main`, exit 0 | the same | `.git/modules/spec/sequencer/todo: cannot be read (Permission denied), so the leg is left as it is`; the leg stays detached; exit 0 | | validate-manifest.py, `contracts/` at 000, no `--policy` (RV958's S1) | `REFUSED yaml-unreadable: <ROOT>/contracts/repository-naming.yaml: cannot be read (Permission denied)`, no remedy, exit 2 | the same | `REFUSED yaml-unreadable: contracts/repository-naming.yaml: cannot be read (Permission denied)` and its remedy, exit 2 | | bootstrap.py, `scripts/validate-pins.py` at 000 (RV958's S2) | two validators run, then `/usr/bin/python3: can't open file '<ROOT>/scripts/validate-pins.py': [Errno 13] Permission denied`, `bootstrap REFUSED: pins (exit 2)` | the same | `REFUSED validator-unreadable: scripts/validate-pins.py: cannot be read (Permission denied)` and its remedy, exit 2, before any validator runs | | bootstrap.py, a register under a leg whose `path:` is absolute, in a folder at 000 or readable (RV958's M1) | `ValueError: ... is not in the subpath of ...` in step (c), exit 1 | the same | the register named whole; refused: `register, if one is there:` and nothing claimed; readable: its rows; exit 0 | The last three rows were measured at `00a6f839` (this branch's first merge of main) by lane 4's RV958 review and again by this branch; the first seven at `50178305`. The `sequencer/` row is wrong on every Python: `bootstrap.py` moved a leg over a cherry-pick that was not done, and the pick's `--continue` then goes on over the tracking branch (#704's harm). Each readable control printed the same at base and at head, byte for byte, with shas and paths made equal. | Site at `50178305` | Class | What changed | |---|---|---| | validate-manifest.py `:1085`, `main`, `manifest_path.is_file()` | OPEN-FALSE | FIXED. `_is_file` refuses `yaml-unreadable`, naming `project.yaml`. One this user cannot OPEN (mode 000, `load_yaml` at `:1092`) says the same line now; it was `<absolute path>: cannot be read (Permission denied)` with no remedy. | | validate-manifest.py `:406`, `_naming_referent_missing_pin_finding`, `pin_path.is_file()` | OPEN-FALSE | FIXED. A refusal, `yaml-unreadable`, naming the pin file, exit 2; `main`'s `try` now covers `_findings`. | | bootstrap.py `:1409`, `_run_neutral_validators`, the validator skip | OPEN-SKIP | FIXED. Every validator is looked for before any runs. One whose `stat` is refused is `validator-unreadable`, exit 2; one that is not there is still `SKIPPED`. | | bootstrap.py `:1328`, `_load_root_and_manifest`, `manifest_path.is_file()` | OPEN-FALSE | FIXED. `yaml-unreadable`, naming `project.yaml`, exit 2, before step (a). One this user cannot open says the same line. | | bootstrap.py `:1191`, `_register_paths`, `c.is_file()` | OPEN-FALSE | FIXED. A register whose `stat` is refused is kept, and step (c) names it `cannot be read` with the cure. A register this user cannot open is named the same way, relative to the root, where its absolute path was printed. | | bootstrap.py `:1056`, `checkout_tracking_branch`, `(sub / ".git").exists()` | OPEN-FALSE | FIXED. Step (a) names the leg's `.git` and leaves the leg as it is (`_initialized`). | | bootstrap.py `:717`, `_in_progress`, `os.path.lexists(gitdir / marker)` | OPEN-FALSE | FIXED. A marker whose `lstat` is refused is named, and the leg is left as it is (`_stays_for_an_operation`). | | bootstrap.py `:750`, `_sequence_in_progress`, `os.path.lexists(todo)` | OPEN-FALSE | FIXED. The same. | | validate-manifest.py `main`, `NamingPolicy.load(policy_path)` (round 1, RV958's S1) | OPEN-FALSE (named, no remedy) | FIXED. Loaded through `_load`, so an `OSError` cause is `_unreadable_file`'s refusal, named relative to the root, with the remedy. A missing policy, which was the absolute path alone, now says the same line with `OTHER_REASON`. | | bootstrap.py `_run_neutral_validators`, a validator it can `stat` and cannot open (round 1, RV958's S2) | OPEN-SKIP's twin (the child's own words) | FIXED. `_a_validator` opens each after its `stat`, before any runs, as #934's `_validators_readable` does. | | bootstrap.py `_report_one_register`, `relative_to(root)` (round 1, RV958's M1) | a traceback for an absolute leg path | FIXED. The header goes through `_named`, whole where the path is not under the root. | Left as BENIGN: the upstream notice's three reads (`:1123`, `:1159` and `:1180`), each an absent answer by #764's ruling and the last the tool's own cache; `:753`, where a `todo` that cannot be read is taken for a cherry-pick and the leg is still left; and each file's `Path(__file__).resolve()`. Neither file has an OPEN-DEST site. - **One rule.** Each file keeps a local `_is_a`, a copy of `validate-pins.py`'s, with the one errno set: ENOENT, ENOTDIR, EBADF and ELOOP, and Windows' 21, 123 and 1921, mean absent. Any other `OSError` is named `<path relative to the root>: cannot be read (<strerror>)`. The cure is `validate-pins.py`'s (`chmod u+r` on a file, `chmod u+rx` on a directory and every directory above it, or fix who owns it), or its `OTHER_REASON` for a cause that is not a permission. Nothing new is imported from `repo_shape.py` (#370, #610), and `scripts/repo_shape.py` is untouched. bootstrap.py's `_is_a` takes `link=True` for the two `lexists` sites, which ask with `lstat`. - **Refusals.** `yaml-unreadable`, the id `load_yaml` and `validate-pins.py` already give such a file, for `project.yaml` in both files and for the referent's pin file. One new id, `validator-unreadable`, for a validator bootstrap cannot reach or open. Each remedy says what the file is read for, then the cure, then `Then run the same command again.` - **A referent's pin file is a refusal, not a finding.** The check cannot run, so the run is refused, exit 2, as `validate-pins.py` refuses the same file. - **Steps (a) and (c) still never fail the run.** A leg or a register this user cannot read is a line in its step, and the step goes on. In a normal run `validate-pins.py` refuses such a leg (`pin-submodule-unreadable`), so the run ends `bootstrap REFUSED: pins (exit 2)`. Under `--skip-validators` the run ends `bootstrap ok`, as it did for a leg NOT INITIALIZED. - **A register whose `stat` is refused is kept, and not said to be there.** Whether one is there cannot be told, so step (c) does not say the degrade line, and heads it `register, if one is there: <leg>/governance/review-authority/register.yaml` (RV958's N1). A register whose `stat` works and whose open is refused keeps `register:`. - **The open as well as the stat.** A validator is opened, and closed unread, before any runs (RV958's S2). A `project.yaml`, the naming policy (RV958's S1) or a register this user cannot open is read off the refusal's `__cause__`, as `validate-pins.py`'s `_load` reads it (#712), so the two causes say one line. Every other `load_yaml` refusal keeps its words; `tests/test_manifest_validator.py`'s not-UTF-8 case, which pins the absolute path, passes. - **Every path is spelled.** bootstrap.py names each through `spelled` (#391); validate-manifest.py's `_unreadable_file` through a local `_shown`, validate-pins.py's (RV958's N3). A path not under the root (a leg's absolute `path:`, a `--policy` given elsewhere) is named whole. - **Readable paths print what they printed**, which lane 1's slice A (`fix/430-a-bump-leg-unsearchable`) relies on when it runs `validate-manifest.py`. Both files are pinned copies (`COPIED_VERBATIM`), so each project takes one `upstream-changed` row for each. No `--help` text changes, so `docs/cli.md` is unchanged. `AGENTS.md`, `templates/assembly-root/AGENTS-shape.md`, the template README and README's degrade rule stay true. `tests/test_assembly_root_unreadable_paths.py`, 71 tests (46 in round 0, 25 added in round 1), on `conftest.scaffolded` through the `project` fixture. - Each refusal is asked twice and must say the same, exactly, stdout and stderr: as a project runs the tool, with the mode taken away and put back in a `finally` (`mode_of`; skipped on Windows and as root), and in-process under `refused_stat` from `tests/test_bump_leg_unopenable.py`, which answers as 3.14 does on any Python. `failing` from `tests/test_validate_pins_named_and_spelled.py` gives an I/O error from a `stat`, an open or a read, whose advice is `OTHER_REASON`, and a refused `lstat`. - Round 1 adds: the naming policy (mode 000 with no `--policy`; an I/O error; a `--policy` outside the root whose name holds an ESC); a validator at mode 000 and one whose open fails; a register under an absolute leg path, readable and refused; a leg in a folder at 000 whose name holds an ESC, inside and outside the root; a leg path holding a NUL; the relative branch of validate-manifest.py's spelling; and, for the survivors RV958 named, an I/O-error row at every site that reads the cause, a directory where `project.yaml`, a validator or a register goes, and anything at a leg's name for `_found`. - The controls: a referent's pin file that is a dangling link (ENOENT) or a link to itself (ELOOP) is still `naming-referent-missing`; a validator that is not there, or a directory in its place, is still `SKIPPED`; a directory where `project.yaml` goes is still `manifest-missing` to the gate and no manifest to `bootstrap.py`; a directory where a register goes is no register. - The errno set: each file's `_is_a` answers False to the seven absent causes and to a name no file can have (a NUL, a lone surrogate), and raises EACCES, EPERM, EIO and ENAMETOOLONG. **At origin/main** (`ebbd9fa4`; both scripts as main has them; the new file kept): 64 of the 71 do not pass (38 fail, and 26 error at setup because main has no `_is_a`). The 7 that pass there by design are the controls: `test_a_referent_pin_that_is_not_there_is_still_missing[dangling-link]` and `[link-to-itself]`, `test_a_validator_that_is_not_there_is_still_skipped[taken-away]` and `[a-directory-in-its-place]`, `test_a_manifest_that_is_a_directory_is_no_manifest_to_either_tool`, `test_only_a_file_is_a_register_where_the_stat_works[a-directory]` and `test_a_leg_path_holding_a_nul_is_not_initialized`. **At `00a6f839`**, this branch's first merge of main (`2f5cabf5`), before round 1: 14 fail, 57 pass. The 14 are round 1's eleven new cells for its changes and the three cells whose step (c) header N1 changed. The 14 new cells that pass there do so by design: each pins behaviour the branch already had and kills a survivor (see Mutants). **Round 1:** 27 mutants, one string each, run against the new file at the round's code on the earlier merge (`b220336b`'s scripts; #948's merge changes none of the mutated lines, and each mutant's text is found once at the head): 27 killed, 0 survived. B* and V* are RV958's thirteen survivors (B2, B3, B5, B6, B7, B8, B10, B11, B15, V1, V2, V4, V5) as this branch reconstructs them, since RV958 did not publish its mutant texts, with B5b for the second branch of `_named`; K* are further guards of the same kind; R* are one or more for each change of the round. | Mutant | What it changes | Result | |---|---|---| | B2 | bootstrap.py `_is_a` without its `except ValueError` | killed: 3 test(s): test_a_leg_path_holding_a_nul_is_not_initialized, test_a_name_no_file_can_have_is_absent[bootstrap.py-a-lone-surrogate], test_a_name_no_file_can_have_is_absent[bootstrap.py-a-nul] | | V1 | validate-manifest.py `_is_a` without its `except ValueError` | killed: 2 test(s): test_a_name_no_file_can_have_is_absent[validate-manifest.py-a-lone-surrogate], test_a_name_no_file_can_have_is_absent[validate-manifest.py-a-nul] | | B5 | bootstrap.py `_named`, the relative branch unspelled | killed: 1 test(s): test_a_leg_in_a_folder_nobody_may_search_is_named_and_spelled[inside-the-root] | | B5b | bootstrap.py `_named`, the whole-path branch unspelled | killed: 1 test(s): test_a_leg_in_a_folder_nobody_may_search_is_named_and_spelled[outside-the-root] | | B6 | bootstrap.py `_named`, out of the root: `path.name` for the whole path | killed: 2 test(s): test_a_leg_in_a_folder_nobody_may_search_is_named_and_spelled[outside-the-root], test_a_register_under_a_leg_path_outside_the_root_is_read | | V5 | validate-manifest.py `_unreadable_file`, out of the root: `path.name` | killed: 1 test(s): test_a_naming_policy_the_gate_cannot_open_is_named_on_every_python[given-outside-the-root-with-an-esc] | | B7 | bootstrap.py `_is_file` takes only `PermissionError` | killed: 2 test(s): test_a_manifest_bootstrap_cannot_stat_or_open_is_the_same_refusal[stat-io-error], test_a_validator_whose_stat_is_refused_stops_step_b_before_any_runs[stat-io-error] | | B8 | bootstrap.py `_a_register` takes only `PermissionError` | killed: 1 test(s): test_a_register_whose_stat_is_refused_is_not_dropped[io-error] | | B10 | bootstrap.py `_load_manifest` maps only a `PermissionError` cause | killed: 1 test(s): test_a_manifest_bootstrap_cannot_stat_or_open_is_the_same_refusal[open-io-error] | | B11 | bootstrap.py `_load_register_rows` names only a `PermissionError` cause | killed: 1 test(s): test_a_register_that_cannot_be_opened_claims_nothing_on_every_python | | V4 | validate-manifest.py `_load` maps only a `PermissionError` cause | killed: 1 test(s): test_a_naming_policy_the_gate_cannot_open_is_named_on_every_python[io-error] | | B3 | bootstrap.py `_is_file` takes anything for a file | killed: 2 test(s): test_a_manifest_that_is_a_directory_is_no_manifest_to_either_tool, test_a_validator_that_is_not_there_is_still_skipped[a-directory-in-its-place] | | B15 | bootstrap.py `_a_register` takes anything for a register | killed: 1 test(s): test_only_a_file_is_a_register_where_the_stat_works[a-directory] | | V2 | validate-manifest.py `_is_file` takes anything for a file | killed: 1 test(s): test_a_manifest_that_is_a_directory_is_no_manifest_to_either_tool | | K1 | bootstrap.py `_is_a` ignores `kind` | killed: 3 test(s): test_a_manifest_that_is_a_directory_is_no_manifest_to_either_tool, test_a_validator_that_is_not_there_is_still_skipped[a-directory-in-its-place], test_only_a_file_is_a_register_where_the_stat_works[a-directory] | | K2 | validate-manifest.py `_is_a` ignores `kind` | killed: 1 test(s): test_a_manifest_that_is_a_directory_is_no_manifest_to_either_tool | | K3 | bootstrap.py `_found` asks for a regular file | killed: 2 test(s): test_anything_at_a_name_in_a_leg_is_found[a-directory], test_anything_at_a_name_in_a_leg_is_found[a-link-to-nothing-asked-of-the-name] | | K4 | bootstrap.py `_found` drops `link` | killed: 3 test(s): test_a_marker_whose_lstat_is_refused_keeps_the_leg_on_every_python[a-merge], test_a_marker_whose_lstat_is_refused_keeps_the_leg_on_every_python[a-sequence], test_anything_at_a_name_in_a_leg_is_found[a-link-to-nothing-asked-of-the-name] | | K5 | bootstrap.py `_found` takes only `PermissionError` | killed: 1 test(s): test_a_leg_whose_stat_is_refused_is_not_uninitialized[io-error] | | R1 | M1: the header back to `relative_to(root)` | killed: 2 test(s): test_a_leg_in_a_folder_nobody_may_search_is_named_and_spelled[outside-the-root], test_a_register_under_a_leg_path_outside_the_root_is_read | | R2 | N1: a refused `stat` headed `register` again | killed: 6 test(s): test_a_leg_bootstrap_cannot_search_is_left_as_it_is, test_a_leg_in_a_folder_nobody_may_search_is_named_and_spelled[inside-the-root], test_a_leg_in_a_folder_nobody_may_search_is_named_and_spelled[outside-the-root], test_a_register_bootstrap_cannot_read_is_named_not_absent[a-folder-nobody-may-enter], test_a_register_whose_stat_is_refused_is_not_dropped[io-error], test_a_register_whose_stat_is_refused_is_not_dropped[permission] | | R3 | S2: `_a_validator` opens nothing | killed: 2 test(s): test_a_validator_at_mode_000_is_refused_before_any_runs, test_a_validator_whose_stat_is_refused_stops_step_b_before_any_runs[open-io-error] | | R4 | S2: `_a_validator` takes only `PermissionError` | killed: 1 test(s): test_a_validator_whose_stat_is_refused_stops_step_b_before_any_runs[open-io-error] | | R5 | S1: the policy back to `NamingPolicy.load(policy_path)` | killed: 3 test(s): test_a_naming_policy_the_gate_cannot_open_is_named_on_every_python[given-outside-the-root-with-an-esc], test_a_naming_policy_the_gate_cannot_open_is_named_on_every_python[io-error], test_a_naming_policy_the_gate_cannot_reach_is_refused_by_name | | R6 | N3: `_shown` returns the name as it is | killed: 2 test(s): test_a_name_that_does_not_print_is_spelled_under_the_root_too, test_a_naming_policy_the_gate_cannot_open_is_named_on_every_python[given-outside-the-root-with-an-esc] | | R7 | N3: the relative branch unspelled | killed: 1 test(s): test_a_name_that_does_not_print_is_spelled_under_the_root_too | | R8 | N3: the whole-path branch unspelled | killed: 1 test(s): test_a_naming_policy_the_gate_cannot_open_is_named_on_every_python[given-outside-the-root-with-an-esc] | **Round 0:** 18 mutants, one string each, run against the new file at `df1bb186` (the move commit after it changes no line). All were killed; none survived. | Mutant | Caught by | |---|---| | M1 validate-manifest.py `:1085`: `_is_file` back to `manifest_path.is_file()` | killed: 2 test(s): test_a_manifest_the_gate_cannot_reach_or_open_is_refused_by_name[behind-a-folder-nobody-may-enter], test_a_manifest_whose_stat_is_refused_is_the_same_refusal | | M2 validate-manifest.py: `_load` back to `load_yaml` | killed: 1 test(s): test_a_manifest_the_gate_cannot_reach_or_open_is_refused_by_name[at-mode-000] | | M3 validate-manifest.py `:406`: `_is_file` back to `pin_path.is_file()` | killed: 4 test(s): test_a_contracts_folder_the_gate_cannot_search_names_the_pin, test_a_referent_pin_the_gate_cannot_reach_is_refused_not_missing, test_a_referent_pin_whose_stat_is_refused_is_refused_on_every_python[io-error], test_a_referent_pin_whose_stat_is_refused_is_refused_on_every_python[permission] | | M4 validate-manifest.py: `_findings` back outside `main`'s `try` | killed: 4 test(s): test_a_contracts_folder_the_gate_cannot_search_names_the_pin, test_a_referent_pin_the_gate_cannot_reach_is_refused_not_missing, test_a_referent_pin_whose_stat_is_refused_is_refused_on_every_python[io-error], test_a_referent_pin_whose_stat_is_refused_is_refused_on_every_python[permission] | | M5 bootstrap.py `:1328`: `_is_file` back to `manifest_path.is_file()` | killed: 2 test(s): test_a_manifest_bootstrap_cannot_reach_or_open_is_refused_first[behind-a-folder-nobody-may-enter], test_a_manifest_bootstrap_cannot_stat_is_the_same_refusal | | M6 bootstrap.py: `_load_manifest` back to `load_yaml` | killed: 1 test(s): test_a_manifest_bootstrap_cannot_reach_or_open_is_refused_first[at-mode-000] | | M7 bootstrap.py `:1409`: the look-ahead back to `(root / argv_[0]).is_file()` | killed: 2 test(s): test_a_scripts_folder_bootstrap_cannot_search_is_refused_not_skipped, test_a_validator_whose_stat_is_refused_stops_step_b_before_any_runs | | M8 bootstrap.py `:1056`: `_found` back to `(sub / ".git").exists()` | killed: 3 test(s): test_a_leg_bootstrap_cannot_search_is_left_as_it_is, test_a_leg_whose_stat_is_refused_is_not_uninitialized[io-error], test_a_leg_whose_stat_is_refused_is_not_uninitialized[permission] | | M9 bootstrap.py `:717`: `_found(..., link=True)` back to `os.path.lexists` | killed: 1 test(s): test_a_marker_whose_lstat_is_refused_keeps_the_leg_on_every_python[a-merge] | | M10 bootstrap.py `:750`: the same for `sequencer/todo` | killed: 2 test(s): test_a_marker_whose_lstat_is_refused_keeps_the_leg_on_every_python[a-sequence], test_an_operation_bootstrap_cannot_see_keeps_the_leg_where_it_is | | M11 bootstrap.py `:1191`: `_a_register` back to `c.is_file()` | killed: 3 test(s): test_a_leg_bootstrap_cannot_search_is_left_as_it_is, test_a_register_bootstrap_cannot_read_is_named_not_absent[a-folder-nobody-may-enter], test_a_register_whose_stat_is_refused_is_not_dropped | | M12 bootstrap.py: a register's open refusal said as before (`unread = False`) | killed: 3 test(s): test_a_leg_bootstrap_cannot_search_is_left_as_it_is, test_a_register_bootstrap_cannot_read_is_named_not_absent[a-folder-nobody-may-enter], test_a_register_bootstrap_cannot_read_is_named_not_absent[at-mode-000] | | M13 validate-manifest.py: ELOOP dropped from `_ABSENT_ERRNOS` | killed: 2 test(s): test_a_name_that_is_not_there_is_absent[validate-manifest.py-ELOOP], test_a_referent_pin_that_is_not_there_is_still_missing[link-to-itself] | | M14 validate-manifest.py: EACCES added to `_ABSENT_ERRNOS` | killed: 6 test(s): test_a_contracts_folder_the_gate_cannot_search_names_the_pin, test_a_manifest_the_gate_cannot_reach_or_open_is_refused_by_name[behind-a-folder-nobody-may-enter], test_a_manifest_whose_stat_is_refused_is_the_same_refusal, test_a_referent_pin_the_gate_cannot_reach_is_refused_not_missing, test_a_referent_pin_whose_stat_is_refused_is_refused_on_every_python[permission], test_every_other_refusal_is_raised[validate-manifest.py-EACCES] | | M15 bootstrap.py: ELOOP dropped from `_ABSENT_ERRNOS` | killed: 1 test(s): test_a_name_that_is_not_there_is_absent[bootstrap.py-ELOOP] | | M16 bootstrap.py: EACCES added to `_ABSENT_ERRNOS` | killed: 12 test(s): test_a_leg_bootstrap_cannot_search_is_left_as_it_is, test_a_leg_whose_stat_is_refused_is_not_uninitialized[permission], test_a_manifest_bootstrap_cannot_reach_or_open_is_refused_first[behind-a-folder-nobody-may-enter], test_a_manifest_bootstrap_cannot_stat_is_the_same_refusal, test_a_marker_whose_lstat_is_refused_keeps_the_leg_on_every_python[a-merge], test_a_marker_whose_lstat_is_refused_keeps_the_leg_on_every_python[a-sequence], test_a_register_bootstrap_cannot_read_is_named_not_absent[a-folder-nobody-may-enter], test_a_register_whose_stat_is_refused_is_not_dropped, test_a_scripts_folder_bootstrap_cannot_search_is_refused_not_skipped, test_a_validator_whose_stat_is_refused_stops_step_b_before_any_runs, test_an_operation_bootstrap_cannot_see_keeps_the_leg_where_it_is, test_every_other_refusal_is_raised[bootstrap.py-EACCES] | | M17 bootstrap.py: `_cure` gives the `chmod` advice for every cause | killed: 1 test(s): test_a_leg_whose_stat_is_refused_is_not_uninitialized[io-error] | | M18 validate-manifest.py: the `chmod` advice for every cause | killed: 1 test(s): test_a_referent_pin_whose_stat_is_refused_is_refused_on_every_python[io-error] | Each run is `run9s.sh` with `SHARDS=3` (three `run9.sh` processes: the gh stub first on `PATH`, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, a blank git identity), one run at a time, at head `b420171f`. - **git 2.43.0, the first brief's whole list, 134 files: 9656 passed, 25 skipped (longest shard 40 min), rc 0.** 2026-10-19T11:37:42Z to 2026-10-19T12:17:59Z. The list is every test file that names the assembly root's `bootstrap.py` or `validate-manifest.py`, or runs a scaffolded root's `make bootstrap` or its validators, with #948's new `test_bootstrap_casefold_twin.py`, and `test_pinned_copy_text.py`, `test_bom_reads.py`, `test_validator_child_encoding.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py` and `test_cli_reference.py`. The 25 skips: 8 of `test_leg_path_yaml_scalars.py` (empty parameter sets, and the templates read the value back); 6 of `test_doctor_legs_members_next.py` (git 2.43 dies on a `shallow` of -2**31, and crashes on `submodule init`; CI's 2.55 does neither); 3 reftable cases of `test_bootstrap_ref_store.py` (git 2.45 and later); 3 NTFS junction cases of `test_update_shape_add_symlink.py`, 3 PowerShell lines of `test_shape_doctor.py` and 1 of `test_stdout_encoding.py` (Windows only); 1 of `test_bump_leg.py` (a disk that ignores case). - **git 2.47.1 (a build first on `PATH`), 24 files: 1764 passed (longest shard 4 min), rc 0.** 2026-10-18T20:07:50Z to 2026-10-18T20:11:58Z. Files: `test_agents_md_codes.py`, `test_assembly_root_unreadable_paths.py`, `test_bom_reads.py`, `test_bootstrap_after_bump.py`, `test_bootstrap_branch_bytes.py`, `test_bootstrap_casefold_twin.py`, `test_bootstrap_leg_moves.py`, `test_bootstrap_non_utf8_branch.py`, `test_bootstrap_pasteable_lines.py`, `test_bootstrap_placement_texts.py`, `test_bootstrap_ref_store.py`, `test_bootstrap_refusal_exit.py`, `test_bootstrap_spelling.py`, `test_bootstrap_upstream_notice.py`, `test_bootstrap_without_git.py`, `test_bump_leg_unopenable.py`, `test_bump_leg_unreadable.py`, `test_cli_reference.py`, `test_manifest_validator.py`, `test_naming_policy.py`, `test_pinned_copy_text.py`, `test_repo_hygiene.py`, `test_validate_pins_named_and_spelled.py`, `test_validator_child_encoding.py`. - A first 2.43.0 run of the same list at the same head, on 2026-10-18, was stopped from outside at 20:31Z (a signal; its shards had reached 52%, 29% and 41% with no failure); the run above is its re-run. - Before #948 landed, the same list less `test_bootstrap_casefold_twin.py` (23 files), on git 2.47.1 at the round's commit on the earlier merge (`b220336b`): 1713 passed, rc 0. <details><summary>The 134 files of the git 2.43.0 run</summary> `test_adopt_attributes_eol_block.py`, `test_adopt_clone_template.py`, `test_adopt_e2e.py`, `test_adopt_git_reads.py`, `test_adopt_ignored_shape_files.py`, `test_adopt_manifest_values.py`, `test_adopt_mount_ignored.py`, `test_adopt_nested_attributes.py`, `test_adopt_origin_repository.py`, `test_adopt_pins.py`, `test_adopt_plan_yaml_quoting.py`, `test_adopt_reencoded_files.py`, `test_adopt_relative_submodule_url.py`, `test_adopt_root_manifest.py`, `test_adopt_root_symlinks.py`, `test_adopt_shape_pin_and_work_dir.py`, `test_adopt_spec_only.py`, `test_adopt_submodules.py`, `test_adopt_verify.py`, `test_agents_md_codes.py`, `test_assembly_root_unreadable_paths.py`, `test_bom_reads.py`, `test_bootstrap_after_bump.py`, `test_bootstrap_branch_bytes.py`, `test_bootstrap_casefold_twin.py`, `test_bootstrap_leg_moves.py`, `test_bootstrap_non_utf8_branch.py`, `test_bootstrap_pasteable_lines.py`, `test_bootstrap_placement_texts.py`, `test_bootstrap_ref_store.py`, `test_bootstrap_refusal_exit.py`, `test_bootstrap_spelling.py`, `test_bootstrap_upstream_notice.py`, `test_bootstrap_without_git.py`, `test_bump_leg.py`, `test_bump_leg_branch_and_push_line.py`, `test_bump_leg_dry_run_tracking.py`, `test_bump_leg_dry_run_truth.py`, `test_bump_leg_paste_lines.py`, `test_bump_leg_rollback.py`, `test_bump_leg_unopenable.py`, `test_bump_leg_unreadable.py`, `test_chain_remedy_callers.py`, `test_cli_reference.py`, `test_doctor_gitlink_repairs.py`, `test_doctor_gitmodules_not_utf8.py`, `test_doctor_legs_members_next.py`, `test_doctor_machine_row.py`, `test_doctor_members_spelled.py`, `test_doctor_next_is_a_command.py`, `test_doctor_no_lazy_fetch.py`, `test_doctor_non_ascii_paths.py`, `test_doctor_partial_clone.py`, `test_doctor_paste_through_git.py`, `test_doctor_pin_restore_merges.py`, `test_doctor_plan_overrides.py`, `test_doctor_recorded_answers.py`, `test_doctor_unfinished_merge.py`, `test_doctor_unreadable_pin.py`, `test_doctor_validator_rows.py`, `test_doctor_validator_site.py`, `test_doctor_validator_verdicts.py`, `test_family.py`, `test_family_bootstrap_gate_order.py`, `test_family_bump_remove_rollback.py`, `test_family_gitmodules_values.py`, `test_family_head_lost_gitmodules.py`, `test_family_holder_bootstrap_texts.py`, `test_family_makefile_siblings.py`, `test_family_member_name.py`, `test_family_members_not_a_list.py`, `test_family_siblings_placement.py`, `test_git_output_decoding.py`, `test_leg_path_yaml_scalars.py`, `test_legs_not_a_list.py`, `test_legs_readers_not_a_list.py`, `test_manifest_validator.py`, `test_naming_policy.py`, `test_non_list_project_fields.py`, `test_non_utf8_oserror_sites.py`, `test_park_resume_targets.py`, `test_pin_source_env_collisions.py`, `test_pinned_copy_text.py`, `test_pins_child_kind_and_siblings_root_spelling.py`, `test_push_branch_refspec.py`, `test_reference_default.py`, `test_repo_hygiene.py`, `test_scaffold_e2e.py`, `test_scaffold_free_text_values.py`, `test_scaffold_ignored_shape_files.py`, `test_scaffold_leg_path_collisions.py`, `test_scaffold_manifest_values.py`, `test_scaffold_mount_ignored.py`, `test_scaffold_pin_and_reuse.py`, `test_scaffold_referent_chain_links.py`, `test_setup_by_hand_commands.py`, `test_setup_project_py.py`, `test_setup_sh.py`, `test_shape_doctor.py`, `test_siblings_family_name.py`, `test_siblings_inputs.py`, `test_stdout_encoding.py`, `test_template_quoted_values.py`, `test_template_text_placeholders.py`, `test_template_values_single_pass.py`, `test_update_shape.py`, `test_update_shape_add.py`, `test_update_shape_add_ignored.py`, `test_update_shape_add_not_a_directory.py`, `test_update_shape_add_symlink.py`, `test_update_shape_apply_lines_and_headers.py`, `test_update_shape_branch_any_directory.py`, `test_update_shape_hardlink.py`, `test_update_shape_header_comments.py`, `test_update_shape_kept_helper.py`, `test_update_shape_landing.py`, `test_update_shape_merged_away.py`, `test_update_shape_messages.py`, `test_update_shape_next_quoting.py`, `test_update_shape_partial_rollback.py`, `test_update_shape_paste_lines.py`, `test_update_shape_pr_base.py`, `test_update_shape_rewrite_symlink.py`, `test_validate_family_branch_step.py`, `test_validate_family_member_path_glob.py`, `test_validate_family_remedies_typed.py`, `test_validate_family_typed_states.py`, `test_validate_pins_lockstep_lines.py`, `test_validate_pins_named_and_spelled.py`, `test_validate_workflow_gitmodules_urls.py`, `test_validator_child_encoding.py`, `test_windows_paths.py`, `test_yaml_indicator_openers.py`, `test_yaml_reader_spaces.py` </details> The branch merged main twice. Most recently, `cf8d5667` merges origin/main `2a78ae1a` (#984, landed) into round 1's head `b420171f`: its one conflict, the import block of `bootstrap.py`, keeps #984's `import unicodedata` and this branch's `from collections.abc import Callable, Iterator`, and the branch's own `_shown` in `validate-manifest.py` is deleted in favour of #981's `_shown(path)` (the account of that merge is on the PR). Before that, `bb58c441` merges origin/main `ebbd9fa4` (#948, #896's twin refusals, landed) into `0ef875c0` with no conflict (`bootstrap.py` auto-merged), and the round's commit `b420171f` sits on it. The round was first written on an earlier merge of main (`00a6f839`, `2f5cabf5`); that merge was replaced, unpushed, once #948 landed. The round's commit was reapplied unchanged (the same `git patch-id`). Its tree is the one `git merge-tree` predicted for the earlier round commit with `ebbd9fa4` (`9f4c2991`). `git merge-tree --write-tree` of head `b420171f`, each ref fetched with `gc.auto=0` and `maintenance.auto=false` just before: - origin/main `c7a3ff16`, re-fetched on 2026-10-19 after the head was pushed: clean as text (tree `977f0980`), and NOT merged here: the landing pilot refreshes the head under the fast-refresh ruling. Since `ebbd9fa4`, main's #981 (`ca6a0b40`) changes `validate-manifest.py`, and its own `_shown(path)` (`text = str(path)`, then `isprintable` or `repr`) lands beside this branch's `_shown(typed)`: the merged file defines `_shown` twice. The two give the same answer for every string this branch passes, the later definition wins, and the new file passes on the merged tree (71 passed, `$Q/v118/w430d/r1/m981.log`), but the refresh should delete this branch's `_shown` and keep #981's. Main's other changes in the files near this slice are to `validate-pins.py` and `tests/conftest.py`, which this branch does not touch. - #948 at `45415296`: clean (tree `9f4c2991`, the head's own). - #973 at `b2887cf5` (`fix/961-paste-words`): clean. #973 does not yet change the NOT INITIALIZED line; when it does, the new file's `not_initialized` helper is the one place to follow it. - Each clean merge's `bootstrap.py` parses, with no top-level name defined twice. - #962's `wip/l5-962-create-twins-20261018` at `414e9e0a`: conflicts in `bootstrap.py`, 8 hunks. 7 of them are with main alone: #962 is stacked on #948's head before #948 was squashed, and `git merge-tree` of origin/main with it gives the same 7. The 8th is this branch's only one, the import lines: `from collections.abc import Callable, Iterator` here against #962's `import unicodedata` and `from collections.abc import Iterator`. Taking both resolves it. - No duplicated block: the new file shares no run of five code lines with any test file but an import block, reuses `as_a_descendant_by_chain`, `link_tree`, `refused_stat`, `failing`, `os_error`, `mode_of`, `CANNOT_RUN_AS_ROOT` and the cure constants instead of copying them, and puts its own repeated lines in helpers (`refused`, `left_as_it_is`, `nothing_claimed`, `not_initialized`, `refusing`, `leg_path`, `up_to_step_b`, `skipping_validators`, `a_register`). - No assert joins conditions with `and`. - Cognitive complexity, counted by hand: `checkout_tracking_branch` 13, unchanged; `_load_register_rows` 11; `_in_progress` 8; `_run_neutral_validators` 7; `_report_one_register` 6; `_register_paths` 4; `read_authority` 4; `validate-manifest.py`'s `main` 9 and `_load` 3; every other new function 2 or less. - Every added line is within 79 columns. Round 0's first two candidates (the naming policy named by its absolute path, and a validator the child cannot open) are fixed in round 1, on RV958's S1 and S2. - **The assembly root's `validate-repository-naming.py` names a naming policy it cannot read by its absolute path, with no remedy.** Measured with `contracts/` at 000 in a full `bootstrap.py` run with round 1's change: `REFUSED yaml-unreadable: <ROOT>/contracts/repository-naming.yaml: cannot be read (Permission denied)`, while `validate-manifest.py`'s refusal just after it names `contracts/repository-naming.yaml` with the remedy. Not this slice's file. - **`OTHER_REASON` tells a validator's I/O error to "take away one nobody wants".** A validator taken away is `SKIPPED`. #934 gives bump-leg.py a `git checkout --` line instead. These are validate-pins.py's words, kept as the plan rule asks. - **The cure's "(and on every directory above it inside the root)"** reads oddly for a folder outside the root (an absolute leg path, a `--policy` given elsewhere). validate-pins.py's words, as RV958's N2 found for a symlinked `project.yaml`. - **Seen in the code, not measured:** validate-manifest.py prints `project.yaml`'s absolute path raw in `manifest-missing`, `manifest-unreadable` and `<n> finding(s) in <path>`, where `validate-pins.py` and `bootstrap.py` spell every path they print (#391, #712). Part of #430 Review: RV958, lane 4's independent reviewer, not the writer (w430d): READY at `7e87e05f` (issuecomment-6278103469), the reviewed head, after its READY on round 1 at `cf8d5667` (issuecomment-6271860975); the delta was a merge of main and one test-only commit for CI. CI (Brett Heap's ruling 27): H0 is the head 7c06c57 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 051176c; and the local gate passed on its predicted squash tree 194700b over main 051176c: the adopt_reencoded_files, assembly_root_unreadable_paths, repo_hygiene, agents_md_codes, cli_reference sets, 294 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green". Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Closes #909.
scripts/repo_shape.py,scripts/validate-repository-naming.pyandtemplates/assembly-root/scripts/validate-manifest.pyare pinned copies, so this change re-pins every project once.scripts/repo_shape.pyis a family holder's pinned copy too, so every family holder re-pins it once as well.What changed, by section
validate-manifest.pyasks no reasons (U172)_chain_and_linksnow asks the lookup what it tried (tried=) and why a tree it reached could not be read (with_reasons=), as the naming gate asks. Both sit behind the guardremedies=already has (_library_takes_remedies): every library that takesremedies=(v1.1.0 and later) also takestried=andwith_reasons=. A v1.0.x library still gets the plain call and its own words.SHAPE_PIN_SOURCE_<LINK>, or a checkout beside the root (anything at that path), is named in thedeclared-unverifiedWARNING with the lookup's reason. Only a source that is not there is offered. Lane 2's three measurements now read differently:openXdox/beside the root:(the openXdox beside this root, <path>, could not answer: it holds no project.yaml; SHAPE_PIN_SOURCE_OPENXDOX would let it be read);(SHAPE_PIN_SOURCE_OPENXDOX, <path>, could not answer: no such file or directory; a checkout of openXdox beside this root would let it be read);--link-sourceto callers that do not take it (validate-manifest.py, setup's step (4)), and the naming gate prints--link-sourcevalues raw #661 gave, unchanged._link_sources_from_argssplits every--link-sourcevalue at its first=and nothing reports a value no chain link used, so a link tree under a directory nameda=b, or behind a mistyped key, is silently skipped (declared-unverified, exit 0), andrepo_shape.link_manifest_pathlets a source this user cannot enter (keyed,SHAPE_PIN_SOURCE_<LINK>or a sibling checkout) end the naming gate in aPermissionErrortraceback before Python 3.14 #338)._link_remediestakes the lookup'striedand is written onto the answer'sremediesonce the lookup returns.tests/test_pinned_copy_text.py::test_no_pinned_script_imports_what_an_older_library_lacksholds the rule that a root's copy imports nothing a kept library may lack. So validate-manifest spells_shown(asvalidate-pins.pyand the naming gate already do) andSOURCE_ABSENTfor itself. The existing_shownparity test now covers validate-manifest, and a new test holdsSOURCE_ABSENTequal torepo_shape.LINK_SOURCE_ABSENT.tests/test_naming_policy.py::test_the_projects_own_manifest_gate_keeps_its_warning_for_a_tree_it_reachedheld scripts/validate-repository-naming.py:_link_sources_from_argssplits every--link-sourcevalue at its first=and nothing reports a value no chain link used, so a link tree under a directory nameda=b, or behind a mistyped key, is silently skipped (declared-unverified, exit 0), andrepo_shape.link_manifest_pathlets a source this user cannot enter (keyed,SHAPE_PIN_SOURCE_<LINK>or a sibling checkout) end the naming gate in aPermissionErrortraceback before Python 3.14 #338's scoping ("this change reaches the scaffold alone"). validate-manifest.py, repo_shape.py and scaffold-project.py: a chain source that is there and cannot answer is still offered, oneSHAPE_PIN_SOURCE_*can answer for two chains, and a Kelvin-sign link is recorded as typed #909's Direction supersedes that, so the test is renamed..._says_why_a_tree_it_reached_was_unreadand asserts the reason.The
SHAPE_PIN_SOURCE_*guard is asked per chain (U113, U145)link_pins_from_treestakes a new keyword,run_products: every product the caller's run reads. It asks scripts/repo_shape.py:pin_source_env_nameturns every character outside[A-Z0-9_]into_, so the neutral productsopen-glass,open_glassandopen.glassshare oneSHAPE_PIN_SOURCE_OPEN_GLASS, and a project that pins two of them gets aneutral-pin-source-unreadableFINDING (exit 1) for whichever one the variable does not name #275's guard over those products as well as over the chain's links. A variable that two products of the run share is read for no chain. The answer is aLinkPinswhose newenv_sharedattribute names those links, and_chain_link_findingsoffers the variable to none of them.[*target.pins, *self.links]at:352. validate-manifest passes_run_products(pins, legs), built once in_findingsand handed down through_leg_findings. Each passes the keyword only where its library takes it (_library_takes("run_products")), by the skew rule.declared-unverifiedWARNING that does not offerSHAPE_PIN_SOURCE_OPENSS. Before, the gate saidFINDING codexQuill: the declared chain openss -> openQuill is broken (openss declares openInk, not openQuill), exit 1.pin_source_env_nameturns every character outside[A-Z0-9_]into_, so the neutral productsopen-glass,open_glassandopen.glassshare oneSHAPE_PIN_SOURCE_OPEN_GLASS, and a project that pins two of them gets aneutral-pin-source-unreadableFINDING (exit 1) for whichever one the variable does not name #275's own rule ("a variable two declared products share answers for NEITHER, wherever it is read") broken for a chain: in a project pinningopen+ sharp s andopenss, the chain beginning atopenssstill read the variable while the other pin sits on no chain. A test holds that row.--referent-chain's), not only the chains a classification consults. This matches what the gate'sLinkSources.linksalready holds and what validate-manifest reads, so the two gatesmake validateruns give one answer. A test holds a spec leg's chain sharing the variable.pin_source_env_shared(the gate does not import it, and its import block is setup-project.py and validate-repository-naming.py: step (4) reads and names what step (5) does,--link-remedyreads a link holding=, strips its words and notes an unused one, and the Windows hand clone passes-c core.autocrlf=false(#907, #926) #949's neighbour). The guard is asked at each lookup over the same products, so every chain of a run gets the same answer.run_products. It reads one chain. Its first link is a--pin, and its later links areopen<Product>names, ASCII letters and digits, which share a variable with a--pinonly where they are the same pin file._refuse_colliding_pinsalready refuses two--pins that share one.A Kelvin-sign link is recorded as typed (U114, U146)
referent_chain_links(raw, pins=())spells a first link that equals a--pinunderpin_product_keyin that pin's spelling, keeping an owner typed before it.--pin openKdox@<sha> --referent-chain 'open<U+212A>dox,openInk'now plans and recordsopenKdox -> openInk. A first link that differs from its pin by ASCII case alone is spelled as the pin too._declarationpasses the pins. That is the reading that is checked, printed and recorded. The two readings before it (refuse_an_unread_pin_source, which asks only whether there is a chain, and_refuse_malformed_chain_links, which judges the middle links) keep the typed spelling, so those refusals quote the flag as typed.pin_product_keyis imported insidereferent_chain_links, beside its one use, so the module's import block stays untouched for scaffold-project.py,family.py initand shape_materialize.py: the plan names the tracking branch, and on disk the visibility; an unread root or reused leg is said to be unread, the root before the plan; every holder gets the members' NOTE (#913) #969, scaffold-project.py: a first scaffold's pushes and a--reuse-empty-repore-run's pushes and seed fetch no longer ask at the terminal: a push git has no login for fails naming the fix, and a leg git reaches over ssh is refused with ssh's reason (#915) #971 and scaffold-project.py and family.py init: a flag passed empty is refused by name, a passed --id is told what an id may hold once the name passes, and an empty folder this user cannot list is told the permission (#911, #930) #972, which edit this file.Where the diff goes past the brief's line ranges
validate-manifest.py: besides:178-250(_chain_and_links,_link_remedies,_library_takes_remediesand the new helpers beside them),_leg_findingstakesrun_products(:832-866), and_findingsbuilds it once before its loop (:1067-1070). The run's products cannot be known inside_chain_and_links, which sees one leg. Lane 5's slice D (:86-115,:396-406,:1085-1101) is untouched, and the merge with the assembly root's validate-manifest.py and bootstrap.py: a path this user cannot read is named, never read as absent, so ascripts/nobody may search no longer SKIPs every validator intobootstrap ok(#430, slice D) #958 is clean (below).scaffold-project.py::1071-1075, and one line at:1164, where_declarationpasses the pins.validate-repository-naming.py: the call at:352, and a_library_takes(keyword)helper placed betweenTargetandLinkSources, away from setup-project.py and validate-repository-naming.py: step (4) reads and names what step (5) does,--link-remedyreads a link holding=, strips its words and notes an unused one, and the Windows hand clone passes-c core.autocrlf=false(#907, #926) #949's hunks.Out of scope
SHAPE_PIN_SOURCE_*can answer for two chains, and a Kelvin-sign link is recorded as typed #909 has no "Needs a ruling" or "Documented limit" section.link-source-unreadableWARNING, whichmake validateruns first.project.yamlas the ordinary case. U172 is validate-manifest's line.scripts/shape_materialize.py(scaffold_link_remedies) is not in this change's files and is unchanged. It cites_library_takes_remedies, which keeps its name.Tests
Each run used a stub
ghfirst on PATH (exit 97),GH_TOKEN=bogusand an emptyGH_CONFIG_DIR:/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-<run> -o tmp_path_retention_policy=failed <files>, one process at a time. Every run but the doctor's was on this head.tests/test_chain_pin_sources.py tests/test_chain_remedy_callers.py tests/test_chain_link_fold.py tests/test_link_source_chain_defaults.py tests/test_pin_source_env_collisions.py tests/test_pin_source_unreadable.py tests/test_naming_policy.py tests/test_scaffold_referent_chain_links.py tests/test_pinned_copy_text.py tests/test_manifest_validator.py:798 passed in 141.82s (0:02:21)tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_update_add_codes_documented.py tests/test_repo_hygiene.py tests/test_no_auto_maintenance.py:217 passed in 43.50stests/test_legs_not_a_list.py tests/test_non_list_project_fields.py tests/test_reader_refusals_named.py tests/test_yaml_flow_quotes.py tests/test_scaffold_pin_and_reuse.py tests/test_scaffold_e2e.py tests/test_template_text_placeholders.py tests/test_update_shape_kept_helper.py tests/test_doctor_validator_verdicts.py tests/test_setup_project_py.py:919 passed in 516.14s (0:08:36)tests/test_adopt_e2e.py tests/test_adopt_nested_attributes.py tests/test_adopt_pins.py tests/test_adopt_reencoded_files.py tests/test_adopt_spec_only.py tests/test_adopt_submodules.py:486 passed in 528.58s (0:08:48)tests/test_shape_doctor.py, run once, as the brief allows, on the pre-rebase commitcd4eaba7:445 passed, 3 skipped in 714.12s (0:11:54). The rebase brought main's2f5cabf5, which touchesrepo_shape.pyonly intree_digest_from_ghand next tofind_repo_root, nowhere near the chain readers.Mutants
Each one breaks a fixed line, runs the named tests, and restores the file. All 21 were killed.
tried=triedtest_a_source_that_is_there_is_said_and_not_offered(4 rows)with_reasons=Truetest_a_manifest_beside_the_root_that_is_not_a_declaration_says_whytest_with_nothing_there_both_sources_are_offered_shown)test_a_variable_holding_a_control_character_is_spelledtest_a_source_that_is_there_is_said_and_not_offered(3 rows)test_a_source_that_is_there_is_said_and_not_offered(2 rows)test_the_manifest_gate_reads_a_shared_variable_for_no_chain(4 rows)_run_productswithout the pinstest_the_manifest_gate_reads_a_shared_variable_for_no_chain[pin-*]run_productsnever passedtest_the_manifest_gate_reads_a_shared_variable_for_no_chain[*-set]run_productspassed without the guardtest_both_copies_run_beside_a_library_with_no_run_productsremediestest_a_source_that_is_there_is_said_and_not_offered(4 rows)linksalone[set]/[unset], declared-pins, other-chain, manifest[*-set], library tests (7)_chain_link_findingsignoresenv_sharedtest_the_gate_reads_a_shared_variable_for_no_chain[set,unset]run_productstest_the_library_asks_the_guard_over_the_runLinkPinsbuilt withoutenv_sharedtest_the_library_asks_the_guard_over_the_runtarget.pinstest_a_variable_two_declared_pins_share_is_read_for_no_chaintest_a_variable_a_link_of_another_chain_shares_is_read_for_no_chaintest_a_first_link_that_is_a_pin_is_spelled_as_the_pin(3 rows), plan and record teststest_a_first_link_that_is_a_pin_is_spelled_as_the_pin[owner-kept]_declarationpasses no pinstest_the_plan_names_the_chain_in_the_pins_spelling,test_the_manifest_records_the_chain_in_the_pins_spellingpin_product_keytest_a_first_link_that_is_a_pin_is_spelled_as_the_pin(3 rows)Merges
git merge-tree --write-treeof this head with each:66177b62: clean. The head is one commit behind; main's newest commit touches onlytests/test_repo_hygiene.py.scripts/nobody may search no longer SKIPs every validator intobootstrap ok(#430, slice D) #958 (lane 5, slice D,0ef875c0, validate-manifest.py): clean--link-remedyreads a link holding=, strips its words and notes an unused one, and the Windows hand clone passes-c core.autocrlf=false(#907, #926) #949 (8c5d6051, validate-repository-naming.py): clean#ends the block as a comment (#917) #974 (W917,e63ca75c, repo_shape.py): clean0094731b, scaffold-project.py): clean--reuse-empty-repore-run's pushes and seed fetch no longer ask at the terminal: a push git has no login for fails naming the fix, and a leg git reaches over ssh is refused with ssh's reason (#915) #971 (W915,e40af32d, scaffold-project.py): cleanfamily.py initand shape_materialize.py: the plan names the tracking branch, and on disk the visibility; an unread root or reused leg is said to be unread, the root before the plan; every holder gets the members' NOTE (#913) #969 (W913,f3802d6f, scaffold-project.py): cleanFix round 1
On RV981's review (issuecomment-6254819059), head
499f7baa, one commit onac5b5307.open<U+212A>doxwas read throughSHAPE_PIN_SOURCE_OPEN_DOX. The Kelvin sign is upper case and outside[A-Z0-9_], sopin_source_env_nameturns it into_, and that is the variable ofopen-dox,open_doxandopen.dox. On main, that variable set to a tree declaringopenQuillrefused a chain that holds (chain-not-declared, exit 2), and the WARNING offered that variable. The link is the pinopenKdoxnow, soSHAPE_PIN_SOURCE_OPENKDOXis read for it, before--pin-source. U114 was therefore not only cosmetic. Two dry-run rows intests/test_chain_pin_sources.pyhold both directions:SHAPE_PIN_SOURCE_OPEN_DOXset to that tree: exit 0, no Kelvin sign in the output;SHAPE_PIN_SOURCE_OPENKDOXset to it:chain-not-declared.test_the_manifest_gate_reads_a_shared_variable_for_no_chainhas aqualified-chainrow: the spec leg's chain begins atopensoft/+open+ U+00DF. validate-manifest hands its products over as the manifest spells them, so the library'srepo_basenameonrun_productsis now held.SOURCE_ABSENT's comment says every tagged library hasLINK_SOURCE_ABSENTand the naming gate imports it. The local copy keeps validate-manifest's import block clear of the assembly root's validate-manifest.py and bootstrap.py: a path this user cannot read is named, never read as absent, so ascripts/nobody may search no longer SKIPs every validator intobootstrap ok(#430, slice D) #958.repo_shape.pyonce too._run_products' docstring says it counts the chain of a leg whoserepository:is bad or missing, which the gate's_project_chain_linksleaves out. Such a leg is a finding in both gates.Tests
Same command and stub
ghas above, plusGIT_SSH_COMMAND=false. All runs are on499f7baa.tests/test_chain_pin_sources.py(the one test file changed):32 passed in 8.54stests/test_chain_remedy_callers.py tests/test_chain_link_fold.py tests/test_link_source_chain_defaults.py tests/test_pin_source_env_collisions.py tests/test_pin_source_unreadable.py tests/test_naming_policy.py tests/test_scaffold_referent_chain_links.py tests/test_pinned_copy_text.py tests/test_manifest_validator.py):802 passed in 133.08s (0:02:13)tests/test_no_auto_maintenance.py:217 passed in 49.73sMutants
All are now killed:
repo_basenameonrun_productstest_the_manifest_gate_reads_a_shared_variable_for_no_chain[qualified-chain-set]test_the_kelvin_link_is_not_read_through_another_products_variable,test_the_kelvin_link_is_read_through_its_pins_variabletest_a_source_that_is_there_is_said_and_not_offered(3 rows)linksalone*-setrows, and the library testMerges
git merge-tree --write-treeof499f7baa:cf66102f: clean#ends the block as a comment (#917) #974e63ca75c: clean--link-remedyreads a link holding=, strips its words and notes an unused one, and the Windows hand clone passes-c core.autocrlf=false(#907, #926) #949ced3d7c2: clean--reuse-empty-repore-run's pushes and seed fetch no longer ask at the terminal: a push git has no login for fails naming the fix, and a leg git reaches over ssh is refused with ssh's reason (#915) #971155fcd2d: clean0094731b: cleanSummary by Sourcery
Improve chain-source diagnostics and scope pin-source reads per validation run while preserving declared-pin spelling in scaffolded chains.
New Features:
Bug Fixes:
Enhancements:
Tests:
Chores:
Lane: openreposhape-3 (openRepoShape-3)
🤖 Generated with Claude Code