Skip to content

adopt-project.py: check names the case twins the plan keeps in one repository as plan-case-twins on every disk, a one-blob set as a NOTE in a leg and, in the root, two paths in one folder, and execute refuses the finding before any leg (#1161) - #1189

Draft
brettheap wants to merge 23 commits into
mainfrom
fix/1161-check-names-case-twins

Conversation

@brettheap

@brettheap brettheap commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1161

Stacked on #1158 (head 4c7b8acd), which landed as ef619c74; this branch was restacked onto it by three merges:

The net diff is identical: git diff ef619c74 6a31aea7 is byte for byte git diff 4c7b8acd a131fc25 (sha256 04b2909a71bc, 3 files, 686 insertions, 27 deletions); at the head, 744 and 27. adopt-project.py and tests/test_adopt_check_throwaway_repos.py at ef619c74 are the blobs of 4c7b8acd, so "before" and "base" below, 4c7b8acd, are what main holds. It is outside v1.2.0 by Brett Heap's ruling of 2026-10-21T10:32:04Z ("leave the follow-ups out of v1.2.0").

Merge with main after #1105

47478d51 merges main at 12122952 (#1105's squash) into this branch at 7d2f651e, the head the next section ends at. Their merge-base is 11560eb9, #1223's squash, and main holds 8 commits since it: #1166, #1133, #1104, #1131, #1160, #1190, #1243 and #1105. Of them #1190 and #1105 change adopt-project.py (#1190 cmd_plan, _grouped_findings, _refuse_a_partial_clone_short and main; #1105 _predict_collisions), in lines this PR does not touch, and none changes a test file of this PR's. The merge was clean: git merge-tree --write-tree of main and 7d2f651e gave tree 73fffbb8, the merge's tree.

Merge with main after #1169 and #1223

5c4fcd9d merges main at 11560eb9 (#1223's squash) into this branch at 58aa9d2b, the head r1189c read. Their merge-base is ef619c74, #1158's squash, and main holds 20 commits since it: #1124, #1140, #1149, #1126, #1107, #1157, #1088, #1193, #1187, #1119, #1156, #1208, #1121, #1200, #1180, #1109, #1235, #1144, #1169 and #1223. Of them #1119, #1121, #1180 and #1169 change adopt-project.py, and #1119 and #1169 change tests/test_adopt_check_throwaway_repos.py.

What

Measured with git 2.43, on the bench disk (keeps case) and on a FUSE disk that ignores case and keeps it (casefold lookups), with the work dir and every repository on it.

Before (the tool at 4c7b8acd): on the bench disk check prints plan ok for every scenario below, and execute exits 0 and verifies each one it was run on. not measured means not run at 4c7b8acd; (bench disk only) means the base run of the row's test, exit 0 on the bench disk, and nothing on the disk that ignores case. On the disk that ignores case:

scenario check before execute before, disk that ignores case check after
A/a in the root, own blobs plan ok exit 0, verified FINDING plan-case-twins
A/a in the root, one blob plan ok exit 0, verified (1 run, r1189's) NOTE, plan ok; execute verified (3 of 3 runs: 1 at 4d40ce9f, 2 at a131fc25)
D/A/D/a in the root, one folder, one blob plan ok (bench disk only) not measured NOTE, plan ok; execute verified (2 of 2 runs, at a131fc25)
src/A/src/a, one blob, code leg plan ok exit 0, verified (2 of 2 runs) NOTE, plan ok; execute verified (3 of 3 runs)
specs/A/specs/a, one blob, spec leg plan ok not measured NOTE, plan ok; execute verified (1 run)
D/x/d/x in the root, own blobs plan ok in 2 of 4 fresh runs, else plan-file-reencoded ("will not add file alias 'D/x'") refused before any leg as plan-file-reencoded FINDING plan-case-twins (then plan-file-reencoded where git refuses the alias)
D/x/d/x in the root, one blob plan ok (bench disk only) not measured FINDING plan-case-twins and FINDING plan-file-reencoded (2 of 2 runs, at a131fc25); execute refused before any leg, as plan-case-twins (2 of 2)
AB/Ab/ab in the root, one blob plan ok (bench disk only) not measured the same: both findings, execute refused before any leg as plan-case-twins (2 of 2 runs, at a131fc25)
F/x/f in the root plan ok exit 1, adopt-lost: F/x, both legs and the split already pushed FINDING plan-case-twins
src/A, src/a, src/D/x, src/d/x, own blobs, code leg plan ok 5 runs, two outcomes: exit 0, verified, in 2 (the first round's); exit 2 in 3 (r1189's), refused mounting the legs at git rm -r -q -- src ("the following files have local modifications: src/A, src/D/x") after both legs were pushed FINDING plan-case-twins
A, D/x root; a, d/x code leg plan ok exit 1, adopt-lost for A and D/x, both legs and the split already pushed plan ok (Not in this PR, (a), #1183)
A root; a answered drop plan ok exit 1, adopt-lost: A, both legs and the split already pushed plan ok (Not in this PR, (a), #1183)
no twins (control) plan ok exit 0 plan ok

After: check exits 1 and names, one finding per repository the split makes (the root, the spec leg, the code leg), every set of source paths that one answered entry sends into one repository whose paths differ only in case, or where one path names in another case a folder above another. A set whose paths all fold to one name and hold one blob in one mode is left out of the finding: a NOTE names it, and check still says plan ok for it. In a leg that holds for every such set, folder twins (src/D/x, src/d/x) and sets of three or more included. In the root the NOTE takes only two paths whose names differ only in their last part, A and a or D/A and D/a: folder twins of one blob there (D/x, d/x), two paths whose folders are twins too (D/A, d/a) and sets of three or more (AB, Ab, ab) stay in the finding, since on the FUSE disk execute met git's will not add file alias for them. The FINDING says why a clone on a macOS or Windows disk checks out one path of each set, and that execute could stop there before any leg exists, at git's will not add file alias, or after both legs are pushed, as adopt-lost or at git rm mounting the legs, so execute refuses them on every disk. Its remediation says to rename all but one twin of each set in the source, commit, and re-run plan. It no longer offers to answer the twins into different repositories: on a disk that ignores case execute loses those too (#1183). execute runs the same checks first, so it refuses such a plan before it makes either leg.

  • Behaviour change: every set of source paths that one answered entry sends into one repository is now refused by execute on every disk, and check names it plan-case-twins and exits 1, unless its paths fold to one name and hold one blob in one mode (the NOTE). In the root the NOTE takes only two paths whose names differ only in their last part, so folder twins and sets of three or more of one blob there are refused too. A source that keeps Makefile and makefile side by side in the root is one: before, check printed plan ok, and on a disk that keeps case execute split the source and verified it. A source that keeps only makefile is not, though the shape writes the root's Makefile beside it (Not in this PR, (c), adopt-project.py: check passes a kept path that is a case twin of a file the shape writes into the made root (makefile beside the shape's Makefile), so a clone of that root on a disk that ignores case keeps one of each pair (#1161) #1191). Two params of tests/test_adopt_make_reads_first.py held Makefile and makefile, [Makefile+makefile] and [all-three], and they changed:
    • test_plan_writes_the_line_and_check_prints_it, at those two params, now expects exit 1, the plan-case-twins finding naming Makefile and makefile, and the follow-up line check still prints (its other params still expect exit 0).
    • test_the_split_message_names_the_file_make_reads_in_the_clone no longer runs those two params, since execute refuses their source before any leg and there is no clone to run make in; the new test_execute_refuses_makefile_beside_makefile_before_any_leg, at the same two params, asserts that refusal as plan-case-twins: no bare repository, no assembly or leg clone in the work directory, no split branch in the source.
    • The alternative not taken is a NOTE, not a finding, on a disk that keeps case, where such a source does split and verify. The finding is asked on every disk for the reason Why gives first: the plan is written on one disk and the clone that loses a twin is made on another, so core.ignorecase where check runs says nothing about the reader's disk.

Why

  • Asked on every disk. The plan is written on one disk and the clone that loses a twin is made on another, so core.ignorecase where check runs says nothing about the reader's disk.
  • One repository at a time: the root (what the plan keeps), the spec leg and the code leg, and only the source's paths, each sent there by one answered entry. Not asked here, each a follow-up:
  • Two kinds of set: exact twins (whole paths equal under casefold), and a file against a folder (a path's casefold equals a folder above another path). D/x beside d/y is no finding: on any disk the two folders merge into one and no path is lost.
  • A set of one blob is a NOTE (r1189's SHOULD 5). Where every path of a set folds to ONE name and holds one blob in one mode, the one path a disk that ignores case checks out holds what each path holds; git warns the following paths have collided, and git status is clean (r1189). A file against a folder, two modes (100644 beside 100755), submodules (whose object is a commit, not a blob), or one path of another blob keep the set in the finding. check already prints NOTEs beside its findings (the pins' digests, the symlink probe, relative submodule urls), so the set is named in one, the first of the NOTEs _entry_findings returns. execute prints no NOTE and does not refuse it.
  • The root's NOTE is narrower than a leg's (r1189b, the second review, of 4d40ce9f: adopt-project.py: check names the case twins the plan keeps in one repository as plan-case-twins on every disk, a one-blob set as a NOTE in a leg and, in the root, two paths in one folder, and execute refuses the finding before any leg (#1161) #1189 issuecomment-6364543347, SHOULD 1). In the root it takes two paths whose names differ only in their last part, A and a or D/A and D/a, the shape that verified there in every run measured. Every other set of one blob there is the finding, with its rename remediation: on the FUSE disk D/x and d/x, and AB, Ab and ab, met git's will not add file alias, which the adopt-project.py check: a kept or moved file other than .gitattributes whose blob git add would re-encode passes, and execute ends adopt-lost or dies mounting the legs after both legs are pushed #654 rehearsal reports as plan-file-reencoded; and D/A beside d/a has folders that are twins too. Every such set measured in a leg verified. What execute did on the FUSE disk:
    • in the root, with the sets the NOTE takes, adoption verified in every run: files A and a, 4 runs (1 at 4c7b8acd, r1189's; 1 at 4d40ce9f; 2 at a131fc25); files D/A and D/a, 2 runs at a131fc25; for r1189b, 1 run of symlinks to one target (A, a) and 1 of executables (A, a);
    • in a leg, adoption verified in every run: in the code leg, files src/A and src/a, 5 runs (2 at 4c7b8acd, 3 with this check), and for r1189b 1 run each of symlinks to one target (src/A, src/a) and of folder twins (src/D/x, src/d/x), alone and beside folders that only merge; in the spec leg, files specs/A and specs/a, 1 run;
    • with the sets the root leaves to the finding: for r1189b, at 4d40ce9f, folder twins (D/x, d/x), alone and beside folders that only merge, met plan-file-reencoded in check (1 run each), and three paths (AB, Ab, ab) passed check and met it in execute, before any leg (1 run); at a131fc25, 2 runs each of folder twins and of three paths: check printed plan-case-twins and plan-file-reencoded, and execute refused both before any leg as plan-case-twins.
  • The remediation is a rename in the source, and nothing else (r1189's MUST 1). Answering the twins into different repositories, which it offered before, passes check and loses them on a disk that ignores case after both legs are pushed (r1189 on FUSE: check exit 0, execute exit 1, adopt-lost for A and D/x).
  • Paths git refuses are left out (plan-path-git-refuses already names them), and so are drop and unanswered entries: the code reads each entry's answer with _covering, as _PlannedRoot._keep does, and folds with casefold, as _KeptTree does. An unanswered twin is named by plan-unresolved alone, with no twins remediation.
  • Sets are named set by set, at most 10 paths per repository, the rest counted, by the file's _named_and_counted, in the finding and in the NOTE alike.
  • Nothing to register in the docs: no doc lists check's plan-* codes (AGENTS.md names none; docs/cli.md is --help, which takes the docstring's first line only). The module docstring's check paragraph and _entry_findings' docstring name the new check and the NOTE.

Tests

  • Base (4c7b8acd) against head, whole of tests/test_adopt_check_throwaway_repos.py, CPython 3.12:
    • at a131fc25: base 34 failed, 52 passed, 2 skipped; head 86 passed, 2 skipped. The 34 are the 29 that failed at 4d40ce9f (base 29 failed, 52 passed, 2 skipped; head 81 passed, 2 skipped) and the 5 rows a131fc25 added;
    • at the head, 58aa9d2b: 91 passed, 2 skipped. Its 5 new ids, with the 8 rows of test_a_set_of_one_blob_is_a_note_and_not_the_finding that were there, were run at base alone, 13 ids: 12 failed, 1 passed (the execute row, which passes at base, where check has no NOTE for execute to refuse). The whole file was not run at base at 58aa9d2b; by addition, 38 of its ids fail there.
  • Under a 3.14-like sitecustomize shim the file was run only at 4d40ce9f: the same 29 fail on base (the shim loaded in 192 processes) and head passed 81, 2 skipped (193 processes). Not made, for the rounds since: the shim run at a131fc25 and at 58aa9d2b; the prev run, the tool at 4d40ce9f against this head's tests; and the prevw run, the same with the tests' twins_kept sentence set back to "would", which would have told the rows of the root rule from the sentence's.
  • The rows that fail on base and pass on head, 34 at a131fc25 and 4 more at 58aa9d2b:
    • test_check_names_each_twin_git_refuses_and_each_set_it_holds (tests/test_adopt_check_throwaway_repos.py: rows pin that refused_paths holds each case twin under its own name and blob, or names it, with core.ignorecase true or false, and that check names the twins git refuses and none it holds (#1033) #1158's row, renamed: it now also expects the twins finding for A/a, D/x/d/x, F/x/f);
    • test_check_names_the_case_twins_the_plan_keeps_in_one_repository, 14 params: file-twins, folder-twins, a-folder-and-a-file, a-file-and-a-folder, three-twins-none-in-lower-case, twins-in-the-spec-leg, non-ascii-twins-in-the-code-leg, two-sets-in-the-code-leg-set-by-set, a-set-in-each-repository, ten-named-and-the-rest-counted, and, for r1189's SHOULD 6, a-file-and-a-folder-in-the-code-leg (src/E, src/e/x), a-file-and-a-folder-two-levels-above (f, F/G/x), sharp-s-folded-as-ss (src/straße, src/STRASSE, known limit 1) and one-set-of-four; each also asserts the whole remediation line, which offers no answer into different repositories, and no NOTE;
    • test_twins_come_before_coverage_and_name_executes_refusal (r1189's SHOULD 6): beside a path no entry covers, check lists plan-case-twins then plan-uncovered, and execute's refusal code is plan-case-twins;
    • test_a_set_of_one_blob_is_a_note_and_not_the_finding, 10 params (r1189's SHOULD 5, and r1189b's SHOULD 1): A/a of one blob is the NOTE and plan ok, exit 0; of two blobs, the finding, exit 1; three paths of one blob in the code leg, the NOTE; F/x/f of one blob, the finding; and, in the root, folder twins of one blob (D/x, d/x) and three paths of one blob (AB, Ab, ab) are the finding, and two paths in one folder (D/A, D/a) are the NOTE; folder twins of one blob are the NOTE in the code leg (src/D/x, src/d/x) and in the spec leg (specs/D/x, specs/d/x); and a file against a folder of one blob and one last name in the code leg (src/F/f, src/f) is the finding;
    • test_only_one_blob_in_one_mode_is_left_to_the_note, 8 params, _case_twin_findings asked itself: one blob in two modes, submodules at one commit, and one of three paths in another blob are the finding; symlinks to one target are the NOTE; one repository can hold a set of each; two NOTE sets are named set by set; STRASSE and straße of one blob are one name for the NOTE, as casefold folds them; and, in the root, D/A beside d/a of one blob is the finding, as their folders are twins too. At base they fail because the function is not there;
    • test_every_path_of_a_leg_set_is_asked_its_mode, 2 params, the same call over the code leg: src/AB, src/Ab and src/ab of one blob, the last an executable or a symlink, hold two modes, so the finding and no NOTE, whichever path differs;
    • test_the_one_blob_note_is_the_first_of_the_entries_notes: _entry_findings lists the twins' NOTE before the pins';
    • test_execute_refuses_case_twins_before_any_leg.
  • Against the tool at an earlier head of this PR, 8684af86, with the test file of e4f8cc51, the 10 rows of test_a_set_of_one_blob_is_a_note_and_not_the_finding and test_only_one_blob_in_one_mode_is_left_to_the_note then in it fail: check named A/a of one blob in the root, and three paths of one blob in the code leg, as plan-case-twins, where they are now the NOTE; the two finding rows fail on the sentence NIT 7 changed; the 6 direct calls fail as the function returned no NOTEs.
  • Controls that pass on both: test_paths_no_one_repository_holds_as_one_name_are_no_finding (4 params: twins in different repositories, a twin dropped, D/x beside d/y, no twins), and test_a_twin_still_unanswered_is_named_by_plan_unresolved_alone. Its docstring now says the first two are lost on a disk that ignores case (adopt-project.py: on a disk that ignores case, execute loses a case twin the plan keeps in the root when its twin goes to a leg #1183), not held apart. test_execute_takes_sets_of_one_blob_past_checks_findings passes on both too: it runs cmd_execute in the process on a plan keeping A and a in the root and src/A and src/a in the code leg (plan ok), and sees it go on past check's findings to the next step, where a sentinel stops it before any leg exists (no remotes directory); at base there is no NOTE for execute to refuse. test_on_a_disk_that_ignores_case_check_names_the_twins_too skips unless git init in the work dir writes core.ignorecase=true. On the FUSE disk it passes at 4d40ce9f, beside test_on_a_disk_that_ignores_case_each_twin_is_held_or_named (2 passed); r1189 saw it fail at base.
  • Mutants of the new code, run in a clone, restored after each with its blob checked against HEAD's: 36 of 36 killed. 31 were killed at a131fc25, in one chain, and the 5 that survived there at 58aa9d2b; every blob was restored.
    • The 36: r1189b's 26, re-anchored for a131fc25 (r1189's five survivors at 8684af86, killed by the SHOULD 6 rows: m01 (casefold as lower) by sharp-s-folded-as-ss, m03 (the top level only) by a-file-and-a-folder-in-the-code-leg, m04 (the immediate parent only) by a-file-and-a-folder-two-levels-above, m11 (the finding listed last) by the coverage row, m15 (len(twins) // 2) by one-set-of-four; and b01-b21: the NOTE's conditions, its text, its place among the NOTEs, the remediation, the sentence and execute's refusal; b21 is now on the sentence's could); and this round's 10 for the root rule and the sentence (f01-f10): the root's shape not asked; two paths only; one folder only; every repository, not the root alone; the top level only; the folders folded; last parts that differ; three paths taken; the spec leg treated as the root; the sentence back to "would".
    • The 5 that survived at a131fc25, all killed at 58aa9d2b:
      • b17 (execute refusing the NOTE) and f09 (the spec leg treated as the root), expected, by test_execute_takes_sets_of_one_blob_past_checks_findings and by the row folder-twins-of-one-blob-in-the-spec-leg;
      • b06 (the blob and mode of the first two paths only), by test_every_path_of_a_leg_set_is_asked_its_mode;
      • b02 (the one-name test always true) and b18 (only the last part folded), which were not expected, are not equivalent: in a leg a set that does not fold to one name reaches _one_blob_twins, src/F/f beside src/f of one blob, a file against a folder, and the row a-folder-and-a-file-of-one-blob-in-the-code-leg kills both. In the root they are unreachable, as the test for two paths in one folder returns first.
    • Before the root rule, at 4d40ce9f and 6e3ebfa6: 32 of 32 killed, r1189's 17 (m01-m17) and the NOTE's own 15 (n01-n15). n15 survived at 6e3ebfa6 and is killed at 4d40ce9f by sharp-s-and-ss-of-one-blob; n13 survives without test_the_one_blob_note_is_the_first_of_the_entries_notes, which is why that row exists. Those 32 were not run again at this head, nor were the first round's 23, of the code as it was then.
  • The gate, call by call, each with df -h /workspace first and one basetemp, CPython 3.12. The tool's code has not changed since a131fc25 but for a comment (e45e1225), so the runs made at a131fc25 stand for it (they ran before the restack, which brought in files of main's that this PR does not touch; the make-reads-first file ran again after it, at 6a31aea7); two calls were run again at the head:
    • at 58aa9d2b: test_path_guard_hygiene, test_agents_md_codes, test_cli_reference, test_repo_hygiene, test_no_auto_maintenance: 278 passed, 2 skipped (the session began with GIT_CONFIG_GLOBAL set). At a131fc25 it was 277 passed, 2 skipped; the one more is tests/test_repo_hygiene.py::test_the_one_line_install_is_the_same_everywhere[docs/release-notes/v1.2.0.md], a file the restack brought in from main;
    • at 58aa9d2b: tests/test_adopt_check_throwaway_repos.py, whole file: 91 passed, 2 skipped;
    • at 6a31aea7, after the restack: that file with tests/test_adopt_make_reads_first.py: 119 passed, 2 skipped (86 + 33);
    • at a131fc25: tests/test_adopt_make_reads_first.py, whole file: 33 passed; tests/test_scaffold_leg_path_collisions.py: 104 passed; tests/test_shape_doctor.py in 4 shards: 111 passed, 1 skipped; 112 passed; 111 passed, 1 skipped; 111 passed, 1 skipped. The skips are the PowerShell row, which runs only on a Windows host;
    • From the first round, at a43edbdc (test_adopt_make_reads_first at 8684af86): g2 test_adopt_root_shape_paths 121 passed, test_adopt_root_symlinks 205 passed, 3 skipped (reftable needs git 2.45); g3 test_adopt_contracts_dir, test_adopt_plan_leg_path_help, test_adopt_device_names, test_adopt_finding_spelling, test_adopt_check_plan_name, test_adopt_printed_sentences, test_adopt_leg_paths: 702 passed; test_adopt_execute_refusals 181 passed; test_adopt_pins and test_adopt_source_clone_record 250 passed; test_adopt_answered_legs, test_adopt_leg_path_collisions, test_adopt_manifest_values, test_adopt_mount_ignored, test_adopt_relative_submodule_url, test_adopt_root_manifest 104, 108 and 61 passed; test_adopt_plan_yaml_quoting, test_adopt_submodule_url_answers 562 passed, test_adopt_symlink_target_bytes 130 passed. Not re-run: the rounds since change only what plan-case-twins says, its remediation, and which sets of one blob are a NOTE. Those files met no set of twins at a43edbdc (they passed with the finding live there), so they meet neither the finding nor the NOTE now.

Known limits

  1. casefold is full case folding, so ß and ss are named as a set, though a disk may hold them apart (row sharp-s-folded-as-ss).
  2. Unicode normalization is not asked: a precomposed é beside e and a combining accent is no finding, though a macOS disk takes them as one name.
  3. At most 10 paths are named per repository and the rest counted, in the finding and in the NOTE, and the cut can fall inside a set.
  4. The one-blob NOTE, and the root's narrower rule for it, rest on the FUSE disk (casefold lookups) with git 2.43, 2 runs each for the sets this round added. Real APFS and NTFS were not measured.
  5. The row for a disk that ignores case skips on every CI disk, which keeps case. The bench measured it on the FUSE disk, and under a global core.ignorecase=true, where folder twins (D/x, d/x) give plan-case-twins, then plan-file-reencoded (3 of 3 runs). A and a there give plan-case-twins alone (r1189).

Not in this PR

Lane: openreposhape-1 (openRepoShape-1)

🤖 Generated with Claude Code

Brett Heap and others added 7 commits October 22, 2026 12:35
…hs` holds each case twin under its own name with its own blob, or names it, with `core.ignorecase` true (#1033)

`refused_paths` (#910, #1031) writes its own index with `update-index -z
--index-info` and no row said what that index makes of case twins where
`core.ignorecase` is true, the default on a disk that ignores case.
Measured with git 2.43: git holds both twins of a path it takes (a file,
a folder, a folder beside a file), each with its own blob, and leaves out
both twins of a path it refuses, which `plan-path-git-refuses` names.

- `with_paths` gains `own_blobs`, so a twin held with its sibling's blob
  would show;
- ten rows ask `refused_paths` in a rehearsal whose own config holds
  `core.ignorecase` true or false (the control), and read the index with
  `git ls-files -s`;
- one row asks a disk that ignores case itself, and skips where `git init`
  wrote no `core.ignorecase=true` (every CI disk, Linux since #597);
- one `check` row, under a global `core.ignorecase=true`, pins that
  `plan-path-git-refuses` names the four twins git refuses and none of the
  six it holds, and that `plan` asks about each twin by name.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s by its mode beside the one it holds (a symlink `.GITMODULES` and the file `.gitmodules`), a file beside a folder, three twins and a non-ASCII pair (#1033)

Round 1 review of #1158:
- `with_paths` and `with_twins` take `links`, written as mode 120000, and
  `test_a_twin_git_refuses_by_its_mode_is_named_beside_the_one_it_holds` asks
  `refused_paths` for the pair with `core.ignorecase` true and false: git
  refuses the symlink and holds the file, so an answer that folds the index's
  paths into one case now fails (hand mutant HM4 was not equivalent).
- `CASE_TWINS` gains `a-file-and-a-folder`, `three-twins` and
  `non-ascii-twins` (spelled with escapes, so the file stays ASCII).
- The `check` row's docstring no longer says no finding refuses twins of each
  other: under a global `core.ignorecase=true` the #654 rehearsal refuses
  `D/x` and `d/x`, and the row does not see it because the rehearsal asks
  nothing of a source that holds a path git refuses.
- The disk row's skip text no longer names the per-run work directory.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…NS` and the `check` row's docstring name the twins they hold, and the held-or-named row's docstring says each twin, not both (#1033)

Round 2 re-check of #1158, two comment NITs, no code change:
- The comment above `ALL_TWINS` and the `check` row's docstring said they
  held "every twin above". They hold the twins of `file-twins`,
  `folder-twins` and `a-folder-and-a-file` and the two refused sets, and not
  the twins of `a-file-and-a-folder`, `three-twins` and `non-ascii-twins`.
  Both now say so. `ALL_TWINS` itself is unchanged.
- `test_each_case_twin_is_held_with_its_own_blob_or_named` said "both twins"
  of a path git takes, and `three-twins` holds three. It says "each twin".

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…repository, `A` and `a`, `D/x` and `d/x`, `f` and `F/x`, as `plan-case-twins` on every disk, and `execute` refuses them before any leg (#1161)

A disk that ignores case holds each such set as one name. Measured with
git 2.43 at 4c7b8ac, on such a disk, `check` printed `plan ok` for every
set; `execute` lost `F/x` after both legs were pushed, and was refused at
folder twins as `plan-file-reencoded` in 2 of 4 fresh sources.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… for each set the plan keeps in the root, the spec leg or the code leg, its absence for twins in different repositories, a dropped twin and folders that only merge, and `execute`'s refusal before any leg; #1158's row names the six twins it holds; `with_paths` merges into a folder HEAD holds (#1161)

`with_paths` wrote a second `src` or `specs` tree beside HEAD's (git
fsck: duplicateEntries); it now merges a new path into the folder HEAD
already holds.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o path in lower case, so a fold of the set's names that skips `casefold` is seen, and the unresolved twin's row says no twins remediation is printed (#1161)

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e` and `makefile` side by side expect `check`'s `plan-case-twins` and exit 1, and `execute` is asked to refuse that source before any leg, so they no longer run `make` in a clone (#1161)

`test_plan_writes_the_line_and_check_prints_it`, at `[Makefile+makefile]` and `[all-three]`: `check` exits 1 and prints the one `plan-case-twins` finding naming `Makefile` and `makefile`, beside the follow-up line it still prints. The other params still expect exit 0.

`test_the_split_message_names_the_file_make_reads_in_the_clone` drops those two params: `execute` now refuses their source before any leg exists, so there is no clone to run `make` in. `test_execute_refuses_makefile_beside_makefile_before_any_leg`, at the same two params, asserts the refusal as `plan-case-twins`: no bare repository, no assembly or leg clone in the work directory, no split branch in the source. Where `check` run on a disk that keeps case once passed this source and `execute` split and verified it, both now refuse it on every disk, as for any other set of case twins.

Measured on CPython 3.12 and under a 3.14-like shim, the whole file: 4c7b8ac 4 failed, 29 passed; head 33 passed. The 4 are the two changed `check` params and the two refusal params.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Implementation

Add case-twin grouping, one-blob NOTE rules, per-repository findings and remediation, and integrate them into check so execute refuses unsafe plans before creating legs.

  • adopt-project.py L117-125, L9432-9605, L9771-9780 (+4 small edits)

Tests

Exercise twin detection, NOTE exceptions, finding order and execution refusal, including disk behavior and the updated Makefile collision expectations.

  • tests/test_adopt_check_throwaway_repos.py L55-63, L214-226, L534-975 (+3 small edits)
  • tests/test_adopt_collision_make_reads.py L309-316, L384-392 (+3 small edits)
  • tests/test_adopt_make_reads_first.py L25-31, L137-159, L267-301 (+4 small edits)
  • tests/test_adopt_next_make_reads_first.py L46-47, L275-276, L292-299

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-23T03:09:10Z) Independent review r1189 of #1189 (Closes #1161) at 8684af86: NOT READY, for two MUSTs and four SHOULDs. The review stopped at LAST STEP, and the parts it did not cover are named at the end.

r1189 review of PR #1189 (Closes #1161), head 8684af86: NOT READY (for the parts covered)

The grouping code is correct on every shape I probed, and base against head reproduces the body's counts. Two things block it: the remediation sends people into a plan that loses a twin after both legs are pushed, and the PR's central "every set" claim is false for files the shape itself writes.

LAST STEP came after my last check had finished; I started no new runs after it. The review checkout l1-r1189 is clean at 8684af860, the mutant clone is clean, and none of my runs is still going.

Setup:

  • Scratch: /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1189 (logs in runs/, FUSE bench in fuse/, mutants.py), with the probe environment in penv.sh.
  • fuse/ holds the writer's cifuse.py, run from my own copy: a disk that ignores case and keeps it, in a private namespace. Below it is called "FUSE"; the disk that keeps case is "the bench disk".

What holds

  • Base against head, both changed test files:

    Run Base 4c7b8acd Head 8684af86
    CPython 3.12 16 failed, 81 passed, 2 skipped 97 passed, 2 skipped
    3.14-like shim the same 16 failed (shim loaded in 222 processes) 97 passed, 2 skipped (226 processes)

    The 16 are exactly the body's 12 throwaway-repos rows plus 4 make rows. The controls (a-twin-dropped, D/x beside d/y, twins in different repositories, no twins, and the unanswered twin) pass on both.

  • The skip-guarded row runs on FUSE. test_on_a_disk_that_ignores_case_check_names_the_twins_too passes at head and fails at base (assert [] == ['FINDING pla...adopt-lost.']`). tests/test_adopt_check_throwaway_repos.py: rows pin that refused_paths holds each case twin under its own name and blob, or names it, with core.ignorecase true or false, and that check names the twins git refuses and none it holds (#1033) #1158's FUSE row passes at both.

  • D/x beside d/y is rightly no finding. With the head tool on FUSE, check says plan ok and execute says "adoption verified" for D/x+d/y, D/x+d/y+d/z, D/E/x+d/e/y+d/E/z, and src/D/x+src/d/y in the code leg. Base's dirmerge run also verified.

  • The body's "before" table at base on FUSE matches, apart from one row (SHOULD 4):

    • A/a and the one-blob A/a: verified.
    • D/x/d/x: plan-file-reencoded, "will not add file alias 'D/x'".
    • F/x/f: adopt-lost: F/x.
    • The across-repositories scenario: adopt-lost for A and D/x.
    • The control: verified.
  • The grouping function (_case_twin_sets, called directly):

    • src/E+src/e/x, f+F/G/x, a/B+A/b/c and F+f/x+f/X are each grouped as one set.
    • D/x+d/y gives no set.
    • Known limit 1 holds (straße/STRASSE is a set) and so does known limit 2 (NFC beside NFD é gives []).
  • The every-disk ruling has precedent. Head refuses a kept Code/x or Spec against the legs' mounts as plan-leg-path-collides "once case is ignored", on the bench disk. The shape-pin twin row (test_adopt_shape_pin_and_work_dir.py:123-130) does the same. So the body's reason holds as policy; it is applied unevenly (MUST 2, SHOULD 5).

  • Body bookkeeping checks out:

MUST

1. The remediation's second option loses a twin after both legs are pushed, on the very disks the finding is about.

  • Where: adopt-project.py:9289-9291, in CASE_TWINS_REMEDIATION: "Or answer the twins of each set into different repositories ... and run check again."
  • Evidence: fuse/run.sh <head tool> head2 across on FUSE (runs/fuse-head2.txt):
    check exit 0
        plan ok
    execute exit 1
        FINDING adopt-lost: `A` (f70f10e4db19) is in no leg, not in the root tree, and not listed as drop
        FINDING adopt-lost: `D/x` (ad304eab5bbc) is in no leg, not in the root tree, and not listed as drop
       | NOTHING has been rolled back: `<fuse>/head2-across/remotes/Northwind-spec.git` and `...-code.git` exist, and both legs are pushed.
    
    The same plan on the bench disk gives check exit 0 and execute exit 0, "adoption verified".
  • Fix: drop that sentence, or confine it to execute run on a disk that keeps case and name follow-up (a). The better fix is to implement (a) here: where the work directory's git init writes core.ignorecase=true, name every twin set of the source commit, whatever its repositories or a drop.

2. False claim: the body's After paragraph says check "names, for each repository the split makes, every set of paths it would hold that differ only in case". The Behaviour-change bullet leans on it.

SHOULD

3. A dropped twin is lost too.

  • Where:
    • adopt-project.py:9272-9273 ("one the plan drops lands nowhere");
    • the control tests/test_adopt_check_throwaway_repos.py:602 (a-twin-dropped);
    • the body's Why and its follow-up (a), which names only twins in different repositories.
  • Evidence, dropped scenario (A kept in the root, a answered drop) on FUSE, at head and at base:
    check exit 0 / plan ok
    execute exit 1
        FINDING adopt-lost: `A` (f70f10e4db19) is in no leg, not in the root tree, and not listed as drop
    
    Both legs and the split were already pushed.
  • Fix: put the dropped twin into follow-up (a)'s text, and reword the comment.

4. The table row "the twins under src/, code leg | plan ok | exit 0, verified" is not what I measured. The code comment adopt-project.py:9263-9264 ("twins in one leg, were adopted") repeats it.

  • Evidence: base tool on FUSE, 3 of 3 runs (runs/fuse-base.txt, runs/fuse-base-leg.txt):
    execute exit 2
        REFUSED mounting the legs. THE EXACT COMMAND WAS:
        git --literal-pathspecs rm -r -q -- src
        exit 1
    error: the following files have local modifications:
        src/A
    NOTHING has been rolled back: ... both legs are pushed.
    
    The writer's 2 runs (w1161/probe/ci-r5/back/leg, runs/bench1-leg) did verify.
  • Fix: state both outcomes with their counts. This strengthens the case for refusing twins in a leg.

5. Sets whose paths hold one blob are refused on every disk though nothing is lost anywhere (known limit 4; my judgment, as the brief asked).

  • Evidence:
    • Base on FUSE, samebytes: execute exit 0, "adoption verified". Head refuses it as plan-case-twins.
    • Cloning onto FUSE a repository holding A and a (fuse/clone1.sh):
      • one blob: git warns "the following paths have collided", and git status --short is empty;
      • two blobs (control): M A.
  • Fix: leave exact-twin sets whose paths share one mode and one blob out of the finding, or make them a NOTE. Keep sets with different blobs and file-against-folder sets.

6. My mutants: 17 run, 12 killed, 5 survived. Each survivor was confirmed against both whole test files (97 passed, 2 skipped each). The file was restored after every mutant, and its blob equals HEAD's (runs/mutants*.txt).

  • The survivors:
    • m03: file-against-folder asked at the top level only;
    • m04: asked of the immediate parent only;
    • m01: casefold replaced by lower;
    • m11: the finding listed after every other entry finding, which changes execute's refusal code when another finding exists;
    • m15: len(twins) // 2 in place of len(sets).
  • Where: the TWINS_KEPT rows, tests/test_adopt_check_throwaway_repos.py:529-570. No row holds a nested or deep file-against-folder set.
  • Fix: add these rows:
    • src/E+src/e/x in the code leg;
    • f+F/G/x;
    • straße+STRASSE (pins known limit 1);
    • twins plus an uncovered path, asserting the order and execute's code;
    • one set of four paths.

NIT

7. The finding's sentence describes what execute did before this PR.

  • Where: adopt-project.py:9343-9346, "execute run there can stop before any leg exists ... or after both legs are pushed, as adopt-lost".
  • It is printed inside execute's own refusal, which says no leg was made, and execute now refuses first.
  • Fix: "...would stop there..., so execute refuses them on every disk".

8. Known limit 5's "(3 of 3 runs: plan-case-twins, then plan-file-reencoded)" belongs to folder twins, not to the row.

  • The row's source is A/a, and on FUSE it passes with one finding.
  • Under a global core.ignorecase=true, A/a gives plan-case-twins alone. D/x/d/x gives plan-case-twins then plan-file-reencoded.
  • Fix: attribute it to folder twins.

Not covered

  • I did not re-run the body's gate (g1-g11, test_shape_doctor in 4 shards, test_scaffold_leg_path_collisions); I read the writer's logs only.
  • I did not run the adopt test files the gate leaves out. My static scan found no source-path twins in them.
  • CI tests was still running at 03:01:51Z; Sonar, package and tests-git-bash passed.
  • I did not measure the _Unasked fallback, where refused=[] and twins of paths git refuses would be named too.
  • The FUSE disk stands in for macOS and Windows. Real APFS and NTFS were not measured.

Lane: openreposhape-1 (openRepoShape-1)

Brett Heap and others added 4 commits October 23, 2026 07:52
…ths hold one blob in one mode to a NOTE, its remediation no longer offers to answer the twins into different repositories, and its sentence says `execute` refuses them on every disk (#1161)

r1189's review of #1189, as lane 1 ruled:
- MUST 1: CASE_TWINS_REMEDIATION drops "Or answer the twins of each set
  into different repositories ... and run `check` again": on a disk that
  ignores case `execute` loses twins in different repositories after both
  legs are pushed (#1183). A comment says why the option is gone.
- MUST 2: the section comment says the finding asks each set of SOURCE
  paths, each sent by one answered entry, and not a kept path that is a
  twin of a file the shape writes into the root (#1191).
- SHOULD 3: the comment no longer says a dropped twin lands nowhere; twins
  in different repositories and a dropped twin are lost too (#1183).
- SHOULD 4: the comment gives both measured outcomes for twins in the code
  leg: adopted in 2 runs, stopped at `git rm -r` in 3.
- SHOULD 5: `_one_blob_twins` leaves a set whose paths fold to one name and
  hold one blob in one mode (not a submodule) out of the finding, and
  `check` names it in a NOTE, the first of `_entry_findings`' NOTEs. On a
  disk that ignores case `execute` verified such a set in 8 of 8 runs (2 in
  the root, 5 in the code leg, 1 in the spec leg). A file against a folder
  stays the finding.
- NIT 7: the finding says `execute` would stop there, before any leg or
  after both are pushed, so it refuses them on every disk.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…folder below the top level and two levels up, `ß` folded as `ss`, one set of four, `plan-case-twins` before `plan-uncovered` and as `execute`'s code, the whole remediation, and a set of one blob in one mode as a NOTE (#1161)

r1189's review of #1189:
- SHOULD 6, rows that kill its surviving mutants: `src/E` with `src/e/x`
  in the code leg (m03), `f` with `F/G/x` (m04), `src/straße` with
  `src/STRASSE`, which pins known limit 1 (m01), the twins beside a path
  no entry covers, asserting the order of the findings and `execute`'s
  refusal code (m11), and one set of four paths (m15).
- MUST 1: the `TWINS_KEPT` row asserts the whole remediation line, which
  offers no answer into different repositories.
- SHOULD 5: `A` and `a` with one blob are a NOTE, `check` exits 0; with two
  blobs they are the finding; three paths of one blob in the code leg are a
  NOTE; `F/x` and `f` of one blob stay the finding. `_case_twin_findings`
  asked itself: one blob in two modes, submodules at one commit and one of
  three paths in another blob are the finding; symlinks to one target are
  a NOTE; a repository can hold a set of each, and two NOTE sets are named
  set by set.
- SHOULD 3: the controls' docstring says twins in different repositories
  and a dropped twin are lost too on a disk that ignores case (#1183).
- NIT 7: `twins_kept` holds the finding's new sentence.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndings` lists the one-blob twins' NOTE before the pins' NOTEs, as its docstring says (#1161)

The order the docstring states had no row: moving `twin_notes` after the
other NOTEs passed every row before this one.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and `straße` of one blob are one name for the NOTE, folded with `casefold` as their set was (#1161)

f1189's mutant n15 (`lower` in place of `casefold` in `_one_blob_twins`)
passed every row before this one: no row held a one-blob set that only
full case folding makes one name.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap brettheap changed the title adopt-project.py: check names the case twins the plan keeps in one repository (A and a, D/x and d/x, f and F/x) as plan-case-twins on every disk, and execute refuses them before any leg (#1161) adopt-project.py: check names the case twins the plan keeps in one repository (A and a, D/x and d/x, f and F/x) as plan-case-twins on every disk, a set of one blob as a NOTE, and execute refuses the finding before any leg (#1161) Oct 23, 2026
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-23T12:09:42Z) Independent re-review r1189b of #1189 at 4d40ce9f: NOT READY, for one SHOULD. All eight of r1189's findings are fixed. The one-blob NOTE also covers root shapes that do not verify on a disk that ignores case.

Lane 1's ruling for the next round: in the root, the NOTE keeps only two paths that differ in their last part. Folder twins and sets of 3 or more stay the finding. The legs keep the NOTE. NITs 2 and 3 are taken. The review stopped at lane 1's STOP; the parts it did not cover are named at the end.

r1189b: re-review of PR #1189 (Closes #1161), head 4d40ce9f: NOT READY (for the parts covered)

I stopped at lane 1's STOP. All my runs are killed by TERM (PIDs 503221, 503225-503227, 510366), and nothing of mine is still running. No pytest call ever started: the gate pair was still waiting for a test slot, with 6 other pytest processes in the container. The review checkout l1-r1189 is clean at 4d40ce9fda, and both mutant clones are clean.

All eight of r1189's findings are fixed as claimed. One new gap holds the PR: the NOTE covers more than its evidence does.

SHOULD

1. The one-blob NOTE covers shapes that do not verify on a disk that ignores case, and the body says check "still says plan ok" for all of them.

  • Where:

    • adopt-project.py:9334-9342 (_one_blob_twins);
    • the comment at :9285-9290;
    • the body's After paragraph: "a NOTE names it, and check still says plan ok for it".
  • The gap: the evidence is 8 runs of 2-path file twins (A/a) only. The NOTE also takes folder twins (D/x/d/x) and sets of 3 or more paths. In the root, on the FUSE disk, those meet git's file-alias refusal instead.

  • Evidence: the head tool on my copy of r1189's FUSE bench, 1 run each (r1189b/runs/fuse-h1.txt):

    scenario (one blob) check execute
    samefolders: D/x, d/x in the root exit 1: FINDING plan-file-reencoded ("will not add file alias 'D/x' ('d/x' already exists in index)"), plus the NOTE exit 2, refused before any leg
    samemerge: the above plus D/y, d/z the same as samefolders the same as samefolders
    samethree: AB, Ab, ab in the root exit 0, NOTE, plan ok exit 2: REFUSED plan-file-reencoded ("will not add file alias 'Ab' ('ab' already exists in index)"), "no leg repository was created"
    samefolderleg, samemergeleg (code leg); samesymleg, samesymroot (symlinks to one target); sameexec (both 100755) NOTE, plan ok exit 0, adoption verified
  • What it means:

  • Not measured: whether a run gets past both rehearsals and stops after the legs are pushed. If one does, this becomes a MUST. Repeating the three root shapes was my next step when STOP came; r1189b/fuse/loop.sh is ready for it.

  • Fix, either of:

NIT

2. "execute would stop there" claims too much for file twins in the root.

  • Where: adopt-project.py:9389.
  • The body's own row 13 says A/a with their own blobs went "exit 0, verified" on FUSE.
  • Fix: "could stop" (r1189 proposed "would").

3. Body row 17 (D/x/d/x in the root) does not say "own blobs". With one blob, the "after" column is now the NOTE, plus plan-file-reencoded on FUSE.

  • Fix: add "own blobs".

r1189's findings: each fixed as claimed (checked statically)

Bookkeeping: holds

Not covered

  • Base against head of the new rows, on 3.12 and under the shim: not run. They were killed while still waiting for a test slot; the shim is built at r1189b/shim/sitecustomize.py.
  • Mutants: 26 are written and every anchor is checked unique and compiling (r1189b/mutants.py): r1189's 5 survivors, plus 21 of mine. None was run.
    • b17, which makes execute refuse the NOTE, may survive: none of the rows I read runs execute on a one-blob set. This is unmeasured.
  • The gate (g1, the 4 test_shape_doctor shards, test_scaffold_leg_path_collisions) and the make_reads_first file at head: not re-run.
  • Repeat FUSE runs of the root shapes (SHOULD 1), and the _Unasked fallback: not run.
  • Real APFS and NTFS were not measured.

Scratch: /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1189b (logs in runs/, bench in fuse/).

Lane: openreposhape-1 (openRepoShape-1)

Brett Heap and others added 2 commits October 23, 2026 12:16
…nly a set of two paths of one blob whose names differ only in their last part, `A` and `a`, and keeps folder twins and sets of three or more of one blob as the finding; its sentence says `execute` could stop there (#1161)

r1189b's review of #1189, as lane 1 ruled:
- SHOULD 1 (the preferred fix): `_one_blob_twins` now takes the
  repository a set is kept in. In the root it answers yes only for two
  paths whose names differ only in their last part, the shape measured
  to verify there (`A`/`a` of files, of symlinks to one target, of
  executables). On r1189b's FUSE disk, folder twins of one blob in the
  root (`D/x`, `d/x`) met git's `will not add file alias` in `check`, and
  three paths of one blob (`AB`, `Ab`, `ab`) in `execute` alone, after
  `check` said `plan ok`: both are the finding again, with its rename
  remediation. In the legs the NOTE is as it was, since those runs
  verified. The section comment, the module docstring's `check`
  paragraph, `_case_twin_findings`' and `_entry_findings`' docstrings say
  so.
- NIT 2: the finding says `execute` "could stop there", as file twins in
  the root verified on that disk.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…folder twins and three paths of one blob are `plan-case-twins`, two paths of one blob in one folder are the NOTE, and folder twins of one blob in the code leg stay the NOTE (#1161)

r1189b's SHOULD 1, as lane 1 ruled: `test_a_set_of_one_blob_is_a_note_and_not_the_finding`
gains folder-twins-of-one-blob-in-the-root and three-of-one-blob-in-the-root
(the finding), two-of-one-blob-in-one-folder-of-the-root and
folder-twins-of-one-blob-in-the-code-leg (the NOTE), and
`test_only_one_blob_in_one_mode_is_left_to_the_note` gains
two-of-one-blob-in-folders-in-another-case (`D/A`, `d/a`: the finding).
`twins_kept` says "could stop", as the finding now does (NIT 2).

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from fix/1033-ignorecase-case-twins-index to main October 23, 2026 15:40
Brett Heap and others added 2 commits October 23, 2026 18:03
… (restack step 1 of 3: #1158 landed as ef619c7)

Lane: openreposhape-1 (openRepoShape-1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stack step 2 of 3)

Its one file, tests/test_adopt_check_throwaway_repos.py, conflicted and takes this branch's side: the merge of #1158's final head 1e71152 already brought #1158's content, and this branch's net change to that file is #1189's own.

Lane: openreposhape-1 (openRepoShape-1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brett Heap and others added 2 commits October 23, 2026 18:08
… measured with the root NOTE's rule, 2 each: `A` and `a` and `D/A` and `D/a` verified, folder twins and three paths of one blob refused before any leg (#1161)

Lane: openreposhape-1 (openRepoShape-1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… of one blob in the spec leg are the NOTE, a file against a folder of one blob and one last name in the code leg is the finding, a leg set's third path in another mode is the finding, and `execute` takes sets of one blob past `check`'s findings (#1161)

Lane: openreposhape-1 (openRepoShape-1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 23, 2026
#1189 (fix/1161-check-names-case-twins) was restacked onto main by merges
and gained two commits of its own since 4d40ce9: `plan-case-twins` in the
root leaves to the NOTE only two paths of one blob whose names differ only
in their last part, and its rows. No conflict: git merged the comment above
`plan-case-twins`, where both sides changed a paragraph, by itself.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap brettheap changed the title adopt-project.py: check names the case twins the plan keeps in one repository (A and a, D/x and d/x, f and F/x) as plan-case-twins on every disk, a set of one blob as a NOTE, and execute refuses the finding before any leg (#1161) adopt-project.py: check names the case twins the plan keeps in one repository as plan-case-twins on every disk, a one-blob set as a NOTE in a leg and, in the root, two paths in one folder, and execute refuses the finding before any leg (#1161) Oct 23, 2026
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-23T22:00:54Z) r1189c, lane 1's independent re-checker (not the writer), re-checked #1189 at 58aa9d2b: READY, with no MUST, no SHOULD and two notes that do not block.

The full re-check

r1189c: re-check of PR #1189 at 58aa9d2bac414882cf4c536c613e80ba16bfce69

Verdict: READY

I found no MUST and no SHOULD. Every checked item holds, and one thing in the brief's wording is wrong (item 3). I did not start the predicted-merge gate; it is the one open question (new finding N1 and "Not covered").

No run of mine is live: pgrep finds none, and the m5old stream, which was still running when LAST STEP came, finished (mutrun.py printed done). My checkouts are clean:

  • Review checkout: /workspace/projects/openRepoShape-worktrees/l1-r1189c, detached at 58aa9d2b, git status --short empty.
  • Clones mut, mut2 and base: each has a tool blob 5fac9479 (HEAD's) and an empty git status.
  • Clone mrg: made but not used for any run.

Scratch folder: /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1189c. Its logs are in runs/.

1. r1189b's SHOULD and NITs: resolved

finding commit where at head evidence
SHOULD 1: the root NOTE took more than its evidence 44ae0a0e (tool) and a131fc25 (rows) adopt-project.py:9352-9367 (_one_blob_twins gains leg; the root rule is :9362-9364) and :9391 (the call passes leg). Comments are at :9284-9310, :9374-9381 and :9593-9596, and the check paragraph in the module docstring is at :116-122. Rows are at tests/test_adopt_check_throwaway_repos.py:686-699 and :790-792. probe and rows below
NIT 2: "would stop" claims too much 44ae0a0e adopt-project.py:9416 ("could stop there before any leg exists"); the test sentence is at tests/test_adopt_check_throwaway_repos.py:527 grep -rn "would stop there before any leg" finds nothing in the repo
NIT 3: body row for D/x/d/x lacks "own blobs" body, refreshed 21:30Z the table now has "D/x/d/x in the root, own blobs" and a separate "one blob" row gh pr view 1189

The body's After paragraph no longer says plan ok for every one-blob shape. It says the NOTE covers every such set in a leg and, in the root, only two paths whose names differ only in their last part.

2. The rule in the root

The code is adopt-project.py:9362-9364:

if leg == "root" and (len(named) != 2 or len({path.rpartition("/")[0] for path in named}) != 1): return False

The parent folders are compared as written, not folded, so D/A against d/a is not the NOTE.

Pinning rows. I ran 22 ids at head: 10 of test_a_set_of_one_blob_is_a_note_and_not_the_finding, 8 of test_only_one_blob_in_one_mode_is_left_to_the_note, 2 of test_every_path_of_a_leg_set_is_asked_its_mode, test_the_one_blob_note_is_the_first_of_the_entries_notes and test_execute_takes_sets_of_one_blob_past_checks_findings. Result (runs/rows-head.txt): 22 passed in 16.83s.

My probe. The script is probe_root_rule.py. It builds a one-blob source with the suite's own helpers (a_source, with_paths(own_blobs=False), planned) and runs check. The output is in runs/probe-root-rule.txt:

=== root A and a, one blob: exit 0
    NOTE the plan keeps 1 set of case twins in the assembly root whose paths hold one blob in one mode, 2 paths: `A` and `a`. ...
    plan ok
=== root D/x and d/x, one blob: exit 1
    FINDING plan-case-twins: the plan keeps 1 set of case twins in the assembly root, 2 paths, ... `D/x` and `d/x`. ...
=== root D/A and D/a, one blob (control: one folder): exit 0
    NOTE ... in the assembly root ... 2 paths: `D/A` and `D/a`.   plan ok
=== root AB, Ab, ab, one blob: exit 1
    FINDING plan-case-twins: ... in the assembly root, 3 paths, ... `AB`, `Ab` and `ab`.
=== root D/A and d/a, one blob: exit 1
    FINDING plan-case-twins: ... in the assembly root, 2 paths, ... `D/A` and `d/a`.
=== code leg src/D/x and src/d/x, one blob: exit 0
    NOTE ... in the code leg ... `src/D/x` and `src/d/x`.   plan ok

Root-rule mutants f01 to f10, run by me in clone mut2 (runs/mf.txt). Each anchor was unique, each compiled, and each blob was restored. All 10 were killed:

mutant killed by
f01 (root shape not asked) folder-twins-of-one-blob-in-the-root
f02 (two paths only) folder-twins-of-one-blob-in-the-root
f03 (one folder only) three-of-one-blob-in-the-root
f04 (every repository) one-blob-in-the-code-leg
f05 (top level only) two-of-one-blob-in-one-folder-of-the-root
f06 (folders folded) folder-twins-of-one-blob-in-the-root
f07 (last parts differ) two-of-one-blob-in-folders-in-another-case
f08 (three taken) three-of-one-blob-in-the-root
f09 (spec leg as root) folder-twins-of-one-blob-in-the-spec-leg
f10 (sentence "would") test_check_names_each_twin_git_refuses_and_each_set_it_holds

3. The new rows of 58aa9d2b: the brief's premise does not hold

The brief says the rows "fail on a131fc25's tool". They do not, because the tool's code is the same:

  • a131fc25:adopt-project.py is blob 4ce5f22c, the same blob as 44ae0a0e's.
  • git diff a131fc25 HEAD -- adopt-project.py has only # comment lines changed (22 of them, from e45e1225). There are no non-comment changes.

I ran the swap as asked (swap.py, runs/swap.txt). It put a131fc25's tool into the clone and ran the 5 new ids: 5 passed in 6.67s, then restored. The clone's tool blob was 5fac9479, equal to HEAD's, and git status --short was empty.

So these rows are not base-failing rows. They are mutant killers, and they were measured as that.

  • With the new rows (full file at head, my run runs/m5head.txt), all five mutants are killed by the row the body names:

    mutant killing row(s)
    b02 a-folder-and-a-file-of-one-blob-in-the-code-leg
    b18 a-folder-and-a-file-of-one-blob-in-the-code-leg
    b06 test_every_path_of_a_leg_set_is_asked_its_mode[executable] and [symlink]
    b17 test_execute_takes_sets_of_one_blob_past_checks_findings
    f09 folder-twins-of-one-blob-in-the-spec-leg
  • Without them (a131fc25's test file swapped in, runs/m5old.txt), all five survive: 86 passed, 2 skipped for each. So the rows are what kills them.

  • After every mutant, the tool and the test file blobs equalled HEAD's (restored ... True ×15 at head and ×5 under old tests).

  • Base against head. Tool 4c7b8acd with head's tests, whole file (runs/base-throwaway-full.txt): 38 failed, 53 passed, 2 skipped.

    • That is the body's "by addition, 38". The failures are 14 + 10 + 8 + 2 + 1 + 1 + 1 + 1.
    • Of the 5 new rows, 4 fail at base. test_execute_takes_sets_of_one_blob_past_checks_findings passes at base, as the body says.
    • The two control tests are not in the failed list.

4. The restack

5. The body's claims

  • Title: 249 characters by len() (cap 250), in the repo's style, ending (#1161).
  • Closing form: grep -niE '(clos|fix|resolv)...#N' finds one hit, line 1 Closes #1161. closingIssuesReferences is [1161]. No other close, fix, resolve or Covers form appears.
  • PR state: DRAFT, milestone v1.2.1, base main. The "Stacked on tests/test_adopt_check_throwaway_repos.py: rows pin that refused_paths holds each case twin under its own name and blob, or names it, with core.ignorecase true or false, and that check names the twins git refuses and none it holds (#1033) #1158 ... restacked" paragraph is correct.
  • Counts:
    • base 38 failed / head 91 passed + 2 skipped, as above;
    • 22 rows ids at head: all pass;
    • FUSE counts reconcile with f1189b/runs/fuse-f.txt. Rounds f1 and f2 are the 2 counted runs. In each, A/a and D/A/D/a gave check the NOTE and execute "adoption verified". D/x/d/x and AB/Ab/ab gave plan-case-twins and plan-file-reencoded in check, and execute exit 2 REFUSED plan-case-twins. Round f3 was killed and is not counted. I did not rerun FUSE.
  • Mutant tally 36 of 36: the body's own record reconciles: cM.txt has 31 KILLED and 5 SURVIVED, cM-f1189c.txt has 5 KILLED, and 36 restores read True. I re-ran 15 of the 36 myself (b02, b06, b17, b18, f01-f10) and all 15 were killed. I did not re-run the other 21 (m01, m03, m04, m11, m15 and b01, b03-b05, b07-b16, b19-b21).

6. The two files at head

  • tests/test_adopt_check_throwaway_repos.py: 91 passed, 2 skipped in 92.95s (runs/head-throwaway.txt).
  • tests/test_adopt_make_reads_first.py: 33 passed in 44.64s (runs/head-mrf.txt).
  • Both match the body (119 passed + 2 skipped together).

Extra run: the throwaway file under a 3.14-like sitecustomize shim at head (runs/head-shim.txt, shim in shim/sitecustomize.py) gave 91 passed, 2 skipped in 91.20s. is_file, exists and is_dir answer False to any OSError in it; I checked its __module__ is sitecustomize. The body says the shim run was made only at 4d40ce9f, so this closes that gap for head. These runs used the probe env. Because of the $(...) expansion on the rows run, I ran those ids with it; all 22 ran and passed.

New findings

N1 (NIT, a note for landing, not a PR defect): main has moved since the restack.

N2 (NIT, body prose): the Tests section's "10 rows" line. It speaks of "the 10 rows of ... then in it" against 8684af86. I did not measure that run and the arithmetic is as r1189b left it. No change needed, but no one has re-measured it.

Not covered

  • The predicted-merge gate (N1) was not run.
  • The first-round and prev/prevw runs, and the 21 mutants listed above, were not re-run. The FUSE disk was not re-measured, so real APFS and NTFS are not measured either.
  • Not run: test_scaffold_leg_path_collisions.py, the four test_shape_doctor.py shards and the doc trio (test_agents_md_codes, test_cli_reference, test_repo_hygiene). The body's counts for them were not re-measured.
  • The probe's rows run used one $(tr ...) expansion of an ids file instead of the line-by-line rule; the result was 22 of 22.

Lane: openreposhape-1 (openRepoShape-1)

Lane: openreposhape-1 (openRepoShape-1)

Brett Heap and others added 2 commits November 8, 2026 14:23
Merges main at 11560eb into
fix/1161-check-names-case-twins at 58aa9d2
(merge-base ef619c7, #1158's squash).

One conflict, tests/test_adopt_check_throwaway_repos.py, in the module
docstring: #1189 and #1169 each added a paragraph after #1033's. Both are
kept, #1161's first, because it opens "so `check` passed twins", which
follows from #1033's measurement, then #1169's "#1136 followed". Every row
and helper of both sides auto-merged untouched.

adopt-project.py auto-merged: both sides change `_entry_findings` (#1189 its
case twins after the source path findings; main the `GIT_ATTR_SOURCE` and
split-clone questions before `_paths_git_refuses`, and `_submodule_findings`'
`unasked`) and the `check` paragraph of the module docstring, in separate
lines.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-11-08T14:49:45Z) Lane 1's check of #1189 after its merge of main: READY carries to 7d2f651e, the new H0, once tests and SonarCloud are green there.

Lane: openreposhape-1 (openRepoShape-1)

Brett Heap and others added 2 commits November 8, 2026 16:47
Main at 1212295, which holds lane 4's #1105 squash and tests/test_adopt_collision_make_reads.py.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e out the trees that keep case twins, which check names and execute refuses as plan-case-twins (#1161)

`test_the_sentences_are_true_of_what_make_does_in_the_clone` (#1105) ran a
real adoption at `[Makefile+makefile]` and `[all-three]`, and
`test_where_the_file_make_reads_includes_makefile_the_merge_alone_is_enough`
(#1105) at `[makefile]`: sources that keep `Makefile` and `makefile` side by
side in the root, each with its own blob. Since #1161 `check` names that set
of case twins as `plan-case-twins` and `execute` refuses it before any leg,
so there is no clone to run `make` in: at the merge 47478d5 all three failed
at `adopt_with_legs_mounted_away`'s `check` (`FINDING plan-case-twins: ...
`Makefile` and `makefile``, `assert 1 == 0`).

Both parametrizes now leave out the rows `keeps_twins` (from
`test_adopt_make_reads_first`) names, as 7d2f651 did in
`test_adopt_next_make_reads_first.py`, and each 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 clone test runs 5
of CLONES, the include test its `[GNUmakefile]` row, and the file's other
functions, which call `_predict_collisions` and `follow_ups_for` and run no
`check`, all their rows, `[Makefile+makefile]` and `[all-three]` among them.

Measured on CPython 3.12, the whole file: at 47478d5 3 failed, 28 passed;
with this change 28 passed, the same 28 node ids.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Nov 8, 2026

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-11-08T17:15:31Z) Lane 1's check of #1189 after #1105 landed: main is merged in, and the agreed cut of #1105's case-twin rows is 472944b5. #1189's own change is unchanged, and READY carries to 472944b5.

Lane: openreposhape-1 (openRepoShape-1)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant