Repository navigation
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
Conversation
… 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>
|
(lane openRepoShape-3, 2026-10-11T18:56Z) @codex review |
|
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 |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Reviewer's GuideThe 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 diagnosticssequenceDiagram
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
Entity relationship diagram for family-beside diagnosticserDiagram
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
Flow diagram for family.yaml assembly-root findingflowchart 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]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
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>
|
(lane openRepoShape-3, 2026-10-11T19:27Z) fix round 1 (independent review, codex is out of quota): README.md says "every |
|
(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 Reproduced offline (Atlas with the InkRouter holder's Findings (shape-doctor.py at 7fe2c56):
Tests:
Merges (
Since this review the branch moved to 6091e6e, one commit: README.md's manifest-kinds line, and a test that holds Report and drivers: |
|
(lane openRepoShape-4, 2026-10-11T19:42:14Z) Landing note from lane openRepoShape-4: our PR #232 (#205) edits |
|
…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>
…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>
… 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>
…, #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>
…, #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>
…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>



Lane: openreposhape-3 (openRepoShape-3)
Claim: #211 (comment)
What
check_manifest_kindscounted afamily.yamlthat declaresfamily-manifestasvalidatedwhatever root it sat in.Contextreads ONE manifest per root andproject.yamlwins, so in an assembly root nothing ever runs that kind's registered validator,validate-family.py: thefamilycheck applies to a holder only. A hand-copied or mis-mergedfamily.yamlpinning two members with no gitlink wasCOMPLIANT, exit 0, whilevalidate-family.pyrefuses the same tree withmember-gitlink-absent, exit 2.family.yamldeclaringfamily-manifestis no longer countedvalidated.family_beside_projectmakes themanifest kindsrow aFINDING(verdictINVALID (manifest-kinds), exit 1). Its reason says the file sits besideproject.yamland that its validator never runs on an assembly root, then names every problem, not only the first:pin.commitis not 40 hex;members_dirthat no row names.gitlinks_underreads 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_statedefaults them for a holder, and each gitlink is read withrecorded_gitlink, the helpervalidate-family.pyrefuses by.FINDINGeven 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 thanINVALID.family.yamlout of the root, or move the membership to a holder of its own (family.py init, thenfamily.py addthere). It never offersfamily.py addin the assembly root, which is whatvalidate-family.py's own refusal would offer: that would mount the members inside a project.--jsoncarries the facts asdetail.family_beside:manifest,members(project,path,pin,gitlink,state:at-pin|off-pin|no-pin|no-gitlink) andundeclared_gitlinks. The key appears only on such a root.;, the unregistered kind first.contracts/*.yamlnothing validates is a finding by name rather than a silence, and so is afamily.yamlbesideproject.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.mdshows only those two edits. Nothing in another paragraph moves, and the cap is not raised. Fix round 1 restored "in the root" ("everykind:-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.htmlfollows the README. Its doctor section gets the same clause. Its footer history gets "afamily.yamlbesideproject.yamlas a manifest kinds finding added (shape-doctor.py reads onlyproject.yamlat a root that also carries afamily.yaml, so an assembly root whosefamily.yamldeclares two unmounted members is COMPLIANT, and itsmanifest kindsrow says that kind has a registered validator, which is never run #211)", and the footer'sREADME.md blobis nowgit 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
InkRouterholder is made withfamily.py init+addIRRS, IRSS. Each "beside" fixture is an Atlas clone with the holder'sfamily.yamlcommitted. Gitlinks are recorded withgit update-index --cacheinfo 160000,…and committed over empty mounts. Both doctors ran from ONE standard path with onlyshape-doctor.pyswapped, andghwas a logging shim that exits 1, so the run stayed offline. Base:maina5b09c3, the current tip, with fixtures built from a5b09c3. My predecessor measured 20e13f0 and 8df21e6 and got the same results.shape-doctor.pyis byte-identical at all three (git diff --quiet 8df21e6 a5b09c3 -- shape-doctor.py). Head: this branch. The last column is the holder template'svalidate-family.py --pinsrun on the same tree.validate-family.py --pinsok 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)REFUSED member-gitlink-absentok/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)REFUSED member-uninitializedok/COMPLIANT (exit 0)FINDING …: members/IRRS records gitlink d6a081bd79f5 where family.yaml pins 46db9573d2b7/INVALID (manifest-kinds) (exit 1)REFUSED member-uninitializedpin.commit: "main"ok/COMPLIANT (exit 0)FINDING …: IRRS's row carries no 40-hexpin.committo compare against/INVALID (manifest-kinds) (exit 1)REFUSED member-pin-commitmembers/Strayok/COMPLIANT (exit 0)FINDING …: members/Stray is a gitlink no family.yaml row names/INVALID (manifest-kinds) (exit 1)REFUSED member-uninitializedCOMPLIANT (exit 0)--jsonbyte-identical to main (cmp)contracts/inventory.yaml(kind: service-inventory)INVALID (manifest-kinds) (exit 1)--jsonbyte-identical to main (cmp)COMPLIANT (exit 0)--jsonbyte-identical to main (cmp)On the five beside roots, every other row and every other
--jsonkey is identical to main. Only themanifest kindsrow 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 -qwith git-filter-repo 2.47.0 on PATH:Ten new tests in
tests/test_shape_doctor.py, placed right aftertest_a_family_holder_gets_the_family_rows:INVALID, and the test says why;members/that no row names;--jsoncarrying the same row;gitlinks_under's refusals;gitlinks_under's-zand stage-0 reading (fix round 1). A gitlink at a non-ASCIImembers/path is listed once, as itself. A gitlink conflicted at stages 1, 2 and 3 is not listed. Both are staged withupdate-index --index-infofed as bytes. Measured by mutation: without-zthe test fails withassert [] == ['members/Ünï'], without the stage-0 condition it fails withmembers/Conflictedlisted, and the other ten shape-doctor.py reads onlyproject.yamlat a root that also carries afamily.yaml, so an assembly root whosefamily.yamldeclares two unmounted members is COMPLIANT, and itsmanifest kindsrow says that kind has a registered validator, which is never run #211 tests pass under both mutations.I ran the new tests against main's
shape-doctor.py: a scratch worktree at a5b09c3 with this branch'stests/test_shape_doctor.pycopied in,-kselecting the ten plustest_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 withgit update-index --cacheinfoand pass the Windows spellings (\members,C:/members, the absolute root) togitlinks_underexplicitly.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 touchesshape-doctor.py,tests/test_shape_doctor.py,README.mdordocs/handbook.html, plus the two my brief named:origin/fix/doctor-unparsable-manifest(#205, lane 4)shape-doctor.py: four hunks, all incheck_manifest_kindsorigin/fix/bump-leg-tracked-workflows-only(#207, PR #222)docs/handbook.html: the footer's history line and itsREADME.md blobonly. README.md merges clean to 1416 linesfix/bump-leg-tracked-workflows-only(#207, local, its round in progress)docs/handbook.html: the same footer hunk. README.md merges clean to 1416 linesfix/family-add-member-case(#206), local = pushedfix/decode-git-output-utf8, local = pushedorigin/fix/adopt-record-submodule-placementfix/plan-report-stdout-encoding(local)Main moved after the test run, from a5b09c3 to 54aa69a (#165 / #173:
adopt-project.pyand two adopt test files, none of them this branch's files).git merge-tree --write-tree origin/main HEADis 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 blobmust then be re-set togit hash-object README.mdof the MERGED README. Neither branch's hash is right after the merge, andtest_the_handbook_follows_the_readmes_outlinefails until the hash is re-set.#205: whichever of the two lands second merges by hand, keeping both sides. Every hunk is additive:
project.yamldoes not parse (a merge conflict) NOT A SHAPE ROOT, exit 2, and its NEXT line tells the reader to adopt that assembly root in place while the Triad row calls it a single repository #205's "THE ROOT'S OWN MANIFEST, WHEN IT DOES NOT LOAD" and this branch's "A REGISTERED KIND WHOSE GATE THIS ROOT NEVER RUNS");project.yamldoes not parse (a merge conflict) NOT A SHAPE ROOT, exit 2, and its NEXT line tells the reader to adopt that assembly root in place while the Triad row calls it a single repository #205'selsewhereand this branch'sbeside;project.yamldoes not parse (a merge conflict) NOT A SHAPE ROOT, exit 2, and its NEXT line tells the reader to adopt that assembly root in place while the Triad row calls it a single repository #205'sreported_byandaside, and this branch'sfamily_beside;if unknown or unreadable or beside is not None:;FINDINGreturn as"; ".join(parts) + aside, "; ".join(fixes), detail.The two cases cannot meet on one root. Under #205, a root whose
project.yamldoes not load and whosefamily.yamldeclaresfamily-manifestis a FAMILY root, and this branch acts only on a PROJECT root. No assertion in #205's tests reads afamily.yamlbeside a goodproject.yaml. #206'sFamily.holdingis a row lookup inscripts/family.py, whichshape-doctor.pydoes not import. The helpers here list index gitlinks and compare rows withrecorded_gitlink, so nothing duplicates it.Left out
shape-doctor.pycould run the registered validator for every declared kind it finds in a root (the issue's alternative fix) instead of reading a strayfamily.yaml's members against the gitlinks itself.idare not checked in an assembly root, because the row is a finding either way.validate-family.pychecks both in a holder.contracts/family.yamldeclaringkind: family-manifestin an assembly root is still countedvalidatedand its row isok: this branch acts only on the rootfamily.yaml, the file the issue names. The general rule would count a kind asvalidatedonly when itscovered_bycheck applies to the root's kind. The reviewer read the mirror case from the code but did not run it: acontracts/*.yamldeclaringproject-manifestin 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:
family.yamlbesideproject.yamlas an invalid manifest-kinds finding, including member pin and gitlink discrepancies and undeclared member gitlinks.Bug Fixes:
Enhancements:
Documentation:
family.yamlbesideproject.yamlis reported as a manifest-kinds finding.Tests: