Skip to content

shape-doctor.py: manifest kinds counts a registered kind validated only on a kind of root that runs its check, so a contracts/family.yaml in an assembly root, or a contracts/project.yaml in a family holder, is a FINDING naming the git rm that takes it out instead of ok and COMPLIANT (#327) - #375

Merged
brettheap merged 3 commits into
mainfrom
fix/doctor-contracts-manifests
Oct 12, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-3 (openRepoShape-3)
Claim: #327 (comment)

What

A finding, not a note

The deciding text is AGENTS.md, "Checking a repository's compliance":

  • A note is "a difference nothing asserts" and never moves the verdict.
  • A FINDING is what something else asserts (a validator, a pin row, a manifest, a gitlink), and it names its exact fix.

A file that declares kind: is a manifest asserting what it is. This row already makes a declared kind that nothing validates a FINDING: an unregistered kind gets "no validator registered for kind ...". #211 also made these same bytes a FINDING when they sit at the top of the root. A note would rank kind: family-manifest under contracts/ below kind: service-inventory in the same directory. It would also make one file a FINDING at one path and a note one directory down. The new sentence therefore opens the way the unregistered kind's does.

Why

Measured by scaffolding an assembly root (Atlas) and a family holder (InkRouter, one member, IRRS) from each head with scaffold-project.py and family.py init/add, then committing one file into a copy of each:

root file committed main ab623dd this head 4710487
assembly root none ok "5 manifest(s), every declared kind has a registered validator"; COMPLIANT, exit 0 identical; the manifest kinds row in --json is byte-identical (cmp)
family holder none ok "3 manifest(s), ..."; COMPLIANT, exit 0 identical; row byte-identical (cmp)
assembly root contracts/family.yaml (family-manifest) ok "6 manifest(s), every declared kind has a registered validator"; COMPLIANT, exit 0 FINDING (below); INVALID (manifest-kinds), exit 1
family holder contracts/project.yaml (project-manifest) ok "4 manifest(s), ..."; COMPLIANT, exit 0 FINDING (below); INVALID (manifest-kinds), exit 1
assembly root family.yaml at the top (#211) FINDING; INVALID (manifest-kinds), exit 1 identical
family holder contracts/spec-pin.yaml (pinned_contract_manifest) ok; COMPLIANT, exit 0 identical (see Left out)
assembly root contracts/project.yaml (project-manifest) ok; COMPLIANT, exit 0 identical (see Left out)
family holder contracts/family.yaml (family-manifest) ok; COMPLIANT, exit 0 identical (see Left out)

This head, with the root's path shown as <root>:

FINDING  manifest kinds   no validator that runs in an assembly root is registered for kind contracts/family.yaml (family-manifest: scripts/validate-family.py runs only in a family holder)
         NEXT             take contracts/family.yaml out of this root (`git -C <root> 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
INVALID (manifest-kinds)   (exit 1)

FINDING  manifest kinds  no validator that runs in a family holder is registered for kind contracts/project.yaml (project-manifest: scripts/validate-manifest.py runs only in an assembly root)
         NEXT            take contracts/project.yaml out of this root (`git -C <root> rm -- contracts/project.yaml`, committed on a branch and landed as a pull request), or move what it describes to an assembly root of its own (`setup.sh`, or `adopt-project.py plan` for a repository that exists), which a holder pins with `family.py add`, 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
INVALID (manifest-kinds)   (exit 1)

The new tests run that git rm, commit it, and run the doctor again: both roots are back to COMPLIANT, exit 0.

Issue check. The issue's measurements reproduced exactly on 71b58a1, and again on ab623dd after the rebase: 6 and 4 manifests, ok, COMPLIANT, exit 0. Nothing in it turned out wrong, and this PR takes its first direction. One thing it did not say: the rule as worded there ("its covered_by check applies to the root's kind") would have made every family holder INVALID if applied to the old one-check table. A holder's own contracts/shape-pin.yaml and contracts/repository-naming.yaml were registered only to pins and naming, and neither check runs in a holder. Bite (b) below shows it.

Tests

All on this head, 4710487, rebased on main ab623dd. Each run used --basetemp with git-filter-repo 2.47.0 first on PATH:

files result
tests/test_shape_doctor.py 258 passed, 3 skipped in 440.21s
tests/test_family.py tests/test_shape_advisory.py tests/test_commit_trailers.py tests/test_openreposhape_command.py plus the four guards tests/test_repo_hygiene.py tests/test_cli_reference.py tests/test_git_output_decoding.py tests/test_stdout_encoding.py 486 passed, 1 skipped in 250.12s
tests/test_gh_guard.py (it runs the doctor against the session gh guard) 6 passed in 4.49s
  • Main ab623dd collects 257 tests in tests/test_shape_doctor.py and this head collects 261. The four new ones are:

    • test_a_contracts_family_yaml_in_an_assembly_root_is_a_finding;
    • test_a_contracts_project_yaml_in_a_family_holder_is_a_finding;
    • test_a_contracts_family_yaml_and_one_beside_project_yaml_say_both;
    • test_a_registered_kind_is_answered_only_where_its_check_runs.

    This diff adds tests and changes none, so every test that passes on main passes here.

  • On main ab623dd, the four new tests: 4 failed.

  • Bites, each on this head with the fix broken one way, running the four new tests and the three holder and shape-doctor.py reads only project.yaml at a root that also carries a family.yaml, so an assembly root whose family.yaml declares two unmounted members is COMPLIANT, and its manifest kinds row says that kind has a registered validator, which is never run #211 tests they lean on:

    • (a) a registered kind counted validated wherever it sits: 3 failed;
    • (b) pinned_contract_manifest without family: 4 failed, two of them existing holder tests (test_a_family_holder_gets_the_family_rows, test_a_root_with_no_family_yaml_beside_gets_the_row_it_always_got);
    • (c) the NEXT naming git rm --cached, a removal that leaves the file on disk: 2 failed;
    • unbroken: 7 passed.
  • Skips, all Windows-only, and CI's Windows job runs them:

    • 3 in tests/test_shape_doctor.py: "the PowerShell line is printed, and powershell.exe exists, only on a Windows host";
    • 1 in tests/test_stdout_encoding.py: "the default this is about is Windows' own".

    No new test is skipped on any OS. shlex.split reads the git rm line back only off Windows; on Windows the line is held to the doctor's own quote_arg. The removal itself runs as an argv list on every OS.

  • The untouched assembly root and family holder: the manifest kinds row in --json is byte-identical between main ab623dd and this head (cmp). The full --json differs only in commit shas: this standard's own, and the scaffolded legs' and member's.

Composes with

Each trial merge is git merge-tree --write-tree --name-only <branch> <this head>, against this head 4710487 and against origin/main ab623dd alone. The table lists every OPEN pull request whose diff touches shape-doctor.py or tests/test_shape_doctor.py:

PR branch @ head merged with this head merged with origin/main alone
#358 (lane 2) fix/doctor-manifest-kinds-unreadable-pin @ ebffb08 CONFLICT in shape-doctor.py CONFLICT in shape-doctor.py
#267 (lane 2) fix/doctor-root-shape-pin-unreadable @ 8badae0 CLEAN CLEAN
#354 (lane 4) fix/doctor-legs-members-next @ 604e2d7 CLEAN CLEAN
#357 (lane 4) fix/doctor-validator-cannot-run @ 8ad856f CLEAN CLEAN
#347 fix/update-shape-pinned-copy-symlink @ e965ab0 CLEAN CLEAN
#343 fix/pin-files-not-a-list @ 039530b CLEAN CLEAN
#245 fix/adopt-relative-submodule-url @ a0272b2 CLEAN CLEAN

ManifestKind.runs_in() reads the registry: applies_to, CHECKS and MANIFEST_VALIDATORS. None of the shape-doctor.py PRs above changes it. The diffs of #267, #343, #347, #354 and #357 do not name it at all, and #358's names MANIFEST_VALIDATORS only inside the "register the kind" sentence it moves.

#358 and this PR both change the manifest kinds row

#358's conflict is with main, not with this PR. #358 is based on 39ac59c, before main's #211 (eb6df75) and #205 (d94d155) rewrote check_manifest_kinds. It conflicts in shape-doctor.py with origin/main alone. #267's head is an ancestor of #358's, so the merged tree below carries #267 too.

The two rules compose in behaviour. For the test, I resolved this head merged with ebffb08 by hand in a scratch worktree. The resolution kept every rule from both sides:

On that tree:

One decision is left for whichever of the two lands second: the NEXT of a row that holds both. My scratch resolution joined the fixes with ; , as main's #211 already does for family_beside. #358's restore command ends in a # shell comment, so this PR's take ... out of this root (...) clause lands inside that comment. Pasted, the line runs only the restore. #358's own docstring states the rule for this case: "where a row has both it names the restore first ... The next run names the other", with one command per row. Under that rule the NEXT is the restore alone while a file does not load, and that is what I would pick. The same question already stands between #358 and #211's family_beside on main.

Left out

Closes #327

🤖 Generated with Claude Code

Summary by Sourcery

Report registered manifests as invalid when their validators do not run in the current root type, and provide safe removal or relocation guidance.

New Features:

  • Detect manifests whose registered validator does not run for the current root type and report them as compliance findings with actionable remediation guidance.
  • Include repository-state-aware removal instructions and validator execution details for misplaced manifests.

Bug Fixes:

  • Prevent family manifests in assembly roots and project manifests in family holders from being incorrectly counted as validated and marked compliant.
  • Ensure family-holder validation covers pinned contract and repository naming manifests.

Enhancements:

  • Derive manifest validator coverage from check applicability to keep root-kind behavior synchronized with the check registry.

Tests:

  • Add coverage for misplaced manifests, combined findings, repository states, read-only git status handling, and validator applicability.

…` only on a kind of root that runs its check, so a `contracts/family.yaml` in an assembly root, or a `contracts/project.yaml` in a family holder, is a FINDING naming the `git rm` that takes it out instead of `ok` and COMPLIANT (#327)

`check_manifest_kinds` sent every `kind:`-bearing file except #211's
top-level `family.yaml` to `MANIFEST_VALIDATORS.get(kind)` and counted it
`validated` whenever the kind had an entry, whatever the root. The gate
registered for `family-manifest`, `validate-family.py`, runs in a family
holder only, and the one for `project-manifest`, `validate-manifest.py`, in
an assembly root only. So those files were checked by nothing while the row
said "every declared kind has a registered validator".

- `ManifestKind.covered_by` is now the ids of the checks that validate the
  kind, one for each kind of root, and `ManifestKind.runs_in()` reads each
  check's `applies_to` out of `CHECKS`: the registry alone says where a
  check runs. `pinned_contract_manifest` and `repository-naming-policy` name
  `family` beside `pins` and `naming`, because a holder's
  `validate-family.py` reads `contracts/shape-pin.yaml` and loads the naming
  policy itself.
- A file whose registered kind is checked nowhere on this kind of root goes
  under `detail.registered_elsewhere` (manifest, kind, validator, runs_in),
  not `validated`, and the row is a FINDING: "no validator that runs in
  <an assembly root | a family holder> is registered for kind <path>
  (<kind>: <validator> runs only in <root>)". The verdict is
  `INVALID (manifest-kinds)`, exit 1.
- A FINDING, not a note. The row already calls a declared kind that nothing
  validates a FINDING (an unregistered kind), the same bytes as `family.yaml`
  at the top of the root are one (#211), and a note is a difference nothing
  asserts, whereas a file that declares `kind:` asserts what it is.
- The next command is `git -C <root> rm -- <path>`, quoted for the reader's
  shell, committed on a branch and landed as a pull request. The other exit,
  as the human decides, is the kind's `home`: `family.py init` then
  `family.py add` for a family manifest; `setup.sh` or `adopt-project.py
  plan`, then `family.py add`, for a project manifest. The line also rules
  out renaming the file to `project.yaml` or `family.yaml` in this root.

Unchanged: a root with no such file gets the same `manifest kinds` row. On
an untouched assembly root and family holder, its `--json` row is
byte-identical between the two heads, because the key is added only when
there is one, as `family_beside` is. #211's top-level case still runs first
and still checks the members. The unregistered and unreadable cases are
untouched. Every test in tests/test_shape_doctor.py that passed before
still passes.

Left out: a registered kind at a path its check never reads, in a root
where that check does run, is still counted `validated`. Examples are a
`contracts/project.yaml` in an assembly root, a `contracts/family.yaml` in a
holder, and a `contracts/<x>-pin.yaml` that no leg or declared descent
names.

Lane: openreposhape-3 (openRepoShape-3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-12T14:44:49Z) @codex review

@sourcery-ai

sourcery-ai Bot commented Oct 12, 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 8 hours and 45 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@sourcery-ai

sourcery-ai Bot commented Oct 12, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates manifest kinds so a registered manifest kind is counted as validated only where one of its registered checks actually runs, turning misplaced family/project manifests into actionable findings while preserving existing behavior and adding comprehensive regression tests.

Sequence diagram for misplaced manifest finding

sequenceDiagram
    participant Doctor as shape-doctor.py
    participant Registry as MANIFEST_VALIDATORS
    participant Checks as CHECKS
    participant Report as manifest kinds row
    Doctor->>Registry: Look up manifest kind
    Registry-->>Doctor: ManifestKind.covered_by
    Doctor->>Checks: Read check.applies_to via runs_in()
    Checks-->>Doctor: Root kinds where validator runs
    Doctor->>Report: Add registered_elsewhere detail
    Doctor->>Report: Emit FINDING and git rm NEXT
Loading

Flow diagram for manifest kind validation by root type

flowchart TD
    A["Manifest with kind"] --> B{"Kind registered?"}
    B -->|No| C["FINDING: unregistered"]
    B -->|Yes| D["ManifestKind.runs_in()"]
    D --> E{"Validator runs in this root?"}
    E -->|Yes| F["validated"]
    E -->|No| G["FINDING: registered_elsewhere"]
    G --> H["NEXT: git rm, or move to the kind's home"]
Loading

File-Level Changes

Change Details Files
Make manifest-kind validation depend on whether the registered validator runs for the current root kind.
  • Expanded validator coverage from one check ID to per-kind tuples of check IDs.
  • Derived applicable root kinds from each check’s registry metadata instead of maintaining a second root-kind mapping.
  • Classified registered-but-ungated manifests as findings with registered_elsewhere diagnostics and invalid verdicts.
  • Preserved existing handling for unregistered, unreadable, and top-level family.yaml cases.
shape-doctor.py
Add holder-specific coverage for shared contract manifests.
  • Registered pin and repository-naming kinds with both project and family validators.
  • Enabled family validation to answer the shape-pin and naming-policy manifests locally.
shape-doctor.py
Provide actionable remediation for manifests found outside the roots their validators support.
  • Generated shell-quoted git -C <root> rm -- <path> commands.
  • Explained the appropriate family-holder or assembly-root home for each manifest kind.
  • Explicitly ruled out renaming manifests to the root’s own conventional filename.
shape-doctor.py
Add regression coverage for cross-root manifest findings and registry-derived coverage.
  • Tested misplaced family and project manifests individually and together with the existing top-level family case.
  • Verified JSON details, human-readable output, exit status, remediation commands, and return to compliance after removal.
  • Added assertions that every registered kind’s coverage is derived from valid checks and expected root applicability.
tests/test_shape_doctor.py

Assessment against linked issues

Issue Objective Addressed Explanation
#327 Count a registered manifest kind as validated only when a check registered for that kind runs on the current root; otherwise report it as a finding rather than allowing the root to remain compliant. ✅

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

… file, so a `contracts/family.yaml` nobody committed is to be deleted instead of handed to a `git rm` that says "did not match any files", one with uncommitted changes is told to commit or drop them first, and a project manifest's other exit names `setup-project.py` for Windows: the fix round on the review of 4710487 (#327)

The review of 4710487 found that the NEXT named `git -C <root> rm --
<path>` for every file, while the doctor reads the disk. Measured with
the suite's own fixtures, that command exits 128 ("did not match any
files") for a file that was copied in and never added, or that the root's
excludes ignore. It exits 1 ("has local modifications") for a committed
file edited since. The "committed on a branch" clause described nothing
for either.

- `git_states` asks ONE `git --no-optional-locks --literal-pathspecs
  status --porcelain=v1 -z --untracked-files=all --ignored` about the
  files the row names. A path it does not list is `committed`: HEAD, the
  index and the work tree agree on it, and that is the one state `git rm`
  takes. `??` alone is `untracked`, `!!` alone is `ignored`, and anything
  else is `changed`: an edit, a staged change, a file staged and never
  committed, or a removal staged while the file is still on disk.
  `--no-optional-locks` keeps `status` from writing the refreshed index
  back, because the doctor writes nothing into the repository.
- `removal_ways` gives each state its own exit:
  - `committed`: the `git rm`, committed on a branch and landed as a pull
    request;
  - `untracked` or `ignored`: delete it, with nothing to commit;
  - `changed`: commit or drop the changes first, with the `git status`
    that shows them. `git rm` will not discard them, and the next run
    names the `git rm`.
  A row whose files come out different ways names each file in its own
  clause. Each file's state is `detail.registered_elsewhere[].git`.
- `git_states` declines a directory that is not its own repository
  (`not_its_own_repository`, as for a leg). There, `status` would answer
  about the enclosing repository, with paths relative to its top, and
  every file would read as `committed`. The NEXT then carries no
  parenthesis and reads like #211's: take it out of this root.
- The project manifest's other exit names `setup-project.py` on Windows
  beside `setup.sh`, which is a bash script. It is one sentence on every
  platform, so it names both.

Unchanged: a committed, clean file gets the NEXT it got at 4710487, byte
for byte. The two earlier tests' NEXT assertions are untouched, and only
their `detail` gains `"git": "committed"`.

Tests: 6 new. Untracked, ignored, committed-then-edited and staged-never-
committed (each FINDING, then followed to COMPLIANT), three files that
come out three ways with a stat-dirty index left byte for byte, and
`git_states` in a directory inside another repository. The first five
fail on 4710487 on the NEXT; the last, on the missing helper.

Lane: openreposhape-3 (openRepoShape-3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-12T15:38:33Z) fix round 1 (independent review, codex is out of quota):

The head is now 0bf6a9f, a fast-forward from 4710487 with one new commit.

1. should-fix: the NEXT's git rm fails for a file not cleanly committed. Fixed.

git_states now asks ONE git --no-optional-locks --literal-pathspecs status --porcelain=v1 -z --untracked-files=all --ignored about the files the row names. --no-optional-locks is there so the doctor never writes the index back. The new removal_ways gives each state its own exit. Measured with contracts/family.yaml (the holder's family.yaml) in a scaffolded Atlas root. Every case is FINDING, INVALID (manifest-kinds), exit 1, on both heads:

file NEXT at 4710487 that git rm NEXT at 0bf6a9f detail...git
committed, clean git -C <root> rm -- contracts/family.yaml, committed on a branch... exit 0 the same, byte for byte committed
copied in, never added the same git rm exit 128, did not match any files (delete it, which git does not track, so there is nothing to commit) untracked
ignored through .git/info/exclude the same git rm exit 128, did not match any files (delete it, which git does not track, ...) ignored
committed, then edited the same git rm exit 1, has local modifications (it has changes no commit holds, which git rm will not discard: commit or drop them (git -C <root> status -- contracts/family.yaml shows them), and the next run names the git rm) changed
staged, never committed the same git rm exit 1, has changes staged in the index as for edited changed

2. nit: setup.sh on every OS. Fixed, since home is this PR's line. The project manifest's other exit is now "an assembly root of its own (setup.sh, or setup-project.py on Windows, for a new one; adopt-project.py plan for a repository that exists), which a holder pins with family.py add". It is one sentence on every OS, so it names both entry points; PROJECT_HOME matches.

Tests. Six new tests in tests/test_shape_doctor.py:

  • untracked and ignored: deleting the file is COMPLIANT;
  • committed then edited, and staged never committed: committing gives a NEXT that names the git rm, and running it is COMPLIANT;
  • three files that come out three ways, with a stat-dirty index that must stay byte for byte;
  • git_states in a directory inside another repository.

Evidence:

Unchanged. A committed, clean file gets 4710487's NEXT byte for byte (measured above). The two earlier tests' NEXT assertions are untouched; their detail gains only "git": "committed". The #358 composition note in the description stands.

The squash message now describes the final state.

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit cd1fc79 into main Oct 12, 2026
5 checks passed
@brettheap
brettheap deleted the fix/doctor-contracts-manifests branch October 12, 2026 17:34
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 pushed a commit that referenced this pull request Oct 12, 2026
…plan-manifest-invalid` joins `_entry_findings` after #255's values, and this branch's `plan-root-symlink` stays after the source's own submodules

origin/main moved 92e29d9 -> dd8d0ea (twelve commits), merged by sha:
- f0a8bf6 (#330 via #381), scripts/family.py;
- 87d2eb7 (#333 via #379), update-shape.py;
- cd1fc79 (#327 via #375), shape-doctor.py;
- 45e01f4 (#348 via #396), tests/test_agents_md_codes.py: every
  code-shaped name AGENTS.md spells is a code a tool prints;
- 3ef059c (#258 and #261 via #314), adopt-project.py, scaffold-project.py
  and scripts/shape_materialize.py: the project's YAML reader and its
  `validate-manifest.py` are asked about the manifest's values before
  anything exists; `check` reports `plan-manifest-invalid` through
  `_manifest_findings` in `_entry_findings`; `_repository_urls` refuses
  `adopt-name-too-long`;
- f7f4b14 (#366 via #399), scripts/family.py;
- cb7063f (#301 and #280 via #390), scripts/repo_shape.py and the
  template's validate-manifest.py: a bracket or brace in a plain flow
  scalar is refused;
- 5678f68 (#278, #297 and #298 via #357), shape-doctor.py;
- 53bee5b (#289 via #352), scripts/bump-leg.py;
- f5cb616 (#369 via #398), scripts/shape_materialize.py: the docstrings
  of `force_stage` and `stage_written`;
- a92e60b (#326 via #400), scripts/family.py;
- dd8d0ea (#292 via #353), adopt-project.py: `_elector` keeps a
  `user.name` byte that is not UTF-8, `_remote_repository` reads an
  origin URL that is not UTF-8 as none, and `adopt-no-org` is asked
  before `checked_value`.

scripts/shape_materialize.py, scripts/repo_shape.py and every other file
merge with no conflict. adopt-project.py conflicts in two hunks of
`_entry_findings`, each resolved by keeping both sides:
1. The docstring. Main's order, with #261's manifest findings after
   #255's values and before #190's relative urls; this branch's symlinks
   stay after the source's own submodules.
2. The return. Main's `_manifest_findings(plan)` call, and the findings
   in main's order: each entry's path (#196), coverage, the legs and what
   the root keeps where the shape writes, the source's own submodules
   (#166), this branch's `_root_symlink_findings`, #255's values, #261's
   manifest findings, and #190's relative urls last. The remediation and
   the NOTEs are main's.

How each of main's commits that reach this branch's code meets it:
- 3ef059c: `_manifest_findings` asks the validator the template ships
  about the values `execute` writes; it reads no symlink and no path
  the shape writes. Its materializer half adds the validator's helpers,
  `refuse_unstorable_names` and `MANIFEST_VALIDATOR`, and holds
  `election_date` to the validator's `DATE_RE`; it leaves `place`,
  `_materialize`, `copy_tree`, `template_files` and `_refuse_unwritable`
  as they were.
  In `execute`, its `adopt-name-too-long` (in `_repository_urls`) comes
  before #255's clone-URL refusal, `_refuse_what_check_finds` and this
  branch's `core.symlinks` probe, all before any leg. Its new paragraph
  of the module docstring follows the one 6b53a6d corrected; both merge
  with no conflict.
- f5cb616: docstrings only.
- dd8d0ea: `_remote_repository`, `_checked_plan_inputs`' organisation and
  `_elector`, none of them this branch's.
- cb7063f: the plans `check` and `execute` read go through this reader.
  The plans `plan` writes for this branch's tests hold their entries as
  block mappings, each path `double_quoted` where it needs to be, and
  the rehearsal below read every one of them.

Checked on the merged tree:
- AST (fin183g/names.py, base 92e29d9): 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 dd8d0ea does (1f90a0f's
  import and 3ef059c's, the same objects); the merge adds none.
- Both sides intact (fin183g/sides_intact.py): the only lines that differ
  from either side's own change are those of the two hunks.
- A rehearsal of this merge (tree d21f8b4, the same as this one) passed
  tests/test_adopt_root_symlinks.py with main's
  test_adopt_{manifest_values,git_reads,relative_submodule_url}.py,
  test_scaffold_manifest_values.py, test_agents_md_codes.py and
  test_yaml_flow_brackets.py: `440 passed`.

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