From 83b3671b31bde0f8114bcd1483536171ec1a2e52 Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Tue, 20 Oct 2026 05:56:59 +0000 Subject: [PATCH 1/2] 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) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 15 +- adopt-project.py | 98 ++++++-- docs/handbook.html | 2 +- tests/test_adopt_make_reads_first.py | 346 +++++++++++++++++++++++++++ 4 files changed, 437 insertions(+), 24 deletions(-) create mode 100644 tests/test_adopt_make_reads_first.py diff --git a/README.md b/README.md index fba86cc0..56111ec2 100644 --- a/README.md +++ b/README.md @@ -807,13 +807,14 @@ The contracts decision costs something, and the plan says so instead of letting it be discovered later: `scripts/validate.py` reads `contracts/*.yaml` from beside it and must now read ACROSS the root. `plan` finds every such file with `git grep` and lists up to eight per directory in `follow_ups:`, counting the -rest; the root Makefile it materializes exports `CONTRACTS_DIR`, pointing at the -`contracts/` of the spec leg where the plan mounts it (`$(CURDIR)/spec/contracts` -by default). The tool does not make the edit — naming it is the -honest half, making it silently would be the dangerous one. And `.specify/` and -the ten identical assistant stubs stay at the root because an assistant reading -`CLAUDE.md` from inside the code leg would be told about specifications it -cannot see. +rest; the shape's Makefile exports `CONTRACTS_DIR`, pointing at the `contracts/` +of the spec leg where the plan mounts it (`$(CURDIR)/spec/contracts` by +default); it is the root's Makefile, or, where the source keeps a `Makefile` of +its own, `shape/Makefile`, which holds the export until a person merges the two. +The tool does not make the edit — naming it is the honest half, making it +silently would be the dangerous one. And `.specify/` and the ten identical +assistant stubs stay at the root because an assistant reading `CLAUDE.md` from +inside the code leg would be told about specifications it cannot see. ### Ambiguous is an answer, and no source line is overwritten diff --git a/adopt-project.py b/adopt-project.py index d44df71e..0c38b408 100755 --- a/adopt-project.py +++ b/adopt-project.py @@ -3515,7 +3515,8 @@ def _search_by_hand(named: str, why: str) -> str: "spec leg") -def _root_keeps(surviving: set[str], path: str) -> bool: +def _root_keeps(surviving: set[str], path: str, *, + fold: bool = False) -> bool: """Whether the root keeps `path`: as its own entry, or inside a directory it keeps there. `surviving` is each path the plan's entries send to the root. @@ -3525,10 +3526,85 @@ def _root_keeps(surviving: set[str], path: str) -> bool: Makefile line says where the block waits when that copy is the one that moved (#906). Two readings of "the source keeps its own `Makefile`" is how a follow-up says the root wires what `shape/Makefile` holds. + + `fold` asks it as a disk that ignores case finds a path, where `place`'s + `Path.exists()` holds a kept `makefile` to be `Makefile` (#977), as + `_KeptTree` asks it for `check`. """ + if fold: + surviving = {kept.casefold() for kept in surviving} + path = path.casefold() return path in surviving or f"{path.split('/')[0]}/" in surviving +#: The names GNU make looks for when no `-f` is given, in its order: it reads +#: the first it finds, and only that one. +GNU_MAKE_READS = ("GNUmakefile", "makefile", "Makefile") + + +def _makefile_follow_up(surviving: set[str]) -> str: + """The follow-up that says which Makefile wires `CONTRACTS_DIR`, where + the shape's is written, and, where the source keeps a file GNU make reads + before it, which one. `surviving` is each path the plan's entries send + to the root. + + WHERE THE ROOT KEEPS A `Makefile` OF THE SOURCE'S OWN, the shape's goes + beside it as `shape/Makefile` with the block in it, and the root's is + the source's, untouched (#906). + + GNU MAKE READS THE FIRST OF `GNUmakefile`, `makefile` AND `Makefile` IT + FINDS (#977). #906 asked only whether the source keeps `Makefile`, byte + for byte, so a source that keeps `makefile` or `GNUmakefile` got "the + root Makefile wires `CONTRACTS_DIR`": the root's `Makefile` is the + shape's, with the block, and `make` there reads the source's file and + never it, so `make contracts-dir` answered `No rule to make target`. The + line names that file wherever the source keeps one, beside a `Makefile` + of its own or not, and says what it takes for `make` at the root to + export `CONTRACTS_DIR`. + + AND ON A DISK THAT IGNORES CASE the source's `makefile` is `Makefile`: + `place` finds the name taken and writes the shape's as `shape/Makefile`, + so the line says the shape's is written as the root's `Makefile` where + case is kept, and as `shape/Makefile` where it is not, naming the kept + path that takes the name there. Whether GNU make itself reads a name by + its case there is its build's to say, so nothing more is said of it. + """ + aside = _root_keeps(surviving, SHAPE_MAKEFILE) + folded = (not aside + and _root_keeps(surviving, SHAPE_MAKEFILE, fold=True)) + twins = sorted(kept.rstrip("/") for kept in surviving + if kept.rstrip("/").casefold() == SHAPE_MAKEFILE.casefold()) + first = next((name for name in GNU_MAKE_READS + if name != SHAPE_MAKEFILE and _root_keeps(surviving, name)), + None) + whose = "the shape's" if aside or folded or first else "the root" + said = (f"{whose} Makefile wires `CONTRACTS_DIR` to the `contracts/` of " + "the spec leg, where the plan's `legs.spec_path` mounts it, and " + "exports it, so the code leg's tooling takes the path from the " + "environment instead of assuming it sits beside it") + copy = f"`{COLLISION_DIR}/{SHAPE_MAKEFILE}`" + if aside: + said += ( + "; but the source keeps a Makefile of its own at the root, so " + f"that block waits in {copy} until the two are merged, and the " + "run adds none of it to the root's Makefile") + elif folded: + said += ( + f"; it is written as the root's `{SHAPE_MAKEFILE}`, or as {copy}" + f"{_CASE_ASIDE}, where the source's {_spelled(twins[0])} is that " + "path, until the two are merged") + elif first: + said += f"; it is written as the root's `{SHAPE_MAKEFILE}`" + if first: + listed = _and_listed([f"`{name}`" for name in GNU_MAKE_READS]) + said += ( + f"{'; and' if aside or folded else ', but'} GNU make at the root " + f"reads the source's {_spelled(first)}, the first of {listed} it " + f"finds, so `make` there exports `CONTRACTS_DIR` only once " + f"{_spelled(first)} does, or includes a Makefile that does") + return said + + def follow_ups_for(source: "Source", entries: list["Entry"], names: dict, collisions: list[str]) -> list[str]: """The code changes an in-place split MAKES NECESSARY, named in the plan. @@ -3575,26 +3651,16 @@ def follow_ups_for(source: "Source", entries: list["Entry"], names: dict, make target` until the two are merged. The line then says the block waits in `shape/Makefile`, as #634 words it for the pinned `AGENTS-shape.md`, of the plan's entries as `_predict_collisions` reads - them (`_root_keeps`), the reading that moves the copy (#906). + them (`_root_keeps`), the reading that moves the copy (#906). And it + names the source's file GNU make reads before the shape's, and where a + disk that ignores case writes the shape's (`_makefile_follow_up`, #977). """ out = cross_leg_references(source, entries) if any(e.leg == "spec" and (e.path == "contracts/" or e.path.startswith("contracts/")) for e in entries): - own = _root_keeps({e.path for e in entries if e.leg == "root"}, - SHAPE_MAKEFILE) - whose = "the shape's" if own else "the root" - waits = ( - "; but the source keeps a Makefile of its own at the root, so " - f"that block waits in `{COLLISION_DIR}/{SHAPE_MAKEFILE}` until " - "the two are merged, and the run adds none of it to the root's " - "Makefile") - out.append( - f"{whose} Makefile wires `CONTRACTS_DIR` to the `contracts/` of " - "the spec leg, where the plan's `legs.spec_path` mounts it, and " - "exports it, so the code leg's tooling takes the path from the " - "environment instead of assuming it sits beside it" - + (waits if own else "")) + out.append(_makefile_follow_up( + {e.path for e in entries if e.leg == "root"})) if any(e.path in ("CODEOWNERS", ".github/CODEOWNERS") for e in entries): out.append( "CODEOWNERS in the assembly root now names paths that live in the " diff --git a/docs/handbook.html b/docs/handbook.html index d526011d..f4827d14 100644 --- a/docs/handbook.html +++ b/docs/handbook.html @@ -1275,7 +1275,7 @@

The three legs, and naming

- +

Reference

Adopting an existing repository

diff --git a/tests/test_adopt_make_reads_first.py b/tests/test_adopt_make_reads_first.py new file mode 100644 index 00000000..2f99cf64 --- /dev/null +++ b/tests/test_adopt_make_reads_first.py @@ -0,0 +1,346 @@ +# SPDX-License-Identifier: Apache-2.0 +"""The Makefile follow-up names the root file GNU make reads first (#977). + +#906 made the line say that a root `Makefile` the source keeps leaves the +block in `shape/Makefile`. It asks whether the source keeps `Makefile` byte +for byte (`_root_keeps`), and GNU make reads the first it finds of +`GNUmakefile`, `makefile` and `Makefile`, and only that one. So a source +that keeps `makefile` or `GNUmakefile` got "the root Makefile wires +`CONTRACTS_DIR`": the root's `Makefile` is the shape's, with the block, and +`make` there never reads it, so `make contracts-dir` answers `No rule to make +target` (r906's probe on #964, both names). And where the source keeps +`Makefile` beside one of them, the line said where the block waits and +nothing of the file `make` reads instead. + +A disk that ignores case was never asked: there the source's `makefile` IS +`Makefile`, `place` finds the name taken, and the shape's goes beside it as +`shape/Makefile`, where the line said the root's `Makefile` holds it. + +The line now says which file the shape's Makefile is written as, on each +disk, and names the source's file GNU make reads at the root wherever that +is not a `Makefile` the line already names. Each kept name in the rows below +has its own `which` target, so `make which` at the root of a real adoption +prints the name of the file GNU make read, and the line names that file. + +README.md's sentence (#977's (b)) said "the root Makefile it materializes +exports `CONTRACTS_DIR`", untrue where the source keeps a `Makefile` of its +own. Its section is bound to the handbook's `adopt` section +(`tests/test_repo_hygiene.py`), whose marker moves with it. + +The tests that run `execute` need `git filter-repo` and skip without it, and +the ones that run `make` skip without it. No network and no GitHub: the legs +are bare repositories on disk, and `tests/conftest.py` keeps a `gh` guard +first on PATH. +""" + +from __future__ import annotations + +import html +import re +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +from conftest import (ADOPT, REPO, git, load_without_bytecode, + make_source_repo, run_script, write_plan) +from test_adopt_contracts_dir import (EDITS, MAKEFILE_FOLLOW_UP, + OWN_MAKEFILE_FOLLOW_UP, PROJECT, + SPEC_PATH, SPLIT_BRANCH, TREE, + adopt_with_legs_mounted_away, + contracts_dir_line, needs_filter_repo) +from test_adopt_printed_sentences import NAMES, entries_of, makefile_lines +from test_repo_hygiene import readme_fingerprint, readme_segments + +sys.path.insert(0, str(REPO / "scripts")) +from repo_shape import load_yaml # noqa: E402 + +needs_make = pytest.mark.skipif( + shutil.which("make") is None or sys.platform == "win32", + reason="needs make, and a POSIX `$(CURDIR)` spelling") + + +@pytest.fixture(scope="module") +def adopter(): + """`adopt-project.py` loaded as a module, for its helpers.""" + return load_without_bytecode(ADOPT, "adopt_make_reads_first") + + +# --- the line, word for word ------------------------------------------------- + +#: What every form of the line says first, after whose Makefile it is. +WIRES = ( + "Makefile wires `CONTRACTS_DIR` to the `contracts/` of the spec leg, " + "where the plan's `legs.spec_path` mounts it, and exports it, so the code " + "leg's tooling takes the path from the environment instead of assuming " + "it sits beside it") +SHAPES = f"the shape's {WIRES}" + + +def written_as(twin: str = "") -> str: + """Where the shape's Makefile is written when the source keeps no + `Makefile`: the root's `Makefile`, or, where the source keeps `twin`, a + name a disk that ignores case reads as `Makefile`, `shape/Makefile` + there.""" + if not twin: + return "; it is written as the root's `Makefile`" + return ("; it is written as the root's `Makefile`, or as " + "`shape/Makefile` once case is ignored, as a macOS or Windows " + f"disk does, where the source's `{twin}` is that path, until the " + "two are merged") + + +def reads(name: str, joint: str = "; and") -> str: + """The clause naming the source's file GNU make reads at the root.""" + return (f"{joint} GNU make at the root reads the source's `{name}`, the " + "first of `GNUmakefile`, `makefile` and `Makefile` it finds, so " + f"`make` there exports `CONTRACTS_DIR` only once `{name}` does, " + "or includes a Makefile that does") + + +#: `(the root files the plan keeps, the line, the file GNU make reads first +#: at the root of a clone on a disk that keeps case)`. +ROWS = [ + pytest.param((), MAKEFILE_FOLLOW_UP, "Makefile", id="none"), + pytest.param(("Makefile",), OWN_MAKEFILE_FOLLOW_UP, "Makefile", + id="Makefile"), + pytest.param(("makefile",), + SHAPES + written_as("makefile") + reads("makefile"), + "makefile", id="makefile"), + pytest.param(("GNUmakefile",), + SHAPES + written_as() + reads("GNUmakefile", ", but"), + "GNUmakefile", id="GNUmakefile"), + pytest.param(("makefile", "GNUmakefile"), + SHAPES + written_as("makefile") + reads("GNUmakefile"), + "GNUmakefile", id="makefile+GNUmakefile"), + pytest.param(("Makefile", "makefile"), + OWN_MAKEFILE_FOLLOW_UP + reads("makefile"), + "makefile", id="Makefile+makefile"), + pytest.param(("Makefile", "GNUmakefile"), + OWN_MAKEFILE_FOLLOW_UP + reads("GNUmakefile"), + "GNUmakefile", id="Makefile+GNUmakefile"), + pytest.param(("Makefile", "makefile", "GNUmakefile"), + OWN_MAKEFILE_FOLLOW_UP + reads("GNUmakefile"), + "GNUmakefile", id="all-three"), +] + +#: `(path, leg)` rows no rule writes, a person's answers: `MAKEFILE` kept in +#: the root, a name GNU make never reads where case is kept and one a disk +#: that ignores case takes for `Makefile`; and `makefile` and `GNUmakefile` +#: sent to the legs, which leaves the root nothing GNU make reads but the +#: shape's. +ANSWERED = [ + pytest.param((("MAKEFILE", "root"),), SHAPES + written_as("MAKEFILE"), + id="MAKEFILE"), + pytest.param((("makefile", "code"), ("GNUmakefile", "spec")), + MAKEFILE_FOLLOW_UP, id="sent-to-legs"), +] + + +def follow_ups_of(adopter, monkeypatch, rows) -> list: + """`follow_ups_for` over plan entries made of `(path, leg)` rows, beside + a `contracts/` the spec leg holds, as `plan` calls it.""" + # The read-across lines are the `git grep`'s, which this asks nothing of. + monkeypatch.setattr(adopter, "cross_leg_references", + lambda source, entries: []) + entries = entries_of(("contracts/", "spec"), ("src/", "code"), *rows) + return adopter.follow_ups_for(None, entries, NAMES, + adopter._predict_collisions(entries)) + + +@pytest.mark.parametrize("kept, said, first", ROWS) +def test_the_line_names_the_file_gnu_make_reads_first( + adopter, monkeypatch, kept, said, first): + """One line per plan, and it names the source's file GNU make reads + before the shape's wherever that file is not the shape's `Makefile` or + the source's own `Makefile`, which #906's line already names.""" + ups = follow_ups_of(adopter, monkeypatch, + [(name, "root") for name in kept]) + assert makefile_lines(ups) == [said] + if first in ("makefile", "GNUmakefile"): + assert f"reads the source's `{first}`, the first of" in said + else: + assert "GNU make" not in said + + +@pytest.mark.parametrize("rows, said", ANSWERED) +def test_a_name_only_a_disk_that_ignores_case_takes_for_makefile( + adopter, monkeypatch, rows, said): + """`MAKEFILE` is no name GNU make reads where case is kept, and the + shape's `Makefile` is the root's there; where case is ignored `place` + finds `Makefile` taken by it and writes the shape's to `shape/Makefile`. + A name sent to a leg is in no root at all.""" + assert makefile_lines(follow_ups_of(adopter, monkeypatch, rows)) == [said] + + +@pytest.mark.parametrize("kept, said, first", ROWS) +def test_the_line_says_where_each_disk_writes_the_shapes_makefile( + adopter, monkeypatch, kept, said, first): + """One reading of the disk, `check`'s: `_KeptTree` answers for each disk + whether the root holds `Makefile`, which is where `place` writes the + shape's copy beside it, and the line says `shape/Makefile` for exactly + the disks on which it does.""" + rows = [(name, "100644", name) for name in kept] + held = {fold: adopter._KeptTree(rows, fold).holds("Makefile") + for fold in (False, True)} + line, = makefile_lines(follow_ups_of( + adopter, monkeypatch, [(name, "root") for name in kept])) + aside_where_case_is_kept = "so that block waits in `shape/Makefile`" + aside_once_ignored = "or as `shape/Makefile` once case is ignored" + assert (aside_where_case_is_kept in line) is held[False] + assert (aside_once_ignored in line) is (held[True] and not held[False]) + assert ("the root Makefile wires" in line) is ( + not any(held.values()) and first == "Makefile") + + +# --- `plan` and `execute`, end to end ---------------------------------------- + +def own_makefile(name: str) -> str: + """A root makefile the source keeps under `name`, with a `which` target + that prints its own name: `make which` says which file GNU make read.""" + return (f"which:\n\t@echo {name}\n" + "build:\n\t@echo the source's own build\n") + + +def tree_keeping(kept: tuple) -> dict: + return {**TREE, **{name: own_makefile(name) for name in kept}} + + +def disk_keeps_case(directory: Path) -> bool: + probe = directory / "Case-Probe" + probe.write_text("", encoding="utf-8") + return not (directory / "case-probe").exists() + + +@pytest.mark.parametrize("kept, said, first", ROWS[2:]) +def test_plan_writes_the_line_and_check_prints_it(tmp_path, kept, said, + first): + """The rules keep each of GNU make's three names in the root + (`root-project-makefile`), so `plan` writes the line for the source as + it is, and `check` prints it as the plan holds it.""" + if not disk_keeps_case(tmp_path): + pytest.skip("this disk ignores case: it cannot hold the rows apart") + source = make_source_repo(tmp_path / "Thing", tree_keeping(kept), + edits=EDITS) + plan = tmp_path / "adoption-plan.yaml" + written = write_plan(source, plan, project=PROJECT) + assert written.returncode == 0, written.stderr + written.stdout + follow_ups = [str(item) for item in load_yaml(plan)["follow_ups"]] + assert makefile_lines(follow_ups) == [said] + checked = run_script(ADOPT, "check", "--plan", str(plan)) + assert checked.returncode == 0, checked.stderr + checked.stdout + assert f" follow-up: {said}" in checked.stdout + + +def make(clone: Path, *args: str) -> subprocess.CompletedProcess: + return subprocess.run( + ["make", "--no-print-directory", "-s", *args], cwd=str(clone), + capture_output=True, text=True, encoding="utf-8", errors="replace", + check=False) + + +@needs_filter_repo +@needs_make +@pytest.mark.parametrize("kept, said, first", ROWS[1:]) +def test_the_split_message_names_the_file_make_reads_in_the_clone( + tmp_path, kept, said, first): + """The measurement the line rests on, on a disk that keeps case. After a + real `execute`, the split commit's message holds the line; `make which` + at the root of a recursive clone prints the name of the file the line + says GNU make reads; `make contracts-dir` there has no rule, for that + file is the source's; and the block is in the file the line says the + shape's Makefile is written as, where the same target prints the spec + leg's `contracts/`.""" + if not disk_keeps_case(tmp_path): + pytest.skip("this disk ignores case: it cannot hold the rows apart") + adopted = adopt_with_legs_mounted_away(tmp_path, by_hand=False, + tree=tree_keeping(kept)) + message = " ".join(git("log", "-1", "--format=%B", SPLIT_BRANCH, + cwd=adopted["source"]).stdout.split()) + clone = adopted["clone"] + # Each kept name is the source's bytes. + for name in kept: + assert (clone / name).read_text(encoding="utf-8").splitlines() == \ + own_makefile(name).splitlines() + # The file GNU make reads, as `make` itself says, is the one the split + # message names: the source's `Makefile` by #906's words, any other by + # name. + which = make(clone, "which") + assert which.returncode == 0, which.stderr + read = which.stdout.strip() + if read == "Makefile": + assert "the source keeps a Makefile of its own at the root" in message + assert "GNU make" not in message + else: + assert f"GNU make at the root reads the source's `{read}`, the " \ + "first of" in message + assert read == first + assert said in message + assert MAKEFILE_FOLLOW_UP not in message + at_the_root = make(clone, "contracts-dir") + assert at_the_root.returncode != 0, at_the_root.stdout + assert "No rule to make target 'contracts-dir'" in at_the_root.stderr + # Where the line says the shape's Makefile is: the root's `Makefile` + # where the source keeps none of that name, and `shape/Makefile` where + # it does. + shapes = "shape/Makefile" if "Makefile" in kept else "Makefile" + assert (clone / "shape" / "Makefile").is_file() is ("Makefile" in kept) + block = (clone / shapes).read_text(encoding="utf-8") + assert contracts_dir_line(block) == ( + f"CONTRACTS_DIR ?= $(CURDIR)/{SPEC_PATH}/contracts") + in_the_shapes = make(clone, "-f", shapes, "contracts-dir") + assert in_the_shapes.returncode == 0, in_the_shapes.stderr + assert Path(in_the_shapes.stdout.strip()) == ( + clone.resolve() / SPEC_PATH / "contracts") + + +# --- README.md and the handbook (#977's (b)) --------------------------------- + +ADOPTING = "Adopting an existing repository" + +#: The sentence, in r906's words as its re-check corrected them, with +#: `Makefile` spelled as the file it is: a source that keeps `makefile` has +#: the shape's Makefile written as the root's `Makefile` on a disk that +#: keeps case. +SENTENCE = ( + "the shape's Makefile exports `CONTRACTS_DIR`, pointing at the " + "`contracts/` of the spec leg where the plan mounts it " + "(`$(CURDIR)/spec/contracts` by default); it is the root's Makefile, or, " + "where the source keeps a `Makefile` of its own, `shape/Makefile`, which " + "holds the export until a person merges the two.") + + +def segment(name: str) -> str: + readme = (REPO / "README.md").read_text(encoding="utf-8") + (text,) = [text for title, text in readme_segments(readme) + if title == name] + return text + + +def test_readme_says_which_makefile_exports_contracts_dir(): + """It said "the root Makefile it materializes exports `CONTRACTS_DIR`", + and where the source keeps a `Makefile` of its own the root's is the + source's, untouched, and the export is in `shape/Makefile` only.""" + said = " ".join(segment(ADOPTING).split()) + assert SENTENCE in said + assert "the root Makefile it materializes" not in said + + +def test_the_handbook_reads_the_sentence_as_it_now_stands(): + """The handbook's `adopt` section reads README.md § Adopting an existing + repository, and says what the sentence leans on: a shape file whose name + the source holds, `Makefile` among them, is written under `shape/`. Its + marker records the section as it now stands, so the page was read + against the new sentence.""" + page = (REPO / "docs" / "handbook.html").read_text(encoding="utf-8") + section = page.split('
', 1)[1].split("
", + 1)[0] + marker = re.search(rf"", section) + assert marker is not None + assert marker.group(1) == readme_fingerprint(segment(ADOPTING)) + text = " ".join(html.unescape(re.sub(r"<[^>]+>", "", section)).split()) + assert ("A shape file whose name the source already holds — README.md, " + "Makefile, .gitignore — is written under shape/") in text From c121ed909e01ebe93823a977ec21fa291ec50df1 Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Tue, 20 Oct 2026 10:49:28 +0000 Subject: [PATCH 2/2] tests/test_adopt_make_reads_first.py (#977, review round 1): the ANSWERED 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 --- tests/test_adopt_make_reads_first.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/test_adopt_make_reads_first.py b/tests/test_adopt_make_reads_first.py index 2f99cf64..59a53bca 100644 --- a/tests/test_adopt_make_reads_first.py +++ b/tests/test_adopt_make_reads_first.py @@ -126,7 +126,8 @@ def reads(name: str, joint: str = "; and") -> str: "GNUmakefile", id="all-three"), ] -#: `(path, leg)` rows no rule writes, a person's answers: `MAKEFILE` kept in +#: `(path, leg)` rows no shipped rule writes, which a `--path-policy` rule can +#: (a person's answer in the plan changes no follow-up): `MAKEFILE` kept in #: the root, a name GNU make never reads where case is kept and one a disk #: that ignores case takes for `Makefile`; and `makefile` and `GNUmakefile` #: sent to the legs, which leaves the root nothing GNU make reads but the @@ -329,18 +330,19 @@ def test_readme_says_which_makefile_exports_contracts_dir(): def test_the_handbook_reads_the_sentence_as_it_now_stands(): - """The handbook's `adopt` section reads README.md § Adopting an existing - repository, and says what the sentence leans on: a shape file whose name - the source holds, `Makefile` among them, is written under `shape/`. Its - marker records the section as it now stands, so the page was read - against the new sentence.""" + """The handbook's `adopt` section reads README.md \u00a7 Adopting an + existing repository, and says what the sentence leans on: a shape file + whose name the source holds, `Makefile` among them, is written under + `shape/`. Its marker records the section as it now stands, so the page + was read against the new sentence.""" page = (REPO / "docs" / "handbook.html").read_text(encoding="utf-8") section = page.split('
', 1)[1].split("
", 1)[0] - marker = re.search(rf"", section) assert marker is not None assert marker.group(1) == readme_fingerprint(segment(ADOPTING)) text = " ".join(html.unescape(re.sub(r"<[^>]+>", "", section)).split()) - assert ("A shape file whose name the source already holds — README.md, " - "Makefile, .gitignore — is written under shape/") in text + assert ("A shape file whose name the source already holds \u2014 " + "README.md, Makefile, .gitignore \u2014 is written under " + "shape/") in text