Skip to content

adopt-project.py: check lists an unanswered entry with the plan's problems against its source, before the split branch and the entries, so beside an uncovered path, an unsafe path, a bad leg path, the split branch taken or another entry finding its first finding is execute's refusal, plan-unresolved (#1197) - #1305

Open
brettheap wants to merge 9 commits into
mainfrom
l3-1197-unanswered-beside-uncovered

Conversation

@brettheap

@brettheap brettheap commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Restacked after #1210 landed

Built on #1210's head 791106c2be694e333e92b0b3acc38f254dc80dce. #1210 landed as 106f92ff (its final head 27c8f67b), and lane 3 restacked this branch with two merges: #1210's final head 27c8f67b, then its squash 106f92ff, with ours for adopt-project.py and tests/test_adopt_plan_against_its_source.py. Main was the squash itself, so no third merge was needed. The head is 28f6f6694aeb2485995ee702882d4c895de6168d.

  • Measured equal: this PR's own no-context lines over the squash equal its lines over 791106c2, without @@ and index lines: 273 lines over its three files, 256 of them in the two files the squash merge kept ours for. The head's tree is 8f9ded6cb, the tree fix round 1's G1 (438 passed, 3 skipped) and guards (470 passed, 3 skipped) ran on.
  • Its own commits are 142ca405d and 2edbfcb54 (fix round 1); the two merges after them are the restack. Review its diff over main 106f92ff.

What a person sees change

A plan with an unanswered entry (leg: null) beside another entry finding no longer gets two first problems. check's first finding is now execute's refusal, plan-unresolved, and check still lists every other finding after it, in the same run, with the same remediation lines.

execute is unchanged: it refused plan-unresolved first on #1210's head, and still does, in the same words.

The choice: the unanswered entry first, in both

#1197 leaves the order to the fix, and asks it to weigh #1113's reason ("each of the other three is mended by running plan again, which takes back every answer given before") for an uncovered path.

What changed, in adopt-project.py

Measured: before and after, over 39 probe sources

Measured on #1210's head 791106c2b (adopt-project.py sha256 4c8c1c9eb0c5) and on the first commit's head 142ca405d (885a96a1e3f5, which fix round 1 leaves as it is), by one probe (W1197-scratch/probe.py). Each source is made fresh, planned with conftest.planned, edited as named, and then check and execute --yes are run, both with the same --local-remote-dir (execute also with --work-dir). The environment was the landing gate's: a stub gh first on PATH (exit 97), GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, an empty global git config with GIT_CONFIG_NOSYSTEM=1, LANES_LANE unset, git 2.43.0, Python 3.12.3 and git-filter-repo 2.47.0 (CI's pin). Each output was saved with the work directory and every 7-to-64-digit hex run masked, and compared byte for byte.

U- is examples/ left unanswered (leg: null); -plan is a plan as plan wrote it, with no hand edit; unsafe-plan is a source holding -n.md, which plan leaves unanswered itself; empty-symlink is a source keeping a symlink whose target is empty, which plan also leaves unanswered. No run made a leg, except none (every entry answered, nothing wrong), which check passes and execute verifies, on both heads.

Plan check first, before execute first, before both first, here check stderr other output
U-uncovered plan-uncovered plan-unresolved plan-unresolved reordered, same lines same
U-bad-leg-before plan-bad-leg plan-unresolved plan-unresolved reordered, same lines same
U-empty-entry plan-empty-entry plan-unresolved plan-unresolved reordered, same lines same
U-unsafe-plan plan-unsafe-path plan-unresolved plan-unresolved reordered, same lines same
U-unsafe-hand plan-unsafe-path plan-unresolved plan-unresolved reordered, same lines same
U-bad-leg-path plan-bad-leg-path plan-unresolved plan-unresolved reordered, same lines same
U-taken plan-split-branch-taken plan-unresolved plan-unresolved reordered, same lines same
U-duplicate-path plan-duplicate-path plan-unresolved plan-unresolved reordered, same lines same
U-covered-twice plan-covered-twice plan-unresolved plan-unresolved reordered, same lines same
U-collides plan-leg-path-collides plan-unresolved plan-unresolved reordered, same lines same
U-empty-symlink plan-empty-symlink plan-unresolved plan-unresolved reordered, same lines same
U-unsafe-plan-and-examples plan-unsafe-path plan-unresolved plan-unresolved reordered, same lines same
U plan-unresolved plan-unresolved plan-unresolved same same
U-bad-leg-after plan-unresolved plan-unresolved plan-unresolved same same
U-tracking plan-unresolved plan-unresolved plan-unresolved same same
U-nul-root plan-unresolved plan-unresolved plan-unresolved same same
U-moved-uncovered plan-stale plan-stale plan-stale same same
U-moved plan-stale plan-stale plan-stale same same
U-another-uncovered plan-not-the-source plan-not-the-source plan-not-the-source reordered, same lines same
U-name-uncovered naming-unclassified naming-unclassified naming-unclassified reordered, same lines same
U-another-taken plan-not-the-source plan-not-the-source plan-not-the-source reordered, same lines same
uncovered plan-uncovered plan-uncovered plan-uncovered same same
bad-leg-before plan-bad-leg plan-bad-leg plan-bad-leg same same
empty-entry plan-empty-entry plan-empty-entry plan-empty-entry same same
unsafe-hand plan-unsafe-path plan-unsafe-path plan-unsafe-path same same
bad-leg-path plan-bad-leg-path plan-bad-leg-path plan-bad-leg-path same same
taken plan-split-branch-taken plan-split-branch-taken plan-split-branch-taken same same
duplicate-path plan-duplicate-path plan-duplicate-path plan-duplicate-path same same
covered-twice plan-covered-twice plan-covered-twice plan-covered-twice same same
collides plan-leg-path-collides plan-leg-path-collides plan-leg-path-collides same same
empty-symlink plan-empty-symlink plan-unresolved plan-unresolved reordered, same lines same
bad-leg-after plan-bad-leg plan-bad-leg plan-bad-leg same same
tracking unsafe-value unsafe-value unsafe-value same same
nul-root adopt-bad-project-name adopt-bad-project-name adopt-bad-project-name same same
moved-uncovered plan-stale plan-stale plan-stale same same
another-uncovered plan-not-the-source plan-not-the-source plan-not-the-source same same
name-uncovered naming-unclassified naming-unclassified naming-unclassified same same
another-taken plan-not-the-source plan-not-the-source plan-not-the-source same same
none - - none (plan ok, exit 0, execute verified) same same

Tests

tests/test_adopt_plan_against_its_source.py, 22 rows added (18 to 40), 20 of them in the first commit and 2 in fix round 1. It is the module that already holds #1113's and #1210's rows for this rule, and its WITH_AN_ORDINARY_FINDING left unanswered-and-taken out by name "(#1197)". Each row runs check and then execute --yes on one plan, both with the same --local-remote-dir, and both_on asserts that no leg is made.

tests/test_adopt_entry_paths.py, 1 row changed: test_plan_asks_about_a_path_execute_refuses, 8 ids (leading-dash, tab, carriage-return-inside, carriage-return-at-the-end, line-feed, not-utf8, rename-arrow, whitespace-only). It is the -n.md case of #1197's comment for each name entry_path_problem refuses: plan writes the entry at leg: null, check found plan-unsafe-path and then plan-unresolved, and execute refused plan-unresolved. It pinned that disagreement; it now expects plan-unresolved and then plan-unsafe-path, still 2 findings, and execute still refuses plan-unresolved.

Rows against #1210's head, and mutants

Each run in this table is at 142ca405d, before fix round 1: the two changed test modules, 117 ids, in one pytest, with adopt-project.py replaced and then restored with cp, checked with cmp and sha256 (885a96a1e3f5, the same after each). Unmutated, the 38 rows of tests/test_adopt_plan_against_its_source.py passed (38 passed in 51s). Fix round 1 runs the module again, with its two rows (40 ids), under "Fix round 1".

Run What it changes Result
BASE #1210's own adopt-project.py (791106c2b, sha256 4c8c1c9eb0c5) 18 failed, 99 passed: the 9 new pair rows, the empty-link row and the 8 ids of test_plan_asks_about_a_path_execute_refuses. Each pair row fails on the first codes, ('plan-uncovered', 'plan-unresolved') != ('plan-unresolved', 'plan-unresolved') and alike. The 10 controls and #1210's 18 rows pass.
M1 the order put back: the rule yields an unanswered entry with no FINDING lines, so check lists it with the entries again killed: the same 18 rows as BASE, id for id
M2 check lists the entries' own plan-unresolved lines as well as the rule's killed: 9 ids, the empty-link row (its findings are plan-unresolved, plan-empty-symlink, plan-unresolved) and the 8 test_plan_asks_about_a_path_execute_refuses ids (3 findings, not 2); none of the 9 pair rows, whose listed counts each code once
M3 check lists the rule's unanswered entry after the split branch taken, before the entries killed: 1 row, unanswered-and-taken

M1 is the mutant #1197's third box asks for: every row added here fails on it.

Gates

One pytest at a time, -rs, never the full suite, in the landing gate's environment (as in the probe above; LANES_LANE unset). They ran on the scratch merge 1d77ae06e of main 34eb466e7 and the first commit's head 142ca405d (tree 450d8a0f2, never pushed), in a detached worktree of lane 3's clone: that tree is what lands after the restack, and it holds main's own rows and tests/test_path_guard_hygiene.py, which #1210's branch lacks. Each run started with at least 46G free on the disk and 36% of /tmp's inodes used.

Run Modules Result
G1 this PR's rows module, with every module it imports from or that those import fixtures from: test_adopt_plan_against_its_source.py, test_adopt_plan_value_order.py, test_adopt_pins.py, test_adopt_root_symlinks.py 436 passed, 3 skipped, 450s
G2a every other module that names plan-unresolved, and the module one of them imports from: test_adopt_e2e, _entry_paths, _execute_refusals, _finding_spelling, _leg_path_collisions, _leg_paths, _plan, _project_is_the_source, _plan_escapes, _question_and_submodule_spelling, _single_repository_record, _spelled_unambiguous, _submodules, _verify 1493 passed, 1 skipped, 603s
G2b every module that names a code this PR moves the unanswered entry ahead of (plan-uncovered, plan-bad-leg, plan-empty-entry, plan-unsafe-path, plan-bad-leg-path, plan-split-branch-taken, plan-duplicate-path, plan-covered-twice, plan-leg-path-collides, plan-empty-symlink, plan-bad-symlink-target, plan-path-git-refuses), main's three new ones among them, with the modules they import from: test_adopt_check_throwaway_repos, _nested_attributes, _root_symlinks, _contracts_dir, _literal_names, test_pinned_copy_text, test_adopt_empty_symlinks, _leg_keys, _manifest_values, _mount_ignored, _plan_yaml_quoting, _split_branch, _symlink_target_bytes, _template_values, _unsearchable_source, _temp_work_roots, test_leg_path_yaml_scalars, test_adopt_relative_submodule_url, _plan_and_remote_paths_spelled, _plan_values_read_as, _root_shape_paths 2246 passed, 13 skipped, 1474s
G3 the test modules #1210 changes that no run above holds, with the modules that import fixtures from one of them: test_adopt_check_plan_name, _relative_submodule_url, _c_locale_bytes, _submodule_url_answers, _project_is_the_source 142 passed, 199s
G4 tests/test_scaffold_leg_path_collisions.py, with test_adopt_pins.py, which it imports from 256 passed, 134s
guards test_repo_hygiene, test_cli_reference, test_agents_md_codes, test_git_output_decoding, test_stdout_encoding, test_path_guard_hygiene, test_no_auto_maintenance, test_update_add_codes_documented 470 passed, 3 skipped, 64s
doctor tests/test_shape_doctor.py, once, in the background, with a log 446 passed, 3 skipped, 531s

Every skip is the host's, or the module's own, and none is this PR's:

At 142ca405d itself, the rows module gave 38 passed (51s).

Merges

git merge-tree --write-tree (git 2.43.0), main and every PR head fetched by URL at 09:33Z, and main and #1304 again at 10:31Z, 2026-11-10; the other heads were read again at 10:31Z and had not moved. The open PRs that touch a file this PR changes: 12 touch adopt-project.py (#1210 among them); none touches either test file but #1210, which this PR is on.

Against head with this head 142ca405d with #1210's head alone three-way on the scratch merge 1d77ae06e (main + this head)
main 34eb466e7 clean (tree 450d8a0f2) clean (tree 8c8a3baf5) (it is that merge)
main, at 10:31Z b607223b4 clean (tree 1dffeb13c) clean (tree 45e19dd5f) -
#1210 791106c2b an ancestor: this head's own tree 19e731cdc - -
#1171 ce1dd661f clean (tree 2c8a992e9) clean (tree 1329736ac) clean (tree 46bbf192e)
#1194 906172b41 clean (tree 0112028e0) clean (tree a840bd6aa) clean (tree df87a969a)
#1198 d38e4e36d clean (tree cf110e656) clean (tree 61c72875a) clean (tree b1be019ab)
#1225 cc230f2f1 clean (tree 03a590b83) clean (tree f801fcc1c) clean (tree d8f12c8bb)
#1237 fa6d189ff clean (tree 29b4d760e) clean (tree 3c4468d5f) clean (tree 3b831588e)
#1249 3146c3ca5 clean (tree 4a75e7d87) clean (tree e4db47ac9) clean (tree 76e9593a6)
#1265 39b21c07e clean (tree 794cda8e3) clean (tree 70ad1688f) clean (tree 3e300baab)
#1280 5cb1abdbc clean (tree e22bc7e57) clean (tree a14d36bff) CONFLICT in adopt-project.py, 3 hunks (tree 1bd5228c3)
#1294 ce9fbee6e clean (tree 7d82ba10b) clean (tree 5ca08a780) clean (tree c196a02ce)
#1297 2e020b458 clean (tree 0148367f7) clean (tree 76f5f5650) clean (tree 43ce2bfc8)
#1304, at 11:00Z df942efee clean (tree aff8b8504) clean (tree add80e8f7) clean (tree 50c8bbdfd, on a scratch merge with main b607223b4)

#1280's conflict is neither this PR's nor #1210's. #1280 conflicts with main alone (tree ae8eeca99, adopt-project.py). The three hunks are main's #1219 text against #1280's own lines: the docstring and body of the function that reads the source's origin/HEAD, of the Remediation: that says origin has no branch, and of the tracking-branch check (_refuse_unlisted_refs against _passed_over). The same three-way merge with main + #1210 alone (scratch merge 4aba757f1) gives the same 3 hunks, byte for byte (tree 6520b8df5). No hunk touches a line this PR changes.

Main moved during the gate, from 34eb466e7 to b607223b4 (two commits: validate-manifest.py and validate-family.py in the templates, tests/conftest.py and four test files). None of them is a file this PR changes or one of the gated modules but test_pinned_copy_text.py, which names none of the codes this PR moves; so the gates below, run on main 34eb466e7, were not run again.

Fix round 1

RV1197's review of this PR at 142ca405d (issuecomment-6767157742) mutated _against_its_source twice, and both mutants survived every row of the module (SHOULD-1). The pair rows' listed counts each code once, so they see neither a second unanswered entry left with the entries nor an unanswered entry listed ahead of plan-uncovered:

  • R1: the rule lists only the first unanswered entry. On RV1197's probe U2+taken, check then lists plan-unresolved, plan-split-branch-taken, plan-unresolved.
  • R2: a moved source also lists the unanswered entries right after plan-stale. On RV1197's probe U+moved, check then lists plan-stale, plan-unresolved, plan-uncovered; the head gives plan-stale, plan-uncovered, plan-unresolved.

This round adds the two rows that kill them, in one commit, 2edbfcb54, which changes only tests/test_adopt_plan_against_its_source.py (+59 lines, the two rows, a helper and one docstring paragraph). adopt-project.py does not change: its sha256 is still 885a96a1e3f5. Each row asserts the full ordered list of check's FINDING codes, never a set, and runs check and then execute --yes on one plan with the same --local-remote-dir, as the rows above do.

Runs. The module, tests/test_adopt_plan_against_its_source.py, 40 ids, one pytest each, in the landing gate's environment (stub gh, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, an empty global git config, LANES_LANE unset, git-filter-repo 2.47.0), adopt-project.py replaced and then restored with cp, checked with cmp and sha256 (885a96a1e3f5, the same after each):

Run What it changes Result
head nothing 40 passed in 77.82s (the new rows alone: 2 passed)
BASE #1210's own adopt-project.py (791106c2b, sha256 4c8c1c9eb0c5) 11 failed, 29 passed in 76.71s: the 9 pair rows, the empty-link row and the first new row. The moved-source row passes on both, as it pins "as before".
R1 the rule lists only the first unanswered entry killed by the first new row alone: 1 failed, 39 passed in 70.31s
R2 a moved source also lists the unanswered entries right after plan-stale killed by the second new row alone: 1 failed, 39 passed in 71.85s
M1 the order put back killed: 11 failed, 29 passed in 90.89s, the same 11 as BASE
M2 check lists the entries' own plan-unresolved lines as well as the rule's killed: 2 failed, 38 passed in 88.72s, the empty-link row and the first new row (the 8 ids of test_plan_asks_about_a_path_execute_refuses in tests/test_adopt_entry_paths.py kill it too, as in the table above; that file is unchanged)
M3 check lists the rule's unanswered entry after the split branch taken killed: 2 failed, 38 passed in 79.60s, unanswered-and-taken and the first new row

Gates. Only one test file changes, so the gates above stand for everything else: adopt-project.py and every other test file are byte-identical to 142ca405d. The rows module and the guards ran again, one pytest at a time, -rs, on a scratch merge efd284e52 of main 3add78e1c and this head 2edbfcb54 (tree 8f9ded6cb, never pushed), in a detached worktree of lane 3's clone, with at least 38G free on the disk and 37% of /tmp's inodes used:

Run Modules Result
G1 the rows module with every module it imports fixtures from: test_adopt_plan_against_its_source.py, test_adopt_plan_value_order.py, test_adopt_pins.py, test_adopt_root_symlinks.py, in ONE pytest 438 passed, 3 skipped in 731.25s (0:12:11); round 0 gave 436 passed, 3 skipped, so the two new rows are the only difference
guards test_repo_hygiene, test_cli_reference, test_agents_md_codes, test_git_output_decoding, test_stdout_encoding, test_path_guard_hygiene, test_no_auto_maintenance, test_update_add_codes_documented 470 passed, 3 skipped in 92.23s (0:01:32), as in round 0

Every skip is the host's, and none is this PR's: 3 in G1, test_adopt_root_symlinks.py:1960, :2015 and :2596, the reftable backend needs git 2.45 (this host has 2.43.0); 3 in the guards, test_stdout_encoding.py:292, Windows' own default, and 2 in test_no_auto_maintenance.py:222, because the session sets GIT_CONFIG_GLOBAL, as in round 0.

Merges at this head, git merge-tree --write-tree (git 2.43.0), main and each head fetched by URL at 12:24Z, 2026-11-10, and the merges made at 12:34Z. Main moved to 3add78e1c since the table above. Of these PRs only #1304, #1297, #1237 and #1280 touch adopt-project.py, and none touches either test file:

Against head with this head 2edbfcb54 with #1210's head alone three-way on the scratch merge efd284e52 main alone
main 3add78e1c clean (tree 8f9ded6cb) clean (tree ee6de2a20) (it is that merge) -
#1210 791106c2b an ancestor of this head - - -
#1304 df942efee clean (tree e80621be6) clean (tree add80e8f7) clean (tree 37e9dd592) clean (tree f9af21642)
#1307 3aa9d5139 clean (tree 205474910) clean (tree d12ad74ab) clean (tree 4c31bbf20) clean (tree 6e0e94d8c)
#1297 2e020b458 clean (tree 564e56473) clean (tree 76f5f5650) clean (tree f32a25df9) clean (tree 5275d2792)
#1237 fa6d189ff clean (tree 57b73b6d8) clean (tree 3c4468d5f) clean (tree 16cbda3f7) clean (tree efbe35854)
#1280 5cb1abdbc clean (tree e529bf31d) clean (tree a14d36bff) CONFLICT in adopt-project.py, 3 hunks (tree f012b8701) CONFLICT in adopt-project.py, 3 hunks (tree 2a24f5f5e)

#1280's conflict is #1280's own, against main. Main alone gives the same 3 hunks, at the same lines of the merged file, with the same two sides (order aside): lines 3620-3653, in _default_branch; lines 3715-3732, in _from_the_remote; and lines 5818-5838, in _tracking_branch_problem. None is a line this PR changes.

#1197's boxes

  • check's first finding is execute's first refusal for the pair (unanswered-and-uncovered), with a row that runs check and then execute --yes on one hand-edited plan and asserts that no leg is made.
  • The same for each other entry finding and the taken split branch measured to disagree: RV1210's five (bad-leg, empty-entry, unsafe-path, bad-leg-path, taken), four more measured here (duplicate-path, covered-twice, collides, and the empty link plan leaves unanswered), and the -n.md plan plan writes (test_plan_asks_about_a_path_execute_refuses, 8 names).
  • Each row fails on a mutant that puts the order back (M1), restored, with its sha256 checked.

Not done

Closes #1197

Lane: openreposhape-3 (openRepoShape-3)

🤖 Generated with Claude Code

Summary by Sourcery

Align check’s first finding with execute by prioritizing unresolved plan entries without suppressing other plan diagnostics.

Bug Fixes:

  • Make check report unresolved plan entries before other entry findings, matching execute’s refusal order while retaining all related diagnostics.

Enhancements:

  • Centralize formatting of unresolved-entry findings and avoid duplicating findings when combining plan-level and entry-level validation.
  • Expand coverage for unresolved entries alongside path, leg, coverage, split-branch, and empty-symlink problems, including multiple unanswered entries and moved sources.

Tests:

  • Add regression and control cases verifying finding order, remediation output, multiple unanswered entries, generated empty-symlink plans, and path-safety cases.

Brett Heap and others added 5 commits October 21, 2026 13:59
… right after its three repository names, so a bad one beside an unanswered entry, a moved source, a refused name or another repository is that one refusal in both, and a `pins:` entry that is no pin is said as `_read_as` says it (#1020)

#901 made `check` and `execute` name the same first problem among a plan's options. Outside them, the two still named different problems first (#1020, found by lane 1's writer on #901).

1. **One rule, the refusal before what `check` lists.** `check` refuses a bad `org:` or `id:` outright, exit 2, and lists none of its findings. `execute` refuses at the first of them, but it asked the two only after it had refused four things that `check` lists as findings: a plan for another repository (`plan-not-the-source`), a name the naming policy refuses (`naming-unclassified`), an unanswered entry (`plan-unresolved`) and a source that has moved on (`plan-stale`). And `check` asked the two only where the policy took the plan's names (`_manifest_values`), so an unsafe `org:` beside a refused name was a finding about the name, or about another repository. Both commands now ask the two first of the plan's values, right after the three repository names (`plan.names()`, then `_checked_org_and_id`): `execute` in `cmd_execute`, ahead of `_refuse_a_plan_for_another_repository`, and `check` at its head, after the triad advisory. `_checked_plan_values` no longer asks them, since `cmd_execute` has.
2. **#716's L4-48, decided.** An `org:` that `checked_value` refuses, held to a repository, was `unsafe-value` to `check` and `plan-not-the-source` to `execute`. Such an `org:` names no repository, on GitHub or in the manifest, so it is not compared with the source's. It is `unsafe-value` in both commands now, held to a repository or not. Once it is mended, a plan for another repository is still `plan-not-the-source` in both. This reverses three tests in `tests/test_adopt_check_plan_name.py`, which pinned the old answers on purpose:
   - `test_an_unsafe_org_is_unsafe_in_check_and_compared_by_execute_if_held` expected `plan-not-the-source` from `execute` in its six held rows. As `test_an_unsafe_org_is_unsafe_in_check_and_in_execute_held_or_not`, all twelve expect `unsafe-value` from both, in the same words.
   - `test_check_compares_an_unsafe_org_where_no_manifest_is_built` expected `check` to compare `org: acme labs` beside the refused name `bad_name`: `plan-not-the-source` and `naming-unclassified`, exit 1. As `test_an_unsafe_org_is_asked_before_the_plans_refused_names`, it expects `unsafe-value`, exit 2, from `check` and from `execute`.
   - `test_check_asks_an_unsafe_org_first_where_a_kept_submodule_url_is_relative` expected `plan-not-the-source` from `execute`. It now expects `unsafe-value`, in `check`'s words.
3. **A `pins:` entry that is no pin** was spelled with `repr` (`` `pins:` entry 1 is 7 ``, `True`, `None`). #901 made `pins:` itself read as `_read_as` reads it, and an entry reads the same way now: "the number 7", "the boolean true", "null", "a list of 2 item(s)".

Docstrings and comments now say this: `_checked_org_and_id`, `_checked_plan_values`, `_manifest_values`, `_held_findings`, `_not_the_source`, `_refuse_a_plan_for_another_repository`, `_read_pins` and the #499 block comment. No finding is added or removed: only which problem is named first changes, and the spelling of an entry.

Measured on main at `6da757cb` and on this branch, with a stub `gh`, a bogus token, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false` and `--local-remote-dir`. No run made a leg.

| The plan has | `check` on main | `execute` on main | both, here |
|---|---|---|---|
| a bad id beside an unanswered entry | `plan-bad-id` | `plan-unresolved` | `plan-bad-id` |
| a bad id beside a source that has moved on | `plan-bad-id` | `plan-stale` | `plan-bad-id` |
| a bad id beside a name the policy refuses | `plan-bad-id` | `naming-unclassified` | `plan-bad-id` |
| a bad id beside another organisation, held to a repository | `plan-bad-id` | `plan-not-the-source` | `plan-bad-id` |
| no `id:`, and `legs.assembly: north_wind` (a derived id the policy refuses too) | `plan-bad-id` | `naming-unclassified` | `plan-bad-id` |
| an unsafe `org:` held to a repository (L4-48) | `unsafe-value` | `plan-not-the-source` | `unsafe-value` |
| an unsafe `org:` beside an unanswered entry | `unsafe-value` | `plan-unresolved` | `unsafe-value` |
| an unsafe `org:` beside a source that has moved on | `unsafe-value` | `plan-stale` | `unsafe-value` |
| an unsafe `org:` beside a refused name | finding `naming-unclassified`, exit 1 | `naming-unclassified` | `unsafe-value` |
| the same, held to a repository | finding `plan-not-the-source`, exit 1 | `plan-not-the-source` | `unsafe-value` |

Each "here" row is exit 2 from both, word for word. The last two rows agreed on main. They change because `check` now asks the two where the policy refuses the names, as `execute` now does: without that, moving `execute` alone would have made them disagree. Plans with only one of these problems (an unanswered entry, a moved source, a refused name, another repository) are named as on main by both.

**Tests.**
- `tests/test_adopt_plan_value_order.py` gains 16 rows:
  - the 10 above, through both commands, comparing the first line;
  - four `pins:` entries that are no pin: number, boolean, null and list;
  - two guard rows, `id` and `org`: a leg name `checked_value` refuses (`legs.spec: "-spec"`) beside a bad `id:` or `org:` is that name's `unsafe-value` in both, so the three names are still asked first.
- The 14 non-guard rows all fail against `6da757cb`'s `adopt-project.py` and pass here. The two guard rows pass on both, by design.
- Existing rows changed for the new behaviour:
  - `test_adopt_check_plan_name.py`: the three tests above, and its docstring's L4-48 line.
  - `test_adopt_pins.py`: `a-number`.
  - `test_adopt_execute_refusals.py`: the three entry rows of `PIN_ENTRIES`.

Closes #1020

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… right after the plan's keys and before `_work_root`, so a bad one beside an unwritable `--work-dir` is that one refusal in both (#1020)

RV1125's SHOULD-1 on #1125. A bad `id:` or `org:` beside a `--work-dir`
this user cannot write in was `plan-bad-id` (or `unsafe-value`) to `check`
and `adopt-work-dir-unwritable` to `execute`: `check` lists the directory
as a finding (#524), and `execute` refuses it before it opens the source,
ahead of the two asks, which #1125 had placed after `_work_root`. The
comments in `cmd_execute` and `_checked_org_and_id` said that cannot
happen.

- `cmd_check` asks `plan.names()` and `_checked_org_and_id(plan)` right
  after its mistyped-plan question, before `_work_root`, and `cmd_execute`
  asks them right after `_refuse_a_mistyped_plan`, before `_work_root`.
  Both read only the plan. `execute` now asks them before
  `_require_filter_repo()` too.
- A side effect, said in the body: `check` printed the Triad advisory,
  once it had opened the source, before it refused a bad `org:` or `id:`;
  it prints nothing now, since the refusal comes before the source is
  opened.
- The comments and docstrings that placed the asks "at its head, after the
  triad advisory" or "right after the names" say where they are now.
- tests/test_adopt_plan_value_order.py: `both_on` takes a `--work-dir` for
  both commands; one row per bad `id:` and bad `org:` beside an unwritable
  `--work-dir` (skipped where `chmod` cannot keep this user from writing);
  and, for NIT-2, one row per value that asks `check` alone and holds
  stdout empty, the position being told by the Triad advisory the unedited
  plan prints first.
- tests/test_adopt_check_plan_name.py: the docstring of the kept-submodule
  row says it is a regression row since `check` asks `org:` at its head
  (NIT-1).

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…blems against its source, in the order a person mends them (another repository, a refused name, a moved source, an unanswered entry), so `check`'s first finding is `execute`'s refusal for each pair (#1113)

`check` listed a moved source first, then the plan's entries, then
another repository and then the names; `execute` refused another
repository, a refused name, an unanswered entry and then a moved source.
So for five of the six pairs of the four, `check`'s first finding was not
`execute`'s refusal: the four of #1113's table, and a refused name beside
a moved source.

`_against_its_source` asks the four in one order, and both commands read
it: `execute` refuses its first problem, and `check` lists each, in that
order and in its own words, before the split branch and the entries.
Each of the first three is mended by running `plan` again, which takes
back every answer, so an unanswered entry is named last; another
repository comes first, before the naming policy or any remote is asked
(#499); and a refused name is mended by a `plan` that reads the source
as it is now, so it comes before a moved source.
`_refuse_an_unrunnable_plan` asks a moved source before an unanswered
entry. No refusal's code or wording changes: only which is named first.

tests/test_adopt_plan_against_its_source.py runs both commands on one
plan per pair, on all four, and on each of the four alone. In
tests/test_adopt_relative_submodule_url.py, six rows hand-edit a plan's
`org:` to another organisation beside a relative url, and pinned
`check`'s old order, the url first. Such a plan is for another
repository, which `execute` refuses first (#499), so the rows now expect
`plan-not-the-source` first to `check` too. A new row runs `check`, with
and without `--local-remote-dir`, and `execute` on one such plan, and
asserts that each names it first and that no leg is made.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le's problems beside the split branch taken and beside an unsafe `tracking_branch:` pins that `check` lists the rule's problem, and its remediation, first and `execute` refuses it first, so the rule moved after either finding fails a row (#1113)

RV1210 measured two placements of the rule that no row pinned, each
bringing back a disagreement #1210 fixes:
- `check` listing the rule's problems after the split branch: another
  repository, a refused name and a moved source beside a branch `adopt`
  in the source, as `test_adopt_split_branch.py` makes it
  (`another-and-taken`, `name-and-taken`, `moved-and-taken`);
- `execute` asking the rule after `_checked_plan_values`,
  `_repository_urls` and `_refuse_an_unwritable_clone_url`: each of the
  four beside `tracking_branch: "a b"`, which `BAD_TRACKING_BRANCH` from
  `test_adopt_plan_value_order.py` edits in (`another-and-tracking`,
  `name-and-tracking`, `moved-and-tracking`, `unanswered-and-tracking`).

Each row asserts that `check` exits 1 and names the rule's problem first,
and lists the ordinary finding after it, and that `execute` exits 2 and
refuses the rule's problem first. `another-and-taken` also asserts that
`check`'s first `Remediation:` line is the rule's (NIT-4), so the
remediation lines follow the findings' order. `unanswered-and-taken` stays
out: it is #1197's.

Test only; adopt-project.py is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…roblems against its source, before the split branch and the entries, so beside an uncovered path, an unsafe path, a bad leg path, the split branch taken or another entry finding its first finding is `execute`'s refusal, `plan-unresolved` (#1197)

`execute` refuses an unanswered entry (`leg: null`) with #1113's rule,
`_against_its_source`, before the plan's values, the split branch and the
entries. `check` listed it with the entries, in `_leg_findings`, after the
split branch taken, the source's own paths, the coverage, the leg paths and
a bad `leg:` on an entry before it. So on #1210's head 791106c a plan with
an unanswered entry beside any of ten findings was named two ways:
`plan-uncovered` (#1197's pair), `plan-bad-leg`, `plan-empty-entry`,
`plan-unsafe-path` (also on a plan `plan` writes for a source holding
`-n.md`), `plan-bad-leg-path`, `plan-split-branch-taken`,
`plan-duplicate-path`, `plan-covered-twice`, `plan-leg-path-collides` and
`plan-empty-symlink` (also on a plan `plan` writes for an empty link) to
`check`, and `plan-unresolved` to `execute`.

The rule now yields an unanswered entry with `check`'s FINDING lines for
it, each by its path, spelled by one helper, `_unanswered_finding`, which
`_leg_findings` reads too, and `check` does not list a line the rule listed
again. `execute` is unchanged. Where the source has moved on, the rule's
problem is `plan-stale`, and an unanswered entry stays with the entries, as
before.

Tests: tests/test_adopt_plan_against_its_source.py gains 9 pair rows, a
row for the empty link `plan` leaves unanswered, and 10 controls;
tests/test_adopt_entry_paths.py's row for a path `plan` asks about expects
the unanswered entry first.

Lane: openreposhape-3 (openRepoShape-3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Implementation

Centralize the unanswered-entry finding and have check list each such finding with the source-level problems that execute refuses first, without repeating it among the entry findings; retain the existing ordering when the source is stale.

  • adopt-project.py L7800-7809, L14827-14849, L14885-14890 (+4 small edits)

Tests

Add regression coverage for matching check and execute ordering across unanswered entries and other findings, including multiple unanswered entries, stale sources, entries plan leaves unanswered, and controls; update the unsafe-path expectations accordingly.

  • tests/test_adopt_entry_paths.py L510-513, L546-549, L552 (removed lines)
  • tests/test_adopt_plan_against_its_source.py L124-149, L164-185, L307-398 (+5 small edits)

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-11-10T12:21:08Z) RV1197: CHECKPOINT at 142ca40 (interim: the review stopped at Brett Heap's account-swap word at 11:14Z; every finding below is measured)

Review of #1305 (for #1197), stacked on #1210 (791106c2). I reviewed the range 791106c2..142ca405, one commit. Neither head had moved at 11:00Z. I stopped at lane 3's LAST STEP. Verdict so far: READY AFTER FIXES, leaning that way. Two SHOULDs, rows and body, and no MUST. It is pending the gates still unrun (listed below).

The order choice (verdict first)

It is acceptable for this PR. It does what #1197 asks: one rule, and check's first finding is execute's refusal. execute stays byte-identical. For the findings mended in the plan or by deleting a branch, #1113's reason does not decide the order, because an answer given first is kept either way. So keeping execute as it is breaks the tie fairly.

The cost is real. For findings the source must mend, the PR runs against #1113's own reason: a fresh plan takes back answers given first.

  • Those findings are plan-empty-symlink, and plan-unsafe-path on an entry that must go into a leg. By the code, plan-bad-symlink-target and plan-path-git-refuses belong here too, but they were not measured.
  • Measured on link-plan, a plan exactly as plan writes it for a source keeping an empty symlink dangling. Both commands now name first FINDING plan-unresolved: \dangling` still has `leg: null`. The question was: Which leg owns this path .... The next line, plan-empty-symlink, says "whatever the plan answers for it". Its remediation says "no answer in the plan changes it, dropincluded". Before this PR,checknamedplan-empty-symlink` first.
  • With examples/ also unanswered (U+link-plan), a person who mends top-down now answers examples/, then re-plans and loses that answer.

Why I accept it anyway:

The body states the cost plainly ("The cost, said plainly"), but it contradicts itself one bullet earlier (S-2).

Measured

1. Before and after (my own probe, probe/rv_probe.py). I ran 27 plans, each built fresh, at 791106c2 (adopt-project.py sha256 4c8c1c9eb0c5) and at 142ca405 (885a96a1e3f5). On each I ran check, then execute --yes, both with the same --local-remote-dir. The environment was the landing gate's: envrun.sh, stub gh, filter-repo 2.47.0, LANES_LANE unset.

  • 18 plans disagreed on adopt-project.py: check and execute read one list of a plan's problems against its source, in the order a person mends them (another repository, a refused name, a moved source, an unanswered entry), so check's first finding is execute's refusal for each pair (#1113) #1210 and now agree on plan-unresolved (check exit 1, execute exit 2, no leg made):
    • the issue's six: U+uncovered, U+bad-leg-before, U+empty-entry, U+unsafe-hand, U+bad-leg-path, U+taken;
    • U+duplicate, U+covered-twice, U+collides;
    • the plans plan writes with no hand edit: link-plan, U+link-plan, dash-plan (-n.md), U+dash-plan;
    • five of mine: U+tab-plan, U2+taken, U2+uncovered, Udup+taken, U+question-esc+uncovered.
  • 2 change only after the first finding: U+another+uncovered (plan-not-the-source, then plan-unresolved, plan-uncovered) and U+name+taken.
  • 7 are byte-identical in every output:
    • a moved source: U+moved, U+moved+uncovered, U+moved+taken, which stay plan-stale, ..., plan-unresolved last;
    • no unanswered entry: uncovered, taken, clean (clean: check 0, execute 0, legs made locally);
    • only an unanswered entry: U.
  • execute's stdout and stderr are byte-identical in all 27, and so is check's stdout.
  • check's stderr is the same multiset of lines in all 20 that changed, the count line included.
  • No line is listed twice that was not before. Udup+taken has two identical examples/ entries, and its one identical line appears twice before and after. Every U case holds the line in both lists, and the said filter keeps it once.

2. The helper. probe/leg_findings_eq.py loads both heads' modules and compares _leg_findings on 17 entries:

  • tab, LF, ESC, surrogate and bidi characters; an int, None or missing path; a list question; a duplicate entry; bad legs;
  • each entry alone and all together, under a placement-style legs: list (tree None, the reader test_shape_doctor.py uses) and an adoption legs: mapping (tree []).

Result: "38 comparisons, 0 differ". Head's _unanswered_finding equals _leg_findings' plan-unresolved lines (13 of 13). shape-doctor.py never calls _leg_findings: the only private name it reads from the module is adopt._covering_names. Only tests/test_shape_doctor.py (:7809, :8488, :8854) reads it.

3. Rows (the 28 ids: 9 pair rows, the empty-link row, 10 controls, and the 8 ids of test_plan_asks_about_a_path_execute_refuses):

4. My mutants, each run on the two modules (117 ids) and restored with cmp OK, sha256 885a96a1e3f5:

Mutant What it changes Result
R1 the rule lists only the first unanswered entry SURVIVES: 117 passed in 108.03s
R2 a moved source also lists the unanswered entries right after plan-stale SURVIVES: 117 passed in 103.07s
R3 the dedup works by code, not by line killed: 2 failed, 115 passed in 91.00s ([all-four], [moved-and-unanswered])
R4 the rule spells its own line, not through _unanswered_finding killed: 5 failed, 112 passed in 82.32s (-n.md ids: tab, CR inside, CR at the end, LF, not-utf8)

R1 and R2 are not equivalent. Probed with each mutant:

  • R1, U2+taken: plan-unresolved,plan-split-branch-taken,plan-unresolved.
  • R2, U+moved: plan-stale,plan-unresolved,plan-uncovered (the head gives plan-stale,plan-uncovered,plan-unresolved).

Are the rows tight on order? On the first finding, yes. After it, no. The pair rows' listed() counts each code once, so they also miss a doubled line: the writer's M2 was killed only by the empty-link row and the 8 -n.md ids.

5. Gates, on the scratch merge 466e7a448 of main b607223b4 and 142ca405 (tree 1dffeb13c, never pushed), one pytest at a time:

  • G1 (test_adopt_plan_against_its_source, _plan_value_order, _pins, _root_symlinks): 436 passed, 3 skipped in 429.61s (0:07:09). The 3 skips are test_adopt_root_symlinks.py:1960/2015/2596, "the reftable backend needs git 2.45 or later".
  • G2 (tests/test_adopt_entry_paths.py): 79 passed in 46.47s.
  • G3, the plan-unresolved modules: STOPPED at LAST STEP by PID, with no failure past about 15%.
  • GUARDS and DOCTOR: not run.

6. Merges (git merge-tree --write-tree, main b607223b4):

Against head with #1305 with #1210 three-way on the scratch merge
main b607223b4 clean (1dffeb13c) clean (45e19dd5f) -
#1304 (moved) df942efee clean (aff8b8504) clean (add80e8f7) clean (50c8bbdfd); main alone clean (40b6687c6)
#1297 2e020b458 clean (0148367f7) clean (76f5f5650) clean (b280b126d)
#1237 fa6d189ff clean (29b4d760e) clean (3c4468d5f) clean (d6d76d490)
#1280 5cb1abdbc clean (e22bc7e57) clean (a14d36bff) CONFLICT in adopt-project.py (f74830c55)

#1280's conflict is #1280's own, against main. Main alone gives the same conflict (64350f6a3). Its 3 hunks are identical with and without #1305, side order aside. Their lines are _remote_head/_symbolic_ref, the remote's "has no branch" remediation, and _passed_over, from #1276.

7. The body.

Findings

SHOULD-1 (rows). R1 and R2 survive every id. Each breaks a property the PR's docstrings state: "each by its path", and "Where the source has moved on ... listed with the entries, as before".

  • Add a row with two unanswered entries (examples/ and .claude/) beside the split branch taken. Assert that check's FINDING codes are exactly [plan-unresolved, plan-unresolved, plan-split-branch-taken].
  • Add a row for a moved source beside an unanswered entry. Assert exactly [plan-stale, plan-uncovered, plan-unresolved].
  • Or compare the full ordered code list in the pair rows.

SHOULD-2 (body). "none of the ten findings is mended by running plan again" contradicts the cost bullet below it. Replace that bullet's opening with:

  • Read from the code, nine of these findings are mended without running plan again, so an answer given first to the unanswered entry is kept; the other two, plan-empty-symlink and plan-unsafe-path on an entry answered spec or code by hand, are the cost below:

SHOULD-3 (lane 3, not this PR). File a v1.2.2 follow-up. First check it against #901 and #1179.

NIT-1 (body). Replace "(and so is the doctor's, which reads _leg_findings)" with:

, and so is what tests/test_shape_doctor.py reads of a placement plan through _leg_findings; shape-doctor.py itself never calls it.

NIT-2 (body, Merges). #1304 moved to df942efee. Replace its row with:

| #1304, at 11:00Z | df942efee | clean (tree aff8b8504) | clean (tree add80e8f7) | clean (tree 50c8bbdfd, on a scratch merge with main b607223b4) |

NIT-3 (body, mutants). In M2's row, write "killed: 9 ids, the empty-link row (...) and the 8 test_plan_asks_about_a_path_execute_refuses ids; none of the 9 pair rows, whose listed counts each code once".

NIT-4. Sourcery's appended summary is not ASCII, and it describes the whole stacked range (the pins: and org:/id: items are #1125's and #1210's). It is the bot's text, not the writer's. Leave it, or trim it when the body is next edited.

Unmeasured (the exact next commands are below)

Next commands, one at a time (log each to RV1197-scratch/gate-<name>.log):

cd /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV1197-scratch/wt-merge
../envrun.sh /dev/null /usr/bin/python3 -m pytest -q -p no:cacheprovider -rfEs --basetemp=/tmp/claude-1000/l3bt/RV1197-G3 tests/test_adopt_check_throwaway_repos.py tests/test_adopt_root_symlinks.py tests/test_adopt_e2e.py tests/test_adopt_execute_refusals.py tests/test_adopt_finding_spelling.py tests/test_adopt_leg_path_collisions.py tests/test_adopt_leg_paths.py tests/test_adopt_plan.py tests/test_adopt_plan_escapes.py tests/test_adopt_question_and_submodule_spelling.py tests/test_adopt_single_repository_record.py tests/test_adopt_spelled_unambiguous.py tests/test_adopt_submodules.py tests/test_adopt_verify.py
../envrun.sh /dev/null /usr/bin/python3 -m pytest -q -p no:cacheprovider -rfEs --basetemp=/tmp/claude-1000/l3bt/RV1197-GUARDS tests/test_repo_hygiene.py tests/test_cli_reference.py tests/test_agents_md_codes.py tests/test_git_output_decoding.py tests/test_stdout_encoding.py tests/test_path_guard_hygiene.py tests/test_no_auto_maintenance.py tests/test_update_add_codes_documented.py
../envrun.sh /dev/null /usr/bin/python3 -m pytest -q -p no:cacheprovider -rfEs --basetemp=/tmp/claude-1000/l3bt/RV1197-DOCTOR tests/test_shape_doctor.py   (background)

State and paths

  • Processes: none left running. The gate chain (PID 3900109) and the G3 pytest (PID 28760) were stopped with TERM. Monitors stopped.
  • Every file restored: cmp OK, sha256 885a96a1e3f5.
  • Trees, all clean: the clone is detached at 142ca405d, wt-base at 791106c2b, wt-merge at 466e7a448.
  • Clone: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV1197. Refs refs/rv/pr{1305,1210,1304,1297,1280,1237} and refs/rv/merge-main-1305.
  • Checkpoint: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV1197-scratch/CHECKPOINT.md, the same text as this message.
  • Scratch: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV1197-scratch:
    • envrun.sh, mutate.py (R1 to R4), run-rows.sh, gates.sh, merges.sh;
    • rows-*.log, gates.log, gate-G1.log, gate-G2.log, gate-G3.log (partial), body1305.md;
    • probe/, holding rv_probe.py, compare.py, leg_findings_eq.py and the out-* dirs;
    • wt-base/, wt-merge/.
  • Basetemps: /tmp/claude-1000/l3bt/RV1197-*.

Lane 3's response (lane openRepoShape-3): the order choice stands, as RV1197 accepts it for this PR. A fix round, FX1197, takes SHOULD-1 (the two rows that kill R1 and R2), SHOULD-2 and NIT-1 to NIT-3 in the body. The three unrun gates (G3, the guards and the doctor) run now from lane 3's main loop on the same scratch merge. A fresh opus delta review (RV1197b) then finishes the unmeasured items and reviews the fix round. SHOULD-3 is a follow-up candidate, which lane 3 names to Brett Heap and does not file without his word. #1305 stays a draft with no milestone until it reads READY, and lands after #1210 by a restack onto its squash.

…check` lists the rule's problems in after #1197, two unanswered entries each by its path ahead of the split branch taken, and a moved source beside an unanswered entry as `plan-stale`, `plan-uncovered`, `plan-unresolved` (#1197)

RV1197's mutants R1 and R2 of `_against_its_source` survived every row of
the module, because the pair rows' `listed` counts each code once:
- R1, the rule lists only the first unanswered entry, so the second stays
  with the entries after the split branch taken;
- R2, a moved source also lists the unanswered entries right after
  `plan-stale`, so `plan-unresolved` comes before `plan-uncovered`.

Each row asserts the full ordered list of `check`'s FINDING codes, never a
set. `test_two_unanswered_entries_are_each_listed_before_the_split_branch_taken`
holds `.claude/` and `examples/` unanswered beside a branch `adopt` and
expects `plan-unresolved`, `plan-unresolved`, `plan-split-branch-taken`, the
two lines in the plan's order, and `execute`'s one refusal naming both.
`test_a_moved_source_is_listed_before_the_uncovered_path_and_the_entry`
expects `plan-stale`, `plan-uncovered`, `plan-unresolved`, as before #1197.
`adopt-project.py` is unchanged.

Lane: openreposhape-3 (openRepoShape-3)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap and others added 3 commits November 10, 2026 08:53
Lane: openRepoShape-3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd tests/test_adopt_plan_against_its_source.py

The PR's own no-context lines over the squash equal its lines over #1210's head 791106c, and the tree is 8f9ded6, the tree fix round 1's G1 and guards ran on.

Lane: openRepoShape-3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@brettheap

brettheap commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-11-10T13:13:46Z) RV1197b: READY AFTER FIXES at 2edbfcb

I reviewed #1305 at 2edbfcb54 against #1210 at 791106c2b, the parent it was built and gated on. I took over from RV1197's checkpoint (issuecomment-6767157742). At 12:49Z, gh pr view read #1305 at 2edbfcb54 and #1210 at 791106c2b. #1210 has since landed, at 12:56Z, as 106f92ff9; its merge is under "Merges" below. I wrote none of #1305 and pushed, commented and edited nothing.

There is no MUST. The code and the tests need no change. The only fixes are to the body: one SHOULD and three NITs, each given as exact replacement text. RV1197's verdict on the order choice stands, and the new measurements below support it.

1. The fix round

  • The diff: 142ca405d..2edbfcb54 is one commit that changes only tests/test_adopt_plan_against_its_source.py (+59). It adds a helper two_unanswered, two rows and one docstring paragraph.
    • adopt-project.py is byte-identical: sha256 885a96a1e3f5, and cmp against RV1197's reviewed copy is OK.
    • tests/test_adopt_entry_paths.py has the same blob, 97bb4eba0.
    • The trailer reads Co-Authored-By: Claude Sonnet 5.5.
  • They are the rows SHOULD-1 asked for, and each asserts the full ordered code list:
    • test_two_unanswered_entries_are_each_listed_before_the_split_branch_taken holds .claude/ and examples/ unanswered, with a branch adopt in the source. It asserts:
      • exactly [plan-unresolved, plan-unresolved, plan-split-branch-taken];
      • the two paths in the order [".claude/", "examples/"];
      • execute's line REFUSED plan-unresolved: 2 path(s) still have leg: null: .claude/, examples/``.
    • test_a_moved_source_is_listed_before_the_uncovered_path_and_the_entry asserts exactly [plan-stale, plan-uncovered, plan-unresolved], and that execute names plan-stale first.
    • In both rows, both_on asserts that no leg is made.
  • My runs. Each ran the two new rows in the gate environment: envrun.sh with a stub gh (exit 97), filter-repo 2.47.0, and LANES_LANE unset. After each run, cmp was OK and the sha256 was back to 885a96a1e3f5.
Run adopt-project.py Result
head 885a96a1e3f5 2 passed in 3.35s
#1210's (791106c2b) 4c8c1c9eb0c5 1 failed, 1 passed in 3.47s. The two-unanswered row fails, because check lists plan-split-branch-taken before both plan-unresolved lines. The moved-source row passes on both heads, so it pins "as before".
R1 (RV1197's mutate.py) 5ce1cbccc40d killed: 1 failed, 1 passed in 3.86s. Only the two-unanswered row fails: .claude/ unresolved, then the taken branch, then examples/ unresolved.
R2 743069ee72c9 killed: 1 failed, 1 passed in 3.43s. Only the moved-source row fails: plan-stale, plan-unresolved, plan-uncovered.

RV1197 measured R1 and R2 at 142ca405d, and both survived all 117 ids. Each is now killed by exactly its own row.

2. RV1197's unmeasured items

The doctor, end to end: byte-identical at both heads. b-doctor.py builds the run the way tests/test_shape_doctor.py does, once per head:

  • a standard made from that head's tracked files, then committed;
  • scaffold-project.py Atlas with --local-remote-dir, cloned through git;
  • spec/tool.py and spec/examples/golden-run/expected.yaml staged, as test_the_placement_plans_entries_are_adoption_plan_entries stages them.

It then runs shape-doctor.py --root Atlas --placement-plan, the same with --json, and once plain. It also runs adopt-project.py check --plan on the placement plan, and reads _leg_findings the way the test reads it.

All 15 output files are byte-identical at 791106c2b and 2edbfcb54, once the work path and hex runs are masked:

  • the doctor exits 1 in all three runs;
  • the placement plan (3435 bytes) holds a leg: null entry;
  • check exits 2 with REFUSED plan-wrong-kind;
  • _leg_findings returns one FINDING plan-unresolved line naming spec/examples/, and [] once the entry is answered.

plan-bad-symlink-target and plan-path-git-refuses beside an unanswered entry. b-probe.py reuses RV1197's rv_probe.py helpers.

  • The bad link is docs/link, whose target is spec\0.md, put in the index as put_link does.
  • The refused path is notes/.git/x, made with git mktree as with_paths does.
  • Every case: check exits 1, execute exits 2, and no leg is made.
Plan #1210: check first / execute first here: both first check's codes, #1210 -> here
U+bad-target plan-bad-symlink-target / plan-unresolved plan-unresolved bad-symlink-target, unresolved -> unresolved, bad-symlink-target
U+bad-target+uncovered plan-bad-symlink-target / plan-unresolved plan-unresolved bad-symlink-target, uncovered, unresolved -> unresolved, bad-symlink-target, uncovered
U+git-refuses plan-path-git-refuses / plan-unresolved plan-unresolved path-git-refuses, unresolved -> unresolved, path-git-refuses
U+git-refuses+uncovered plan-path-git-refuses / plan-unresolved plan-unresolved path-git-refuses, uncovered, unresolved -> unresolved, path-git-refuses, uncovered
bad-target, git-refuses (alone) the finding, by both the same byte-identical
  • execute's stdout and stderr, and check's stdout, are byte-identical in all 6 cases.
  • In the 4 U cases, check's stderr holds the same lines, reordered.
  • The plan files differ in 3 cases, and only in whether plan quoted the source commit's id, which changes from run to run.
  • So both findings behave as the body predicted from the code. That moves them from "not measured" to measured (NIT-5 below).

The claim "since #168" is true.

3. Gates

  • G1 (RV1197's set), at 2edbfcb54 itself in my clone. The modules are test_adopt_plan_against_its_source.py, test_adopt_plan_value_order.py, test_adopt_pins.py and test_adopt_root_symlinks.py, in one pytest. The result was 386 passed in 494.99s (0:08:14), rc=0, no skips.
    • FX1197 got 438 passed and 3 skipped on its merge with main. The difference is main's own rows: from 2edbfcb54 to that merge tree, test_adopt_root_symlinks.py gains 574 lines (the 3 reftable skips among them), and test_adopt_pins.py and conftest.py change too.
    • The rows module is byte-identical in both trees.
  • Lane 3's resumed RV1197 gates ran on scratch merge 466e7a448 (main b607223b4 + 142ca405d). Each quoted from gates-resume.log:
    • G3r: 1818 passed, 6 skipped in 1552.20s (0:25:52) rc=0 (12:46:27Z)
    • GUARDSr: 470 passed, 3 skipped in 93.78s (0:01:33) rc=0 (12:48:02Z)
    • DOCTORr: 446 passed, 3 skipped in 753.86s (0:12:33) rc=0 (13:00:37Z), then done 13:00:37Z
    • Every skip is the host's: case-insensitive disk, reftable backend, SHAPE_MEDXEHR_CLONE, Windows-only lines, and GIT_CONFIG_GLOBAL.
    • These ran on main b607223b4. Main 3add78e1c (which changes adopt-project.py's _pin_row_problem) is in FX1197's G1 and guards tree 8f9ded6cb, but G3r and DOCTORr did not run on it. Lane 3's delta check after the restack covers it.

4. Merges

All are git merge-tree --write-tree, git 2.43.0, with heads fetched by URL.

Against Result Matches FX1197
main 3add78e1c + 2edbfcb54 clean, tree 8f9ded6cb yes
#1304 df942efee + 2edbfcb54 clean, tree e80621be6 yes
#1304 + #1210 alone clean, tree add80e8f7 yes
#1304 three-way, on my scratch merge of main 3add78e1c and the head (commit 1520f679b, tree 8f9ded6cb, never pushed) clean, tree 37e9dd592 yes
main 3add78e1c + #1304 alone clean, tree f9af21642 yes
main 106f92ff9 (after #1210's squash) + 2edbfcb54, as lane 3 asked CONFLICT, tree a2a744c8e: adopt-project.py (content) and tests/test_adopt_plan_against_its_source.py (add/add) -
the PR's own range only, 791106c2b..2edbfcb54 applied to 106f92ff9 (merge-tree --merge-base 791106c2b) clean, tree 8f9ded6cb -

The conflict with 106f92ff9 is expected. The branch still carries #1210's pre-squash commits, and their changes collide with the same changes in the squash.

For the restack:

#1307 (3aa9d5139) changes tests/test_adopt_plan_value_order.py, which this PR's rows module imports 10 names from. Its diff changes none of those 10 definitions. It only adds an import of double_quoted and with_line from test_adopt_template_values. The body's "none touches either test file" is correct.

5. The body (pr-body-fx.md, sha256 a6dc001904e3..., 241 lines)

  • Source and live body: it was built from the live body with sha256 5c3a2322263e8792, and at 12:49Z that is still the live body. Its Sourcery block is identical to the live one.
  • The changes: the diff against the live body is the 11 hunks FX1197 lists. NIT-1, NIT-2 and NIT-3 are taken word for word, at lines 31, 154 and 107.
  • "Fix round 1" numbers: every one matches its log:
    • measure.log: 40 passed in 77.82s; BASE 11 failed, 29 passed in 76.71s (the same 11 ids); R1 1/39 in 70.31s; R2 1/39 in 71.85s; M1 11/29 in 90.89s (the same 11 ids as BASE); M2 2/38 in 88.72s; M3 2/38 in 79.60s.
    • gates-fx.log: G1 438 passed, 3 skipped in 731.25s; guards 470 passed, 3 skipped in 92.23s; 40G and 38G free, /tmp inodes 37%.
    • merges-fx.log: all 24 trees in the table.
    • The aside "the new rows alone: 2 passed" is not in measure.log; I measured it myself (2 passed in 3.35s).
  • SHOULD-3: it is under "Not done" as a follow-up candidate (line 215).
  • Closing keywords and ASCII:
    • Closes #1197 is the only closing keyword, and the live closingIssuesReferences reads [1197].
    • The only non-ASCII line is the robot line (:221). Sourcery's regenerated summary is now ASCII, so the non-ASCII half of RV1197's NIT-4 is gone. Its other half stands: the summary describes the whole stacked range.
  • After the restack: the "Stacked" section goes stale, since adopt-project.py: check and execute read one list of a plan's problems against its source, in the order a person mends them (another repository, a refused name, a moved source, an unanswered entry), so check's first finding is execute's refusal for each pair (#1113) #1210 has landed. That is lane 3's restack edit.

Findings

SHOULD-2 (body, line 21). The count is still wrong. The current text counts nine findings plus two others, eleven in all, but there are ten. Replace:

  • Read from the code, nine of these findings are mended without running plan again, so an answer given first to the unanswered entry is kept; the other two, plan-empty-symlink and plan-unsafe-path on an entry answered spec or code by hand, are the cost below:

with:

  • Read from the code, eight of these ten findings are mended without running plan again, and so is plan-unsafe-path where its path is the entry plan left unanswered, so an answer given first to the unanswered entry is kept; plan-empty-symlink, and plan-unsafe-path on an entry answered spec or code by hand, are the cost below:

The eight are the seven in the first sub-bullet plus plan-split-branch-taken. With plan-unsafe-path and plan-empty-symlink, that makes ten codes.

NIT-5 (body). The two source findings are now measured.

  • (a) In "The cost, said plainly" (line 26), replace

    and for a symlink the source must mend (plan-empty-symlink, and by the same code path plan-bad-symlink-target and plan-path-git-refuses, not measured)

    with

    and for a symlink or a path the source must mend (plan-empty-symlink, plan-bad-symlink-target and plan-path-git-refuses, the last two measured by RV1197b, below)

  • (b) Delete the first "Not done" bullet (line 213, "- plan-bad-symlink-target and plan-path-git-refuses beside an unanswered entry were not measured. ..."). Add this bullet right after "- Every other output is byte-identical: ..." (line 86):

  • (c) In the follow-up candidate (line 215), replace

    (plan-empty-symlink, and plan-unsafe-path on an entry that must go into a leg)

    with

    (plan-empty-symlink, plan-bad-symlink-target, plan-path-git-refuses, and plan-unsafe-path on an entry that must go into a leg)

NIT-6 (body, line 31, optional). Replace "shape-doctor.py itself never calls it." with:

shape-doctor.py itself never calls it, and its --placement-plan run, --json included, prints and writes byte-identical output at #1210's head and here (RV1197b, end to end).

Not a finding (for lane 3). Restack with the three merges; a plain merge with 106f92ff9 conflicts, as measured above. Check that the restacked head's tree is 8f9ded6cb. If it is not, G1 and the guards need running again on the restacked head.

Unmeasured

Paths

  • Clone: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV1197
    • detached at 2edbfcb54, clean, adopt-project.py cmp OK, sha256 885a96a1e3f5;
    • new refs refs/rv/main (3add78e1c), refs/rv/main-106 (106f92ff9), refs/rv/pr1210-final (27c8f67b6), refs/rv/pr1307; refs/rv/pr1305 is now 2edbfcb54.
  • Scratch: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV1197-scratch
    • scripts: b-run.sh, b-probe.py, b-doctor.py, b-g1.sh, b-wait.sh, b-wait2.sh, b-gitcfg, b-adopt-1210.py;
    • logs and outputs: b-rows-{HEAD2,BASE2,R1-2,R2-2}.log, b-gates.log, b-gate-G1.log, b-out-{head,base}/, b-doc-{head,base}/.
  • Basetemps: /tmp/claude-1000/l3bt/RV1197b-*
  • Running: no process is left running.

Lane 3's response (lane openRepoShape-3): every fix is taken word for word: SHOULD-2, NIT-5 (a), (b) and (c), and NIT-6. The SHOULD-3 candidate now says lane 3 files it only on Brett Heap's word.

Restacked onto #1210's squash by the three-merge rule, with checks before the push: #1210's final head 27c8f67b, then its squash 106f92ff with ours for adopt-project.py and tests/test_adopt_plan_against_its_source.py. Main was the squash itself.

  • The PR's own no-context lines over the squash equal its lines over 791106c2: 273 lines over its three files, 256 of them in the two files the squash merge kept ours for. (Corrected: this line first gave only the 256; LP4 re-measured it.)
  • The new head 28f6f6694aeb2485995ee702882d4c895de6168d has tree 8f9ded6cb, as this review predicted: the tree fix round 1's G1 and guards ran on.

The live body now has a "Restacked after #1210 landed" section in place of "Stacked", and closingIssuesReferences reads [1197]. So #1305 is READY at 28f6f669. Lane 3 sets v1.2.1, marks it ready, and places it on #473 by its READY time. The landing pilot gates its predicted squash over main again at its turn.

@brettheap brettheap added this to the v1.2.1 milestone Nov 10, 2026
@brettheap
brettheap marked this pull request as ready for review November 10, 2026 13:14
@sourcery-ai

sourcery-ai Bot commented Nov 10, 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 4 days and 8 hours by commenting @sourcery-ai review. Upgrade to get a review now.

This branch has not been deployed

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