Repository navigation
adopt-project.py and README.md: the Makefile follow-up names a kept makefile or GNUmakefile GNU make reads first, and where a case-blind disk writes the shape's; README says which Makefile exports CONTRACTS_DIR (#977) - #1046
Conversation
…makefile` or `GNUmakefile` GNU make reads first, and where a case-blind disk writes the shape's; README says which Makefile exports `CONTRACTS_DIR` (#977) GNU make reads the first it finds of `GNUmakefile`, `makefile` and `Makefile`, and only that one. #906's line asked only whether the source keeps `Makefile`, byte for byte (`_root_keeps`), so a source that keeps `makefile` or `GNUmakefile` got "the root Makefile wires `CONTRACTS_DIR`" while `make contracts-dir` at the clone's root answered `No rule to make target` (r906's probe). `_makefile_follow_up` now writes the line: it says the shape's Makefile is written as the root's `Makefile`, names the source's file GNU make reads at the root ("the first of `GNUmakefile`, `makefile` and `Makefile` it finds") and what it takes for `make` there to export `CONTRACTS_DIR`. Beside a kept `Makefile`, #906's words stand and the clause follows them. On a disk that ignores case (`_root_keeps` gains `fold`, as `_KeptTree` asks `check`'s disk), a kept `makefile` is `Makefile`, `place` writes the shape's as `shape/Makefile`, and the line says so with `CASE_ASIDE`. A plain `Makefile`, or none, prints the line it printed before. README.md § Adopting an existing repository said "the root Makefile it materializes exports `CONTRACTS_DIR`"; it now says the shape's Makefile does, and that it is the root's Makefile or, where the source keeps a `Makefile` of its own, `shape/Makefile` (r906's wording, `Makefile` in backticks: a kept `makefile` leaves the shape's at the root where case is kept). The handbook's `adopt` section says nothing of `CONTRACTS_DIR` and its sentence that a shape file whose name the source holds, `Makefile` among them, goes under `shape/` stays true, so only its marker moves, to the segment's new blob. +1 README line, within the cap. tests/test_adopt_make_reads_first.py: the line word for word for the source keeping none, `Makefile`, `makefile`, `GNUmakefile` and two or three at once, and `MAKEFILE`; the line against `_KeptTree` on both disks; `plan` and `check` per row; a real `execute` per row where each kept file's `which` target makes `make which` in the clone print the file GNU make read, which the split message names, `make contracts-dir` has no rule, and `-f` on the file the line names prints the spec leg's `contracts/`; README's sentence and the handbook marker. 8 mutants of the fix, README and marker, each killed. Committed at a usage checkpoint: the new test file's last edit (shared helpers imported from test_adopt_printed_sentences, a comment) is not yet re-run; everything before it is. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR updates adoption follow-ups and documentation to account for GNU make selecting only the first available GNUmakefile, makefile, or Makefile, including case-insensitive path behavior, and adds broad unit, integration, and documentation-consistency tests. Flow diagram for GNU make follow-up selectionflowchart TD
A["Root files surviving in the assembled repository"] --> B["_makefile_follow_up(surviving)"]
B --> C{"_root_keeps(surviving, Makefile)"}
C -->|yes| D["Shape Makefile is written as shape/Makefile; mention the source Makefile"]
C -->|no| E{"_root_keeps(surviving, Makefile, fold=True)"}
E -->|yes| F["Case-insensitive collision: shape Makefile may be written as shape/Makefile"]
E -->|no| G["Shape Makefile is written as the root Makefile"]
D --> H["Check GNU_MAKE_READS order"]
F --> H
G --> H
H --> I{"First source file found in GNUmakefile, makefile, Makefile"}
I -->|source GNUmakefile or makefile| J["Explain that GNU make reads the source file and requires it to export CONTRACTS_DIR or include a Makefile that does"]
I -->|only Makefile or none| K["Keep the existing follow-up wording"]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-1, 2026-10-20T10:17Z) r1046 PARTIAL review of PR #1046 (closes #977) at 83b3671Stopped at the 77% usage checkpoint. Nothing is applied and nothing of mine is running. At 10:17Z all three trees were clean: the worktree VERDICT (as of now): NOT READY at 83b3671There is one MUST, and it is in the PR body only: the fix is a body edit, with no code change. The remaining checks (resume steps below) may add findings. They cannot remove this one. FINDINGS (most severe first)MUST-1: the body's
|
| Case | MAKEFILE_LIST | Root rc | Block in | Line |
|---|---|---|---|---|
| none | Makefile |
0, prints the path | Makefile |
"the root Makefile wires", true |
Makefile |
Makefile |
2, no rule | shape/Makefile |
#906's line, true |
makefile |
makefile |
2 | Makefile |
names makefile, true |
GNUmakefile |
GNUmakefile |
2 | Makefile |
names GNUmakefile (", but"), true |
Makefile + makefile |
makefile |
2 | shape/Makefile |
true |
Makefile + GNUmakefile |
GNUmakefile |
2 | shape/Makefile |
true |
makefile + GNUmakefile |
GNUmakefile |
2 | Makefile |
the folded clause names makefile, and the read clause names GNUmakefile; true |
| all three | GNUmakefile |
2 | shape/Makefile |
true |
MAKEFILE, shipped rules |
Makefile |
0 | Makefile |
"the root Makefile wires", true here (see MUST-1) |
MAKEFILE, policy rule |
Makefile |
0 | Makefile |
the folded line, no GNU make clause; true here |
- The line names the file make reads: it never names a file make does not read.
CONTRACTS_DIRandmake contracts-dir: what the line says of both is true in every row.- Plan, check and message: in every row the plan line, the check line and the line in the split message are identical. Each is one line, printable and ASCII (the message holds " - " + the line exactly).
The PR's follow-ups, measured
make -n bootstrapat the clone's root exits 2 ("No rule") in every row except none andMAKEFILE. This confirms the NEXT follow-up.- The collision follow-up: in the all-three clone, after
cat shape/Makefile >> Makefile, bothmake -n bootstrapandmake contracts-dirat the root still say "No rule", whilemake -f Makefile contracts-dirworks. So the "arrive only with that merge" sentence the PR lists as a follow-up is untrue there. The follow-up is real, and it is outside adopt-project.py: a kept lower-casemakefileorGNUmakefilestill gets "the root Makefile wiresCONTRACTS_DIR", and README.md'sCONTRACTS_DIRsentence has no kept-Makefile case (follow-ups to #906) #977.
Item 2: a disk that ignores case
I could not make one. The kernel has no CONFIG_UNICODE (zcat /proc/config.gz). unshare -rm plus mount -t tmpfs -o casefold is refused ("wrong fs type, bad option"), while a plain tmpfs mounts. There is no fusermount, fuse2fs, ciopfs or Python FUSE module.
From the code, the line's claim is consistent:
executeclones the source, mounts the legs, and thenmaterialize_assembly_rootrunsplace. At that point a keptmakefileis on disk, soPath("Makefile").exists()is True there, and the copy goes toshape/Makefile._root_keeps(fold=True)and_KeptTree(fold=True).holdsboth usecasefold._makefile_follow_up's docstring declines to say what GNU make reads by case there, and the line says nothing about it forMAKEFILE.
Items 3 and 4: printing, README and handbook
- Printing:
checkprints follow-ups with_shown, and these lines need no spelling, so they are unchanged. - Handbook marker: recomputed with
readme_segmentsandreadme_fingerprint; it is 65fb9bf156f22898f6c75833f702861d4cc849e6, matchingdocs/handbook.html. The handbook says nothing ofCONTRACTS_DIR. Its "written under shape/" sentence (handbook.html:1309) stays true. - README: 1479 lines, which is the cap. The added lines are at most 80 columns.
- Old claim: no other README or handbook sentence makes it. README:1113 ("appends a
CONTRACTS_DIRblock to an adopted Makefile") is true.
Item 5: the 23 that fail on base
Base adopt-project.py, README.md and docs/handbook.html were put in place and then restored with git checkout HEAD --. Result: 23 failed and 10 passed, and the 10 are exactly the ones the body lists.
Item 7: issues and the writer's counts
- Closing references:
closingIssuesReferencesis [977] only, and the body has one "Closes adopt-project.py: a kept lower-casemakefileorGNUmakefilestill gets "the root Makefile wiresCONTRACTS_DIR", and README.md'sCONTRACTS_DIRsentence has no kept-Makefile case (follow-ups to #906) #977". - Mutant counts: the body's counts match the writer's logs (M2 9, M3 3, M4 4, M5 19, M6 13, M7 3, M8 2). M1's "18 of 33" is not verified: its log was overwritten by the end-to-end re-run. W1 is queued to check it.
Item 8: style
git diff --check e2f9fc42 83b3671bis clean.- No added line is over 79 columns in adopt-project.py or the new test.
- There are no tabs.
- There is no
assert a and b. The two asserts at :193 and :194 have the formassert X is (A and B), which is not compound. - Non-ASCII: NIT-2.
TEST COUNTS
Every run used:
EMAILexported andLANES_LANEunset;- the stub gh first on PATH,
GH_TOKEN=bogus-r1046and an emptyGH_CONFIG_DIR; GIT_CONFIG_GLOBAL=/dev/nullandGIT_SSH_COMMAND=false;- the basetemp
_lane1-scratch/r1046/bt.
Results:
- New file at head: 33 passed.
- Base (as above): 23 failed, 10 passed.
- Gate A at head: 515 passed, 0 skipped (
logs/gate-A.log). The files:test_adopt_make_reads_first33test_adopt_contracts_dir34test_adopt_printed_sentences54test_repo_hygiene100test_readme_sections76test_agents_md_codes47test_cli_reference23test_readme_windows_quoting_sentence44test_scaffold_leg_path_collisions104
test_shape_doctor.py, 4 shards: 118 + 110 + 117 + (100 passed, 3 skipped) = 445 passed and 3 skipped (448 ids).- Main merge: the four files gave 263 passed (
logs/merge-four.log).
MUTANTS (in the mut clone, each restored; left=[] after each)
Code mutants run the new file, test_adopt_contracts_dir and test_adopt_printed_sentences (121 tests).
| Mutant | Result |
|---|---|
R1, first may be Makefile |
KILLED, 4: new file [Makefile] line and e2e; printed_sentences the-root-keeps-it and plan-says-where |
| R2, fold casefolds the path but not the kept set | KILLED, 1: only test_a_name_only...[MAKEFILE] (weakly covered) |
R3, joint ignores folded |
KILLED, 6: [makefile] and [makefile+GNUmakefile], in line, plan and e2e |
| R4-R10, R11-R12 (README), W1 (the writer's M1, to verify "18 of 33") and R13 | NOT RUN (checkpoint) |
The mutant definitions are in _lane1-scratch/r1046/mutants.py, and the runner is run-mutant.sh.
MERGES
git merge-tree --write-treeof 83b3671 with main b0babe1: clean, tree 2565a12a4030c39ff62d9a949c16512ab93575a2.- The same with
fix/910-check-throwaway-reposabd1b98: clean, tree fa99cd047ca45fd934553901c0c8ffc34e4065c9. - Main merge, built in the
git clone --sharedcheckout_lane1-scratch/r1046/mergeat the commit-tree 8eaa1f7e (parents b0babe1 and 83b3671): the new file,test_adopt_printed_sentences,test_repo_hygieneandtest_readme_sectionsgave 263 passed.
NOT VERIFIED YET
- The mutants R4-R13 and W1.
- A disk that ignores case: impossible here; the result is reasoned from the code.
- Skip reasons for the 3 skips in the doctor shards (not printed; the writer says PowerShell, Windows only).
RESUME STEPS
- Go to the worktree and check its state:
cd /workspace/projects/openRepoShape-worktrees/r1046-make-reads-first || exit 1git status --shortmust be empty, and HEAD must be 83b3671.- Check that
df -i /workspacehas room. - Check that
git -C /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1046/mut status --shortis empty.
- Run the code mutants:
export EMAIL=<the r1046 EMAIL value the brief gives>S=/workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1046CODE=(tests/test_adopt_make_reads_first.py tests/test_adopt_contracts_dir.py tests/test_adopt_printed_sentences.py)- Run
for m in R4-whose-ignores-folded R5-no-written-as-for-GNUmakefile R6-search-order-reversed R7-aside-folds-case R8-legs-fed-as-root R9-first-folds-case R10-twins-keep-slash R13-listed-order-text; do "${S}/run-mutant.sh" "$m" "${CODE[@]}"; donein batches of 3-4, each call under 9 minutes (about 75 s per mutant).
- Run the README mutants:
for m in R11-readme-unticked-Makefile R12-readme-whitespace-only; do "${S}/run-mutant.sh" "$m" tests/test_adopt_make_reads_first.py tests/test_repo_hygiene.py tests/test_readme_sections.py; done - Verify the writer's "18 of 33":
"${S}/run-mutant.sh" W1-writers-M1-order tests/test_adopt_make_reads_first.py - Check main again. Run
git -c gc.auto=0 -c maintenance.auto=false fetch -q --no-write-fetch-head origin main. If it moved past b0babe1, redo the merge-tree and the four-file run in a new--sharedclone (the scratchmergeclone is at 8eaa1f7e). - Write the final report to
_lane1-notes-20261011/v11/c1046-r1046.md, merging in this partial. The verdict stays NOT READY unless the body is fixed (MUST-1); a body fix needs no re-gate of the code.
…ERED comment says a --path-policy rule, not a person's answer, reaches the follow-ups, and the file is ASCII SHOULD-1 and NIT-2 of r1046's review of #1046. The comment above ANSWERED said its two rows were "`(path, leg)` rows no rule writes, a person's answers". `plan` builds its follow-ups only from what the rules answer, so a person's answer in the plan changes no follow-up: both rows reach `follow_ups_for` only through a `--path-policy` rule (`MAKEFILE` kept in the root; `makefile` and `GNUmakefile` sent to the legs). The comment now says so. The file held four non-ASCII characters, a section sign on two lines and an em dash on two lines. Each is now its Python escape: Python turns it into the character in the plain docstring and the plain assert strings, and `re` does in the `rf` pattern. The values are unchanged. The two lines the escapes pushed past 79 columns are rewrapped. No assertion means anything else than before. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-1, 2026-10-20T11:12Z) r1046: independent review of PR #1046 (closes #977) at c121ed9VERDICT: NOT READY at c121ed9One MUST is open, and it is in the PR body only. It is the round-1 note's claim that "every string value [is] unchanged".
FINDINGS (most severe first)MUST-2: the body's "Review round 1" note says "every string value unchanged", and two string constants changedWhere: live PR body, line 199: "The new test file is ASCII: its section signs and em dashes are written as escapes, with two lines rewrapped and every string value unchanged (NIT-2)." Evidence: I parsed both versions of tests/test_adopt_make_reads_first.py with
The assert string at :346-348 is unchanged, because adjacent literals join into one constant and its value is identical. The commit message of c121ed9 says only "The values are unchanged" of the escapes, and then that two lines are rewrapped. Read that way it is defensible. The body's "every string value unchanged" is not. Fix (body only): replace "with two lines rewrapped and every string value unchanged" with: "with two lines rewrapped: the assert's string is unchanged, the NIT-A: the body's merge section is out of date (partial NIT-4)Where: live body lines 157-168, "main at Now: main is 8a05721, and the merge with c121ed9 is clean (tree b8dcb1a9). Since dc7cd49, main has moved only Fix: refresh the section at landing. NIT-B: two follow-ups the body marks "inferred" are now measuredWhere: live body lines 172-183. Evidence: in my all-three clone (
So, where GNU make reads a kept Fix: optionally, mark both "measured (r1046)" when the follow-ups are filed. Both stay outside #977. NIT-C: a mutant survives: directory entries named
|
| Case | MAKEFILE_LIST | Root rc | Block in | Line |
|---|---|---|---|---|
| none | Makefile |
0 | Makefile |
"the root Makefile wires": true |
Makefile |
Makefile |
2, no rule | shape/Makefile |
#906's line: true |
makefile |
makefile |
2 | Makefile |
names makefile: true |
GNUmakefile |
GNUmakefile |
2 | Makefile |
names GNUmakefile: true |
Makefile + makefile |
makefile |
2 | shape/Makefile |
true |
Makefile + GNUmakefile |
GNUmakefile |
2 | shape/Makefile |
true |
makefile + GNUmakefile |
GNUmakefile |
2 | Makefile |
true |
| all three | GNUmakefile |
2 | shape/Makefile |
true |
MAKEFILE, shipped rules, answered root |
Makefile |
0 | Makefile |
"the root Makefile wires": true on this disk |
MAKEFILE, --path-policy rule |
Makefile |
0 | Makefile |
the case clause, no GNU make clause: true on this disk |
- The file the line names: it never names a file that make does not read.
CONTRACTS_DIRandmake contracts-dir: what the line says of both is true in every row.- The same line everywhere: the plan's line,
check's line and the split message's line are identical in every row. Each is one printable ASCII line.
Item 2: a disk that ignores case
I could not make one here, and found no other way:
- The kernel has no
CONFIG_UNICODE. unshare -rmwithmount -t tmpfs -o casefoldis refused.- There is no fusermount, fuse2fs or ciopfs, and no Python FUSE module.
From the code, the line's claim is consistent:
place(Path.exists()) runs after the clone and the leg mounts, so a keptmakefileis on disk at that point. On such a diskPath("Makefile").exists()is then True, and the copy goes toshape/Makefile._root_keeps(fold=True)and_KeptTree(fold=True).holdsboth usecasefold.
Items 3 and 4: printing, README and the handbook
- Spelling: the kept names are
_spelled. The constants spell the same bytes. - Handbook marker: recomputed with the repo's
readme_segmentsandreadme_fingerprint, it is 65fb9bf156f22898f6c75833f702861d4cc849e6, the same as indocs/handbook.html. - Handbook sentence: handbook.html:1309 stays true.
- README: 1479 lines, which is the cap. Its added lines are at most 80 columns.
- The old claim: no other README or handbook sentence still makes it.
Item 5: the tests that fail on base
With e2f9fc4's adopt-project.py, README.md and handbook in place, the file gives 23 failed and 10 passed, exactly as the body lists. I restored the files with git checkout HEAD --.
Item 7: the issue and the body's counts
- Closing issues:
closingIssuesReferencesis [977], and the body has one "Closes adopt-project.py: a kept lower-casemakefileorGNUmakefilestill gets "the root Makefile wiresCONTRACTS_DIR", and README.md'sCONTRACTS_DIRsentence has no kept-Makefile case (follow-ups to #906) #977". - The writer's mutant counts: they match the writer's logs: M2 9, M3 3, M4 4, M5 19, M6 13, M7 3, M8 2.
- M1, rerun as W1: 18 of 33. In the end-to-end rows
makefile+GNUmakefileand all three, it fails on the check of make'swhichoutput against the message. That matches the body's M1 row.
Item 8: style
git diff --checkis clean for e2f9fc4..83b3671 and for 83b3671..c121ed9.- No added line is over 79 columns.
- There are no tabs.
- There is no
assert a and b. - The test file is ASCII.
MUTANTS (run in my mut clone, each restored; the clone stayed clean after each)
- Code mutants run against the new file,
test_adopt_contracts_dirandtest_adopt_printed_sentences(121 tests). - README mutants run against the new file,
test_repo_hygieneandtest_readme_sections(209 tests). - W1 runs against the new file only (33 tests).
| Mutant | Result |
|---|---|
R1: first may be Makefile |
KILLED, 4 |
| R2: folding casefolds the path, not the kept set | KILLED, 1 (...[MAKEFILE] only) |
R3: the joining word ignores folded |
KILLED, 6 |
R4: whose ignores folded |
KILLED, 1 (...[MAKEFILE] only) |
R5: no "written as the root's Makefile" for GNUmakefile |
KILLED, 3 |
| R6: the search order reversed | KILLED, 6 |
R7: aside folds case |
KILLED, 9 |
| R8: leg entries fed as root | KILLED, 2 (sent-to-legs; printed_sentences it-goes-to-the-code-leg) |
R9: first folds case |
KILLED, 5 |
R10: twins without rstrip("/") |
SURVIVES (NIT-C) |
| R13: the listed order's text | KILLED, 18 |
R11: README Makefile without backticks |
KILLED, 3 (including hygiene test_the_handbook_follows_the_readmes_outline) |
| R12: README whitespace only, marker not moved | KILLED, 2 (the marker test and the hygiene outline test) |
W1: the writer's M1, makefile before GNUmakefile |
KILLED, 18 of 33 |
Totals: 13 killed and 1 survives (contrived). R1-R3 ran at 83b3671 and the rest at c121ed9; the code is identical in both.
TEST COUNTS
Environment for every run:
EMAILexported,LANES_LANEunset.- The stub
ghfirst on PATH,GH_TOKEN=bogus-r1046, and an emptyGH_CONFIG_DIR. GIT_CONFIG_GLOBAL=/dev/nullandGIT_SSH_COMMAND=false.- One reused basetemp,
_lane1-scratch/r1046/bt.
At c121ed9 (the mut clone): the new file, test_adopt_printed_sentences, test_repo_hygiene and test_readme_sections gave 263 passed. That is 33 + 54 + 100 + 76, and it includes the marker test.
At 83b3671, with code, README and handbook identical to c121ed9:
- Gate A (
logs/gate-A.log): 515 passed, 0 skipped. By file:test_adopt_make_reads_first: 33test_adopt_contracts_dir: 34test_adopt_printed_sentences: 54test_repo_hygiene: 100test_readme_sections: 76test_agents_md_codes: 47test_cli_reference: 23test_readme_windows_quoting_sentence: 44test_scaffold_leg_path_collisions: 104
test_shape_doctor.pyin 4 shards: 118 + 110 + 117 + 100 = 445 passed, 3 skipped (448 tests).
MERGES
main 8a05721 (fetched with the no-maintenance flags; it was b0babe1 at the partial):
git merge-tree --write-treewith c121ed9 is clean, tree b8dcb1a91709a109fabf5718bb4ebfa147c31578.- I built it in the
git clone --sharedcheckout_lane1-scratch/r1046/merge, as commit-tree 3ebced2a (parents 8a05721 and c121ed9). - The four files (new, printed_sentences, repo_hygiene, readme_sections) gave 263 passed (
logs/merge2-four.log). - From b0babe1 to 8a05721, main touched none of
adopt-project.py, README.md, docs/handbook.html, tests/readme-cap.d,shape_materialize.pyor the imported test files. - The earlier merge with b0babe1 (tree 2565a12a) also gave 263 passed.
fix/910-check-throwaway-repos b83d9c0 (newest on origin; it was abd1b98 at the partial):
merge-treewith c121ed9 is clean, tree 8332cfe4164f07d3077ca43ef190782e5c49dc01.- With abd1b98 it was also clean, tree 883c365f.
NOT VERIFIED
- A disk that ignores case: I could not make one here, so that part rests on the code (item 2).
- Batches B and C: I did not run the writer's batch B (
attributes_eol_block,verify,ignored_shape_files,leg_path_collisions,submodules) or batch C (root_manifest,root_symlinks,symlink_target_bytes,root_shape_paths). They were not in my brief; the writer's logs show 368 and 403 passed. - The 3 doctor skips: their reasons were not printed. The writer says they are the PowerShell rows, Windows only.
STATE
- The worktree
r1046-make-reads-firstis untouched at 83b3671, andgit status --shortis clean. - My scratch clones are both clean:
mutis at c121ed9 andmergeis at 3ebced2a. - No mutant is applied, and no process of mine is running.
- Writes outside scratch: the only one was the merge-tree tree objects written into the shared object store.
- No
ghwrites, and nothing was posted.
|
(lane openRepoShape-1, 2026-10-20T11:15:20Z) #1046 is READY at
Lane: openreposhape-1 (openRepoShape-1) |
|
|
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 1 day and 6 hours by commenting |
…round a loop is refused in words that name the link and the loop, not "(File exists)", and a symlink to a file is named as one (#978) (#1065) ## What `plan`, `check` and `execute` refused a `--work-dir` that is a symlink round a loop as "making it a directory failed (File exists)". `os.mkdir` of such a link fails with EEXIST, the error a file in the way gets, so the person was told a file is there and found a link. The way out, "Pass a --work-dir this user can write in", was the one for a folder this user cannot write in, and said nothing of a link. #963's review (r903) left it tracked as #978. `_work_root` now asks a new `_unmade_work_dir` why the directory could not be made. The code stays `adopt-work-dir-unwritable`, exit 2, and nothing is made. - The `--work-dir` is named as given, made absolute and not resolved. On base the reason named `os.path.realpath`'s reading of it, which for a link into a loop is another link, a path the person never typed. - A loop is named only where `os.path.realpath` left a symlink in the path (it follows every other one) and `os.stat` of that link fails with ELOOP (`_the_loop_in`). So a link to a file, or a long chain of links, is never called a loop. - The way out names the link: the `--work-dir` itself where it is a symlink, else the link in the loop. - A file at the `--work-dir`, or above it, keeps the system's words and #524's way out, as the issue asks. - A dangling link is no loop: `realpath` reads it as the path it leads to, which is made, as on base (the controls below). - `_work_dir_refusal` takes the way out as `fix=`, #524's by default. Names are printed with `_spelled`. No `shell_quoted` call is added. Measured with `plan` of a local source, `<A>` the directory that holds the links, before on `1a47ed07` and after on this head (exit 2 in every row): | `--work-dir` | Before | After | | --- | --- | --- | | `self`, a link to itself | `` --work-dir `<A>/self` cannot be made: making it a directory failed (File exists) `` | `` --work-dir `<A>/self` cannot be made: it is a symbolic link that leads round a loop `` | | `a`, with `a -> b` and `b -> a` | `` --work-dir `<A>/a` cannot be made: making it a directory failed (File exists) `` | `` --work-dir `<A>/a` cannot be made: it is a symbolic link that leads round a loop `` | | `into -> a`, into that pair | `` --work-dir `<A>/a` cannot be made: making it a directory failed (File exists) `` | `` --work-dir `<A>/into` cannot be made: it is a symbolic link that leads round a loop, at the symbolic link `<A>/a` `` | | `through -> loop/sub`, with `loop -> loop` | `` --work-dir `<A>/loop/sub` cannot be made: making it a directory failed (Too many levels of symbolic links) `` | `` --work-dir `<A>/through` cannot be made: it is a symbolic link that leads round a loop, at the symbolic link `<A>/loop` `` | | `loop/sub`, no link itself | `` --work-dir `<A>/loop/sub` cannot be made: making it a directory failed (Too many levels of symbolic links) `` | `` --work-dir `<A>/loop/sub` cannot be made: the way to it leads round a loop, at the symbolic link `<A>/loop` `` | | `tofile -> a-file` | `` --work-dir `<A>/a-file` cannot be made: making it a directory failed (File exists) `` | `` --work-dir `<A>/tofile` cannot be made: it is a symbolic link that leads to `<A>/a-file`, which is not a directory `` | | `a-file`, a file | `(File exists)` | unchanged | | `a-file/under` | `(Not a directory)` | unchanged | The way out: - Before, in every row: "Remediation: Pass a --work-dir this user can write in, or none: without one, each run makes a new one." - After, in the six symlink rows: "Remediation: Remove the symbolic link `<link>`, or make it lead to a directory, or pass another --work-dir, or none: without one, each run makes a new one." `<link>` is `<A>/self`, `<A>/a`, `<A>/into`, `<A>/through`, `<A>/loop` and `<A>/tofile`. - `execute` still says "no leg repository was created and nothing was pushed." first. The two file rows keep the way out they had. **Wider than the issue.** The issue asks for the EEXIST case, where the `--work-dir` is itself the link. A path that runs through a loop fails `os.mkdir` with ELOOP instead, and said only the system's words, with the same way out for a folder this user cannot write in. It is named the same way now: `--work-dir through` and `--work-dir loop/sub` reach the same place. So the words `tests/test_adopt_clone_claimed.py` held for a loop above the work directory change. That row's point, #650's code `adopt-work-dir-unwritable` and not `adopt-work-dir-shared`, is unchanged. ### The comment wording in #963's code r903's round-2 NITs a-c (#963 issuecomment-6253937080). Comments and docstrings only. - **(a)** `_ask_git_branch` passes an empty temporary directory as `--git-dir`, which is no repository. - `adopt-project.py`, `_checked_flag_inputs`: "That question is asked of a throwaway repository in the system temp folder, which it removes" is now "That question is asked with an empty temporary directory as its `--git-dir`, which `plan` removes". - `adopt-project.py`, `cmd_plan`: "runs `git check-ref-format` in a throwaway repository first" is now "runs `git check-ref-format` first, with an empty temporary directory as its `--git-dir`, which `plan` removes". - `tests/test_adopt_plan_flag_refusals_first.py`, the module docstring: "in a throwaway repository" is now "with an empty temporary directory as its `--git-dir`, which `plan` removes". - The same file's `test_the_branch_a_tracking_branch_names_is_asked_before_the_source`: "is asked of a throwaway repository in the temp folder ... and it removed what it made" is now "is asked of git with an empty temporary directory as its `--git-dir` ... and the directory was removed". - "which it removes" in r903's words is "which `plan` removes" here, so that it does not read as git removing it. - **(b)** The refusals still made after the source are listed "among them", in `_checked_flag_inputs`' docstring and the test's module docstring: a `--project` of 251 to 255 characters is refused `unsafe-value` after git's reads. - **(c)** The naming refusals come "with a remedy about the source where it names a repository". ## Tests - `tests/test_adopt_work_dir_refusal.py`, 53 tests: - `test_a_work_dir_that_is_a_symlink_loop_is_refused` held "(File exists)" through `mkdir_says`. It now holds the new words, for a link to itself and for a pair, through `plan`, `check` and `execute`: 6. - New: a link into a pair and a link through a loop (6), a path under a loop (3), and a link to a file (3), each through the three commands; each asserts exit 2, the whole of stderr, no plan written, no leg, and nothing made or changed under the test's directory. - New: `_the_loop_in` asked directly, which calls a link to a file or to a directory no loop (1). - New controls: a dangling link to `target-a` and to `missing-dir/target-b`, through `plan` and `check`, exit 0 and the target made (4). - `tests/test_adopt_clone_claimed.py`, 42 tests: `test_a_loop_or_a_file_above_the_work_dir_keeps_its_650_refusal`'s three `a-symlink-loop` rows hold the new words; its file rows keep the system's. - `tests/test_adopt_plan_flag_refusals_first.py`: docstrings only. - **Fail on base.** With `1a47ed07`'s `adopt-project.py` in this head's tree, the two changed files: 22 failed, 73 passed. The 22 are exactly the #978 rows: the 19 in `test_adopt_work_dir_refusal.py` and the 3 `a-symlink-loop` rows. The 4 dangling-link controls pass on both. On this head: 95 passed. - **The gate**, on this head, one basetemp, `EMAIL` exported, `LANES_LANE` unset, a stub `gh` first on PATH, `GH_TOKEN=bogus-w978`, an empty `GH_CONFIG_DIR`, `GIT_CONFIG_GLOBAL=/dev/null` and `GIT_SSH_COMMAND=false`, in 17 chunks and 4 shards, each under 9 minutes: - 46 files: the 34 that name `adopt-work-dir-unwritable`, `_work_root` or `work_dir`; `test_adopt_plan_flag_refusals_first`, `test_repo_hygiene`, `test_agents_md_codes`, `test_cli_reference`, `test_readme_windows_quoting_sentence` and `test_scaffold_leg_path_collisions`; and the 7 that read `adopt-project.py`'s source (`test_git_output_decoding`, `test_no_auto_maintenance`, `test_non_utf8_git_failures`, `test_shape_advisory`, `test_pinned_copy_text`, `test_openreposhape_package`, `test_template_values_single_pass`): 3444 passed, 2 skipped, in 17 chunks. - `test_shape_doctor.py`, all 448 tests in 4 shards: 445 passed and 3 skipped. **Mutants**, each applied alone to `adopt-project.py`, then `tests/test_adopt_work_dir_refusal.py` run, then restored by sha256. All 12 killed. They ran on a file that differs from this head's only in two comment lines ("which `plan` removes"). | Mutant | Failed | Killed by | | --- | --- | --- | | M1 ELOOP dropped from the errno check | 6 | through-a-loop, under-a-loop | | M2 the same-link branch always taken | 6 | into-a-pair, through-a-loop | | M3 the "is a link" test dropped from the link-to-a-file branch | 3 | the file AT the `--work-dir` (keeps `File exists`) | | M4 the way out under a loop names no link | 3 | under-a-loop | | M5 the `--work-dir` named as `realpath` reads it | 9 | into-a-pair, through-a-loop, link-to-a-file | | M6 only the last part of the path asked for a link | 6 | through-a-loop, under-a-loop | | M7 no loop ever found | 15 | every loop row | | M8 the link named where its target goes | 3 | link-to-a-file | | M9 the old way out kept | 18 | every symlink row | | M10 `_same_entry` always false | 6 | to-itself, a-pair | | M11 `realpath`'s reading passed for the path as typed | 9 | into-a-pair, through-a-loop, link-to-a-file | | M12 any link left in the path called a loop, with no ELOOP | 1 | `test_only_a_symlink_that_loops_is_called_a_loop` | M12 survives every command-level row: on Linux `realpath` leaves no link in the path but one that loops. The direct test of `_the_loop_in` holds the guard. ## Merge with main `git merge-tree --write-tree` of this head with `main` at `eee089ca` is clean (tree `02c1717d`). Of this branch's files, only `adopt-project.py` moved on main. #1046 changed other functions. #1041 added the `adopt-bad-project-name` refusal inside `_checked_flag_inputs`, below the docstring and the comment this branch rewords. A `--project` the naming policy refuses as well (`NUL.git`) is still refused only after the source, and the docstring's list of what comes after the source, read "among them", stays true. The branch was not merged, and the gate ran on this head. ## Not in this PR - A link to a path under a file (`w -> a-file/sub`) still names the path `realpath` reads, `<A>/a-file/sub`, with "(Not a directory)". True, but it does not name the link. - Not measured on Windows, where a loop is named only if `os.stat` of the link reports ELOOP. It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its reviewed head `a4e179dd` is green on Linux and SonarCloud. The branch merged main once and was never rebased; the last merge is update-branch's, clean, giving 228531a, which is green on SonarCloud and Git Bash. Lane 1's gate passed on its tree: the 47 test files that run `--work-dir` refusals and #650's claimed clones, read `adopt-project.py`'s source, and the doctor's, 3,898 passed and 3 skipped. Closes #978 Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



What
Two follow-ups to #906 that lane 1's review of #964 (r906) found and
ruled out of that pull request. Neither is a regression: both were as
untrue on
mainbefore #906 as after it.(a) The Makefile follow-up names the root file GNU make reads first.
The line is in
plan'sfollow_ups:, incheck's output and in thesplit commit's message, which is also the pull request's body. GNU make
reads the first file it finds of
GNUmakefile,makefileandMakefile, and only that one. #906 asked only whether the source keepsMakefile, comparing names byte for byte (_root_keeps). So a sourcethat keeps
makefileorGNUmakefilegot "the root Makefile wiresCONTRACTS_DIR...". But the clone's root then holds the shape'sMakefile, with the block, and the source's file, whichmakereadsinstead, so
make contracts-diransweredNo rule to make target 'contracts-dir'(r906's probe, both names)._makefile_follow_upnow writes the line:that file is not the
Makefileadopt-project.py: five printed sentences say what the run does not do: the Makefile follow-up beside a root Makefile, thecontracts/-only read-across path,execute would stop there,a path names no attribute, and WHAT WILL HAPPEN under--local-remote-dir#906's words already name. It alsosays what it takes for
makethere to exportCONTRACTS_DIR.makefileISMakefile.place(Path.exists()) finds the name taken and writes the shape'sMakefile as
shape/Makefile. The line says so inCASE_ASIDE'swords.
_root_keepsgainsfold, which asks the question the way_KeptTreeasks it forcheck's disk that ignores case.Makefile, or none of the three names and noother case of
Makefilethat a rule keeps, gets the line it got before,byte for byte.
_spelled, and the three names GNU makelooks for with
_and_listed. Noshell_quotedcall is added.CONTRACTS_DIR..."Makefileshape/Makefileuntil the two are merged, and the run adds none of it to the root's Makefile"makefileMakefile, or asshape/Makefileonce case is ignored, as a macOS or Windows disk does, where the source'smakefileis that path, until the two are merged; and GNU make at the root reads the source'smakefile, ..."GNUmakefileMakefile, but GNU make at the root reads the source'sGNUmakefile, ..."makefileandGNUmakefilemakefile, and GNU make reads the source'sGNUmakefileMakefileandmakefilemakefile, ..."MakefileandGNUmakefile, or all threeGNUmakefile, ..."MAKEFILE, kept in the root by a--path-policyrule (with the shipped rulesplanasks about it, and a person'srootanswer keeps the lineplanwrote: "the root Makefile wires ...")Makefile, or asshape/Makefileonce case is ignored, ..., where the source'sMAKEFILEis that path, until the two are merged"The clause that names the file, in full:
(b) README.md, section "Adopting an existing repository".
CONTRACTS_DIR,pointing at the
contracts/of the spec leg where the plan mounts it(
$(CURDIR)/spec/contractsby default)."CONTRACTS_DIR, pointing at thecontracts/of the spec leg where the plan mounts it(
$(CURDIR)/spec/contractsby default); it is the root's Makefile, or,where the source keeps a
Makefileof its own,shape/Makefile, whichholds the export until a person merges the two."
This is r906's wording, as its re-check corrected it, with one change:
Makefileis in backticks in "aMakefileof its own". A source thatkeeps
makefilehas the shape's Makefile written as the root'sMakefileon a disk that keeps case, so the plain word would haveclaimed
shape/Makefilefor it.The section is bound to the handbook's
adoptsection. The handbooksays nothing of
CONTRACTS_DIR, and its sentence that a shape filewhose name the source already holds,
Makefileamong them, is writtenunder
shape/stays true. So only the section's marker moves, to thesection's new blob, computed with
readme_segmentsandreadme_fingerprint.The tail of the paragraph is reflowed, and README.md grows by one line,
to 1479, which is the cap. No
tests/readme-cap.d/file is needed whileno other README pull request lands first.
Measured
On a disk that keeps case (Linux, GNU Make 4.3, git 2.43), each row ran
a real
execute --local-remote-dirand made a recursive clone of thesplit. Each file the source keeps has its own
whichtarget, somake whichat the clone's root prints the name of the file GNU make read.make whichprintsmake contracts-dirat the rootmake -f <it> contracts-dirprints the spec leg'scontracts/)makefilemakefilemakefileMakefileGNUmakefileGNUmakefileGNUmakefileMakefilemakefileandGNUmakefileGNUmakefileGNUmakefileMakefileMakefileMakefileMakefileshape/MakefileMakefileandmakefilemakefilemakefileshape/MakefileMakefileandGNUmakefileGNUmakefileGNUmakefileshape/MakefileGNUmakefileGNUmakefileshape/MakefileThe disk that ignores case is not measured: there is none here. What
the line says of it rests on
place'sPath.exists(), and is testedagainst
_KeptTree(..., fold=True),check's model of that disk.Tests
tests/test_adopt_make_reads_first.py, new: 33 tests.makefileand
GNUmakefilesent to legs._KeptTreeon both disks.planandcheckfor each row but none andMakefile, whichtest_adopt_contracts_dirandtest_adopt_printed_sentencesalreadyplan and check.
executefor each row except none, with themakemeasurements above.
it now stands.
e2f9fc42(main when the branch was made), 23 failand 10 pass. The 10 are the handbook marker test, which was current
on base, and rows whose line does not change: none and
Makefilealone;
makefileandGNUmakefilesent to legs; and theMakefile-plus-another rows of the disk test, which asks only wherethe copy goes.
ghfirston PATH,
GH_TOKEN=bogus,GIT_SSH_COMMAND=false,GIT_CONFIG_GLOBAL=/dev/null:test_adopt_contracts_dir,test_adopt_printed_sentences,test_adopt_leg_keys,test_adopt_plan,test_adopt_e2e,test_repo_hygiene,test_readme_sections,test_agents_md_codes,test_cli_reference,test_readme_windows_quoting_sentenceandtest_scaffold_leg_path_collisions: 652 passed, 1 skipped (theMedxEHR test, no clone here).
Makefile,makefileorCONTRACTS_DIR:attributes_eol_block,verify,ignored_shape_files,leg_path_collisions,submodules: 368passed.
root_manifest,root_symlinks,symlink_target_bytes,root_shape_paths: 403 passed.test_shape_doctor.pyin 4 shards: 445 passed, 3 skipped (thePowerShell line, Windows only).
test_adopt_printed_sentences): the new file andtest_adopt_printed_sentences, 87 passed (33 and 54).Mutants of the change, each restored. Each one is killed:
makefilebeforeGNUmakefilemakefile+GNUmakefileand all three fail onmake whichagainst the split message itself.GNUmakefileormakefileis kept_root_keepsfolds case by defaultthe-root-keeps-itrow among themtest_the_handbook_follows_the_readmes_outlineamong themMerge with main
git merge-tree --write-treeof this head withmainatdc7cd49bisclean (tree
ae9abfcb). Of this branch's files, onlyadopt-project.pymoved on main: #1030 (#900) changed_spelled, and#1038 (#994) changed a hint and a docstring.
_spelledchanges only abackslash, a double quote or a backtick inside a name. The names this
line spells are plain letters, so its words do not change. README.md,
docs/handbook.htmlandtests/readme-cap.d/did not move on main, sothe marker stays current and the cap still holds.
On the merge, built in a
git clone --sharedcheckout: the new file,test_adopt_printed_sentences,test_repo_hygieneandtest_readme_sections, 263 passed.Not in this PR (follow-ups for lane 1 to file)
make bootstrap, measured. For a source that keepsmakefileorGNUmakefile,execute's NEXT block printsmake bootstrap.make -n bootstrapin the clone exits 2 withNo rule to make target 'bootstrap'. ForMakefileandGNUmakefile, its NOTEsays the shape's make targets "arrive only once the follow-up merges
the two". But GNU make at the root reads
GNUmakefile, not the mergedMakefile(inferred; before the merge,make -n bootstrapthereexits 2, as the NOTE says).
collision_follow_up's sentence. Inshape_materialize.py,"The shape's make targets ... arrive only with that merge" is likely
untrue where the source also keeps
GNUmakefileormakefile, whichGNU make reads before the merged
Makefile. Inferred, not measured.AGENTS-shape.mdin the pinned template. "Where the source kept aMakefile of its own, that block waits in
shape/Makefile" has thesame gap for a kept
makefile. It is pinned bytes, so it is left._predict_collisionsstays byte for byte, so on such a disk a planfor a kept
makefilelists no "merge shape/Makefile into Makefile"follow-up.
execute's own collision report lists it.Makefileandmakefile, two names that collide in a clone on a disk thatignores case.
gnumakefileare not asked about.Review round 0, body only (lane 1's reviewer r1046's partial review, issuecomment-6288714370): the
MAKEFILErow and the byte-for-byte sentence now say that a--path-policyrule, not a person's answer, is what keepsMAKEFILEin the root. Follow-ups are built from the rules' answers alone, so aMAKEFILEa person answersrootkeeps "the root Makefile wires ...", which a disk that ignores case makes untrue, as on base.Review round 1 (
c121ed90, test file only): the comment aboveANSWEREDsays a--path-policyrule, not a person's answer, reaches the follow-ups (r1046's SHOULD-1). The new test file is ASCII: its section signs and em dashes are written as escapes, with two lines rewrapped: the assert's string is unchanged, therepattern matches the same text (its raw string now holds the escape), and one docstring changes only where its lines break (NIT-2).tests/test_adopt_make_reads_first.py33,tests/test_adopt_printed_sentences.py54 andtests/test_repo_hygiene.py100 passed.Closes #977
Lane: openreposhape-1 (openRepoShape-1)
🤖 Generated with Claude Code
Summary by Sourcery
Make adoption follow-ups accurately reflect GNU make's filename precedence and the generated Makefile's location.
Bug Fixes:
Enhancements:
Documentation:
Tests: