Repository navigation
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
brettheap wants to merge 9 commits into
Conversation
… 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>
Reviewer's GuideImplementationCentralize the unanswered-entry finding and have
TestsAdd regression coverage for matching
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(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 ( The order choice (verdict first)It is acceptable for this PR. It does what #1197 asks: one rule, and The cost is real. For findings the source must mend, the PR runs against #1113's own reason: a fresh
Why I accept it anyway:
The body states the cost plainly ("The cost, said plainly"), but it contradicts itself one bullet earlier (S-2). Measured1. Before and after (my own probe,
2. The helper.
Result: "38 comparisons, 0 differ". Head's 3. Rows (the 28 ids: 9 pair rows, the empty-link row, 10 controls, and the 8 ids of
4. My mutants, each run on the two modules (117 ids) and restored with
R1 and R2 are not equivalent. Probed with each mutant:
Are the rows tight on order? On the first finding, yes. After it, no. The pair rows' 5. Gates, on the scratch merge
6. Merges (
#1280's conflict is #1280's own, against main. Main alone gives the same conflict ( 7. The body.
FindingsSHOULD-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".
SHOULD-2 (body). "none of the ten findings is mended by running
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
NIT-2 (body, Merges). #1304 moved to
NIT-3 (body, mutants). In M2's row, write "killed: 9 ids, the empty-link row (...) and the 8 NIT-4. Sourcery's appended summary is not ASCII, and it describes the whole stacked range (the Unmeasured (the exact next commands are below)
Next commands, one at a time (log each to State and paths
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>
Lane: openRepoShape-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
(lane openRepoShape-3, 2026-11-10T13:13:46Z) RV1197b: READY AFTER FIXES at 2edbfcb I reviewed #1305 at 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
RV1197 measured R1 and R2 at 2. RV1197's unmeasured itemsThe doctor, end to end: byte-identical at both heads.
It then runs All 15 output files are byte-identical at
The claim "since #168" is true.
3. Gates
4. MergesAll are
The conflict with For the restack:
#1307 ( 5. The body (
|
|
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 |



Restacked after #1210 landed
Built on #1210's head
791106c2be694e333e92b0b3acc38f254dc80dce. #1210 landed as106f92ff(its final head27c8f67b), and lane 3 restacked this branch with two merges: #1210's final head27c8f67b, then its squash106f92ff, with ours foradopt-project.pyandtests/test_adopt_plan_against_its_source.py. Main was the squash itself, so no third merge was needed. The head is28f6f6694aeb2485995ee702882d4c895de6168d.791106c2, without@@andindexlines: 273 lines over its three files, 256 of them in the two files the squash merge kept ours for. The head's tree is8f9ded6cb, the tree fix round 1's G1 (438 passed, 3 skipped) and guards (470 passed, 3 skipped) ran on.142ca405dand2edbfcb54(fix round 1); the two merges after them are the restack. Review its diff over main106f92ff.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 nowexecute's refusal,plan-unresolved, andcheckstill lists every other finding after it, in the same run, with the same remediation lines.examples/besiderelease.yamlin no entry.checknamedplan-uncoveredfirst; it now namesplan-unresolvedfirst, asexecuterefuses it.-n.mdplan (adopt-project.py:checkandexecutestill name different first problems for a plan with an unanswered entry beside an uncovered path,plan-uncoveredandplan-unresolved, a pair #1113's four do not cover (follows #1113) #1197's comment): the same forplan-bad-leg(on an entry sorted before the unanswered one),plan-empty-entry,plan-unsafe-path,plan-bad-leg-pathandplan-split-branch-taken, and for the planplanwrites for a source holding-n.md, with no hand edit.plan-duplicate-path,plan-covered-twice,plan-leg-path-collidesandplan-empty-symlink. The last one is also reached with no hand edit:planleaves the entry of a symlink whose target is empty atleg: nullitself.executeis unchanged: it refusedplan-unresolvedfirst 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
planagain, which takes back every answer given before") for an uncovered path.planagain, and so isplan-unsafe-pathwhere its path is the entryplanleft unanswered, so an answer given first to the unanswered entry is kept;plan-empty-symlink, andplan-unsafe-pathon an entry answeredspecorcodeby hand, are the cost below:plan-uncovered,plan-empty-entry,plan-duplicate-path,plan-covered-twice,plan-bad-leg,plan-bad-leg-pathandplan-leg-path-collidesare mended in the plan.execute's own remediation for them says so: "cover every source path exactly once, mount neither leg over a path the assembly root holds".plan-split-branch-takenis mended by deleting the branch or settingadopt_branch:(SPLIT_BRANCH_REMEDIATION). Deleting a branch does not move the source's commit, so nothing goesplan-stale.-n.mdplanplanwrites, the unsafe path IS the unanswered entry. Its question saysrootordropis the only answerexecutetakes, and either answer clears both findings:_unsafe_path_findingsskips an entry answeredrootordrop.execute's side keeps every other output as it was.executerefusesplan-unresolvedin adopt-project.py:checkandexecutestill name different first problems for four pairs with no bad id: stale and unanswered, another repository and unanswered, another repository and stale, a refused name and unanswered #1113's rule, before_checked_plan_values,_repository_urlsand_refuse_what_check_finds, which adopt-project.py:checkandexecuteread 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), socheck's first finding isexecute's refusal for each pair (#1113) #1210 orders and pins (unanswered-and-tracking, and rv1210d's NUL-root probe). To name the other finding first in both,executewould have to refuse those findings before the unanswered entry, ahead of the plan's values. That would changeexecute's order and words for plans adopt-project.py:checkandexecuteread 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), socheck's first finding isexecute's refusal for each pair (#1113) #1210 settles. Movingcheck's line changes only wherechecklists it.plan-unsafe-pathon an entry answeredspecorcodeby hand) and for a symlink or a path the source must mend (plan-empty-symlink,plan-bad-symlink-targetandplan-path-git-refuses, the last two measured by RV1197b, below), the mend is a commit in the source and a freshplan. That fresh plan takes back an answer given first.checklisted those first for that reason (adopt-project.pycheckpasses a source whose tree holds a symlink with an empty target, so with symlinks onexecutecreates both leg repositories and then fails cloning the source (on main with exit 2 and nothing on stderr, #171), and withcore.symlinks=falseit saysadoption verifiedover a leg or root that no clone with symlinks on can check out #303, in_entry_findings). Butexecutehas refused an unanswered entry before them since adopt-project.pyexecuterefuses onlyplan-unresolvedandplan-stale, so a plancheckrejects as uncovered or with a bad leg creates and pushes both legs before it fails verification #168 (_refuse_what_check_findsruns after_refuse_an_unrunnable_plan), so a person metplan-unresolvedatexecuteeither way.checknow says whatexecutesays first, and still prints the link's or the path's finding and remediation ("no answer in the plan changes it,dropincluded") in the same run. Putting the source's own findings first in both commands would be a change toexecute's order, outside adopt-project.py:checkandexecutestill name different first problems for a plan with an unanswered entry beside an uncovered path,plan-uncoveredandplan-unresolved, a pair #1113's four do not cover (follows #1113) #1197; lane 3 can file it if it is wanted._refuse_an_unrunnable_planraisesplan-staleand never asks about the entries, so the rule yields no unanswered entry.checklists it with the entries, as before; the fresh plan asks it again. Measured: byte-identical (U-moved,U-moved-uncoveredbelow).What changed, in
adopt-project.py_unanswered_finding(new): the one spelling ofcheck'splan-unresolvedline for an entry,_spelledpath and_shownquestion, as_leg_findingswrote it inline._leg_findingscalls it, so its output is unchanged, and so is whattests/test_shape_doctor.pyreads of a placement plan through_leg_findings;shape-doctor.pyitself never calls it, and its--placement-planrun,--jsonincluded, prints and writes byte-identical output at adopt-project.py:checkandexecuteread 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), socheck's first finding isexecute's refusal for each pair (#1113) #1210's head and here (RV1197b, end to end)._against_its_source: an unanswered entry is yielded as_Unmended(refusal, found), with one_unanswered_findingline per unanswered entry, in the plan's order. It was yielded with no FINDING lines.executereads only.refusal, so it is unchanged.cmd_check: it lists the rule's lines first, as before, and then the entry findings less any line the rule has listed (said), so each unanswered entry is listed once, now with the rule's problems._against_its_sourcesays where the unanswered entry is listed and why (the paragraph above, shortened), and_entry_findings' adopt-project.pycheckpasses a source whose tree holds a symlink with an empty target, so with symlinks onexecutecreates both leg repositories and then fails cloning the source (on main with exit 2 and nothing on stderr, #171), and withcore.symlinks=falseit saysadoption verifiedover a leg or root that no clone with symlinks on can check out #303 paragraph points at it.Measured: before and after, over 39 probe sources
Measured on #1210's head
791106c2b(adopt-project.pysha2564c8c1c9eb0c5) and on the first commit's head142ca405d(885a96a1e3f5, which fix round 1 leaves as it is), by one probe (W1197-scratch/probe.py). Each source is made fresh, planned withconftest.planned, edited as named, and thencheckandexecute --yesare run, both with the same--local-remote-dir(executealso with--work-dir). The environment was the landing gate's: a stubghfirst on PATH (exit 97),GH_TOKEN=bogus, an emptyGH_CONFIG_DIR,GIT_SSH_COMMAND=false, an empty global git config withGIT_CONFIG_NOSYSTEM=1,LANES_LANEunset, 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-isexamples/left unanswered (leg: null);-planis a plan asplanwrote it, with no hand edit;unsafe-planis a source holding-n.md, whichplanleaves unanswered itself;empty-symlinkis a source keeping a symlink whose target is empty, whichplanalso leaves unanswered. No run made a leg, exceptnone(every entry answered, nothing wrong), whichcheckpasses andexecuteverifies, on both heads.checkfirst, beforeexecutefirst, beforecheckstderrU-uncoveredplan-uncoveredplan-unresolvedplan-unresolvedU-bad-leg-beforeplan-bad-legplan-unresolvedplan-unresolvedU-empty-entryplan-empty-entryplan-unresolvedplan-unresolvedU-unsafe-planplan-unsafe-pathplan-unresolvedplan-unresolvedU-unsafe-handplan-unsafe-pathplan-unresolvedplan-unresolvedU-bad-leg-pathplan-bad-leg-pathplan-unresolvedplan-unresolvedU-takenplan-split-branch-takenplan-unresolvedplan-unresolvedU-duplicate-pathplan-duplicate-pathplan-unresolvedplan-unresolvedU-covered-twiceplan-covered-twiceplan-unresolvedplan-unresolvedU-collidesplan-leg-path-collidesplan-unresolvedplan-unresolvedU-empty-symlinkplan-empty-symlinkplan-unresolvedplan-unresolvedU-unsafe-plan-and-examplesplan-unsafe-pathplan-unresolvedplan-unresolvedUplan-unresolvedplan-unresolvedplan-unresolvedU-bad-leg-afterplan-unresolvedplan-unresolvedplan-unresolvedU-trackingplan-unresolvedplan-unresolvedplan-unresolvedU-nul-rootplan-unresolvedplan-unresolvedplan-unresolvedU-moved-uncoveredplan-staleplan-staleplan-staleU-movedplan-staleplan-staleplan-staleU-another-uncoveredplan-not-the-sourceplan-not-the-sourceplan-not-the-sourceU-name-uncoverednaming-unclassifiednaming-unclassifiednaming-unclassifiedU-another-takenplan-not-the-sourceplan-not-the-sourceplan-not-the-sourceuncoveredplan-uncoveredplan-uncoveredplan-uncoveredbad-leg-beforeplan-bad-legplan-bad-legplan-bad-legempty-entryplan-empty-entryplan-empty-entryplan-empty-entryunsafe-handplan-unsafe-pathplan-unsafe-pathplan-unsafe-pathbad-leg-pathplan-bad-leg-pathplan-bad-leg-pathplan-bad-leg-pathtakenplan-split-branch-takenplan-split-branch-takenplan-split-branch-takenduplicate-pathplan-duplicate-pathplan-duplicate-pathplan-duplicate-pathcovered-twiceplan-covered-twiceplan-covered-twiceplan-covered-twicecollidesplan-leg-path-collidesplan-leg-path-collidesplan-leg-path-collidesempty-symlinkplan-empty-symlinkplan-unresolvedplan-unresolvedbad-leg-afterplan-bad-legplan-bad-legplan-bad-legtrackingunsafe-valueunsafe-valueunsafe-valuenul-rootadopt-bad-project-nameadopt-bad-project-nameadopt-bad-project-namemoved-uncoveredplan-staleplan-staleplan-staleanother-uncoveredplan-not-the-sourceplan-not-the-sourceplan-not-the-sourcename-uncoverednaming-unclassifiednaming-unclassifiednaming-unclassifiedanother-takenplan-not-the-sourceplan-not-the-sourceplan-not-the-sourcenoneplan ok, exit 0,executeverified)checkandexecuteread 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), socheck's first finding isexecute's refusal for each pair (#1113) #1210's head, and each now gives one first problem,plan-unresolved, to both.check's stderr in each holds the same lines as before, the count line included, reordered: the unanswered entry's lines moved up. Measured by sorting each stderr's lines; every one is the same multiset.check's order after its first finding: another repository or a refused name, beside an unanswered entry and an uncovered path or a taken split branch. The first finding is the rule's, as before, and the unanswered entry now comes right after it.execute's stdout and stderr in all 39,check's stdout in all 39, andcheck's stderr in the other 23. Among them, a moved source beside an unanswered entry (U-moved,U-moved-uncovered), an unanswered entry before the plan's values (U-tracking,U-nul-root, adopt-project.py:checkandexecuteread 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), socheck's first finding isexecute's refusal for each pair (#1113) #1210's own pins), and each finding alone.docs/linkwhose target isspec\0.md(plan-bad-symlink-target) and a pathnotes/.git/x(plan-path-git-refuses), each beside an unansweredexamples/, and each again withrelease.yamluncovered. On adopt-project.py:checkandexecuteread 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), socheck's first finding isexecute's refusal for each pair (#1113) #1210's headchecknamed the source's finding first andexecuterefusedplan-unresolved; here both nameplan-unresolvedfirst.execute's stdout and stderr andcheck's stdout are byte-identical,check's stderr holds the same lines reordered, and each finding alone is byte-identical on both heads.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 itsWITH_AN_ORDINARY_FINDINGleftunanswered-and-takenout by name "(#1197)". Each row runscheckand thenexecute --yeson one plan, both with the same--local-remote-dir, andboth_onasserts that no leg is made.test_the_rules_problem_is_named_before_an_ordinary_finding:unanswered-and-taken,uncovered,bad-leg,empty-entry,unsafe-path,bad-leg-path,duplicate-path,covered-twiceandcollides. Each asserts what adopt-project.py:checkandexecuteread 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), socheck's first finding isexecute's refusal for each pair (#1113) #1210's rows assert:checkexits 1 andexecuteexits 2, both nameplan-unresolvedfirst,executesaysREFUSED plan-unresolved:, andchecklists the ordinary finding after it. The eight new ordinary findings are hand edits inORDINARY, beside adopt-project.py:checkandexecuteread 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), socheck's first finding isexecute's refusal for each pair (#1113) #1210's two; an uncovered path is adopt-project.py:checkandexecutestill name different first problems for a plan with an unanswered entry beside an uncovered path,plan-uncoveredandplan-unresolved, a pair #1113's four do not cover (follows #1113) #1197's own edit (release.yaml's entry taken out, inholding).test_an_empty_link_plan_leaves_unanswered_is_named_unanswered_by_both: a source keepingdangling, a symlink whose target is empty, made in the index astest_adopt_empty_symlinks.pymakes it.planleaves its entry atleg: nullitself. Both commands nameplan-unresolvedfirst,executenamesdangling, andchecklists exactlyplan-unresolvedand thenplan-empty-symlink.test_each_ordinary_finding_alone_is_named_by_both, one perORDINARYfinding, every entry answered: both commands name that finding first,checkexit 1 andexecuteexit 2, andchecklists none of the rule's four. They prove each hand edit makes the finding it is named for, and they pass on adopt-project.py:checkandexecuteread 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), socheck's first finding isexecute's refusal for each pair (#1113) #1210's head too.test_two_unanswered_entries_are_each_listed_before_the_split_branch_takenandtest_a_moved_source_is_listed_before_the_uncovered_path_and_the_entry. Each asserts the full ordered list ofcheck's FINDING codes, never a set. See "Fix round 1".ANSWERSfromtest_adopt_pins.pyandmake_source_repoandplannedfromconftest.py, andBAD_LEG_PATHfromtest_adopt_plan_value_order.pybeside what adopt-project.py:checkandexecuteread 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), socheck's first finding isexecute's refusal for each pair (#1113) #1210 imports from it: constants and helpers, no fixture.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.mdcase of #1197's comment for each nameentry_path_problemrefuses:planwrites the entry atleg: null,checkfoundplan-unsafe-pathand thenplan-unresolved, andexecuterefusedplan-unresolved. It pinned that disagreement; it now expectsplan-unresolvedand thenplan-unsafe-path, still 2 findings, andexecutestill refusesplan-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, withadopt-project.pyreplaced and then restored withcp, checked withcmpand sha256 (885a96a1e3f5, the same after each). Unmutated, the 38 rows oftests/test_adopt_plan_against_its_source.pypassed (38 passed in 51s). Fix round 1 runs the module again, with its two rows (40 ids), under "Fix round 1".adopt-project.py(791106c2b, sha2564c8c1c9eb0c5)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.checklists it with the entries againchecklists the entries' ownplan-unresolvedlines as well as the rule'splan-unresolved,plan-empty-symlink,plan-unresolved) and the 8test_plan_asks_about_a_path_execute_refusesids (3 findings, not 2); none of the 9 pair rows, whoselistedcounts each code oncechecklists the rule's unanswered entry after the split branch taken, before the entriesunanswered-and-takenM1 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_LANEunset). They ran on the scratch merge1d77ae06eof main34eb466e7and the first commit's head142ca405d(tree450d8a0f2, 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 andtests/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.test_adopt_plan_against_its_source.py,test_adopt_plan_value_order.py,test_adopt_pins.py,test_adopt_root_symlinks.pyplan-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,_verifyplan-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_pathstest_adopt_check_plan_name,_relative_submodule_url,_c_locale_bytes,_submodule_url_answers,_project_is_the_sourcetests/test_scaffold_leg_path_collisions.py, withtest_adopt_pins.py, which it imports fromtest_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_documentedtests/test_shape_doctor.py, once, in the background, with a logEvery skip is the host's, or the module's own, and none is this PR's:
test_adopt_root_symlinks.py, the reftable backend needs git 2.45 (this host has 2.43.0).test_adopt_plan.py:567,SHAPE_MEDXEHR_CLONEis unset.test_adopt_check_throwaway_repos.py, which need a disk that ignores case; 8 intest_leg_path_yaml_scalars.py, an empty parameter set or "the rule has nothing to refuse".test_shape_doctor.py:10516, the PowerShell line, only on a Windows host.test_stdout_encoding.py:292, Windows' own default; 2 intest_no_auto_maintenance.py:222, because the session setsGIT_CONFIG_GLOBAL, as rv1210d recorded for adopt-project.py:checkandexecuteread 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), socheck's first finding isexecute's refusal for each pair (#1113) #1210.At
142ca405ditself, 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 touchadopt-project.py(#1210 among them); none touches either test file but #1210, which this PR is on.142ca405d1d77ae06e(main + this head)34eb466e7450d8a0f2)8c8a3baf5)b607223b41dffeb13c)45e19dd5f)791106c2b19e731cdcce1dd661f2c8a992e9)1329736ac)46bbf192e)906172b410112028e0)a840bd6aa)df87a969a)d38e4e36dcf110e656)61c72875a)b1be019ab)cc230f2f103a590b83)f801fcc1c)d8f12c8bb)fa6d189ff29b4d760e)3c4468d5f)3b831588e)3146c3ca54a75e7d87)e4db47ac9)76e9593a6)39b21c07e794cda8e3)70ad1688f)3e300baab)5cb1abdbce22bc7e57)a14d36bff)adopt-project.py, 3 hunks (tree1bd5228c3)ce9fbee6e7d82ba10b)5ca08a780)c196a02ce)2e020b4580148367f7)76f5f5650)43ce2bfc8)df942efeeaff8b8504)add80e8f7)50c8bbdfd, on a scratch merge with mainb607223b4)#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'sorigin/HEAD, of theRemediation:that saysoriginhas no branch, and of the tracking-branch check (_refuse_unlisted_refsagainst_passed_over). The same three-way merge with main + #1210 alone (scratch merge4aba757f1) gives the same 3 hunks, byte for byte (tree6520b8df5). No hunk touches a line this PR changes.Main moved during the gate, from
34eb466e7tob607223b4(two commits:validate-manifest.pyandvalidate-family.pyin the templates,tests/conftest.pyand four test files). None of them is a file this PR changes or one of the gated modules buttest_pinned_copy_text.py, which names none of the codes this PR moves; so the gates below, run on main34eb466e7, were not run again.Fix round 1
RV1197's review of this PR at
142ca405d(issuecomment-6767157742) mutated_against_its_sourcetwice, and both mutants survived every row of the module (SHOULD-1). The pair rows'listedcounts each code once, so they see neither a second unanswered entry left with the entries nor an unanswered entry listed ahead ofplan-uncovered:U2+taken,checkthen listsplan-unresolved,plan-split-branch-taken,plan-unresolved.plan-stale. On RV1197's probeU+moved,checkthen listsplan-stale,plan-unresolved,plan-uncovered; the head givesplan-stale,plan-uncovered,plan-unresolved.This round adds the two rows that kill them, in one commit,
2edbfcb54, which changes onlytests/test_adopt_plan_against_its_source.py(+59 lines, the two rows, a helper and one docstring paragraph).adopt-project.pydoes not change: its sha256 is still885a96a1e3f5. Each row asserts the full ordered list ofcheck's FINDING codes, never a set, and runscheckand thenexecute --yeson one plan with the same--local-remote-dir, as the rows above do.test_two_unanswered_entries_are_each_listed_before_the_split_branch_taken(kills R1):.claude/andexamples/both left atleg: null,release.yamlanswered, and a branchadoptin the source.checkexits 1 with exactlyplan-unresolved,plan-unresolved,plan-split-branch-taken, the two unanswered lines naming.claude/and thenexamples/;executeexits 2 and refusesplan-unresolved: 2 path(s) still haveleg: null:.claude/,examples/``.test_a_moved_source_is_listed_before_the_uncovered_path_and_the_entry(kills R2): a source with a commit afterplan, andexamples/unanswered.checkexits 1 with exactlyplan-stale,plan-uncovered,plan-unresolved, andexecuteexits 2 and refusesplan-stalefirst. It pins "as before": it passes on adopt-project.py:checkandexecuteread 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), socheck's first finding isexecute's refusal for each pair (#1113) #1210'sadopt-project.pytoo.Runs. The module,
tests/test_adopt_plan_against_its_source.py, 40 ids, one pytest each, in the landing gate's environment (stubgh,GH_TOKEN=bogus, an emptyGH_CONFIG_DIR,GIT_SSH_COMMAND=false, an empty global git config,LANES_LANEunset, git-filter-repo 2.47.0),adopt-project.pyreplaced and then restored withcp, checked withcmpand sha256 (885a96a1e3f5, the same after each):adopt-project.py(791106c2b, sha2564c8c1c9eb0c5)plan-stalechecklists the entries' ownplan-unresolvedlines as well as the rule'stest_plan_asks_about_a_path_execute_refusesintests/test_adopt_entry_paths.pykill it too, as in the table above; that file is unchanged)checklists the rule's unanswered entry after the split branch takenunanswered-and-takenand the first new rowGates. Only one test file changes, so the gates above stand for everything else:
adopt-project.pyand every other test file are byte-identical to142ca405d. The rows module and the guards ran again, one pytest at a time,-rs, on a scratch mergeefd284e52of main3add78e1cand this head2edbfcb54(tree8f9ded6cb, 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:test_adopt_plan_against_its_source.py,test_adopt_plan_value_order.py,test_adopt_pins.py,test_adopt_root_symlinks.py, in ONE pytesttest_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_documentedEvery skip is the host's, and none is this PR's: 3 in G1,
test_adopt_root_symlinks.py:1960,:2015and: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 intest_no_auto_maintenance.py:222, because the session setsGIT_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 to3add78e1csince the table above. Of these PRs only #1304, #1297, #1237 and #1280 touchadopt-project.py, and none touches either test file:2edbfcb54efd284e523add78e1c8f9ded6cb)ee6de2a20)791106c2bdf942efeee80621be6)add80e8f7)37e9dd592)f9af21642)3aa9d5139205474910)d12ad74ab)4c31bbf20)6e0e94d8c)2e020b458564e56473)76f5f5650)f32a25df9)5275d2792)fa6d189ff57b73b6d8)3c4468d5f)16cbda3f7)efbe35854)5cb1abdbce529bf31d)a14d36bff)adopt-project.py, 3 hunks (treef012b8701)adopt-project.py, 3 hunks (tree2a24f5f5e)#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 isexecute's first refusal for the pair (unanswered-and-uncovered), with a row that runscheckand thenexecute --yeson one hand-edited plan and asserts that no leg is made.bad-leg,empty-entry,unsafe-path,bad-leg-path,taken), four more measured here (duplicate-path,covered-twice,collides, and the empty linkplanleaves unanswered), and the-n.mdplanplanwrites (test_plan_asks_about_a_path_execute_refuses, 8 names).Not done
execute's refusal of them ahead of the plan's values. It is lane 3's to file, if wanted.checkandexecute: one bad plan value is named in different orders, or by two or three findings, and two pin sentences say more thanexecutedoes #901 and tests/test_adopt_plan_value_order.py: follow-up of PR #1125 (#1020) from its review: four mutants of the head ask (L1, L2, L4, L11) that none of its 85 rows kills, so a badid:ororg:beside a--work-dirthat is a file or a shared one, or beside a gone source, is pinned through neitherchecknorexecute#1179): the source's own findings that no answer mends (plan-empty-symlink,plan-bad-symlink-target,plan-path-git-refuses, andplan-unsafe-pathon an entry that must go into a leg) should come before an unanswered entry in both commands, by adopt-project.py:checkandexecutestill name different first problems for four pairs with no bad id: stale and unanswered, another repository and unanswered, another repository and stale, a refused name and unanswered #1113's reason. It would also settle a disagreement that predates this PR, wherechecknames an entry finding first andexecuterefuses an unsafe plan value first:link-dropped+tracking(danglinganswereddrop,tracking_branch: "a b") givesplan-empty-symlinkagainstunsafe-valueat791106c2and at142ca405, and at the headuncovered+trackinggivesplan-uncoveredandtaken+trackinggivesplan-split-branch-taken, each againstunsafe-value.Closes #1197
Lane: openreposhape-3 (openRepoShape-3)
🤖 Generated with Claude Code
Summary by Sourcery
Align
check’s first finding withexecuteby prioritizing unresolved plan entries without suppressing other plan diagnostics.Bug Fixes:
checkreport unresolved plan entries before other entry findings, matchingexecute’s refusal order while retaining all related diagnostics.Enhancements:
Tests: