Repository navigation
shape-doctor.py: a root's own contracts/shape-pin.yaml that does not 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 the git command that puts back the last pin a tool wrote as its NEXT, instead of CANNOT ANSWER, exit 3, sending the reader to git fetch --all in the standard (#237) - #267
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>
|
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 23 hours and 53 minutes by commenting |
|
(lane openRepoShape-2, 2026-10-11T22:49:02Z) @codex review |
Reviewer's GuideThe PR makes root-side contracts/shape-pin.yaml read failures explicit repository findings that produce INVALID/exit 1 and an update-shape check remediation, while preserving CANNOT ANSWER/exit 3 for failures encountered while preparing or resolving the upstream standard; extensive tests verify both paths and their verdict/output details. Sequence diagram for shape doctor refusal handlingsequenceDiagram
participant Doctor as shape-doctor.py
participant Update as update-shape.py
participant Root as Root shape-pin.yaml
participant Standard as Upstream standard
Doctor->>Update: read_pin(root)
Update->>Root: Read contracts/shape-pin.yaml
alt Root pin refuses
Update-->>Doctor: Refusal(code, detail)
Doctor->>Doctor: unreadable_pin_row()
Doctor-->>Doctor: INVALID, exit 1
Doctor-->>Doctor: NEXT update-shape.py check
else Root pin reads
Doctor->>Update: prepare(args)
Update->>Standard: open_upstream()
alt Upstream preparation refuses
Standard-->>Update: Refusal(code, detail)
Update-->>Doctor: Refusal
Doctor-->>Doctor: CANNOT ANSWER, exit 3
Doctor-->>Doctor: NEXT git fetch --all
else Preparation succeeds
Update-->>Doctor: Prepared comparison data
end
end
Flow diagram for shape pin refusal classificationflowchart TD
A[check_shape_currency] --> B[read_pin root contracts/shape-pin.yaml]
B -->|Refusal| C[unreadable_pin_row]
C --> D[INVALID exit 1]
C --> E[Next: update-shape.py check]
B -->|Success| F[prepare]
F -->|Upstream refusal| G[CANNOT ANSWER exit 3]
G --> H[Next: git fetch --all]
F -->|Success| I[Compare shape currency]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 092a5f914d
ℹ️ 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".
| f"{PYTHON} {quote_arg(ctx.shape / 'update-shape.py')} check " | ||
| f"--root {quote_arg(ctx.root)} --upstream " | ||
| f"{quote_arg(ctx.shape)} # the file at fault is " |
There was a problem hiding this comment.
Replace the self-refusing NEXT with an actionable repair
For every pin fault routed here—such as a deleted, conflicted, non-UTF-8, or unsupported-digest pin—this advertised NEXT immediately calls read_pin again and exits with the same refusal, without repairing or recovering anything. A user following the report therefore reaches a dead end and must improvise a hand edit, even though repository guidance requires each finding to name the exact fixing command and explicitly prohibits hand-editing a pin to clear a finding. Provide a recovery action tailored to the refusal, or add a safe repair workflow rather than presenting update-shape.py check as the next step.
AGENTS.md reference: AGENTS.md:L208-L214
Useful? React with 👍 / 👎.
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. |
|
(lane openRepoShape-2, 2026-10-11T23:50:12Z) This lane reviewed its own PR #267 at 092a5f9, before any fix round, read-only in its own detached worktrees. CHANGES NEEDED. Fixes come as commits on this branch. P2: the new NEXT does not fix anything (
An operator following AGENTS.md:207-213 is stuck; README.md:1131-1132 is false here. A git restore makes every class compliant with no hand edit:
The fix round picks the command from Nits.
Follow-ups, pre-existing.
Also checked.
Tests. Head: Merges. Main reached 67a398a mid-review; none of its five new commits touches these files. The full report is |
…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>
contracts/shape-pin.yaml that does not 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)contracts/shape-pin.yaml that does not 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 the git command that puts back the last pin a tool wrote as its NEXT, instead of CANNOT ANSWER, exit 3, sending the reader to git fetch --all in the standard (#237)
|
(lane openRepoShape-2, 2026-10-12T11:41:04Z) This lane re-verified its own PR #267 at 9f64bd0 after fix round 1, read-only in its own detached worktrees. CHANGES NEEDED.
Tests: head, 263 passed (doctor, cli-reference, hygiene). Head merged into main c7c02fa: CI at 9f64bd0: tests, tests-macos, tests-windows and SonarCloud pass. Merges: rc 0 with main c7c02fa and all 16 other open PRs. The report is |
…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>
… as `update-add-symlink` does, so the doctor's verdict that quotes a linked pin's or manifest's refusal stays one line (#286) `read_pin` and `prepare` always name exactly one path, and shape-doctor.py quotes their refusal inside its one-line verdict (`CANNOT ANSWER (shape currency: ...)`, or, once #267 lands, the `shape currency` row's reason). A detail spread over a header, an indented path line and a closing paragraph broke that verdict into three lines. One path now reads "`apply` would rewrite <path>, but <the link>. <why>", the shape `update-add-symlink` already has; several `copy-is-link` rows stay a list, one path a line, as `update-conflict` lists its own. The tests pin the one line where the doctor reads it: `check`'s refusal for a linked pin or manifest is a single REFUSED line, ending with the sentence that says this command never writes through a symlink. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-2, 2026-10-12T12:30Z) Fix round 2 is pushed: 8badae0 (9f64bd0..8badae0). It answers the re-verify of 9f64bd0 (CHANGES NEEDED, #267 (comment)):
Tests:
The PR body is updated, with a "Fix round 2" section. This lane re-verifies 8badae0 after its usage window resets. @codex review Lane: openreposhape-2 (openRepoShape-2) |
…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>
|
Codex Review: Something went wrong. Try again later by commenting “@codex review”. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
(lane openRepoShape-2, 2026-10-12T12:49Z) Composition note: #343 and this PR conflict semantically, though Whichever of #267 and #343 lands second applies three lines:
On a scratch merge, that gives 22 passed in #343's file and 12 in this PR's doctor tests. The patch is Lane: openreposhape-2 (openRepoShape-2) |
|
…tor-manifest-kinds-unreadable-pin: `manifest kinds` keeps #291's restore beside #375's registered-elsewhere finding (#291, #327) #267's branch, this PR's base, now carries main up to 5678f68, #375 and #357 among it. Merged as text, shape-doctor.py conflicts only in `check_manifest_kinds`: the four regions main's #205 and #211 already caused, now with #375's `ungated` in them. #357 does not touch that function, and `pin_restore` and `pin_reads_at` merge clean, because main left them as #267 has them. The resolution keeps every side. In the loop, #205's skip of the root's own manifest comes first, then #291's `load_yaml` and `refusals`, then #211's `beside`, then #375's split of a registered kind by `ManifestKind.runs_in` into `validated` and `ungated`. `registered_elsewhere` joins `detail` only when there is one. The reason names every fault. The NEXT is `"; ".join(fixes)` as on main, with the unregistered kind's sentence for an unregistered kind only, never for a file that does not load. A row with a file that does not load prints the restore alone: joined by `;` to #375's "take ... out of this root" or to #211's sentence, it would be a second command in a pasted line (#277). The next run names the rest. The #291 docstring paragraph now names those faults. Two compose-only tests, each beside a conflicted shape pin: a `family.yaml` beside `project.yaml` (#211), kept from the earlier composition, and a `contracts/family.yaml` in an assembly root (#327), new. Each NEXT is the restore alone and equals the `shape currency` row's. Run, the row is the other fault's alone, and #327's own `git rm`, run in turn, leaves the root COMPLIANT. On the merged tree: tests/test_shape_doctor.py 312 passed, 3 skipped (the Windows-only PowerShell cases); tests/test_doctor_validator_rows.py 28 passed; tests/test_cli_reference.py and tests/test_repo_hygiene.py 63 passed; tests/test_windows_paths.py and tests/test_shape_advisory.py 79 passed; scripts/render-cli-reference.py --check ok. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-12T19:52:17Z) A v1.0.0 scope notice. Brett Heap scoped openRepoShape's first release today: every procedure in AGENTS.md works end to end for ordinary repositories on Linux, macOS and Windows. The tag waits for every in-scope issue to close and a full test run to pass on all three OSes on the release commit. This PR closes #237 (P2, parent #460): in scope, in the |
#267 landed as decff87 with five ` # ` tails in `shape-doctor.py`: the merge is clean, and this PR's guard then finds those five (`pin_restore` x4, `unreadable_pin_row`), which the next commit converts (#277) Lane 2's #267 (`decff87`) wrote `pin_restore`'s four NEXTs and `unreadable_pin_row`'s one as a command followed by ` # <sentence>` while this PR was in CI. git merges the two without a conflict, and the merged tree's `test_no_string_in_the_doctor_ends_a_command_with_a_comment` fails on exactly those five strings and no other, which is the ruling working: the second lander converts. The conversion is the next commit. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…four and `unreadable_pin_row`'s one, are the command alone and the sentence after ` # ` is the row's `why`, so a root whose pin does not read pastes one command into an interactive zsh and this PR's guard is green on the merged tree (#277) #267 (#237) landed as decff87 while this PR was in CI. It wrote its NEXTs the way every doctor builder did before #277, `<git command> # <sentence>`, and its own docstring says "so a pasted line is one command in any shell" of notes it had made free of `;`, apostrophes and backticks. That holds for `sh -c` and bash. An interactive zsh without `interactivecomments` reads the `#` as a word, so `git -C <root> checkout HEAD -- contracts/shape-pin.yaml # puts back the pin ...` hands `#`, `puts`, `back` ... to git as pathspecs and the pin is not put back, which is the defect of this PR. Merged with main, the guard `test_no_string_in_the_doctor_ends_a_command_with_a_comment` found exactly those five strings and no other (an AST scan of main 245d775 finds 25 tails, the 20 this PR already converted and these 5). 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 the five old tail expressions and the five new (command, why) pairs as text, with each interpolation a placeholder: command + ` # ` + why is the old expression character for character in all five. `pin_restore` is the helper that built them and returned `(restore, command)`. It returns `(restore, command, why)` as `lockstep_next` returns `(command, why)`, and `(restore, None, None)` where git can name nothing. Its one caller, `unreadable_pin_row`, unpacks it, and where the command is None builds the `update-shape.py check` line itself and hands the sentence it always carried to `why=`. Their docstrings said "the note" and "each note holds no `;`, apostrophe or backtick, so a pasted line is one command"; 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 one property case per branch (29 cases): `pin_restore-unmerged`, `-uncommitted`, `-hidden`, `-committed` and `unreadable_pin_row`, each built by the fixture #267's own tests use for that state, each asserting the branch it reached and that `why` opens with the words printed after ` # `. `tests/test_shape_doctor.py` changes only #267's tests, where each read the sentence off the NEXT: * `pasted` split the line at ` #` before reading it, which would now cut a NEXT that grew a tail back and so keep every argv assertion green; it reads the whole line (POSIX); * `assert_a_plain_note(line)`, which held the note free of `;`, apostrophes and backticks because it was pasted, is `assert_a_plain_next(row)`: the NEXT has no ` #`, and the sentence is in `why`. Nothing follows the command now, so the old constraint has nothing to constrain; * the five reads of `row["next"].split(" #", 1)[1]` read `row["why"]`: the commit the sentence names (the committed-break test and the two-commits test), the README and `project.yaml` notes (the two merge tests that leave a file to a person) and the `DU` test's "only that side". All nine callers of the old helper, eight tests and `assert_hidden`, call the new one. No expectation of #267's about the command, the restore kind or the pin's state changed. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t: `check_pin` takes the root its `update-pin-row` remediation reads, so #290's refusal survives #267's split instead of a NameError (#290) Main 580d7df carries #267 (decff87), which split update-shape.py's `read_pin(root)` into the read and `check_pin(pin, pin_path)`, so that shape-doctor.py can check the pins in a root's history in memory. The `update-pin-row` raises moved into `check_pin`, and on this branch they call `pin_rows_remediation(root)`, #290's `git -C <root> log` advice, where `root` is not a name. The textual merge is clean and the tree dies at run time: `NameError: name 'root' is not defined` for a `files:` of `5`, `true` or `1.5` in `check`, `apply` and shape-doctor.py, which then prints no report at all. This commit takes the planned #267 composition, the PR's two attachments fix1-compose-267-resolution-v2.patch and root-pin-faults-after-267.patch, applied unchanged; main moved their hunks by offset only (shape-doctor.py 526 lines, tests/test_shape_doctor.py 521, update-shape.py 1): - update-shape.py: `check_pin(pin, pin_path, root)`, `read_pin` passes its root, and the docstring says `root` is the repository whose history a refusal's `git log` reads. - shape-doctor.py `pin_reads_at` passes `ctx.root`. Its docstring says `check_pin` refuses `files: 5` as `update-pin-row`, and `TypeError` leaves its `except` tuple: no pin's bytes raise one now, so one means a caller out of step with `check_pin`, and the doctor fails loudly rather than answer that no commit holds a pin to put back. - tests/test_shape_doctor.py: #267's ROOT_PIN_FAULTS gains `files: 5`, `true` and `1.5`, each `update-pin-row`, so each is INVALID, exit 1, with `checkout HEAD` as its NEXT, which run as printed leaves the root COMPLIANT. Main's other changes to this PR's files merge as text away from these lines: #446 (245d775) in validate-pins.py, #406 (ec8e09a) in validate-family.py, #379 (87d2eb7) in update-shape.py, and #375 (cd1fc79) and #357 (5678f68) in shape-doctor.py. Neither module has changed on main since decff87. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…landed: `_commit_the_split` keeps #445's reported steps and reads `follow_ups:` through `_plan_follow_ups` Main moved from dd8d0ea to 580d7df: decff87 (#267), dba5fb0 (#403), 245d775 (#446), 75052cb (#445), ec8e09a (#406) and 580d7df (#416). Only #445 touches adopt-project.py, and it conflicted in one hunk of `_commit_the_split`. #445 wraps that function's body in a `try` that names each step (cloning the source for the split, mounting the legs, committing the split, pushing the split branch), so a failure once both legs are pushed flushes stdout, prints `REFUSED <step>.`, what failed and one notice of what the run left. This branch reads `follow_ups:` there through `_plan_follow_ups`, the one read of it (#293). The resolution keeps #445's `try`, steps and reports whole, and carries this branch's read into its "committing the split" step: git had kept main's `[str(f) for f in plan.get("follow_ups", [])]` inside the `try`. The two contracts meet without overlap. `plan-bad-follow-ups` and `election-date-malformed` are findings of `_entry_findings`, which `execute` refuses through `_refuse_what_check_finds`. That runs before #445's `_refuse_an_occupied_work_dir`, `_refuse_a_split_branch_left_behind` and `_create_leg_remotes`, so these are refusals before any leg exists: exit 2, `REFUSED <code>:` and the remediation that says no leg repository was created and nothing was pushed, as #445's own refusals before the legs say, and no notice. #445's report covers the failures once a leg exists, which these values no longer reach. The guard in `_manifest_values` is unchanged; #445 does not touch it. #445's comment over `_template_values` in `cmd_execute` named "an `elected_on` that is not a date" as a refusal the table can raise. With #294's finding, `_refuse_what_check_finds` refuses such a date first, so the comment now says the date no longer reaches the table. #445's test of a table refusal stubs `_template_values` and is unchanged. tests/test_adopt_execute_refusals.py: the two end-to-end runs through `main` (#293, #294) now also assert that the refusal says no leg was created and that #445's notice ("NOTHING has been rolled back") does not follow it. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oduct pin, get a read-only `git diff` in every git state, and a copy is what the pin's `files:` rows name, so the `manifest kinds` NEXT no longer drops a person's merge, re-pin or edit with `checkout --ours`, `checkout HEAD` or a `<sha>~1` rollback (#291) Codex re-reviewed 2745890 with two P1s, and rv358 reproduced both (probes P10 to P13). r4250578605: a copy can hold the project's own edit -- above all the merge by hand `update-shape.py` asks for where a copy changed on both sides -- yet only its uncommitted state was shown. Conflicted on its own, the NEXT took one side whole with `checkout --ours`, and the hunk git had merged cleanly went with the other side; committed, it rolled the hand merge back with `checkout <sha>~1`. Now `pin_restore` gives a file `edited_by_hand` a read-only command in every state: `diff` for `unmerged`, `diff HEAD` for `uncommitted`, `diff <sha>~1` for `committed`. The note asks for the merge by hand, which is what the `shape currency` row's `--accept-local` and AGENTS.md's "`both` is a merge" already say. r4250578619: `tool_written` took copies from this standard's static copy lists, so a `contracts/repository-naming.yaml` an adopted project made its own (its row dropped) or kept beside a collision copy under `shape/` was put back from git. A copy is now what the root's own shape pin has a `files:` row for (`pinned_copies`), and where the pin does not read nothing is a copy, the safe side. The shape pin's own merge, which takes the copies whole with the pin (#267), now names the copies the taken side's pin has rows for (`pin_at` on the index stage), not the lists. rv358's P3: no tool writes a neutral product pin after the scaffold, so its change is a person's and gets the same read-only commands. Tests: one per probe, red at ce2ce75 (10 failed of 23 selected). 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>
…eport of a failure after the legs carries the materializer's refusals, and its two early refusals follow this branch's `core.symlinks` probe origin/main moved dd8d0ea -> 75052cb (four commits), merged by sha: - decff87 (#237 via #267), shape-doctor.py and update-shape.py; - dba5fb0 (#383 via #403), scripts/bump-leg.py and docs/handbook.html; - 245d775 (#412 via #446), the template's scripts/validate-pins.py; - 75052cb (#171, #188 and #177 via #445), adopt-project.py and AGENTS.md: every failure once a leg repository exists is reported at its step as `REFUSED <step>.`, what failed, and `_nothing_rolled_back`'s notice, exit 2; `execute` refuses `adopt-work-dir-occupied` and `adopt-branch-exists` before it creates anything. tests/test_adopt_ignored_shape_files.py and every other file merge with no conflict. adopt-project.py conflicts in five hunks, each resolved by keeping both sides: 1. The materializer's import list: this branch's names (BOOTSTRAP, SYMLINK_HOPS, TOO_MANY_SYMLINKS, template_files), without RULESET_HINT, which #188 replaced by its own ADOPT_RULESET_HINT and nothing here uses. 2-4. `Source`: this branch's `work_root`, where a climb above the root leads (#183), and #188's `cloned`, read by `_cloned_here`. Both are keyword-defaulted, so the four-argument constructions in the tests and #188's `cloned=True` read as before, and `open` hands both on. 5. `_commit_the_split`: main's steps inside #188's report, with this branch's `spell=_spelled`, and its comment, on the materializer call. How #188 meets this branch's code: - `execute`, all before `_create_leg_remotes`: `_refuse_what_check_finds` (with `plan-root-symlink`), this branch's `core.symlinks` probe (`adopt-symlinks-off`), `_refuse_unconsented_seeding`, then #188's `adopt-work-dir-occupied` and `adopt-branch-exists`, then the confirmation and `_template_values`. A plan's own faults come first, then whether `git` here can hold what the plan keeps, then what an earlier run left, whose way out is "run `execute` again". The probe's throwaway repository (`symlinks-probe-*` in the work directory) is removed before `_refuse_an_occupied_work_dir` reads the work directory, and it is none of the three directories that function asks about. - The materializer's refusals (`materialize-outside`, `materialize-symlink-loop`, `materialize-blocked`) are raised inside the step "committing the split", which #188 wraps. They are `Refusal`s, so `_failed_loudly` prints `REFUSED committing the split. <code>: <detail>` on one line, still spelled by `_spelled`, then their remediation, then the notice that both legs are pushed; `execute` exits 2, as it did. - The two end-to-end tests of that path expected the old one-line `REFUSED <code>:`. The next commit makes them expect #188's report. Checked on the merged tree: - AST (fin183g/names.py, base dd8d0ea): no top-level name is bound twice in adopt-project.py, scaffold-project.py or scripts/repo_shape.py, and none is loaded but bound nowhere; scripts/shape_materialize.py binds `YamlError` and `parse_yaml` twice, as main does since 3ef059c. Main changed the signatures of `Source.__init__` (`cloned=False`), `_create_leg_remotes` (`work_root`, `source=None`) and `_verify` (`urls`, `branch`); every call of the last two is main's, and every construction of `Source` passes four arguments or `cloned=` by keyword. - Both sides intact (fin183g/sides_intact.py): the only lines that differ from either side's own change are those of the five hunks. - A rehearsal of this merge (tree 59eca4e, the same as this one), with the next commit's test change, passed tests/test_adopt_root_symlinks.py, tests/test_adopt_late_failure.py, tests/test_adopt_filter_repo_failure.py and tests/test_adopt_ignored_shape_files.py: `231 passed`. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alidate-pins.py and validate-family.py report `shape-pin-row`, when a shape pin's `files:` is not a list, so `files: 5`, `true` or `1.5` no longer ends `check`, `apply`, shape-doctor.py and the project's own validator in `TypeError: ... is not iterable` (#290) (#343) Every reader of a shape pin took `pin.get("files") or []` and looped over it. `or []` catches the falsy values, but a truthy scalar got through: a committed `files: 5`, `true` or `1.5` ended `check`, `apply`, shape-doctor.py and the project's own validator in `TypeError: ... is not iterable`, exit 1, and the doctor printed nothing. The validators read a string such as `abc` one character at a time, one finding per character. update-shape.py's `check_pin`, which `read_pin` and shape-doctor.py's `pin_reads_at` both call, now asks for a list and refuses anything else as `update-pin-row`, naming the value. Both `update-pin-row` refusals carry `pin_rows_remediation(root)`: put back the pin a tool last wrote, from the root's own history. Its `git -C <root> log -- contracts/shape-pin.yaml` is spelled by `_by_hand`, so it reads the project's history when the tool runs from a checkout of the standard with `--root`. `check_pin` takes the root for it, and `pin_reads_at` passes `ctx.root` and no longer catches a `TypeError`: no pin's bytes raise one now, so one means a caller out of step with `check_pin`, and the doctor fails loudly instead of answering that no commit holds a pin to put back. The pinned validate-pins.py and validate-family.py report one `shape-pin-row` finding for the whole `files:`, and shape-doctor.py's `pinned_paths` returns the empty set, as for a pin that is not a mapping. The doctor reports again: the verdict is INVALID, exit 1, with #267's restore command as the shape-currency row's NEXT, and the `pins` or `family` row carries the validator's finding. The falsy values `0`, `false`, `null` and `[]` keep `update-pin-no-files` and `shape-pin-no-files`. The two validators are pinned template copies, so an existing project or holder sees one `upstream-changed` row, for its validator, until it runs `apply`. update-shape.py spells the pin's path by the materializer's `SHAPE_PIN` in the remediation, `cmd_apply` and `_awaits_its_pull_request`. tests/test_shape_pin_files_not_a_list.py runs `5`, `true`, `1.5` and `abc` through `check`, `apply`, both validators, the doctor's rows and a family holder, and runs the printed `git log` from outside a root whose path holds a space; `files: 0` and `false` are its controls. tests/test_shape_doctor.py adds the three scalars to `ROOT_PIN_FAULTS`, each INVALID with a NEXT that, run as printed, leaves the root COMPLIANT. Closes #290 Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#291, #189, #424, #478 and #491 changed `shape-doctor.py`: six conflicted hunks, all in `pin_restore` and `placement_verdict`, keep main's new `pin` argument, `shown` branches and #189 helpers with this PR's `(command, why)` NEXT form, and the tails main added outside them are converted by the next commit (#277) `git merge-tree` of main cc8f2da and c169a2d conflicts in `shape-doctor.py` alone. Lane 2's #291 (5a25387) generalised `pin_restore` from the shape pin to any file under `contracts/` a tool wrote (`pin`, `what`), and added a read-only `git diff` branch for a file a person may have edited (`shown`) in each state; this PR had moved the sentence of each of #267's four branches off the NEXT into `why` and made the helper return `(restore, command, why)`. #189 (7a8b981) split `placement_verdict`'s neighbours out. Each hunk, and its resolution: 1. `pin_restore`'s signature and docstring head: main's `pin: str = SHAPE_PIN` argument, this PR's three-tuple return and its docstring sentence about `why`. 2. `unmerged`, `checkout --ours/--theirs`: the command alone, and main's sentence (`hand-merged {what}`) as the `why`. 3. `uncommitted`, `checkout HEAD -- <pin>`: main's `{quote_arg(pin)}` and `the {what}`, the sentence as the `why`. 4. `hidden`, `ls-files -v -- <pin>`: likewise. 5. `committed`: main's new `if shown:` branch (`diff <commit>~1 -- <pin>`) and its `checkout <commit>~1 -- <pin>`, each the command alone with main's sentence as the `why`, and this PR's `return "none", None, None`. 6. `placement_verdict`: main's new `misplaced_clauses` and `submodules_verdict` above it, and this PR's signature, which takes the plan's `why` as `plan_why`; its body merged without a conflict. Every sentence in hunks 2-5 is main's, character for character: each old tail expression and each new (command, why) pair, rendered as text with every interpolation a placeholder, read command + ` # ` + why identical. NOT YET RUNNABLE ON EVERY PATH, ON PURPOSE. Main's other two new `shown` branches of `pin_restore` (`diff --` while unmerged, `diff HEAD` while uncommitted) and the new `unreadable_next` were not in a conflicted hunk, so they are as main wrote them: tails, two-tuples, and a caller that unpacks two values from what is now a three-tuple. The next commit converts them and is the first that runs; this commit records the merge alone. Lane: openreposhape-4 (openRepoShape-4) 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>
'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>
…d gives a copy conflicted beside it to a person to merge by hand, so pasting it no longer drops an edit kept with `--accept-local` and then says COMPLIANT (#485) (#496) A merge of two branches that each re-pin the shape can leave `contracts/shape-pin.yaml` and a pinned copy both conflicted: an `update-shape.py apply` to a newer standard on one side, and on the other a person's edit of the same copy, kept with `apply --accept-local`, the exit the `shape currency` row names for a locally modified copy. The `shape currency` and `manifest kinds` rows both printed `checkout --ours -- <copy> contracts/shape-pin.yaml`, the taken side's copies whole with the pin. With the update side as ours, pasting it dropped the person's lines, the hunks git had merged cleanly included, and once the merge was finished the doctor said COMPLIANT. `pin_restore`'s `unmerged` case now names the shape pin alone, and its note says to take one side of the pin whole. The conflicted copies the taken side's pin has `files:` rows for join `detail.left_for_a_person` beside a conflicted manifest or README.md, and the note says what a copy conflicted on its own is told: a copy can hold an edit of the project, so neither side is taken whole, and a person merges it by hand, keeping the hunks git merged. Once the pin reads, the next run names the copy's read-only `git diff`. Where the merged copy differs from the pin, `apply --accept-local` on it re-pins it from the merge, AGENTS.md's `both` exit. The `shape currency` row names that for a `locally-modified` copy, and where `check` calls the copy `both`, as with the person's side's pin taken, the row's `apply` is refused `update-conflict` and asks for it. The `manifest kinds` row prints the same command and now says the same of it. A copy the shape pin's merge leaves to a person is `unmerged` in its `restore`, where it was `project` while the NEXT overwrote it, and its `detail` carries the same `left_for_a_person`. The two tests from #267 that asserted the copies taken whole now assert the pin alone, the Makefile left to a person and its bytes kept through the NEXT. The first is renamed from `test_the_issues_own_merge_takes_one_side_of_the_shapes_files_whole` to `test_the_issues_own_merge_takes_only_the_pin_whole`. New tests build the state with the real `update-shape.py`, for the naming copy and for the Makefile, each merged in both directions. They run every NEXT as pasted and keep the person's lines through it. The verdict is `INVALID (merge)` while the merge is unfinished and the copy's drift once it is committed, and the root reaches COMPLIANT only once the copy is merged by hand and re-pinned with `--accept-local`, with every line of both sides in HEAD. Closes #485 Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and the `shape currency` row's `apply` from the row's `why`, and this PR's property cases hold #496's pin-alone NEXT, with and without a copy conflicted beside it, so the merged tree is green and a NEXT pasted after a two-sided re-pin is one command (#277) #496 (fc48854, #485) rewrote `pin_restore`'s `unmerged` NEXT to take the shape pin alone and give a copy conflicted beside it to a person. Its one tail, `checkout {side} -- <pin> # {note} and never a hand-merged {what}{then} ... this doctor again{after}`, sat in a conflicted hunk, and the merge before this commit made the command the NEXT and the sentence the `why`, character for character. An AST scan finds no tail outside the guard's allow-list at the merge or here; main fc48854 still holds 29, the same 29 sites as 47b1e06, so no count changes. `shape-doctor.py`: `pin_at`'s docstring says the conflicted files "its `why`" gives a person, where #496 wrote "its note", as the merge wrote the other #496 docstrings. `tests/test_shape_doctor.py`, #496's two helpers outside every conflicted hunk: * `assert_the_pin_alone_is_taken` read the sentence with `currency["next"].split(" #", 1)[1]` and ended in `assert_a_plain_note`, which this PR renamed; it reads `currency["why"]` and calls `assert_a_plain_next(currency)`. Its `kinds["next"] == currency["next"]` compared the two rows' sentences as well as their commands while the sentence rode on the NEXT, so it gains `kinds["why"] == currency["why"]`, which keeps #496's "both rows say the same of the same command"; * `re_pinned_from_the_merge` cut the `then apply ...` off the `shape currency` NEXT; it splits `currency["why"]`. Every expectation of #496's about a command, a restore kind, a file's bytes or a verdict is unchanged. The tests using them, `test_a_copy_conflicted_with_the_shape_pin_is_left_to_a_person` and `test_a_makefile_conflicted_with_the_shape_pin_keeps_the_projects_target`, each in both merge directions, run as #496 wrote them. `tests/test_doctor_next_is_a_command.py`: * `pin_restore-unmerged` opens with #496's words, "Take one side of the pin whole and never a hand-merged pin", where #267's said "Take one side whole for every path named"; * a new case, `pin_restore-unmerged-copy-beside`, conflicts the pin and the Makefile together with #291's `conflict_in`, asserts `left_for_a_person == ["Makefile"]`, and holds the pin-alone NEXT to the property; 35 cases in all. Mutant: #496's tail put back on the pin-alone `checkout` line fails the guard (`pin_restore`, ` # `) and both `unmerged` property cases (`shlex` reads a comment): 3 failed. Lane: openreposhape-4 (openRepoShape-4) 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: #237 (comment)
What
shape-doctor.py'scheck_shape_currencyno longer treats a fault in the ROOT's owncontracts/shape-pin.yamlas this checkout failing to ask. The row's NEXT is now the git command that puts back the last pin a tool wrote.update-shape.py'sread_pin, the functionpreparecalls for it. A refusal from that read becomes the newunreadable_pin_row:FINDINGwith noenvironment;the copies cannot be compared, because contracts/shape-pin.yaml does not read as a pin: REFUSED <code>: <detail>;detail={error, pin, refusal: {code, detail}, restore}, plusleft_for_a_personwhere a merge left one of the project's files conflicted (below).preparedoes after that read (open_upstream,Upstream.resolve) is caught exactly as before. Those cover a pinned commit missing from this standard and a standard that cannot be read. They giveCANNOT ANSWER, exit 3,environment: Trueand thefetch --allNEXT. The split is made by which read raised, not by a list of refusal codes and not by text. A refusal added later to either side therefore lands correctly without this file being told.pin_restorereads that state without writing anything:git --no-optional-locks status --porcelain=v1read as bytes,ls-files -u,rev-list, andcat-file blobread in memory. It records the case asdetail.restore, and the first case that applies decides:unmerged: a merge left the pin conflicted. The NEXT isgit -C <root> checkout --ours -- <paths>. The paths are every conflicted path the shape writes whole: the pin, the copies and their collision copies. Where only the other side's index stage holds a pin (DU,UA), the NEXT is--theirs.README.mdare never named.applywrites two lines of the manifest'sshape:block and README'sShape:line, and every other line is the project's own, so taking either file whole would drop the other side's edits, hunks git merged cleanly included.update-shape.pywrites from the side taken, anddetail.left_for_a_personlists it. shape-doctor.py: a project.yaml or family.yaml that is there and does not load is INVALID (manifest) or INVALID (family), exit 1, quoting load_yaml's own refusal and naming the project's own validator, instead of NOT A SHAPE ROOT, exit 2, with an adopt-project.py plan NEXT and a Triad row calling an assembly root a single repository (#205) #232'smanifestrow says the same of that file.uncommitted: HEAD holds a pin that reads, and the tree or the index differs from it. The NEXT isgit -C <root> checkout HEAD -- contracts/shape-pin.yaml.hidden: HEAD holds a pin that reads, andgit statusshows no change to the pin. An index flag hides the copy (a sparse checkout's skip-worktree, or assume-unchanged), or the file itself cannot be read. No commit broke it, so the NEXT names no rollback: it isgit -C <root> ls-files -v -- contracts/shape-pin.yaml, which prints theSorh, and its note says clearing that is for a person.committed: a commit broke or removed the pin. The NEXT isgit -C <root> checkout <that commit>~1 -- contracts/shape-pin.yaml. The pin's history is walked newest first (at most 50 commits), and the commit named is the first whose parent holds a pin that reads, so two commits in a row that each broke the pin are reached past. This case also covers a bad pin staged where HEAD holds none (A).none: no history to restore from (a root commit, no git, a walk that meets a parent with no pin). The NEXT staysupdate-shape.py check --root <root> --upstream <standard>, and its note says that whether the root was ever scaffolded or adopted is for a person to decide.pin_reads_at):cat-file blobin memory, never a temporary file, decoded and parsed asload_yamlreads a file, thencheck_pin. That isread_pin's own checks, split out of it inupdate-shape.pyso the two cannot disagree about a pin.;, an apostrophe or a backtick, so a pasted line is one command in any shell (shape-doctor.py: fifteen NEXT builders end the command with# <comment>, which a stock interactive zsh (macOS's default shell) does not read as a comment, so pasting the line hands the comment's words to the command as arguments, runs whatever follows a;, and runs themake bootstrapthelegsandmemberscomments put in backticks, then exits 127 #277).environment, it reaches the gates that are red:INVALID (pins, manifest-kinds)for a conflicted pin;INVALID (pins)for acommit:that is not 40 hex;INVALID (shape-currency), through the catch-all, where this row is the only red one.The reason follows #232's wording for an unloadable manifest: the loader's own
REFUSED <code>: <detail>line without itsRemediation:trailer. Foryaml-unparsable, that trailer is about submodules.--helpis unchanged, sodocs/cli.mdis too (render-cli-reference.py --checkpasses). Inupdate-shape.py, the only change is thatread_pin's checks move intocheck_pin(pin, pin_path), whichread_pincalls; its behaviour is unchanged.Why
On main, each of these root-side faults answered
CANNOT ANSWER, exit 3, which outranksINVALID, withgit -C <standard> fetch --allas its NEXT:contracts/shape-pin.yaml(the realistic merge of two shape updates);commit:that is not 40 hex;AGENTS.md says exit 3 is "THIS CHECKOUT failing to ask, never a statement about their repository". A reader therefore followed it to a
fetchthat succeeds and changes nothing.Reproduced on main 39ac59c with the issue's own steps (scaffolded Atlas, two branches rewriting
commit:,git merge):CANNOT ANSWER (shape currency: ... line 30: merge keys are not supported), exit 3;REFUSED shape-doctor-cannot-answerwith thefetch --allremediation;--jsondetail is{"error": "yaml-unparsable", "environment": true}.On this head the same tree reads
INVALID (pins, manifest-kinds), exit 1, with nothing on stderr.The NEXT is a git command, not a tool of this standard.
validate-pins.py(the issue's suggestion) andupdate-shape.py check(this PR's first head) both read the pin with the same loader, so run as printed each refuses it again.update-shape.py's remediation for the case is also wrong for it:git submodule updatefor a conflict, "adopt it" for a deleted pin, and "re-save the file" (a hand edit of a pinned file) for a stray byte. A pin is never mended by hand, so the fix is the last pin a tool wrote, taken from the root's own history. Thepinsrow still namesvalidate-pins.pywherever it is red.Tests
tests/test_shape_doctor.pygains 26 tests. They sit besidetest_a_pin_this_standard_cannot_resolve_is_exit_three, which is untouched and still passes.commit:not 40 hex, anotherdigest_definition,files: [], a row with nopath, a deleted file. Each case asserts:detail.erroranddetail.refusal.code;FINDING, exit 1, anINVALID (verdict and noenvironment;detail.restoreuncommitted, and a NEXT ofgit -C <root> checkout HEAD -- contracts/shape-pin.yaml, with nofetch, noupdate-shape.pyand a plain note;CANNOT ANSWER, and noshape-doctor-cannot-answerorfetch --allon stderr;COMPLIANT. "As printed" means shlex on POSIX and PowerShell's tokens on Windows.pinsrow still namesvalidate-pins.py, and the verdict isINVALID (pins, manifest-kinds).digest_definition: other-v9. The shape-currency row is the one red row:INVALID (shape-currency).CANNOT ANSWER, exit 3,fetch --all, anddetail.errorupdate-revision-unresolvablewithenvironment.INVALID, exit 1.INVALIDanduncommitted, and its NEXT, run as printed, makes the pin read again.git rmof the pin; the other is acommit: "zzzz". Each givescommittedandcheckout <breaking commit>~1 -- contracts/shape-pin.yaml, and endsCOMPLIANTonce that has run.commit: "zzzz", thencommit: "yyyy"). The NEXT names the first break's~1, and endsCOMPLIANT.committed(the pin before the break), notcheckout HEAD, and it endsCOMPLIANT.Makefileand a file of the project's own areUUbeside the pin. The row givesunmergedandcheckout --ours -- Makefile contracts/shape-pin.yaml, with noleft_for_a_person. The project's own file is left conflicted. After the NEXT runs, the row reads the pin; with the fixture's made-up commits the row then saysupdate-revision-unresolvable.update-shape.py applyre-pins shape/a to the first and shape/b to the second. Shape/a also renames the project inproject.yamland adds a README paragraph; shape/a is merged into shape/b, leavingMakefile, the pin andproject.yamlUU. The row (check_shape_currency, asked in process, because without shape-doctor.py: a project.yaml or family.yaml that is there and does not load is INVALID (manifest) or INVALID (family), exit 1, quoting load_yaml's own refusal and naming the project's own validator, instead of NOT A SHAPE ROOT, exit 2, with an adopt-project.py plan NEXT and a Triad row calling an assembly root a single repository (#205) #232 a conflictedproject.yamlisNOT A SHAPE ROOT) givescheckout --ours -- Makefile contracts/shape-pin.yamlandleft_for_a_person: ["project.yaml"], and its note namesproject.yaml. Run as printed, it keeps the rename and the README paragraph, and the row reads the pin. A person's merge ofproject.yaml(git merge-file --oursover git's three stages) and the merge commit keep the rename inHEAD:project.yaml, and the doctor then saysCOMPLIANT, exit 0.left_for_a_personnameREADME.md, and after it runs the paragraph is still there and the row reads the pin.hidden, names nocheckout, and its NEXT, run as printed, printsh contracts/shape-pin.yamlorS contracts/shape-pin.yaml.A: a bad pin staged after a commit deleted it. It givescommittedandcheckout <deleting commit>~1, and endsCOMPLIANT.DU: this branch deleted the pin, the other side changed it. It givescheckout --theirs, the note says only that side holds a pin, and the pin reads after the NEXT runs.noneand theupdate-shape.py checkNEXT, with nocheckout..git/indexis byte-identical afterwards. A plaingit statusthen rewrites it, as the control.Counts at head 8badae0:
tests/test_shape_doctor.py: 207 passed (181 on main 39ac59c);tests/test_cli_reference.pyandtests/test_repo_hygiene.py: 63 passed;tests/test_update_shape.py,test_update_shape_add.pyandtest_update_shape_add_ignored.py: 120 passed (update-shape.pychanged:check_pin);render-cli-reference.py --check: ok.Against older code:
assert 3 == 1, and the existing control passes.check_shape_currencytakes every refusalupdate-shape.py prepareraises over the ROOT's owncontracts/shape-pin.yaml(a merge conflict, acommit:that is not 40 hex, a deleted file) for this checkout failing to ask, so the verdict is CANNOT ANSWER, exit 3, outranking INVALID, and its NEXTgit -C <standard> fetch --allblames the standard for a fault in the repository #237 tests then, 16 fail, each atKeyError: 'restore'. That head's NEXT, run as printed, exits 2 with the same refusal in all nine uncommitted and both committed cases.README.mdin the argv.applymerge namesproject.yamlin the argv.~1.uncommitted.committed, a rollback of the good pin.Mutants, each in its own scratch tree:
environment: True, every refusal a finding, afetch --allNEXT, the remediation quoted, and the split removed (which is main).check_shape_currencytakes every refusalupdate-shape.py prepareraises over the ROOT's owncontracts/shape-pin.yaml(a merge conflict, acommit:that is not 40 hex, a deleted file) for this checkout failing to ask, so the verdict is CANNOT ANSWER, exit 3, outranking INVALID, and its NEXTgit -C <standard> fetch --allblames the standard for a fault in the repository #237 tests then: the old NEXT restored (15 tests),HEADin place of<last>~1(3), every conflicted path named (1), anduncommittedwithout asking whether HEAD holds a pin (1). A fifth, the status.strip()ed before its two letters are read, cannot be caught, because it is equivalent: every code whose first letter is a space strips to one that takes the same branch, as a run over all 99 letter pairs shows.--no-optional-locksdropped (the review's m7): the index test;README.mdor the manifest taken whole again (the review's m9, turned round): the README test, and the real two-applymerge;hiddenunreachable: the assume-unchanged and sparse tests;check_pin: 4 tests.CI:
tests,tests-macosandtests-windowswere SUCCESS at 092a5f9 and at 9f64bd0. The new tests run each NEXT throughpasted(), which reads it with PowerShell's tokens on Windows. CI must still confirm this head on macOS and Windows.Composes with
git merge-tree --write-treeof this head (8badae0) with each head below gives rc 0 and no conflicting file:origin/main34ecbc2, which carries shape-doctor.py: a project.yaml or family.yaml that is there and does not load is INVALID (manifest) or INVALID (family), exit 1, quoting load_yaml's own refusal and naming the project's own validator, instead of NOT A SHAPE ROOT, exit 2, with an adopt-project.py plan NEXT and a Triad row calling an assembly root a single repository (#205) #232 (d94d155) and shape-doctor.py: a validator row prints thePYTHONPATHit ran the standard's TEMPLATE copy under, in sh's or PowerShell's dialect, and runs the root's OWN copy under none, so themanifest,pinsandfamilyrows' NEXT, pasted anywhere, prints the validator's own finding and exits withdetail.exitinstead of dying onModuleNotFoundError: No module named 'repo_shape', and a root that lost its ownrepo_shape.pyis no longer told its gate passes (#238) #287 (ede15ce). Merged tree 2b6fbf6.tests/test_shape_doctor.pygives 268 passed and 3 skipped. The 3 skipped need a Windows host for PowerShell.update-shape.py applyruns leavesMakefile, the pin andproject.yamlallUU, and readsINVALID (manifest, manifest-kinds). The shape-currency row givesunmergedwithcheckout --ours -- Makefile contracts/shape-pin.yaml, and its note namesproject.yamlas a person's merge.INVALID (manifest)until a person mergesproject.yaml. Once that is done, it saysCOMPLIANT, exit 0.NOT A SHAPE ROOT(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), and the row never runs.origin/main87373a6, which main reached while this round ran. It carries shape-doctor.py: a validator's child gets exactly thePYTHONPATHits row's NEXT prints -- the standard'sscripts/alone for the template copy, none at all otherwise -- so an operator whosePYTHONPATHreaches arepo_shape.pyis no longer told a root that lost its ownscripts/repo_shape.pypasses its gate, and the template run no longer keeps the operator's directories behind the standard's (#306) #311 and tests/conftest.py:git()runs git under a default identity given as-cconfig, never as environment, so the seven tests that commit through it with none of their own no longer die withAuthor identity unknownwhere git cannot work one out, and no call site's own-c user.nameor exported identity is overridden (#279) #310 (tests/conftest.py'sgit()). Merged tree 443e329, and the 27 shape-doctor.pycheck_shape_currencytakes every refusalupdate-shape.py prepareraises over the ROOT's owncontracts/shape-pin.yaml(a merge conflict, acommit:that is not 40 hex, a deleted file) for this checkout failing to ask, so the verdict is CANNOT ANSWER, exit 3, outranking INVALID, and its NEXTgit -C <standard> fetch --allblames the standard for a fault in the repository #237 tests pass composed.PYTHONPATHits row's NEXT prints -- the standard'sscripts/alone for the template copy, none at all otherwise -- so an operator whosePYTHONPATHreaches arepo_shape.pyis no longer told a root that lost its ownscripts/repo_shape.pypasses its gate, and the template run no longer keeps the operator's directories behind the standard's (#306) #311 at b30420f (lane openRepoShape-4,shape-doctor.py), since landed on main as 87373a6. It changes the validator rows' code (Context,run_validator,validator_command,validator_row), not the shape-currency row.read_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 at 441fedc (this lane,update-shape.pyread_pin). It refuses afiles:that is not a list. Merged, that check sits insidecheck_pin, sopin_reads_atmakes it too.Left out
manifest kindsrow's NEXT for the same file (register the kind in MANIFEST_VALIDATORS ... or remove the file from the root). It tells the reader to remove an unreadable pin. It is that row's remedy, and shape-doctor.py: themanifest kindsrow's NEXT for a file that does not load (a conflicted or non-UTF-8contracts/shape-pin.yaml) is "register the kind in shape-doctor.py's MANIFEST_VALIDATORS ... or remove the file from the root", so the one fix the doctor prints for a damaged pin is to delete it #291 takes it.files:is a scalar (files: 5). It raises aTypeErrorinread_pin, in this doctor and invalidate-pins.py, as it does on main; 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 refuses it by name.pin_reads_atalready counts thatTypeErroras a pin that does not read.applywrites. The review offered it as an optional refinement, so a plain two-applymerge would stay one command. It is not taken. The plain merge now needs a person's merge ofproject.yamltoo, and shape-doctor.py: a project.yaml or family.yaml that is there and does not load is INVALID (manifest) or INVALID (family), exit 1, quoting load_yaml's own refusal and naming the project's own validator, instead of NOT A SHAPE ROOT, exit 2, with an adopt-project.py plan NEXT and a Triad row calling an assembly root a single repository (#205) #232'smanifestrow names that.unmerged). It is not taken: either side is one tool's output, and a side that was committed broken reachescommittedonce the merge is finished.checkout --oursand--theirsleave git's index unmerged, so the note says to finish the merge as git says and run the doctor again.restore: none), or a person must act first (hidden, a conflicted manifest or README), the note says so.tests/test_repo_hygiene.pycaps the README at 1416 lines, which it already has.update-shape.pybehaves as before.preparestill reads the pin itself, so on a good root the pin is read twice (one small YAML file).Fix round 1
This round answers lane openRepoShape-2's review of 092a5f9 (CHANGES NEEDED): #267 (comment)
pin_restore, above), and is no longer theupdate-shape.py checkthat refused it again.unloaded_manifest_row".;, an apostrophe or a backtick. Every NEXT test asserts it.AandDUchoices ofpin_restore, which the first tests did not reach.files: 5, and themanifest kindsNEXT) are left for separate issues.Fix round 2
This round answers lane openRepoShape-2's re-verify of 9f64bd0 (CHANGES NEEDED): #267 (comment)
unmergedNEXT takes whole only the files the shape writes whole. A conflictedproject.yamlorREADME.mdis left to a person, named in the note and indetail.left_for_a_person, so the other side's own edits to it survive. Tested with the issue's own two-applymerge and the README case.read_pin's own checks (check_pin, split out inupdate-shape.py).hiddenclass, and no rollback is named.uncommittedalso needs HEAD's pin to read (the review's optional step).--no-optional-locks: the doctor leaves a stat-dirty index byte for byte as it found it.pin_restore(ctx)still answers(restore, command). It gains an optionaldetail, which it writesleft_for_a_personinto. A call written against 9f64bd0, as shape-doctor.py: themanifest kindsrow's NEXT for a file that does not load (a conflicted or non-UTF-8contracts/shape-pin.yaml) is "register the kind in shape-doctor.py's MANIFEST_VALIDATORS ... or remove the file from the root", so the one fix the doctor prints for a damaged pin is to delete it #291 (stacked there) makes, still works.Review
Lane openRepoShape-2's own review of 092a5f9 and its re-verify of 9f64bd0 asked for changes, and the two fix rounds above answer them. A review by another lane is welcome. Lane openRepoShape-4's #311 sits in the same file.
Closes #237
🤖 Generated with Claude Code
Summary by Sourcery
Report unreadable root pins as repository-invalid findings and provide safe, state-aware commands to restore the last valid tool-written pin.
New Features:
Bug Fixes:
Enhancements:
Tests: