Skip to content

shape-doctor.py: a project.yaml or family.yaml that is there and does not load is INVALID (manifest) or INVALID (family), exit 1, quoting load_yaml's own refusal and naming the project's own validator, instead of NOT A SHAPE ROOT, exit 2, with an adopt-project.py plan NEXT and a Triad row calling an assembly root a single repository (#205) - #232

Merged
brettheap merged 4 commits into
mainfrom
fix/doctor-unparsable-manifest
Oct 11, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-4 (openRepoShape-4)
Claim: #205 (comment)

Fixes #205.

What

shape-doctor.py now 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 and whose NEXT names 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;
  • no adopt-project.py advice appears anywhere except the machine row's own git-filter-repo fact, and there is no Triad row calling the root a single repository.

A directory with neither file is still NOT A SHAPE ROOT, exit 2, naming both ways in. --help is unchanged, so docs/cli.md is too.

The branch has two commits: 9f2e6c0 (the doctor and 14 tests) and 50aa967 (tests only). 9f2e6c0's subject starts WIP (#205):, and the no-force-push ruleset keeps it that way; this PR's title is the squash subject.

Why (repro on main)

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 saying "This is a single repository." Meanwhile the project's own validate-manifest.py named the real fault. The issue has the full repro; the review below re-ran it against main (NOT A SHAPE ROOT (exit 2)) and this head (INVALID (manifest) (exit 1), quoting REFUSED yaml-unparsable: …/project.yaml: line 77: …).

Tests

macOS and Windows have not run these tests yet; this PR's CI is the first time.

Not changed here (found by the review, both already on main)

  • A two-shape-update merge that conflicts contracts/shape-pin.yaml as well as project.yaml still reads CANNOT ANSWER, exit 3: check_shape_currency takes the project's own unparsable pin file for a fault of this checkout, and that verdict outranks INVALID. The rows are right; the verdict is not. On main the same tree reads NOT A SHAPE ROOT. A follow-up issue will carry it.
  • When a root has no validator copy of its own, the NEXT names the standard's template validator, which exits 1 with ModuleNotFoundError if pasted. main's validator row does the same through the shared validator_for, so the fix belongs there. A follow-up issue will carry it.
  • The review's three test and docstring nits will follow as one more commit on this branch, together with anything CI or review turns up.

Landing notes

🤖 Generated with Claude Code

Summary by Sourcery

Treat present but unloadable manifests as invalid roots, report the loader's refusal and correct validator, and prevent dependent checks from producing misleading results.

New Features:

  • Classify present but unloadable project.yaml and family.yaml files as invalid roots with filename-derived kinds and loader refusal details.
  • Expose manifest dependencies in reports by marking dependent checks as n/a and linking them to the owning manifest finding.
  • Name the root's own manifest validator as the remediation command for unloadable manifests.
  • Refuse placement-plan generation when the manifest cannot be loaded.

Bug Fixes:

  • Distinguish unreadable or unparsable manifests from absent manifests, preserving NOT A SHAPE ROOT only for directories with neither manifest.
  • Prevent duplicate diagnostics, adoption guidance, and misleading Triad advice for roots with unloadable manifests.

Enhancements:

  • Preserve manifest kind declarations as higher priority than unloadable neighboring manifests.
  • Add structured JSON metadata for refusal details and rows waiting on the manifest.

Documentation:

  • Update shape-doctor exit-code and behavior documentation for unloadable manifests.

Tests:

  • Add coverage for broken project.yaml and family.yaml files across text, JSON, validator remediation, placement-plan, absent-manifest, and precedence scenarios.

brettheap and others added 2 commits October 11, 2026 16:39
…ere and does not load is INVALID (manifest) or INVALID (family), exit 1, quoting load_yaml's own refusal and naming the project's own validator, instead of NOT A SHAPE ROOT, exit 2, with an adopt-project.py plan NEXT and a Triad row calling an assembly root a single repository (#205)

Context.__init__ read each manifest through _read, which turned every
load_yaml Refusal (yaml-unparsable, yaml-unreadable) into None, so a
manifest that was there and did not load (a merge conflict, a tab, a
non-UTF-8 byte) read exactly like a missing one. Now each manifest is
loaded once through _load, which keeps the refusal. `kind:` still
decides first, so no root that declared a kind reads differently. Only
where no file that loads declares one does the filename decide: the
root is PROJECT (or FAMILY), its manifest is None, and
manifest_refusal holds the refusal.

The manifest (family) row is then a FINDING that quotes
`REFUSED <code>: <detail>` verbatim and untruncated. That is the line
the project's own validator prints about the same file through the
same loader. The row does not repeat the refusal's default
remediation, which points at git submodule update. Its NEXT is the
gate validator_for picks (the project's own copy first) plus the
file to fix. Which side of a conflict wins is left to the person.

naming, pins (validate-pins.py reads project.yaml before any pin),
legs, leg shape files, placement and members wait on that row as
n/a with detail.waits_on. Before, they answered questions they
could not ask: "declares no non-assembly leg", and "no members yet"
as an ok. manifest kinds no longer lists the same file a second time
under "remove the file from the root". The kind line says the
manifest does not load. --placement-plan refuses by name, exit 3,
and writes nothing. --help is unchanged, so docs/cli.md is too.

WIP: the code and the tests are complete and the 14 new tests pass
(11 of them fail against 20e13f0's doctor). The brief's targeted run
(test_shape_doctor.py whole, test_shape_advisory.py,
test_manifest_validator.py, test_load_yaml_refusals.py,
test_cli_reference.py) has not run yet. Rename the subject once it
passes.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…to all three breakages an assembly root is, in text and in --json, no report of an unloadable manifest names adopt-project.py anywhere but the machine row's own git-filter-repo fact, that row is the row the root had before it broke, and an absent manifest is still NOT A SHAPE ROOT, exit 2, in the text report too (#205)

9f2e6c0 made the doctor read a project.yaml or family.yaml that is
there and does not load as the kind its filename names, INVALID, exit
1. Its tests broke project.yaml three ways (a merge conflict, a tab, a
non-UTF-8 byte) and family.yaml one way. This commit adds tests only;
shape-doctor.py is unchanged.

family.yaml now breaks all three ways (FAMILY_BREAKAGES). The conflict
is in the last commit: line, a member's pin. The byte goes into the
family's name. Each case checks the same verdict and rows as --json in
the text report: every reason and every NEXT verbatim, the kind line
naming the family row, and none of NOT A SHAPE ROOT, single
repository, the way in, what is here, remove the file or git
submodule.

no_adoption_offered is strict now. Before, it looked for the phrase
"adopt-project.py plan" and skipped the machine row. Now the name
appears nowhere: not in the text report outside the machine row's
own line, not in any row's reason, next or detail, and not in the
payload's top-level fields. The machine row may name it only while
git-filter-repo is missing. That clause is a fact about the
workstation, printed in a COMPLIANT report as well, and the Windows CI
job has no git-filter-repo by design. Holding the machine row to it as
well would turn that job red over a sentence #205 must leave alone.

machine_row_unchanged holds that row to the row the same root had
before its manifest broke, in the project and family --json tests.
The id, label, status, preflight path and git-filter-repo path must
match. When the preflight's exit and report lines also match, the
whole row must match. The preflight asks gh, which answers from its
host, so two runs it answered differently are compared only on what
does not come from it.

The absent-manifest case also checks family_yaml false and no
assembly-root row. It runs the text report too: kind line, NOT A
SHAPE ROOT (exit 2), the way-in NEXT. Its two cases name both ways in,
adopt-project.py plan for a git repository and setup.sh
(setup-project.py on Windows) for a plain directory, each where it
applies.

Tested with git-filter-repo 2.47.0 on PATH: the brief's targeted run
(test_shape_doctor.py, test_shape_advisory.py,
test_manifest_validator.py, test_load_yaml_refusals.py,
test_cli_reference.py) was 298 passed on 9f2e6c0 and 300 passed (one
pytest warning: a garbage temp directory it could not delete) on this
commit. The 16 #205 tests also pass without git-filter-repo on
PATH. Against 20e13f0's doctor 13 of them fail; the 3 that pass
(absent twice, kind-outranks) guard what must not change. --help is
unchanged, so docs/cli.md is too.

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

sourcery-ai Bot commented Oct 11, 2026

Copy link
Copy Markdown

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

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

@brettheap

Copy link
Copy Markdown
Contributor Author

@codex review

@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 doctor now distinguishes an absent manifest from a present manifest that load_yaml refuses: such roots remain classified by filename, report a single INVALID finding with the loader’s refusal and the owning validator as the next step, suppress dependent cascades and adoption guidance, and are covered by extensive project/family regression tests.

Sequence diagram for unloadable manifest reporting

sequenceDiagram
    participant Doctor as shape-doctor.py
    participant Loader as load_yaml
    participant Validator as project validator
    participant Report as report

    Doctor->>Loader: load_yaml(manifest_path)
    Loader-->>Doctor: Refusal(code, detail)
    Doctor->>Doctor: store manifest_refusal
    Doctor->>Validator: validator_for(ctx, own, template)
    Validator-->>Doctor: owning validator path
    Doctor->>Report: unloaded_manifest_row(ctx, check_id, label, own, template)
    Report-->>Report: INVALID finding with REFUSED code and detail
    Report-->>Report: dependent rows are n/a with waits_on manifest or family
    Report-->>Report: suppress adoption and Triad guidance
Loading

Flow diagram for manifest refusal classification

flowchart TD
    A[Context loads project.yaml and family.yaml once] --> B{Valid kind declaration found?}
    B -->|Yes| C[Classify by declared kind]
    B -->|No| D{Manifest refusal found?}
    D -->|Yes| E[Classify by filename kind]
    E --> F[Store manifest_refusal]
    D -->|No| G[NOT A SHAPE ROOT]
    C --> H[Normal validation]
    F --> I[INVALID manifest or family]
    I --> J[Manifest row quotes REFUSED code and detail]
    I --> K[Dependent rows are n/a and wait on manifest row]
    I --> L[Placement plan refuses without writing]
    G --> M[Exit 2]
    I --> N[Exit 1]
Loading

File-Level Changes

Change Details Files
Treat present but unloadable project.yaml or family.yaml files as invalid roots rather than absent manifests.
  • Load each present manifest once while preserving load_yaml Refusal details.
  • Use the filename-derived root kind only when no loaded manifest declares a kind.
  • Return INVALID (manifest) or INVALID (family) with exit 1 and quote the loader refusal in the owning row.
  • Keep a valid manifest declaration authoritative over an unloadable stray manifest.
shape-doctor.py
tests/test_shape_doctor.py
Make the report identify one manifest fault and suppress dependent checks and misleading remediation.
  • Add a shared validator selector for the owning validator or standard template.
  • Add manifest/family finding rows with refusal data and a validator NEXT command naming the file to fix.
  • Mark manifest-dependent rows n/a with JSON waits_on metadata.
  • Exclude the owning unloadable file from duplicate manifest-kind findings.
  • Prevent adoption advice and Triad/single-repository messaging for unloadable manifests.
shape-doctor.py
tests/test_shape_doctor.py
Define placement-plan behavior and preserve existing absent-manifest behavior.
  • Refuse placement plans with exit 3 and do not write output when placement depends on an unloadable manifest.
  • Keep directories with neither manifest as NOT A SHAPE ROOT with exit 2 and the existing way-in guidance.
  • Document the new exit-1 condition without changing CLI help.
shape-doctor.py
tests/test_shape_doctor.py
Add comprehensive regression coverage for malformed project and family manifests.
  • Cover merge conflicts, invalid indentation, and non-UTF-8 bytes in text, JSON, and validator NEXT flows.
  • Verify refusal codes/details, waiting rows, validator commands, machine-row stability, and absence of duplicate guidance.
  • Cover absent manifests, kind precedence, and placement-plan refusal.
tests/test_shape_doctor.py

Assessment against linked issues

Issue Objective Addressed Explanation
#205 Distinguish a manifest that is present but unreadable from an absent manifest, treating an unreadable project.yaml or family.yaml as the root kind indicated by its filename. ✅
#205 Report an unreadable root manifest as INVALID (manifest) or INVALID (family), exit 1, preserving and quoting load_yaml's refusal and directing the user to the appropriate project validator rather than suggesting adoption. ✅
#205 Prevent misleading follow-on diagnostics for unreadable manifests, including NOT A SHAPE ROOT, adoption guidance, Triad/single-repository advice, duplicate manifest-kind findings, and manifest-dependent checks; ensure placement plans refuse without writing output while preserving absent-manifest behavior. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

…REFUSED line from Refusal.__str__ instead of spelling it a second time, the tests hold their NEXT to the root's own validator as a path and not as a substring, and the `conflicted` docstring claims only the one-conflicted-manifest case it builds (#205)

The review of 50aa967 (MERGEABLE WITH NITS) left three nits for this
branch; this commit is all three, and the report text is byte-identical.

N1. The NEXT tests asserted that `scripts/validate-manifest.py --root`
(`validate-family.py --root`) appears in the command, and the standard's
template copy contains that string too: a NEXT that named the template
passed. Mutant M5 (`used = ctx.shape / template`) was killed only by
accident, through the template's `ModuleNotFoundError: No module named
'repo_shape'` when the command was pasted, and would have survived once
that is fixed. `assert_next_runs_the_roots_own` compares the words as
paths, both sides resolved so a symlinked temporary directory (macOS) or a
`\` spelling (Windows) is one path: the NEXT's validator is
`<root>/scripts/<script>`, its `--root` argument is the root, and
`detail.validator` is the same file. It stands in the three places that
checked the substring (the `--json` report, the pasted-command test, the
family holder), and M5 now fails each of the nine cases on that assertion,
before any command runs.

N2. shape-doctor.py spelled `REFUSED {code}: {detail}` a second time, next
to `Refusal.__str__`, which is where the loader and every validator print
it. `refusal_line` takes it from there: `str(refusal)` less the
`\n<remediation>` trailer, so the quoted line has one definition. It is
not the text up to the first newline, which would cut a refusal whose
`detail` holds one: `detail` opens with the path the loader was given, and
a root directory with a newline in its name would be quoted short, with
no error to say so. A new test pins it for the loader's default remedy, a
one-line remedy, a several-line remedy, and a path with a newline in it;
the first-line and whole-`str()` mutants both fail it. The doctor's text
and `--json` output on a project and a family holder broken two ways each
(a merge conflict, a non-UTF-8 byte) is identical before and after, byte
for byte.

N3. The `conflicted` docstring named "two shape updates in an assembly
root" as the case it builds. That case also conflicts
`contracts/shape-pin.yaml` and reads CANNOT ANSWER, not INVALID (review
S1, a defect on main, filed apart). It now says what it does build: one
conflicted manifest and nothing else in the tree, and says nothing about
a tree where other files conflict too.

Run, over these bytes (sha256 recorded first): tests/test_shape_doctor.py
tests/test_load_yaml_refusals.py tests/test_cli_reference.py, with
git-filter-repo 2.47.0 on PATH: 222 passed, no skips, exit 0.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-4, 2026-10-11T19:24:40Z) Review round 1 is in: 1c9282e, one commit on 50aa967, answering the adversarial review's three nits.

  • N1: the NEXT tests now assert that the command's validator is exactly <root>/scripts/validate-manifest.py (and the family validator for a holder), with --root the root, compared through resolved paths. The review's mutant that names the template validator is now caught by that assertion in all 9 cases; before, it survived the --json test.
  • N2: the quoted REFUSED <code>: <detail> line is the Refusal's own str() less its remediation (refusal_line). That keeps a detail holding a newline whole, where taking the first line would have cut it short. The doctor's text and --json output on four broken roots (project and family, a conflict and a non-UTF-8 byte each) are byte-identical before and after.
  • N3: the conflicted fixture's docstring now says what it builds, and no longer claims the two-shape-update case, which (with the pin file conflicted too) still reads CANNOT ANSWER and goes to a follow-up issue.

tests/test_shape_doctor.py tests/test_load_yaml_refusals.py tests/test_cli_reference.py: 222 passed. The output is unchanged, so docs/cli.md is too.

@brettheap

Copy link
Copy Markdown
Contributor Author

@codex review

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

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-4, 2026-10-11T19:28:19Z) The two pre-existing defects this description says follow-up issues will carry are now filed and re-verified on main 54aa69a: #237 (any refusal over the root's own contracts/shape-pin.yaml, a merge conflict among them, reads CANNOT ANSWER, exit 3, with a fetch --all NEXT that blames the standard) and #238 (the template-validator fallback's NEXT dies with ModuleNotFoundError on the manifest, pins and family rows). Neither is changed by this PR.

…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>
@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit d94d155 into main Oct 11, 2026
5 checks passed
@brettheap
brettheap deleted the fix/doctor-unparsable-manifest branch October 11, 2026 23:54
This was referenced Oct 12, 2026
brettheap added a commit that referenced this pull request Oct 12, 2026
…, #272 via #274, #205 via #232) into fix/adopt-record-submodule-placement

Brings in 23e16ae (#251), 60588d5 (#206), 018ce0d (#207), 67a398a (#272)
and d94d155 (#205) on top of 5751460. No rebase. The first four change
none of this branch's files. d94d155 changes shape-doctor.py and
tests/test_shape_doctor.py, as this branch does.

One conflict, in shape-doctor.py's module docstring, under EXIT CODES.
Both sides added a clause to exit 1's list of findings: #205 "a manifest
is there and does not load", and #189 "or, for a submodule, a leg other
than the one `project.yaml` records for it". Both are kept: #205's in its
place in the list, and #189's after the path-policy clause it qualifies.
Only the docstring's first line reaches `--help`, so docs/cli.md is
unchanged.

Merged without a conflict, and checked:

- `check_placement`: #205 adds its `waiting_row` gate after the
  family-holder return and before `ctx.legs()`. The merged function is
  this branch's plus exactly those eight lines. So a `project.yaml` that
  does not load makes the placement row `n/a`, waiting on the `manifest`
  row, before the record is read. The record is read from `ctx.manifest`,
  which #205 now loads once, with the same `load_yaml`, so a manifest that
  loads gives the record it always gave.
- #205 removes `Context._declares`, and nothing on this branch names it.
  This branch leaves `placement_row` unchanged, and #205's gate calls it as
  (NA, reason, None, detail).
- 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. Every
  definition only one side changed is that side's text. The two that both
  changed are the docstring above and `check_placement`.
- These commits leave adopt-project.py, scripts/repo_shape.py and
  scripts/shape_materialize.py untouched. So the record's writer and
  reader are the ones the round trip and the revert table ran on at
  a01ab02, and the record is still spelled by `double_quoted`.
- AGENTS.md is untouched: 472 lines, under the cap of 472.
- The branch's diff against main is still #189 alone: 12 files, +1670
  -157. The one deletion more than against 5751460 is the EXIT CODES
  sentence above, which now rewraps two of #205's lines.

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
…, #205, #215, #241 and #249/#250 merge with no conflict, and `_entry_findings` still hands `_leg_findings` the tree and ends with the root-symlink findings

origin/main moved 60588d5 -> b30595c while the targeted runs of 8f624aa
went: 018ce0d (#207 via #222), 67a398a (#272 via #274), d94d155 (#205
via #232), c2ff2b0 (#215 via #240), c3a9645 (#241 via #256) and b30595c
(#249 and #250 via #284). It is merged again because c2ff2b0 changes
adopt-project.py and b30595c changes scripts/shape_materialize.py's
`render`.

Conflicts: none. `git merge-tree --write-tree` gave tree be093a5, and
the merge stages the same tree.

Checked on that tree:
- `_entry_findings` still calls `_leg_findings(plan, tree)` and ends
  with `_root_symlink_findings(plan, source, tree)`.
- No top-level name is bound twice in adopt-project.py,
  scaffold-project.py, scripts/repo_shape.py or
  scripts/shape_materialize.py, none is loaded but bound nowhere (AST),
  and main changed the signature of no function this branch calls.
- Both sides are intact by +/- line sets: each side's change against the
  merge base 60588d5 reappears, line for line, against the other side.
- c2ff2b0 adds helpers to tests/conftest.py (`planned`, `GITLINK`,
  `findings` and others) and no fixture. tests/test_adopt_root_symlinks.py
  imports its conftest names one by one and none of these, so its own
  `planned` and `GITLINK` are unaffected.
- b30595c's `render` spells a double-quoted YAML value with
  `double_quoted`; this branch's walker reads `assembly_root_paths()` and
  never renders.

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
…t read names the git command that puts back the last pin a tool wrote (`checkout --ours`, `checkout HEAD` or `checkout <commit>~1`, chosen from the pin's own git state), so the row's NEXT fixes it instead of refusing it again (#237)

The review of PR #267 at 092a5f9 found the new row's NEXT a dead end
(F1). `update-shape.py check` reads the pin with the same `read_pin`,
so run as printed it exited 2 with the same refusal every time, and
the remediation it then printed was wrong for the case: `git submodule
update` for a conflict, "adopt it" for a deleted pin, "re-save the
file" (a hand edit of a pinned file) for a stray byte. An operator who
follows AGENTS.md ("Run the one the row names ... never edit a pinned
file") was stuck, and README's "names the exact command that fixes it"
was false for this row.

`pin_restore` now picks the command from the pin's state, read-only:
- unmerged (porcelain UU, AA, ...): `checkout --ours` (or `--theirs`
  where only that side holds a pin, read off the index stages, not the
  letters) of every conflicted path the shape writes: the pin, the
  manifest, README.md and the copies, never a file of the project's own;
- uncommitted, where HEAD holds a pin: `checkout HEAD -- <pin>`;
- otherwise, where the last commit to change the pin has a parent that
  holds one: `checkout <that commit>~1 -- <pin>`;
- none of these: the old `update-shape.py check` NEXT, saying that
  whether the root was ever scaffolded or adopted is a person's call.
`--json` records which, as `detail["restore"]`. Status is read as bytes
with `--no-optional-locks`, so the doctor still writes nothing.

N2: no note carries a `;`, an apostrophe or a backtick, so a pasted
line is one command (#277). N1: the docstring names #232's
`unloaded_manifest_row`, which is not on main or this branch.

Tests run each printed command and ask the doctor again: the nine
uncommitted faults and two committed ones (a `git rm` and a `commit:
"zzzz"`, each followed by an unrelated commit) end COMPLIANT, exit 0;
the issue's own merge, with `Makefile` and a file of the project's own
also conflicted, names only `Makefile` and the pin and then reads the
pin; a squashed root commit keeps the check NEXT. At 092a5f9 each of
these fails, and head's NEXT run as printed exits 2.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 12, 2026
…es and leaves a conflicted `project.yaml` or `README.md` to a person, and every pin it puts back out of history is read first, so the other side's own edits survive it and two breaking commits in a row, a sparse checkout or assume-unchanged no longer name a wrong rollback (#237)

Fix round 2 on PR #267, answering lane openRepoShape-2's re-verify of
9f64bd0 (V1, V2).

V1 (P2). The unmerged NEXT named the manifest and README.md beside the pin
and the copies, and `checkout --ours` writes stage 2 whole. `apply` writes
only two lines of the manifest's `shape:` block and README's `Shape:` line;
every other line is the project's own, so the other side's edits to those
files went, hunks git had merged cleanly included, and the doctor then said
COMPLIANT. Now only the files the shape writes whole are taken: the pin,
each `copy_source` key and its collision copy. A conflicted manifest or
README.md is named in the note as a person's merge and listed in
`detail["left_for_a_person"]`, as #232's manifest row already says of the
same file. The review's optional refinement (naming either file where the
dropped side changed only tool-written lines) is not taken.

V2 (P3). The committed class named `<last>~1` whenever it held a pin. It
never asked whether HEAD's pin was the broken one, or whether the pin it
put back read. A sparse checkout or assume-unchanged over a good re-pin
got a rollback of that re-pin, and two commits in a row that each broke
the pin cycled between `checkout <last>~1` and `checkout HEAD`.

- `update-shape.py`: `read_pin`'s checks move into `check_pin(pin,
  pin_path)`, which `read_pin` calls, so the doctor checks a pin out of
  git with the same code.
- `pin_reads_at` reads `<rev>:contracts/shape-pin.yaml` with `cat-file
  blob` in memory (no temporary file: the doctor writes nothing), decodes
  and parses it as `load_yaml` does, and runs `check_pin`.
- `uncommitted` needs HEAD's pin to read (the review's optional step 4,
  so a dirty copy over a broken HEAD goes straight to history).
- New class `hidden`: HEAD's pin reads and status is clean. The NEXT is
  `git ls-files -v`, which shows the S or h, and no rollback is named.
- `committed` walks `rev-list -n 50 HEAD -- <pin>` newest first and names
  the first commit whose parent's pin reads.

`pin_restore(ctx)` still answers `(restore, command)`; it gains an optional
`detail` it writes `left_for_a_person` into, so existing callers are
unchanged.

Tests: the README case and the issue's own two-`apply` merge (real re-pins
from a copy of the standard with two more commits; shape/a also renames the
project and adds a README paragraph), two breaking commits, a dirty copy
over a committed break, assume-unchanged and a sparse checkout. All six
fail at 9f64bd0 for the review's reasons.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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