Skip to content

shape-doctor.py: a family.yaml beside an assembly root's project.yaml is a manifest kinds FINDING, INVALID (manifest-kinds), naming each member it pins with no gitlink, off its pin or with no 40-hex pin and each gitlink under members/ no row names, so a root its family gate refuses is no longer COMPLIANT, exit 0, with that file counted as validated (#211) - #220

Merged
brettheap merged 4 commits into
mainfrom
fix/doctor-reads-family-yaml
Oct 11, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

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

What

check_manifest_kinds counted a family.yaml that declares family-manifest as validated whatever root it sat in. Context reads ONE manifest per root and project.yaml wins, so in an assembly root nothing ever runs that kind's registered validator, validate-family.py: the family check applies to a holder only. A hand-copied or mis-merged family.yaml pinning two members with no gitlink was COMPLIANT, exit 0, while validate-family.py refuses the same tree with member-gitlink-absent, exit 2.

  • In an assembly root, a family.yaml declaring family-manifest is no longer counted validated. family_beside_project makes the manifest kinds row a FINDING (verdict INVALID (manifest-kinds), exit 1). Its reason says the file sits beside project.yaml and that its validator never runs on an assembly root, then names every problem, not only the first:
    • each member row with no gitlink (mode 160000) at its path;
    • each member whose gitlink is not its pin, with both commits;
    • each member whose pin.commit is not 40 hex;
    • each gitlink under members_dir that no row names. gitlinks_under reads the index at stage 0 with -z. An empty directory, an absolute one in either platform's spelling, or one that climbs with .. lists nothing rather than every gitlink in the root.
      Paths and pins default exactly as member_state defaults them for a holder, and each gitlink is read with recorded_gitlink, the helper validate-family.py refuses by.
  • It is a FINDING even when every member is recorded at its pin, on purpose. The reason says so (though each is recorded at its pin today). A family holder is not a project, and a manifest whose gate this root never runs is exactly what the row exists to report. The issue asked for no verdict better than INVALID.
  • The next command offers both exits and picks neither. Either take family.yaml out of the root, or move the membership to a holder of its own (family.py init, then family.py add there). It never offers family.py add in the assembly root, which is what validate-family.py's own refusal would offer: that would mount the members inside a project.
  • --json carries the facts as detail.family_beside: manifest, members (project, path, pin, gitlink, state: at-pin | off-pin | no-pin | no-gitlink) and undeclared_gitlinks. The key appears only on such a root.
  • Beside an unregistered kind, the row says both. The two clauses and the two next commands are joined with ; , the unregistered kind first.
  • README.md gains one clause in the doctor's row list, inside the manifest kinds parenthesis: "so a contracts/*.yaml nothing validates is a finding by name rather than a silence, and so is a family.yaml beside project.yaml". README.md is capped at 1416 lines and main is exactly 1416, so the file stays at 1416. One phrase of the same paragraph is tightened to make room: "a comparison and not a gate, because no leg file" becomes "a comparison and not a gate: no leg file". Lines 1115-1129 are re-wrapped at 80 columns; main already has 18 lines of exactly 80 characters and 64 longer ones. git diff --word-diff a5b09c3 HEAD -- README.md shows only those two edits. Nothing in another paragraph moves, and the cap is not raised. Fix round 1 restored "in the root" ("every kind:-bearing manifest in the root against a registry"), which the first trim had dropped. My predecessor's six-line sentence (README at 1421) was dropped for this.
  • docs/handbook.html follows the README. Its doctor section gets the same clause. Its footer history gets "a family.yaml beside project.yaml as a manifest kinds finding added (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)", and the footer's README.md blob is now git hash-object README.md (20bb4a3e884c3232380407bda345af6e29d428c6). The page's doctor section never said "in the root", not even on main, so fix round 1 moved only the blob.

Why

Measured on this machine (Eagle, py-bench). Each fixture is built from a one-commit standard. Atlas, IRRS and IRSS are scaffolded into local bare remotes, and the InkRouter holder is made with family.py init + add IRRS, IRSS. Each "beside" fixture is an Atlas clone with the holder's family.yaml committed. Gitlinks are recorded with git update-index --cacheinfo 160000,… and committed over empty mounts. Both doctors ran from ONE standard path with only shape-doctor.py swapped, and gh was a logging shim that exits 1, so the run stayed offline. Base: main a5b09c3, the current tip, with fixtures built from a5b09c3. My predecessor measured 20e13f0 and 8df21e6 and got the same results. shape-doctor.py is byte-identical at all three (git diff --quiet 8df21e6 a5b09c3 -- shape-doctor.py). Head: this branch. The last column is the holder template's validate-family.py --pins run on the same tree.

root main a5b09c3 this branch validate-family.py --pins
Atlas + family.yaml, members not mounted (the issue) ok manifest kinds 6 manifest(s), every declared kind has a registered validator / COMPLIANT (exit 0) FINDING … so nothing checks the 2 member(s) it pins: members/IRRS has no gitlink (mode 160000), members/IRSS has no gitlink (mode 160000) / INVALID (manifest-kinds) (exit 1) exit 2 REFUSED member-gitlink-absent
… both gitlinks at their pins ok / COMPLIANT (exit 0) FINDING … so nothing checks the 2 member(s) it pins, though each is recorded at its pin today / INVALID (manifest-kinds) (exit 1) exit 2 REFUSED member-uninitialized
… IRRS's gitlink off its pin ok / COMPLIANT (exit 0) FINDING …: members/IRRS records gitlink d6a081bd79f5 where family.yaml pins 46db9573d2b7 / INVALID (manifest-kinds) (exit 1) exit 2 REFUSED member-uninitialized
… IRRS's pin.commit: "main" ok / COMPLIANT (exit 0) FINDING …: IRRS's row carries no 40-hex pin.commit to compare against / INVALID (manifest-kinds) (exit 1) exit 2 REFUSED member-pin-commit
… both at pin + a gitlink at members/Stray ok / COMPLIANT (exit 0) FINDING …: members/Stray is a gitlink no family.yaml row names / INVALID (manifest-kinds) (exit 1) exit 2 REFUSED member-uninitialized
Atlas, no family.yaml COMPLIANT (exit 0) text and --json byte-identical to main (cmp) n/a
Atlas + contracts/inventory.yaml (kind: service-inventory) INVALID (manifest-kinds) (exit 1) text and --json byte-identical to main (cmp) n/a
the InkRouter holder COMPLIANT (exit 0) text and --json byte-identical to main (cmp) n/a

On the five beside roots, every other row and every other --json key is identical to main. Only the manifest kinds row and the verdict change (diffed with a script that removes that row and the verdict, then compares the rest).

Tests

Targeted set on head 6091e6edf49d78963cbc6f90c7969e81d2ffcf77 (based on main a5b09c3), run as /usr/bin/python3 -m pytest tests/test_shape_doctor.py tests/test_family.py tests/test_manifest_validator.py tests/test_repo_hygiene.py tests/test_cli_reference.py -q with git-filter-repo 2.47.0 on PATH:

369 passed in 323.58s (0:05:23)

Ten new tests in tests/test_shape_doctor.py, placed right after test_a_family_holder_gets_the_family_rows:

I ran the new tests against main's shape-doctor.py: a scratch worktree at a5b09c3 with this branch's tests/test_shape_doctor.py copied in, -k selecting the ten plus test_a_family_holder_gets_the_family_rows. Result at fix round 1: 9 failed, 2 passed, 180 deselected in 30.21s. The two that pass are the holder test and the unchanged-row test, which is what they should do on main. None of the new tests is skipped on Windows. They stage gitlinks with git update-index --cacheinfo and pass the Windows spellings (\members, C:/members, the absolute root) to gitlinks_under explicitly.

Composes with

Trial merges (git merge-tree --write-tree <sibling> HEAD) against this head, re-run at fix round 1. The list is every sibling branch, local or pushed, that touches shape-doctor.py, tests/test_shape_doctor.py, README.md or docs/handbook.html, plus the two my brief named:

sibling sha result
origin/fix/doctor-unparsable-manifest (#205, lane 4) 50aa967 CONFLICT shape-doctor.py: four hunks, all in check_manifest_kinds
origin/fix/bump-leg-tracked-workflows-only (#207, PR #222) 52f4bff CONFLICT docs/handbook.html: the footer's history line and its README.md blob only. README.md merges clean to 1416 lines
fix/bump-leg-tracked-workflows-only (#207, local, its round in progress) 995fcb1 CONFLICT docs/handbook.html: the same footer hunk. README.md merges clean to 1416 lines
fix/family-add-member-case (#206), local = pushed 6c917d6 CLEAN
fix/decode-git-output-utf8, local = pushed b6f7cfd CLEAN
origin/fix/adopt-record-submodule-placement e167d8b CLEAN
fix/plan-report-stdout-encoding (local) 3006303 CLEAN

Main moved after the test run, from a5b09c3 to 54aa69a (#165 / #173: adopt-project.py and two adopt test files, none of them this branch's files). git merge-tree --write-tree origin/main HEAD is CLEAN. The tests ran on a5b09c3.

#207 / #222: whichever of the two lands second regenerates the handbook footer by hand. README.md itself merges CLEAN: the two edits are in different paragraphs, and the merged file is 1416 lines (measured on the merge-tree result). The handbook footer keeps both history clauses, #207's and this branch's. Its README.md blob must then be re-set to git hash-object README.md of the MERGED README. Neither branch's hash is right after the merge, and test_the_handbook_follows_the_readmes_outline fails until the hash is re-set.

#205: whichever of the two lands second merges by hand, keeping both sides. Every hunk is additive:

The two cases cannot meet on one root. Under #205, a root whose project.yaml does not load and whose family.yaml declares family-manifest is a FAMILY root, and this branch acts only on a PROJECT root. No assertion in #205's tests reads a family.yaml beside a good project.yaml. #206's Family.holding is a row lookup in scripts/family.py, which shape-doctor.py does not import. The helpers here list index gitlinks and compare rows with recorded_gitlink, so nothing duplicates it.

Left out

  • shape-doctor.py could run the registered validator for every declared kind it finds in a root (the issue's alternative fix) instead of reading a stray family.yaml's members against the gitlinks itself.
  • A member's tree digest and its id are not checked in an assembly root, because the row is a finding either way. validate-family.py checks both in a holder.
  • A contracts/family.yaml declaring kind: family-manifest in an assembly root is still counted validated and its row is ok: this branch acts only on the root family.yaml, the file the issue names. The general rule would count a kind as validated only when its covered_by check applies to the root's kind. The reviewer read the mirror case from the code but did not run it: a contracts/*.yaml declaring project-manifest in a family holder.

Closes #211

🤖 Generated with Claude Code

Summary by Sourcery

Detect family manifests placed beside project manifests and report them as invalid assembly-root findings with actionable membership details.

New Features:

  • Report a root-level family.yaml beside project.yaml as an invalid manifest-kinds finding, including member pin and gitlink discrepancies and undeclared member gitlinks.
  • Expose family-beside-project validation facts in JSON output and provide corrective guidance for moving or removing the family manifest.

Bug Fixes:

  • Prevent assembly roots from being reported compliant when their family manifest validator does not run.

Enhancements:

  • Preserve existing manifest-kind reporting for unaffected roots while combining family findings with unregistered-kind findings.
  • Document the new doctor finding in the README and handbook, including the refreshed README content hash.

Documentation:

  • Document that a family.yaml beside project.yaml is reported as a manifest-kinds finding.

Tests:

  • Add coverage for family manifests with missing, mismatched, invalid, and correctly pinned gitlinks, undeclared gitlinks, JSON/text parity, combined findings, and safe gitlink discovery.

… is a `manifest kinds` FINDING, INVALID (manifest-kinds), naming each member it pins with no gitlink, off its pin or with no 40-hex pin and each gitlink under members/ no row names, so a root its family gate refuses is no longer COMPLIANT, exit 0, with that file counted as validated (#211)

check_manifest_kinds read every kind:-bearing manifest against
MANIFEST_VALIDATORS and counted a family.yaml declaring family-manifest
as `validated` in whatever root it sat. Context reads ONE manifest per
root and project.yaml wins, so in an assembly root that kind's registered
validator, validate-family.py, never runs: the `family` check applies to
a holder only. A hand-copied or mis-merged family.yaml pinning two
members with no gitlink was COMPLIANT, exit 0, while validate-family.py
refuses the same tree member-gitlink-absent, exit 2.

- In an assembly root, a family.yaml declaring family-manifest is no
  longer counted `validated`. family_beside_project makes the row a
  FINDING (verdict INVALID (manifest-kinds), exit 1) whose reason says
  the file sits beside project.yaml and its validator never runs on an
  assembly root, then names every problem, not only the first:
  - each member row with no gitlink (mode 160000) at its path;
  - each member whose gitlink is not its pin, with both commits;
  - each member whose pin.commit is not 40 hex;
  - each gitlink under members_dir that no row names (gitlinks_under:
    the index at stage 0, read with -z; an empty, absolute or `..`
    directory lists nothing rather than every gitlink in the root).
  Paths and pins default exactly as member_state defaults them for a
  holder, and each gitlink is read with recorded_gitlink, the helper
  validate-family.py refuses by.
- It is a FINDING even when every member is recorded at its pin, and the
  reason says so ("though each is recorded at its pin today"): a family
  holder is not a project, and a manifest whose gate this root never runs
  is the case the row exists to report. The issue asked for no verdict
  better than INVALID.
- The next command offers both exits and decides neither: take
  family.yaml out of the root, or move the membership to a holder of its
  own (`family.py init`, then `family.py add` there), and never
  `family.py add` in the assembly root, which validate-family.py's own
  refusal would offer and which would mount the members inside a project.
- --json carries the facts as detail.family_beside: manifest, members
  (project, path, pin, gitlink, state at-pin | off-pin | no-pin |
  no-gitlink) and undeclared_gitlinks. The key is there only on such a
  root.
- Beside an unregistered kind, the two clauses and the two next commands
  are joined with "; ", the unregistered kind first.

Unchanged, byte-identical to main where measured (text and --json, one
standard path, only shape-doctor.py swapped): a scaffolded assembly root,
the same root with a contract of an unregistered kind, and a family
holder. On the roots that carry both files every other row and key is
identical too; only the manifest kinds row and the verdict move.

README.md: one clause in the doctor's row list, in the manifest kinds
parenthesis: "and so is a `family.yaml` beside `project.yaml`". The file
stays at its cap of 1416 lines: two phrases of the same paragraph are
tightened ("manifest in the root against" -> "manifest against", the
handbook's own wording; "a comparison and not a gate, because no leg
file" -> "a comparison and not a gate: no leg file") and nine lines are
re-wrapped. docs/handbook.html carries the same clause in its doctor
section, names #211 in its footer history, and pins the new README.md
blob.

Tests: nine in tests/test_shape_doctor.py, beside the family-holder
test: every member at its pin (deliberately INVALID), no gitlink, off
its pin, a pin that is not 40 hex, a gitlink no row names, the text
table and --json carrying the same row, beside an unregistered kind, the
row every root without such a file has always got, and gitlinks_under's
refusals.

Left out: running validate-family.py itself over an assembly root (the
issue's alternative fix), and checking a member's tree digest or id
there, since the row is a finding either way.

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-11T18:56Z) @codex review

@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 1 day and 5 hours 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 11, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR closes the assembly-root false-compliance gap by treating a family.yaml beside project.yaml as an INVALID manifest-kinds finding, inspecting all member gitlinks and exposing structured diagnostics, with focused regression tests and synchronized documentation.

Sequence diagram for family manifest diagnostics

sequenceDiagram
    participant Doctor as shape-doctor.py
    participant Context
    participant Index as Git index
    participant Output
    Doctor->>Context: check_manifest_kinds(ctx)
    Context->>Context: family_beside_project(ctx, data)
    loop each family member row
        Context->>Index: recorded_gitlink(root, path)
        Index-->>Context: gitlink or no gitlink
    end
    Context->>Index: gitlinks_under(root, members_dir)
    Index-->>Context: stage-0 gitlinks
    Context-->>Doctor: sentence and family_beside detail
    Doctor->>Output: Row FINDING INVALID manifest-kinds
    Output-->>Doctor: exit 1
Loading

Entity relationship diagram for family-beside diagnostics

erDiagram
    FAMILY_MANIFEST {
        string manifest
        string members_dir
    }
    MEMBER_ROW {
        string project
        string path
        string pin
        string state
    }
    GITLINK {
        string path
        string commit
    }
    FAMILY_MANIFEST ||--o{ MEMBER_ROW : pins
    MEMBER_ROW o|--o| GITLINK : records
    FAMILY_MANIFEST ||--o{ GITLINK : scans_under_members_dir
Loading

Flow diagram for family.yaml assembly-root finding

flowchart TD
    A[Assembly root] --> B{project.yaml and family.yaml present?}
    B -->|No| C[Existing manifest kinds behavior]
    B -->|Yes| D[family.yaml declares family-manifest]
    D --> E[Do not count as validated]
    E --> F[Inspect member rows with recorded_gitlink]
    F --> G[Scan index gitlinks under members_dir]
    G --> H[Build family_beside detail]
    H --> I[FINDING: INVALID manifest-kinds]
    I --> J[Exit 1]
Loading

File-Level Changes

Change Details Files
Detect and report a stray family manifest in an assembly root as an invalid manifest-kinds finding.
  • Added index-based gitlink discovery constrained to a safe members directory.
  • Compared each declared member’s path, pin, and recorded gitlink, reporting missing, mismatched, malformed, and undeclared entries.
  • Added dedicated JSON detail and actionable alternatives without recommending family membership inside the project.
  • Preserved existing behavior for normal assembly roots, family holders, and unrelated unregistered manifests.
shape-doctor.py
Add regression coverage for family manifests beside project manifests and gitlink edge cases.
  • Covered at-pin, missing-gitlink, off-pin, invalid-pin, undeclared-gitlink, combined-finding, text/JSON parity, and unchanged-root behavior.
  • Verified safe path handling and stage-0 index enumeration.
  • Confirmed the change fails the relevant new cases on the base implementation.
tests/test_shape_doctor.py
Update user-facing documentation and handbook metadata for the new finding.
  • Documented that family.yaml beside project.yaml is a manifest-kinds finding.
  • Updated handbook history and synchronized the README blob hash.
  • Kept README within the existing line cap and retained the handbook outline correspondence.
README.md
docs/handbook.html

Assessment against linked issues

Issue Objective Addressed Explanation
#211 Detect an assembly root that contains both project.yaml and a family.yaml declaring family-manifest, and report it as an INVALID manifest-kinds finding rather than COMPLIANT, without counting family.yaml as validated. ✅
#211 Identify the family membership problems that the unrun family validator would detect, including missing or mismatched gitlinks, invalid or non-40-hex pins, and undeclared gitlinks under the members directory, while exposing the relevant facts in text and JSON output. ✅
#211 Update the compliance documentation to state that a family.yaml beside project.yaml is a manifest-kinds finding. ✅

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

…in the root, and a test holds gitlinks_under to the -z and stage-0 reading its docstring names, so dropping either is no longer invisible to the suite (#211)

Fix round 1, from the independent review of #220 (rv-docs).

- README.md: "every `kind:`-bearing manifest in the root against a
  registry" again. The first commit's trim dropped "in the root", the
  README's only statement that this row stops at the root and reads
  neither the legs nor members/. Lines 1115-1129 are re-wrapped at 80
  columns. Main already has 18 lines of exactly 80 characters and 64
  longer ones. The paragraph stays 19 lines and README.md stays 1416. The handbook's #doctor section never
  said "in the root", main's included, so only its footer's `README.md
  blob` moves.
- tests/test_shape_doctor.py: test_gitlinks_under_reads_names_unquoted_and_stage_0_only.
  It stages members/<non-ASCII> at stage 0 and a conflicted
  members/Conflicted at stages 1, 2 and 3 with `update-index --index-info`
  fed as bytes, then asserts that gitlinks_under lists the first once, as
  itself, and the second not at all. Measured in a scratch worktree:
  without `-z` (split on newlines) it fails `assert [] == ['members/Ünï']`;
  without the stage-0 condition it fails with members/Conflicted listed.
  The other ten #211 tests passed under both mutations.

shape-doctor.py is unchanged.

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-11T19:27Z) fix round 1 (independent review, codex is out of quota): README.md says "every kind:-bearing manifest in the root against a registry" again (lines 1115-1129 re-wrapped at 80 columns, still 1416 lines; handbook footer blob re-taken as 20bb4a3e884c3232380407bda345af6e29d428c6; the page's #doctor section never carried "in the root", so only the blob moved); new test_gitlinks_under_reads_names_unquoted_and_stage_0_only (a non-ASCII members/ gitlink listed once as itself, a stage 1/2/3 conflicted one not listed; it fails under each of the -z and stage-0 mutations); PR body: stale ba93b1c row replaced by the current #207/#222 shas, and the contracts/family.yaml follow-up added to Left out; shape-doctor.py unchanged; targeted set 369 passed on 6091e6e.

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-2, 2026-10-11T19:30:24Z)

This lane reviewed PR #220 at 7fe2c56 read-only in its own detached worktrees; nothing authored, no claim.

MERGEABLE WITH NITS. No P1, P2 or P3. Sequencing: whichever of #220 and #222 lands second keeps both footer history clauses in docs/handbook.html and re-sets the README blob. Whichever of #220 and #205 lands second hand-merges four additive hunks in check_manifest_kinds.

Reproduced offline (Atlas with the InkRouter holder's family.yaml committed, both doctors run from one standard). Main says ok manifest kinds 6 manifest(s) ... and COMPLIANT (exit 0). Head names members/IRRS and members/IRSS with no gitlink and says INVALID (manifest-kinds) (exit 1). validate-family.py refuses the same tree, exit 2, member-gitlink-absent. Roots without such a file are byte-identical to main in text and --json, and the doctor still writes nothing.

Findings (shape-doctor.py at 7fe2c56):

Tests:

Merges (git merge-tree --write-tree):

Since this review the branch moved to 6091e6e, one commit: README.md's manifest-kinds line, and a test that holds gitlinks_under to its -z and stage-0 reading, which answers the second nit's stage-0 survivor. This lane did not re-run it.

Report and drivers: opensoft/brett-wip handoffs/xFactory/attachments/lane-openRepoShape-2/review220-7fe2c56/.

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-4, 2026-10-11T19:42:14Z) Landing note from lane openRepoShape-4: our PR #232 (#205) edits check_manifest_kinds too, and git merge-tree conflicts in four hunks there: the docstring, the locals (your beside, our elsewhere), the detail/condition block, and the FINDING return. The resolution is a pure union: keep both locals, keep your condition with our aside added, and return "; ".join(parts) + aside, "; ".join(fixes), detail. Put your docstring paragraph first, since its "THAT CASE TOO" points back at it. Our review applied that union in scratch against your 7fe2c56, and tests/test_shape_doctor.py passed 206. The two features never fire on the same root. We suggest this PR lands first, and lane 4 then merges main into #232 with the union. If #232 lands first instead, we have the mirror-image resolution ready for your branch.

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit eb6df75 into main Oct 11, 2026
5 checks passed
@brettheap
brettheap deleted the fix/doctor-reads-family-yaml branch October 11, 2026 23:09
brettheap added a commit that referenced this pull request Oct 11, 2026
…workflows-only

The handbook footer conflicted with #220 (#211), which landed first: the footer
keeps both history clauses (#211, then #207) and its `README.md blob` is re-taken
from the merged README (`git hash-object README.md`). README.md is 1416 lines,
AGENTS.md 464.

Lane: openreposhape-3 (openRepoShape-3)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 11, 2026
…b6df75) and this branch both edit check_manifest_kinds, resolved as their union, so a family.yaml beside an assembly root's project.yaml and a project.yaml that does not load each keep their own verdict (#205)

#220 (#211) landed on main as eb6df75 and #205 (this branch) both edit
`check_manifest_kinds` in shape-doctor.py, and git conflicts in four hunks
there: the docstring, the locals, the detail/condition block and the FINDING
return. Nothing else conflicts; tests/test_shape_doctor.py auto-merges with
no top-level name defined twice.

The resolution is the union of both sides:

- the docstring keeps both paragraphs, #220's first, because its "IS THAT
  CASE TOO" points back at the base paragraph just above it and would
  otherwise sit two paragraphs from its antecedent;
- both locals stay: this branch's `elsewhere` (the root's own manifest,
  when it does not load) and #220's `beside` (a family.yaml declaring
  `family-manifest` in an assembly root);
- this branch's `elsewhere` skip is kept in the loop, and its `aside` block
  comes before #220's `beside` detail, with #220's widened condition
  `unknown or unreadable or beside is not None`;
- the FINDING return is `"; ".join(parts) + aside, "; ".join(fixes), detail`,
  so the `aside` reaches the row on the finding path as it does on the OK one.

The two never fire on the same root. `elsewhere` needs the root's own
manifest to be one that does not load, and `beside` needs the root to be a
PROJECT with a family.yaml that loads and declares `family-manifest`; a root
whose project.yaml does not load beside a family.yaml reads as a FAMILY root
under #205's filename rule, so `beside` cannot fire, and a root whose
project.yaml loads has no `elsewhere`. Each feature keeps its own verdict.

The resolution was made by resolve_lane3.py, the union review205-50aa967.md
reviewed against #220 at 7fe2c56 and #205 at 50aa967, with one line changed
so the docstring paragraphs come in the order above (the script as written
emits this branch's paragraph first). #220's change to check_manifest_kinds
is byte-identical at 7fe2c56 and in the landed eb6df75.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This was referenced Oct 11, 2026
brettheap added a commit that referenced this pull request Oct 11, 2026
… 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)

`shape-doctor.py` keeps the `Refusal` that `load_yaml` raises for a manifest that is there and does not load, instead of reading it as "no manifest". A `project.yaml` or `family.yaml` that fails to load makes the root the kind its filename names: the verdict is `INVALID (manifest)` or `INVALID (family)`, exit 1; the `manifest` (or `family`) row is a FINDING that quotes the loader's own `REFUSED <code>: <detail>` line (from `Refusal.__str__`, never spelled a second time) with a NEXT naming the project's own validator on that file; every row that reads the manifest says `n/a` and waits on it (`waits_on` in `--json`), so the report names one fault, not a cascade; `--placement-plan` refuses with exit 3 and writes nothing; and no `adopt-project.py` advice or Triad row appears. A directory with neither file is still `NOT A SHAPE ROOT`, exit 2. `--help` is unchanged.

Before this, a merge conflict, a tab or a non-UTF-8 byte in an assembly root's `project.yaml` made the doctor report `NOT A SHAPE ROOT`, exit 2, with a NEXT telling the reader to `adopt-project.py plan` the assembly root in place and a Triad row calling it a single repository, while the project's own `validate-manifest.py` named the real fault.

#220 (#211, landed as eb6df75) edits `check_manifest_kinds` too. The branch merged main after it, and the four conflicting hunks are their union: both docstring paragraphs, #220's first; both locals; this change's `elsewhere` skip and `aside` block before #220's `beside` detail; the FINDING return carries the `aside`. The two never fire on the same root: `elsewhere` needs the root's own manifest not to load, and `beside` needs a project.yaml that loads beside a family.yaml declaring `family-manifest`.

tests/test_shape_doctor.py gains the #205 tests: project.yaml and family.yaml each broken three ways (conflict marker, tab, non-UTF-8 byte) in the text report, in `--json` and through the NEXT command; the absent-manifest cases; `--placement-plan`'s refusal; a manifest that declares its kind outranking one that does not load; and the NEXT held to the root's own validator as a path. On the merge with main c3f4987, tests/test_shape_doctor.py, tests/test_shape_advisory.py and tests/test_cli_reference.py: 261 passed.

Closes #205

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 12, 2026
…, #252 via #257, #195 via #226, #210 via #219, #211 via #220, #248 via #265, #212 via #223) into fix/adopt-record-submodule-placement

Brings in 06b5204 (#214), 60d08a1 (#217), 2727b08 (#209), 4b2add8 (#252),
aaa40eb (#195), d89fffa (#210), eb6df75 (#211), c3f4987 (#248) and
5751460 (#212) on top of 39ac59c. No rebase. 5751460 landed while the
merge of c3f4987 was being checked, and it changes adopt-project.py,
scripts/shape_materialize.py and tests/test_adopt_submodules.py, as this
branch does, so this one merge takes it too.

No conflicts. Both sides change adopt-project.py, scripts/repo_shape.py,
scripts/shape_materialize.py, shape-doctor.py, tests/test_adopt_submodules.py
and tests/test_shape_doctor.py. Every top-level definition only one side
changed is that side's text in the merge, byte for byte. The one thing
both changed is shape_materialize.py's imports, and both are kept: main's
`codecs` and this branch's `path_spelling_fault` and `PATH_*` codes.
Checked:

- adopt-project.py: main calls `escape_what_stdout_cannot_encode()` first
  in `main` (#214). It makes a `.gitmodules` git's submodule reader cannot
  read a finding where the tip holds no submodule and the history does, or
  where `ignore`, `update` or `branch` has no value (#195). And every path
  a finding names is now `_spelled` (#212). This branch's
  `submodule_record`, `_resolution_text` and the record's preamble are
  unchanged by the merge, and so are `_answered_legs`, `_kept_in_a_leg`,
  `_commit_the_split`, `_refuse_what_check_finds`, `cmd_execute` and `Plan`.
  The record and #195's finding read the kept submodules the same way,
  `_kept_in_a_leg(_answered_legs(...))` over the tip's gitlinks. `execute`
  refuses #195's finding in `_refuse_what_check_finds`, before any leg
  exists and before `_commit_the_split` writes the record. So a source
  #195 refuses gets no record, and so does a source whose tip holds no
  submodule.
- scripts/repo_shape.py: main refuses an explicit key in a flow mapping
  and reads an empty flow-mapping value as null (#252). A block scalar's
  record is now a (key, body) pair, and `_BLOCK_SENTINEL` is gone (#248);
  nothing on this branch names it. The record is a block sequence of
  double-quoted scalars, and it reads back through the merged reader with
  0 mismatches (`submodule_record`, then `parse_yaml`, path and
  resolution):
  - 4,525 names, plus 35 more;
  - 240 aimed at #252 and #248 (`? a`, `{a: }`, `|`, `a: |`, a NUL, the
    old sentinel).
- scripts/shape_materialize.py: main adds `escape_what_stdout_cannot_encode`,
  `run()` reading UTF-8 (#217), and `first_and_more(paths, spell)` (#212).
  This branch's `LEG_SEGMENT_REASONS` and `_spelling_reason` are untouched.
- shape-doctor.py: main adds the `manifest kinds` finding for a
  `family.yaml` beside `project.yaml` (#211), and the stdout escaping.
  This branch's placement row and `recorded_submodules` are untouched.
- tests: #212 respells the finding text that the earlier tests in
  test_adopt_submodules.py assert. None of this branch's tests quotes a
  finding.
- No top-level name is bound twice in adopt-project.py,
  scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py,
  shape-doctor.py, validate-manifest.py or update-shape.py. Each merged
  module's names are the base's, plus what either side added, minus what
  either side removed. No side added a name the other side also added,
  in the tests either.
- AGENTS.md is untouched by main: 472 lines, under the cap of 472.
- The branch's diff against main is still #189 alone: 12 files, +1670
  -156, file by file the same as against 39ac59c.

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
…, #209, #252, #195, #210, #211, #248, #212, #251 and #206 merge with one hunk, the materializer import list, kept on both sides, and `_entry_findings` still hands `_leg_findings` the tree and ends with the root-symlink findings

origin/main moved 39ac59c -> 60588d5: 06b5204 (#214 via #246), 60d08a1
(#217 via #230), 2727b08 (#209 via #218), 4b2add8 (#252 via #257),
aaa40eb (#195 via #226), d89fffa (#210 via #219), eb6df75 (#211 via
#220), c3f4987 (#248 via #265), 5751460 (#212 via #223), 23e16ae (#251
via #271) and 60588d5 (#206; its subject names no pull request).

Conflict, resolved by keeping both sides:
- adopt-project.py, the `shape_materialize` import list. This branch
  adds BOOTSTRAP, SYMLINK_HOPS and template_files; #214 adds
  escape_what_stdout_cannot_encode. The resolution is the union.

Auto-merged: scripts/shape_materialize.py, where #214's escaping, #217's
UTF-8 `run()` and #212's `first_and_more(paths, spell=None)` sit apart
from this branch's #183 section, and adopt-project.py's other hunks.
#195 adds `import os` at the line where this branch added it, and the
merge keeps it once.

Checked on the merged tree:
- `_entry_findings` still calls `_leg_findings(plan, tree)` and ends
  with `_root_symlink_findings(plan, source, tree)`. `main` still calls
  `escape_what_stdout_cannot_encode()` first.
- No top-level name is bound twice in adopt-project.py,
  scaffold-project.py, scripts/repo_shape.py or
  scripts/shape_materialize.py, and none is loaded but bound nowhere
  (AST). `first_and_more` gives its new `spell` a default, so this
  branch's call is unchanged.
- Both sides are intact by +/- line sets: each side's change against the
  base reappears against the other side, apart from the import hunk and
  the shared `import os`.
- #212 says this branch's `plan-root-symlink` names its paths raw, and
  that whichever pull request lands second should spell them. That was
  04792d2. Since d11cf9f every path the finding names goes through
  `_spelled`.

One clash the textual merge does not show is fixed in the next commit:
#217's tests/test_git_output_decoding.py refuses a shipped text-mode
`subprocess` call that names no codec, and this branch's
`_checks_out_symlinks` probe names none.

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
…e-notice

It merged with no conflict. Main brings nine commits since 39ac59c:

- 06b5204, six tools escape a name their stdout cannot encode (#214
  via #246), with tests/test_stdout_encoding.py;
- 60d08a1, `shape_materialize.run()` and 26 other git and gh calls
  read the child's output as UTF-8 (#217 via #230), with
  tests/test_git_output_decoding.py;
- 2727b08, update-shape.py's `gh pr create --base` (#209 via #218),
  with tests/test_update_shape_pr_base.py;
- 4b2add8, scripts/repo_shape.py's flow mappings (#252 via #257), with
  tests/test_yaml_flow_mappings.py;
- aaa40eb, adopt-project.py refuses a `.gitmodules` the history's
  gitlinks make unreadable (#195 via #226), with
  tests/test_adopt_history_gitlinks.py;
- d89fffa, family.py `bump` to the current pin (#210 via #219);
- eb6df75, shape-doctor.py's family.yaml beside a project.yaml (#211
  via #220);
- c3f4987, scripts/repo_shape.py's block-scalar records (#248 via
  #265), with tests/test_yaml_block_scalar_keys.py;
- 5751460, adopt-project.py spells every path a FINDING or a refusal
  names (#212 via #223), with tests/test_adopt_finding_spelling.py.

Three files changed on both sides: adopt-project.py,
tests/test_adopt_ignored_shape_files.py and tests/test_adopt_verify.py.
In each, `git diff -U0` of this merge against 5a8b649 equals main's
diff against 39ac59c, and this merge against 5751460 equals the
branch's diff against 39ac59c, apart from the hunk headers. Neither
side's lines are changed.

How main's changes meet this branch's failure handling:

- Every definition the late failures go through is byte-identical to
  5a8b649's: LATE_FAILURES, the notice (`_nothing_rolled_back`),
  `_failed_loudly`, `_report_late`, the three builders' handlers,
  `_verify` and `cmd_execute` among them. Three definitions differ, each
  by main's hunk alone.
- `main()` gains `escape_what_stdout_cannot_encode()` as its first
  statement (06b5204). The branch writes every REFUSED block and notice
  to stderr, which Python opens with `backslashreplace` whatever the
  locale.
- `run()` names its codec (60d08a1). Output that is not UTF-8 now
  reaches the branch's handlers as `CommandFailed`, not as
  `UnicodeDecodeError`. LATE_FAILURES keeps `UnicodeError`, because
  `git_out` in scripts/repo_shape.py still decodes strictly.
- `Source.registered_submodules` reads the history (aaa40eb). Its one
  caller in `execute` is `_refuse_what_check_finds`, before
  `_create_leg_remotes`, so the new refusal comes before any leg
  repository exists and needs no notice.
- 5751460 spells the paths of the verification's findings. The
  branch's footer and notice follow those findings unchanged.

No top-level name is bound twice in adopt-project.py,
scaffold-project.py, scripts/repo_shape.py or
scripts/shape_materialize.py, and a `symtable` check finds no name
they read that nothing binds. On the merged tree, ten test files give
439 passed and 1 skipped (Windows' own stdout default): the two shared
ones, main's four new ones that read adopt-project.py
(finding_spelling, history_gitlinks, stdout_encoding,
git_output_decoding), and test_adopt_late_failure,
test_adopt_filter_repo_failure, test_adopt_execute_refusals and
test_adopt_plan_escapes.

Lane: openreposhape-1 (openRepoShape-1)
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