Skip to content

adopt-project.py: execute's NEXT prints python3 scripts/bootstrap.py where GNU make at the root reads a kept GNUmakefile or makefile, and its NOTE names that file and what it takes for make bootstrap to run there (#1047) - #1121

Merged
brettheap merged 3 commits into
mainfrom
fix/1047-next-make-bootstrap
Oct 23, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

GNU make reads the first it finds of GNUmakefile, makefile and Makefile, and only that one. Where the source keeps GNUmakefile or makefile in the root, make bootstrap at the root of the clone reads the source's file, not the shape's Makefile, and exits 2 with "No rule to make target 'bootstrap'". execute's NEXT printed it there anyway. Closes #1047.

This PR also covers the third box of #1048 (see #1048): the _next_steps NOTE that said the shape's targets "arrive only once the follow-up merges the two", which is untrue where GNU make reads a kept GNUmakefile or makefile. #1105 covers that issue's other two boxes, and leaves this one to this PR.

Measured

GNU Make 4.3 on Linux, a disk that keeps case. One directory per set of files, each file with a which target:

files make which reads make -n bootstrap
GNUmakefile GNUmakefile exit 2, no rule
makefile makefile exit 2
Makefile (with a bootstrap rule) Makefile exit 0
GNUmakefile + makefile GNUmakefile exit 2
GNUmakefile + Makefile GNUmakefile exit 2
makefile + Makefile makefile exit 2
all three GNUmakefile exit 2

A GNUmakefile that holds include Makefile runs the shape's rule (exit 0), and so does make -f Makefile bootstrap.

What NEXT printed on main, from a real plan, check and execute --local-remote-dir, with the printed command run in a recursive clone of the split ("S" is the source's own scripts/bootstrap.py, kept with scripts/):

the root keeps NEXT on main in the clone
none make bootstrap exit 0
Makefile python3 scripts/bootstrap.py, #480's NOTE exit 0
makefile, GNUmakefile, or both make bootstrap exit 2, no rule
Makefile + makefile, Makefile + GNUmakefile, all three python3 scripts/bootstrap.py, and a NOTE saying the targets arrive once the two Makefiles are merged the command exits 0; make bootstrap exits 2, and still does after cat shape/Makefile >> Makefile
S + GNUmakefile, S + makefile, S + Makefile + GNUmakefile no command; the NOTE says to merge, "and then run make bootstrap" make bootstrap exits 2

What changed

adopt-project.py, _next_steps:

  • It takes surviving=, the paths the plan's entries send to the root (paths_for["root"], passed by cmd_execute).
  • It asks them for a kept GNUmakefile or makefile in GNU make's order, with _root_keeps, as _makefile_follow_up asks them. So NEXT names the same file as the plan's CONTRACTS_DIR follow-up.
  • Where it finds one, the command is python3 scripts/bootstrap.py. The NOTE names the file in the follow-up's words, and says make bootstrap and the shape's other make targets run there only once that file has them, or includes a Makefile that does:
    • the shape's Makefile is the root's: "NOTE the shape's Makefile is the root's Makefile, but GNU make at the root reads the source's GNUmakefile, the first of GNUmakefile, makefile and Makefile it finds, so make bootstrap and the shape's other make targets run there only once GNUmakefile has them, or includes a Makefile that does; python3 scripts/bootstrap.py is what the shape's make bootstrap runs."
    • the source also keeps its own Makefile: "NOTE the root keeps its own Makefile and the shape's is shape/Makefile, and GNU make at the root reads ..." (the same clause, and the same ending).
    • the source's own script went aside too: the merges first, as before, "and then run python3 scripts/bootstrap.py; GNU make at the root reads ...".
  • A root that keeps neither name gets the block it got before, byte for byte. That includes MAKEFILE, which GNU make never reads where case is kept.

Tests

tests/test_adopt_next_make_reads_first.py, new, 40 tests (38, and the 2 of the fix round below):

  • 13 rows word for word: none, Makefile, makefile, GNUmakefile, makefile+GNUmakefile, Makefile+makefile, Makefile+GNUmakefile, all three, S, S+Makefile, S+GNUmakefile, S+makefile, S+Makefile+GNUmakefile.
  • 3 rows that keep make bootstrap: MAKEFILE; longer names (makefile.am, Makefile.in, GNUmakefile.bak); and the names below the root.
  • 13 rows in which NEXT names the same file as _makefile_follow_up, for the same root paths.
  • 9 end to end rows, one for each row that keeps GNUmakefile or makefile. Each is a real execute, and its clone is made by pasting NEXT's own clone line. In that clone:
    • NEXT's lines are the row's;
    • make which prints the file the NOTE names;
    • make -n bootstrap has no rule;
    • the printed command exits 0 and ends bootstrap ok. For S rows, it does so once the shape's script is merged, as the NOTE says;
    • where a Makefile is kept, the follow-up's merge alone still leaves no rule;
    • an include Makefile in the file GNU make reads makes make -n bootstrap run the shape's recipe.

On main's adopt-project.py, the first 38 all fail. The 9 end to end rows fail on what NEXT prints. The unit rows that guard the unchanged cases fail there only for want of the surviving= keyword. M1 below shows the same rows passing and every row that keeps GNUmakefile or makefile failing. On this head, all 40 pass.

Mutants of the change, each restored and checked by sha256. Each run is the new file plus the five next tests of test_adopt_e2e.py (43 tests). Every mutant is killed:

mutant failed
M1 first never found 27
M2 makefile asked before GNUmakefile 6
M3 the command ignores first 6
M4 the branch for a Makefile kept beside first dropped 6
M5 the branch for a script gone aside ignores first 9
M6 the lookup folds case 7, two of them existing test_adopt_e2e.py rows
M7 cmd_execute passes no paths 9, the end to end rows

Gate on this head, named files only, one basetemp per run. Each run had EMAIL set, GIT_CONFIG_GLOBAL an empty file, GIT_CONFIG_NOSYSTEM=1, a stub gh first on PATH, GH_TOKEN=bogus and an empty GH_CONFIG_DIR:

  • the new file, test_adopt_make_reads_first, test_adopt_e2e: 98 passed, with the new file at 38 tests (at c04bb710; the fix round's gate is below).
  • test_adopt_github_exits, test_adopt_printed_sentences, test_repo_hygiene, test_readme_windows_quoting_sentence, test_cli_reference, test_agents_md_codes: 291 passed.
  • test_scaffold_leg_path_collisions: 104 passed.
  • test_adopt_contracts_dir, test_adopt_verify, test_adopt_pins, test_adopt_root_manifest, test_adopt_ignored_shape_files, test_adopt_nested_attributes, test_adopt_shape_pin_and_work_dir: 378 passed.
  • test_shape_doctor.py in 4 shards: 112 + 112 + 112 + 109 = 445 passed, and 3 skipped (the PowerShell line, which is printed only on a Windows host).

Fix round 1 (RV1121's SHOULD-1)

RV1121 found the code right and one test missing: nothing held cmd_execute to handing _next_steps(surviving=) only the root's paths. Its mutant X4, which passes paths_for["root"] + paths_for["code"] + paths_for["drop"], kept all 43 tests green. The case is a real one: the root-project-makefile rule sends a kept GNUmakefile to the root, and its reason says that moving a Makefile that only builds the code to the code leg "is a one-line edit of the plan" (contracts/path-classification.yaml, lines 236-237).

Two end to end rows, test_next_keeps_make_bootstrap_where_the_plan_sends_the_makefile_elsewhere[code] and [drop]. Each keeps GNUmakefile in the source, edits the plan to send it to leg: code (then leg: drop), runs a real execute, and pastes NEXT's own clone line. Each asserts that NEXT's lines after the clone are make bootstrap and nothing else, that the clone's root has no GNUmakefile, and that make -n bootstrap there exits 0 and runs python3 scripts/bootstrap.py.

The execute, paste and clone steps of the nine existing end to end rows moved into one helper, executed_and_cloned, which the two new rows share, so no block of the test is written twice. Those rows assert what they asserted. The file now holds 40 tests (38 and 2).

  • X4, on this head's adopt-project.py with the one line changed: the new file and the five next rows of test_adopt_e2e.py (45 selected): 2 failed, 43 passed. Both new rows fail on NEXT's lines, which are python3 scripts/bootstrap.py and the NOTE "the shape's Makefile is the root's Makefile, but GNU make at the root reads the source's GNUmakefile, ...", naming a file the root does not have. The file was restored and its sha256 read 60bad59a... again.
  • On main's adopt-project.py: the two rows pass, since main printed make bootstrap there. They hold the unchanged case against the new lookup, and X4 is the mutant that tells them apart.
  • On this head 1c5173758, same environment as above: the new file 40 passed, test_adopt_e2e.py 27 passed, test_repo_hygiene.py 100 passed.
  • Merge: git merge-tree --write-tree of this head with main at 823624d93: clean (tree 73ddafb2).

RV1121's four NITs are not in this PR; the lane files them.

Merge

Not in this PR

Lane 4 lands nothing outside v1.2.0 until after the v1.2.0 tag, on Brett's later word, so this is a draft.

🤖 Generated with Claude Code
Lane: openreposhape-4 (openRepoShape-4)

Summary by Sourcery

Fix adoption follow-up instructions for roots where GNU make prioritizes a source-kept makefile over the shape's Makefile.

Bug Fixes:

  • Update execute's NEXT instructions to run the shape's bootstrap script when GNU make would read a source-kept GNUmakefile or makefile without the shape's bootstrap target.
  • Clarify NEXT notes with the GNU make file-selection rules and the steps required for make bootstrap to work.

Enhancements:

  • Limit the file detection to paths the adoption plan keeps in the repository root and align the reported file with the Makefile follow-up.

Tests:

  • Add unit and end-to-end coverage for GNU make's file precedence, root path placement, generated NEXT instructions, and bootstrap execution.

…py` where GNU make at the root reads a kept `GNUmakefile` or `makefile`, and its NOTE names that file and what it takes for `make bootstrap` to run there (#1047)

GNU make reads the first it finds of `GNUmakefile`, `makefile` and
`Makefile`, and only that one. Where the source keeps `GNUmakefile` or
`makefile` in the root, `make bootstrap` there reads the source's file,
not the shape's `Makefile`, and exits 2 with "No rule to make target
'bootstrap'" (measured with GNU Make 4.3, a real `execute` and a
recursive clone of the split).

- A root that keeps `makefile`, `GNUmakefile` or both, and no `Makefile`
  of its own: NEXT printed `make bootstrap`. It now prints `python3
  scripts/bootstrap.py`, which runs the shape's bootstrap there, and a
  NOTE that names the file GNU make reads, in the words of the Makefile
  follow-up (`_makefile_follow_up`), and says `make bootstrap` runs
  there only once that file has the shape's targets, or includes a
  Makefile that does.
- A root that keeps its own `Makefile` beside one of them: the command
  was already `python3 scripts/bootstrap.py`, but the NOTE said the
  shape's targets arrive once the follow-up merges the two `Makefile`s.
  After that merge `make bootstrap` still has no rule. The NOTE now says
  which file GNU make reads instead. This is #1048's third box (see
  #1048).
- A root that also keeps its own `scripts/bootstrap.py`: NEXT names no
  command, and its NOTE said to run `make bootstrap` after the merges.
  It now says `python3 scripts/bootstrap.py`, with the same clause.

`_next_steps` reads the plan's root paths (`paths_for["root"]`), asked
with `_root_keeps` as `_makefile_follow_up` asks them. A root that keeps
none of those files gets the block it got before, byte for byte.

tests/test_adopt_next_make_reads_first.py, new: each row word for word,
the file NEXT names against the one the Makefile follow-up names, and a
real `execute` for each row that keeps `GNUmakefile` or `makefile`. In the
clone NEXT's own clone line makes, the printed command runs, `make -n
bootstrap` has no rule, the merge alone does not change that, and an
`include Makefile` in the file GNU make reads does.

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

sourcery-ai Bot commented Oct 21, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates adoption NEXT guidance to account for GNU make reading the first available root-level GNUmakefile, makefile, or Makefile, avoiding misleading make bootstrap instructions when source files shadow the shape’s Makefile; adds extensive unit and end-to-end regression coverage.

Flow diagram for GNU make file selection in adoption NEXT

flowchart TD
    A["adopt-project.py _next_steps"] --> B["Inspect surviving root paths"]
    B --> C{"GNUmakefile or makefile kept?"}
    C -->|Yes| D["NEXT: python3 scripts/bootstrap.py"]
    C -->|No| E["NEXT: make bootstrap"]
    D --> F["NOTE names the file GNU make reads"]
    F --> G["make bootstrap works after that file has or includes the shape targets"]
Loading

File-Level Changes

Change Details Files
Propagate root-surviving paths into NEXT-step generation and detect the file GNU make will read before Makefile.
  • Pass paths_for["root"] from cmd_execute to _next_steps via a keyword-only argument.
  • Use GNU_MAKE_READS and _root_keeps to select the first kept root-level GNUmakefile or makefile.
  • Preserve existing output byte-for-byte when neither name is kept, including case-sensitive and nested-name cases.
adopt-project.py
Make NEXT recommend the shape’s bootstrap script and accurately explain GNU make’s file-selection behavior.
  • Print python3 scripts/bootstrap.py whenever a kept GNUmakefile or makefile would shadow the shape’s targets.
  • Add NOTES identifying the exact file GNU make reads, its search order, and the requirement that it contain or include the shape’s targets.
  • Handle combinations involving the source’s own Makefile and/or scripts/bootstrap.py, including running the script after required merges.
adopt-project.py
Add comprehensive unit and end-to-end coverage for GNU make’s first-file behavior and NEXT output.
  • Cover all relevant combinations of Makefile, makefile, GNUmakefile, source bootstrap scripts, unrelated names, and nested paths.
  • Verify NEXT names the same file as the Makefile follow-up and retains unchanged output for unaffected cases.
  • Exercise real execute flows, pasted clone commands, GNU make failures before inclusion, and successful bootstrap-script execution.
tests/test_adopt_next_make_reads_first.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1047 When the root keeps a GNUmakefile or makefile, make execute's NEXT command run the shape's bootstrap instead of invoking make bootstrap against the source's file. ✅
#1047 Make NEXT's NOTE accurately identify which kept file GNU make reads first and explain what must change for make bootstrap and the shape's other targets to work there, including cases where the shape's Makefile or bootstrap script was moved aside. ✅
#1047 Preserve the existing NEXT behavior for root files GNU make does not select, and verify the relevant file-selection and clone behavior with tests. ✅

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

RV1121: independent review of #1121 at c04bb710: NOT READY

One SHOULD, a missing test row; the code is right. Everything NEXT prints, I measured true and pasteable on a disk that keeps case. The three texts that name the file GNU make reads first agree, on a merge of main, #1105 and this PR.

Measured

GNU make's read order (GNU Make 4.3, Linux, a disk that keeps case). Each file had a which target, and Makefile had a bootstrap rule:

the directory holds make which make -n bootstrap
GNUmakefile GNUmakefile exit 2
makefile makefile exit 2
Makefile Makefile exit 0
GNUmakefile + makefile GNUmakefile exit 2
GNUmakefile + Makefile GNUmakefile exit 2
makefile + Makefile makefile exit 2
all three GNUmakefile exit 2
MAKEFILE alone no makefile read ("No rule to make target 'which'") exit 2
MAKEFILE + Makefile Makefile exit 0
a directory GNUmakefile/ or makefile/, + Makefile "GNUmakefile: Is a directory. Stop." (the same for makefile) exit 2
GNUmakefile a symlink to Makefile the Makefile's rule runs exit 0

The PR body's table matches every row.

surviving= is _leg_paths(plan)["root"]: the CURRENT plan's entries whose leg: is root.

  • _makefile_follow_up asks the same set, {e.path for e in entries if e.leg == "root"}, at plan time. Both ask through _root_keeps, without folding case.
  • A name below the root, and a longer name, is not matched. A kept directory is.
  • A file the plan sends to a leg, or drops, is not in the set. Measured: see SHOULD-1.
  • The shape writes no GNUmakefile or makefile of its own.
  • Not measured: a leg MOUNTED at a path named GNUmakefile or makefile. I found no rule in leg_path_problem that refuses one. A mount is not in surviving, so NEXT keeps make bootstrap there, as main does.

The python3 line. The shape's Makefile runs $(PYTHON) scripts/bootstrap.py with PYTHON ?= python3, so python3 scripts/bootstrap.py is what its make bootstrap runs. NEXT prints the same command main's #480 NOTE prints for a kept Makefile (but see NIT-2).

The texts on main 823624d93 + #1105 5a4de7f76 + #1121 c04bb710. I built a scratch merge (tree dac63519) and ran a real plan, check and execute --local-remote-dir for four sources: Makefile+GNUmakefile, Makefile+makefile, GNUmakefile, and scripts/+Makefile+GNUmakefile.

Docs and pins. No README.md, docs/ (handbook, cli.md), templates/ or ADOPTED_SENTENCES text quotes a NEXT or NOTE line this PR changes. test_adopt_e2e.py (:275, :312, :369, :425) pins only the rows the PR keeps byte for byte.

Tests

The new file on main 823624d93: 38 failed, each for the right reason.

  • The 29 unit rows fail with TypeError: _next_steps() got an unexpected keyword argument 'surviving'.
  • The 9 end-to-end rows fail at :280, on NEXT's lines.

On a scratch merge of main 823624d93 and #1121 (tree 47091c4a, adopt-project.py blob identical to the head's):

  • the new file and the five test_next_* rows of test_adopt_e2e.py: 43 passed;
  • tests/test_adopt_next_make_reads_first.py: 38 passed;
  • test_adopt_e2e.py: 27 passed;
  • test_adopt_printed_sentences.py + test_repo_hygiene.py: 154 passed;
  • test_scaffold_leg_path_collisions.py: 104 passed;
  • test_shape_doctor.py in 4 shards: 118 + 110 + 117 + 100 = 445 passed, and 3 skipped. All three skips are the PowerShell line (:10510), which is printed only on a Windows host.

Each run used /usr/bin/python3 -m pytest -p no:cacheprovider with its own basetemp and tmp_path_retention_policy=failed. The environment:

  • EMAIL set, and GIT_CONFIG_GLOBAL an empty file;
  • GIT_CONFIG_NOSYSTEM=1, and test author and committer identities;
  • a stub gh first on PATH, GH_TOKEN=bogus and an empty GH_CONFIG_DIR;
  • GIT_ALLOW_PROTOCOL, GIT_SSH_COMMAND and the LANES_* variables unset.

Mutants (mine)

These are of the new lookup, its wiring in cmd_execute, and the new text. Each run was the new file plus the five test_next_* rows of test_adopt_e2e.py (43 tests). After each, git checkout restored the file, and its sha256 matched 60bad59a... again.

mutant result
X1 the lookup drops the directory form (name in surviving) survived, 43 passed
X2 the lookup matches a basename below the root killed, 1 failed (below-the-root)
X3 the lookup folds case (fold=True) killed, 7 failed, two of them existing test_adopt_e2e.py rows (the writer's M6 count)
X4 cmd_execute passes paths_for["root"] + paths_for["code"] + paths_for["drop"] survived, 43 passed
X6 the script-aside NOTE drops the reads clause killed, 9 failed
X8 runs says "is what make bootstrap runs" killed, 12 failed
X9 the command is printed even where the script went aside killed, 6 failed
X10 the listed order is sorted(GNU_MAKE_READS) killed, 18 failed
X12 python3 wherever surviving is not empty killed, 4 failed

7 of 9 were killed. X4 is SHOULD-1, and X1 is part of NIT-1.

Merge

git merge-tree --write-tree, all clean (exit 0):

Findings

SHOULD-1: no test holds surviving to the ROOT's paths.

Mutant X4 passes all 43 tests: cmd_execute hands _next_steps the code leg's and the dropped paths as well as the root's. This is a case the repository invites. root-project-makefile keeps GNUmakefile and makefile in the root, but its reason says moving such a file to the code leg "is a one-line edit of the plan" (contracts/path-classification.yaml:236-237).

I probed it on the scratch merge with a source that keeps GNUmakefile, and the plan edited to leg: code, then to leg: drop:

  • On the head: NEXT keeps make bootstrap, the clone has no GNUmakefile, and make -n bootstrap exits 0 there. Both rows passed, so the code is right.
  • Under X4: both rows fail. NEXT prints python3 scripts/bootstrap.py and "NOTE the shape's Makefile is the root's Makefile, but GNU make at the root reads the source's GNUmakefile, ...", which names a file the root does not have.

Fix: add those two rows to tests/test_adopt_next_make_reads_first.py. For each:

  1. Run write_plan, then replace - path: GNUmakefile\n leg: root\n with leg: code (or drop) and a resolution:.
  2. Run execute --local-remote-dir, and assert that NEXT is local_next(source) plus [" make bootstrap"].
  3. Paste the clone line. Assert that the clone has no GNUmakefile, and that make -n bootstrap exits 0.

The two rows took 9-12 s here.

NIT-1: a kept DIRECTORY named GNUmakefile/ or makefile/. GNU make stops on it with "Is a directory". NEXT now prints python3, which runs, so that much is better than main. But the NOTE's "only once GNUmakefile has them, or includes a Makefile that does" cannot hold for a directory. No row covers it, and X1 survives. _makefile_follow_up says the same, and the PR body names this. For a follow-up: the one shared helper the body plans could say the name is a directory, with a row for it.

NIT-2: python3 on Windows. The template's scripts/bootstrap.py says "python3 scripts/bootstrap.py (python on Windows)". NEXT now prints python3 in nine more cases, as main's #480 NOTE already did for a kept Makefile. For a follow-up: name the host's interpreter in all the NEXT rows at once.

NIT-3: wording across the three texts once #1105 lands. The file and the opening clause agree. The endings differ:

For a follow-up: align them in the shared helper the body already plans.

NIT-4: other make bootstrap sentences in the shape. templates/assembly-root/README.md (:17 and :34) and AGENTS-shape.md (:19, which the body names) give make bootstrap with no word on a kept GNUmakefile or makefile. For a follow-up, with #1105's template change.

Lane: openreposhape-4 (openRepoShape-4)

…ke bootstrap` where the plan sends a kept `GNUmakefile` to the code leg or drops it, since only the root's paths count and the clone's root has no such file, so a `_next_steps` handed the code leg's or the dropped paths as well fails two rows (#1047)

RV1121's review of #1121 found that nothing held `cmd_execute` to
handing `_next_steps(surviving=)` only the root's paths: the mutant that
passes `paths_for["root"] + paths_for["code"] + paths_for["drop"]` kept
all 43 tests green. The case is a real one: the `root-project-makefile`
rule sends a kept `GNUmakefile` to the root, and its reason says that
moving a Makefile that only builds the code to the code leg "is a
one-line edit of the plan".

Two end-to-end rows, one for `leg: code` and one for `leg: drop`. Each
runs a real `execute` of a source that keeps `GNUmakefile`, with the plan
edited as above, pastes NEXT's own clone line, and asserts that NEXT
keeps `make bootstrap`, that the clone's root has no `GNUmakefile`, and
that `make -n bootstrap` there exits 0 and runs `python3
scripts/bootstrap.py`. Under the mutant both rows fail on NEXT's lines:
`python3 scripts/bootstrap.py` and a NOTE naming a `GNUmakefile` the root
does not have.

The `execute`, paste and clone steps of the nine existing end-to-end rows
are now `executed_and_cloned`, which the two new rows share; those rows
assert what they asserted.

Lane: openreposhape-4 (openRepoShape-4)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

RV1121: independent review of #1121 at 1c517375: READY

This re-checks fix round 1, one commit on c04bb710 that changes only tests/test_adopt_next_make_reads_first.py (+67 -21). SHOULD-1 from my first review (issuecomment-6319598532) is done. No MUST or SHOULD remains, and the four NITs from that review stand for a follow-up.

The fix

The two new rows (leg: code, leg: drop) are right.

  • Each edits the plan's GNUmakefile entry the way root-project-makefile's reason invites, and runs a real execute.
  • Each asserts four things: NEXT's only line after the clone is make bootstrap; the clone's root has no GNUmakefile; make -n bootstrap exits 0; and its last line is the shape's recipe, python3 scripts/bootstrap.py.
  • They are the rows I probed by hand in round 1.

The refactor keeps what the nine end-to-end rows asserted. The rows now call executed_and_cloned. It asserts lines[:2] == local_next(source) and returns lines[2:], and each row then asserts that result == said.

  • local_next returns exactly two lines, so the two asserts together are the old lines == [*local_next(source), *said].
  • lines[0] of the slice is the old lines[2].
  • kept_scripts is still applied between plan and execute, and the clone is still pasted from NEXT's own clone line.

I measured it with the mutants below. X10 and X9 fail the same end-to-end rows, and the same totals (18 and 6), as on c04bb710.

On main's code (823624d93, run before the merge), the file now gives 38 failed and 2 passed. The 38 fail for the reasons I gave in round 1. The 2 that pass are the new rows: main prints make bootstrap there too, so they guard the wiring rather than the bug.

Mutants

These ran on a scratch merge of main 4ed7c7bf5 and 1c517375. Each run was the new file plus the five test_next_* rows of test_adopt_e2e.py (45 tests). After each, git checkout restored the file, and its sha256 matched 8f3cd060... again.

mutant c04bb710 1c517375
X4 cmd_execute passes paths_for["root"] + paths_for["code"] + paths_for["drop"] survived killed, 2 failed: both new rows
X1 the lookup drops the directory form survived survived (NIT-1, unchanged)
X10 the listed order is sorted killed, 18 killed, 18: 9 unit and 9 end to end
X9 the command is printed even where the script went aside killed, 6 killed, 6: 3 unit and 3 end to end

Tests

These ran on the scratch merge of main 4ed7c7bf5 and #1121 1c517375 (commit caaad5e9, tree f06f404f).

run result
the new file + the five test_next_* rows of test_adopt_e2e.py 45 passed
tests/test_adopt_next_make_reads_first.py 40 passed
test_adopt_e2e.py 27 passed
test_adopt_printed_sentences.py + test_repo_hygiene.py 154 passed
test_scaffold_leg_path_collisions.py 104 passed
test_shape_doctor.py in 4 shards 118 + 110 + 117 + 100 = 445 passed, and 3 skipped (the PowerShell line, which is printed only on a Windows host)
#1091's test_adopt_root_shape_paths.py + test_adopt_root_symlinks.py, since #1091 changed adopt-project.py on main 326 passed, and 3 skipped (the reftable backend needs git 2.45 or later)

The environment was the same as in round 1:

  • /usr/bin/python3 -m pytest -p no:cacheprovider, one basetemp per run, and tmp_path_retention_policy=failed;
  • EMAIL set, GIT_CONFIG_GLOBAL an empty file, GIT_CONFIG_NOSYSTEM=1, and test identities;
  • a stub gh first on PATH, GH_TOKEN=bogus and an empty GH_CONFIG_DIR;
  • GIT_ALLOW_PROTOCOL, GIT_SSH_COMMAND and the LANES_* variables unset.

Merge

git merge-tree --write-tree, all clean (exit 0):

Findings

The PR body already says the first 38 fail on main and that all 40 pass on this head. Both counts match what I measured.

Lane: openreposhape-4 (openRepoShape-4)

This was referenced Oct 21, 2026
@brettheap brettheap added this to the v1.2.1 milestone Oct 23, 2026
@brettheap
brettheap marked this pull request as ready for review October 23, 2026 21:35
@sourcery-ai

sourcery-ai Bot commented Oct 23, 2026

Copy link
Copy Markdown

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

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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sourcery assessment

Approved.

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit 0ed3e57 into main Oct 23, 2026
5 checks passed
@brettheap
brettheap deleted the fix/1047-next-make-bootstrap branch October 23, 2026 21:37
brettheap pushed a commit that referenced this pull request Oct 23, 2026
…teps` asks `_make_reads_first` once for a kept file or directory, words both through `_make_reads_first_clause`, and #1121's NOTEs name `python` on Windows

#1121 (#1047) and this branch both changed `_next_steps`, and git
stopped on four hunks of adopt-project.py (84 lines). The resolution:

- execute's comment names both: a kept `GNUmakefile` or `makefile` is
  what GNU make reads first (#1047), and a directory of either name
  stops it (#1132).
- The docstring keeps #1121's paragraph and this branch's two.
- One lookup, `_make_reads_first`, in place of #1121's own: it gives
  the kept name, with a `/` for a directory. The command line is
  `python3 scripts/bootstrap.py` where a `Makefile` went aside or
  `first` is set, as #1121 had it.
- One clause, `_make_reads_first_clause(first, ..., "has them")`. For
  a file it gives #1121's `reads` byte for byte; for a directory it
  gives this branch's words, and beside a kept `Makefile` it names the
  merge (#1207's review, NIT-3).
- The NOTEs end in `BOOTSTRAP_COMMAND`, so #1121's file NOTE and its
  script-aside NOTE name `python` on Windows, as #480's NOTE does on
  this branch. tests/test_adopt_next_make_reads_first.py moves three
  pins to that: OWN_MAKEFILE_NOTE, RUNS and script_aside. Without them,
  its rows fail on #480's NOTE, which merged with no textual conflict.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Nov 8, 2026
… leave out the trees that keep case twins, which execute now refuses as plan-case-twins (#1161)

`test_the_printed_bootstrap_runs_in_the_clone_next_makes` (#1121) ran a
real `execute` at `[Makefile+makefile]` and `[all-three]`, sources that keep
`Makefile` and `makefile` side by side in the root, each with its own blob.
Since #1161 `execute` refuses that set of case twins before any leg as
`plan-case-twins`, so there is no clone to run `make` in: at the merge
5c4fcd9 both failed (`REFUSED plan-case-twins`, `assert 2 == 0`).

Its parametrize now leaves out the rows `keeps_twins` (from
`test_adopt_make_reads_first`) names, as #1189 did there in 8684af8, and
its docstring says why, and that
`test_execute_refuses_makefile_beside_makefile_before_any_leg` pins the
refusal for those trees. Every other row is as it was: the test runs 7
rows, the file's other functions all of ROWS.

Measured on CPython 3.12, the whole file: at 5c4fcd9 2 failed, 38
passed; with this change 38 passed.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Nov 8, 2026
…eeps a `GNUmakefile` or a `makefile`, which GNU make reads first, the Makefile collision follow-up no longer says the shape's make targets arrive with the merge, and the pinned file says `make` reads the first of the three and the block reaches it only once that file holds it or includes a Makefile that does (#1048) (#1105)

Two sentences said a kept `Makefile` is what holds the shape's block back. GNU make reads the first of `GNUmakefile`, `makefile` and `Makefile` it finds, and only that one, so where the root also keeps `GNUmakefile` or `makefile`, `make` reads that file, and reaches the merged `Makefile` only where that file includes it.

**Measured** (a real `execute`, then `make -n` in the clone, for each kept name; r1046's NIT-B, repeated; the include rows are RV1105's SHOULD-1):

| the source keeps | file `make` reads | after `cat shape/Makefile >> Makefile`, `make -n bootstrap` |
|---|---|---|
| `Makefile` | `Makefile` | exit 0 |
| `Makefile` + `makefile` | `makefile` | exit 2, "No rule to make target 'bootstrap'" |
| `Makefile` + `GNUmakefile` | `GNUmakefile` | exit 2, same |
| all three | `GNUmakefile` | exit 2, same |
| `Makefile` + a `makefile` or `GNUmakefile` that ends in `include Makefile` | that file | exit 0, runs `python3 scripts/bootstrap.py` (exit 2 before the merge) |
| `makefile` or `GNUmakefile` alone | that file | no merge to make: the shape's `Makefile` is the root's, and `make contracts-dir` has no rule until that file holds the block or includes `Makefile` |

**What changed**
- `scripts/shape_materialize.py`, `collision_follow_up`: takes `reads_first`, the file GNU make reads before the shape's `Makefile`. Where it is set, the Makefile's follow-up says GNU make reads that file, the first of the three it finds, and that the shape's make targets reach `make` there only once that file has them, or includes a Makefile that does; until then `python3 scripts/bootstrap.py` is what the shape's `make bootstrap` runs. Where `scripts/bootstrap.py` went aside too, it says neither command runs the shape's bootstrap until the script is merged, and then `python3 scripts/bootstrap.py` runs it. Where the root keeps only the source's `Makefile`, the sentence is what it was, byte for byte. `GNU_MAKE_READS` joins `SHAPE_MAKE_TARGETS`.
- `adopt-project.py`, `_predict_collisions`: reads the file from the plan's root paths as `_makefile_follow_up` does, so the `CONTRACTS_DIR` line and the collision line of one plan name the same file.
- `templates/assembly-root/AGENTS-shape.md`: says `make contracts-dir` prints the block wherever `make` reads the shape's Makefile, names the three files in the order the code reads them, and says that where a `GNUmakefile` or `makefile` is kept, beside a `Makefile` or instead of one, `make` reads it, and not the shape's Makefile unless that file includes it. It is one verbatim copy: the digest in every project's `contracts/shape-pin.yaml` is rendered from its bytes at adopt and scaffold time, and no digest or `tree_sha256` of it is stored in this repository, so there is no pin to move here; each project re-pins through `update-shape.py` as for any template change.
- `tests/test_pinned_copy_text.py`: `ADOPTED_SENTENCES` holds the new text.
- `tests/test_adopt_collision_make_reads.py` (new, 31 rows): the follow-up per kept set, the two lines of one plan naming one file, no collision line where no `Makefile` is kept, the two name tuples equal, the pinned file naming every file the code reads first, the script-aside tail where `make` reads another file, and, in a real adoption's clone, `make -n` held to both sentences for each kept name and for a kept `GNUmakefile` or `makefile` that ends in `include Makefile`.

**Fix round 1** (RV1105's review, `b28f40de0` on top of `5a4de7f76`)
- **SHOULD-1:** the follow-up said the targets reach `make` "only once `X` holds them, or includes a Makefile that does, and not with that merge alone". The last clause is untrue where `X` already ends in `include Makefile`: the merge alone brings them. The clause is gone, and the docstring paragraph, the `SHAPE_MAKE_TARGETS` comment and the `ADOPTED_SENTENCES` comment now say the merge alone brings the targets only where that file includes `Makefile`.
- **NIT-1 (and RV1121's NIT-3):** the follow-up now uses the words of #1121's NOTE: "has them", and "is what the shape's `make bootstrap` runs". `_next_steps` is not touched.
- **NIT-2:** the script-aside tail, where `reads_first` is set, said "neither `make bootstrap` nor `python3 scripts/bootstrap.py` runs the shape's bootstrap before both merges are made", which reads as if `make bootstrap` runs it after them. It now says neither runs it until the script is merged, and then `python3 scripts/bootstrap.py` runs it. I did not keep "before both merges": where `X` is given the targets itself, as the sentence before invites, `make bootstrap` runs the shape's bootstrap once the script is merged, with no `Makefile` merge at all.
- **NIT-3:** the template's "and never the shape's Makefile" is now ", and not the shape's Makefile unless that file includes it". `ADOPTED_SENTENCES` changes with it. No other copy or regex in `tests/` pins that sentence (`git grep` for "never the shape" and "reads the first of them").
- **NIT-4:** the `collision_follow_up` docstring no longer says `execute` reports through it. It says `plan` predicts the follow-ups and stores them in `follow_ups:`, the split commit carries those, `execute` names its collisions by path only (its `beside` lines and the "WRITTEN BESIDE" list), and `Materialized.follow_ups()` has no caller. The method stays.
- NITs 5 to 8 are left to the lane.

**Rows of this round:** `tests/test_adopt_collision_make_reads.py` and `tests/test_pinned_copy_text.py`, 118 rows. With this round's test files on `5a4de7f76`'s code, 17 fail and 101 pass; on `b28f40de0` all 118 pass. The 17 fail for the right reason:

| rows | why they fail on `5a4de7f76` |
|---|---|
| 4 collision rows (`Makefile+makefile`, `Makefile+GNUmakefile`, all three, `Makefile+GNUmakefile+scripts`) | the line holds "not with that merge alone" |
| 2 new script-tail rows (`GNUmakefile`, `makefile`) | the line holds "before both merges are made" |
| 2 new include rows in a real clone (`GNUmakefile`, `makefile`) | `make -n bootstrap` exits 2 before the merge and 0 after it alone (both asserted first, and passing), then the split message holds "not with that merge alone" |
| 7 clone rows, the pinned-file row and `test_a_scaffolded_root_is_told_its_makefile_exports_none` | the pinned file still says "and never the shape's Makefile" |

**Mutants of this round**, each run against the 118 rows on `b28f40de0`, each file restored and its sha256 checked:

| id | mutant | rows failing | result |
|---|---|---|---|
| MA | ", and not with that merge alone" put back | 6 | killed |
| MB | the script-aside tail put back ("before both merges are made") | 3 | killed |
| MC | the template's "and never the shape's Makefile" put back | 9 | killed |
| MD1 | include rows: `not in message` inverted to `in message` | 2 | killed |
| MD2 | include rows: after the merge, `returncode == 0` inverted | 2 | killed |
| ME | "holds them" put back | 6 | killed |
| MF | "the shape's `bootstrap` target runs" put back | 5 | killed |

**Gate** on `b28f40de0`:

| files | passed | failed | skipped |
|---|---|---|---|
| `test_adopt_collision_make_reads`, `test_pinned_copy_text`, `test_adopt_make_reads_first`, `test_adopt_contracts_dir`, `test_adopt_printed_sentences`, `test_adopt_e2e`, `test_repo_hygiene`, `test_scaffold_leg_path_collisions` | 470 | 0 | 0 |
| `test_shape_doctor`, 8 shards (62 + 56 + 54 + 56 + 60 + 57 + 58 + 45 collected) | 445 | 0 | 3 (PowerShell rows, Windows host only) |
| **total** | **915** | **0** | **3** |

**Merge-tree:** `b28f40de0` against main `4ed7c7bf5` is clean, and so is it against each open PR that touches these files: #1104, #1106, #1118, #1119, #1120, #1121 (`1c5173758`), #1122, #1123 and #1125.

**Round 0** (`5a4de7f76`, main `6da757cbe` merged in): on main's code (plus the constant alone), 12 of the then 27 rows failed and 15 passed; the 30 test files that run the changed code or read the template gave 2109 passed, 0 failed and 4 skipped (3 PowerShell rows in `test_shape_doctor.py`, 1 MedxEHR row in `test_adopt_plan.py` that needs a clone `SHAPE_MEDXEHR_CLONE` names); nine mutants each failed 1 to 11 of the then 114 rows.

**Not here**
- The third box of #1048, the `_next_steps` NOTE in `adopt-project.py`, is covered by the change #1047 makes to it: see #1047 and #1121.
- `templates/assembly-root/AGENTS-shape.md` line 19 and the five-commands table say `make bootstrap` without this caveat. They say nothing about a kept file, so they are outside #1048's two sentences.

Closes #1048

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Nov 8, 2026
…LE_NOTE` no longer calls #480's NOTE unchanged, since #1132 adds `python` on Windows to it (rv1207b's NIT-A)

The merge of #1121 moved that pin to "(`python` on Windows)", and the
comment above it still said "unchanged" (#1207's review, NIT-A, at
`tests/test_adopt_next_make_reads_first.py:64`). Comment only.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

adopt-project.py: execute's NEXT prints make bootstrap where GNU make at the root reads a kept makefile or GNUmakefile, which has no bootstrap rule

1 participant