Repository navigation
update-shape.py read_pin refuses update-pin-row, and the pinned validate-pins.py and validate-family.py report shape-pin-row, when a shape pin's files: is not a list, so files: 5, true or 1.5 no longer ends check, apply, shape-doctor.py and the project's own validator in TypeError: ... is not iterable (#290) - #343
Conversation
…alidate-pins.py and validate-family.py report `shape-pin-row`, when a shape pin's `files:` is not a list, so `files: 5`, `true` or `1.5` no longer ends `check`, `apply`, shape-doctor.py and the project's own validator in `TypeError: ... is not iterable` (#290) Every reader of `contracts/shape-pin.yaml` took `pin.get("files") or []` and looped over it. `or []` rescues the falsy values, which reach the `no files` refusal or finding; a truthy scalar was iterated. On main 71b58a1 a pin with `files: 5`, `files: true` or `files: 1.5` committed over a clean scaffold ended `update-shape.py check` and `apply`, `shape-doctor.py`, the root's `scripts/validate-pins.py` and a holder's `scripts/validate-family.py` in `TypeError: 'int' (or 'bool', 'float') object is not iterable`, exit 1. The doctor printed nothing on stdout at all, and its one job is to say what is wrong. Each of the four readers now asks for a list: - `update-shape.py` `read_pin` raises `update-pin-row` naming the pin and the value (`files:` is 5, not a list of `path:`/`sha256:` rows). It is reached from `prepare`, so `check`, `apply` and the doctor's shape-currency row all get the refusal; the doctor's row already carries a `read_pin` refusal by its code. The two `update-pin-row` raises now carry a remediation about the pin (put back the pin a tool last wrote, from the root's own history) instead of `repo_shape`'s default trailer, which is about submodules and fixes nothing in this file. - `templates/assembly-root/scripts/validate-pins.py` `_check_shape_pin` and `templates/family-root/scripts/validate-family.py` `_check_shape_pin_files` report one `shape-pin-row` finding for the whole key, exit 1, the id they already use for a row that is not a mapping. A string `files: abc` is now one finding, where it was one per character. - `shape-doctor.py` `pinned_paths` returns the empty set, as it does for a pin that is not a mapping. It is read only when an agent file is missing, so it was masked while `read_pin` crashed first; with `read_pin` fixed, a root with a scalar pin and no `AGENTS.md` reached it. The falsy values are unchanged: `files: 0`, `false`, `null` and `[]` keep `update-pin-no-files` and `shape-pin-no-files`. The two validators are pinned template copies (`COPIED_VERBATIM`, `FAMILY_COPIED_VERBATIM`). Nothing in this repository records their digest: a scaffold, an adoption and `family.py init` digest the bytes they copy, so a project cut from here records the new digest, and every project already cut reads the two files as `upstream-changed` in `update-shape.py check` until its owner runs `apply`. tests/test_shape_pin_files_not_a_list.py: 18 cases over `files: 5`, `true` and `1.5` through each reader (update-shape check and apply, the root's validate-pins.py, the doctor's shape-currency row and its text report, the doctor's agent-files row with no `AGENTS.md`, `pinned_paths` in process, and a holder's validate-family.py, update-shape check and doctor). All 18 fail on 71b58a1 with the TypeError. Two controls for `files: 0` and `false` pass before and after. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pping `files:` gets, so it no longer claims the old code refused one character at a time (#290) Before #290, `read_pin` refused a string or a mapping `files:` at its first character or key, as a row that is not `path:`/`sha256:`; it was the two validators that reported one finding per character. The comment now says what changed for `read_pin` itself: the value is named for what it is. Comment only. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
|
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 10 hours and 51 minutes by commenting |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Reviewer's GuideThe PR hardens all four shape-pin readers against truthy non-list Sequence diagram for reporting an invalid shape-pin files valuesequenceDiagram
participant User
participant Doctor as shape-doctor.py
participant Reader as update-shape.py read_pin
participant Validator as Pinned validator
participant Pin as shape-pin.yaml
User->>Doctor: Run shape-doctor.py
Doctor->>Reader: read_pin(root)
Reader->>Pin: Read files
Pin-->>Reader: Truthy non-list value
Reader-->>Doctor: Refusal update-pin-row
Doctor->>Validator: Validate shape pin
Validator->>Pin: Read files
Pin-->>Validator: Truthy non-list value
Validator-->>Doctor: Finding shape-pin-row
Doctor-->>User: Complete report with refusal and finding
Flow diagram for non-list shape-pin files handlingflowchart TD
A[Read shape-pin.yaml] --> B{files is a list?}
B -- Yes --> C[Iterate path and sha256 rows]
B -- No --> D[Named diagnostic]
D --> E[update-shape.py: update-pin-row refusal]
D --> F[Validators: shape-pin-row finding]
D --> G[shape-doctor.py: pins nothing and reports]
C --> H[Normal validation or shape update]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6e2689acb8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "copied file. The tool that cut this root wrote it and only " | ||
| "`update-shape.py apply` moves it, so it is never mended by hand: put " | ||
| "back the pin a tool last wrote from this repository's own history " | ||
| "(`git log -- contracts/shape-pin.yaml` names the commit that changed " |
There was a problem hiding this comment.
Point the history lookup at the target root
When the documented workflow runs this script from the openRepoShape checkout with --root /path/to/project, this unqualified git log inspects the checkout rather than the target repository, so it usually shows no pin history—or the wrong history—and the user cannot follow the new remediation. Print a properly quoted git -C <root> log -- contracts/shape-pin.yaml command instead.
AGENTS.md reference: AGENTS.md:L250-L255
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
(lane openRepoShape-2) Fixed in 039530b. PIN_ROWS_REMEDIATION is now pin_rows_remediation(root), spelled with the module's own quoter _by_hand, so the remediation prints git -C <root> log -- contracts/shape-pin.yaml quoted for a POSIX shell. Both update-pin-row raises in read_pin pass the root.
tests/test_shape_pin_files_not_a_list.py asserts the command computed with shlex.join, so it holds on Windows too. Two new tests cover the rest. One runs the tool from the checkout against a root path holding a space and executes the printed command; it finds the root's commits, where the unqualified command found none. The other covers the second raise. At 6e2689a, 8 of 22 fail; at 039530b, 22 pass. A mutant dropping -C <root> fails 8.
…` as `git -C <root> log`, so run from the openRepoShape checkout with `--root <project>` it shows the project's pin history and not the checkout's (#290) PIN_ROWS_REMEDIATION was a constant that told the person to run a bare `git log -- contracts/shape-pin.yaml`. This tool is run from a checkout of openRepoShape with `--root /path/to/project`, so that command inspects the checkout, which has no such pin, and shows no history (or the wrong one): the person cannot follow the remediation. Codex review comment on PR #343, #343 (comment). The remediation is now `pin_rows_remediation(root)`, and both `update-pin-row` raises in `read_pin` pass the root they were given. It spells the command with `_by_hand`, this module's one quoter for a line a person pastes (`shlex.join(["git", "-C", root, ...])`), so a root holding a space is one argument. The rest of the text is unchanged. tests/test_shape_pin_files_not_a_list.py asserts the qualified command, computed with `shlex.join` rather than written as a literal so it holds on Windows, where a backslash path is quoted. A new test runs the tool from the checkout against a root whose path holds a space, takes the printed command out of stderr, runs it from the checkout, and requires the project's own commits to the pin. A second new test covers the other raise in `read_pin` (a `files:` list whose row is not a mapping), which no test reached before. At 6e2689a 8 of the file's 22 tests fail on the missing `git -C <root>`; dropping the `-C <root>` pair from the command fails them again. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. Hooray! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
(lane openRepoShape-2, 2026-10-12T15:39Z) This lane reviewed its own PR #343 at 039530b, read-only, in its own detached worktrees. Nothing was authored. MERGEABLE WITH NITS. Sequencing: whichever of #343 and #267 lands second must take the three-line Findings:
Main ab623dd reproduces the issue: a TypeError, exit 1, in all five readers. Head refuses Tests:
Merges: rc 0 with main and every open head except #358, whose 4 hunks are its own conflict with main. main moved to 161c6c9 during the review, still rc 0. Report: opensoft/brett-wip handoffs/xFactory/attachments/lane-openRepoShape-2/review343-039530b/final-report.md |
…, `abc`, is a case of its own beside `5`, `true` and `1.5`, so a guard widened to `isinstance(files, (list, str))` in `read_pin`, `validate-pins.py` or `validate-family.py` fails the file (#290) The PR promised that a string `files:` gets one finding and one refusal naming the whole value, and nothing tested it. A string is iterable, so the three scalars the file already had could not tell the right guard from one that lets a string through: the two validators gave one finding per character and `read_pin` refused it as a bad ROW. Three one-line mutants each passed all 22 tests (review of 039530b: mu6 at validate-pins.py, mu7 at update-shape.py's `read_pin`, mu9 at validate-family.py). With `files-abc` in `NOT_A_LIST`, each of the six parametrised tests runs it, so every reader is asked for exactly one finding or one whole-`files:` refusal, and each mutant now fails. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…_pull_request` spell the pin's path by the materializer's `SHAPE_PIN`, so `contracts/shape-pin.yaml` has no third copy in this module for Sonar's S1192 (#290) `pin_rows_remediation` spelled `"contracts/shape-pin.yaml"` a third time in update-shape.py, after `cmd_apply`'s set of paths to stage and `_awaits_its_pull_request`; Sonar called the third copy a CRITICAL S1192 and it was the one new issue on the PR. `scripts/shape_materialize.py` already names the path once, as `SHAPE_PIN`, because a second spelling is how the lists beside it would stop matching what is written, and this module already takes its other constants from there. All three now use that name. Two things left as they were, on purpose. The `root / "contracts" / "shape-pin.yaml"` joins in `read_pin` and `_repin` are two short strings, not the flagged literal, and #347 rewrites the top of `read_pin` (a merge of this branch with it conflicts there if the join changes). The name comes in by its own `from shape_materialize import SHAPE_PIN` line after the existing import block, not as one more name in it, because #380 rewraps that block and a merge conflicts there if this branch does too. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t: `check_pin` takes the root its `update-pin-row` remediation reads, so #290's refusal survives #267's split instead of a NameError (#290) Main 580d7df carries #267 (decff87), which split update-shape.py's `read_pin(root)` into the read and `check_pin(pin, pin_path)`, so that shape-doctor.py can check the pins in a root's history in memory. The `update-pin-row` raises moved into `check_pin`, and on this branch they call `pin_rows_remediation(root)`, #290's `git -C <root> log` advice, where `root` is not a name. The textual merge is clean and the tree dies at run time: `NameError: name 'root' is not defined` for a `files:` of `5`, `true` or `1.5` in `check`, `apply` and shape-doctor.py, which then prints no report at all. This commit takes the planned #267 composition, the PR's two attachments fix1-compose-267-resolution-v2.patch and root-pin-faults-after-267.patch, applied unchanged; main moved their hunks by offset only (shape-doctor.py 526 lines, tests/test_shape_doctor.py 521, update-shape.py 1): - update-shape.py: `check_pin(pin, pin_path, root)`, `read_pin` passes its root, and the docstring says `root` is the repository whose history a refusal's `git log` reads. - shape-doctor.py `pin_reads_at` passes `ctx.root`. Its docstring says `check_pin` refuses `files: 5` as `update-pin-row`, and `TypeError` leaves its `except` tuple: no pin's bytes raise one now, so one means a caller out of step with `check_pin`, and the doctor fails loudly rather than answer that no commit holds a pin to put back. - tests/test_shape_doctor.py: #267's ROOT_PIN_FAULTS gains `files: 5`, `true` and `1.5`, each `update-pin-row`, so each is INVALID, exit 1, with `checkout HEAD` as its NEXT, which run as printed leaves the root COMPLIANT. Main's other changes to this PR's files merge as text away from these lines: #446 (245d775) in validate-pins.py, #406 (ec8e09a) in validate-family.py, #379 (87d2eb7) in update-shape.py, and #375 (cd1fc79) and #357 (5678f68) in shape-doctor.py. Neither module has changed on main since decff87. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
(lane openRepoShape-2, 2026-10-12T21:18Z) This lane re-verified its own PR at e68a01d (the main merge with #267's composition) and at 91a4a4f (update-branch onto main d6df36b; tree e358779), read-only in its own detached worktrees, with nothing authored. MERGEABLE WITH NITS. CI on 91a4a4f on all three OSes is the gate. #419 and #347 follow this PR. e68a01d is the clean textual merge of 9e6caeb and 580d7df plus the two planned patches, byte for byte. pyflakes is clean. For Findings:
Tests on the combined tree (2e85f54, the same tree as 91a4a4f): 862 passed, 6 skipped, 0 failed. Merges: e68a01d with 1184f67, with 6d4e1e9 and with d6df36b are each rc 0. With #358 ce2ce75 it is rc 1 in three files, on the composition lines. Report: handoffs/xFactory/attachments/lane-openRepoShape-2/review343-e68a01d/final-report.md in opensoft/brett-wip. |
…tf8, so the stacked branch holds the parent's last commits before its squash #343 (issue #290) was the parent of this branch. This merges the final head of #343, 91a4a4f, which is clean: merge-tree of 91a4a4f with the previous tip 11dff7f is rc 0. This branch's own change stays the pinned validate-pins.py template's refusal of a workflow file that is not UTF-8 and its test file. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t-utf8, so #343's own five files stay as the branch holds them and the branch now sits on main #343 (issue #290) squash-landed on main as 437a360. This branch already held #343's final head 91a4a4f. git diff 91a4a4f 437a360 over shape-doctor.py, templates/assembly-root/scripts/validate-pins.py, templates/family-root/scripts/validate-family.py, tests/test_shape_pin_files_not_a_list.py and update-shape.py is empty, so main changed those five files by nothing but the squash, and each is taken as this branch holds it (git checkout --ours, a no-op that the merge's own resolution already gave). For validate-pins.py that is #343's final version plus this branch's own refusal of a workflow file that is not UTF-8 (#359). Everything else the merge brings is main's: 437a360 is the shared store's origin/main at this merge, so no later main commit is merged. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
36538fd..437a360 is #355, #401, #456 and #343; #401 adds `_leg_path_help` and the `--spec-path`/`--code-path` help to adopt-project.py, away from every line this branch changes. The merge has no conflict. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s `check_pin(..., root)` and uncaught `TypeError` composed into this branch's `pin_at`, so the branch is current with main after #343 landed and differs from it only in shape-doctor.py and tests/test_shape_doctor.py (#291) Main after d6df36b brings #343 (437a360, #290): update-shape.py's `check_pin` takes the root and refuses a `files:` that is not a list as `update-pin-row`, so shape-doctor.py's `pin_reads_at` passed `ctx.root` and stopped catching `TypeError`, and `pinned_paths` reads a `files:` that is not a list as pinning nothing. 437a360 equals #343's final head 91a4a4f on all six of its files. Git conflicted in `pin_at` alone, the body of the old `pin_reads_at` that this branch moved there. The resolution is the one f358 wrote down and rv358c ran: the docstring keeps #343's two sentences (`check_pin` refuses `files: 5` as `update-pin-row`; a `TypeError` is not caught), then this branch's ANY OTHER FILE and THE ROWS, NOT A YES paragraphs; the body keeps this branch's (a file other than the shape pin reads as its mapping, the shape pin as its rows' paths) with `check_pin(data, ..., ctx.root)` and `except (Refusal, us.YamlError, ValueError)`. `pinned_paths` and the test file's three new ROOT_PIN_FAULTS cases merged clean. The merge's change to each file over this branch is #343's own lines, one for one, with `ctx.root` on this branch's form of the `check_pin` line; its change over main is this branch's lines over d6df36b, with that same `ctx.root`. `git diff --stat 437a360` names shape-doctor.py and tests/test_shape_doctor.py only. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… own "so", and `pinned_copies` no longer catches `TypeError`, so the pin's `unmerged` and `committed` notes read as one sentence and a caller out of step with `check_pin` fails loudly here as it does in `pin_at` (#291) Two nits from the re-verify of a5563c3 (rv358c, N1 and N2). N1. `BY_HAND_WHY["pin"]` ended "after the scaffold, so every change to one was made by a person", and `pin_restore` appends ", so neither side is taken whole" to the `unmerged` note and ", so nothing is rolled back" to the `committed` note, so both printed two `so` clauses in one sentence. The reason now joins its two facts with "and" ("..., and every change to one since was made by a person"), which leaves each note one conclusion; the copy's reason, which never ended in "so", and the `uncommitted` note are unchanged. The P13 test now also asserts that no sentence of the note holds two ", so " clauses; it opens on the same words it asserted before. N2. #343 (437a360) stopped `pin_at` catching `TypeError`: `check_pin` now refuses a `files:` that is not a list, or a row that is not a mapping, as `update-pin-row`, so nothing in a pin's bytes raises one, and one means a caller out of step with `check_pin`. `pinned_copies` calls `read_pin`, which runs the same `check_pin`, so it drops `TypeError` from its `except` by the same reasoning. A pin that does not read still makes the empty set, and nothing is a copy for that run. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s cite #343, #401 and #456, drop the known limits main no longer has (#290, #391, #225, #294), list all 30 open milestone issues, state the Windows park-and-resume exception beside the 1.0 promise, and give a pin procedure that pastes into macOS's zsh (#472) A fact-check of cbe44fe found nothing wrong at the drafting sha, but main had moved. Three PRs landed and fourteen milestone issues were filed in the forty-five minutes after the draft, and several sentences were true only with a qualifier the notes had dropped. This commit refreshes the notes against 437a360 and takes the checker's corrections. Refresh: - Section 5 adds #343 (`update-pin-row` and `shape-pin-row` for a shape pin whose `files:` is not a list), #401 (`plan --help` for `--spec-path` and `--code-path`) and #456 (`make bootstrap` escapes what the console cannot print). - Section 6 drops #290 and #391, which those PRs closed. - Section 6 also drops #225 and #294. Both are still open, but offline probes at 437a360 show #314 fixed them: - `scaffold-project.py --spec-path null --dry-run` refuses `scaffold-manifest-invalid` before anything exists; - `adopt-project.py check` on a plan with `elected_on: someday` and no `reference:` refuses `election-date-malformed`. - #181 now names the input that still reaches it, a source file named `null`. The probe showed `plan` writing `path: null` and `check` then finding the path uncovered. - New unfiled checklist items join section 6: - #458's `ImportError` item, now that #456 is on main; - #457's L2-6; - the `CONTRACTS_DIR` item; - four adoption inputs that cannot be tested on Windows. - Section 7 is rebuilt from the milestone, which now holds 30 open issues. #340 is closed, so its line is gone. Corrections: - **Section 1** states the promise as ruled, then says that on Windows park and resume run under WSL2, because native Windows parks nothing (README). - **The Windows CI sentence** says the adoption suites skip there until #471 lands, with a comment to drop the qualifier then. - **The pin procedure** ends its push line without a `# comment`. An interactive zsh without `interactivecomments` would hand the comment's words to `git push` as refspecs, the class #277 reports. The pull request step is in prose after the block. The section also says that the shape pin always records a commit, and that `apply` counts the files it re-copies before it asks. - **The YAML reader** sentence names `yaml-unparsable` beside `yaml-unreadable`, and claims no more than the reader does. - **The doctor** sentence keeps AGENTS.md's qualifier, "into the repository", and says that outside a checkout it clones the standard first. - **#303** gives both outcomes. **#389** is described as the gap that #305's refusal covers. - **The hermetic-tests** sentence claims only what its PRs and AGENTS.md establish: no test reaches the real `gh`, and the suite scaffolds into local bare repositories. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…: main 437a360 with #343's `check_pin(..., root)`, and the neutral pin's single-`so` reason and `pinned_copies` without `TypeError`, beside #485's pin-alone merge, so this branch stays one commit over #358 (#485) This branch is stacked on #358 (#291), whose head moved from a5563c3 to 65def40: 3af2829 merged main 437a360, #343's squash among it, composed into `pin_at`; 65def40 reworded the neutral product pin's reason so its notes read as one sentence, and dropped `TypeError` from `pinned_copies`' `except`. Merged so the PR's diff against its base stays #485's own change. Git merged it with no conflict: #485's hunks are in `pin_restore`'s `unmerged` case, `unreadable_next`, `check_manifest_kinds`' call and their docstrings, and the tests; 65def40's are in `pin_at`'s body and docstring, `pinned_copies`' `except`, `BY_HAND_WHY["pin"]` and `pinned_paths`. Every line 65def40 changes over a5563c3 is taken as #358 has it. #485's composite note uses `BY_HAND_WHY["copy"]`, which 65def40 leaves as it was, and holds one ", so " to a sentence, the rule 65def40's new assertion makes for the neutral pin. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_spelled` now spells every symlink this branch names, and #283's tracking-branch question is not asked by adopt-project.py, so it meets none of this branch's refusals Brings in, by sha, main's ten commits since 75052cb: ec8e09a (#406), 580d7df (#416), 1184f67 (#404), d6df36b (#346), 37665ff (#452), 36538fd (#378, #329), 6c2a371 (#355, #283), 8782333 (#401, #340), ec4a7a0 (#456, #391) and 437a360 (#343, #290). No conflict. The tree is `git merge-tree --write-tree 437a360 95f6fc7`'s, ae7be0a. Three files changed on both sides merged by git alone: - adopt-project.py: 36538fd rewrites `_spelled`'s docstring and `_spelled_character` (a byte that is not UTF-8 is `\udcNN`, a character `\xNN`, by `repo_shape.double_quoted`), and two docstrings (`_refused_by_policy`, `_index_entries`); 8782333 adds `_leg_path_help` and the `help=` of `plan`'s `--spec-path` and `--code-path`. This branch's hunks are elsewhere. - scripts/shape_materialize.py: 6c2a371 adds `import tempfile` and the branch-name question (`_ask_git_branch`, `_branch_verdict`, `branch_problem`, `refuse_bad_branch`) after `run`. This branch's hunks are elsewhere, and it adds no import of `tempfile` to either file (adopt-project.py has had one since before 75052cb), so no name is bound twice. - tests/test_adopt_finding_spelling.py: 36538fd's `\udcfe` lines and this branch's `follows too many symlinks` line are more than 100 lines apart. How main's changes meet this branch's code: - 36538fd: every source path and symlink target this branch's findings and the materializer's refusals name is spelled by `_spelled` (`execute` hands it to the materializer as `spell`), so they now spell a byte that is not UTF-8 as `\udcNN`, as the plan's own `path:` does. One test of this branch pinned the old spelling, `nowhere\xff`; the next commit pins `nowhere\udcff`. Nothing in this branch has an escape table of its own. - 6c2a371: `refuse_bad_branch` is called by scaffold-project.py and scripts/family.py only. adopt-project.py's `--tracking-branch` is not asked of git (6c2a371 leaves it as its own change), so the question falls nowhere among `check`'s findings or `execute`'s refusals. - 8782333: `plan --help` text only; `plan` refuses nothing new, and the help names `shape/` as where `execute` writes its copy, which this branch does not move. Checked on the merged tree: no top-level name in adopt-project.py, scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py or scripts/family.py is bound twice that was not already bound twice on main (shape_materialize.py's `YamlError` and `parse_yaml`, as on main), no name is loaded and bound nowhere, and each side's own change reappears whole in the merge (no line of either side lost). Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… conflict with #394 (#225) 7a8b981 (#394, issue #189) moved a leg path's segment rule into `repo_shape.path_spelling_fault` and added it, with `PATH_CLIMBS`, `PATH_ITSELF` and `PATH_NOT_CANONICAL`, to the `from repo_shape import (...)` list in scripts/shape_materialize.py, the list this branch had added `parse_yaml` to. GitHub could no longer update the branch. The one conflicting hunk is resolved as main's list. The branch's `parse_yaml` is left out because main has imported it at the top of the module since #380 (`from repo_shape import YamlError, _strip_comment, parse_yaml`), and `_read_back` uses that binding. Naming it in both imports was a redefinition that pyflakes reported. With this merge the pull request changes no import line. Everything else merged without a conflict and keeps both sides. From main: the `EXCEPT THE SEGMENTS` comment, `LEG_SEGMENT_REASONS`, and `_spelling_reason` asking `path_spelling_fault`. From this branch: the #225 paragraph, `_leg_path_lines`, `_misread_reason`, and the read-back step in `leg_path_problem` and `_canonical_spelling`. The read-back is still asked last, after the spelling, Git's and Windows' names and the alphabet. 7a8b981's `submodules:` record adds no line that holds a leg path. Each row's `path:` is a submodule's path inside its leg, and `leg:` is `spec` or `code`. `execute` appends the block in code instead of rendering it from a template, and it double-quotes every path. The rule still finds the four real template lines. An adoption at `--spec-path legs/v2 --code-path c0de` with a recorded submodule writes a leg path on those four lines only, and its bootstrap passes 7a8b981's validator. The merge also brings in ec4a7a0 (#456), 437a360 (#343) and d222a12 (#397). None of them touches a file of this pull request. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d `open_glass` share one `SHAPE_PIN_SOURCE_<PRODUCT>`, so `--pin` refuses the pair and `validate-pins.py` notes a declared one and reads the variable for neither (#275) (#409) `pin_source_env_name` turns every character outside `[A-Z0-9_]` into `_`, so `open-glass`, `open_glass` and `open.glass` are three legal product names, with three pin files (`contracts/<product lowercased>-pin.yaml`), and one variable, `SHAPE_PIN_SOURCE_OPEN_GLASS`. On ab623dd (this branch's main parent; the same rows reproduce on c7c02fa), with the issue's own probe against local bare remotes and a `gh` that exits 97: | step | ab623dd | this head (6494714) | |---|---|---| | `scaffold-project.py --pin open-glass@<A> --pin open_glass@<B>` | exit 0, three repositories | exit 2 `scaffold-pin-collides`, nothing created | | `validate-pins.py`, no source, on a project that already declares the pair | exit 0, both skips offer `SHAPE_PIN_SOURCE_OPEN_GLASS` | exit 0, one `note neutral-pin-source-collides`; the skips offer `--pin-source <product>=<path>` and a sibling checkout only | | `SHAPE_PIN_SOURCE_OPEN_GLASS=<open-glass>` | `neutral-pin-source-unreadable` for `open_glass`, exit 1 | exit 0, the note; the variable is read for neither pin, so each pin is read from its own source or skipped | | `SHAPE_PIN_SOURCE_OPEN_GLASS=<open_glass>` | `neutral-pin-source-unreadable` for `open-glass`, exit 1 | the same | | `--pin-source open-glass=... --pin-source open_glass=...` | exit 0 | both pins recompute, exit 0, the note | **What changed.** Every variable name is kept: an operator may have set one and the docs name them. What is done instead: - `scripts/repo_shape.py`: `pin_source_env_collisions(products)` is the one pure answer, `{variable: [products]}` for each variable two or more distinct products share. Distinct means distinct PIN FILES (`pin_product_key`, `lower()`, which is what `contracts/<product lowercased>-pin.yaml` is named by), so a product typed twice or in two cases is #241's file collision and never this one. `pin_source_env_shared` is the set of those keys. - `scaffold-project.py`: `_refuse_colliding_pins` (left where it is) names each variable's `--pin` arguments as typed after #241's file sentences, in the same `scaffold-pin-collides` refusal. Dry run and real run say the same no, before any process starts and ahead of the leg-path refusal. #241's wording and remediation are unchanged when only a file collides. A NEW pair is still refused, so the situation is not created anew. - `templates/assembly-root/scripts/validate-pins.py`: one NOTE per variable, `neutral-pin-source-collides`, naming every product and the variable, the keyed `--pin-source <product>=<path>` that reads each pin, and that which declaration stays is the human's decision. It is never a finding and never moves the exit code. A product whose variable is shared does not read it (rule 2 of `resolve_neutral_pin_source`), and its skip hint offers the keyed `--pin-source <product>=<path>` and not the variable or the bare form. A variable one product has to itself reads and is offered exactly as before. - `scripts/repo_shape.py`, the two chain readers: `link_pins_from_trees` does not read a variable two links of one chain share, and the `declared-unverified` hint stops naming it. Only a hand-written chain can have two such links, since scaffold takes every link but the first from the naming policy's alphanumeric `open<Product>` form. Each reader of `pin_source_env_name`, decided: | site | decision | |---|---| | `scaffold-project.py --pin` | refuse (above) | | `validate-pins.py` rule 2 and the skip hint | a note, and the variable is neither read nor offered for a shared product | | `repo_shape.py` `resolve_link_source` (via `link_pins_from_trees`) | skip the variable when another link of the chain shares it | | `repo_shape.py` `_chain_link_findings` hint | omit the variable for a shared link | | `adopt-project.py --pin`, `plan`, `check`, `execute` | not in this PR, see below | **Fix round 1 (from the review of 34dbd86).** **The design change: a declared pair is a NOTE and not a finding. This is the lane coordinator's decision, and Brett may reverse it.** The first round made `neutral-pin-source-collides` a FINDING. Main through ab623dd scaffolds such a project (`--pin open-glass@A --pin open_glass@B` exits 0), and `update-shape.py apply` runs the NEW `validate-pins.py` over the rewritten root, so for a project already declaring the pair the finding ended every apply as `REFUSED update-validators-red`, rolled back, for this update and every later one, until a person deleted a true declaration of descent. AGENTS.md tells an agent never to edit "a pinned file, a digest or a manifest to make a row go green", and the finding's remediation was exactly that, so the person was locked out. The coordinator chose the review's option (b): it is a NOTE now. The guard stays (the variable is read for neither pin), each pin still verifies from its own source, scaffold still refuses a new pair, and `validate-pins.py` says the rest in one line. To put the finding back, restore 34dbd86's `report.finding(...)` call in `_report_pin_source_collisions` (it is not a word swap for `report.advise`: `advise` takes one string, `finding` a code and a detail), give the finding the remediation 34dbd86 had, and turn the tests that expect the note (and `test_a_project_declaring_a_colliding_pair_takes_a_shape_update`) back into exit 1; the first round's versions of all of them are on 34dbd86. - **Proved on a real old project.** Main (ab623dd) scaffolded the pair into local bare remotes; `update-shape.py apply` to the first round's head 34dbd86 exited 2 `REFUSED update-validators-red` and rolled back (`git branch` shows only `main`); to this head it exits 0, the project's validator passes and prints the note, and the doctor's `pins` row is `ok` before and after. The doctor prints `passes` for a validator that exits 0 and does not echo its stdout, so the note itself is visible in `update-shape.py apply`'s output and in `validate-pins.py`'s, and the verdict moves for no reason but the update (`COMPLIANT, SHAPE BEHIND` to `COMPLIANT`). It is now a test: `test_a_project_declaring_a_colliding_pair_takes_a_shape_update`, over local stand-ins beside the project, so no `gh` is asked. - **Collisions by the variable.** Pairs were decided by `casefold()`, but the pin file is `lower()`. `openß`/`openss`, `openſ`/`opens`, `openfi`/`openfi` and `openς`/`openσ` (the letters are U+00DF, U+017F, U+FB01 and U+03C2 beside U+03C3) share a variable and have two pin files, and were never found. They are now, from the pin-file fold (`pin_product_key`). A pool of 31 names is checked pairwise against `neutral_pin_path` and `pin_source_env_name` themselves, and no single product is ever a collision. They are reachable only from a hand-written `project.yaml`. - **Windows.** The guard's only test needed the `#!` `gh` stub and was skipped on Windows. `test_a_shared_variable_answers_for_neither_pin_with_no_gh` puts a checkout of each pin beside the project, names one in the variable, and needs no `gh`: on ab623dd it is `neutral-pin-source-unreadable`, here each pin reads its own sibling. The hint and the non-ASCII rows are tested in-process, with no skip and no filesystem. - **The hint.** For a product two pins share, the skip hint offers the keyed `--pin-source <product>=<path>`: the bare form answers for every pin with no more specific source, so with two pins it gives the other the finding this issue is about. - **Trailers and title.** The trailers of the earlier commits (a30091f and 34dbd86 name Sonnet 5.5) are settled in this message, which carries Opus 5.5. The PR title now names `repo_shape.py`. - `scaffold-project.py` is not touched this round: `fix/adopt-pin-writes-pin-file` moves that code, and the change would only conflict with it. **Left out.** - `adopt-project.py` is untouched. Its half arrives with #273's PR, which moves scaffold's pin machinery into a shared module and routes adopt through `pin_source_env_collisions`. Once it lands, the one place that still keys by `casefold()` (`pin_variable_collisions` in `scripts/shape_materialize.py`) should read `pin_product_key`; it cannot differ for an ASCII `--pin`, which is all `PIN_ARG_RE` takes. - `scaffold-project.py`'s own `_colliding_pin_variables` also keys its argument lists by `casefold()` and one docstring there still says the project's validator reports the pair "as a finding"; both go with the code when #273 moves it. #312's `PinSources` keys `--pin-source` by `casefold()` too, so `openß=` and `openss=` are one key there; that is #312's. - A product in `neutral_product_pins:` and a chain link in `naming.referent_chain` that share a variable are read by two validators in one process, and neither sees both sets. Not detected here. - The doctor does not print a passing validator's stdout, so its `pins` row does not carry the note. Not changed: it would add a file to this PR. - No `--help` text changed, so `docs/cli.md` is untouched. `README.md` is at its line cap and its blob is pinned by `docs/handbook.html`, so the one README sentence this wants needs a regenerated handbook and waits for its own change. - `validate-pins.py` and `repo_shape.py` ship into projects, so every project's `shape-pin.yaml` reads `upstream-changed` for both until `update-shape.py apply`, which now succeeds for a project that declares such a pair. **Evidence.** All on the head (6494714), with git-filter-repo 2.47.0 on PATH, a `gh` that cannot be reached (a stub first on PATH, `GH_TOKEN` bogus, an empty `GH_CONFIG_DIR`), and the repository-local `user.name` blanked at command-line scope, as the standard's checkout sets one that two scaffold tests read. - `tests/test_pin_source_env_collisions.py` is 137 tests and passes on this head. Laid over 34dbd86, 50 fail and 87 pass: the note and update tests, the non-ASCII rows, the hint, the sibling guard, and `pin_product_key` itself (which 34dbd86 does not have); the scaffold refusals, the chain readers and the controls hold on both. - Targeted, run on ad6f569, whose tree differs from this head in `tests/test_pin_source_env_collisions.py` only (the last edits made one comparison read the committed blob and wrapped one line): the new file, `test_pin_validator`, `test_neutral_pin_validator`, `test_cli_reference`, `test_manifest_validator`, `test_naming_policy` and `test_repo_hygiene` **445 passed**; the scaffold pin, collision, referent-chain, leg-path and e2e files **245 passed**; `tests/test_update_shape*.py` **218 passed, 3 skipped** (junctions, NTFS only); `tests/test_shape_doctor.py` **254 passed, 3 skipped** (the PowerShell line, a Windows host only). - On this head itself: the new file, `test_repo_hygiene`, `test_cli_reference`, `test_pin_validator` and `test_neutral_pin_validator` **244 passed**. - This head merged with origin/main a983e6a, in a throwaway worktree: the new file, `test_pin_validator`, `test_neutral_pin_validator`, the scaffold pin, collision, referent-chain and e2e files, the whole doctor file and `test_cli_reference` **590 passed, 3 skipped** (the PowerShell line); the new file alone, in its final form, **137 passed**. origin/main had moved twice since; the merge-tree below is against it. - Twelve mutation probes, each hunk reverted alone: all caught. The note-to-finding one is caught by the update test (a committed mutant, since the test clones the repository's HEAD) and the guards by the sibling test, run with every test that needs a `#!` stub deselected as a Windows runner would; the hint one by an in-process test. - Nothing ran on macOS or Windows before this PR's CI. **Merge-tree, as computed during the fix round on 2026-10-12** (every sha and head is the one it had then; main and the other branches have moved since). `git merge-tree --write-tree` of this head (6494714) against origin/main 1f90a0f is clean, and so is the merge of origin/main a983e6a, which the merged-tree runs above used. #312, #309, #270, #268, #228, #245 and #351 have landed. The rest were computed on ad6f569 (the same tree but for one test file) against a983e6a. Open PRs touching this branch's files, all clean against the head: #314, #355 and #373 (`scaffold-project.py`), #343 (`validate-pins.py`), #390 (`repo_shape.py`) and #394 (`repo_shape.py`, which conflicts with origin/main by itself). The other 25 open PRs and the lane-5 branches `fix/adopt-check-link-and-manifest`, `fix/scaffold-free-text-values`, `fix/family-init-values`, `docs/adopt-execute-exits`, `fix/yaml-flow-brackets-surrogates` and `fix/adopt-pin-writes-pin-file` (#273's, 7a94b6b, also clean for this head) are clean too. #356, #382 and `fix/adopt-record-submodule-placement` conflict with origin/main and not with this head; #358, `fix/adopt-late-failure-notice` and `fix/adopt-plan-quote-yaml-keywords` conflict with both, with origin/main by themselves, so they are not this branch's. Neither are the five PRs below #200 that origin/main has not landed (#94, #96, #98, #100 and #161), which conflict with it too. The PR set is read from the refs under `refs/pull/` that origin/main's history has no `(#n)` subject for, not from GitHub, so a closed one may be in it. **One conflict, with `fix/link-source-values` (#338, 40762ca) in `scripts/repo_shape.py`, four hunks.** Not resolved in either branch; take both sides: (1) `resolve_link_source`'s signature, `default_source=None, env_shared: bool = False, *, tried: list | None = None`; (2) its docstring, both paragraphs; (3) `link_pins_from_trees`'s docstring, both paragraphs; (4) its call, `default_source, pin_product_key(name) in shared, tried=asked)` followed by #338's `if tried is not None: tried[name] = asked`. The auto-merged `resolve_link_source` body already composes: a shared variable is never an `env` candidate and so never appears in #338's `tried`. **Known limits after the re-verification (APPROVE WITH NITS, 0 blockers, 0 should-fixes, 3 nits, at 6494714).** For the four non-ASCII pairs this change now detects (`ß`/`ss`, `ſ`/`s`, `fi`/`fi`, `ς`/`σ`), the keyed `--pin-source <product>=<path>` form does NOT separate the two pins: #312's source lookup folds both keys into one, so one pin can get a false `unreadable` finding. A sibling checkout does separate them. Only a hand-written `project.yaml` can declare such a pair. The doctor's `pins` row still prints only "passes" for a validator that passes, so it does not carry the collision note; that needs `shape-doctor.py`, and is left for a follow-up. The wording for restoring the finding is under "Fix round 1" above: it is 34dbd86's `report.finding(...)` call, not a word swap. Reviewed adversarially at 34dbd86 (CHANGES NEEDED: 0 blockers, 2 should-fixes, 5 nits). The first should-fix was the finding itself, which locked a project that already declares the pair out of every `update-shape.py apply`; the second was a description that had gone stale. Fix round 1 (6494714) made the pair a NOTE, which is the coordinator's decision and one Brett may reverse (the way back is above), keyed collisions by the pin file's own fold, gave the guard a test that runs on Windows, and made a shared product's skip hint offer the keyed `--pin-source`. Its re-verification at 6494714 found every finding handled: APPROVE WITH NITS (0 blockers, 0 should-fixes, 3 nits: the keyed form and the non-ASCII pairs, the doctor's silence about the note, and the wording for restoring the finding, each stated above). CI is green on Linux, macOS and Windows, with SonarCloud passing, at b70a07d (main a73b4e9 merged in). Its merge with main 5a25387 passed the targeted set (the six pin and scaffold test files, `tests/test_validate_pins_unreadable_workflow.py`, `tests/test_ruleset_hint_tracking_branch.py` and `tests/test_shape_doctor.py`): 850 passed, 3 skipped. Closes #275 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… is `PRODUCT=PATH` only when its key names a chain link, and a link source that cannot be entered is a WARNING with the system's reason, not a traceback (#338) (#453) **What was wrong.** Two faults, one in the parser and one in the lookup. - **The parser.** `_link_sources_from_args` split every `--link-source` value at its first `=`. A link tree under a directory named `a=b`, passed bare, became a source keyed to a link `a` that no chain names. Nothing asked for it, so the link stayed `declared-unverified`, exit 0. The WARNING then offered `--link-source openXdox=<path>` to the person who had passed exactly that. A mistyped key failed the same silent way. - **The lookup.** `repo_shape.link_manifest_path` asked `Path.is_dir()` and then `is_file()`. Before Python 3.14 those swallow only ENOENT, ENOTDIR, EBADF and ELOOP. So each of these ended the naming gate in a traceback, exit 1: - a keyed source under a parent this user cannot enter; - `SHAPE_PIN_SOURCE_<LINK>` pointing there; - a checkout `../openXdox` at mode 000, which every `--project` run asks for; - a 300-character name. **What changed.** **How a value is read** (`LinkSources` in `scripts/validate-repository-naming.py`). This mirrors #312's `PinSources`, with a chain link where that file has a declared pin: - The text before the first `=` is a LINK only when it casefolds to a link of a chain this run reads. Those are the entries of `--referent-chain`, and of each leg's `naming.referent_chain:` under `--project`. Both are read before any leg is classified. - The keyed reading wins even over a directory spelled like the whole value. `./` reads that directory. - Any other value is a bare PATH, taken whole. **What a lookup says** (`scripts/repo_shape.py`): - `link_manifest_lookup` asks `stat()` inside a guard, as `_digest_from_source` does in #312. It returns the system's reason when a source cannot answer, or says which of four plain cases it is (`no such file or directory`, `it holds no project.yaml`, `its project.yaml is not a file`, `it is neither a directory nor a file`), and it never raises. It replaces `link_manifest_path`, which is gone: nothing calls it. `link_declarations` gives the reason a manifest could not be read, and `declared_neutral_pins`, which it left with no caller, is removed (fix round 1). - `resolve_link_source` and `link_pins_from_trees` take a keyword-only `tried`. It records each source asked, in order, with its reason. Nothing in `repo_shape` prints. - `link_pins_from_trees` also takes a keyword-only `with_reasons` (fix round 2). Asked, it returns a `LinkPins`, a dict equal to the plain mapping that also remembers why a tree it reached could not be read. Only the scaffold asks. **What the validator prints** (stderr, after each name's WARNINGs): - `WARNING link-source-unreadable: <path> (<how>) could not answer for <link>: <reason>`. This covers a `--link-source` asked for a link that holds a pin, a `SHAPE_PIN_SOURCE_<LINK>` likewise, and a checkout beside the project that exists and still cannot answer. A keyed value that also names a directory gets a second line saying how to ask for that directory with `./`. A bare value holding `=` says it was read as a PATH. - `NOTE --link-source <typed>: not used — <why>.` for a value no such link asked for. The why is one of five: a later value replaced it; no name reads that link's own declaration (a keyed value); no name records a chain at all; no name reads any chain link's own declaration (a bare value, where chains exist and none is read); or every link that is read had a more specific source. - Three kinds of source are never named: a variable or a checkout asked only for a chain's LAST link (no one ever reads that link's own declaration); a variable or a checkout for a chain no classification reads (fix round 1: a spec leg's chain, a name with no referent, a chain whose shape fails; a `--link-source` value for such a chain is the NOTE); and a checkout that is absent or holds no manifest. - `--quiet` drops both, with the WARNINGs. So `make bootstrap` and the doctor's `naming` row print nothing new. `--help`, the module docstring and `docs/cli.md` (regenerated) say all of this. **Fix round 1 (lane 5's review of 40762ca: 0 blockers, 2 should, 4 nits).** Second commit, 4b21263. Each review finding, in order: - **A link manifest whose `neutral_product_pins:` is not a list of names is a tree that cannot answer** (SHOULD-1). `link_declarations` iterated `data.get(key) or []` as it stood, against the `link_pins_from_trees` docstring this change wrote ("nothing a source does raises out of here"). `5` ended the naming gate, the scaffold and the project's `validate-manifest.py` in `TypeError: 'int' object is not iterable`. A scalar `openDox` was read as the characters `D`, `e`, `n`, `o`, `p`, `x`, so the chain broke, and under `--project` that is a FINDING. A mapping was read as its keys, so `openDox: yes` DECLARED `openDox`. Each is now `(None, why)`, as `not a mapping` is: declared-unverified, with the reason named ("`neutral_product_pins` is not a list", "holds an entry that is not text"). An absent or null value, an empty list, and a blank entry (`null`, `''`) in a list, declare nothing, as ever. The fix is in the shared function, so the scaffold and `validate-manifest.py` are reached too. **Three behaviours change on purpose here, and the tests of fix round 2 pin each:** - **A falsy value is no longer an empty list.** `neutral_product_pins: 0`, `false` or `''` was read as no pin at all (`or []`), so a chain through the tree was BROKEN, and under `--project` that is a FINDING. It is now a tree that cannot answer: the link is declared-unverified, a WARNING, exit 0. - **An entry that is not text is no longer read as text.** `[openInk, 0]` and `[openInk, false]` dropped the entry as a blank, and `[openInk, true]` read it as the text `True`, so a chain through the list VERIFIED. Each is now declared-unverified, with a WARNING saying the list "holds an entry that is not text". That is stricter than the link tree's own `validate-manifest.py`, which takes `str(pin)` of an entry (`_declared_pins`, `templates/assembly-root/scripts/validate-manifest.py:85`): a chain is better left unverified than verified through a name nobody wrote. - **The scaffold no longer refuses `chain-not-declared` over a pins value that is a scalar.** With `--pin-source` a tree whose `neutral_product_pins:` is the scalar `openInk`, the scaffold used to refuse (`REFUSED chain-not-declared: ... openXdox declares I, e, k, n, o, p, not openInk`, exit 2, from the characters). It is now the declared-unverified WARNING and the scaffold goes on, exit 0, which is its documented rule for a tree that cannot answer: "a warning, never a refusal, because the check is offline". The warning says the tree was reached and could not be read (fix round 2, below). - **The reach wording is corrected** (SHOULD-2): "Who else this reaches" below, and the docstring of the test that made the claim. What stops crashing is the naming and manifest steps. `make validate`, `validate.yml` and `make bootstrap` still end in a traceback on the sibling row before Python 3.14, at a file this change does not touch (below). On 3.14 the sibling is passed over without a word there instead, which is #430. - **The mutation gaps** (nit). Six of the review's ten mutations survived. Five now die, each by a new test: a `project.yaml` that is a directory; a device or a FIFO as the source; the order A, B, A; a value holding `=` with no chain at all; and every failure of `SHAPE_PIN_SOURCE_<LINK>` for a link that holds a pin (a path that is not there, a directory with no manifest, a file that is no manifest), not only a permission. The sixth, the `typed in self.used` test in `unread_lines`, could not be killed because it was dead: a value is read as one link's keyed source or is the one bare PATH, so every lookup asks it of the same path and it is used or unread, never both. That was asserted over 293 tests and the test is removed. - **A chain no classification reads** (nit). `classify` consults a link's own declaration only for a name that claims a referent, with a recorded chain whose shape holds. For any other chain (one recorded on a spec leg; one on a name with no `open<Product>` to reach; one that does not begin at a held pin or does not end at a referent of the name) a source was still asked, so a path that was not there was reported as `link-source-unreadable` and a good tree was counted used, each for a question nobody put. `NamingPolicy.reads_link_declarations` now says when `resolve_referent` reads a chain, and `LinkSources.link_pins` asks it first: such a chain asks nothing, and a value for it is the NOTE that no name reads that link's declaration. A test runs `resolve_referent` with a mapping that records whether it was read, over every way a chain is and is not, and holds the new method to it. - **Dead code and stale sentences** (nit). `declared_neutral_pins` is removed: `git grep` over the tree, every pushed branch and every sibling worktree finds no caller. `link_manifest_lookup`'s docstring no longer says "replaces" over a name that is gone. This description no longer says `tests/test_naming_policy.py` takes `SEARCH_DENIED` from #312: it defines a mark of its own, with the same condition and its own reason text. **Fix round 2 (lane 5's re-review of 4b21263: 0 blockers, 0 should, 3 nits).** Third commit, 2ac911c. Each finding, in order: - **The three behaviours the body did not state are stated and tested** (nit 1). Round 1's not-a-list fix changed three behaviours on purpose, each listed under SHOULD-1 above, and the re-review found two of them had no test. R1 (`if declared is None` read as `if not declared`, which makes `0`, `false` and `''` declare nothing again) and R2 (`if pin is None or pin == ""` read as `if not pin`, which drops a `0` or `false` entry as a blank) both passed all 210 cases of `tests/test_naming_policy.py` at 4b21263. Six rows join `NOT_A_LIST_OF_NAMES`, which runs at the CLI and in process: `0`, `false` and `''` as the value, and `0`, `false` and `true` among the names. R1 now fails 9 cases and R2 fails 6. - **The scaffold's warning is true for a tree it reached** (nit 1, the third behaviour). Over a `--pin-source` whose `neutral_product_pins:` is not a list, round 1 turned the `chain-not-declared` refusal into a declared-unverified WARNING that says the tree "is not reachable here (a sibling checkout, SHAPE_PIN_SOURCE_<LINK>, or --link-source <LINK>=<path> would let it be read)". `--pin-source` did reach the tree, and only its declaration could not be read, so that blamed a path that was right. Now `link_pins_from_trees` takes a keyword-only `with_reasons`; asked, it returns a `LinkPins`, a dict equal to the plain mapping that also keeps `unreadable`, `{link: why}` for each link whose source answered and whose manifest could not be read. `_chain_link_findings` words such a link "which was reached but could not be read as a declaration (<why>)", in a branch of its own, and only the scaffold asks. The new text is asserted for each of the eleven not-a-list rows, with `scaffold-project.py --dry-run` run as a subprocess. - **Every other path's text is the bytes it was.** The sentence for a link no source reached is the same code in the same lines, and four tests hold it: the generic warning for a plain mapping, for an empty `LinkPins` and for an absent tree; the scaffold's own old warning for a clone that holds no `project.yaml`; the naming validator's two lines (its warning, and the `link-source-unreadable` line that names the reason); and `validate-manifest.py`'s. The naming validator and `validate-manifest.py` do not ask for reasons, so they print what they printed. - **One more scaffold path changes with it, on purpose.** A tree the scaffold reached and could not read as a declaration for any other reason (a manifest that does not parse, or is not a mapping) used to say "not reachable here" too, and was no truer. It now says what it is, with the system's reason, and a test for each of the two (a manifest that is not a mapping, one that does not parse) holds it, at the scaffold's command line and in process: narrowing the wording to the not-a-list reasons fails both. - `_declaration` still returns its four values, so nothing else in `scaffold-project.py` moved. - **A qualified chain link in the spy test** (nit 2). `_chain_read_cases` had no case whose chain link is qualified, so `reads_link_declarations` could take the chain as typed and not through `_chain_links` (R3) and pass every test. With `--referent-chain testorg/openXdox,openDox` that turns a verified chain into a declared-unverified one. `("codexDox", {"openXdox"}, ("testorg/openXdox", "openDox"), True)` is a case now, and R3 fails it. - **The description** (nit 3): the ten corrections of the re-review are applied (the reason a lookup returns, the fifth NOTE reason, the third kind of silent source, the SHOULD-1 bullet, "before Python 3.14", the `validate-pins.py` bullet, the 3.14.7 rows, the merge-tree section, the filed numbers, and the project's own `validate-manifest.py`). The mutation proofs: all three survivors were reproduced first at 4b21263 (210 passed each) and each dies at 2ac911c. Six mutations of the new code each die too: the warning ignoring the reasons (12 cases fail), the lookup recording none (12), the scaffold not asking (11), `with_reasons` on by default (3), an absent tree called reached (3), and the reached sentence used for a tree nothing reached (17). **Where it departs from #312, and why.** - **An explicit source that cannot answer stays a WARNING, exit 0.** #312 made an unreadable `--pin-source` a finding. Three reasons for the difference: - Exit 1 here means "a name is wrong", and this module has always said an unread link tree is a warning. - `SHAPE_PIN_SOURCE_<PRODUCT>` also answers `validate-pins.py`'s digest question, from a checkout that need not hold a manifest. - The lookup falls through to the next source, which may still verify the link. - **The fall-through is kept**, as it always was. With `--pin-source`, a keyed value is final. - **The lines go to stderr, spelled `WARNING`/`NOTE` as this file spells them.** #312's notes are ` note` lines on stdout, where that file's report lives. **Who else this reaches.** The guard is in `repo_shape.py`, so the same `link_pins_from_trees` lookup stops crashing for these callers too: - **The project's own `validate-manifest.py`.** It reaches the lookup through `SHAPE_PIN_SOURCE_<LINK>` or a sibling checkout, and now gives its usual declared-unverified WARNING instead of the traceback. - **`scaffold-project.py --referent-chain`.** A `SHAPE_PIN_SOURCE_<LINK>` under a locked parent no longer kills the plan, and `--pin-source` verifies the link. Neither of these prints the reason a source could not be entered; the scaffold says it only for a tree it reached and could not read (fix round 2, above). A `--pin-source` the scaffold cannot enter is still a traceback: that is #339 (`pin_digest_from_source`), and this PR does not touch it. **What this does not reach, and what the doctor says.** - **The project's gates still end in a traceback on the sibling row.** The NAMING and MANIFEST steps of `make validate`, `validate.yml` and `make bootstrap` stop crashing for a checkout `../openXdox` at mode 000. Those targets also run `validate-pins.py`, whose `resolve_neutral_pin_source` asks `(sibling / ".git").exists()` with no guard (`templates/assembly-root/scripts/validate-pins.py:463` on main), so before 3.14 a sibling at mode 000 still ends them in a `PermissionError`. That file is #312's, and `fix/pin-source-env-collisions` (PR #409, since landed), `fix/adopt-pin-writes-pin-file` and open PRs #343 and #419 touched it when this was written, so this PR does not edit it. The same line is listed for Python 3.14 by #430 (lane 2), where the helper that would cure both halves is asked for; the pre-3.14 traceback reproduction is added there as a comment, not filed apart. A `SHAPE_PIN_SOURCE_<LINK>` under a locked parent is different: `validate-pins.py` makes that a `neutral-pin-source-unreadable` FINDING (#312), and that stays. - **The doctor's `naming` row** runs the STANDARD's copy of the validator, so it stops crashing at once. **Its `manifest` row** runs the project's OWN `validate-manifest.py` on the project's own `repo_shape.py`, so it changes only once the project has applied this change. Over an existing project the doctor says `INVALID (manifest, pins)` until `update-shape.py apply` lands the two `upstream-changed` rows below. - **The verdict.** For `SHAPE_PIN_SOURCE_<LINK>` under a locked parent, on a project that carries these copies, `INVALID (naming, manifest, pins)` becomes `INVALID (pins)`, and `pins` is #312's finding for that locked variable, which is right. For a sibling at mode 000 the `pins` that remains is the traceback above, not a finding. The two copied scripts this change edits (`scripts/repo_shape.py` and `scripts/validate-repository-naming.py`) are copied into every project and pinned there. A project takes the change through `update-shape.py` (two `upstream-changed` rows), and a family holder takes the `repo_shape.py` row. Neither file has a second copy in this repository. **Evidence.** - **Issue rows, main ab623dd** (`repro338.sh`, a `gh` stub first on PATH). Under 3.10.21 and 3.12.3, rows 4-7 (keyed under a mode-000 parent, `SHAPE_PIN_SOURCE_OPENXDOX` there, a sibling at mode 000, a 300-character name) are a traceback, exit 1. Rows 2-3 (`a=b`, the typo) are a silent declared-unverified, exit 0. Under 3.14.7 row 1 verifies, and rows 2-7 are the silent declared-unverified WARNING, exit 0. - **Issue rows, this PR.** Under 3.10.21, 3.11.16, 3.12.3, 3.12.14, 3.13.15, 3.14.7 and 3.15.0rc2 (first commit), all 7 rows exit 0 with no traceback, and the output is byte-identical across versions. `a=b` verifies. The other five rows each name their source with its reason. Re-run on the fix-round head, 4b21263, under 3.10.21, 3.12.3 and 3.14.7: byte-identical to the first commit's run. Re-run on the fix-round-2 head, 2ac911c, under the same three: all 7 rows exit 0 with no traceback, the output is byte-identical across the three versions, and it is identical to the re-review's runs of the previous head over main. - **Reach on main.** Main gives a `PermissionError` traceback, exit 1, for `scaffold --dry-run`, the project's `validate-manifest.py`, `validate-repository-naming.py --project` and its `--quiet` form, each with `SHAPE_PIN_SOURCE_OPENXDOX` under a locked parent. This PR gives exit 0 for all four. `validate-pins.py` on the sibling row is the exception said above. - **New tests.** 17 cases (11 functions) in the first commit; 16 fail on ab623dd. The fix round adds 31 cases in `tests/test_naming_policy.py`, and 17 of them fail on 40762ca: the five shapes of pins that are not a list (each at the CLI, in-process and, for the scalar, under `--project`), and the chains nobody reads. The other 14 guard what a mutation could break (below) or the side the fix must not move (an absent, null or blank pin list still answers). - **Targeted run on 4b21263**, files `test_naming_policy`, `test_manifest_validator`, `test_scaffold_referent_chain_links`, `test_cli_reference`, `test_repo_hygiene`, `test_neutral_pin_validator`, `test_scaffold_e2e`, `test_shape_doctor`, `test_git_output_decoding` and `test_pin_validator`: **765 passed, 3 skipped**. The 3 skips are the PowerShell-only rows. (40762ca: 734 passed, 3 skipped.) `render-cli-reference.py --check` is ok. - **Mutation probes, first commit.** Reverting the stat guard, the parse, keyed-over-directory, the NOTE lines, the chain's-end restriction, the sibling-absence filter, the manifest reason, the `--quiet` drop, the "read as a PATH" sentence, the `./` line, the project-chain read, the WARNING lines or the env reporting each fails at least one new test. - **Targeted runs on 2ac911c (fix round 2).** Under 3.12.3, `test_naming_policy` (238 cases: 210 and 28 new), `test_cli_reference`, `test_manifest_validator`, `test_scaffold_referent_chain_links`, `test_repo_hygiene` and `test_setup_project_py`: **468 passed**; and `test_scaffold_e2e`, `test_scaffold_pin_and_reuse`, `test_scaffold_leg_path_collisions`, `test_scaffold_pin_collisions`, `test_neutral_pin_validator`, `test_git_output_decoding` and `test_pin_validator`: **312 passed**. Under 3.10.21 and under 3.14.7, `test_naming_policy`, `test_manifest_validator` and `test_scaffold_referent_chain_links`: **321 passed** each. `render-cli-reference.py --check` is ok, and `ast.parse(feature_version=(3, 9))` passes on the three changed files. - **Mutation probes, fix round.** The review's ten, re-run on 4b21263: MW1, MW2, MW3, MW5 and MW11 now die (they survived), and MW4, MW7, MW12 and MW13 still die; MW6 is the dead test, removed. (Fix round 2 kills the three the re-review found, R1 to R3, and six more: see above.) Nine more for the new code (the list check, the entry check, the null and blank tolerances, `reads_link_declarations` always true, without the shape check, without the held pins, and the validator ignoring it): eight die. The ninth, `reads_link_declarations` ignoring whether the name has a referent, survives as an equivalent mutant: with no referent `_chain_shape_problem`'s last question always reports a problem, so the explicit early return is redundant, and is kept because `resolve_referent` has the same first line. The spy test holds the two together. - **Windows.** The mode-000 rows skip on Windows and as root, using a `SEARCH_DENIED` mark this file defines, with the same condition as #312's. The new FIFO and `/dev/null` rows skip where those do not exist. `a=b` and the 300-character name run everywhere. On Windows the long-name row asserts the line, not POSIX's reason. None of it had been run on Windows or macOS when this was written; the last paragraph gives the CI result. **Merge-tree.** Recomputed at 19:04Z with `git merge-tree --write-tree` of 2ac911c against `origin/main` as it was then, 53bee5b, and against every one of the other 37 pushed branches that touches a file this PR changes (a scan of each head against its merge-base with main): - **Clean:** origin/main 53bee5b (it carries #314 and #390); #394 `fix/adopt-record-submodule-placement` 563ea16 (`scripts/repo_shape.py` and `validate-manifest.py`) and #406 `fix/validate-family-unnamed-mounts` 369af2e (`scripts/repo_shape.py`); #397 abb27e6, #400 7062a47, #401 409f10b, #416 7cca975, #452 e2df9a8 and #395 1ef3310 (`docs/cli.md`); #355 6fdbe3d, #373 741cbf2 and #395 (`scaffold-project.py`). - **Conflict, four hunks each, all in `scripts/repo_shape.py`:** `fix/pin-source-env-collisions` (PR #409) 6494714, and `fix/adopt-pin-writes-pin-file` 421e4e5 (no PR). Both also touch `scaffold-project.py`, and that merges clean with this PR's one edit there. Fix round 2's new hunks merge with neither: #409's change to the "not reachable here" sentence (the `offered` text, when a variable is shared) and the reached branch sit in different lines of `_chain_link_findings`. The four hunks, each taking both sides: 1. the `resolve_link_source` signature: `default_source=None, env_shared: bool = False, *, tried: list | None = None`; 2. its docstring: both paragraphs; 3. the `link_pins_from_trees` docstring and the lines after it: both paragraphs, then `found`, this PR's `unreadable` and #409's `links` and `shared`; 4. the call: `resolve_link_source(name, root, keyed_sources, default_source, pin_product_key(name) in shared, tried=asked)` (the same spelling on both branches), followed by `if tried is not None: tried[name] = asked`. Resolved that way in a throwaway and committed there, the merge with 6494714 passed **496 tests** (`test_naming_policy`, `test_pin_source_env_collisions`, `test_neutral_pin_validator`, `test_scaffold_referent_chain_links`, `test_manifest_validator`, `test_cli_reference`), and the merge with 421e4e5 passed **559 tests** (the same files and `test_pin_source_unreadable`). `render-cli-reference.py --check` is ok on both, pyflakes is clean, and the grammar parses as 3.9's. Neither branch carried the resolution when this was measured. #409 has since landed, and the merge of main into this branch (the last paragraph) takes those four hunks both ways. The earlier counts (468 and 434 from the first fix round, 430 from the re-review) are those runs' own, at their heads. **Left out.** Each of these was found by lane 5's reviews of this change, and each is filed or folded: - **A bare `--link-source` answers for every link with no more specific source** (#441). In a chain of three or more links, one bare tree is read as two links' declarations, and a chain that holds is reported broken, a FINDING for a name with no direct pin on its referent. Pre-existing, the same on main; `validate-pins.py`'s `--pin-source` has the same documented rule and was not examined. - **An empty path after a key (`--link-source openXdox=`) is `Path("")`, the current directory** (#442). Run from a project root, the project's own `project.yaml` is read as the link's declaration. Pre-existing; `validate-pins.py`'s `--pin-source openXdox=` has the same shape (there a finding naming `.`). - **`validate-pins.py`'s `(sibling / ".git").exists()` is unguarded** (the sibling row above), folded into lane 2's #430, which already covers the Python 3.14 half: the pre-3.14 traceback and its reproduction are added there as a comment. #312's file. - **The project's OWN manifest** with `neutral_product_pins: 5` ends in a `TypeError` in two gates before either can say anything about it (#443): `_pins_from_project` in the naming gate (`scripts/validate-repository-naming.py:302` on main), and `_declared_pins` in the project's own `validate-manifest.py` (`templates/assembly-root/scripts/validate-manifest.py:85`), which ends it before its own `manifest-neutral-product-pins` finding (`_neutral_product_pins_type_finding`, `:524` on main) can print. Reproduced on 3ef059c. That is the project's own declaration, read by the project's own gates, and not a link source, so it is outside this change; "read by the project's own gates" covers two crashing gates. Also not done here: - **Python 3.9 (the floor) was not run** because it is not installed here. The grammar of the three changed files was checked with `ast.parse(feature_version=(3, 9))`. Windows and macOS were not run here; CI ran them (the last paragraph). Reviewed adversarially at 40762ca, the branch's first commit (CHANGES NEEDED: 0 blockers, 2 should-fixes, 4 nits). The head stopped every crash in the issue's table, and the should-fixes were a docstring claim the code did not keep for a `neutral_product_pins:` that is not a list, and reach claims that overstated what the project's gate does on the sibling row. Fix round 1 (4b21263) resolved every finding, and the re-review of 4b21263 read APPROVE WITH NITS (0 blockers, 0 should-fixes, 3 nits: three behaviour changes this description had not stated, two of them untested; a spy-test case for a qualified chain link; and description corrections). Fix round 2 (2ac911c) resolved those three, and its re-verification of 2ac911c read APPROVE WITH NITS (0 blockers, 0 should-fixes, 2 nits: no test of the scaffold's wider wording for a manifest that is not a mapping or does not parse, now pinned by two tests that each fail when the wording is narrowed, and a cross-reference in this description that pointed the wrong way, now corrected). Main (9736424, #409's squash) was merged into the branch as ced8fac. `scripts/repo_shape.py` conflicted in four hunks; each takes both sides, as the review of 2ac911c resolved it: `resolve_link_source(..., default_source=None, env_shared: bool = False, *, tried=None)`, both docstrings kept, `link_pins_from_trees` opening with `found`, `unreadable`, `links` and `shared`, and the call passing `pin_product_key(name) in shared` with `tried=asked`. Nothing else conflicted. After the merge, `tests/test_pin_source_env_collisions.py` and the targeted set passed (667 and 312), and `render-cli-reference.py --check` printed `[ok]`. CI is green on Linux, macOS and Windows, with SonarCloud passing, at 938a131 (main 48585e6 merged in). Its merge with main 994be39 passed the targeted set (the naming, manifest, scaffold, pin and setup test files): 983 passed. Closes #338 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… writes its pin file as the scaffold does, so an adopted root passes `validate-pins.py` (#273); a `--pin-source` that cannot answer is `pin-source-unreadable` (#339) (#508) **Brought up to date after #409 squash-landed** by merging origin/main (9736424, #409's squash) as f9ed46f. Its `scaffold-project.py` conflict was resolved to this branch's side once `git diff b70a07d 9736424 -- scaffold-project.py` (#409's final head against its squash) came out empty. So the file is the one tested on this branch plus main's own later changes to it, and holds nothing of #409's; details below. This branch carries #275's adopt half. It was built on `fix/pin-source-env-collisions`, PR #409 (#275's scaffold half and its fix round), merged at 6494714 as 583f647, over the earlier 5690235 (34dbd86). After 6494714, #409 only merged main, five times, up to its final head b70a07d, and none of those merges has a hand edit (`git show --remerge-diff` is empty for each). Adopt's refusal of two products that share a source variable goes through #409's function, so this branch lands after #409. It was never rebased. `git diff b70a07d 9736424` is empty for each file #409 touched: `scaffold-project.py`, `scripts/repo_shape.py`, `templates/assembly-root/scripts/validate-pins.py` and `tests/test_pin_source_env_collisions.py`. What f9ed46f resolves, four files with one hunk each: - **`scaffold-project.py`.** - The conflict hunk is main's block from `PIN_COLLISION_REMEDIATION` through `_pin_collision_remediation`, which #409's squash edits: it adds `PIN_VARIABLE_REMEDIATION`, `_colliding_pin_variables` and `_pin_collision_remediation` and changes `_refuse_colliding_pins`. This branch moved all of that to `scripts/shape_materialize.py`. The hunk takes this branch's side. (The merge commit's message says "re-adds"; this is the exact account.) - #409's auto-merged import of `PIN_SOURCE_ENV_PREFIX, pin_source_env_collisions` is dropped, because nothing in the file uses either name any more. - It is not `git checkout --ours` of the whole file. Main changed the file beyond #409 since this branch last merged it: #355's `--tracking-branch` check (6c2a371) and a73b4e9's `RULESET_HINT`. Those lines stay. - The result is the file `git merge-tree` gives for this branch with main before the squash (518010b). Its diff from the file tested here has the same patch-id as main's own 3ef059c..518010b change to it. - **`templates/assembly-root/scripts/validate-pins.py`.** Main's side, which is #409's final blob, with main's #359 workflow reader in it. This branch had no change of its own to the file beyond #409. - **`adopt-project.py`, `_commit_the_split`.** - Main: #445 wraps the function in its late-failure `try`, and #394 builds an `append` with the plan's submodule record. - This branch: its only edits there were a comment and `neutral_pins=_recorded_pins(plan)` on the materializer call. - Both now sit on main's call inside the `try`: `append=append, neutral_pins=_recorded_pins(plan)`. - **`docs/cli.md`.** It is regenerated with `scripts/render-cli-reference.py`, and `--check` is ok. It differs from main's only in the `--pin`, `--pin-owner` and `--pin-source` help of `adopt-project.py plan`. `scripts/repo_shape.py`, `templates/assembly-root/scripts/validate-pins.py` and `tests/test_pin_source_env_collisions.py` are main's blobs, which are #409's: this branch adds nothing to them. Nothing of #409 is lost. Its scaffold behaviour is `shape_materialize.refuse_colliding_pins` and `pin_variable_collisions`, keyed by #409's `pin_product_key`. **73d65c6, after the merge.** - **The change.** `_read_pins` reads `plan.get("pins")` where it read `plan.data.get("pins")`. - **Why.** #445's `tests/test_adopt_late_failure.py` hands `_commit_the_split` a `SplitPlan` as its plan, a `dict` that adds `entries`. On f9ed46f, three of its tests stopped at "committing the split" with `AttributeError: 'SplitPlan' object has no attribute 'data'` (3 failed, 83 passed in that file). - **Nothing else changes.** `Plan.get` with no default returns `plan.data.get(key)` exactly, so a loaded plan reads as before. `adopt-project.py plan --pin` took a NAME only (`--pin openGlass`) and refused a trailing `@<commit>`. `plan`, `check` and `execute` each exited 0, and `execute` printed `adoption verified`. The manifest then listed the name in `neutral_product_pins:` with no `contracts/<product>-pin.yaml` beside it, and on its first run the adopted root's own `validate-pins.py` refused it as `neutral-pin-missing`. #273's table, reproduced on c7c02fa with the issue's own commands (local bare remotes, and a stub `gh` that exits 97): | `--pin` | c7c02fa | this branch | |---|---|---| | `openGlass` | `execute` exit 0, `- openGlass`; clone's `validate-pins.py` exit 2 `neutral-pin-missing` | `plan` exit 2 `pin-malformed`, with the scaffold's remediation (`--pin openGlass@<40 hex>`); no plan is written | | `opensoft/openGlass` | exit 0, `- opensoft/openGlass`; exit 2 `neutral-pin-missing` for `contracts/opensoft/openglass-pin.yaml` | `plan` exit 2 `pin-malformed` | | `spec` | exit 0; `validate-pins.py` exit 0, reading the leg's `contracts/spec-pin.yaml` as the neutral pin | `spec@<sha>` is `adopt-pin-collides` at `plan` | | `shape` | exit 0; `contracts/shape-pin.yaml` read as the neutral pin | `shape@<sha>` is `adopt-pin-collides` at `plan` | | `openGlass@<sha>`, `opensoft/openGlass@<sha>` | `plan` exit 2 `adopt-pin-malformed` | `plan`, `check` and `execute` exit 0, and `gh` is called 0 times. `contracts/openglass-pin.yaml` is written and the manifest lists `- openGlass`. In the clone, `validate-manifest.py` exits 0, and `validate-pins.py` exits 0 with `openGlass: pin commit … digest recomputes` | Brett ruled for decision 1's first option ("go with your recommendation on #273"): adopt takes the scaffold's pin and writes the scaffold's pin file. That reverses the NAME-only `--pin` from #6, and the comment that described it is replaced with the new contract. - **One implementation.** The scaffold's pin code moved into `scripts/shape_materialize.py`, next to `neutral_pin_path` and the materializer that writes the file. What moved: - `PIN_ARG_RE`, `parse_pin`, `pin_digest_from_source` and `pin_digest_from_gh`; - `declared_pin_values`, `declared_pin_products` and `PIN_COLLISION_REMEDIATION` (in #309's wording); - from #275's branch, `PIN_VARIABLE_REMEDIATION` and the variable check; - `_refuse_colliding_pins`, split into the pure `pin_collisions` and `pin_variable_collisions`, and `refuse_colliding_pins(refusal=...)`, which raises one refusal for both. The code did not move verbatim. `_refuse_colliding_pins` became the three functions above; `pin_digest_from_source` was rewritten for #339 (below); and fix round 1 changed three of them (below): `pin_variable_collisions` keys each pin by #275's `pin_product_key`, `pin_digest_from_source` spells a path that is not printable, and `pin_digest_from_gh` refuses a missing `gh`. The scaffold's behaviour is otherwise unchanged. `scaffold-project.py` imports what it calls under its old names. Lane 5's review ran 34 scaffold cases (every pin refusal, the `gh` stub's four modes, dry and real successes) on 34dbd86 and on this branch at 7a94b6b. rc, stdout, stderr, the `gh` log and the files made were identical, apart from the shape's own commit and digest and #339's row. Its pin tests and #275's pass as they are. `tests/test_git_output_decoding.py`'s exemption for `pin_digest_from_gh` follows the function to its new file. - **`plan`.** `--pin`, `--pin-owner` and `--pin-source` mean what they mean for the scaffold. `plan` reads the digest from `--pin-source` or `gh api`, and records product, repository, commit, digest and `digest_source` in the plan, so `check` and `execute` read no network for a pin. `plan` refuses: - a name with no `@<commit>`, as `pin-malformed`. That includes an empty `--pin ''`, which adopt used to drop and now refuses, as the scaffold always has; - `spec`, `code` and `shape`, in any case, as `adopt-pin-collides` (#273 decision 4), in the scaffold's sentence and remediation (#241, #309); - one product given twice, the same way, whether it is a repeat or two names that share one file (decision 5); - two products that one `SHAPE_PIN_SOURCE_<PRODUCT>` names, such as `open-glass` and `open_glass`, the same way, through #275's check; - a leg path over a pin's file, as `adopt-leg-path-collides`; - a `--pin-source` path that `check` would refuse in `digest_source:` (fix round 1), as `adopt-unwritable-value`. It is asked only when a `--pin` records the path; - with no `gh` on PATH and no `--pin-source`, `gh-not-found` (fix round 1). - **`check` and `execute`.** These are new `check` findings, and `execute` refuses each one before any leg exists. They come from `_leg_findings`, the list `check` and `_refuse_what_check_finds` share: - `plan-pin-unrecorded`: a name alone (an old plan's `pins: [openGlass]`, or a hand edit), or an entry missing its commit or digest; - `plan-pin-malformed`; - `plan-pin-collides`, which covers two pins at one file, a pin at a file the shape writes, and two products that share one source variable; - `plan-root-holds-pin`: a source path kept in the root at the pin's file. The materializer would write the shape's pin beside it under `shape/`, and `validate-pins.py` would read the source's file instead. How each finding ends: - `plan-pin-unrecorded` and `plan-pin-malformed` end with the `plan --pin <product>@<40 hex commit>` that records the pin whole. - `plan-pin-collides` ends with re-running `plan` with each product once, or, for two products that share a source variable, with one product of each such group. - `plan-root-holds-pin` ends with the kept path's own remedy: send that entry to a leg, `drop` it, or rename it in the source and re-run `plan`. A leg moved over a pin's file in the plan is `plan-leg-path-collides`. - **Also part of #386: its `pins:` row.** A hand-edited `pins:` that is not a list (a name such as `pins: openGlass`, a number, a mapping, a quoted string) is now `plan-pin-malformed` in `check` (`` `pins:` is 'openGlass', not a list of pins ``), and `execute` refuses it before any leg exists. - Where: `_read_pins` in `adopt-project.py`; the test row is `plan-pin-malformed-not-a-list` in `tests/test_adopt_pins.py`. - Before: on main, `check` said `plan ok` for the name, the mapping and the string, and gave a traceback for the number (reproduced by lane 5's review on cb7063f). #386 records that `execute` then wrote the name as eight one-letter pins. - Not fixed here: #386's other keys, `allow_empty_legs:`, `paths:`, `legs:` and `source:`, so #386 stays open. Lane 2's #435 takes all of #386, `pins:` included, and its `pins:` rule keeps only text items where this branch writes mappings; whichever lands second reconciles them (#435 (comment), and "Where the meaning meets" below). - **`execute`.** It writes each pin through `materialize_assembly_root(neutral_pins=...)`. The manifest lists the product with the owner stripped (decision 3), so the entry and the file agree. - **Docs.** `--help` is updated and `docs/cli.md` regenerated (`render-cli-reference.py --check` ok). `pin_digest_from_source` caught only `Refusal`. A directory that is no clone is git's own failure, so it was always `pin-source-unreadable`, exit 2. A path `subprocess.run` cannot enter never reaches git: it is a `FileNotFoundError`, `NotADirectoryError` or `PermissionError` out of `cwd=`, and nothing caught it. `scaffold-project.py --pin ... --pin-source <that>` ended `--dry-run` and the real run alike in a traceback, exit 1. Since the move above, `adopt-project.py plan --pin` reads its digest through the same function and ended the same way. Both reach it through `declared_pin_values`, and `check` and `execute` read no source, so that call is the only reader. The issue's table, run on ab623dd for the scaffold and on d806e8f for `adopt-project.py plan` (adopt has no `--pin-source` on ab623dd), then on this head. `gh` is a stub that logs and exits 97, with `GH_TOKEN` bogus and an empty `GH_CONFIG_DIR`; it was called 0 times in every run: | `--pin-source` | before | this branch | |---|---|---| | a directory that is not a clone | `pin-source-unreadable`, exit 2 | unchanged | | a path that does not exist | `FileNotFoundError` traceback, exit 1 | `pin-source-unreadable`, exit 2: `<path> could not answer for opensoft/openGlass @ <40 hex>: no such directory` | | a file | `NotADirectoryError`, exit 1 | exit 2, `... it is a file, not a directory` | | under a parent this user cannot enter | `PermissionError`, exit 1 | exit 2, `... Permission denied` | | a symlink loop (not in the issue) | `RuntimeError` out of `Path.resolve()` on 3.12, exit 1 | exit 2, `... Too many levels of symbolic links` | The real run and `--dry-run` give the same stdout and stderr, and nothing exists afterwards (no remote directory, no work directory; for adopt, no plan file). What changed, in `scripts/shape_materialize.py` only: - `pin_digest_from_source` catches `OSError` as well as `Refusal` and raises the existing `pin-source-unreadable` with its remediation unchanged. The message names the path and the reason. - `stat()` judges the source inside the guard, as #312 does in `validate-pins.py`, and the sentences are that file's: a missing path, or one beneath a file, is "no such directory"; a file is "it is a file, not a directory"; any other `OSError`, from `stat()` or from git's `cwd=`, is the system's own reason (`strerror`). It is not `exists()`/`is_dir()`, which before 3.14 swallow only a missing path. A source `stat()` can read but `cwd=` cannot enter, a directory with no search bit, fails in `tree_digest` and the same guard answers it. - `declared_pin_values` resolves the path through a guarded `_resolved_pin_source`. `Path.resolve()` raises a `RuntimeError` on a symlink loop before 3.13 (an `OSError` since), before the digest was asked for. As typed, `stat()` meets the loop and says so. - A source that answers is read exactly as before: a relative path resolves against the working directory, a symlink to a clone is followed, `digest_source` reads `local-clone (<path as typed>)`, and the digest is `tree_digest`'s. `tests/test_pin_source_unreadable.py` (46 tests in the #339 commit; fix round 1 adds 17 for S1 and moves one, below) runs each case through `scaffold-project.py --dry-run`, its real run and `adopt-project.py plan`: missing, a file, beneath a file, relative and missing, a parent and a source with no search bit (skipped on Windows and as root, by a mark, so before any fixture builds a directory), a 300-character name, a symlink loop (skipped on Windows) and a directory that is no clone. Each asserts exit 2, the refusal naming the path and the reason, its remediation, no traceback and nothing created. It also holds `--dry-run` and the real run to the same stdout and stderr, checks the path is printed escaped under an ASCII stderr and as a surrogate when it is not UTF-8, and checks that a relative source and a symlink to a clone read as before. The session `gh` guard fails any test whose run called `gh`. origin/main ab623dd reached this branch through implQ's branch: 34dbd86 is a30091f merged with ab623dd. One file conflicted there, `templates/assembly-root/scripts/validate-pins.py`, in seven hunks, because #312 (8cdca5e) replaced `keyed_sources` and `default_source` with `PinSources` and #275 added `env_shared` and `shared`. #312's blob is the same at its PR head, at the squash and at ab623dd, so it landed the file implQ resolved against. The resolution keeps #312's side in all seven and adds #275's on top: | # | hunk | resolution | |---|---|---| | 1 | `resolve_neutral_pin_source` signature | #312's `sources: PinSources` and 3-tuple return, plus #275's `env_shared: bool = False` | | 2 | its docstring tail | #312's `Returns (path, how, typed)` text, plus #275's "Rule 2 is skipped when `env_shared`" paragraph | | 3 | `_recompute_neutral_pin_digest` signature | #312's `sources`, plus #275's `shared: frozenset[str]` after `report` | | 4 | the `resolve_neutral_pin_source(...)` call inside it | #312's 3-tuple unpack, plus `env_shared = product.casefold() in shared` and the extra argument | | 5 | `_check_one_neutral_product_pin` signature | #312's `sources`, plus #275's `shared` | | 6 | its call of `_recompute_neutral_pin_digest` | `sources, report, shared` | | 7 | `_check_neutral_product_pins` docstring and body | #312's `PinSources(...)` and `_note_unused_pin_sources`, plus #275's paragraph, `_report_pin_source_collisions(products, report)` and `shared = frozenset(pin_source_env_shared(products))` | This branch's own merge of 34dbd86 (5690235) had no conflict, and neither had its merge of #409's fix round, 6494714 (583f647). That fix round made `neutral-pin-source-collides` a note and keyed "one product" by `repo_shape.pin_product_key`. At 421e4e5, `templates/assembly-root/scripts/validate-pins.py`, `scripts/repo_shape.py` and `tests/test_pin_source_env_collisions.py` were byte-identical to the tree `git merge-tree` gives for 6494714 with origin/main 3ef059c (blobs 5604007, 091966b, 5220787), so this branch added nothing to those three files beyond #409. The 34dbd86 merge brought in #335, #312, #317, #270 (f795cf7), #344 and #349 with no conflict. Earlier merges of origin/main (87373a6, for #309's reworded constant, and e43fa95) are unchanged. Fix round 1 merged origin/main three more times: - **cd1fc79 (2bcfa69).** It brought #245, #351, #350, #345, #372, #371, #354, #380, #377 and the rest since ab623dd, with no conflict. - **45e01f4 (4098b61).** It brought #396's `tests/test_agents_md_codes.py`. That test finds `scaffold-pin-collides` as a string constant of `scripts/shape_materialize.py`, which `SHIPPED` lists. - **3ef059c (421e4e5), #314.** One conflict, one hunk in `adopt-project.py`. This branch's `_refuse_an_unrecordable_pin_source` and #314's section "The project must be able to hold what `execute` writes into it (#261)" were added at the same place; both are kept. Where the two meet, lane 5's earlier recipe applies: - `_flag_plan` builds each pin as the `PIN_FIELDS` mapping `plan` writes, not `list(pins)` of products. So the plan `plan` asks the validator about is the plan it writes. - #314's `test_plan_asks_what_check_asks_of_the_same_keys` plans `--pin openGlass@<commit> --pin-source <stand-in>` where it planned the name alone, which `plan` now refuses. Then f9ed46f, after #409 squash-landed, and 73d65c6 after it (top of this description). On the merged tree exactly one test failed: `tests/test_adopt_template_values.py::test_check_finds_and_execute_refuses_a_hand_edited_value[pins-surrogate]`. It plans, then hand-edits the plan to `pins: ["openX\ud800"]`, a name-only pin with a lone surrogate. Before this branch the refusal's headline was `REFUSED plan-unwritable-value`. A name-only pin in a plan is now `plan-pin-unrecorded` (#273), so `check` has a second, independent finding for that very plan, and the headline is the first finding's: ``` REFUSED plan-pin-unrecorded: 2 finding(s) ... FINDING plan-pin-unrecorded: `pins:` entry 1, 'openX\ud800', is a name with no commit and no digest ... FINDING plan-unwritable-value: a `pins:` item is 'openX\ud800', which `execute` writes into the assembly root's `project.yaml`, and it holds a lone surrogate ... ``` Nothing about #270's contract changed. The lone surrogate is still found as `plan-unwritable-value`, listed under the refusal, and `execute` still refuses before any leg exists. `check` builds `_leg_findings`, where `_pin_findings` sits, before it calls `_value_findings`, so the pin finding comes first. d806e8f changes one assertion for that row, `pins` only: the headline is `REFUSED plan-pin-unrecorded: ` where it was `PLAN_REFUSED`. The other 15 rows and the finding assertions, including the `plan-unwritable-value` line, are untouched. Lane 5's review of 7a94b6b found 3 should-fix and 5 nits. Each is answered here: - **#409's `pin_product_key` (review N1).** The merge of #409's fix round could not carry it, because this branch had moved the function that used it out of `scaffold-project.py`. `pin_variable_collisions` now keys each pin by that function, where it casefolded. `openß` and `openss` are two pin files and one `SHAPE_PIN_SOURCE_OPENSS`, and a casefold named each of the two pins twice. No `--pin` or recorded `pins:` entry `PIN_ARG_RE` admits can spell either, so the test asks the function directly. `refuse_colliding_pins`' docstring now says, in #409's words, that the root's `validate-pins.py` only notes such a pair. - **S1, a `--pin-source` path that is not printable.** `pin_digest_from_source` names the path as typed unless it is not printable (`str.isprintable`), and then as Python's `repr` spells it (`shown_source`). That covers the refusal's head and git's own reason, which names the directory it ran in. Printed raw, an ESC reached the terminal, and a line break or a carriage return began a line of the path's choosing, a forged `REFUSED` line among them, from the scaffold and from adopt. The traceback #339 replaced had shown a `repr`. A printable path is named byte for byte as before. - **S2, no `gh` on PATH.** `pin_digest_from_gh` refuses as `gh-not-found`, exit 2, as `repo_shape.tree_digest_from_gh` names it, and offers `--pin-source`. It was a `FileNotFoundError` traceback, exit 1, from the scaffold and, new with this branch, from `adopt-project.py plan --pin`. - **S3, a hand-edited `commit:` or `tree_sha256:`.** The lane's decision is to accept it and document it (option (c)); no behaviour changed. A pair that disagrees passes `check` and `execute` (`adoption verified`), because neither reads a network for a pin, by design. The adopted root's own `validate-pins.py` then exits 1 with `neutral-pin-digest-mismatch` wherever it can read the product. `check` now says so once, after the pins it will write: "each pin as `plan` read it, not read again here: the root's own `validate-pins.py` recomputes its digest wherever it can read the product". Options (a), `execute` recomputes before it creates the legs, and (b), `check` recomputes from a clone still on the machine, are filed as #449. - **N2, a `--pin-source` that `plan` cannot record.** `plan` asks `check`'s question (`one_printable_line`) of the path it records in each pin's `digest_source:`, before any digest is read. It refuses one that fails as `adopt-unwritable-value`, in #270's words (`unwritable_reason`), or as "an invisible formatting character" where those name nothing. Before, it was planned, exit 0, and printed raw in the report. `check` then refused it as `plan-pin-malformed` and said to re-run the same `plan`. The report's `pin` line and `check`'s `write` line now spell the digest's source with `_shown`. The scaffold half is pre-existing (a line break in the path is written into the pin file; a byte that is not UTF-8 dies after all three repositories exist) and is filed as #450. - **N3, six hunks no test guarded.** Each now has a test that fails when it alone is reverted: - the two name guards (a `pins:` name holding an ESC and a forged line); - the lower-casing of the commit and the digest (an upper-cased hand edit); - the `digest_source` printability check; - the not-text check (`commit: 1234567` unquoted); - the gitlink filter (a submodule kept where the pin is written is `plan-submodule-root` alone). - **N4.** This body. - **N5, #339's third decision.** A `--pin-source` that nothing reads is still silent, now in adopt too. This PR does not decide it. #451, filed, lays out refuse, note and leave, so closing #339 loses nothing. Lane 5's second review, of 421e4e5, found no blocking item, 2 should-fix items and 3 nits. The two should-fix items are this body: the landing after #409 (above) and #386 (above). The code needed no change for them. f63d1ed adds two tests and no code: - **n1. `plan` asks nothing of a `--pin-source` no `--pin` records.** With the `args.pin and` guard dropped, such a path holding an ESC or a zero-width joiner was refused in a sentence naming no file ("`execute` writes into , and ..."), and every test still passed. `test_a_pin_source_no_pin_records_is_not_asked_about` holds the plan to exit 0 and `pins: []`, with the path named nowhere in the plan or the report. The ESC row is POSIX-only by the file's `POSIX_NAMES` mark; the zero-width joiner row runs everywhere. It holds #451's answer today, silence, so a decision there changes this test on purpose. - **n2. A printable `--pin-source` is printed as it is.** With `_spelled` in place of `_shown` in `plan`'s `pin` line, every test still passed. `test_a_printable_pin_source_is_printed_as_it_is` holds `plan`'s `pin` line, and `check`'s `write` line for the plan `plan` wrote, to their exact text, ending `(local-clone (<path>))` with no backtick. - **n3** needs a code change and is left out (below). **Fix round 2, on f63d1ed.** Each run had a `gh` stub first on PATH (it logs and exits 97), `GH_TOKEN` bogus, an empty `GH_CONFIG_DIR`, `git-filter-repo` 2.47.0 next on PATH, and the identity blanked with `GIT_CONFIG_COUNT`; the session `gh` guard came first inside each run. - **The pin set: 812 passed, 2 skipped.** It covers adopt pins, adopt plan, #339's file, #275's tests, the pin and neutral-pin validators, git output decoding, adopt template values, CLI reference, scaffold pin and reuse, scaffold pin collisions, #314's two files and #396's file. The skips are `test_adopt_plan.py:403` (no MedxEHR clone) and `test_adopt_template_values.py:223` (this host reads `git config user.name` from the repository, not from `GIT_CONFIG_GLOBAL`). That is fix round 1's 809 passed, plus the three new rows. - **Shape doctor and scaffold leg-path collisions: 369 passed, 3 skipped** (the PowerShell line exists only on a Windows host). - **Mutation probes**, each one line changed in a throwaway worktree, over `tests/test_adopt_pins.py` and `tests/test_pin_source_unreadable.py` less the five end-to-end adoptions: - the `args.pin and` guard dropped: 2 failed, both n1 rows; - `plan`'s `pin` line `_spelled`: 1 failed, the n2 test; - `check`'s `write` line `_spelled`: 2 failed, the n2 test and `test_an_upper_cased_hand_edit_is_written_lower_cased`; - `plan`'s `pin` line raw: 1 failed, the no-break-space test that already guarded that hunk. **On the merged tree, 73d65c6** (the merge f9ed46f and the `_read_pins` commit), with the same harness: - **The pin set** above, with #445's `test_adopt_late_failure.py`, `test_adopt_git_reads.py` and `test_adopt_filter_repo_failure.py` and #387's `test_adopt_empty_symlinks.py` and `test_adopt_root_manifest.py`: **988 passed, 2 skipped**. The skips are the two above. - **Shape doctor, scaffold leg-path collisions and the adopt suites:** **967 passed, 3 skipped** (the PowerShell line). The adopt suites are e2e, execute refusals, verify, root shape paths (#387's too), leg-path collisions, clone template, ignored shape files and submodules. - **On the merge alone, f9ed46f** (the same tree as before the `_read_pins` commit): `tests/test_adopt_late_failure.py` gave 3 failed, 83 passed, the three `AttributeError` rows above. - `render-cli-reference.py --check` is ok on the merged tree. pyflakes reports the same four warnings on `adopt-project.py`, `scaffold-project.py` and `scripts/shape_materialize.py` as before the merge. **Fix round 1, on 421e4e5.** Each run had `git-filter-repo` 2.47.0 on PATH and the identity blanked with `GIT_CONFIG_COUNT`. A `gh` stub that logs and exits 97 was first on PATH, with `GH_TOKEN` bogus and an empty `GH_CONFIG_DIR`, and the session `gh` guard came first inside each run. - **The pin set:** 809 passed, 2 skipped (the same files and skips as above). - **Doctor and scaffold:** 782 passed, 3 skipped (the PowerShell line exists only on a Windows host). It covers shape doctor, scaffold leg-path collisions, scaffold e2e, referent chain links, naming policy, repo hygiene and #351's free-text values. - **The adopt suites:** 882 passed. It covers e2e, execute refusals, verify, spec-only, leg paths, root shape paths, plan escapes, leg-path collisions, clone template and submodules. - **Before the #314 merge,** on 74ce4d9's tree: the first two sets gave 577 passed, 2 skipped and 782 passed, 3 skipped. On 2bcfa69's tree (the merges alone, before the fix) they gave 542 passed, 2 skipped and 782 passed, 3 skipped. After the 45e01f4 merge, `tests/test_agents_md_codes.py`, repo hygiene and CLI reference: 110 passed. - **Lane 5's second review** also ran 421e4e5 merged with main cb7063f and with 53bee5b: the pin set, the manifest-value set, and the doctor set with #357's `tests/test_doctor_validator_rows.py`. All passed, with only the skips above. Proof that fix round 1's tests test something: - **On 2bcfa69's tree,** with this head's two test files laid over it, 31 of the new rows fail: - S1: 17; - N2 and its report spelling: 7; - S2: 3; - the `pin_product_key` port: 2; - `check`'s S3 line: 1; - the not-UTF-8 `adopt plan` row: 1. The N3 rows pass there, as they must: they guard hunks this branch already had. - **Mutation probes.** Each hunk was reverted alone, in a throwaway worktree, and both test files were run: 18 of 18 were caught. - **The port:** the casefold keying (2 fail); the casefold lookup alone (2). - **S1:** the path named raw (13); git's reason named raw (2); every path `repr`'d, printable or not (27). - **S2:** no `except` (3). - **N2:** no `plan` check (11); only #270's question asked (1); the formatting-character reason said otherwise (1); the report's `pin` line raw (1); `check`'s `write` line raw (1). - **S3:** the line dropped (1). - **N3:** the `_pin_fix` name guard (1); the `_read_pins` path guard (1); the lower-casing (1); the `digest_source` check (1); the not-text check (1); the gitlink filter (1). - **The issue rows reproduced.** S1, S2 and N2 were reproduced on 2bcfa69's tree and shown gone on the fix (a probe driving both tools; `gh` was called 0 times). **Before fix round 1.** Runs were on 7a94b6b, with `git-filter-repo` 2.47.0 first on PATH and the repository-local `user.name` blanked with `GIT_CONFIG_COUNT` (a workstation whose standard checkout sets one fails `test_the_scaffold_refuses_with_no_elector` on main itself; CI sets none). 7a94b6b differs from ffd1297 in `tests/test_pin_source_unreadable.py` only, so the three suites that do not read that file ran on ffd1297: - the new file: 46 passed; - adopt pins, adopt plan, git output decoding: 172 passed, 1 skipped (the MedxEHR clone is absent); - scaffold pin and reuse, pin collisions, e2e, referent chain links, neutral pin and pin validators, #275's tests, naming policy, repo hygiene, CLI reference, scaffold leg-path collisions: 571 passed; - shape doctor: 254 passed, 3 skipped (the PowerShell line exists only on a Windows host); - the new file with repo hygiene, git output decoding and CLI reference, once more after the last edit: 176 passed. Earlier on 5690235 and d806e8f, before the #339 commit: the adopt suites (e2e, execute refusals, verify, spec-only, leg paths, root shape paths, plan escapes, leg-path collisions, clone template) 740 passed, and `tests/test_adopt_template_values.py` with #273's other adopt tests 489 passed, 2 skipped. They were not re-run on 7a94b6b: the #339 diff touches `pin_digest_from_source` and the call in `declared_pin_values`, which those suites reach through `test_adopt_pins.py` only. Nothing was run on macOS or Windows. Proof the new tests test something: - **On d806e8f.** 38 of the 46 fail, each on the traceback and its exit 1. The 8 that pass are the unchanged behaviour: the not-a-clone refusal (4 rows) and the valid-source rows (4). - **Mutation probes.** Ten hunks reverted one at a time, each failing at least one new test: the `OSError` catch (16 fail), the catch narrowed to `OSError` only (21), the missing-path clause (9), the clause narrowed to `FileNotFoundError` (3), the `S_ISDIR` judgement (3), the loop guard (4), no resolve at all (3), the old `.resolve()` call (4), the reason as `str(exc)` (12), and the file sentence as the OS's (3). - #273's own 52 tests still fail on c7c02fa (51 fail or error; the one that passes checks the test's own table), and its 18 mutation probes were each caught. **Re-verified after the merge.** An independent re-verification of f9ed46f and 73d65c6 found the code correct: - #409 is carried whole. A 30,000-case fuzz of `--pin` lists through #409's final refusal and this branch's port differed in 0 cases. - `scaffold-project.py` equals the tested file plus main's own changes that are not #409's (same patch-id). - `_commit_the_split` keeps #445's `try` and #394's `append`. - `_read_pins` reads a loaded plan as before. Its findings were all in this description (the #506 bullet and three stale facts), now fixed. Runs on the head and on its merge with main 48585e6: 988 passed, 2 skipped; and 967 passed, 3 skipped. `git merge-tree --write-tree` was run at 01:48Z on 2026-10-13, comparing hunks per file. Each PR head, fetched from `refs/pull/<n>/head` with no `gh`, was merged with 73d65c6 and with origin/main 9736424 alone. Two sets were asked about: - every PR this description listed before that main does not now contain; - every PR numbered above #468 whose head main does not contain: #473, #474, #490, #491, #493, #494, #495, #496, #497, #503, #504 and #505. **Open state, read with `gh` by the coordinator at 02:00Z and refreshed at 02:25Z after a re-verification on main 48585e6.** Every pull request named below is open. #184, #94, #96, #98, #100 and #161 are closed, and #465 landed as 48585e6. `fix/adopt-plan-quote-yaml-keywords` has no pull request. #506 (`fix/scaffold-reuse-seeded-legs`) opened after this list was made, and the re-verification reads it. - **Landed since the last draft of this list:** #343, #346, #355, #358, #373, #378, #382, #387, #394, #397, #401, #404, #409, #419, #452, #456, #465, #466 and #484. #394's `_commit_the_split` conflict and #401's `docs/cli.md` conflict are both resolved in f9ed46f, above. - **Clean, 20:** #266, #347, #356, #374, #376, #384, #395, #453, #467, #468, #473, #474, #490, #491, #493, #494, #495, #497, #503 and #505. #453, #468 and #491 were updated onto main since the first read and now merge clean. #507 (lane 5's #486-#488) shares only the generated `docs/cli.md` with this branch and merges clean with it. - **Conflicting with main alone, and this branch adds no hunk:** - #402: `shape-doctor.py` 5; - #496: `shape-doctor.py` 10 and `tests/test_shape_doctor.py` 1. **Conflicts this branch adds:** - **#435** (3efcc7a, lane 2's `fix/adopt-plan-field-types`, for #386). - It adds 2 hunks in `adopt-project.py`: 5 here, against 3 with main alone. They are `render_plan`'s `pins:` line and `Plan.pins`. - Keep this branch's `_pin_lines` and `_pin_products`. The rest is under "Where the meaning meets". - **#504** (37ef29a, `fix/adopt-root-kept-symlinks`). - It adds 1 hunk in `adopt-project.py`: 2 here, against 1 with main alone. It is the materializer call in `_commit_the_split`. - Keep both arguments and both comments: `append=append, neutral_pins=_recorded_pins(plan), spell=_spelled`. - **#506** (119d8c4, `fix/scaffold-reuse-seeded-legs`). - It adds 1 hunk in `scaffold-project.py`. With main alone, #506 conflicts only in `docs/handbook.html`. - #506 inserts `LEG_TEMPLATES` right after `declared_pin_products`, which this branch moved out, and rewrites `_reusable_remotes` further down. Keeping both sides breaks the file with an unterminated string literal. - Keep only #506's `LEG_TEMPLATES` block. Drop #506's `declared_pin_values`/`declared_pin_products`, which moved here, and this branch's three-line `_reusable_remotes` opener, which #506 rewrites below. Resolved that way, pyflakes is clean and #506's scaffold tests pass (88). - **`fix/adopt-plan-quote-yaml-keywords`** (ed9968f; a branch with no pull request). - It adds 2 hunks in `adopt-project.py`: 3 here, against 1 with main alone. - One is `render_plan`'s `pins:` line. Keep `_pin_lines`, which already sends product, repository and `digest_source` through `y`. - The other is `_refuse_unholdable_values`' signature. Keep `pins: dict`, with that branch's `_validator_judged` and `_named_as_read` above it. - `fix/adopt-late-failure-notice` is no longer on origin. #445, the late-failure notice, is on main and merged here. **Where the meaning meets:** - **#435 and #386.** The coordinator has said this on #435: #435 (comment) - #435's `_plan_key_findings` (`plan-bad-pins`) keeps only text items in `pins:`, and its `PINS_REMEDIATION` tells a person to write `pins: [openGlass]`. - This branch writes each pin as a mapping, and it makes a name alone `plan-pin-unrecorded`. - Read from #435's code, not run: on a tree with both, the plan `plan --pin` writes is `plan-bad-pins` in `check`. - Whichever lands second reconciles them, with three changes: - `_plan_key_findings` takes this branch's mappings, or leaves `pins:` items to `_read_pins`; - a `pins:` that is not a list gives one finding, not both `plan-bad-pins` and `plan-pin-malformed`; - `PINS_REMEDIATION` says the mapping `plan` writes. - Until that is done, `tests/test_adopt_pins.py::test_a_plan_with_a_whole_pin_checks_ok` fails. - **#314.** It landed, and it is merged here (above). - **`fix/adopt-plan-quote-yaml-keywords`.** Its pin tests are written against the name-only `--pin`. They need the `name@commit` form, and their expected `pins: [...]` line becomes `_pin_lines`'s mapping. - **Two unwritable values are refused in two runs (lane 5's second review, n3).** `plan --elected-by 'A<LF>B' --pin openGlass@<sha> --pin-source <a path holding an ESC>` refuses the elector alone, exit 2. Re-run with the elector retyped, it refuses the source; #270's refusal names every unwritable value at once. Reproduced on 421e4e5, with `gh` called 0 times. One refusal would need a code change, not made here: - `_refuse_an_unrecordable_pin_source` would split into a function that returns the sentence; - `_checked_plan_inputs` would ask it before #270's `unwritable_values` raise, and raise one `adopt-unwritable-value` that names both, with both remediations; - a test would add the two-value row. N2's question is `one_printable_line`, not `unwritable_reason`, so it cannot simply join #270's list. - **Docs.** The README, AGENTS.md and `docs/handbook.html` are unchanged, because none of them calls adopt's `--pin` a name. A sentence saying adopt now takes the scaffold's `--pin` is a follow-up. It costs an AGENTS.md line-cap raise, and a README edit forces the handbook's README blob to change. - **A product spelled `null`, `true` or `false`.** The scaffold already accepts `--pin null@<sha>`: it exits 0 and builds a root whose `validate-pins.py` exits 1 `neutral-pin-entry-malformed`. Adopt's `plan` writes `product: null` through `y`, and `check` then finds `plan-pin-malformed` and refuses it before any leg exists. The one fix for both tools is in `parse_pin`, and it would change the scaffold's behaviour. - **Wording shared with the scaffold.** `digest_source` is written bare into the pin file, so a `--pin-source` path holding ` #` is cut short there. `templates/assembly-root/contracts/neutral-product-pin.yaml` and `validate-pins.py`'s `neutral-pin-missing` remediation name only the scaffold as the writer. Two comments in `scripts/repo_shape.py` still name the scaffold's `pin_digest_from_gh`, in a digest-pinned shipped copy. - **Filed as follow-ups:** - #449: S3, a hand-edited `commit:` or `tree_sha256:` that disagrees, with options (a) and (b); - #450: the scaffold half of N2, a `--pin-source` path no file can hold; - #451: #339's third decision (N5), a `--pin-source` that nothing reads. - **`validate-pins.py` prints a `--pin-source` path raw** in `neutral-pin-source-unreadable`, the twin of S1. It is a shipped template that #409 and other open PRs edit, so it is left for its own change. No issue is filed for it yet. - **Windows and macOS** were not run here; CI ran both (below). Two assertions that depend on what Windows answers for a path (a name past the filesystem's limit, a path beneath a file) check only the refusal and its remediation there. Fix round 1's rows that need a control character or a byte that is not UTF-8 in a path, or a symlink to `git`, are skipped on Windows by a mark, and so is fix round 2's ESC row. The rows for a no-break space and a zero-width joiner run everywhere, and so does the in-process `gh-not-found` test. Reviewed independently by lane 5 at 7a94b6b (3 should-fix, 5 nits: fix round 1) and at 421e4e5 (0 blockers, 2 should-fix, 3 nits: fix round 2). After #409's squash, the re-verification of f9ed46f and 73d65c6 found the code correct; its 1 should-fix and 3 nits were in this description and are fixed. GitHub's update-branch then merged main twice (33bae3f, 2170526), with no hand edit. CI's tests, tests-macos and tests-windows and SonarCloud all passed at 2170526 (main cac6bb2 merged in). On that head merged with main 57c85e9 (#490, #474 and #468, none of which touches this branch's files), the pin and adopt set with `tests/test_update_shape_landing.py` gave 1112 passed, 2 skipped, and the doctor, adopt end-to-end, unfinished-merge and bootstrap set gave 1056 passed, 3 skipped (the doctor's PowerShell line, which exists only on a Windows host). Closes #273 Closes #339 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…5 cites the 34 pull requests merged since #343, section 6 drops the 13 known-limit issues that closed and adds the 63 checklist lines the five parents gained, section 7 lists the 12 open milestone issues, and the preflight sentence offers only what the preflight offers (#472) Refresh round 1 of the draft, against main d41cc29 (#402, merged 2026-10-13T21:17:39Z). GitHub was read between 21:23 and 21:43 UTC: the merged pull requests, the v1.0.0 milestone open and closed, every open issue, and the five tracking parents with their sub-issues, bodies and comments. Every merge commit of the 217 merged pull requests is on main. Section 5 places each of the 34 pull requests merged after #343 (2026-10-12T22:31:20Z) in its area, from its own body, one clause each; all 217 merged pull requests are now cited, with #16, #53 and #57 still in two areas. Three earlier sentences are made true of main: `check` has more verdicts than the four it named, and #465's docs and #505's accuracy pass show that `validate-pins.py` may ask `gh api` and that `openRepoShape --preflight` offers only `gh`, its login and its credential helper (`setup-project.py --preflight` also offers `git`). Section 6 lists the 47 open, unmilestoned sub-issues (it listed 60). It drops #189, #293, #303, #328, #386, #286, #259, #275, #322, #324, #338, #339 and #359, which closed. #389 is reworded now that #305's refusal has landed. #420 and the `a=b` line are narrowed to a hand-edited `adopt_branch:`, because an offline probe on d41cc29 shows #493's `plan-tracking-branch-missing` now stops a bad `tracking_branch:` at `check`, with no leg made, while `adopt_branch: "a..b"` still passes `check` and fails after both legs exist. The family annotated-tag line says `bump --to` now refuses one (#466). The 63 new lines are the parents' checklist lines posted or added since the first build, each checked against the code at d41cc29. Left out, and said so in the refresh record: docstring, comment and wording nits (as L2-7 was), lines that describe a branch not on main (#504's, #507's, #511's, #515's and #517's), lines filed into the milestone (#485, #514, #516), and three with no effect a person meets. Section 7 lists the 12 open milestone issues; 24 of the 30 it listed have closed. Lane 4's #510, #511, #512 and #513 have not landed, so #471, #499, #500, #501 and #502 stay open here and the Windows qualifier stays. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-2 (openRepoShape-2)
Claim: #290 (comment)
What
A shape pin whose
files:is not a list is now refused or reported by name in all four places that read it, instead of being iterated:files: 5/true/1.5beforeupdate-shape.pyread_pin:check,apply, and the doctor's shape-currency rowTypeError: 'int'('bool','float')object is not iterable, exit 1REFUSED update-pin-row: <pin>: `files:` is 5, not a list of `path:`/`sha256:` rows, so no copied file can be read from it, exit 2, and a remediation about the pin that namesgit -C <root> log -- contracts/shape-pin.yamltemplates/assembly-root/scripts/validate-pins.py_check_shape_pin(the root's own validator)FINDING shape-pin-row: contracts/shape-pin.yaml: `files:` is 5, not a list ..., exit 1templates/family-root/scripts/validate-family.py_check_shape_pin_files(the holder's own validator)shape-pin-rowfinding, exit 1shape-doctor.pypinned_pathsread_pincrashed first)shape-doctor.pytherefore prints a report again for such a root. Its shape-currency row carries the refusal, and itspins(orfamily) row carries the validator's finding. Main now carries #267 (decff87), so the verdict is INVALID, exit 1, and the shape-currency row's NEXT is the git command that puts back the last pin that reads (checkout HEADfor an uncommitted break,checkout '<bad>~1'for a committed one). Before #267 it was CANNOT ANSWER, exit 3. Neither verdict is set here: #267 sets them.Two choices made in
read_pin:files: 0,false,nulland[]keepupdate-pin-no-filesandshape-pin-no-files. The issue left this to the author, and the guard only covers whator []let through. A control test holds this.update-pin-rowraises carrypin_rows_remediation(root). Since shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267 they sit incheck_pin, which now takes the root for it (see "Merged main after shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267 landed"). It says: put back the pin a tool last wrote, from the root's own history (git -C <root> log -- contracts/shape-pin.yamlnames the commit that changed it), and run again. Before this, they printedrepo_shape's default trailer, which is aboutgit submodule updateand the lockstep invariant. The doctor's row quotes only the refusal's detail, never its remediation.Another change you can see: a string
files: abcused to produce oneshape-pin-rowfinding per character (three). It now produces one finding, andupdate-shape.pykeeps the same id,update-pin-row, with a message that namesfiles:.Why
Every reader took
pin.get("files") or []and looped over it.or []catches the falsy values, so they reach theno filesrefusal or finding. A truthy scalar got through and was iterated. On main 71b58a1,files: 5committed over a clean scaffold madeshape-doctor.pyexit 1 with nothing on stdout. The doctor exists to say what is wrong, and here it said nothing.The two validators are pinned template copies (
COPIED_VERBATIM,FAMILY_COPIED_VERBATIM), so this changes their digests. Main has changed both files too since this branch began (#446validate-pins.py, #406validate-family.py), so the digests below are named per revision:git grep -E '\b[0-9a-f]{64}\b'at this head finds only the actionlint download checksum in.github/workflows/tests.yml. A scaffold, an adoption or afamily.py initcomputes the digest of the bytes it copies.scripts/validate-pins.pyas56c122cd…, and one made from this head, e68a01d, records5f5b992a…. Each is the sha256 of the template at that revision. Forscripts/validate-family.pythe two are94a7b837…and29f05dbc…. (Before the main merge:61f13a77…at 71b58a1 andc909bddc…at 9e6caeb forvalidate-pins.py;905887a6…and758a0959…forvalidate-family.py.)upstream-changedrow until they runapply. Checked before the main merge, against 441fedc (whose validators are byte-identical to 9e6caeb's): a project cut at 71b58a1 getsupstream-changed 1 / unchanged 10(scripts/validate-pins.py), and a holder getsupstream-changed 1 / unchanged 9(scripts/validate-family.py).applymoves the row toc909bddc…, and the project'svalidate-pins.pythen sayspins ok.update-shape.pyandshape-doctor.pyare not pinned copies, andscripts/repo_shape.pyis not touched.Tests
tests/test_shape_pin_files_not_a_list.py(new): 28 tests. Most are parametrised overfiles: 5,files: true,files: 1.5andfiles: abc. Each case commits that value over a clean scaffold (conftestproject) or a holder made byfamily.py init.update-shape.py checkandapplyexit 2 withREFUSED update-pin-row, the value, the pin remediation with itsgit -C <root> logcommand, and nosubmodule. A refusedapplyleaves the pin's bytes unchanged.shlex.join, asupdate-shape.pyspells it, never written as a literal, so it holds on Windows, where a backslash path is quoted.update-pin-rowraise (afiles:list whose row is5, notpath:/sha256:), which had no test before.scripts/validate-pins.pyexits 1 with exactly oneshape-pin-rowfinding.shape-doctor.py --json: the shape-currency row is a FINDING witherror: update-pin-row, and thepinsrow namesshape-pin-row. The text report shows the value and ends on a verdict line. The tests check the row, not the verdict.shape-doctor.py --jsonwith noAGENTS.md: theagent-filesrow is reached, withpinned: [].pinned_pathson the scalar pin, in process, returnsset().validate-family.pygives oneshape-pin-rowfinding,update-shape.py checkrefusesupdate-pin-row, and the doctor's shape-currency row andfamilyrow say the same.files: abcis the one value in that table that is iterable, so a guard writtenisinstance(files, (list, str))lets it through and each reader takes its characters for rows. It is a case of its own in every parametrised test: each validator reports exactly oneshape-pin-rowfinding, andupdate-shape.pyrefuses with the whole-files:message (`files:` is 'abc', not a list).files: 0andfiles: falsestill getupdate-pin-no-filesandshape-pin-no-files.Against main's code (origin/main 92e29d9, this test file copied onto it): 25 failed and 3 passed. 19 fail with the TypeError (
'int','bool'or'float'object is not iterable), 5 arefiles: abc(three findings from a validator where one is expected, or the per-row refusal where the whole-files:message is expected), and 1 is the non-mapping-row test, which wants this PR's pin remediation. The 3 that pass are thefiles: 0andfiles: falsecontrols, by design, andpinned_pathsonabc, which iterates a string without error. This head merged with main 92e29d9 (no #267): 28 of 28 pass.Mutants, scratch clones of 9e6caeb, the head before the main merge, run one at a time against the 28-test file (baseline 28 passed). Each
isinstance(..., list)guard changed toif False::read_pin→ 17 failed,validate-pins.py→ 7 failed,validate-family.py→ 4 failed,pinned_paths→ 6 failed. Each of those three guards widened to(list, str), which brings back one finding per character:validate-pins.py→ 1 failed,read_pin→ 4 failed,validate-family.py→ 1 failed (all three passed the 22-test file at 039530b). For the remediation: dropping the-C <root>pair from the printed command → 10 failed; the secondupdate-pin-rowraise passing the wrong root → 1 failed (the non-mapping-row test, the only one that reaches it); the first raise losing its remediation → 9 failed. All ten are killed.Targeted runs at 9e6caeb, before the main merge, with git-filter-repo on PATH, one file set per run (the runs at the merge head, e68a01d, are under "Merged main after #267 landed"):
tests/test_shape_pin_files_not_a_list.py28,tests/test_update_shape.py77,tests/test_update_shape_add.py17,tests/test_update_shape_add_ignored.py26,tests/test_update_shape_landing.py33,tests/test_update_shape_pr_base.py27,tests/test_pin_validator.py19,tests/test_neutral_pin_validator.py11,tests/test_cli_reference.py13,tests/test_repo_hygiene.py50: 301 passed.tests/test_shape_doctor.py: 242 passed, 3 skipped.tests/test_family.py99,tests/test_family_add_excludes.py23,tests/test_family_add_refusals.py37: 159 passed. These carry the shape-pin digest checks. A fresh scaffold'svalidate-pins.py(pins okintest_pin_validator.py) and a fresh holder'svalidate-family.py(test_the_family_passes_its_own_gate) recompute every pin row, including the rows for the two changed validators.test_update_shape.py'sapplycases leave the project's validators green.scripts/render-cli-reference.py --check:[ok]. No--helptext changed.tests/test_scaffold_e2e.py44,tests/test_validator_child_encoding.py51 andtests/test_windows_paths.py38 passed at 441fedc together withtest_family.pyandtest_family_add_refusals.py(269 passed). They were not repeated:git diff 441fedc 9e6caeb -- templates scriptsis empty, and the commits since touchupdate-shape.pyand the new test file only.Composes with
git merge-tree --write-tree e68a01d <head>, run 2026-10-12T20:49Z, against this lane's open heads as the shared store had them at 20:4xZ (gh pr listwas not re-read):--tracking-branchis asked ofgit check-ref-format --branchbefore anything exists, so~,a..b,x.lockanda/are refused under--dry-runand no longer die at the firstgit init -baftergh repo createhas made the repositories (#283) #355 16073e2, scaffold-project.py: a leg path the project's own YAML reader would read back as another type (null,~,true,False,2026,1.5) is refused before anything is created, under--dry-runalike, through the one definition adopt-project.py asks, so--spec-path 2026no longer creates three repositories whose first bootstrap failsmanifest-leg-path, andadopt-project.py executeno longer saysadoption verifiedbefore that bootstrap fails (#225) #266 3410be7, update-shape.py:apply --addrefuses a path under a file the root keeps asupdate-add-not-a-directorybefore it writes anything, and a write that fails after the copies isupdate-write-failed, rolled back, so neither leaves a rewritten copy and an unmoved pin behind a traceback (#285) #346 6d4e1e9, shape_advisory.py and shape-doctor.py: anoriginURL that is not UTF-8 names nothing, and themembersrow reads it asmake siblingsdoes, so a single repository is still advised, a leg clone is still sent to its root, andshape-doctor.py --rootno longer ends in a UnicodeDecodeError traceback (#385) #404 d74d862, adopt-project.py:plan --helpsays what--spec-pathand--code-pathare, the default and key each records, and the two refusalsplanmakes on them, sodocs/cli.mdno longer lists the two flags that choose where a leg is mounted as bare lines (#340) #401 da27e69, validate-pins.py (assembly-root template): a workflow file under.github/workflows/that is not UTF-8, or that this user cannot open, is ONEworkflow-unreadablerefusal naming every such file, exit 2, so the project's pins gate no longer ends in a UnicodeDecodeError traceback, exit 1, with no line naming the file (#359) #419 11dff7f (stacked on this PR) and update-shape.py: a pinned copy,contracts/shape-pin.yamlor the manifest kept as a symlink, or under one, is refused asupdate-rewrite-symlinkbefore anything is written, andcheckcalls such a copycopy-is-link, soapplyno longer writes the standard's bytes at the link's end and reports a commit that does not hold them (#286) #347 9423361.checkfinds, andexecuterefuses before any leg exists, a planfollow_ups:that is not a list of text and anelected_on:thatelection_datedoes not read, with or without areference:, sofollow_ups: 5is no longer a traceback andelected_on: somedayis no longer refused under a flag neither command takes (#293, #294) #356 10323a7 and adopt-project.py:checknames, andexecuterefuses before the work directory is made, a plan whosesource:orlegs:is not a mapping or whoseallow_empty_legs:orpaths:is not a list of whatplanwrites, and asks the same first of apins:that is not a list, so a number or a boolean in any of the four no longer ends both in a traceback andallow_empty_legs: codeis no longer read as four legs (#386) #435 3efcc7a: rc 1 inadopt-project.py. Main 580d7df alone gives the same (adopt-project.py: every failure once a leg repository exists flushes stdout, then printsREFUSED <step>., what failed and one notice of what the run left, so a failedgit filter-repono longer exits 2 with an empty stderr (#171, #188, #177) #445), so this change does not cause it.manifest kindsrow puts back a shape or leg pin undercontracts/that does not load with the git commandpin_restorepicks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-onlygit diff, and leaves any other file to a person whatever it is called, so its NEXT no longer tells the reader to delete the pin (#291) #358 ce2ce75: rc 1 inshape-doctor.py, as against main 580d7df alone, and here also inupdate-shape.pyandtests/test_shape_doctor.py. Those two conflicts lie exactly on this merge's composition lines. Both sides carry shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267's text from merge base 5678f68, and this side addsroottocheck_pinand the threefiles:rows toROOT_PIN_FAULTS. Whichever of update-shape.pyread_pinrefusesupdate-pin-row, and the pinned validate-pins.py and validate-family.py reportshape-pin-row, when a shape pin'sfiles:is not a list, sofiles: 5,trueor1.5no longer endscheck,apply, shape-doctor.py and the project's own validator inTypeError: ... is not iterable(#290) #343 and shape-doctor.py: themanifest kindsrow puts back a shape or leg pin undercontracts/that does not load with the git commandpin_restorepicks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-onlygit diff, and leaves any other file to a person whatever it is called, so its NEXT no longer tells the reader to delete the pin (#291) #358 lands second keeps this side there. Inpin_reads_at, which shape-doctor.py: themanifest kindsrow puts back a shape or leg pin undercontracts/that does not load with the git commandpin_restorepicks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-onlygit diff, and leaves any other file to a person whatever it is called, so its NEXT no longer tells the reader to delete the pin (#291) #358 rewrote to take apinargument, it appliesfix1-compose-343-resolution-358.patch(the lane's attachments,impl291-doctor-kinds-pin/tools/) instead of v2's hunk. Before this merge, x358 proved that variant on shape-doctor.py: themanifest kindsrow puts back a shape or leg pin undercontracts/that does not load with the git commandpin_restorepicks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-onlygit diff, and leaves any other file to a person whatever it is called, so its NEXT no longer tells the reader to delete the pin (#291) #358 together with this branch: the new file 28 passed,tests/test_shape_doctor.py304 passed and 3 skipped, and the sixtests/test_update_shape*.pyfiles 223 passed and 3 skipped.contracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267 is no longer open. It landed as decff87, and this merge composes it (below). Before it landed,merge-treecalled this branch and shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267 clean (rc 0) while the merged tree died at run time. A cleanmerge-treeis not the gate for this pair; CI on the merge head is.Left out
read_pinrefuses. Those are shape-doctor.pycheck_shape_currencytakes every refusalupdate-shape.py prepareraises over the ROOT's owncontracts/shape-pin.yaml(a merge conflict, acommit:that is not 40 hex, a deleted file) for this checkout failing to ask, so the verdict is CANNOT ANSWER, exit 3, outranking INVALID, and its NEXTgit -C <standard> fetch --allblames the standard for a fault in the repository #237 / shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267's (unreadable_pin_rowandpin_restore), unchanged here. With shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267 on main the row is INVALID with a restore command, where it used to be CANNOT ANSWER withgit -C <standard> fetch --all.files: 0andfiles: falsetoupdate-pin-row. They stayupdate-pin-no-files, as the issue allows.Fix round 1
Answers the codex review comment on this PR, #343 (comment) (P2). The remediation printed a bare
git log -- contracts/shape-pin.yaml. This tool is run from a checkout of openRepoShape with--root /path/to/project, so that command inspects the checkout, shows no pin history, and the person cannot follow the remediation.PIN_ROWS_REMEDIATIONis nowpin_rows_remediation(root), spelled with the module's own_by_hand(root, "log", "--", "contracts/shape-pin.yaml"), so it printsgit -C <root> log -- contracts/shape-pin.yamlwith the root quoted (a root holding a space is one argument). Bothupdate-pin-rowraises passroot. The rest of the text is unchanged.git -C <root> logshows the root's 2. The tests fail at 6e2689a (8 of 22) for that reason and pass at 039530b (22 of 22). Dropping the-C <root>pair fails 8.git logfor this refusal:shape-doctor.py,validate-pins.pyandvalidate-family.pycarry none. The validators run inside the root by design.Fix round 2 (review of 039530b)
contracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267: proved in this round, applied in the main merge e68a01d (below), after shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267 landed as decff87. Where shape-doctor.py: themanifest kindsrow puts back a shape or leg pin undercontracts/that does not load with the git commandpin_restorepicks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-onlygit diff, and leaves any other file to a person whatever it is called, so its NEXT no longer tells the reader to delete the pin (#291) #358 is in the tree, v2'spin_reads_athunk does not apply andfix1-compose-343-resolution-358.patchis the one to take (see Composes with).files:, fixed in7c998ff.files-abcis added toNOT_A_LIST, so the six parametrised tests run it. The three mutants that widen a guard to(list, str)passed all 22 tests at 039530b; at 9e6caeb they fail 1, 4 and 1.9e6caeb.pin_rows_remediation,cmd_applyand_awaits_its_pull_requestspell the pin's path by the materializer'sSHAPE_PIN, so the literal"contracts/shape-pin.yaml"appeared 0 times inupdate-shape.pyat 9e6caeb (3 at 039530b). After the main merge it appears once, in_refuse_what_head_does_not_commit, which update-shape.py: a--prre-run finishes the landing only when the working tree holds what HEAD commits at the pin, the manifest and every pinned copy, and is refused asupdate-rerun-uncommittedotherwise, so an in-placeapply --at Cleft uncommitted on a branch that re-pins B no longer gets B pushed under a pull request titled C (#333) #379 added on main (main 580d7df has 3), so the module stays under S1192's three copies. The tworoot / "contracts" / "shape-pin.yaml"joins inread_pinand_repinare left alone: update-shape.py: a pinned copy,contracts/shape-pin.yamlor the manifest kept as a symlink, or under one, is refused asupdate-rewrite-symlinkbefore anything is written, andcheckcalls such a copycopy-is-link, soapplyno longer writes the standard's bytes at the link's end and reports a commit that does not hold them (#286) #347 rewrites the top ofread_pin, and the import is its own line because update-shape.py and bump-leg.py: a pin rewrite keeps each line's own indentation, spacing and trailing comment and reads its result back before writing it, so adigests:child indented four spaces takes the update instead ofupdate-pin-unrecognised/bump-leg-pin-unrecognised, and a# commentoncommit:ortree_sha256:survives every pin rewrite,rewrite_manifestincluded (#323) #380 rewraps the import block.git submodule updatetrailer onupdate-pin-no-filesandread_pin's other refusals goes to repo_shape.py'sRefusalgives every refusal built without a remediation the assembly root's own (run git submodule update --init --recursive, then python3 scripts/validate-pins.py ... contracts/<leg>-pin.yaml), so in a family holderfamily.py addover an unparsable.gitmodules(git-failed),bumpon afamily.yamlof the wrong kind, and the holder's ownvalidate-family.pyon a shortpin.commiteach tell the operator to run a file no holder carries and to move a leg pin a holder does not have #370. The doctor's NEXT for this refusal is shape-doctor.pycheck_shape_currencytakes every refusalupdate-shape.py prepareraises over the ROOT's owncontracts/shape-pin.yaml(a merge conflict, acommit:that is not 40 hex, a deleted file) for this checkout failing to ask, so the verdict is CANNOT ANSWER, exit 3, outranking INVALID, and its NEXTgit -C <standard> fetch --allblames the standard for a fault in the repository #237 / shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267's.Merged main after #267 landed
Merge commit e68a01d (parents 9e6caeb and main 580d7df, the shared store's
origin/mainat 20:45Z). This is the branch's first merge of main, so it brings everything since its base 71b58a1, #267 (decff87) among it. Since fix round 2's proof on main 92e29d9, the main commits that touch this PR's files are #267 and five more: #446 (245d775,validate-pins.py), #406 (ec8e09a,validate-family.py), #379 (87d2eb7,update-shape.py), and #375 (cd1fc79) and #357 (5678f68), both inshape-doctor.py. The textual merge was clean:merge-treerc 0, andgit merge --no-ff --no-commitstopped with no conflict.git diff --stat 580d7df e68a01dlists only this PR's six files.Why not
update-branch. #267 splitread_pinintoread_pin(root)andcheck_pin(pin, pin_path). Theupdate-pin-rowraises moved intocheck_pin, and on this branch they callpin_rows_remediation(root), whererootis not a name. Shown on the unpatched merge, a scratch tree holding the same merge without the two patches:update-shape.py:468:34: undefined name 'root'and:474:48. The patched head gives no message.contracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267'stest_a_root_pin_that_does_not_read_is_invalid_not_cannot_answer: 19 failed and 18 passed. The new file has 18 failed and 10 passed, each of the 18NameError: name 'root' is not defined. shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267's owna-row-with-no-pathcase fails too: the doctor exits 1 with an empty stdout, so it has no report to read.The resolution is the planned #267 composition from fix round 2 (
fix1-compose-267-resolution-v2.patchandroot-pin-faults-after-267.patch), applied unchanged.git apply --checkwas rc 0. Main moved the hunks by offset only: 526 lines inshape-doctor.py, 521 intests/test_shape_doctor.pyand 1 inupdate-shape.py. No hunk needed adapting, and neither module has changed on main since decff87. The patches make these changes:check_pin(pin, pin_path, root), withread_pinpassing its root;pin_reads_atpassesctx.root, its docstring saysfiles: 5is anupdate-pin-rowrefusal, andTypeErrorleaves itsexcepttuple;contracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267'sROOT_PIN_FAULTSgainsfiles: 5,trueand1.5.The only
check_pin(callers areread_pinandpin_reads_at, and both pass three arguments.Targeted runs at e68a01d use
/usr/bin/python3 -m pytest -q -p no:cacheprovider -o tmp_path_retention_policy=failed, with git-filter-repo on PATH and a stubghfirst:tests/test_shape_pin_files_not_a_list.py28,tests/test_shape_doctor.py297 (shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267's root-pin test now has 12 cases) andtests/test_doctor_validator_rows.py28: 350 passed, 3 skipped, 0 failed.tests/test_update_shape.py82,_add17,_add_ignored26,_add_symlink41,_landing41 and_pr_base27;tests/test_validate_pins_without_git.py58,tests/test_pin_validator.py19 andtests/test_neutral_pin_validator.py25;tests/test_validate_family_duplicates.py20,_member_path_glob21,_unnamed_mounts39 and_without_git14;tests/test_family.py99,tests/test_family_add_excludes.py23 andtests/test_family_add_refusals.py65;tests/test_scaffold_e2e.py44,tests/test_validator_child_encoding.py55 andtests/test_windows_paths.py38;tests/test_cli_reference.py13 andtests/test_repo_hygiene.py50: 814 passed, 3 skipped, 0 failed.scripts/render-cli-reference.py --check:[ok].The pinned validators' digests move with main's own changes to them (see Why). CI on e68a01d, on all three OSes, is the gate.
Closes #290
🤖 Generated with Claude Code
Summary by Sourcery
Report malformed shape-pin
files:values clearly across all readers instead of allowing them to crash validation and diagnostic commands.Bug Fixes:
files:values consistently across update-shape.py, shape-doctor.py, and both pinned validators by reporting actionable refusals or findings instead of raising TypeError or iterating invalid values.Enhancements:
files:values as pinning no paths.files:values and report string scalars as a single malformed field.Tests:
files:values in assembly roots, family holders, update-shape commands, validators, and shape-doctor reports.