Skip to content

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

Merged
brettheap merged 2 commits into
mainfrom
l3-909-chain-pin-sources
Oct 19, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Closes #909.

scripts/repo_shape.py, scripts/validate-repository-naming.py and templates/assembly-root/scripts/validate-manifest.py are pinned copies, so this change re-pins every project once. scripts/repo_shape.py is a family holder's pinned copy too, so every family holder re-pins it once as well.

What changed, by section

validate-manifest.py asks no reasons (U172)

The SHAPE_PIN_SOURCE_* guard is asked per chain (U113, U145)

A Kelvin-sign link is recorded as typed (U114, U146)

Where the diff goes past the brief's line ranges

Out of scope

Tests

Each run used a stub gh first on PATH (exit 97), GH_TOKEN=bogus and an empty GH_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.

  • New and core chain files: 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)
  • Docs guards: 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.50s
  • Other files that run the changed readers: tests/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)
  • Adopt files that run the naming gate: 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 commit cd4eaba7: 445 passed, 3 skipped in 714.12s (0:11:54). The rebase brought main's 2f5cabf5, which touches repo_shape.py only in tree_digest_from_gh and next to find_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.

# File Mutation Killed by
M1 validate-manifest drop tried=tried test_a_source_that_is_there_is_said_and_not_offered (4 rows)
M2 validate-manifest drop with_reasons=True test_a_manifest_beside_the_root_that_is_not_a_declaration_says_why
M3 validate-manifest an absent checkout counted as there test_with_nothing_there_both_sources_are_offered
M4 validate-manifest path not spelled (_shown) test_a_variable_holding_a_control_character_is_spelled
M5 validate-manifest a set variable still offered test_a_source_that_is_there_is_said_and_not_offered (3 rows)
M6 validate-manifest a checkout that is there still offered test_a_source_that_is_there_is_said_and_not_offered (2 rows)
M7 validate-manifest offer's guard over the chain alone test_the_manifest_gate_reads_a_shared_variable_for_no_chain (4 rows)
M8 validate-manifest _run_products without the pins test_the_manifest_gate_reads_a_shared_variable_for_no_chain[pin-*]
M9 validate-manifest run_products never passed test_the_manifest_gate_reads_a_shared_variable_for_no_chain[*-set]
M10 validate-manifest run_products passed without the guard test_both_copies_run_beside_a_library_with_no_run_products
M11 validate-manifest offer not written onto remedies test_a_source_that_is_there_is_said_and_not_offered (4 rows)
M12 repo_shape guard over links alone gate [set]/[unset], declared-pins, other-chain, manifest [*-set], library tests (7)
M13 repo_shape _chain_link_findings ignores env_shared test_the_gate_reads_a_shared_variable_for_no_chain[set,unset]
M14 repo_shape plain dict returned for run_products the gate test (2 rows), test_the_library_asks_the_guard_over_the_run
M15 repo_shape LinkPins built without env_shared the gate test (2 rows), test_the_library_asks_the_guard_over_the_run
M16 naming gate products without target.pins test_a_variable_two_declared_pins_share_is_read_for_no_chain
M17 naming gate products without the chains' links test_a_variable_a_link_of_another_chain_shares_is_read_for_no_chain
M18 scaffold no re-spelling test_a_first_link_that_is_a_pin_is_spelled_as_the_pin (3 rows), plan and record tests
M19 scaffold the owner prefix dropped test_a_first_link_that_is_a_pin_is_spelled_as_the_pin[owner-kept]
M20 scaffold _declaration passes no pins test_the_plan_names_the_chain_in_the_pins_spelling, test_the_manifest_records_the_chain_in_the_pins_spelling
M21 scaffold compared as typed, not by pin_product_key test_a_first_link_that_is_a_pin_is_spelled_as_the_pin (3 rows)

Merges

git merge-tree --write-tree of this head with each:

Fix round 1

On RV981's review (issuecomment-6254819059), head 499f7baa, one commit on ac5b5307.

  • S1. The Kelvin fix changes which variable is read for the first link, and a test now holds it. As typed, open<U+212A>dox was read through SHAPE_PIN_SOURCE_OPEN_DOX. The Kelvin sign is upper case and outside [A-Z0-9_], so pin_source_env_name turns it into _, and that is the variable of open-dox, open_dox and open.dox. On main, that variable set to a tree declaring openQuill refused a chain that holds (chain-not-declared, exit 2), and the WARNING offered that variable. The link is the pin openKdox now, so SHAPE_PIN_SOURCE_OPENKDOX is read for it, before --pin-source. U114 was therefore not only cosmetic. Two dry-run rows in tests/test_chain_pin_sources.py hold both directions:
    • SHAPE_PIN_SOURCE_OPEN_DOX set to that tree: exit 0, no Kelvin sign in the output;
    • SHAPE_PIN_SOURCE_OPENKDOX set to it: chain-not-declared.
  • S2. test_the_manifest_gate_reads_a_shared_variable_for_no_chain has a qualified-chain row: the spec leg's chain begins at opensoft/ + open + U+00DF. validate-manifest hands its products over as the manifest spells them, so the library's repo_basename on run_products is now held.
  • N1. SOURCE_ABSENT's comment says every tagged library has LINK_SOURCE_ABSENT and 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 a scripts/ nobody may search no longer SKIPs every validator into bootstrap ok (#430, slice D) #958.
  • N2. The re-pin line above now says every family holder re-pins repo_shape.py once too.
  • N3. _run_products' docstring says it counts the chain of a leg whose repository: is bad or missing, which the gate's _project_chain_links leaves out. Such a leg is a finding in both gates.
  • N4 is lane 3's follow-up, and N5 is not in this round.

Tests

Same command and stub gh as above, plus GIT_SSH_COMMAND=false. All runs are on 499f7baa.

  • tests/test_chain_pin_sources.py (the one test file changed): 32 passed in 8.54s
  • The same file with the core chain files (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): 802 passed in 133.08s (0:02:13)
  • Docs guards and tests/test_no_auto_maintenance.py: 217 passed in 49.73s

Mutants

All are now killed:

# Mutation Killed by
A1 the library drops repo_basename on run_products test_the_manifest_gate_reads_a_shared_variable_for_no_chain[qualified-chain-set]
A2 the scaffold records the pin's spelling but reads the typed chain test_the_kelvin_link_is_not_read_through_another_products_variable, test_the_kelvin_link_is_read_through_its_pins_variable
M5 (re-made) a set variable still offered test_a_source_that_is_there_is_said_and_not_offered (3 rows)
M12 (re-made) the guard asked over links alone 8 rows: the gate's set/unset, declared-pins and other-chain tests, the manifest gate's three *-set rows, and the library test

Merges

git merge-tree --write-tree of 499f7baa:

Summary by Sourcery

Improve chain-source diagnostics and scope pin-source reads per validation run while preserving declared-pin spelling in scaffolded chains.

New Features:

  • Make manifest validation explain why reachable chain sources could not be read and offer only sources that are genuinely absent.
  • Coordinate pin-source environment handling across all products and recorded chains in a validation run.
  • Record scaffolded referent-chain first links using the spelling of their matching declared pins.

Bug Fixes:

  • Prevent shared SHAPE_PIN_SOURCE_* variables from being consumed by multiple products or chains.
  • Preserve correct pin spelling for case-insensitive and Kelvin-sign referent-chain matches.

Enhancements:

  • Keep pinned validator copies compatible with older repo_shape libraries while adopting the new lookup metadata.

Tests:

  • Add comprehensive coverage for source diagnostics, run-wide environment collisions, library-version skew, and pin-spelling behavior.

Chores:

  • Re-pin the affected project validator copies.

Lane: openreposhape-3 (openRepoShape-3)
🤖 Generated with Claude Code

…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>
@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates 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 lookup

sequenceDiagram
    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
Loading

Sequence diagram for reasoned manifest source diagnostics

sequenceDiagram
    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
Loading

Flow diagram for pin spelling of the first referent link

flowchart 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
Loading

File-Level Changes

Change Details Files
Improves manifest chain diagnostics so sources that exist are reported with lookup reasons instead of being offered as remedies.
  • Passes lookup-tracking and unreadable-tree reason data when supported by the pinned library.
  • Builds remedies after lookup completion, offering only sources that are absent.
  • Adds printable path handling and keeps absence wording synchronized with the library.
  • Propagates run-wide product information through manifest leg processing.
templates/assembly-root/scripts/validate-manifest.py
tests/test_chain_pin_sources.py
tests/test_naming_policy.py
tests/test_pinned_copy_text.py
Makes pin-source environment collision handling consistent across all products read by a validation run.
  • Adds run_products support to link_pins_from_trees and records shared variables in LinkPins.env_shared.
  • Suppresses warnings' offers for variables shared by declared pins or links from any recorded chain in the run.
  • Adds compatibility checks so naming and manifest pinned copies only pass newer keywords when supported.
  • Keeps scaffold behavior unchanged by omitting run_products for its single-chain lookup.
scripts/repo_shape.py
scripts/validate-repository-naming.py
templates/assembly-root/scripts/validate-manifest.py
tests/test_chain_pin_sources.py
Normalizes a scaffold referent chain's first link to the spelling used by its matching --pin.
  • Matches the first link by pin_product_key, including case folding and Kelvin-sign normalization.
  • Preserves any owner prefix while replacing only the product spelling.
  • Passes declared pins into the declaration-time chain parsing so planning and manifest recording use the normalized spelling.
scaffold-project.py
tests/test_chain_pin_sources.py

Assessment against linked issues

Issue Objective Addressed Explanation
#909 Have validate-manifest.py retain and report why a pin source that is present could not answer, offering only sources that are absent, while remaining compatible with older repo_shape.py copies. ✅
#909 Evaluate SHAPE_PIN_SOURCE_* collisions across all products read in a run, so a shared variable is not used or offered for any chain it could ambiguously answer. ✅
#909 When a scaffolded chain’s first link matches a --pin under pin_product_key, record and display it using the pin’s spelling. ✅

Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-18T20:33:50Z) RV981, independent review of #981 at head ac5b530 (opus, lane 3).

Verdict: READY. No MUST.

SHOULD
S1. The Kelvin fix also changes which variable the scaffold reads, and no test holds it. On main the first link open[U+212A]dox was read through SHAPE_PIN_SOURCE_OPEN_DOX, the variable of open-dox/open_dox/open.dox: with it naming a tree that declares openQuill, main refused a holding chain (chain-not-declared, exit 2), and the warning offered SHAPE_PIN_SOURCE_OPEN_DOX. The head reads and offers SHAPE_PIN_SOURCE_OPENKDOX. So U114's "cosmetic" was not true on main. Mutant "record the pin spelling but read the typed chain" in _declaration (scaffold-project.py:1181-1184) survives: 89 passed across test_chain_pin_sources.py, test_scaffold_referent_chain_links.py and test_chain_link_fold.py. Fix: a dry-run row with SHAPE_PIN_SOURCE_OPEN_DOX set to a tree declaring openQuill, expecting exit 0, and a line in the PR body.
S2. Nothing tests the run guard with an owner-qualified link. Mutant "drop repo_basename on run_products" (repo_shape.py:3321-3322) survives: 218 passed. In validate-manifest.py, whose _run_products passes links raw, it brings R444 back: a spec-leg chain [opensoft/open[U+00DF], openQuill] beside openss -> openDox goes from exit 0 to exit 1. Fix: add an opensoft/open[U+00DF] row to test_the_manifest_gate_reads_a_shared_variable_for_no_chain.

NIT
N1. validate-manifest.py:238-242: LINK_SOURCE_ABSENT is in every tagged library (cac6bb2, before v1.0.0) and the gate imports it. The local copy keeps the import block clear of #958; say that instead of #610.
N2. The PR body says every project re-pins once. repo_shape.py is a family holder's pinned copy too, so every holder does as well.
N3. _run_products counts chains of legs with a bad or missing repository:, which the gate's _project_chain_links leaves out. Harmless (both exit 1 there); reword the docstring.
N4. A set variable the guard passes over is named by no line. #275's existing behaviour; follow-up, not this PR.
N5. Later: the gate's _library_takes_remedies can call _library_takes once #949 lands, and pin_product_key's import in referent_chain_links can move to module level after #969/#971/#972.

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.
Mutants M5, M10, M12, M13, M17, M21: each killed. Mine, A1 and A2: survive (S2, S1).
merge-tree clean with main ebbd9fa, #958, lane 5 WIP b49f34e, #974, #949 ced3d7c, #969, #971 and #972.
Not done: pytest on the merges with #974/#969/#971/#972 and the lane 5 WIP; the writer's adopt and other-reader batches; Windows.

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>
@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(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:

  • S1 fixed. The two dry-run rows hold both directions: SHAPE_PIN_SOURCE_OPEN_DOX set gives exit 0 and no Kelvin sign; SHAPE_PIN_SOURCE_OPENKDOX set gives chain-not-declared. Passing env by monkeypatching test_chain_link_fold.run_script is sound: scaffold_pinning looks the name up when it runs, the default import mode makes it the same module object pytest collected, and monkeypatch undoes it at teardown (precedent: test_github_limits_init_check_org.py:697). One process ran test_chain_pin_sources.py and then test_chain_link_fold.py: 802 passed, no leak.
  • S2 fixed by the qualified-chain row.
  • N1 and N2 fixed. N3 fixed, but see NIT 1. N4 and N5 deferred, accepted.

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).
Mutants: A1 killed by [qualified-chain-set]; A2 killed by both Kelvin rows; M17 killed by test_a_variable_a_link_of_another_chain_shares_is_read_for_no_chain; M21 killed by 7 tests.
merge-tree clean with main cf66102, #974 e63ca75, #949 ced3d7c, #971 155fcd2, #972 0094731 and #958 0ef875c.
Checks at 499f7ba: SonarCloud, package and tests-git-bash pass; tests pending when read.

Lane 3: READY. #981 lands as it is, by lane 3's pilot, once its tests and a gate on the predicted squash are green. NIT 1 (one word in validate-manifest.py:228-231) and NIT 2 go to a follow-up. The #974 failure is fixed on #974's own fix round (the test's edit now removes the whole leg, its naming: block included).

Lane: openreposhape-3 (openRepoShape-3)

@sourcery-ai

sourcery-ai Bot commented Oct 19, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 2 days and 7 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit ca6a0b4 into main Oct 19, 2026
5 checks passed
@brettheap
brettheap deleted the l3-909-chain-pin-sources branch October 19, 2026 12:48
brettheap pushed a commit that referenced this pull request Oct 19, 2026
…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>
brettheap added a commit that referenced this pull request Oct 20, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant