Skip to content

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

Merged
brettheap merged 8 commits into
mainfrom
fix/doctor-root-shape-pin-unreadable
Oct 12, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-2 (openRepoShape-2)
Claim: #237 (comment)

What

shape-doctor.py's check_shape_currency no longer treats a fault in the ROOT's own contracts/shape-pin.yaml as this checkout failing to ask. The row's NEXT is now the git command that puts back the last pin a tool wrote.

  • The pin is read first, on its own. The read uses update-shape.py's read_pin, the function prepare calls for it. A refusal from that read becomes the new unreadable_pin_row:
    • a FINDING with no environment;
    • the reason 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}, plus left_for_a_person where a merge left one of the project's files conflicted (below).
  • Refusals about this checkout are unchanged. A refusal from anything prepare does 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 give CANNOT ANSWER, exit 3, environment: True and the fetch --all NEXT. 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.
  • The NEXT comes from the pin's own git state. The new pin_restore reads that state without writing anything: git --no-optional-locks status --porcelain=v1 read as bytes, ls-files -u, rev-list, and cat-file blob read in memory. It records the case as detail.restore, and the first case that applies decides:
  • The verdict ladder is untouched. With no environment, it reaches the gates that are red:
    • INVALID (pins, manifest-kinds) for a conflicted pin;
    • INVALID (pins) for a commit: 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 its Remediation: trailer. For yaml-unparsable, that trailer is about submodules. --help is unchanged, so docs/cli.md is too (render-cli-reference.py --check passes). In update-shape.py, the only change is that read_pin's checks move into check_pin(pin, pin_path), which read_pin calls; its behaviour is unchanged.

Why

On main, each of these root-side faults answered CANNOT ANSWER, exit 3, which outranks INVALID, with git -C <standard> fetch --all as its NEXT:

  • a merge conflict in a root's contracts/shape-pin.yaml (the realistic merge of two shape updates);
  • a commit: that is not 40 hex;
  • a deleted pin;
  • a non-UTF-8 byte.

AGENTS.md says exit 3 is "THIS CHECKOUT failing to ask, never a statement about their repository". A reader therefore followed it to a fetch that succeeds and changes nothing.

Reproduced on main 39ac59c with the issue's own steps (scaffolded Atlas, two branches rewriting commit:, git merge):

  • the verdict is CANNOT ANSWER (shape currency: ... line 30: merge keys are not supported), exit 3;
  • stderr carries REFUSED shape-doctor-cannot-answer with the fetch --all remediation;
  • the --json detail 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) and update-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 update for 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. The pins row still names validate-pins.py wherever it is red.

Tests

tests/test_shape_doctor.py gains 26 tests. They sit beside test_a_pin_this_standard_cannot_resolve_is_exit_three, which is untouched and still passes.

  • Nine ways the root's pin can fail, in one parametrised test. The faults: a merge conflict, not YAML at all, a non-UTF-8 byte, a list not a mapping, commit: not 40 hex, another digest_definition, files: [], a row with no path, a deleted file. Each case asserts:
    • the refusal code, in detail.error and detail.refusal.code;
    • FINDING, exit 1, an INVALID ( verdict and no environment;
    • the REFUSED line quoted without its remediation;
    • detail.restore uncommitted, and a NEXT of git -C <root> checkout HEAD -- contracts/shape-pin.yaml, with no fetch, no update-shape.py and a plain note;
    • in the text report, no CANNOT ANSWER, and no shape-doctor-cannot-answer or fetch --all on stderr;
    • that the NEXT, run as printed, makes the doctor exit 0 with COMPLIANT. "As printed" means shlex on POSIX and PowerShell's tokens on Windows.
  • The conflicted pin's other rows. The pins row still names validate-pins.py, and the verdict is INVALID (pins, manifest-kinds).
  • digest_definition: other-v9. The shape-currency row is the one red row: INVALID (shape-currency).
  • The control pair. It uses one standard that lacks the scaffold's commit.
    • With a good pin: CANNOT ANSWER, exit 3, fetch --all, and detail.error update-revision-unresolvable with environment.
    • With a conflicted pin on the same root: INVALID, exit 1.
  • A family holder's conflicted pin. It is INVALID and uncommitted, and its NEXT, run as printed, makes the pin read again.
  • Two faults committed and followed by an unrelated commit. One is a git rm of the pin; the other is a commit: "zzzz". Each gives committed and checkout <breaking commit>~1 -- contracts/shape-pin.yaml, and ends COMPLIANT once that has run.
  • Two commits in a row that each broke the pin (commit: "zzzz", then commit: "yyyy"). The NEXT names the first break's ~1, and ends COMPLIANT.
  • A further uncommitted change over a committed break. HEAD's pin does not read either, so the NEXT is committed (the pin before the break), not checkout HEAD, and it ends COMPLIANT.
  • The issue's own merge. Makefile and a file of the project's own are UU beside the pin. The row gives unmerged and checkout --ours -- Makefile contracts/shape-pin.yaml, with no left_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 says update-revision-unresolvable.
  • The issue's merge with real re-pins. A copy of the standard gains two commits, and update-shape.py apply re-pins shape/a to the first and shape/b to the second. Shape/a also renames the project in project.yaml and adds a README paragraph; shape/a is merged into shape/b, leaving Makefile, the pin and project.yaml UU. 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 conflicted project.yaml is NOT A SHAPE ROOT) gives checkout --ours -- Makefile contracts/shape-pin.yaml and left_for_a_person: ["project.yaml"], and its note names project.yaml. Run as printed, it keeps the rename and the README paragraph, and the row reads the pin. A person's merge of project.yaml (git merge-file --ours over git's three stages) and the merge commit keep the rename in HEAD:project.yaml, and the doctor then says COMPLIANT, exit 0.
  • README.md conflicted over the project's own first line, with a paragraph the other side added that git merges cleanly. The NEXT takes the pin alone, the note and left_for_a_person name README.md, and after it runs the paragraph is still there and the row reads the pin.
  • Assume-unchanged over a broken copy, and a sparse checkout that leaves the pin out, each after a second good commit to the pin. Each gives hidden, names no checkout, and its NEXT, run as printed, prints h contracts/shape-pin.yaml or S contracts/shape-pin.yaml.
  • A : a bad pin staged after a commit deleted it. It gives committed and checkout <deleting commit>~1, and ends COMPLIANT.
  • DU: this branch deleted the pin, the other side changed it. It gives checkout --theirs, the note says only that side holds a pin, and the pin reads after the NEXT runs.
  • A squashed root commit holding a bad pin. It gives none and the update-shape.py check NEXT, with no checkout.
  • The doctor never writes the root's index. After a committed break, every tracked file is made stat-dirty. The doctor runs, and .git/index is byte-identical afterwards. A plain git status then rewrites it, as the control.

Counts at head 8badae0:

  • tests/test_shape_doctor.py: 207 passed (181 on main 39ac59c);
  • tests/test_cli_reference.py and tests/test_repo_hygiene.py: 63 passed;
  • tests/test_update_shape.py, test_update_shape_add.py and test_update_shape_add_ignored.py: 120 passed (update-shape.py changed: check_pin);
  • render-cli-reference.py --check: ok.

Against older code:

Mutants, each in its own scratch tree:

CI: tests, tests-macos and tests-windows were SUCCESS at 092a5f9 and at 9f64bd0. The new tests run each NEXT through pasted(), 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-tree of this head (8badae0) with each head below gives rc 0 and no conflicting file:

Left out

Fix round 1

This round answers lane openRepoShape-2's review of 092a5f9 (CHANGES NEEDED): #267 (comment)

Fix round 2

This round answers lane openRepoShape-2's re-verify of 9f64bd0 (CHANGES NEEDED): #267 (comment)

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:

  • Distinguish unreadable root pins from environment-level shape currency failures and report them as invalid findings with actionable recovery commands.
  • Determine safe pin restoration commands from the root repository's merge, working-tree, index, and commit history state.

Bug Fixes:

  • Prevent malformed, conflicted, deleted, hidden, or otherwise unreadable root pins from incorrectly producing CANNOT ANSWER with a fetch recommendation.
  • Avoid restoring a previously broken pin or overwriting project-owned edits during merge recovery.

Enhancements:

  • Share pin validation between on-disk reads and in-memory historical Git blobs.
  • Keep restoration diagnostics read-only, shell-safe, and explicit about files requiring manual merges.

Tests:

  • Add comprehensive coverage for malformed root pins, Git restoration states, merge conflicts, hidden files, historical recovery, shell-pastable commands, and index preservation.

brettheap and others added 2 commits October 11, 2026 22:40
…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>
@sourcery-ai

sourcery-ai Bot commented Oct 11, 2026

Copy link
Copy Markdown

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

You can request another review in 23 hours and 53 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-2, 2026-10-11T22:49:02Z) @codex review

@sourcery-ai

sourcery-ai Bot commented Oct 11, 2026

Copy link
Copy Markdown

Reviewer's Guide

The 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 handling

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

Flow diagram for shape pin refusal classification

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

File-Level Changes

Change Details Files
Classify failures while reading the root checkout's own shape pin as repository-invalid findings instead of environment-level inability to answer.
  • Read the root pin explicitly before invoking the broader update-shape preparation flow.
  • Convert any root-pin Refusal into a finding without environment metadata, preserving structured refusal details.
  • Keep downstream preparation refusals as CANNOT ANSWER with exit 3 and fetch guidance by separating the two paths based on which read raised.
  • Add a shared pin path and generate an update-shape check NEXT that identifies the faulty file while omitting fetch/remediation wording.
shape-doctor.py
Add regression coverage for root-pin refusal classification, output details, remediation guidance, verdict precedence, and control behavior.
  • Cover nine malformed, invalid, unsupported, and missing root-pin cases with assertions for exit 1, INVALID, FINDING, refusal details, and no environment/fetch messaging.
  • Verify existing validator rows and verdict combinations remain correct, including the catch-all shape-currency verdict.
  • Retain coverage proving standard-side resolution failures remain CANNOT ANSWER with exit 3 and fetch guidance.
  • Cover family roots and distinguish root-pin failures from an otherwise identical standard-side failure.
tests/test_shape_doctor.py

Assessment against linked issues

Issue Objective Addressed Explanation
#237 Classify refusals caused by the root's own contracts/shape-pin.yaml as an INVALID finding rather than CANNOT ANSWER, without marking them as environment failures. ✅
#237 Continue classifying refusals caused by the standard or an unresolvable pinned revision as CANNOT ANSWER with exit 3 and the fetch --all remediation. ✅
#237 Provide an appropriate remediation and regression coverage for root-pin failures, including malformed, invalid, missing, conflicted, and non-UTF-8 pins, while preserving the existing validator and control-case behavior. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread shape-doctor.py Outdated
Comment on lines +1131 to +1133
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 "

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-12T12:38:12.749007Z 8badae0 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@brettheap

Copy link
Copy Markdown
Contributor Author

(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 (shape-doctor.py:1131-1138). update-shape.py check calls read_pin again, so in all 15 probed faults it exits 2 with the same refusal. The remediation it then prints is wrong for the case:

  • git submodule update --init --recursive for six codes, the trailer the row's own docstring (:1105-1112) strips as misleading;
  • "adopted with adopt-project.py" for a deleted pin (update-shape.py:381-383);
  • "re-save the file" (a hand edit) for a non-UTF-8 byte.

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:

  • checkout HEAD -- for an uncommitted change (11 fixtures);
  • checkout <last>~1 -- for a committed one (4);
  • checkout --ours/--theirs for a merge, which on the realistic two-apply merge gives COMPLIANT, SHAPE BEHIND.

The fix round picks the command from git status --porcelain on the pin.

Nits.

Follow-ups, pre-existing.

  • files: 5 crashes the doctor, update-shape.py and validate-pins.py with a TypeError (update-shape.py:404).
  • The manifest kinds row tells the reader to remove an unreadable pin (:997-999).

Also checked.

  • No legitimate root lacks a pin: the root's own validator refuses one without it since v0.
  • read_pin and prepare cannot disagree on a refusal.
  • Windows quoting holds.
  • The 13 new tests fail on main at assert 3 == 1, and 3 mutants are killed.

Tests. Head: test_shape_doctor.py, test_cli_reference.py and test_repo_hygiene.py, 257 passed. Merged with main c3f4987: test_shape_doctor.py, 204 passed. render-cli-reference.py --check ok.

Merges. merge-tree with main, #232 b047658, 88c81e0, 2f5cda9, #245 1abe078, #264, #266, #268, #269 and #270: all clean.

Main reached 67a398a mid-review; none of its five new commits touches these files.

The full report is handoffs/xFactory/attachments/lane-openRepoShape-2/review267-092a5f9/final-report.md in opensoft/brett-wip.

…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>
@brettheap brettheap changed the title 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 update-shape.py check as its NEXT, instead of CANNOT ANSWER, exit 3, sending the reader to git fetch --all in the standard (#237) 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) Oct 12, 2026
@brettheap

Copy link
Copy Markdown
Contributor Author

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

  • V1 (P2), shape-doctor.py:1147: the merge NEXT takes project.yaml and README.md whole. Composed with main, the issue's own two-apply merge loses the other branch's name: edit, which git had merged cleanly. The doctor then says COMPLIANT, and once the merge is committed git merge calls that branch merged. At 9f64bd0 alone, a README conflicted only over project lines is also taken whole. This lane's first-pass fix list asked for both files. Fix: name only the files the shape writes whole, and leave the manifest and README to a person, as the manifest row already does.
  • V2 (P3), shape-doctor.py:1181-1188: the committed NEXT never reads the pins it trusts. A pin hidden by a sparse checkout or assume-unchanged rolls back a good re-pin (DRIFTED). Two successive broken commits cycle between checkout <last>~1 and checkout HEAD.
  • V3 (nit): no test pins --no-optional-locks. Mutant m7 survives, and a scratch test kills it. m6 is equivalent. m9 is V1's fix direction.

Tests: head, 263 passed (doctor, cli-reference, hygiene). Head merged into main c7c02fa: tests/test_shape_doctor.py 261 passed, 3 skipped (Windows-only). With #311: the 20 #237 tests passed.

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 handoffs/xFactory/attachments/lane-openRepoShape-2/review267-092a5f9/reverify-1.md in opensoft/brett-wip.

brettheap and others added 2 commits October 12, 2026 11:58
…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>
brettheap added a commit that referenced this pull request Oct 12, 2026
… 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>
@brettheap

Copy link
Copy Markdown
Contributor Author

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

  • V1 (P2), fixed in f80179c. The unmerged NEXT takes whole only the pin and the copies. A conflicted project.yaml or README.md is named in the note and in detail.left_for_a_person, so the other side's edits survive.
  • V2 (P3), fixed in f80179c. Every pin the NEXT puts back is read first, with read_pin's checks split out as check_pin.
    • A sparse checkout or assume-unchanged over a good pin is a new hidden class.
    • Two breaking commits in a row are reached past.
    • uncommitted needs HEAD's pin to read.
  • V3 (nit), fixed in 8badae0. A test pins --no-optional-locks.

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)

brettheap added a commit that referenced this pull request Oct 12, 2026
…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>
brettheap added a commit that referenced this pull request Oct 12, 2026
…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>
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

An unknown error occurred
ℹ️ 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".

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-2, 2026-10-12T12:49Z)

Composition note: #343 and this PR conflict semantically, though git merge-tree is rc 0. #343 (039530b) makes update-pin-row's remediation pin_rows_remediation(root), so it prints git -C <root> log. This PR moves those raises into check_pin(pin, pin_path), which has no root. So the merged tree dies with NameError: name 'root' is not defined. #343's test file then fails 14 of 22.

Whichever of #267 and #343 lands second applies three lines:

  • check_pin takes root;
  • read_pin passes root;
  • pin_reads_at passes ctx.root.

On a scratch merge, that gives 22 passed in #343's file and 12 in this PR's doctor tests. The patch is tools/fix1-compose-267-resolution.patch in lane 2's attachments (impl290-pin-files-list), and #343's body, under Composes with, says the same.

Lane: openreposhape-2 (openRepoShape-2)

@sonarqubecloud

Copy link
Copy Markdown

brettheap added a commit that referenced this pull request Oct 12, 2026
…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>
@brettheap
brettheap merged commit decff87 into main Oct 12, 2026
5 checks passed
@brettheap
brettheap deleted the fix/doctor-root-shape-pin-unreadable branch October 12, 2026 19:44
@brettheap

Copy link
Copy Markdown
Contributor Author

(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 v1.0.0 milestone. Nothing changes in how it lands. Known-limit issues stay open, without the milestone, under their area's parent.

brettheap pushed a commit that referenced this pull request Oct 12, 2026
#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>
brettheap pushed a commit that referenced this pull request Oct 12, 2026
…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>
brettheap pushed a commit that referenced this pull request Oct 12, 2026
…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>
brettheap pushed a commit that referenced this pull request Oct 12, 2026
…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>
brettheap pushed a commit that referenced this pull request Oct 12, 2026
…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>
brettheap pushed a commit that referenced this pull request Oct 12, 2026
…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>
brettheap pushed a commit that referenced this pull request Oct 12, 2026
…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>
brettheap added a commit that referenced this pull request Oct 12, 2026
…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>
brettheap pushed a commit that referenced this pull request Oct 13, 2026
#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>
brettheap pushed a commit that referenced this pull request Oct 13, 2026
…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>
brettheap pushed a commit that referenced this pull request Oct 13, 2026
'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>
brettheap added a commit that referenced this pull request Oct 13, 2026
…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>
brettheap pushed a commit that referenced this pull request Oct 13, 2026
… 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>
brettheap added a commit that referenced this pull request Oct 13, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant