Skip to content

shape-doctor.py: manifest kinds prints one command for its NEXT, the git rm, git clean or git status its files need as git has them, where it printed prose joined by ; whose backticks ran family.py in zsh, and counts a manifest validated only at a path its gate reads, so a contracts/<x>-pin.yaml no leg or product names is a FINDING (#548, #439) - #555

Merged
brettheap merged 5 commits into
mainfrom
l3-548-manifest-kinds
Oct 15, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-3 (openRepoShape-3)
Claim: #548 (comment) (#548), #439 (comment) (#439)

What

Why

Brett Heap's ruling 11 (2026-10-12): "#439 (registered kind at a path no check reads): FINDING". The ruling comment on #439 adds: "The verdict is INVALID, exit 1, and the NEXT takes the file out as git holds it." The rule across the doctor is that a NEXT is one pasteable command or nothing, and prose belongs in the row's why (#277). manifest kinds was the row #277 left out.

The defect: pasting main's #211 NEXT into zsh runs the commands in its backticks, measured with a logging family.py first on PATH:

$ zsh -f -c "<main's NEXT>"
zsh:1: no matches found: (, then  there),
ran family.py init
ran family.py add
ran family.py add

The same happens in zsh -f -i. The new test test_the_family_beside_next_pasted_into_zsh_runs_no_family_py pastes both lines through a pty.

Measured on main 5ed3e5d (before) and head (after). The roots were scaffolded into local bare remotes, and the holder was made with family.py init and add. Everything ran offline: a gh stub that exits 97, GH_TOKEN=bogus, an empty GH_CONFIG_DIR and an empty global git config. Each file was committed by itself unless marked otherwise. Root paths are shortened to their names.

Untouched roots. Both runs print ok manifest kinds 5 manifest(s), every declared kind has a registered validator (holder: 3 manifest(s)), COMPLIANT, exit 0. The status, reason and detail in --json compare byte-identical (cmp) for the assembly root, the holder, and the #327 contracts/family.yaml root.

#439's five files. Main says ok, 6 (holder 4) manifests, COMPLIANT exit 0 for each. Head:

p-contracts-project (exit 1)
  FINDING  manifest kinds   no gate here reads contracts/project.yaml (project-manifest, which a gate here reads only at project.yaml)
           NEXT             git -C p-contracts-project rm -- contracts/project.yaml
           WHY              takes contracts/project.yaml out of this root: commit that on a branch and land it as a pull request. Never rename it onto a path a gate here reads
p-release-pin (exit 1)
  FINDING  manifest kinds   no gate here reads contracts/release-pin.yaml (pinned_contract_manifest, which a gate here reads only at contracts/code-pin.yaml, contracts/shape-pin.yaml, contracts/spec-pin.yaml)
           NEXT             git -C p-release-pin rm -- contracts/release-pin.yaml
           WHY              takes contracts/release-pin.yaml out of this root: commit that on a branch and land it as a pull request. Or, if contracts/release-pin.yaml pins a product this root descends from, the human names that product under `neutral_product_pins:` in project.yaml -- and nobody declares a pin the human has not asked for. Never rename it onto a path a gate here reads
p-naming-old (exit 1)
  FINDING  manifest kinds   no gate here reads contracts/naming-old.yaml (repository-naming-policy, which a gate here reads only at contracts/repository-naming.yaml)
           NEXT             git -C p-naming-old rm -- contracts/naming-old.yaml
h-contracts-family (exit 1)
  FINDING  manifest kinds  no gate here reads contracts/family.yaml (family-manifest, which a gate here reads only at family.yaml)
           NEXT            git -C h-contracts-family rm -- contracts/family.yaml
h-spec-pin (exit 1)
  FINDING  manifest kinds  no gate here reads contracts/spec-pin.yaml (pinned_contract_manifest, which a gate here reads only at contracts/shape-pin.yaml)
           NEXT            git -C h-spec-pin rm -- contracts/spec-pin.yaml

Each verdict is INVALID (manifest-kinds), exit 1. Run as printed and committed, each root is COMPLIANT again (tested).

#548's rows. Main:

p-unregistered (contracts/inventory.yaml, kind service-inventory, untracked)
           NEXT             register the kind in shape-doctor.py's MANIFEST_VALIDATORS, beside the check that validates it, or remove the file from the root
p-beside (family.yaml beside project.yaml, committed)
           NEXT             an assembly root is not a family holder: take family.yaml out of this root, or move the membership to a holder of its own (`family.py init`, then `family.py add` there), as the human decides -- never `family.py add` here, which would mount the members inside a project
p-beside-unregistered (both)
           NEXT             register the kind in shape-doctor.py's MANIFEST_VALIDATORS, beside the check that validates it, or remove the file from the root; an assembly root is not a family holder: take family.yaml out of this root, or move the membership to a holder of its own (`family.py init`, then `family.py add` there), as the human decides -- never `family.py add` here, which would mount the members inside a project
p-contracts-family (#327, committed)
           NEXT             take contracts/family.yaml out of this root (`git -C p-contracts-family rm -- contracts/family.yaml`, committed on a branch and landed as a pull request), or move what it describes to a family holder of its own (`family.py init`, then `family.py add` there), as the human decides -- never rename it to project.yaml or family.yaml in this root: a family holder is not a project, and an assembly root is not a family holder

Head, as of fix round 2 (the reasons are unchanged):

p-unregistered
           NEXT             git -C p-unregistered status --ignored -- contracts/inventory.yaml
           WHY              shows contracts/inventory.yaml, which git does not track: no commit holds it, so deleting it cannot be undone and is a person's act, never this row's, with nothing to commit after. A person deletes it with: git -C p-unregistered clean -f -- contracts/inventory.yaml. Or register the kind contracts/inventory.yaml declares in shape-doctor.py's MANIFEST_VALIDATORS, beside the check that validates it, as the human decides
p-beside
           NEXT             git -C p-beside rm -- family.yaml
           WHY              takes family.yaml out of this root: commit that on a branch and land it as a pull request. Or move the membership family.yaml records to a holder of its own (`family.py init`, then `family.py add` there), as the human decides -- never `family.py add` here, which would mount the members inside a project
p-beside-unregistered
           NEXT             git -C p-beside-unregistered rm -- family.yaml
           WHY              takes family.yaml out of this root: commit that on a branch and land it as a pull request. The next run names the way out for contracts/inventory.yaml. Or register the kind contracts/inventory.yaml declares in shape-doctor.py's MANIFEST_VALIDATORS, beside the check that validates it, as the human decides. Or move the membership family.yaml records to a holder of its own (`family.py init`, then `family.py add` there), as the human decides -- never `family.py add` here, which would mount the members inside a project
p-contracts-family
           NEXT             git -C p-contracts-family rm -- contracts/family.yaml
           WHY              takes contracts/family.yaml out of this root: commit that on a branch and land it as a pull request. Or move what contracts/family.yaml describes to a family holder of its own (`family.py init`, then `family.py add` there), as the human decides -- never rename it to project.yaml or family.yaml in this root: a family holder is not a project, and an assembly root is not a family holder

#408's path. Measured in process with a check patched to raise Refusal("probe-refused", ..., REMEDIATION) and PathPolicy.load patched to raise the same:

  • main: both rows' "next" is Remediation: run `git submodule update --init --recursive`, then `python3 scripts/validate-pins.py`. and "why" is null;
  • head: "next" is null and "why" is run `git submodule update --init --recursive`, then `python3 scripts/validate-pins.py`.

Tests

The guard. tests/test_shape_doctor.py::rows_of now asks assert_next_holds_no_prose of every row a --json run returns. The check is no backtick outside a single-quoted value, and no Remediation:. Every row the suite reads, from any builder, is therefore a case. This is the "property over the rows the suite builds" #408 asked for: a remedy arrives at run time, from another module, where no scan of the doctor's strings can see it. test_the_rows_guard_finds_a_backtick_or_a_remediation_in_a_next shows it failing on main's #211 NEXT, on REMEDIATION and on a bare Remediation: line, and passing a quoted path that holds a backtick.

New tests:

Updated tests. Every #211/#327 test now asserts the bare command and the why. The untracked and ignored cases run the NEXT (git status --ignored; the file stays), then the git clean -f the why names (-x for ignored), to COMPLIANT. The edited and staged cases run the git status, then the git rm it leads to. The #211-plus-unregistered case runs both NEXTs in turn, then the why's git clean, to COMPLIANT. The linked-record test, and tests/test_update_shape_merged_away.py's (fix round 1), assert no NEXT and the sentence in why.

Runs. Fix round 2, head f112441, the same 13 files on Linux with the git-filter-repo wrapper and the gh stub: 848 passed, 3 skipped in 562.19s, exit 0, the same 3 Windows-only skips. Fix round 1, head abbce43, Linux, with a git-filter-repo 2.47.0 wrapper first on PATH as well, so the files that skip without it run: the same 13 files, 840 passed, 3 skipped in 630.97s, exit 0. The 3 skips are the PowerShell tests that need a Windows host. The first round's runs follow.

First round, head 557a259, Linux, /usr/bin/python3 -m pytest <files> -q --basetemp=… -o tmp_path_retention_policy=failed -p no:cacheprovider, with the gh stub above:

  • the 13 files tests/test_shape_doctor.py, test_doctor_next_is_a_command.py, test_doctor_gitlink_repairs.py, test_doctor_legs_members_next.py, test_doctor_unfinished_merge.py, test_doctor_validator_rows.py, test_family.py, test_shape_pin_files_not_a_list.py, test_update_shape_merged_away.py, test_refusal_default_remediation.py, test_repo_hygiene.py, test_agents_md_codes.py and test_cli_reference.py (every file that reads rows through rows_of, plus the guards): 822 passed, 17 skipped in 523.00s, exit 0. The skips are existing ones; none of the new tests skips on Linux.
  • After one Windows-only fix to a test helper (--json spells the root as_posix()), the 14 tests that use it: 14 passed.

Windows and macOS are CI's to show. The case-folding branch and os.link run there for the first time.

Fix round 1 (independent review of 557a259)

One commit on top, abbce43.

  • SHOULD-FIX-1, CI red. tests/test_update_shape_merged_away.py:342 still asserted the linked-record shape currency NEXT as prose. That file skips without git-filter-repo, which is why the first round's run missed it. It now asserts no NEXT and the sentence in why. Every file that both reads doctor rows and skips without git-filter-repo (test_shape_doctor.py and test_update_shape_merged_away.py) ran with git-filter-repo 2.47.0 on PATH. Before the fix, that file gave 3 failed, 7 passed; after it, 10 passed.

  • SHOULD-FIX-2, unread took out a file a gate reads. read_by_a_gate(ctx) is every root path a gate that runs here reads, as any kind. A registered kind at one of those paths is never unread. own_pins are the pins the shape itself writes: the shape pin, plus contracts/spec-pin.yaml and contracts/code-pin.yaml in an assembly root, whatever the legs declare. pins_read_here counts them as read. Measured on a scaffolded root with one committed edit (manifest row unchanged throughout):

    - role: specs                                        manifest row: FINDING manifest-roles
      main 5ed3e5de  ok       manifest kinds   5 manifest(s), every declared kind has a registered validator
      557a2595       FINDING  manifest kinds   no gate here reads contracts/spec-pin.yaml (pinned_contract_manifest, which a gate here reads only at contracts/code-pin.yaml, contracts/shape-pin.yaml, contracts/specs-pin.yaml)
                     NEXT             git -C p-role-typo rm -- contracts/spec-pin.yaml
      abbce435       ok       manifest kinds   5 manifest(s), every declared kind has a registered validator
    
    contracts/repository-naming.yaml: kind: pinned_contract_manifest      manifest row: REFUSED policy-wrong-kind
      main 5ed3e5de  ok       manifest kinds   5 manifest(s), every declared kind has a registered validator
      557a2595       FINDING  manifest kinds   no gate here reads contracts/repository-naming.yaml (pinned_contract_manifest, which a gate here reads only at contracts/code-pin.yaml, contracts/shape-pin.yaml, contracts/spec-pin.yaml)
                     NEXT             git -C p-naming-wrong-kind rm -- contracts/repository-naming.yaml
      abbce435       ok       manifest kinds   5 manifest(s), every declared kind has a registered validator
    

    One step past the review. An unregistered kind, or one whose check runs elsewhere (shape-doctor.py: manifest kinds counts a contracts/*.yaml that declares a registered kind as validated though that kind's validator never runs on this kind of root, so a contracts/family.yaml in an assembly root, or a contracts/project.yaml in a family holder, is ok and COMPLIANT where the same bytes at the root are a FINDING #327), sitting at a gated path is still this row's finding, as on main. But taking_out never puts that file in the NEXT, and the why says the gate's own row judges it. Otherwise a kind: service-inventory in the naming policy, or kind: family-manifest in the shape pin, would get a NEXT of git rm on the file the gate reads. On main that git rm was prose; shape-doctor.py: no NEXT holds prose or a backtick (manifest kinds joins prose NEXTs with "; ", and zsh runs FAMILY_BESIDE_FIX's backticks) #548 made it runnable. (Fix round 2 replaces that why. No gate checks the shape pin's kind, so such a file now gets a read-only NEXT, and the row names its real judge.)

  • SHOULD-FIX-3, no irreversible deletion in a NEXT. For untracked or ignored files the NEXT is now git -C <root> status --ignored -- <files>. The why names the git clean -f (-x where ignored) for a person to run, and gives the alternative. Committed files keep git rm; changed files keep git status. No test expects git clean in a NEXT, and AGENTS.md is untouched.

  • nit-4. The pathspec test now goes through the row with contracts/*-pin.yaml (POSIX; Windows takes no * in a name) and contracts/[s]hape-pin.yaml. git ls-files shows each pattern matching more than one file. The row's NEXT is git -C <root> --literal-pathspecs rm -- '<name>', and run as printed it stages that file alone.

  • nit-5. The guard reads a NEXT with outside_single_quotes, moved into tests/test_shape_doctor.py beside rows_of and imported back by tests/test_doctor_next_is_a_command.py; importing it the other way would be circular. The guard test now passes git -C '/w/it'\''s `here`' status, which the regular expression had misread.

  • nit-6. The shape-doctor.py calls an assembly root whose project.yaml does 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 paragraph of check_manifest_kinds no longer describes main's NEXT as current.

Fix round 2 (re-review of abbce43)

One commit on top, f112441.

  • SHOULD-FIX, a wrong kind: in a pin was a dead end. Round 1 kept a file a gate reads out of every NEXT and said "its own row judges it". For the shape pin and a declared neutral product pin no row does: validate-pins.py's _check_shape_pin_header reads their fields, never their kind. Now:

    • kinds_read_here(ctx) maps each path a gate here reads to the kind it reads it as. kind_checked_by(ctx) names the paths whose gate refuses another kind, and that gate's row: the naming policy (manifest; family in a holder, both via NamingPolicy.load, policy-wrong-kind) and each declared leg's pin (pins, pin-wrong-kind). Nothing else, and nothing while the root's own manifest does not load.
    • A file at such a path declaring another kind gets a read-only NEXT (showing_the_kind): git -C <root> diff HEAD -- <file> for a change no commit holds, else git -C <root> log -p -1 -- <file>.
    • Its why says what that shows, then the declared kind, the kind a gate here reads the path as, and who judges it. "The row's gate finds that too" is said only where kind_checked_by says so; otherwise it is "no gate here checks the kind it declares, so this row is the one that says so". It ends "put its kind line back to ".
    • No register, move-to-a-holder or rename way is offered for such a file. Files beside it that the row would take out are named for the next run.

    Measured on a scaffolded root, one edit each, committed unless marked. The only other FINDING in any of them is the pins row in E3:

    E1  contracts/shape-pin.yaml  kind: pinned_contract_manifes
      abbce435  FINDING  manifest kinds   no validator registered for kind contracts/shape-pin.yaml (pinned_contract_manifes)
                         (no NEXT)
                         WHY  contracts/shape-pin.yaml is read by a gate here, whose own row judges it: this row never takes out a file a gate reads. Or register the kind contracts/shape-pin.yaml declares in shape-doctor.py's MANIFEST_VALIDATORS, beside the check that validates it, as the human decides
      head      FINDING  manifest kinds   (reason unchanged)
                         NEXT git -C p-e1 log -p -1 -- contracts/shape-pin.yaml
                         WHY  shows the last commit that changed contracts/shape-pin.yaml. contracts/shape-pin.yaml declares kind pinned_contract_manifes where a gate here reads it as pinned_contract_manifest, and no gate here checks the kind it declares, so this row is the one that says so: put its kind line back to pinned_contract_manifest
    E1, not committed
      abbce435  as E1
      head      NEXT git -C p-e1u diff HEAD -- contracts/shape-pin.yaml
                WHY  shows the change to contracts/shape-pin.yaml that no commit holds. contracts/shape-pin.yaml declares kind pinned_contract_manifes where ... (as E1)
    E2  contracts/shape-pin.yaml  kind: family-manifest
      abbce435  FINDING  manifest kinds   no validator that runs in an assembly root is registered for kind contracts/shape-pin.yaml (family-manifest: scripts/validate-family.py runs only in a family holder)
                         (no NEXT)
                         WHY  contracts/shape-pin.yaml is read by a gate here, whose own row judges it: this row never takes out a file a gate reads. Or move what contracts/shape-pin.yaml describes to a family holder of its own (`family.py init`, then `family.py add` there), as the human decides -- never rename it ...
      head      FINDING  manifest kinds   (reason unchanged)
                         NEXT git -C p-e2 log -p -1 -- contracts/shape-pin.yaml
                         WHY  shows the last commit that changed contracts/shape-pin.yaml. contracts/shape-pin.yaml declares kind family-manifest where a gate here reads it as pinned_contract_manifest, and no gate here checks the kind it declares, so this row is the one that says so: put its kind line back to pinned_contract_manifest
    E3  contracts/spec-pin.yaml  kind: pinned_contract_manifes      pins row: FINDING pin-wrong-kind (both)
      abbce435  FINDING  manifest kinds   no validator registered for kind contracts/spec-pin.yaml (pinned_contract_manifes)
                         (no NEXT)
                         WHY  contracts/spec-pin.yaml is read by a gate here, whose own row judges it: ... Or register the kind contracts/spec-pin.yaml declares in shape-doctor.py's MANIFEST_VALIDATORS, ...
      head      FINDING  manifest kinds   (reason unchanged)
                         NEXT git -C p-e3 log -p -1 -- contracts/spec-pin.yaml
                         WHY  shows the last commit that changed contracts/spec-pin.yaml. contracts/spec-pin.yaml declares kind pinned_contract_manifes where a gate here reads it as pinned_contract_manifest, and the pins row's gate finds that too: put its kind line back to pinned_contract_manifest
    E4  contracts/shape-pin.yaml  kind: repository-naming-policy
      abbce435  ok       manifest kinds   5 manifest(s), every declared kind has a registered validator      COMPLIANT, exit 0
      head      FINDING  manifest kinds   contracts/shape-pin.yaml declares kind repository-naming-policy where a gate here reads it as pinned_contract_manifest, and no gate here checks the kind it declares
                         NEXT git -C p-e4 log -p -1 -- contracts/shape-pin.yaml
                         WHY  shows the last commit that changed contracts/shape-pin.yaml. contracts/shape-pin.yaml declares kind repository-naming-policy where ... so this row is the one that says so: put its kind line back to pinned_contract_manifest
    

    Head's verdicts: E1, E2 and E4 INVALID (manifest-kinds), E3 INVALID (pins, manifest-kinds), exit 1 each.

    One step past the review: E4. A registered kind whose check runs here, at a gated path read as another kind that no gate checks, was validated, COMPLIANT. It is now this row's FINDING, listed under a new detail.misdeclared (manifest, kind, read_as, git), which is present only when there is one. Where a gate does check the kind, the file stays validated and that gate's row is the finding, as round 1 measured: the naming policy declaring pinned_contract_manifest still reads ok manifest kinds beside the manifest row's policy-wrong-kind.

  • NIT E3. The leg pin of the wrong kind no longer gets "Or register the kind"; covered by the E3 test.

  • NIT, the untracked why. The deleting command now follows a colon, with no backticks a pasted sentence would run: "... never this row's, with nothing to commit after. A person deletes it with: git -C clean -f -- ". The changed group's "which git rm will not discard" lost its backticks too.

Tests:

  • tests/test_shape_doctor.py::test_a_wrong_kind_in_the_shape_pin_is_shown_never_a_dead_end covers E1 committed, E1 not committed, E2 and E4, through the whole doctor. It checks the reason, the detail keys, the NEXT and the whole why. It runs the NEXT, which changes nothing and prints the +kind: line. With the line put back, the root is COMPLIANT.
  • test_a_leg_pin_of_the_wrong_kind_is_offered_no_registration covers E3.
  • test_a_declared_neutral_pin_of_the_wrong_kind_is_this_rows_to_say runs in process, because the pins row would ask gh.
  • run_read_only accepts a log NEXT.
  • The round-1 naming-policy test now expects the diff HEAD NEXT and the manifest row as the judge.
  • tests/test_doctor_next_is_a_command.py adds taking_out-committed and taking_out-changed to CASES, and the untracked entry opens with the new sentence.

Composes with

Trial merges (git merge-tree --write-tree --name-only) of f112441 (fix round 2; abbce43 and 557a259 gave the same) against the open branches touching these files:

branch PR result
main (b8b7062) CLEAN
l3-549-doctor-machine-row (9362501) #552 CLEAN
l3-545-doctor-way-in (e64f47e) #553 CONFLICT tests/test_doctor_next_is_a_command.py

The #553 conflict is add/add at the same two anchors: both PRs append fixture builders after _way_in and entries at the end of CASES. Keeping both sides resolves it. Whichever lands second does that.

Left out

Closes #548. Closes #439.

Lands after the v1.0.0 tag (v1.1.0).

🤖 Generated with Claude Code

Summary by Sourcery

Make shape-doctor manifest findings produce safe, actionable commands while validating manifests against the paths their gates actually read.

New Features:

  • Make manifest kinds identify registered manifests placed at paths that no applicable gate reads as findings.
  • Generate a single actionable, git-aware NEXT command for manifest findings, including safe handling of committed, changed, untracked, and ignored files.
  • Show read-only git history or diffs when a gate-read manifest declares the wrong kind.

Bug Fixes:

  • Prevent prose, remediation text, and embedded commands from appearing in NEXT fields where pasting them could execute unintended commands.
  • Prevent the doctor from suggesting removal of files that an applicable gate actually reads.
  • Pass pattern-like filenames to git as literal pathspecs.
  • Move refusal remedies and linked-record repair guidance from NEXT into WHY.

Enhancements:

  • Preserve existing validation behavior for files gates read, including hard links, case-folded names, declared neutral pins, and roots whose own manifest cannot be loaded.

Tests:

  • Add coverage for command-only NEXT values, zsh paste safety, git-state-specific remediation, path-aware manifest validation, wrong-kind manifests, literal pathspec handling, and refusal remedies.

…e `git rm`, `git clean` or `git status` its files need as git has them, where it printed prose joined by `; ` whose backticks ran `family.py` in zsh, and counts a manifest `validated` only at a path its gate reads, so a `contracts/<x>-pin.yaml` no leg or product names is a FINDING (#548, #439)

#548. The rule across the doctor is that a NEXT is one pasteable command
or nothing, and prose is the row's `why` (#277). `manifest kinds` broke it:
each fault's sentence was the NEXT, joined by "; ", and #211's
FAMILY_BESIDE_FIX held `family.py init` and `family.py add` in backticks,
which zsh, interactive or `-c`, runs where the line is pasted. Now every
fault but a file that does not load is a file to take out of the root, so
the NEXT is the one command `removal_next` names for them as git has them:
`git rm` (committed), `git clean -f` (`-x` where one is ignored), or the
`git status` that shows changes `git rm` will not discard. The `why` says
what it does, names files the next run handles, and gives each fault's
other way out as its own sentence (register the kind, move what it
describes to its home, move the membership to a holder, declare the pin),
as the human decides. A name git would read as a pathspec pattern is passed
after `--literal-pathspecs`: `git rm -- 'contracts/shape-pin[.]yaml'` took
the shape pin out with it.

A refusal's remedy reaches no NEXT either (#408): `run_checks`, for a check
that lets a `Refusal` out, and `placement_policy`, for a standard whose path
policy does not load, print no NEXT and put the remedy, less its
`Remediation:` label, in the `why`. The #286 linked-record row's NEXT was the
one other prose NEXT left in the doctor; it is now that row's `why`.

#439. Brett Heap ruled on 2026-10-12: "#439 (registered kind at a path no
check reads): FINDING". `ManifestKind.read_at` names where the check that
runs on this root reads each kind: `project.yaml`, `family.yaml`,
`contracts/repository-naming.yaml`, and for pins the shape pin plus, in an
assembly root, `declared_pins` (a pin per leg and per
`neutral_product_pins:` entry). A file of a kind whose check runs here, at
any other path, is listed under `unread` with the path, the kind and where
that kind is read, and is a FINDING whose NEXT takes it out as git has it.
Only a pin in an assembly root is offered the human declaring it, and the
row never declares one. A file is read by any name it has (`samefile`), so
a case-folded or hard-linked leg pin stays `validated`; while `project.yaml`
does not load, no pin is called unread. `path-classification-policy` is
not decided by path, as before. An untouched root's row is byte for byte
what it was.

Tests: `rows_of` holds every row the suite reads to "no backtick outside a
quoted value, no `Remediation:`" (the guard #408 asked for), shown to fail
on the lines main printed; the #277 property covers five new manifest kinds
fixtures; a zsh paste shows the old #211 line running `family.py` and the
new NEXT running none; #439's five files each run to COMPLIANT by their
NEXT; refusal rows from `run_checks` and `placement_policy` carry no NEXT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 14, 2026

Copy link
Copy Markdown

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

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

@sourcery-ai

sourcery-ai Bot commented Oct 14, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR makes doctor remediation machine-safe and actionable by ensuring every next is one executable command or absent, while adding path-aware manifest validation that flags registered kinds no active gate reads and comprehensively testing Git-state repairs, refusal handling, aliases, and compatibility.

Sequence diagram for path-aware manifest validation

sequenceDiagram
    participant Doctor
    participant Registry
    participant Gate
    participant Git
    Doctor->>Registry: ManifestKind.reads(ctx)
    Registry-->>Doctor: gate-readable paths
    Doctor->>Gate: Check manifest at declared path
    Gate-->>Doctor: validated or not read
    alt Same file as a readable path
        Doctor->>Doctor: read_here via samefile
        Doctor-->>Doctor: validated
    else Gate does not read path
        Doctor->>Git: git_states(path)
        Git-->>Doctor: committed, untracked, ignored, or changed
        Doctor-->>Doctor: FINDING with removal command
    end
Loading

Flow diagram for machine-safe doctor remediation

flowchart TD
    A[Doctor detects finding] --> B{File can be removed?}
    B -->|Yes| C{Git state}
    C -->|Committed| D[git rm]
    C -->|Untracked or ignored| E[git clean -f]
    C -->|Ignored| F[git clean -f -x]
    C -->|Changed| G[git status]
    D --> H[One pasteable next command]
    E --> H
    F --> H
    G --> H
    B -->|No| I[No next command]
    A --> J[Alternative remedies in why]
Loading

File-Level Changes

Change Details Files
Make manifest kinds remediation output executable-safe by emitting at most one shell command and moving alternatives into why.
  • Select git rm, git clean -f/-x, or git status based on each file's Git state.
  • Add literal pathspec handling for names containing Git pattern characters.
  • Move refusal remedies and linked-record prose out of next and into why.
  • Add row-wide guards and zsh/refusal regression tests preventing prose or remediation text in commands.
shape-doctor.py
tests/test_doctor_next_is_a_command.py
tests/test_shape_doctor.py
Detect registered manifests placed at paths that the active gate does not read and report them as findings.
  • Add per-kind read_at metadata and dynamic pin-path resolution.
  • Treat same-file aliases, hard links, and case-folded names as read paths where applicable.
  • Record unread manifests in detail and provide removal or, for eligible pins, neutral-product declaration guidance.
  • Preserve existing validation behavior when gates cannot load and for path-independent policies.
shape-doctor.py
tests/test_shape_doctor.py
Expand coverage for Git-state remediation, path-aware manifest validation, command safety, and unchanged-root compatibility.
  • Add fixtures for unregistered, beside-root, changed, unread, and multi-fault manifests.
  • Verify printed commands can be run to restore compliance across committed, untracked, ignored, staged, and modified states.
  • Assert untouched-root rows remain byte-for-byte compatible and validate pathspec escaping, filesystem aliases, and refusal handling.
tests/test_doctor_next_is_a_command.py
tests/test_shape_doctor.py

Assessment against linked issues

Issue Objective Addressed Explanation
#439 Count a registered-kind manifest as validated only when its gate reads that file at its path in the current root; otherwise report it as a finding with the unread path and applicable remediation. ✅
#548 Make manifest kinds NEXT output a single usable command, or no command, instead of prose that may include backtick-wrapped commands. ✅
#548 Add a guard against NEXT lines containing backticks or Remediation:, including the run_checks path that can emit a refusal remedy. ❌ The guard covers rows and checks for Remediation:, but deliberately permits backticks inside single-quoted values and tests that case as valid. The issue asks for a guard that no NEXT line holds a backtick, without that exception.

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

…read` or takes it out, whatever the fault, so a misspelt leg role or a wrong `kind:` in the naming policy no longer gets a NEXT that removes the spec pin or the policy, and its NEXT shows a file git does not track instead of deleting it (#548, #439, review of #555)

Fix round for the independent review of 557a259.

A file a gate reads is that gate's to judge. `read_by_a_gate` is every
root path a gate that runs here reads, as any kind: each kind's
`read_at` for the kinds whose check runs on this root, plus `own_pins`,
the pins the shape itself writes (the shape pin, and in an assembly
root `contracts/spec-pin.yaml` and `contracts/code-pin.yaml`, whatever
the legs declare). `pins_read_here` counts `own_pins` too. On a
scaffolded root with one committed edit, main says `manifest kinds ok`
and 557a259 said:
- `- role: specs`: "no gate here reads contracts/spec-pin.yaml (...
  only at ... contracts/specs-pin.yaml)", NEXT `git rm -- contracts/spec-pin.yaml`,
  beside the `manifest` row's FINDING manifest-roles;
- `contracts/repository-naming.yaml` declaring `kind:
  pinned_contract_manifest`: "no gate here reads
  contracts/repository-naming.yaml", NEXT `git rm` of it, beside the
  `manifest` row's REFUSED policy-wrong-kind.
Both now say `ok manifest kinds 5 manifest(s)`, as main does. Beyond
the review's two cases, `taking_out` keeps every such path out of the
NEXT for any fault: an unregistered kind, or one whose check runs
elsewhere, at a gated path is still this row's finding, as on main, but
the NEXT never takes the file out, and the `why` says the gate's own row
judges it.

A file no commit holds is shown, never deleted. `git clean` cannot be
undone, and AGENTS.md tells an agent to run the NEXT, so for an
untracked or ignored file the NEXT is `git -C <root> status --ignored --
<files>`, and the `why` names the `git clean -f` (`-x` where ignored) for
a person to run, as `pin_restore` shows a file a person may have edited.
A committed file keeps `git rm`, which history can undo; a changed one
keeps `git status`.

Tests: `tests/test_update_shape_merged_away.py`, which skips without
git-filter-repo, asserted the linked-record row's NEXT as prose; it now
asserts no NEXT and the sentence in `why`. The guard reads a NEXT with
`outside_single_quotes`, moved beside `rows_of`, which reads `quote_arg`'s
`'\''` where the regular expression did not. The pathspec test goes
through the row with `contracts/*-pin.yaml` (POSIX) and
`contracts/[s]hape-pin.yaml`, and checks the `git rm` stages that file
alone. New: both review cases through the doctor, and the gated-path NEXT.
The #205 paragraph of `check_manifest_kinds` no longer describes main's
NEXT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sourcery-ai[bot]
sourcery-ai Bot previously approved these changes Oct 14, 2026

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

Sourcery assessment

Approved.

…nly NEXT that shows the commit or change that set it, and a `why` naming the kind a gate reads that path as and the row that really judges it, so the shape pin declaring `pinned_contract_manifes` or `family-manifest` is no longer a FINDING with no NEXT whose `why` points at a row that says `ok` (#548, #439, review of #555)

Round 2 of the review of PR #555.

What was wrong. Round 1 kept a file a gate reads out of every NEXT and
said "its own row judges it". For the shape pin and a declared neutral
product pin no row does: validate-pins.py's _check_shape_pin_header
reads their fields and never their kind. So `kind: pinned_contract_manifes`
(E1) or `kind: family-manifest` (E2) in contracts/shape-pin.yaml was
INVALID (manifest-kinds) with no NEXT, a `why` sending the reader to a
row that said ok, and an offer to register the kind or move the pin to a
family holder. A leg pin of the wrong kind (E3), which the pins row does
refuse (pin-wrong-kind), was still offered "Or register the kind".

What changes.
- kinds_read_here(ctx) maps each path a gate here reads to the kind it
  reads it as; kind_checked_by(ctx) names the paths whose gate refuses
  another kind, and its row: the naming policy (manifest, or family in a
  holder) and each declared leg's pin (pins). Nothing else, and nothing
  while the root's own manifest does not load.
- A file at such a path declaring another kind is `judged`. The NEXT is
  read-only (showing_the_kind): `git -C <root> diff HEAD -- <file>` for a
  change no commit holds, else `git -C <root> log -p -1 -- <file>`, or
  `git status --ignored` where git does not track it. The `why` names
  what that shows, the declared kind, the kind a gate here reads the path
  as, which row judges it ("the <row> row's gate finds that too" only
  where kind_checked_by says so; otherwise "no gate here checks the kind
  it declares, so this row is the one that says so"), and to put the
  kind line back. No register, move-to-a-holder or rename way is offered
  for a judged file; files beside it to take out are named for the next
  run.
- A registered kind whose check runs here, at a gated path read as
  another kind that no gate checks (the shape pin declaring
  `repository-naming-policy`), was `validated`, COMPLIANT. It is now a
  FINDING of this row, listed under the new `misdeclared` detail key,
  present only when there is one.
- The untracked file's `why` spells the deleting `git clean` after a
  colon, with no backticks a pasted line would run, and the changed
  group's sentence says git rm without backticks too.

Tests. tests/test_shape_doctor.py: E1 committed and uncommitted, E2 and
E4 on the shape pin through the whole doctor, E3 on the spec leg's pin,
and a declared neutral pin in process; each NEXT is run and shown to
change nothing and to print the kind line, and the shape pin, put back,
is COMPLIANT. run_read_only takes a `log` NEXT too. The naming policy's
round-1 test now expects the `diff HEAD` NEXT and the manifest row as
its judge. tests/test_doctor_next_is_a_command.py holds both new NEXTs
to the one-command property and the new untracked sentence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	tests/test_doctor_next_is_a_command.py
@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit 2bf8fd4 into main Oct 15, 2026
5 checks passed
@brettheap
brettheap deleted the l3-548-manifest-kinds branch October 15, 2026 16:32
brettheap pushed a commit that referenced this pull request Oct 15, 2026
… merge for ruling 27

A clean merge of main 2bf8fd4 (#555's
squash, on 26c1600), with no hand resolution: git auto-merged
shape-doctor.py and no path was conflicted.

The merge's tree, f9f6e6f, equals the tree
`git merge-tree --write-tree 2ca4044 origin/main` prints (T), and it is the
tree the targeted set passed on in a scratch merge of the predicted #555
squash before #555 landed: 1106 passed, 4 skipped (three PowerShell tests
that need a Windows host and one that needs a case-insensitive disk), over
the PR's own file, the doctor, bump-leg, update-shape and hygiene files.

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 15, 2026
Brings in #597 (654c476, CI on Linux only), with #565 and #555 before it.
Three conflicts:
	.github/workflows/tests.yml: main's file whole. #587's
	  `/bin/bash -n install.sh` and its comment lived in the macOS job #597
	  removed; no bash 3.2 step is added anywhere, as #597 decided, and this
	  pull request no longer touches tests.yml.
	README.md: the macOS sentence keeps #587's `install.sh` in the list and
	  takes #597's wording: "`setup.sh`, `install.sh` and the `openRepoShape`
	  command are written to use no syntax newer than that, though CI runs on
	  Linux only and no run of it checks them against bash 3.2", with "all
	  three entry points" after it. Four lines, as both sides had: README.md
	  stays 1449 lines, its cap with tests/readme-cap.d/564.txt (+22).
	docs/handbook.html: the macOS panel's sentence says what the merged
	  README says. The seven markers that read README.md § Starting a
	  project in a new organisation (masthead, start, partway, ci, legs,
	  doublepin, park) move to a0e0f23c306cebdf627d90225e33c277f05de64b, as
	  the hygiene check printed; the macOS sentence is the only part of that
	  section the merge changed, and no other part of the seven speaks of
	  CI's runners or bash 3.2. The footer is the same on both sides.
test_install_sh.py's pin check still holds: the Linux job installs
git-filter-repo==2.47.0, install.sh's pin. The tests' dated history that
names the macOS job stays, as #597 left it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 15, 2026
…lean merge after #555 and #597

A merge of main 3f768a6. The brief named
654c476 (#597); main has since moved one commit, to siblings.py and
family.py, which touches AGENTS.md among this branch's files.

No hand resolution: git auto-merged AGENTS.md, adopt-project.py,
shape-doctor.py, tests/test_doctor_next_is_a_command.py and
tests/test_shape_doctor.py, and no path was conflicted, although #555
(2bf8fd4) rewrote shape-doctor.py and both of those tests.

- The merge's tree, 6ad6724, is the tree
  `git merge-tree --write-tree b640d34 origin/main` prints.
- This branch's own changed lines against main are the same 1,289 `+`/`-`
  lines they were against its base 2247dbf, in the same 8 files.
- #555's `placement_policy` refusal (no NEXT, its remedy in the `why`) is
  as main has it; this branch's moved NEXT in `check_placement` stays a
  command, the doctor again, as `unreadable_next`'s is.
- AGENTS.md stays at 490 of its 490 lines.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 15, 2026
… fix/540-pin-source-unread

This merges origin/main 2bf8fd4 as the shared store held it at
2026-10-15T16:50Z, thirteen landings past the 7d9bec9 this branch last
merged: #555 (shape-doctor's manifest kinds), #565 (scaffold's
`scaffold-remote-exists`), #581 (adopt's `check` of a kept pin path and
an unwritable work directory), #568, #559, #557, #560, #580, #556, #558,
#567, #579 and #562. It is done so that the PR's CI merge ref and this
round's rewording run on current main.

The merge was clean, with no conflict hunk. `adopt-project.py`,
`scaffold-project.py`, `scripts/shape_materialize.py`, `docs/cli.md` and
`tests/test_adopt_root_symlinks.py` took main's changes beside this PR's
by the textual merge, and `scripts/render-cli-reference.py --check`
passes on the result.

Main: 2bf8fd4.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment