Repository navigation
shape-doctor.py: the manifest kinds row puts back a shape or leg pin under contracts/ that does not load with the git command pin_restore picks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-only git 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
Conversation
…t read as a pin (a merge conflict, a non-UTF-8 byte, a `commit:` that is not 40 hex, a deleted file) is `INVALID`, exit 1, with `update-shape.py check` as its NEXT, instead of `CANNOT ANSWER`, exit 3, sending the reader to `git fetch --all` in the standard (#237) `check_shape_currency` took every refusal `update-shape.py prepare` raises for this checkout failing to ask. `prepare` also reads the ROOT's own pin, and those refusals are about their repository. The doctor now reads the pin first with `read_pin` (the function `prepare` calls for it), so the two kinds are told apart by which read raised them, not by a code list or by text. A refusal from the pin is a FINDING with no `environment`, quoting `REFUSED <code>: <detail>` without its remediation (the same wording family #232 gives an unloadable manifest); a refusal from the standard stays `CANNOT ANSWER`, exit 3, unchanged. The NEXT is `update-shape.py check` with the file at fault named, not `validate-pins.py`: that validator passes a `digest_definition` this tool will not recompute, so there the shape-currency row is the only red one and the verdict is `INVALID (shape-currency)`. WIP state at commit time: the 13 new tests pass at this head and fail on main 39ac59c (`assert 3 == 1`); tests/test_update_shape*.py and tests/test_cli_reference.py are 133 passed; the full tests/test_shape_doctor.py run was still in progress with no failure so far. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…raph (#237) `tests/test_repo_hygiene.py` caps the README at 1416 lines and pins `docs/handbook.html` to the README's blob; the sentence took it to 1420 and moved the blob, and the doctor's README paragraph already says what stays true (exit 3 is a pin naming a commit THIS checkout does not carry). The argument lives in `unreadable_pin_row`'s and `check_shape_currency`'s docstrings instead. The previous commit's body says the README gained a sentence; this one removes it, and the branch has no README change against its base. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t read names the git command that puts back the last pin a tool wrote (`checkout --ours`, `checkout HEAD` or `checkout <commit>~1`, chosen from the pin's own git state), so the row's NEXT fixes it instead of refusing it again (#237) The review of PR #267 at 092a5f9 found the new row's NEXT a dead end (F1). `update-shape.py check` reads the pin with the same `read_pin`, so run as printed it exited 2 with the same refusal every time, and the remediation it then printed was wrong for the case: `git submodule update` for a conflict, "adopt it" for a deleted pin, "re-save the file" (a hand edit of a pinned file) for a stray byte. An operator who follows AGENTS.md ("Run the one the row names ... never edit a pinned file") was stuck, and README's "names the exact command that fixes it" was false for this row. `pin_restore` now picks the command from the pin's state, read-only: - unmerged (porcelain UU, AA, ...): `checkout --ours` (or `--theirs` where only that side holds a pin, read off the index stages, not the letters) of every conflicted path the shape writes: the pin, the manifest, README.md and the copies, never a file of the project's own; - uncommitted, where HEAD holds a pin: `checkout HEAD -- <pin>`; - otherwise, where the last commit to change the pin has a parent that holds one: `checkout <that commit>~1 -- <pin>`; - none of these: the old `update-shape.py check` NEXT, saying that whether the root was ever scaffolded or adopted is a person's call. `--json` records which, as `detail["restore"]`. Status is read as bytes with `--no-optional-locks`, so the doctor still writes nothing. N2: no note carries a `;`, an apostrophe or a backtick, so a pasted line is one command (#277). N1: the docstring names #232's `unloaded_manifest_row`, which is not on main or this branch. Tests run each printed command and ask the doctor again: the nine uncommitted faults and two committed ones (a `git rm` and a `commit: "zzzz"`, each followed by an unrelated commit) end COMPLIANT, exit 0; the issue's own merge, with `Makefile` and a file of the project's own also conflicted, names only `Makefile` and the pin and then reads the pin; a squashed root commit keeps the check NEXT. At 092a5f9 each of these fails, and head's NEXT run as printed exits 2. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… put back from before the commit that deleted it, and a merge where only the other side holds a pin takes `--theirs`, so the two `pin_restore` branches no earlier test reached are pinned (#237) `pin_restore` (7c11c62) has two choices that none of the round's first tests exercised: - `A ` in the status: the pin was deleted in a commit and a bad one staged at its path. HEAD holds no pin, so `checkout HEAD` names a path git refuses. The code asks `cat-file -e HEAD:./<pin>` before it says "uncommitted", and falls through to the last commit that changed the pin, `<that commit>~1`. The test asserts `committed`, the argv, and that running it ends COMPLIANT. It kills the mutant that drops the `cat-file` check (`elif xy:`). - `DU`: this branch deleted the pin and the other changed it. Only index stage 3 holds a pin, so `--ours` would refuse the path. The test asserts `checkout --theirs -- <pin>`, a note that says only that side holds a pin, and that the pin reads once it has run. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…te under `contracts/` that does not load (a conflicted or non-UTF-8 shape pin, a leg pin, the naming copy) with the git command `pin_restore` picks from its own state, and leaves a file of the project's own to a person, so its NEXT no longer tells the reader to delete the pin (#291) The row collected every file under `contracts/` that does not load as a mapping into `unreadable`, and gave it the unregistered kind's sentence: "register the kind in shape-doctor.py's MANIFEST_VALIDATORS, beside the check that validates it, or remove the file from the root". A file that does not load has no `kind:` to register, so the one half a reader could follow was to delete the file -- for a conflicted `contracts/shape-pin.yaml`, the pin. AGENTS.md tells an agent to run the NEXT a FINDING names, and the sentence is not even a command: the apostrophe in "shape-doctor.py's" leaves `shlex.split` with no closing quotation. `pin_restore` (#237, PR #267) takes the file to put back, defaulting to the shape pin, so its four states, commands and notes are unchanged for the `shape currency` row. `tool_written` admits every `contracts/<name>-pin.yaml` (the shape pin, a leg's, a neutral product's) and the shape copies the root kind lists, which under `contracts/` is the naming policy; each of those is put back from git, and only the shape pin takes the rest of the shape with it in a merge. A leg pin is named alone: `checkout --ours` on a conflicted gitlink exits 0 and changes nothing. A file of the project's own is not put back from git, because each of the three commands throws away somebody's edit. Its case is `project`, and where no file has a git command the NEXT is this doctor again, with a note that what the file should say is for a person to decide. The row quotes the loader's refusal for each file, less the path, and `detail` gains `restore` and `refusals` only when a file does not load. Where an unregistered kind sits beside a file that does not load, the row names the restore first; the next run names the sentence, which stays the unregistered kind's alone. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es and leaves a conflicted `project.yaml` or `README.md` to a person, and every pin it puts back out of history is read first, so the other side's own edits survive it and two breaking commits in a row, a sparse checkout or assume-unchanged no longer name a wrong rollback (#237) Fix round 2 on PR #267, answering lane openRepoShape-2's re-verify of 9f64bd0 (V1, V2). V1 (P2). The unmerged NEXT named the manifest and README.md beside the pin and the copies, and `checkout --ours` writes stage 2 whole. `apply` writes only two lines of the manifest's `shape:` block and README's `Shape:` line; every other line is the project's own, so the other side's edits to those files went, hunks git had merged cleanly included, and the doctor then said COMPLIANT. Now only the files the shape writes whole are taken: the pin, each `copy_source` key and its collision copy. A conflicted manifest or README.md is named in the note as a person's merge and listed in `detail["left_for_a_person"]`, as #232's manifest row already says of the same file. The review's optional refinement (naming either file where the dropped side changed only tool-written lines) is not taken. V2 (P3). The committed class named `<last>~1` whenever it held a pin. It never asked whether HEAD's pin was the broken one, or whether the pin it put back read. A sparse checkout or assume-unchanged over a good re-pin got a rollback of that re-pin, and two commits in a row that each broke the pin cycled between `checkout <last>~1` and `checkout HEAD`. - `update-shape.py`: `read_pin`'s checks move into `check_pin(pin, pin_path)`, which `read_pin` calls, so the doctor checks a pin out of git with the same code. - `pin_reads_at` reads `<rev>:contracts/shape-pin.yaml` with `cat-file blob` in memory (no temporary file: the doctor writes nothing), decodes and parses it as `load_yaml` does, and runs `check_pin`. - `uncommitted` needs HEAD's pin to read (the review's optional step 4, so a dirty copy over a broken HEAD goes straight to history). - New class `hidden`: HEAD's pin reads and status is clean. The NEXT is `git ls-files -v`, which shows the S or h, and no rollback is named. - `committed` walks `rev-list -n 50 HEAD -- <pin>` newest first and names the first commit whose parent's pin reads. `pin_restore(ctx)` still answers `(restore, command)`; it gains an optional `detail` it writes `left_for_a_person` into, so existing callers are unchanged. Tests: the README case and the issue's own two-`apply` merge (real re-pins from a copy of the standard with two more commits; shape/a also renames the project and adds a README paragraph), two breaking commits, a dirty copy over a committed break, assume-unchanged and a sparse checkout. All six fail at 9f64bd0 for the review's reasons. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… for byte as it found it, so dropping `--no-optional-locks` from the pin's `git status` fails a test (#237) Fix round 2 on PR #267, V3 of lane openRepoShape-2's re-verify of 9f64bd0. The doctor's first promise is that it never writes into the repository it is pointed at. `--no-optional-locks` is what keeps `pin_restore`'s `git status` from refreshing the root's index, and the review's mutant m7, which drops it, passed all 20 #237 tests. After a committed break, every tracked file is made stat-dirty; the doctor runs; `.git/index` must be byte-identical. A plain `git status` must then rewrite it, as the control. The test is the review's scratch test, renamed. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dable-pin: `pin_restore(ctx, detail=None, pin=SHAPE_PIN)` and `pin_reads_at(ctx, rev, pin=SHAPE_PIN)` serve the `manifest kinds` row too (#237, #291) Round 2 gave `pin_restore` an optional `detail`, the `hidden` state, a history walk that names only a pin that reads, and a merge that takes the pin and the copies whole and leaves a conflicted manifest or README.md to a person. #291 had given it the file to put back. Both are kept: `pin` follows `detail`, so every call #267 makes is unchanged. `whole` takes the copies and `left` names the manifest and README.md only for the shape pin, and any other file is named alone. `pin_reads_at` reads the shape pin with `check_pin`, and any other file `tool_written` admits as reading when it loads as a mapping, which is what the `manifest kinds` row asks of it. `unreadable_next` passes the file by keyword. For the shape pin every note is byte for byte round 2's. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s named alone and its note says nothing of README.md, so a `pin_restore` that gave every file the shape pin's "a person merges README.md" clause fails a test (#291) #267's round 2 has the unmerged NEXT name a conflicted manifest or README.md for a person, because `apply` writes a line of each beside the shape pin. No tool writes either beside a leg pin, so the merge keeps that clause for the shape pin alone. The leg-pin case now conflicts README.md too, and the test asserts the note does not mention it. With the guard removed the case fails (README.md is named in the note). Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 10 hours and 47 minutes by commenting |
Reviewer's GuideUpdates the manifest-kinds check to diagnose unreadable files separately from unregistered kinds and issue a single safe remediation command: restore tool-written pins or copies from Git, or rerun the doctor for project-owned files requiring human repair. The implementation generalizes pin restoration and adds focused regression tests covering Git states, ordering, notes, detail output, and composed behavior. Sequence diagram for manifest-kinds restorationsequenceDiagram
participant Doctor as shape-doctor.py
participant Loader as load_yaml
participant Git as Git state
participant Restore as pin_restore
participant Person as Person
Doctor->>Loader: load_yaml(path)
Loader-->>Doctor: Refusal or non-mapping
Doctor->>Doctor: tool_written(ctx, rel)
alt tool-written pin or copy
Doctor->>Restore: pin_restore(ctx, pin=rel)
Restore->>Git: inspect status and readable history
Git-->>Restore: restore state and command
Restore-->>Doctor: restore and command
Doctor-->>Person: print one restoration command
else project-owned file or no readable copy
Doctor-->>Person: print doctor command for manual repair
end
Person->>Doctor: run NEXT
Doctor->>Loader: load_yaml(path)
Loader-->>Doctor: readable mapping
Flow diagram for unreadable manifest remediationflowchart TD
A[check_manifest_kinds] --> B[load_yaml for each manifest]
B -->|mapping| C[Classify declared kind]
B -->|Refusal or non-mapping| D[Record unreadable reason]
D --> E{tool_written file?}
E -->|yes| F[unreadable_next]
E -->|no| G[restore project]
F --> H[pin_restore for the file]
H -->|Git copy available| I[Print one checkout or ls-files command]
H -->|No readable Git copy| G
G --> J[Print doctor command for human repair]
C --> K[Report validated or unregistered kind]
D --> L[Add restore and refusals detail]
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
@codex review |
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. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ebffb08280
ℹ️ 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".
| if rel.startswith("contracts/") and rel.endswith("-pin.yaml"): | ||
| return True |
There was a problem hiding this comment.
Restrict automatic restores to declared shape pins
When an adopted project owns a contract such as contracts/release-pin.yaml, this suffix check classifies it as tool-written even if no leg or neutral_product_pins entry declares it. If that file then has an uncommitted syntax error or merge conflict, manifest-kinds emits git checkout HEAD/--ours as its prescribed fix, discarding project-owned edits instead of leaving them for a person as intended. Determine tool ownership from the shape pin, manifest-declared leg/product pins, and copy_source, rather than every *-pin.yaml; this is especially important because the documented doctor procedure tells users to run the exact command in a finding.
AGENTS.md reference: AGENTS.md:L208-L214
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
(lane openRepoShape-2, 2026-10-12T17:08Z)
Fixed in 2745890. What the root declares now decides ownership, not a file's name. tool_written admits only declared_pins and the root kind's shape copies. declared_pins is the shape pin, contracts/<role>-pin.yaml for each leg project.yaml declares, and contracts/<product lowercased>-pin.yaml for each neutral_product_pins: entry. Anything else, such as an undeclared contracts/release-pin.yaml, gets restore: project. Its NEXT is this doctor again, and the file's bytes are never touched. A project.yaml that does not load declares nothing, so for that run a leg pin is left to a person. A copy's uncommitted change is shown with git diff HEAD, not discarded.
Tests: test_a_file_git_cannot_put_back_is_left_to_a_person (the a-file-of-the-projects-own-named-like-a-pin- cases edited, conflicted and committed), test_a_pin_is_what_the_root_declares_not_what_it_is_called, and test_a_leg_pin_is_left_to_a_person_while_project_yaml_does_not_load. All 8 of this round's mutants were killed. Restoring the suffix rule failed 5 tests.
…in the root declares or a shape copy, and shows a copy's uncommitted change instead of discarding it, so a project's own `contracts/release-pin.yaml` that does not load keeps the edit, the merged hunk or the commit a person made (#291) The review of #358 at ebffb08 (F1, codex P1 discussion_r4248225898) found `tool_written` admitting every `contracts/*-pin.yaml` by its suffix. A committed, undeclared `contracts/release-pin.yaml` that stopped loading was then put back from git, and the NEXT, run as AGENTS.md says to run it, discarded `schedule: daily` (an uncommitted edit, `checkout HEAD`), a cleanly merged `- nightly` (a conflict, `checkout --ours`) or `owner: platform-team` (a commit, `checkout <sha>~1`). The same bytes as `contracts/release.yaml` were left alone. The suffix is the template's naming convention, not a declaration. Ownership now comes from what the root declares, all of it read already: the shape pin, `contracts/<role>-pin.yaml` for each leg `project.yaml` declares (the spelling `check_legs`, validate-pins.py and bump-leg.py use), `contracts/<product lowercased>-pin.yaml` for each `neutral_product_pins:` entry (`neutral_pin_path`), and the root kind's copies. Anything else is `project`, and its NEXT is this doctor again. Where `project.yaml` does not load it declares nothing, so a leg or neutral pin that does not load either is the project's for that run: a file wrongly called a tool's loses a person's edit, and one wrongly called the project's costs a run. `tool_written` says so. A copy is not a pin: `update-shape.py`'s `both` exit asks a person to merge one by hand and commit it, and git holds no other copy of that merge until it is committed. So a copy's `uncommitted` NEXT is now the read-only `git diff HEAD -- <copy>`, and nothing is put back (F1's optional point). Its `committed` and `unmerged` cases are unchanged: git keeps the person's bytes in the breaking commit or the other branch. Tests: the three release-pin states left to a person; a declared neutral pin put back beside an undeclared one left alone (in process, because the `pins` row would recheck a declared neutral pin through `gh`); a leg pin left alone while `project.yaml` does not load and put back once it loads; a copy's uncommitted change shown and kept. F2: a leg pin broken by a commit, a leg pin whose newest parent holds a list, the naming copy broken by a commit, and a leg pin added back after its deletion (`none`), which kill the review's surviving mutants mu1, mu2 and mu3. N1: the words for a pin that loads as a list, which kill mu5. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kinds-unreadable-pin: #267's three files kept as this branch has them, which already holds #267's final head f6dd11a (#291) #358 was stacked on #267 and now targets main, where #267 landed as the squash commit decff87. `git diff f6dd11a decff87 -- shape-doctor.py update-shape.py tests/test_shape_doctor.py` is empty, and decff87 is the only commit in 5678f68..decff87 that touches those three files, so main changes them only by what this branch merged from f6dd11a in ce2ce75. Git still conflicted in shape-doctor.py, because the merge base is 5678f68 and this branch rewrote `pin_restore` after f6dd11a. Each of the three is taken from this branch (`checkout --ours`), and `git diff --cached HEAD` on them is empty. Every other path is main's at decff87 (dd8d0ea, a92e60b, f5cb616, 53bee5b), with no conflict. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s `origin_url`/`origin_name` and #346's `apply --add` refusal beside #291's read-only copies, so the branch is current with main and differs from it only in shape-doctor.py and tests/test_shape_doctor.py (#291) Main after decff87 brings, among others, two changes to the files this branch touches: 1184f67 (#404: shape-doctor.py's `origin_url` and `origin_name` read an `origin` that is not UTF-8, plus its doctor tests) and d6df36b (#346: update-shape.py refuses `apply --add` under a file the root keeps). Git merged both without a conflict. Each one's lines in the merge are its own commit's lines, one for one, and the lines this branch adds over main are the ones it adds over f6dd11a: #404's hunks are in `origin_url`/`origin_name`, far from `pin_restore`, and #346 does not touch `read_pin`, `check_pin`, `Row` or `parse_yaml`, which `pinned_copies` and `pin_at` call. `git diff --stat d6df36b` 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>
manifest kinds row puts back a file under contracts/ that does not load and that the root declares a tool writes (the shape pin, a leg or neutral product pin, the naming copy) with the git command pin_restore picks from its own state, 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)manifest kinds row puts back a shape or leg pin under contracts/ that does not load with the git command pin_restore picks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-only git 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)
…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>
…: 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>
|
…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>
… 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>
… text placeholders, so `--id 007`, `--tracking-branch 2026`, `--spec-path true` or `--org null` read back as typed, and `family.py add` quotes a member id alike (#259) (#395) Brett Heap ruled on 2026-10-12: "quote #259". The templates now write each text field a tool fills from a value it was given between double quotes: | template | fields | |---|---| | `templates/assembly-root/project.yaml` | `id`, `elected_on`, `visibility`, `tracking_branch`, the spec leg's `path`, the code leg's `path` | | `templates/assembly-root/contracts/spec-pin.yaml`, `code-pin.yaml` | `submodule_path` | | `templates/family-root/family.yaml` | `id`, `org`, `created_on`, `visibility`, `tracking_branch` | `render` already spells a value between a YAML template's double quotes with `repo_shape.double_quoted` (#284). The single-pass `render` from #316, on main as 34ecbc2, does the same through `YAML_PLACEHOLDER_RE`. So each value reads back exactly. An ordinary value renders as it did before, plus the two quote characters. `family.yaml`'s `org:` is quoted along with the four fields #259 names. It is the same defect in the same file: `family.py init --org 2026` and `--org null` pass `checked_value`, and on c7c02fa the holder then fails `family-org` (#307 lists the `--org null` row). Taking `org:` back out is not a one-line revert. It fails 14 of the new tests (the 8 `family.yaml:org` in-memory cases, and all 6 `family.py init` rows, because every row of `FAMILIES` passes a misread `--org`), and it would also take the `ORG` row out of `TEXT_FIELDS` and put plain `--org` values into `FAMILIES`. The lane keeps `org:` quoted, as the same defect in the same file, unless Brett says otherwise. `scripts/family.py`: `_member_lines` writes each member's own `project.yaml` id into the holder's `members:` block. That id now reads back exactly, so a bare `007` in the row came back as 7, and `validate-family.py` would report `member-id-mismatch` for a member it had just pinned. `_member_id` quotes an id only when the reader would not read the bare text back as the same text. No row whose id reads back as text moves: `irrs` is still `id: irrs`. Two edges: - A legacy row whose bare id the reader misreads moves once. A holder written before this change, by `family.py add` of a member scaffolded with `--id 2026`, holds `id: 2026`. The next `bump`, `add` or `remove` of that holder rewrites it as `id: "2026"`. That is harmless: `validate-family.py` compares with `str()` and passes before and after. - Mixed versions. A `family.py` from before this change, run against a holder this branch wrote, turns `id: "007"` back into `id: 007`, and `validate-family.py` then fails `member-id-mismatch` (the member declares `007`, `family.yaml` records `7`). A current `family.py` that rewrites the holder next then writes that row as `id: "7"`. Nothing in this branch can prevent it: run `family.py` from a current checkout on a holder that has such a member. Left unquoted on purpose: `topic` (`xf-project-<id>`), every `repository` (`<org>/<Name>`; the slash keeps it text), `SHAPE_REPOSITORY` and `DIGEST_DEFINITION` (constants), and the block or list placeholders (`NEUTRAL_PRODUCT_PINS`, `*_NAMING`, `SHAPE_FILES`). **Why.** Every reader of these files uses `repo_shape`'s YAML subset. It reads a bare `007` as 7, `2026` and `20261011` as numbers, `1.5` as a float, `true` as True and `null` as None. The checks on the flags (`PROJECT_ID_RE`, `checked_value`, the leg-path rule) accept every one of these. Reproduced on c7c02fa, with local bare remotes only: - `scaffold-project.py --id 2026|007|true|null|20261011` exits 0, and the project's own `validate-manifest.py` then fails `manifest-id`. - `--spec-path 2026 --code-path 007` fails `manifest-leg-path`, and `validate-pins.py` exits 2. - `--tracking-branch 007` pushes each leg on `007`, and the root's `bootstrap.py` then puts the legs on a local `7` with `fatal: no upstream configured`. `true` and `null` do the same on `True` and `main`. - `adopt-project.py execute` with `--tracking-branch 007` does the same; `--id 007|20261011` gives `manifest-id`; leg paths `2026`/`007` give `manifest-leg-path`. - `family.py init --id 2026|007|true|null|20261011` gives `family-id`; `--org 2026|null` gives `family-org`; `--tracking-branch 007|true|null` reads back as 7/True/None. On this branch every one of those rows reads back as typed, bootstraps onto the typed branch with `origin/<branch>` as upstream, and passes the root's own validators. **Every reader checked.** Every reader reads these fields through `load_yaml`/`parse_yaml` into a dict. None uses a line regex, `split(":")`, `startswith("<field>: ")` or a whole-line rewrite of them: - `templates/assembly-root/scripts/bootstrap.py`: `tracking_branch`, leg `path`. - `validate-manifest.py`: `id`, `elected_on`, `visibility`, leg `path`. - `validate-pins.py`: `submodule_path` against the manifest's `path`. The pin side is read as it stands and the manifest side goes through `str()`, so the two differ in type exactly when a bare value is misread (that is the `pin-path-mismatch` row above). With both lines quoted they are the same text. - `validate-repository-naming.py`: `str(id)` for the topic. - `templates/family-root/scripts/validate-family.py`: `id`, `org`, `created_on`, `visibility`, `str()` member-id compare. - `siblings.py`, family `bootstrap.py`. - `scripts/bump-leg.py`: `tracking`, leg `path`. Its `rewrite_pin` rewrites only `commit:` and `tree_sha256:`. - `update-shape.py`: `tracking_branch_of`. `rewrite_manifest` touches only the `shape:` block. - `scripts/family.py`: `tracking()`, the member's id, and the `members:` rewrite. The member-id writer is the one fixed here. - `shape-doctor.py`: leg `path`, member row `path`, working-clone `str(id)` compare. - `adopt-project.py`: plan reads and `_template_values`. - `setup-project.py`: `family.yaml` `name`/`kind` only. - `scripts/shape_advisory.py`: the single-repository record only. The `str()` fallbacks (`bootstrap.py`, `adopt-project.py`'s `checked_value`) stay, because projects made before this keep their bare lines. **Pins.** The four files are TEMPLATED, not COPIED, so no row of any project's `contracts/shape-pin.yaml` names them. `Atlas` was scaffolded and `family.py init Contoso` was run on c7c02fa and on this branch. Each `contracts/shape-pin.yaml` differs only in `commit:` and `digests.tree_sha256`, which identify the standard itself and change with every commit; every per-file `sha256` row is identical. `update-shape.py check` of the c7c02fa project and holder against this branch reports every copied file `unchanged` (11 and 10 rows): "no copied file differs; `apply` would move the pin alone". **Tests.** - New: `tests/test_template_text_placeholders.py`, 123 tests: - Each of the 13 fields is rendered with each of 8 values the reader misreads (`2026 007 1.5 true null 20261011 ~ False`), and must read back exactly. - 6 `scaffold-project.py` runs, then a recursive clone, `bootstrap.py`, the three validators, and `bump-leg.py`, which refuses on the tracking branch and finds the leg at its path on a branch of its own. - 6 `adopt-project.py` `plan`/`check`/`execute` runs into local bare remotes, then a clone, `bootstrap.py` and the validators. - 6 `family.py init` runs. - One `family.py add` of members whose ids are `007` and `true`. - **All 123 fail on c7c02fa.** - `true`/`null` through adopt: `plan` writes them bare into the plan, and the plan's reader then misreads them. That belongs to the plan writer, #181 (lane 1). Those two rows quote the value in the plan by hand, as #181's writer will, and test `check`/`execute`. - Mutation probes: reverting any one of the 13 quoted lines fails 8 cases of its field. Reverting `_member_id` fails the member test (the rows read 7/True). - The `try/except YamlError` in `_member_id` is guarded by the follow-up commit. The review found that calling the parse directly left the member test passing. Ids such as `[`, `{`, `&a`, `*a` and `!t` reach the `except` (they raise `YamlError` bare), and `_member_id` returns each one exactly, as it does 34 adversarial inputs in all (the review's in-process probe). `test_a_member_id_the_reader_refuses_bare_is_rewritten_between_quotes` (7 cases: `[`, `{`, `&a`, `*a`, `!t`, an unclosed `'` and an unclosed `"`) opens a hand-edited holder whose row holds the id between double quotes, rewrites the `members:` block with `Family.rewritten`, and asks that the id read back exactly and the text come out byte for byte. With the `try/except` removed all 7 fail with the reader's `YamlError`, and with the `except` returning the bare text all 7 fail too; the old member test still passes under the first of those mutants. - 14 existing tests that asserted or edited a bare rendered line now spell it quoted: `test_manifest_validator` ×5, `test_pin_validator`, `test_reference_default`, `test_scaffold_e2e` ×2, `test_setup_project_py`, `test_setup_sh`, `test_family`, `test_shape_doctor` ×2. - Targeted runs on this branch: | files | result | |---|---| | new file + `test_template_quoted_values`, `test_scaffold_e2e`, `test_scaffold_pin_and_reuse`, `test_scaffold_leg_paths`, `test_scaffold_leg_path_collisions`, `test_reuse_visibility`, `test_reference_default` | 405 passed | | the six adopt files | 491 passed, 1 skipped (the MedxEHR clone is not set) | | `test_family`, `test_family_add_refusals`, `test_family_member_path`, `test_manifest_validator`, `test_pin_validator`, `test_bump_leg`, `test_update_shape`, `test_update_shape_pr_base`, `test_setup_project_py`, `test_park_resume_targets`, `test_repo_hygiene`, `test_cli_reference`, `test_setup_sh` | 603 passed | | `test_shape_doctor` | 241 passed, 3 skipped (Windows-only), 1 failed; that one test was fixed, and with its sibling it reran at 2 passed | **The review.** An independent opus review at 233dc71 (the branch's first commit) found no code defect. Its verdict was CHANGES NEEDED (0 blockers, 2 should-fixes, 6 nits) because it asked for this description and its composition counts to be corrected, and they are. Its evidence: - **Readers and rewriters.** Every reader in the shipped tree reads the 13 lines through `load_yaml`/`parse_yaml`, and no line regex or whole-line rewrite touches them. It read `bump-leg.py` `rewrite_pin`, `update-shape.py` `rewrite_shape_pin` and `rewrite_manifest`, `family.py` `rewritten`, the three validators, `siblings.py`, both `bootstrap.py`, the doctor, `setup-project.py`, the workflows, the Makefile and `setup.sh`. No test among main's 17 commits since c7c02fa (through #245's 161c6c9) edits one of these lines bare. - **`_member_id`, the one rewriter changed.** A mutant that writes every id bare fails the member test (the rows read back as `{'IRRS': 7, 'IRSS': True}`). A mutant that quotes every id fails `b" id: irqs" in lines`. So the minimal-churn choice is tested both ways. - **Old projects and holders.** Against a control commit (c7c02fa plus an unrelated README edit), a project and a holder made on c7c02fa give identical `update-shape.py check` and `shape-doctor.py` output, up to the shas. `check --at 233dc71` reports all 11 project files and all 10 holder files `unchanged` and says `no copied file differs`. The doctor's verdict is `COMPLIANT, SHAPE BEHIND (... no copied file differs)` for the control and for 233dc71. This branch's `family.py add` and `bump` on a c7c02fa holder leave the old `atlas` row and the header byte for byte, apart from the legacy-id edge described at the top. - **Tests.** All 123 new tests fail on c7c02fa (`123 failed`) and pass on 233dc71 (`123 passed`). The 14 changed existing tests pass (49 passed). - **Not covered.** Windows and macOS were reviewed by reading and not run: the plan edit and the member-row check are in bytes, and nothing writes to git's stdin in text mode. Of `test_shape_doctor.py` and `test_family.py` the review ran only the changed tests. **Follow-up commit.** A second commit, `3894aa8`, answers two nits from the review, and changes no behaviour. The `_member_id` docstring said "no existing holder's `members:` block moves by a byte", which was false: the next `bump`, `add` or `remove` of a holder written before this change rewrites a legacy `id: 2026` row as `id: "2026"`, harmlessly, because `validate-family.py` compares with `str()`. The docstring now says exactly that, and a grep of every comment and docstring the branch added for "by a byte" and "moves" found one more copy, in the member test's docstring, corrected the same way. The `try/except YamlError` in `_member_id` is guarded by a new test (see Tests), so the new file holds 130 tests. Every count above that says 123 (123 new tests, all failing on c7c02fa and passing on 233dc71) is for the first commit; the follow-up commit adds the 7. The `except` also carries a comment saying which ids reach it. **Composition at the landing head (4984651).** `49846518` is GitHub's update-branch merge of origin/main `cc8f2da4` (#376, a `--trailer` value carried into the commit as typed) into `3220c9ee`. Git reported no conflict, and the merge has no hand edit. #376 changed `scripts/family.py` and `tests/test_family.py`, which this branch also changes. None of the lines it added under `tests/` names a key the templates now quote. The coordinator ran targeted tests on this head, Linux only, before CI: - main's changed test files (`tests/test_commit_trailers.py`, `tests/test_family.py`, `tests/test_bump_leg.py`, `tests/test_update_shape.py`) with `tests/test_family_add_member_id.py`: 421 passed, 1 skipped (a case-insensitive disk only); - this branch's template set (`tests/test_template_text_placeholders.py`, `tests/test_template_quoted_values.py`, `tests/test_scaffold_manifest_values.py`, `tests/test_bootstrap_spelling.py`, `tests/test_bootstrap_after_bump.py`) with the hygiene, CLI-reference and AGENTS.md-codes tests: 652 passed. **Composition at 3220c9e.** `3220c9ee` is GitHub's update-branch merge of origin/main `70d0d1b3` into `e5e768bc`. It brings in three commits: #491 (da19735), in which `setup-project.py` asks at the person's own terminal; #374 (7ab11cf), update-shape's NEXT quoting; and #384 (70d0d1b), adopt's `_spelled` plan values. Git reported no conflict, and the merge has no hand edit. #491 changed `tests/test_setup_project_py.py` and `tests/test_setup_sh.py`, which this branch also changes. A grep of the lines main added under `tests/` for the keys the templates now quote found two hits, and neither reads a rendered template line: `tests/test_update_shape_next_quoting.py` writes its own `project.yaml`, and `tests/test_adopt_question_and_submodule_spelling.py` edits a plan's `- path:`/`question:` entry, which `adopt-project.py` writes bare. The coordinator ran targeted tests on the same tree (`e5a74d19`), Linux only, before CI: - six of main's eight changed test files (the setup, update-shape, `openRepoShape` command and doctor-row tests): 365 passed; - main's other two (adopt's spelling and repo hygiene), `tests/test_adopt_github_exits.py` and nine more files of this branch's sixteen-file set: 943 passed. **Composition, refreshed again 2026-10-13 (head e5e768b).** `6d108d18` merges origin/main `56713e21` into `6cbbac60`. That brings in two commits: 5e02abe, the doctor's and `validate-pins.py`'s gitlink repairs, and 56713e2 (#479), in which adopt's `plan` takes the tracking branch from the source's default branch. Git reported no conflict and `docs/cli.md` auto-merged; the merge has no hand edit. Two kinds of test failed on it, and two commits fix them: - **`7968f902`** changes one expected literal, at `tests/test_adopt_github_exits.py:675`, for `test_a_source_on_another_branch_opens_its_pull_request_against_it[master]` and `[trunk]`. - The test reads the adopted root's `project.yaml`, which this branch's template renders as `tracking_branch: "master"`, so it now expects the quoted line. - The look-alike lines at `:743` and `tests/test_adopt_tracking_branch.py:73-76` read a plan, which `adopt-project.py` writes bare, and they stay as they are. - **`e5e768bc`** adds `git("branch", branch, cwd=source)` to `tests/test_template_text_placeholders.py::test_an_adoption_reads_back_every_value_it_was_given`. - All six rows had failed at `check` with `plan-tracking-branch-missing`: #479 refuses a tracking branch (`2026`, `007`, `1.5`, `20261011`, `true`, `null`) the source does not have, and the test's source had only `main`. - The source now carries each row's branch, in the flag rows and the by-hand rows alike. The test's assertions are unchanged. Targeted runs, Linux only: - On e5e768b: main's seven changed test files, 211 passed; this branch's sixteen-file set, 1223 passed, 2 skipped. - On 7968f90: the same sixteen files gave 6 failed, which are the six rows e5e768b fixes. The scaffold, setup, doctor and family set gave 1094 passed, 22 skipped, and twelve further template-grep files gave 1259 passed. - `render-cli-reference.py --check` is ok. **Composition, refreshed 2026-10-13 (head 6cbbac6).** `d651b594` merges origin/main `aa4e1f59` (#508) into `d526f768`. - **The one conflict.** Git conflicted in one file, `tests/test_adopt_manifest_values.py`, in four hunks, all against #356 (965420b, #293/#294). #356 refuses an `elected_on:` that `election_date` does not read, as `election-date-malformed`, before the validator is asked (`_manifest_values` returns None, adopt-project.py:2258). - **Resolved by what the merged tree prints**, measured with `adopt-project.py plan`/`check`/`execute` into local bare remotes: - `id: "007"` checks clean and `execute` writes it as typed, so it stays in `TEXT_THE_TEMPLATE_NOW_HOLDS`. - `visibility: "true"` gets the validator's `manifest-visibility: visibility is 'true'`. - `elected_on: "20261011"` gets #294's one finding, "the plan's `elected_on:` is the text '20261011'", and the validator is not asked. That row keeps this branch's list, `TEXT_THE_VALIDATOR_REFUSES`, with main's sentence. - Two tests git merged cleanly now ask `said_of`, as main's own rows do: `test_check_refuses_a_text_value_the_validator_refuses` and `test_where_a_misread_rule_judges_the_rest_the_validator_still_answers`. - **Two more failures that no hunk showed**, fixed in the same merge: - `[branch-tilde]` in `tests/test_scaffold_manifest_values.py` was this branch's own `xfail(strict=True)` until #355 landed as 6c2a371, so it XPASSed. The mark is off, and the row asserts `REFUSED scaffold-bad-tracking-branch:`. - #391's `tests/test_bootstrap_spelling.py` and #490's `tests/test_bootstrap_after_bump.py` matched ` path: spec` and `tracking_branch: main` in a scaffolded `project.yaml`, and nine cases failed on the merge. They now match the quoted lines. - **The #507 interplay.** `6cbbac60` switches the holder to `family/members` before the real `add`s in `test_a_member_whose_id_yaml_would_misread_is_pinned_under_that_id`, so the test holds once PR #507's `family-on-tracking-branch` refusal lands. - On the clean merge of 6cbbac6 with #507's head 708f3bb, that file, all 13 `tests/test_family*.py` and `tests/test_shape_doctor.py::test_a_sibling_at_the_right_origin_with_another_id_is_not_the_member` give 686 passed, 19 skipped. - Without the switch line, the member test fails `REFUSED family-on-tracking-branch`. - **Targeted runs on 6cbbac6, Linux only:** - the template, adopt-plan and manifest files (14): 995 passed, 2 skipped; - scaffold e2e, setup, the doctor, leg-path collisions and every family file: 1094 passed, 22 skipped; - the 15 further files a grep for template lines found: 1503 passed; - `render-cli-reference.py --check`: ok. - **Re-verified independently** at d651b59 and 6cbbac6: APPROVE WITH NITS (0/0/3), all three in this paragraph and fixed here. The re-verification also ran the three rows, five mutations, and 26 more files (834 passed; 291 passed, 1 skipped). - **Not run locally:** the full suite, macOS and Windows; CI's three jobs are the check. **Composition, as measured on 2026-10-12.** The merge trees below were computed for 233dc71; the follow-up commit 3894aa8 changes only `scripts/family.py` comments and adds tests. At 15:28Z main was 3ae0109 (#245, #351, #350 and #345 had landed since the merge trees): 3894aa8 merges clean with it, and the review's re-verify ran the family tests on that merge (61 passed). Every sha below is the one it had then; main and the other branches have moved since. `git merge-tree --write-tree --name-only`, run after a fetch with automatic maintenance off on 2026-10-12: - **origin/main 161c6c9**, 17 commits past c7c02fa (the base): clean, tree 7ba8246. #316 (34ecbc2), #317, #318, #311 and #245 were on main by then. - **The 46 other remote branch heads** (the 30 open PR heads and 16 branches with no open PR): clean for 44. Among them #314 (4b8da3d), #266 (3410be7), #355 (a03abcf), lane 1's `fix/adopt-plan-quote-yaml-keywords` (d38e143), `fix/adopt-record-submodule-placement` (70f4d29) and `fix/adopt-late-failure-notice` (9f1778d), #380 (73068a3), and `fix/family-init-values` (14e2d68, #307). The two that conflict, each in the same file as it does with c7c02fa and with origin/main on its own: lane 1's `fix/adopt-root-kept-symlinks` (6c44591, `adopt-project.py`) and #358 `fix/doctor-manifest-kinds-unreadable-pin` (ebffb08, `shape-doctor.py`). This branch touches neither file. - #314 and d38e143 each conflicted with origin/main on their own, in `adopt-project.py`. Merged with this branch, neither gained a conflict. **Composition with #314, which lands before this branch.** Its misread gate (`manifest_misreads`) reads the template, so once the template quotes a field it refuses nothing there, and some of #314's tests expected that refusal. Composing with #314 (landed as 3ef059c), whose misread gate asks the template how a value is written. With `project.yaml` quoting `id`, `elected_on`, `visibility` and `tracking_branch`, the gate refuses nothing on the shipped templates, and 45 of #314's tests flipped on the merge. Forty expected a refusal the value no longer needs. Thirty-four of them now assert that the value is accepted, and the six whose premise was a bare placeholder now assert that no shipped template placeholder is misread. For example: a scaffold of `--id null|true|false|007|2026`, or of `--tracking-branch true|True|TRUE|false|False|null|Null|NULL|007|2026|1.5|1.50|0`, exits 0, and the manifest holds the text it was given. A dry run prints the plan. An id derived from `--project True|Null` is held as typed. `--id=null` reaches `gh repo create`. Adopt's `id: "007"` checks clean and `execute` writes it as typed. The rule's own sentence, remediation and line-break wording are still asserted, on a copy of the standard whose `project.yaml` writes the four lines bare. Four stay refused by #314's validator gate, now in its words about the text: `visibility is 'true'` and `elected_on is '20261011'`. One is a real break. `--tracking-branch=~` reads back as `~`, which git refuses as a branch name, and it dies at `git init -b ~`, after `gh repo create`. That is #283, so the row is `xfail(strict=True)` with that reason, and it turns XPASS (and fails) once PR #355's `git check-ref-format --branch` refusal lands. `--help` for `--id` and `--tracking-branch` no longer says such values are refused, and `docs/cli.md` is regenerated. **Composition with the other branches.** Where another branch's rules meet this one, the tests were run on the merged tree. Quoting changes what some of their tests see, and the changes are not all one kind: some tests expected a refusal the value no longer needs, some are still refused under another rule, and some guard a case quoting does not fix. Whoever lands second must tell them apart, so each branch's flips are classified below. - **#266** (lane 2, `fix/leg-path-yaml-scalars`). 27 flips, all from one cause: its rule asks a hard-coded bare line, `LEG_PATH_LINE = "path: {}\n"`, so it refuses leg paths the quoted templates now read back. 15 are `test_the_rule_agrees_with_every_template_line_that_holds_a_leg_path` (null, Null, NULL, ~, true, True, TRUE, false, False, FALSE, 2026, 007, 0, 1.5, 1.50) and 12 are this branch's own end-to-end tests (6 scaffold rows fail with `REFUSED scaffold-bad-leg-path`, 6 adopt rows with `FINDING plan-bad-leg-path`). Counted on the merge with b2b42f8. #266's head was then 3410be7, whose rule reads each template line that holds a leg path, renders the value into it as `render` does and asks the reader, so it has no hard-coded line left. The merge with this branch was clean (tree 1e348c6); that head was read and the 27 were not re-run on it. At b2b42f8, whichever of #266 and #259 landed second had to drop or re-point the bare rule; 3410be7 re-points it already. - **Lane 1's #181** (`fix/adopt-plan-quote-yaml-keywords`, d38e143). Its gate reads the template too, so `plan --tracking-branch 2026|007|true` exits 0 on the composed tree (exit 2 `adopt-manifest-misread` on d38e143). On the merge, `tests/test_adopt_plan_yaml_quoting.py` and this branch's 6 adopt end-to-end tests give 70 failed, 371 passed: - 57 refusals that are now unnecessary: `plan_refuses` ×14, `plan_names_an_id` ×2, `check_names_an_id` ×4, `check_reports` ×14 and `execute_refuses` ×14 (every value except `elected_on` and `visibility`), `text_the_loader_reads_as_other_text` ×7, and the two tests that need a bare placeholder (`test_the_gate_follows_the_loader_when_the_loader_learns_a_keyword`, `test_the_template_line_says_whether_a_value_is_quoted`). - 11 guards #181 loses. See "Do not flip". - 2 that fail only by cascade: `execute_refuses[pins='null']` and `[pins='True']` pass alone (`2 passed`). An earlier case no longer refuses, and its `execute` pushes the split branch. - The writer half of #181 (`y` asking the reader, so `true`/`null`/`True` are quoted in the plan) is still needed and composes cleanly. With it, `plan --tracking-branch true` reaches the quoted template intact, and this branch's two hand-quoted adopt rows become flag-driven. - **Do not flip these 12 to "accepted".** Each still needs a refusal or a finding, and quoting does not supply it: - **`~` (1, #314).** `test_a_value_the_reader_misreads_is_refused_before_anything_is_made[branch-tilde]`. `--tracking-branch=~` is not a branch name git accepts, and #314 refused it only as a misread. On the merge it passes the plan and dies at `git init -q --bare -b ~ .../Atlas-spec.git` with `fatal: invalid initial branch name: '~'`, and against GitHub that is after `gh repo create` has made all three repositories. That is lane 2's #283 (PR #355). `~` must stay refused, by #283's ref-name check, which is why #314's row for it is not flipped to "accepted". - **A hand-edited plan value that is not text (7, #181).** `test_check_names_a_plan_value_that_is_not_text_by_what_was_read[tracking_branch='007']`, `[tracking_branch='TRUE']`, `[tracking_branch='1.50']`, `[tracking_branch='[a]']`, `[visibility='{a: b}']`, `[id='007']` and `[elected_on='20260902']`. The plan's reader reads each as something other than text, and on the merge `check` passes. Measured on the merge: `tracking_branch: 007` and `id: 007` are adopted silently as `"7"` with every validator passing; `visibility: {a: b}` pushes both legs and then fails `manifest-visibility`; `tracking_branch: [a]` is still refused at `execute` (`unsafe-value`) before any leg exists. On d38e143 alone, `check` exits 1 `plan-manifest-misread` for each of those four. Re-point the gate at "a plan value that is not a string" and these keep their guard. - **A quoted value the validator refuses (4, #181).** `test_check_reports_a_plan_carrying_one[elected_on='20260902']` and `[visibility='true']`, and `test_execute_refuses_a_plan_carrying_one_before_any_leg_exists[elected_on='20260902']` and `[visibility='true']`. The value stays invalid for the validator (`DATE_RE`, the visibility choices), and #181's misread gate caught it only incidentally. On the composed tree `check` passes and `execute` pushes the legs, and the gate fails afterwards. That is what main does today, and #314's validator gate (`plan-manifest-invalid`) would catch it. - **main** (merge with ab623dd, tree cdeec7f, run by the review): this branch's new file, `test_adopt_template_values.py` (#270), `test_template_values_single_pass.py` (#316) and `test_template_quoted_values.py` give 431 passed, 1 skipped. The skip is `test_adopt_template_values.py:223`, which reads `git config user.name` and skips where the shared repository's `.git/config` sets one; it is environmental. The changed tests with `test_family_add_refusals.py` (#317, #318), `test_pin_validator.py` and `test_manifest_validator.py` give 132 passed. #270's `unwritable_reason` is value-based, not template-relative, so it refuses as it did and nothing flips. Main then added #245 (`adopt-project.py` and `repo_shape.remote_key`), which adds no test that edits a bare quoted line; it was not run on that merge. - **#380** (lane 3, `fix/pin-rewrite-indentation`; it landed since as 1f90a0f). It rewrites `commit:` and `tree_sha256:` only, and its tests render the pins from the templates. On the merge at dc0fa7b, `test_pin_rewrite_layout.py` and the 6 scaffold end-to-end tests gave 121 passed. Its head was then 73068a3; that merge was clean and was not re-run. - **#397** (`fix/family-init-values`, #307; measured at 14e2d68): its tests skip themselves where the template quotes. 137 passed, 19 skipped, 0 failed. With its fix-round head abb27e6, this branch's 3894aa8, main 0f67f7b and #345 together: 442 passed, 20 skipped (the 19 rows, and one Windows-only row). - **#345** (run at 875d802; it has since landed as 3ae0109): `test_family_dry_run.py` and the member test, 22 passed. Its `" id: irrs"` assertion holds, because ordinary ids stay bare. **Left out.** - The adoption plan writer `y` still writes `true`/`null` bare into the plan. That is #181 (lane 1). - `templates/assembly-root/contracts/neutral-product-pin.yaml` still writes `product`, `source_repository` and `digest_source` bare, and `family.py` writes a member row's `project:` bare. Neither is a #259 field. - A plan value someone edits to a non-text type (`tracking_branch: 007` or `id: 007`, unquoted, in the plan) is read by the plan's reader as the number 7. `str()` makes it `"7"`, and the quoted template now writes it faithfully. For `id` this is a change: on c7c02fa a bare `id: 007` failed loudly (`validate-manifest: manifest-id`, manifest line `id: 7`), and on this branch it is adopted silently as id `"7"` and every validator passes. `tracking_branch: 007` was already adopted silently as `7`. The plan side is not this branch's: the guard against it belongs with #181 (adopt plan writes YAML keywords and non-text values unquoted), whose gate should refuse a plan value that is not a string. The case is narrow, because `plan --id 007` itself writes `id: "007"`; only a hand edit reaches it. - No docs change: README, AGENTS.md, the handbook and the template comments show none of these lines; `docs/cli.md` is unchanged because no `--help` text changed. Reviewed adversarially at 233dc71 (CHANGES NEEDED: 0 blockers, 2 should-fixes, 6 nits; the code and the tests were sound, and the should-fixes were the composition counts and the claim about a hand-edited plan `id`). Fix round 3894aa8 changes no behaviour: it corrects the description and the two false "moves by a byte" claims, and guards `_member_id`'s `except YamlError` with a test of 7 cases. Its re-verification at 3894aa8 found every finding handled: APPROVE WITH NITS (0 blockers, 0 should-fixes, 1 nit: shas that had moved since the description was written, refreshed here). What was added after that re-verification, main and the composition with #314, is described under "Composition with #314". An independent re-verification of that composition at 77cd0b2 confirmed it with nits only (0 blockers, 0 should-fixes, 5 nits): the 45 flips reproduce test for test, breaking the real rule or the real template's quoting turns the compose tests red, and the `[branch-tilde]` row XPASSes on a merge with #355. **Composition with #381.** #395's quoted `id: "{{PROJECT_ID}}"` composes with #381's `member-duplicate-id` test (`tests/test_family_add_member_id.py`), whose id rewrite now matches the id bare or quoted and keeps its quoting, so the full suite, which failed that one test on Linux at 1ef3310, no longer fails `test_a_member_whose_id_changes_between_the_probe_and_the_mount_is_unwound` on Linux. CI is green on Linux, macOS and Windows, with SonarCloud passing, at 4984651 (main cc8f2da merged in by update-branch). Main af01068 (#505), which landed after, touches none of its files, and the combined tree passed the template, hygiene, CLI-reference, AGENTS.md-codes and setup tests (807 passed). Closes #259 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…three `git diff` lines for a copy or a neutral product pin a person may have edited and `unreadable_next`'s doctor-again line, are the command alone and the sentence after ` # ` is the row's `why`, so a `manifest kinds` NEXT pastes as one command into an interactive zsh and this PR's guard is green on main cc8f2da (#277) #291 (5a25387, PR #358) landed while this PR waited to land. It widened `pin_restore` from the shape pin to any file under `contracts/` a tool wrote, which the merge before this commit resolved in this PR's form, and it added four NEXTs the way every doctor builder wrote one before #277, `<command> # <sentence>`: * `pin_restore`, a file `edited_by_hand` (a copy the shape pin has a row for, or a neutral product pin): `diff -- <file>` while unmerged, `diff HEAD -- <file>` while uncommitted, and `diff <commit>~1 -- <file>` for the commit that broke it. The last sat in a conflicted hunk and the merge converted it; the first two were outside every hunk; * `unreadable_next`, the `manifest kinds` row's NEXT where no file that does not load has a git command: `shape-doctor.py --root <root> # {UNREADABLE_BY_HAND}`. An AST scan of the merge (b16e46f) finds three tails outside the guard's allow-list -- `pin_restore` x2 and `unreadable_next` -- and none after this commit; main cc8f2da itself holds 29, which is 25 plus #291's four. The ruling is this PR's, so the second lander converts them, the phase-1 way: the command alone is the NEXT, the sentence moves VERBATIM, less its `#`, to `why`, and no word is edited. Proved by rendering main's tail expressions and the new (command, why) pairs as text, every interpolation a placeholder: all seven of `pin_restore`'s sentences are main's, character for character, and `UNREADABLE_BY_HAND` is the `why` whole. `pin_restore`'s local `why = BY_HAND_WHY[what]`, which only ever fed the sentence, is `by_hand`, so the name `why` means the row's field. `unreadable_next` returned `(restore, command)`. It returns `(restore, command, why)` as `pin_restore` does: it keeps each file's `(command, why)` from `pin_restore`, hands on the shape pin's or else the first one's, and where none has a command builds the doctor-again line with `UNREADABLE_BY_HAND` as its `why`. Its one caller, `check_manifest_kinds`, passes that `why` to the row; a row with no file that fails to load keeps its prose NEXT and no `why`, as before. The comments on `BY_HAND_WHY` and `UNREADABLE_BY_HAND`, and the docstrings of `pin_restore` and `unreadable_next`, said "the note" and "no `;`, apostrophe or backtick"; they now say the sentence is the row's `why`, off the command's line. Tests. `tests/test_doctor_next_is_a_command.py` gains five property cases (34 in all), each built by the fixture #291's own tests use and each asserting the restore it reached: `pin_restore-unmerged-shown`, `-uncommitted-shown`, `-committed-shown`, `unreadable_next` (a file of the project's own) and `unreadable_next-pin_restore` (a leg pin, the branch that hands on `pin_restore`'s `why`). Its docstring's count is twenty-nine. `tests/test_shape_doctor.py` changes only #291's tests, where each read a sentence off a NEXT: * `assert_no_registration` called `assert_a_plain_note(row["next"])`, which this PR renamed to `assert_a_plain_next(row)` when #267's tests moved; it calls that; * nine reads of `["next"].split(" #", 1)[1]` read `["why"]`, in `test_a_file_a_tool_wrote_that_does_not_load_is_put_back_from_git`, `test_a_file_git_cannot_put_back_is_left_to_a_person`, `test_an_uncommitted_change_to_a_copy_is_shown_and_kept`, `test_a_copy_conflicted_on_its_own_is_shown_and_merged_by_a_person`, `test_a_conflicted_copys_next_agrees_with_shape_currency_and_agents_md` (twice), `test_a_committed_hand_merge_of_a_copy_is_shown_not_rolled_back` and `test_a_neutral_product_pin_is_shown_never_put_back` (twice); * that P10b test read `--accept-local` off the `shape currency` NEXT, whose `apply` this PR made the `why` (#111); it reads the `why`. No expectation of #291's about a command, a restore kind or a file's bytes changed. Mutant: #291's tail put back on `pin_restore`'s `diff HEAD` line fails the guard (it names `pin_restore`, ` # HEAD holds a `), the property case `pin_restore-uncommitted-shown` (`shlex` reads a comment) and #291's own `test_an_uncommitted_change_to_a_copy_is_shown_and_kept` (` #` on the NEXT): 3 failed. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d-copy: #358's server-side merge of main 7a8b981 into 65def40, so this branch holds #358 exactly as it squash-landed before main's newer commits are merged (#485) #358 squash-landed on main as 5a25387, and GitHub retargeted #496 to main. This is step one of the stacked-branch procedure: merge the parent's final head, which git merges with no conflict, so the next merge, of the squash itself, can take this branch's side of #358's two files knowing that side already holds every line of #358's final head. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…py, taking this branch's side of shape-doctor.py and tests/test_shape_doctor.py, so main up to #358's landing is in this branch with #485's change over #358 intact (#485) #358 landed on main as the squash 5a25387 of its final head c637722, which the previous commit merged. `git diff c637722 5a25387 -- shape-doctor.py tests/test_shape_doctor.py` is empty, so main changed those two files by nothing but the squash. Git still conflicts on them, because the squash and this branch's #485 hunks share no history; this branch's side is c637722's text plus #485's change, which is the right resolution for both files. Every other path merged cleanly. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
's tests expect #424's `INVALID (merge)` while the copy and the shape pin are unmerged and the copy's drift once the merge is committed, so the PR lands on main's doctor with every #485 guarantee asserted against the verdict main now gives (#485) Main since #358's squash 5a25387 changed shape-doctor.py in 57c85e9 (#424: a root whose index holds an unmerged path gets a `merge` FINDING naming the paths, NEXT `git -C <root> status`, verdict `INVALID (merge)`, exit 1, on a rung above all but CANNOT ANSWER) and 5e02abe (#478), and tests/test_shape_doctor.py in 994be39 and da19735 (setup fixtures). Git merged every file with no conflict: main's hunks are in the module docstring, `check_pins`, `check_machine`, the merge check and the verdict ladder, none of them in `pin_restore` or `unreadable_next`, where #485's change is. The composition is in #485's own tests. #424 fires in exactly the states they build: `git merge` leaves the copy and the shape pin `UU`, and the pasted NEXT, `checkout --ours -- contracts/shape-pin.yaml`, writes the tree and leaves the index as it was. They asserted only "not COMPLIANT" there, which #424 alone now satisfies. They now assert: - before the NEXT and after it, the verdict is `INVALID (merge)` and the `merge` row names exactly the copy and the pin, `merging` true, with the read-only `git -C <root> status` as its NEXT, while the `shape currency` and `manifest kinds` rows still print #485's NEXT, note, `left_for_a_person` and `restore`, unchanged; - once the hand merge is committed, the `merge` row is gone and the verdict is the copy's drift, `DRIFTED (1 locally-modified)` with the update side's pin taken and `DRIFTED (1 both)` with the person's, so COMPLIANT still waits on the copy kept unlike the pin. That is the guarantee #485 adds over #424: had the NEXT taken the copy whole, the committed merge would read COMPLIANT with the person's lines gone. The person's lines still survive every pasted NEXT, and the rows still agree. #267's two merge tests assert no verdict in the conflicted state and end COMPLIANT after the merge commit, so they are unchanged. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ded twenty-nine of them after ` # ` is the row's `why`, on a `WHY` line of its own and `"why"` in `--json`, so a NEXT pasted into an interactive zsh -- which reads that `#` as a word -- no longer hands the comment to the command as arguments, runs what follows its `;`, or runs the `make bootstrap` in its backticks, and the template validator's line is `env PYTHONPATH=... python3 ...` (#277) (#402) Twenty-nine of the doctor's NEXT builders ended the command with ` # <comment>`, and one printed prose in the command's place. Sixteen were there when this began (3ae0109). Doctor A (#276) then rewrote the `legs` tail and added two more (`lockstep_next` and the `members` git line), doctor B (#278, #298) added two (its crash line and its restore line), and lane 2's #237 (#267), which landed while this was in CI, added five (`pin_restore` four times and `unreadable_pin_row`). Those five are converted the same way. So are the four lane 2's #291 (#358) added while this waited to land, `pin_restore`'s three read-only `git diff` lines for a copy or a neutral product pin a person may have edited and `unreadable_next`'s line for a file under `contracts/` no git command can put back, beside #267's four, which #291 widened to any file a tool wrote and which keep their sentences as `why`. Lane 2's #496 (#485), which landed while this waited to land, rewrote the first of #267's to take the shape pin alone and give a copy conflicted beside it to a person to merge by hand, and that whole sentence is the `why` as well. `sh -c`, bash and `zsh -c` read that `#` as a comment. An interactive zsh without `interactivecomments` reads it as a word; that is zsh's default and macOS's login shell, where a person pastes. Pasted there: - the comment's words reached the command as arguments; - the text after a `;` ran as a second command (the manifest NEXT exited 127, `zsh: command not found: it`); - the `make bootstrap` that the `legs` and `members` comments put in backticks ran as command substitution in the person's current directory. A NEXT is now only the command (R277-2: `next` is the bare command or null). The sentence moved verbatim, less its `#`, to a new keyword-only `Row` argument `why`. It is printed in three places: - as `"why"` on every `--json` row (null where there is none); - as a `WHY` line of its own under `NEXT` in the table; - as ` Why: ...` in the CANNOT ANSWER block on stderr, whenever the row has a `why`. It comes after ` Remediation: ...` when there is a NEXT, and is still printed when there is none. The 32 sites (the twenty-nine tails, the prose NEXT, the `unloaded_manifest_row` branch that passes on `validator_row`'s `why`, and the `unreadable_next` branch that passes on `pin_restore`'s): - `validator_row`: neither readable, the finding, and B's crash line; - `unloaded_manifest_row`: the gate, B's `repo_shape.py` restore line, and the neither-readable branch, which passes on `validator_row`'s `why`; - `pin_restore` (#267's four: the merge, the uncommitted change, the hidden copy and the commit that broke the pin; and #291's three `git diff` lines for a file a person may have edited: unmerged, uncommitted and committed), which now returns `(restore, command, why)`, and `unreadable_pin_row`, whose `update-shape.py check` keeps its sentence as `why`; - `unreadable_next` (#291, the `manifest kinds` NEXT for a file that does not load): the doctor-again line, and the branch that passes on `pin_restore`'s `why`. It returns `(restore, command, why)`; - `check_shape_currency`: cannot answer, the pin alone behind, and copies moved. The `then apply --at ... --branch ... --trailer 'Lane: X'` is the `why`, so the #111 tests read it there; - `check_legs`: A's git line; - `lockstep_next` (A's lockstep gate for `legs` and `members`), which now returns `(command, why)`; - `missing_shape_file_fix`: partly copyable, and all rendered. The prose NEXT is now `python3 <shape>/scaffold-project.py --help`, with the prose as its `why`; - `placement_policy`; - `check_placement`; - `check_agent_files`: all three branches; - `members_verdict`: the holder's bootstrap, and A's plain git line; - `check_machine`; - `check_the_way_in`. Out of scope: the prose NEXTs `check_manifest_kinds` prints for a kind no gate here runs (an unregistered kind, #211, #327), and `Refusal.remediation` printed as a NEXT. `report`'s `placement plan written to ... # nothing moved` line is printed text, not a NEXT, and is left alone. R277-4: `validator_command`'s `sh` branch spells the template's environment `env PYTHONPATH=<q> python3 ...`. A bare `NAME=value` prefix is syntax in sh, bash and zsh only. By their documentation, csh and tcsh report `Command not found` for it and fish before 3.1 refuses it. `env` is a program, works in all of them, and still sets the variable for that one command only. Tests, in `tests/test_doctor_next_is_a_command.py`: - A property over one red fixture per converted builder and branch (35 cases). The NEXT must have: - no shell comment (`shlex.split(next, comments=True) == shlex.split(next)`); - no backtick and no ` # `; - on POSIX, none of `;`, `&`, `|`, `(`, `)` outside a quotation, read from `shlex` tokens; - on POSIX, no `$(` outside single quotes. Its `why` opens with the words printed before this change. - A `zsh -f -i` pty paste (skipped without zsh and on Windows). The manifest NEXT exits with `detail.exit` and prints its `FINDING`. The legs NEXT, pasted where a Makefile's `bootstrap` target touches a marker, leaves none. The pre-#277 lines, pasted the same way, exit 127 or create the marker. - An AST guard over non-docstring string constants in `shape-doctor.py`. It refuses any tail (two or more blanks or a tab, then `#` and a blank) outside `placement_plan_leg` and `report`'s one `nothing moved` string. Owners are named by their whole def path. - The table's `WHY` line sits under its `NEXT`, and stderr has `Why:` after `Remediation:`, or alone when there is no NEXT. `tests/test_doctor_validator_rows.py` and `tests/test_doctor_legs_members_next.py` now read `why` where they read a tail, and `env PYTHONPATH=` where they read the bare prefix. `tests/test_shape_doctor.py` changes where it asserted tail text, the row key set (now with `why`) or the bare prefix, and where #267's, #291's and #496's tests cut the ` #` note off a NEXT or read it from one, or read the `apply` off the `shape currency` NEXT: they read `why`, and the argv is read from the whole line. It also gains `standard_ahead` and `standard_of_another_history`, built from code its tests had inlined. `tests/test_update_shape_next_quoting.py`'s #325 test of the `shape currency` row reads the `check` from `next` and the `apply` from `why`, less its leading `then `, where it cut both from one line at ` # then `. Mutants: - A ` # x` tail put back on `check_legs`'s NEXT fails the AST guard and two property cases. - A tail put back on `lockstep_next` fails the guard and both lockstep cases. - #291's tail put back on `pin_restore`'s `diff HEAD` line fails the guard, its property case and #291's own test of that line. - #496's tail put back on `pin_restore`'s pin-alone `checkout` line fails the guard and both `unmerged` property cases. - A two-space tail, or a ` # {why}` printed in `report`, fails the guard. - `why` dropped from `Row.as_dict` fails the key-set test. Closes #277 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-2 (openRepoShape-2)
Claim: #291 (comment)
Targets main. It was stacked on #267, which landed as decff87; this branch merged decff87 (b88aa65), then main at d6df36b (a5563c3), then main at 437a360 after #343 landed (3af2829). The head is 65def40.
What
shape-doctor.py'smanifest kindsrow no longer gives a file that does not load the unregistered kind's sentence. That sentence was "register the kind in shape-doctor.py's MANIFEST_VALIDATORS, beside the check that validates it, or remove the file from the root".What the root declares decides, not the file's name.
tool_writtenadmits two kinds of file:declared_pinslists.contracts/<role>-pin.yamlfor each legproject.yamldeclares, the spellingcheck_legs,validate-pins.pyandbump-leg.pyuse.contracts/<product lowercased>-pin.yamlfor eachneutral_product_pins:entry, which isneutral_pin_path.pinned_copieslists. These are the paths the root's owncontracts/shape-pin.yamlhasfiles:rows for, read byupdate-shape.py'sread_pin. A file with no row is not a copy, though this standard's copy lists name its path. That covers the file an adopted project made its own by dropping the row, and the source's own file beside ashape/collision copy. Where the shape pin does not read, nothing is a copy.Anything else is the project's own, whatever it is called. A
contracts/release-pin.yamlthat no leg and no product names is the project's file.Where
project.yamldoes not load, it declares nothing. A leg or neutral product pin that does not load either is then the project's for that run. That is the safe side: a file wrongly called a tool's loses a person's edit, and one wrongly called the project's costs a run.The shape pin and a leg pin are put back from git.
pin_restore(ctx, detail=None, pin=SHAPE_PIN), 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 helper, chooses the command from the file's own git state:unmerged:git -C <root> checkout --ours -- <file>, or--theirs.uncommitted:checkout HEAD -- <file>, where HEAD's copy reads.hidden:ls-files -v, where HEAD's copy reads and status shows no change.committed:checkout <breaking>~1 -- <file>, the newest copy in the history walk that reads."Reads" is
pin_reads_at:check_pinfor the shape pin, and for any other file, loading as a mapping. Only the shape pin takes copies with it in a merge, and it names a conflicted manifest or README.md for a person. The copies it takes are the ones the taken side's pin has rows for (pin_aton the index stage). Any other file is named alone. A leg pin's gitlink is left out, becausegit checkout --ourson a conflicted gitlink exits 0 and changes nothing (tools/gitlink-ours-probe.log).A copy and a neutral product pin are shown, never put back (
edited_by_hand). A copy can hold the project's own edit, above all the merge by hand thatupdate-shape.py'sbothexit asks for. No tool writes a neutral product pin after the scaffold. So in every state the NEXT is a read-onlygit diff, and the note leaves the merge or the mend to a person:unmerged:git -C <root> diff -- <file>, which shows the hunks git could not merge. The note: merge it by hand, keeping the hunks git merged, then finish the merge. That is what theshape currencyrow's--accept-localand AGENTS.md's "bothis a merge" say too.uncommitted:diff HEAD -- <file>.committed:diff <breaking>~1 -- <file>, every change since the newest copy that reads.A file of the project's own is not put back from git. Its
restoreisproject. Where no file has a command, the NEXT is this doctor again, and its note says what the file should say is for a person to decide. That covers a file of the project's own, and a declared pin with no good copy in its history (none). It matches shape-doctor.py calls an assembly root whoseproject.yamldoes not parse (a merge conflict) NOT A SHAPE ROOT, exit 2, and its NEXT line tells the reader to adopt that assembly root in place while the Triad row calls it a single repository #205's answer for aproject.yamlthat does not load.The shape pin's command comes first, and it is the same command the
shape currencyrow prints. Then each other file follows in the row's order. A row prints one command, and the next run names the next file.The reason quotes the loader for each file, without the path it starts with:
unreadable as YAML: contracts/shape-pin.yaml (yaml-unparsable: line 30: merge keys are not supported). A file that loads but is not a mapping says what it loaded as (it loads as a list, not a mapping).detailgains two keys, only when a file does not load:restore({file: unmerged|uncommitted|hidden|committed|none|project}) andrefusals({file: {code, detail}}). Every other root reports the row it always did, keys included.Another fault in the same row (an unregistered kind, a kind registered elsewhere (shape-doctor.py:
manifest kindscounts acontracts/*.yamlthat declares a registered kind as validated though that kind's validator never runs on this kind of root, so acontracts/family.yamlin an assembly root, or acontracts/project.yamlin a family holder, isokand COMPLIANT where the same bytes at the root are a FINDING #327), or afamily.yamlbesideproject.yaml): the reason names every fault, and the NEXT is the restore alone. The next run names the rest.Every new note avoids
;, apostrophes and backticks (#277). Each command runs as printed throughshlexon POSIX, and through the file'spowershell_tokenson Windows (pasted).Why
The sentence was written for a file that loads and declares a kind nothing validates. A file that does not load has no
kind:to register, so the only half a reader could follow was "remove the file from the root". For a conflicted or non-UTF-8contracts/shape-pin.yaml, that means deleting the pin. AGENTS.md tells an agent to run the NEXT a FINDING names. That NEXT was not even a command: the apostrophe in "shape-doctor.py's" stopsshlex.splitwith "No closing quotation".The reproduction at 9f64bd0 is in
tools/repro-base-9f64bd0.log. It covers a conflicted shape pin, a committed non-UTF-8 byte in it, a conflictedcontracts/spec-pin.yamland a conflicted project contract. All four gave the sentence.Fix round (review of ebffb08)
contracts/<x>-pin.yamlwas put back from git by its suffix, and the NEXT discarded the person's editdeclared_pins); aproject.yamlthat does not load declares nothing (the safe side)uncommittedNEXT discarded a hand mergegit diff HEAD -- <copy>and keptF3 (a leg pin's history restore ignores the gitlink lockstep) and F4 (the doctor says COMPLIANT in the middle of a merge, as main does, now #424) are follow-ups.
Fix round 8165 (re-verify of 2745890, and codex on 2745890)
checkout --ours, and the project's merged hunk was lost. A committed hand merge gotcheckout <sha>~1. Theshape currencyrow said--accept-localfor the same file.unmergedandcommittedstates get what itsuncommittedstate got: a read-onlygit diffand a merge by hand. The two rows and AGENTS.md now describe one path.files:rows name (pinned_copies), and the pin's own merge names the copies the taken side's rows list.checkout HEAD, which discarded the re-pin.New tests, one per probe, each red at ce2ce75 (
tools/f358-red-ce2ce75.log.gz: 10 failed, 13 passed of the 23 selected):test_a_copy_conflicted_on_its_own_is_shown_and_merged_by_a_person(P10).test_a_conflicted_copys_next_agrees_with_shape_currency_and_agents_md(P10b). It follows the merge by hand, then theshape currencyrow's ownapply --accept-local, to COMPLIANT, with every line of both sides in the copy.test_a_file_the_shape_pin_has_no_row_for_is_the_projects(P11a).test_the_sources_own_file_at_a_collided_copy_path_is_the_projects(P11b).test_a_conflicted_shape_pin_takes_only_the_copies_its_side_has_rows_for(the pin's own merge in the collision layout).test_a_committed_hand_merge_of_a_copy_is_shown_not_rolled_back(P12).test_a_neutral_product_pin_is_shown_never_put_back, with cases uncommitted (P13), committed and conflicted.test_a_pin_is_what_the_root_declares_not_what_it_is_callednow expectsdiff HEADfor the declared neutral pin. The naming-copy case oftest_a_file_a_tool_wrote_that_does_not_load_is_put_back_from_gitmoved to P12's test, because a copy is no longer put back.Mutants at a5563c3: 6 of 6 killed (
tools/f358-mutants.log):unmergedstate takes a side againcommittedstate rolls back againMerged main
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's squash.git diff f6dd11a decff87onshape-doctor.py,update-shape.pyandtests/test_shape_doctor.pyis empty.originURL 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 (1184f67,origin_url/origin_name) and 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 (d6df36b,update-shape.py apply --add).git diff --stat d6df36b a5563c3listsshape-doctor.pyandtests/test_shape_doctor.pyonly.git merge-tree --write-tree d6df36b a5563c3is rc 0.Merged main after #343 landed
read_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's squash (update-shape.pyread_pinand the pinned validate-pins.py and validate-family.py: iteratepin.get("files") or []without asking for a list, so a shape pin withfiles: 5orfiles: trueends shape-doctor.py,update-shape.py checkandapplyand the project's own validator inTypeError: ... is not iterable, exit 1, with no refusal naming the pin #290:check_pin(pin, pin_path, root)refuses afiles:that is not a list asupdate-pin-row, sopin_reads_atpassedctx.rootand stopped catchingTypeError). 437a360 equals 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's final head 91a4a4f on all six of its files.pin_atalone (the body of the oldpin_reads_at, which this branch moved there), in the two hunks this body predicted under "Composes with".tests/test_shape_doctor.pymerged without a conflict.read_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's two sentences (check_pinrefusesfiles: 5asupdate-pin-row; aTypeErroris not caught), then this branch's paragraphs on any other file and on the rows. The body keeps this branch's (a file other than the shape pin reads as its mapping, the shape pin as its rows' paths) withus.check_pin(data, f"{rev}:{SHAPE_PIN}", ctx.root)andexcept (Refusal, us.YamlError, ValueError).ctx.rootlanding on this branch's form of thecheck_pinline. Its change over main is this branch's lines over d6df36b, with that samectx.root.git diff --stat 437a360 65def40listsshape-doctor.pyandtests/test_shape_doctor.pyonly, andgit merge-tree --write-tree 437a360 65def40is rc 0.unmergedandcommittednotes add ", so neither side is taken whole" and ", so nothing is rolled back". The reason now reads "..., and every change to one since was made by a person".test_a_neutral_product_pin_is_shown_never_put_backalso asserts that no sentence of the note holds two ", so " clauses. With the old words put back, its committed and conflicted cases fail (2 failed, 1 passed,tools/m358c-red-n1.log).pinned_copiesno longer catchesTypeError, by 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's reasoning forpin_at: nothing in a pin's bytes raises one now, so one means a caller out of step withcheck_pin. A pin that does not read still gives the empty set.Tests at 65def40 (
tools/m358c-final-summary.log):tests/test_shape_doctor.py: 329 passed, 3 skipped (326 at a5563c3, plus 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's threefiles:cases inROOT_PIN_FAULTS) (tools/m358c-final-doctor.log.gz). The skips need a Windows host for PowerShell.tests/test_doctor_validator_rows.py,tests/test_shape_advisory.py,tests/test_shape_pin_files_not_a_list.py, everytests/test_update_shape*.py(7 files),tests/test_cli_reference.py,tests/test_repo_hygiene.py: 419 passed, 3 skipped (tools/m358c-final-rest.log). The skips are the NTFS-junction test's three cases.scripts/render-cli-reference.py --check: ok. No--helptext changed.Tests at a5563c3
tests/test_shape_doctor.py: 326 passed, 3 skipped (tools/f358-final-a5563c3-doctor.log.gz). The skips need a Windows host for PowerShell.tests/test_doctor_validator_rows.py,tests/test_cli_reference.py,tests/test_repo_hygiene.py,tests/test_shape_advisory.py: 138 passed (tools/f358-final-a5563c3-rows-cli-hygiene-advisory.log).scripts/render-cli-reference.py --check: ok. No--helptext changed.The #291 tests:
test_a_file_a_tool_wrote_that_does_not_load_is_put_back_from_git, 7 cases. The shape pin conflicted, not UTF-8 and committed, and a list. A leg pin conflicted (withMakefileand README.md conflicted beside it, neither named), not UTF-8, not UTF-8 and committed, and past a list in its history. Each case checks the restore, the loader's code, the reason, the argv and the note. It runs the NEXT as printed, and the row isok.test_a_file_git_cannot_put_back_is_left_to_a_person, 6 cases. A conflicted contract of the project's own. A committedcontracts/release-pin.yamlnothing declares, edited, conflicted and committed (projecteach). A leg pin in a squashed root commit, and one added back after its deletion (noneeach). The NEXT is this doctor, and the file's bytes are left alone.test_an_unregistered_kind_beside_a_file_that_does_not_load,test_the_shape_pin_goes_first_and_each_run_names_the_next_file,test_a_pin_is_what_the_root_declares_not_what_it_is_called,test_a_leg_pin_is_left_to_a_person_while_project_yaml_does_not_load,test_an_uncommitted_change_to_a_copy_is_shown_and_kept, and the two compose tests with shape-doctor.py reads onlyproject.yamlat a root that also carries afamily.yaml, so an assembly root whosefamily.yamldeclares two unmounted members is COMPLIANT, and itsmanifest kindsrow says that kind has a registered validator, which is never run #211 and shape-doctor.py:manifest kindscounts acontracts/*.yamlthat declares a registered kind as validated though that kind's validator never runs on this kind of root, so acontracts/family.yamlin an assembly root, or acontracts/project.yamlin a family holder, isokand COMPLIANT where the same bytes at the root are a FINDING #327.The earlier rounds' mutants were killed at 462baf1, ebffb08 and 2745890 (
tools/mutants-462baf1.log,tools/mutant-left-guard.log,tools/x358-mutants-2745890.log).Composes with
read_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 landed as 437a360, and 3af2829 composes it (above).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 (978ca3e) and 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 (7e8bb26) each merge with 65def40 without a conflict (git merge-tree --write-tree, rc 0).contracts/shape-pin.yamland a pinned copy both conflicted, the NEXT takes the taken side's copies whole, so pasting it drops a person's--accept-localedit to the copy and the doctor then says COMPLIANT #485 is worked on a branch stacked on a5563c3 (see "Left out").Left out
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's design for a merge of twoapplyruns, 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's tests assert it. This PR changes only which copies it names. A copy that one side kept with--accept-localcan therefore lose to the other side in that merge. That is now shape-doctor.py: withcontracts/shape-pin.yamland a pinned copy both conflicted, the NEXT takes the taken side's copies whole, so pasting it drops a person's--accept-localedit to the copy and the doctor then says COMPLIANT #485 (v1.0.0), worked on a branch stacked on this one.detailhas noleft_for_a_person. Themanifest kindsrow passes nodetailtopin_restore. Where the shape pin's merge leaves a manifest or README.md to a person, the note names it, and theshape currencyrow'sdetaillists it.unreadable. That is pre-existing, and the row now says what the file loaded as.COMPLIANT(F4, shape-doctor.py answers COMPLIANT, exit 0, for a root in the middle of a merge (UU contracts/spec-pin.yaml,UU spec, a MERGE_HEAD,git commitrefusing), because no row asks git for unmerged paths andrecorded_gitlinkanswers an unmerged gitlink from HEAD, so a root an agent cannot commit is reported compliant, including after thegit checkout --oursthat a conflicted pin's NEXT names #424). Both are follow-ups.Closes #291
🤖 Generated with Claude Code
Summary by Sourcery
Make manifest-kind diagnostics preserve project edits and guide recovery of unreadable tool-managed files without destructive commands.
New Features:
manifest kindsdiagnostic to explain why files fail to load and provide an actionable, shell-safe next command.Bug Fixes:
shape currencydiagnostic and handle multiple simultaneous findings safely.Enhancements:
Tests: