Skip to content

adopt-project.py: _verify fails a split whose root project.yaml is not the shape's manifest (adopt-manifest-not-shape), so a plan that gets past check's plan-root-blocks-manifest no longer ends adoption verified over an assembly the doctor calls NOT A SHAPE ROOT (#389) - #722

Merged
brettheap merged 3 commits into
mainfrom
fix/389-adopt-verify-root-manifest
Oct 16, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-1 (openRepoShape-1)
Claim: #389 (comment)

What

_verify now fails a split whose root project.yaml is not the shape's manifest. It reports a finding of its own, adopt-manifest-not-shape, and exits 1 like its other findings, so adoption verified is never printed over such a split. This is #389, the second bullet of #305's "What would fix it", which the fix for #305 left out on purpose.

It is defence in depth. Through execute as shipped, no plan reaches _verify with such a split: check finds, and execute refuses before any leg exists, every plan known to make one (plan-root-blocks-manifest, and plan-leg-path-collides for a leg mounted at project.yaml). Until now that refusal was all there was. _verify's own rule, since #165, is that it does not lean on check's refusals.

Why (repro on main)

Measured on main da48256c, through a shared clone of it, with a local source and --local-remote-dir, a stub gh first on PATH, a bogus GH_TOKEN, an empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null and GIT_ALLOW_PROTOCOL=file. The source held src/app.py, docs/spec.md and README.md, plus what the first column says. execute ran in process with _refuse_what_check_finds taken away, as tests/test_adopt_verify.py takes it for #165.

the source and the plan check execute on main the split's root execute on this branch
a file project.yaml (name: theirs), answered root (#305) 1, plan-root-blocks-manifest 0, adoption verified project.yaml is the source's blob; the manifest is at shape/project.yaml 1, adopt-manifest-not-shape
a symlink project.yaml -> missing.yaml, answered root 1, plan-root-blocks-manifest 0, adoption verified project.yaml is the source's link; the manifest was written through it, to missing.yaml 1, adopt-manifest-not-shape
a directory project.yaml/a.md, project.yaml/ answered root 1, plan-root-blocks-manifest 0, adoption verified project.yaml/a.md; the manifest is at shape/project.yaml 1, adopt-manifest-not-shape
no project.yaml; the plan's legs.code_path edited to project.yaml (#172) 1, plan-leg-path-collides 0, adoption verified a gitlink at project.yaml; the manifest is at shape/project.yaml 1, adopt-manifest-not-shape

In every row both legs were made and pushed, and so was the split branch. With the refusal in place, as shipped, the first row is REFUSED plan-root-blocks-manifest before any leg exists, on main and on this branch.

shape-doctor.py on a recursive clone of the first row's split on main says NOT A SHAPE ROOT (exit 2), and that clone's root project.yaml begins name: theirs.

How

Tests

New, in tests/test_adopt_verify.py: 8 cases. Seven call _verify directly through the file's verify fixture, which gains source_files= beside root_files=. They need no git filter-repo.

Changed, in tests/test_adopt_root_manifest.py. test_a_kept_project_yaml_is_a_finding pins the new wording of plan-root-blocks-manifest, and test_a_kept_symlink_there_is_a_finding_whatever_it_leads_to now pins the symlink form's new tail. Its execute tests at :307, :330 and :350 are unchanged and pass. The last of them is the control end to end: project.yaml sent to code or dropped still ends adoption verified.

Red on main. Against da48256c's adopt-project.py, 4 new cases fail: the 3 finding cases with exit 0, and the execute test with exit 0 and adoption verified. The 2 controls and #165's test pass there. Of R1's 2 cases, the-manifest-at-the-root passes on main and the-manifest-beside-it fails there (main finds nothing beside it); both fail with adopt-project.py as at 3397926e, this branch's first commit.

Revert table. Each row changes adopt-project.py alone, in place, over both test files, and after each row the fixed file was restored and its sha256 checked. Rows C1 to C6 ran at 3397926e (49 cases, sha256 854f2a35…); rows D1 and D2 ran at 6f308bc7 (51 cases, sha256 ce6ed2c4…).

row what it takes away fails
none nothing 0 of 49 at 3397926e, 0 of 51 at 6f308bc7
C1 _accounted_for no longer calls _split_manifest_findings 4: the beside case, both own-file cases, the execute test
C2 the source's-own-entry condition (kept = False) 3: both own-file cases, the execute test
C3 the beside condition (beside = False) 3: the beside case, the own-file case with the manifest beside it, the execute test
C4, a mutation shape/project.yaml counted by its path alone, not as a blob the source did not have there 1: the control the-sources-own-shape-project-yaml
C5 plan-root-blocks-manifest's text back to "and say adoption verified" 2: test_a_kept_project_yaml_is_a_finding, test_a_kept_symlink_there_is_a_finding_whatever_it_leads_to
C6, a mutation any path under shape/ that the source did not have counts 5: the control the-copy-of-a-kept-readme, the two existing execute tests in test_adopt_verify.py, and test_an_answer_the_question_offers_adopts_with_the_shapes_manifest[code] and [drop]
D1 R1's fix (and not dropped removed) 2: both cases of test_a_dropped_project_yaml_is_never_the_sources_own
D2, a mutation a dropped project.yaml waives the whole check 1: its the-manifest-beside-it case

Runs. Every run used /usr/bin/python3 -m pytest, a stub gh (exit 1) first on PATH, a bogus GH_TOKEN and an empty GH_CONFIG_DIR. Each log names its head with dirty 0 at the start and at the end, and ends EXIT=0.

  • The targeted set is every file that matches grep -l -E 'adopt-project|adopt_project|\bADOPT\b' tests/*.py (86 files at da48256c, tests/conftest.py among them), plus tests/test_shape_doctor.py and tests/test_scaffold_leg_path_collisions.py, which the grep already lists. That is 85 test files and 6,386 tests, run in four shards per mode.
  • Hidden mode is PATH=/usr/local/bin:/usr/bin:/bin, with the stub's directory first.
  • At the head 6f308bc7 (R1's fix) the four files the change touches or the adopt rules name were run in both modes: test_adopt_verify.py, test_adopt_root_manifest.py, test_shape_doctor.py and test_scaffold_leg_path_collisions.py. GIT_ALLOW_PROTOCOL was not set in any run.
  • The 85-file targeted set ran at 3397926e, the first commit. 6f308bc7 changes only _split_manifest_findings (a third argument, and the drop waiver) and adds one test to test_adopt_verify.py.
head files mode passed skipped failed
6f308bc7 (this branch now) 4 git filter-repo on PATH 560 3 0
6f308bc7 4 hidden 550 13 0
a8164b60, 6f308bc7 with main 78fe3fff (tree d61f8602, a scratch commit, never pushed) test_adopt_verify.py, test_adopt_root_manifest.py git filter-repo on PATH 51 0 0
3397926e (the first commit) 85, the targeted set git filter-repo on PATH 6,373 13 0
3397926e 85 hidden 6,028 358 0
3397926e tests/test_cli_reference.py git filter-repo on PATH 22 0 0
5057810f, this branch merged into main abb956e6 (tree 6a6ed973, never pushed) test_adopt_verify.py, test_adopt_root_manifest.py git filter-repo on PATH 49 0 0
5057810f 11 neighbours: test_adopt_verify, root_manifest, e2e, leg_path_collisions, root_shape_paths, ignored_shape_files, late_failure, finding_spelling, root_symlinks, test_shape_doctor, test_scaffold_leg_path_collisions git filter-repo on PATH 1,206 3 0

git merge-tree --write-tree 3397926e abb956e6 is clean (tree 6a6ed973). Main has since moved to 879d8047 (#620, which changes adopt-project.py's adopt-bad-id). git merge-tree --write-tree 3397926e 879d8047 is clean too (tree ab18664e), and the reviewer's targeted runs on that merge passed in both modes: the four files test_adopt_verify, test_adopt_root_manifest, test_shape_doctor and test_scaffold_leg_path_collisions, 558 passed and 3 skipped with git filter-repo on PATH, 548 passed and 13 skipped hidden; and 12 neighbouring files, 939 passed and 1 skipped on PATH, 813 passed and 127 skipped hidden.

At this branch's head 6f308bc7, main is 78fe3fff, which changes adopt-project.py again since 879d8047. git merge-tree --write-tree 6f308bc7 78fe3fff is clean (tree d61f8602), and the two changed test files pass on that tree (a8164b60, a scratch commit, never pushed): 51 passed with git filter-repo on PATH. Main moved to de926142 while those runs ended (14:18Z); git merge-tree --write-tree 6f308bc7 de926142 is clean too (tree 22de4a24). The reviewer ran the four files above on that tree (08417354, a local commit, never pushed): 560 passed and 3 skipped with git filter-repo on PATH, and 550 passed and 13 skipped with it hidden, none failed.

Windows

CI is Linux only since #597.

  • The seven new _verify cases need no git filter-repo. Their fixture writes every file with write_bytes, and nothing reads back a file a helper wrote with write_text.
  • The execute test writes its source through conftest's make_source_repo. It names the source's blob by git rev-parse, and compares only lines of the captured output.
  • Every path the tests create (shape/project.yaml, shape/README.md, project.yaml) is legal on NTFS.

Not in this PR

Known limits

Closes #389

🤖 Generated with Claude Code

Summary by Sourcery

Fail adoption verification when the assembly root does not contain the shape's manifest.

Bug Fixes:

  • Make adoption verification fail when the assembly root's project.yaml is not the shape manifest, preventing false adoption verified results for invalid shape roots.

Enhancements:

  • Add manifest-shape accounting that detects source-owned root manifests and manifests written beside colliding paths while allowing valid copies and dropped source files.
  • Update manifest collision findings to report verification failure with the adopt-manifest-not-shape code.

Tests:

  • Add direct and end-to-end regression coverage for root manifest collisions, symlinks, dropped manifests, valid shape-file copies, and execution past plan-check refusals.

Brett Heap and others added 2 commits October 16, 2026 11:48
…s not the shape's manifest (`adopt-manifest-not-shape`), so a plan that gets past `check`'s `plan-root-blocks-manifest` no longer ends `adoption verified` over an assembly the doctor calls NOT A SHAPE ROOT (#389)

`_verify` never asked whether the split's root `project.yaml` is the
shape's manifest. `check` finds, and `execute` refuses before any leg
exists, a plan that keeps the source's own `project.yaml` in the root
(#305), and that refusal was all there was: with it taken away, `execute`
wrote the manifest beside the source's file as `shape/project.yaml`,
every source blob was in exactly one place, and the run said `adoption
verified` (exit 0) over a root the doctor calls NOT A SHAPE ROOT (exit 2).
The same held for a kept symlink there that leads nowhere (the manifest
written through it), a kept directory `project.yaml/`, and a leg mounted
at `project.yaml` (#172).

`_accounted_for` now adds `_split_manifest_findings`: a finding when the
root's `project.yaml` is the source's own entry (by path and blob,
whatever its mode), or when `shape/project.yaml` holds a blob the source
did not have at that path. A source's own `shape/project.yaml`, kept
where the root's `project.yaml` is the manifest, still verifies, and so
do the shape's other copies beside a kept file. Nothing leans on the pin,
which does not list the rendered manifest.

`plan-root-blocks-manifest` said `execute` "would ... say `adoption
verified`"; it now says the run would fail its verification
(`adopt-manifest-not-shape`), which is what the run does without the
refusal.

Tests: tests/test_adopt_verify.py (the `verify` fixture: the manifest
beside, the source's own file with and without it, two controls; one
`execute` with `_refuse_what_check_finds` taken away, sharing two helpers
with #165's test), and tests/test_adopt_root_manifest.py's two text pins.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s own at the root, so a source file that is byte for byte the manifest this run renders, answered `drop`, no longer fails `adopt-manifest-not-shape` after both legs are pushed (#389)

#389's review (R1): `_split_manifest_findings` matched the source's own
`project.yaml` at the root by blob alone. A source whose own `project.yaml`
held exactly the manifest `execute` renders for this project, answered
`drop`, passes `check` and verified on main; with the check it failed
after both legs and the split were pushed, calling the shape's manifest
the source's own file.

`_mount_the_legs` `git rm`s every path in `paths_for["drop"]` before the
shape writes anything, so a dropped `project.yaml` is gone from the root
and what is at that path is the shape's. `_split_manifest_findings` now
takes that list and does not count the root's `project.yaml` as the
source's own when the plan dropped it. It asks what the split did, not
what `check` refused. The manifest beside the root's `project.yaml` is
still a finding.

Test: `test_a_dropped_project_yaml_is_never_the_sources_own` in
tests/test_adopt_verify.py, two cases (the manifest at the root verifies;
the manifest beside it is still the finding). Both fail with
adopt-project.py as at 3397926.

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

sourcery-ai Bot commented Oct 16, 2026

Copy link
Copy Markdown

Reviewer's Guide

Strengthens adoption verification so any split whose root project.yaml is not the shape manifest produces adopt-manifest-not-shape and fails closed, while distinguishing valid shape-file merges and dropped source paths; tests cover direct and end-to-end regressions plus updated diagnostics.

Sequence diagram for failed adoption verification

sequenceDiagram
    participant Execute as execute
    participant Account as _accounted_for
    participant Verify as _verify
    participant Doctor as shape-doctor.py

    Execute->>Account: _accounted_for(split_tree, before, ...)
    Account->>Account: _split_manifest_findings(tree, before, drops)
    Account-->>Verify: adopt-manifest-not-shape finding
    Verify->>Doctor: Root manifest would be project.yaml
    Verify-->>Execute: exit 1 with MANIFEST_DEFECT
    Note over Execute,Doctor: adoption verified is not printed
Loading

Flow diagram for split manifest verification

flowchart TD
    A["_accounted_for"] --> B["_shape_file_findings"]
    B --> C["_split_manifest_findings"]
    C --> D{"Root project.yaml is source entry or shape/project.yaml is new?"}
    D -->|"No"| E["Continue verification"]
    D -->|"Yes"| F["FINDING adopt-manifest-not-shape"]
    F --> G["MANIFEST_DEFECT"]
    G --> H["_verify exits 1; no adoption verified"]
Loading

Flow diagram for valid and invalid manifest cases

flowchart LR
    A["Split root project.yaml"] --> B{"Source entry kept at root?"}
    B -->|"Yes"| C["adopt-manifest-not-shape"]
    B -->|"No"| D{"New shape/project.yaml manifest?"}
    D -->|"Yes"| C
    D -->|"No"| E["No manifest finding"]
    C --> F["Verification fails"]
    E --> G["Continue; valid shape-file merge"]
Loading

File-Level Changes

Change Details Files
Add defense-in-depth verification that the assembly root's project.yaml is the shape manifest.
  • Inspect the split tree and source tree by path and blob to detect a kept source entry at the root or a newly rendered shape/project.yaml copy.
  • Ignore dropped source entries and unrelated shape-file copies, while preserving legal root-manifest cases.
  • Emit one adopt-manifest-not-shape finding with MANIFEST_DEFECT, causing _verify to exit 1 without printing adoption verified.
  • Update blocked-plan messaging to describe verification failure instead of successful adoption.
adopt-project.py
Expand regression coverage for manifest placement and refactor execute-test setup.
  • Add direct _verify cases for kept files, symlinks, absent targets, mounted paths, dropped manifests, and valid control cases.
  • Add an end-to-end test that bypasses check refusal and confirms execute fails after creating the split.
  • Share plan/check and execute-past-check helpers with the existing execute regression.
  • Update expected finding text and test documentation.
tests/test_adopt_verify.py
tests/test_adopt_root_manifest.py

Assessment against linked issues

Issue Objective Addressed Explanation
#389 Make _verify independently reject a split whose root project.yaml is not the shape’s manifest, rather than relying on check/execute refusals to prevent such a split. ✅
#389 Detect the relevant split cases—including the source’s own project.yaml left at the root and the manifest written beside it—while avoiding false positives for dropped files and unrelated shape copies. ✅
#389 Add regression coverage showing verification fails for the invalid manifest cases, does not print adoption verified, and continues to accept the documented controls. ✅

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

@brettheap
brettheap marked this pull request as ready for review October 16, 2026 17:32
@sourcery-ai

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

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit 3726ada into main Oct 16, 2026
5 checks passed
@brettheap
brettheap deleted the fix/389-adopt-verify-root-manifest branch October 16, 2026 19:09
brettheap pushed a commit that referenced this pull request Oct 17, 2026
main gained 25 pull requests since de92614: #634, #699, #671, #719,
#701, #717, #700, #702, #722, #720, #725, #721, #728, #705, #727, #718,
#744, #743, #749, #740, #737, #734, #742, #751 and #736. No conflict:
AGENTS.md, adopt-project.py and docs/cli.md merged as text, and
README.md and docs/handbook.html are unchanged by the merge. README.md
is 1473 lines, its cap; AGENTS.md 494, its cap; every handbook marker is
current; `render-cli-reference.py --check` is ok.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
…es every reason THIS checkout cannot answer, and the notes are current to main bf1f099 with the 25 pull requests since de92614 (#563, #683)

- W683 and RV-f's N4 on #742: README's exit-3 list (Keeping a project's
  shape current, :1208-1220) and its handbook twin (`doctor`) named only a
  pin whose commit this checkout does not carry. They now say, as
  shape-doctor.py's EXIT CODES does: a pin's commit it lacks, a partial
  clone missing what is compared (#683), a read that fails
  (`update-upstream-unreadable`), a crashed validator of its own (#278,
  #410), or its own naming policy not loading (#678). +1 line, with
  tests/readme-cap.d/683.txt; README.md is 1474, its cap (1422 + 2 + 5 +
  12 + 22 + 10 + 1). The two `Keeping a project's shape current` markers
  move: `doctor` text and marker, `current` marker only.
- The notes, "What changed since v1.0.1", one entry per pull request from
  `git log --first-parent de92614..bf1f099`, each from its squash commit and
  the issues GitHub says it closes:
  - adopt-project.py: #699, #719, #749, #727, #717, #728 (with #621's
    hint), #743 + #720 + #718, #722, #721;
  - update-shape.py, bump-leg.py and a project's own scripts: #702 (with
    the pinned validators), #634;
  - scaffold, setup, family and the shared readers: #725, #671, #744;
  - shape-doctor.py: #742, #701, #737, #751, #736, #734, #700 + #705,
    #740;
  - install, CI and the docs: the exit-3 sentence above.
- Counts (99 pull requests since bfed143; 109 of 135 milestone issues
  closed at 10:28Z), the "current to main" sha and the open list (26) are
  at main bf1f099, still placeholders for tag time.

Left for round B: lane 5's eight lines (#630 issuecomment-6227129879),
whose pull requests #726, #747, #682, #724, #746, #745 and #738 are open
drafts.

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