Repository navigation
tests/test_doctor_next_is_a_command.py: the comment-tail guard reads each string expression whole, across + and an f-string's pieces, and flags a # led by two or more blanks or a tab with or without a blank after it and a # that opens text after a part that is not a constant, so cmd #x, cmd + " " + "# x" and f"{cmd}{pad}# x" no longer reach a NEXT unseen (#679) - #698
Conversation
…each string expression whole, across `+` and an f-string's pieces, and flags a `#` led by two or more blanks or a tab with or without a blank after it and a `#` that opens text after a part that is not a constant, so `cmd #x`, `cmd + " " + "# x"` and `f"{cmd}{pad}# x"` no longer reach a NEXT unseen (#679)
The guard (#277) read every string constant alone and wanted a blank after
the `#`, so three spellings of the defect it guards against were not seen:
a `#` with no blank after it, which an interactive zsh pastes as an argument;
a tail split across `+`; and one that opens with the `#` straight after a
formatted value, which may be the blanks.
`tails_in` now reads each string expression whole (`pieces_of`: a `+` chain
and an f-string, with a part that is not a constant left as a gap) and
`tail_runs` finds the runs of constant text between gaps that hold a tail.
A run is the text `TAIL_ALLOWED` always named, so that table is unchanged.
A format spec's `#` flag (`f"{x:{w}#x}"`) is not a tail.
The sample block has one def for each spelling and one that must stay quiet;
the real file is asserted to yield exactly the allowed pair.
Test-only: shape-doctor.py is untouched, so no pinned copy changes.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR strengthens the test-only AST guard so it analyzes complete string expressions—including File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
|
(lane openRepoShape-3, 2026-10-16T13:54:30Z) Lane 3's independent review of #698 at
Lane: openreposhape-3 (openRepoShape-3) |
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 6 hours and 19 minutes by commenting |
main gained #628 (#600), #639 (#611), #621 (#602), #627 (#606, lane 2's README and handbook), #657 (#647), #656 (#649) and #698. README.md, adopt-project.py and every other file merged as text; docs/handbook.html conflicted at eight marker hunks and nowhere else: - the seven `Starting a project in a new organisation` markers, and the `The three legs` markers in the masthead, `ci` and `legs` that sit in the same hunks: this branch and #627 each moved them for their own README edits; - the one `Adopting an existing repository` marker. Every hunk took this branch's side, and then every marker was recomputed from the merged README.md with tests/test_repo_hygiene.py's `readme_segments` and `readme_fingerprint`: Starting (7) 45a3f886, The three legs (3) 1d9f5bf9, Adopting (1) a7e9e29f. The page's text holds both sides' edits: #627's handbook changes merged as text beside this branch's, so each moved marker reads a section whose page text was amended by the pull request that changed it. Caps: README.md is 1473 lines, its cap: 1422 + 2 (460) + 5 (550) + 12 (563) + 22 (564) + 10 (606). AGENTS.md is 494, its cap. test_repo_hygiene.py and test_readme_sections.py pass (154). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…KEEPS ITS NAME (which `--project` must be), ITS IDENTITY AND ITS FULL HISTORY, and the notes are current to main de92614, with seven more pull requests, lane 2's re-run-`plan` line, lanes 4's and 1's known limits, Developer Mode for a Git Bash symlink and the trusted publishers (#563, L4-47) - L4-47 (#457, lane 4 on #630 at 14:13:32Z): README.md:771-772 and docs/handbook.html:1276-1277 say "it KEEPS ITS NAME (which `--project` must be), ITS IDENTITY AND ITS FULL HISTORY, and becomes the assembly root". Two lines each, no line added: README.md is still 1473, its cap. The § Adopting marker moves to b23e9f98, text and marker; no other marker moves. - The notes, "What changed since v1.0.1": #628 (#600), with lane 2's line for a plan written before v1.1.0 (#473, 12:53:40Z) after it, and #621 (#602) under adopt-project.py; #657 (#647) under update-shape.py, bump-leg.py and a project's own scripts; #639 (#611) under scaffold, setup, family and the shared readers; #656 (#649), #698 (#679) and #627 (#606) under install, CI and the docs. Every pull request since bfed143 is cited (`git log --first-parent bfed143..de92614`, 74). - Known limits: lane openRepoShape-4's ten lines (#630 issuecomment-6217406337: L4-05, -06, -07, -08, -17, -43, -45, -54, -24, -156) and lane openRepoShape-1's twenty-six D9 lines (#457 issuecomment-6214768828), as two lists, one sentence each; and the Git Bash limit says that Windows lets a non-administrator make a symlink only in Developer Mode, as #596 does. - How this release is cut: the trusted publishers are filled from Brett Heap's 13:04:43Z ("the pypi are added") and 13:07:56Z ("they match what you listed"). - The counts (74 pull requests; 83 of 135 milestone issues closed at 17:16Z), the "current to main" sha and the open list (52) are at main de92614, still placeholders for tag time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…another spelling of its row's path moves it back to that path in the index alone, so the printed remedy no longer takes the member's gitlink away from its row (#646) (#670) **Goal (#646): no remedy `family.py` prints un-registers a member that is in the holder.** Three states are outside it, each under "Left out": a `.gitmodules` that a `git mv` made name the variant too (checklist candidate (e)); a file the index holds at or under the row's path, where #574's remedy is kept; and a file or link the index holds at `members` itself (candidate (h)). A member's only gitlink sat at `Members/<P>`, while its row and `.gitmodules` said `members/<P>`. This is #363's state after the fact. `family.py add` of ANOTHER member refused, as #574 rules (`family-members-dir-variant`). Its remedy was #574's remedy for any path under another spelling of `members/`: - `git rm -r --cached -- Members/<P>`, and commit the removal, where nothing is on disk in that spelling; - `git mv -- Members/<P> <new name>`, and commit the rename, where something is; - `git commit`, where HEAD alone holds it (the variant's removal staged, not committed). Each one, typed as printed, left no gitlink at the row's path. The holder's validator then refused `member-gitlink-absent`. Reproduced on BASE a524542 in all five layouts below. All in `scripts/family.py`. - `_members_spellings` takes the rows' own paths (`_rows_paths`: the one path `_member_path` gives a row, `members/<project>`; a row it refuses is `member-path`'s). - **Which path is the member's own.** The index must hold nothing at or under the row's path. Then a gitlink at a case variant of that path, in the index or in HEAD alone, is that member's. It gets a new remedy, `RESTORE`, placed in `REMEDY_ORDER` after the index's own remedies. - **The remedy** is the index-only move that `validate-family.py` prints for the same state since #639. Brett Heap accepted it on 2026-10-16T11:21:49Z ("Accept; merge #639 after #637"), in place of the "committed `git mv`" of his "Refuse on every disk" ruling, because `git mv` to the row's path fails or misplaces the gitlink: - `git rm -r --cached --ignore-unmatch -- <variants>`. It touches no file, and it is no error where HEAD alone holds the variant. Where a sparse checkout keeps a variant off disk (`skip-worktree`), the line says `git rm -r --cached --sparse --ignore-unmatch -- …`. - `git update-index --add --cacheinfo 160000,<commit>,members/<P>`, one line per row, at the commit the variant entry records. For an unmerged variant, that is the commit at stage 2. - A commit, where one is left to make. - **What the disk holds afterwards** gets the validator's own sentences, placed after every other remedy: - where nothing is checked out at the row's path, as in a fresh clone, `git submodule update --init --recursive`; - on a case-sensitive disk, `mv -- Members/<P> ../Members-<P>` for the directory left at the variant; - on macOS and Windows, leave it, since it is the row's own directory. - A directory on disk at a gitlink moved back from HEAD is the move's to name. It is not named a second time as untracked. The branch merges main 35133f2, which holds #639 (c6122e8), in one merge commit with no conflict. Example: the issue's state, the checkout at `members/IRRS`: ``` REFUSED family-members-dir-variant: the family root holds a path at or under another spelling of members/, which a case-insensitive disk takes for members/ itself — in the index: `Members/IRRS` (a submodule) Remediation: every member is mounted under members/, and a holder keeps that directory in one spelling on every disk. Move each member's gitlink held only under another spelling of its row's path, `Members/IRRS` for the row members/IRRS, to that path, at the commit it records, in the index alone, which touches no file (`git rm -r --cached --ignore-unmatch -- Members/IRRS`, then `git update-index --add --cacheinfo 160000,<commit>,members/IRRS`), and commit that; where nothing is checked out at members/IRRS, as in a fresh clone, `git submodule update --init --recursive` then checks the member out there; on a case-sensitive disk, as Linux's usually is, a directory left at Members/IRRS stays there, untracked, and add refuses it as family-members-dir-variant: move each one that is there out of the holder, `mv -- Members/IRRS ../Members-IRRS`; on macOS and Windows it is the row's own directory, so leave it; then re-run. ``` No pinned file changes: `scripts/family.py` is the standard's own tool, not a copy in a holder. No `--help` text changes, so `docs/cli.md` is untouched. A `git grep` of AGENTS.md, README.md, `docs/handbook.html` and the family-root templates found no sentence about `add`'s case-variant remedies, so none became false. 1. **The member's own gitlink: one whose path folds to a row's path, where the index holds nothing at or under that path.** The next commit records the index, and every remedy here ends in one. - If the index holds the row's gitlink, a gitlink at a variant is a second one. Taking it out removes nothing from the row, so it keeps #574's remedy, and moving it back would overwrite the row's gitlink. - If the index holds a FILE at the row's path, the move would replace it without a word. If it holds files UNDER the row's path, the move's second line fails (`appears as both a file and as a directory`) after the first has already taken the variant out. Both keep #574's remedy (lane 3's review RV670, N3). - A file at the variant, or anything under it, also keeps #574's remedy. - `test_only_the_members_own_gitlink_is_moved_back` holds all of these, 9 cases. 2. **The commit named is the one the variant entry records, so the remedy is a move, not a bump.** Brett Heap decided this at 2026-10-16T14:11:42Z: "take all recommendations for #670 and #681". That word replaces his 13:07:34Z take of "the row's pin". - Where a row has two variants, the first found is named, the index's before HEAD's. - **This departs from #646's Direction line (`<pin>`) and from #639's rule:** #639's restore in `validate-family.py` names the row's pin. The two tools give different lines where the variant records a commit the row does not pin, and, since S1, where a sparse checkout keeps the variant off disk: `add` prints `--sparse` and the validator does not (candidate (f)). - **The four reasons, as lane 3's review RV670 measured them:** 1. **A pure rename.** With the variant's commit, the move changes only the path. Where the commits differ, the gate then reports `member-gitlink-mismatch`, which has its own LOCKSTEP text and `bump` remedy, and exits 1, and an `add` of another member still goes through. `test_a_commit_the_row_does_not_pin_is_moved_not_bumped` types that end to end. (Where the variant equals the pin, the bytes are the same either way.) 2. **The pin would silently bump.** With the pin, the paste changes the commit the holder records: a roll-back or an advance, a bump done outside `bump`, which the LOCKSTEP text says the gate refuses. 3. **A row with no pin needs a fallback.** The validator never prints its remedy for such a row, because `member-pin` or `member-pin-commit` stops the row first. So #639's rule has no answer there, and a pin-based `add` would need the variant's commit anyway. 4. **A pin absent from the clone fails.** `update-index --cacheinfo` does not check that the object exists, and the printed `git submodule update --init --recursive` then fails with rc=128 (`Fetched in submodule path 'members/IRRS', but it did not contain 3333…`). The variant's commit is already recorded, so this remedy adds no new unreachable commit. - **The validator is not changed.** Naming the variant's commit there too (it already prints it, as `(at <commit>)`) would edit a pinned template, and that needs its own word, which nobody has given. 3. **An unmerged variant is moved back at its stage-2 commit** (RV670, N2). `ls-files -s` lists stage 1, the merge base, first, and the PR's first head took that one. - Stage 2 is HEAD's entry each time, so the move is a rename relative to HEAD, and the validator also answers an unmerged path from HEAD. In a merge and in a stash pop it is the holder's own side. In a rebase it is the upstream's side, which is HEAD while the rebase runs: after `git rebase --quit` the move names the upstream's commit, not the person's own, which is stage 3 (probed by rv670b, the re-verify of this PR). - The removal takes out every stage, and the commit after it goes through (`$Q/v112/w646b/probes/w646b-unmerged-probe.log`). - An unmerged variant with no gitlink at stage 2 is no member's own: HEAD's entry holds no gitlink there, whatever stages 1 and 3 hold. It keeps #574's remedy, where stage 2 is missing and where it is a file. A gitlink at stage 1, a file at stage 2 and a gitlink at stage 3 is an `OWN` state too (rv670b's NIT-3): git lists stage 1 first, and rv670b's mutant R1, which read the first record's mode in place of stage 2's, passed all 36 tests and moved the file's blob back as a gitlink. - The state is reachable only with unmerged entries and no in-progress mark. A conflicted `git stash pop` leaves it, and so do `git merge --quit` and `git rebase --quit` (rv670b probed both). `add` refuses a merge, rebase, cherry-pick, revert or `git am` in progress (`family-operation-in-progress`) before it gets here. 4. **A variant a sparse checkout keeps off disk is taken out with `--sparse`** (RV670, S1). Without it, `git rm -r --cached --ignore-unmatch` exits 0 and removes nothing, so the index then held two gitlinks, and the next `add` refused again. With it, the entry goes, measured on git 2.43 with and without `core.sparseCheckout` (`$Q/v112/w646b/probes/w646b-sparse-probe-*.log`). The flag is printed only where a variant needs it, so the line stays the validator's everywhere else. Git older than 2.34 has no `git rm --sparse`: see "Left out". 5. **The commit is printed only where one is left to make.** That is: - HEAD holds a variant (RV670's S2 state, HEAD holding the gitlink in both spellings at one commit, is one of these); - HEAD's gitlink at the row's path is another commit, or there is none; - or a pending change is committed first. A move to the variant that is only staged, with HEAD still holding the row's gitlink at that commit, leaves nothing to commit, and a printed `git commit` would fail as typed. 6. **The code stays `family-members-dir-variant`** (or `member-path-taken` where some path is also a variant of the new member's mount path). The add is still refused for this path, as for any path under another spelling of `members/`: with `core.ignorecase` set, git would stage the new gitlink as `Members/<new>` (#362). Only the remedy changes. 7. **The closing sentences come last**, after #574's disk remedies. - Where a `members/` renamed by hand is moved back (`mv -- Members members`), the checkout goes home with it. - If they came before that, the `mv` out of the holder would act on `Members/<P>` while it may still be the member's checkout. And `git submodule update` would make a `members/`, inside which `mv -- Members members` would then put `Members` (`members/Members`). - `test_with_the_other_remedies…[members/ renamed by hand]` holds the order: it types the move back and then `git submodule update`, which checks the member out in the `members/` moved back. 8. **One text on every disk.** The move reads the index and HEAD, never the disk. The dry run and the real run print the same two lines. Asked in process of an emulated folding disk and of a Linux one (`disk_as`, as `tests/test_family_members_case_variants.py` emulates it), each layout gives the same text the subprocess gives. 9. **The disk after the move.** - On a case-sensitive disk the variant keeps either a checkout (15 entries, `?? Members/`) or, after a fresh clone, an empty directory. The gate passes either way. - If the printed `mv` is not typed, the next `add` refuses `family-members-dir-variant ... on disk and untracked: Members/Atlas` with #574's `mv -- Members/Atlas <new name>`, and passes once that is typed. - If the printed `mv` is typed, the next `add` passes at once (`$Q/v110/w646/leftover.log`, `repro-head.log`). All runs used `$Q/v110/run9.sh`: a `gh` stub that exits 97 first on PATH, `GH_TOKEN=bogus-l5`, an empty `GH_CONFIG_DIR`, `/usr/bin/python3 -m pytest`, and local bare remotes only. `tests/test_family_add_member_variant.py` is new, with 37 tests. Every printed command is typed as printed. The holder's gate then passes (or, in the one case where the variant records a commit the row does not pin, names that drift), the member is where its row says at its commit, and the same `add` goes through. - `test_each_layout_moves_the_gitlink_back_as_printed` (4 layouts) and `test_a_fresh_clone_moves_the_gitlink_back_as_printed`. - `test_every_disk_gives_the_same_answer` (4 layouts × folding and Linux, in process). - `test_with_the_other_remedies_each_runs_in_its_order` (8 states): - beside a file under `Members/`; - after a pending change in HEAD; - with nothing left to commit; - `members/` renamed by hand; - HEAD alone, with the checkout at the variant; - HEAD holding both spellings at one commit, the row's removal staged (S2); - kept off disk by a sparse checkout (S1; skipped on git older than 2.34, as #574's sparse case is); - unmerged at stages 1, 2 and 3, with nothing in progress (N2). - `test_every_member_held_in_the_wrong_case_is_moved_back` (two rows). - `test_the_gitlink_is_recorded_at_the_commit_it_records` (4 states). - `test_a_commit_the_row_does_not_pin_is_moved_not_bumped` (S3): typed as printed, the gate exits 1 with `member-gitlink-mismatch` alone, and the same `add` goes through. - `test_only_the_members_own_gitlink_is_moved_back` (9 states, four of them new: a file at the row's path, a file under it, an unmerged variant with no gitlink at stage 2, and one with a gitlink at stage 1, a file at stage 2 and a gitlink at stage 3). - `test_the_rows_that_have_a_path_of_their_own`. Layouts that need two directories differing only in case skip on a disk that folds case. No test makes such directories there. Two existing lines change to the new shapes: - `Spelled` has two more fields, `recorded` and `sparse`, so a seven-field tuple compared in `tests/test_family_members_case_variants.py` gains `"", False`; - the three-argument stand-in for `_members_spellings` in `tests/test_family_add_refusals.py` takes `rows`. The S1 case sits in the new file, beside its other move-back states, and not in `test_family_members_case_variants.py` beside #574's sparse test: it needs the new file's helpers, and that file already imports from the other one. Results on this head. a837a02 is 2104a3d plus one test commit: it adds one state to `OWN` and changes no code, so the runs of 2104a3d below stand for every file it does not touch. | Run | Head | Result | |---|---|---| | the new file | a837a02 | `37 passed in 48.55s` | | `tests/test_family_members_case_variants.py` | a837a02 | `60 passed in 53.83s` | | the new file, with `scripts/family.py` put back to the merge's (bc48c2a) | 2104a3d's tests | `5 failed, 31 passed in 170.09s`: the sparse and unmerged states typed, and the OWN cases of a file at the row's path, a file under it, and an unmerged variant with no gitlink at stage 2 | | the new file, `test_family_members_case_variants.py`, `test_family_add_refusals.py` | 2104a3d | `161 passed in 809.30s` | | `test_family.py`, `test_family_member_path.py`, `test_validate_family_unnamed_mounts.py`, `test_readme_windows_quoting_sentence.py`, `test_repo_hygiene.py` | 2104a3d | `269 passed in 803.63s` | The S2 and S3 cases pass on the merge too, since they pin behaviour the PR already had. Lane 3's mutant X2 fails the S2 case (below). The S3 case holds the variant's-commit rule end to end. Earlier, at the PR's first heads (before the merge with main): | Run | Head | Result | |---|---|---| | the new file, on BASE a524542 | a524542 | `29 failed in 48.75s` | | every `tests/test_family*.py`, 21 files | 2216e1c | `851 passed, 19 skipped in 1248.90s` | | `tests/test_validate_family*.py` (5), `test_refusal_default_remediation.py`, `test_repo_hygiene.py`, `test_siblings_family_name.py`, `test_template_text_placeholders.py`, `test_clone_url_bytes.py`, `test_update_shape_header_comments.py`, `test_update_shape_pr_base.py`, `test_shape_doctor.py` | 2216e1c | `1014 passed, 3 skipped in 1323.30s` | Holder `Fam` with members Atlas and Nova, Atlas's gitlink moved to `Members/Atlas` in the index at its own commit and committed; then `family.py add` of Zed (`$Q/v110/w646/probes/repro.sh`), at the PR's first head. | Layout | BASE a524542 (`repro-base.log`) | head 79754b9 (`repro-head.log`) | |---|---|---| | L1, nothing on disk in that spelling | `git rm -r --cached -- Members/Atlas`, commit: no gitlink for Atlas; gate `member-gitlink-absent` | rm, `update-index`, commit: Atlas at `members/Atlas` at its commit; status clean; gate exit 0 in all three modes; next `add --dry-run` exit 0 | | L2, the only checkout at `Members/Atlas` | `git mv -- Members/Atlas renamed-1`, commit: gitlink at `renamed-1`; gate `member-gitlink-absent` | rm, `update-index`, commit, `git submodule update --init --recursive`, `mv` out: the same end state | | L3, both directories | `git mv`, commit: gitlink at `renamed-1`; `?? members/Atlas/`; gate `member-gitlink-absent` | rm, `update-index`, commit, `mv` out: the same end state | | L4, a fresh recursive clone (an empty `Members/Atlas`) | `git mv`, commit: gitlink at `renamed-1`; gate `member-gitlink-absent` | rm, `update-index`, commit, `git submodule update`, `mv` out: the same end state | | L5, the variant's removal staged, not committed | `git commit`: the removal committed; gate `member-gitlink-absent` | rm (`--ignore-unmatch`, no error), `update-index`, commit: the same end state | In every layout, on both sides, the dry run and the real run printed the same stderr, and the in-process run under a folding disk and under a Linux disk printed the same two lines as the subprocess. This round's states (S1, S2, S3, N2, N3) are reproduced by the tests above, which type each printed line. Not reproduced: a real macOS or Windows disk. The folding disk is emulated. CI runs Linux only (#597). **This round** (`$Q/v112/w646b/probes/w646b-mutate.py`, in throwaway clones at 2104a3d). Each probe changed one hunk or condition, and the new file was then run with `-x`. Every hunk of this round fails a test, and so do lane 3's X1 and X2 and rv670b's R1, which needed a new state (last row). | Probe | What it undoes | Result | First failing test | |---|---|---|---| | M1 | a variant never marked `sparse` | 1 failed, 19 passed | `test_with_the_other_remedies…[kept off disk by a sparse checkout]` | | M2 | `--sparse` never printed | 1 failed, 19 passed | the same | | M3 | back to "no GITLINK at the row's path" (the first heads' rule) | 1 failed, 31 passed | `test_only_the_members_own_gitlink…[a file at the row's path]` | | M4 = lane 3's X1 | "no entry AT the row's path", under it not asked | 1 failed, 32 passed | `…[a file under the row's path]` | | M5 | the first record git lists (stage 1), as the first heads had it | 1 failed, 20 passed | `test_with_the_other_remedies…[unmerged, with nothing in progress]` | | M6 | stage 0 alone: an unmerged variant is never the member's own | 1 failed, 20 passed | the same | | M7 | the last record (stage 3) | 1 failed, 20 passed | the same | | lane 3's X2 | `name in in_head or` deleted | 1 failed, 18 passed | `test_with_the_other_remedies…[HEAD holding both, the row's removal staged]` | | lane 3's X3 | `'posix'` dropped from the move back's three quotings | 36 passed (survives) | none on Linux: `shell_quoted` quotes for `os.name`, POSIX here (see "PowerShell" under "Left out") | | rv670b's R1 | `ours[0] == b"160000"` replaced by the mode of the first record git lists (`mode == 0o160000`), in place of stage 2's | 36 passed (survived) at 2104a3d. At a837a02: `1 failed, 34 passed in 44.07s` (`-x`), and `1 failed, 8 passed, 28 deselected` for `-k test_only_the_members_own_gitlink` | `test_only_the_members_own_gitlink…[unmerged, with a file at stage 2 and gitlinks at 1 and 3]` | The S3 test types the remedy end to end and asserts the gate's finding, so it holds the variant's-commit rule itself; no mutant of this round touches that rule. **The first heads** (`$Q/v110/w646/probes/mutate.py`, in a throwaway clone at 2216e1c). The new file was run with `-x`. | Probe | What it undoes | Result at 2216e1c | First failing test | |---|---|---|---| | H1 | `RESTORE` in `REMEDY_ORDER` | 1 failed | `test_each_layout…[nothing on disk in that spelling]` | | H2 | no member's own gitlink found | 1 failed | the same | | H3 | a gitlink at the row's path not asked | 1 failed, 22 passed | `test_only_the_members_own_gitlink…[a second gitlink beside the row's]` | | H4 | any index entry, not a gitlink alone | 1 failed, 23 passed | `…[a file at the variant]` | | H5 | the index's branch | 1 failed | `test_each_layout…[nothing on disk in that spelling]` | | H6 | the commit always printed | 1 failed, 15 passed | `test_with_the_other_remedies…[with nothing left to commit]` | | H7 | the commit printed only where HEAD holds the variant | 28 passed (survived) | killed at 79754b9: `test_the_gitlink_is_recorded…[staged at another commit, HEAD keeping the row's]` | | H8 | the commit read from HEAD's gitlink at the row's path first | 28 passed (survived) | killed at 79754b9, by the same case | | H9 | HEAD's branch | 1 failed, 3 passed | `test_each_layout…[the removal staged, not committed]` | | H10 | HEAD's file taken for a gitlink | 1 failed, 25 passed | `test_only_the_members_own_gitlink…[a file at the variant in HEAD alone]` | | H11 | the disk named again at a gitlink moved back from HEAD | 1 failed, 17 passed | `test_with_the_other_remedies…[HEAD alone, the checkout at the variant]` | | H12 | the commit not printed after a pending change | 1 failed, 14 passed | `…[after a pending change in HEAD]` | | H13 | no `--ignore-unmatch` | 1 failed | `test_each_layout…[nothing on disk in that spelling]` | | H14 | the last variant found names the commit | 1 failed, 20 passed | `test_the_gitlink_is_recorded…[two variants in the index]` | | H15 | the closing sentences before the disk's remedies | 1 failed, 16 passed | `test_with_the_other_remedies…[members/ renamed by hand]` | | H16 | the closing sentences dropped | 1 failed | `test_each_layout…[nothing on disk in that spelling]` | | H17 | `add` passes no rows | 1 failed | the same | | H18 | `_checked_mount_path` drops the rows | 1 failed | the same | | H19 | `_rows_paths` takes any row's path | 1 failed, 27 passed | `test_the_rows_that_have_a_path_of_their_own` | | H20 | `_rows_paths` does not check the project's type | 1 failed, 27 passed | the same | | H21 | the move's part dropped from the text | 1 failed | `test_each_layout…[nothing on disk in that spelling]` | The branch merges main 35133f2 in bc48c2a, with no conflict: its tree, 088aa26, is the one `merge-tree` predicted. Only #637, #622 and #639 had changed `scripts/family.py` since BASE. `git merge-tree --write-tree` of the head a837a02, last after a fetch at about 18:45Z. Every one is clean. - origin/main 2fac979 (tree 042d309). Since the last check (main de92614) it has landed #634 (30676c9), #659/#671 (6e37d0e), #658/#699 (b618ec1), #650/#719 (3dc34b8), #678/#701 (5c392dd) and #425/#717 (2fac979). The other rows are the heads as fetched now. - Lane 4's five branches that change `scripts/family.py`: `fix/684-family-holder-states` 7015386, `fix/686-bump-reachability` 7567e60, `fix/688-family-init-values` aa95b6b, `fix/689-next-push-refspec` fa51384 (it also changes `tests/test_family_add_refusals.py`) and `fix/693-local-remote-dir` 96ebe45. - Lane 4's other open heads: `fix/674-update-shape-apply` d069902, `fix/690-siblings-inputs` 38a40ab, #698 b069951, #700 fca40df, #702 48bac6e, #703 (`fix/696-setup-family-folder`) 4054ffd, #705 1bc63e8 and #706 (`fix/664-surviving-mutants`) 3e408b1. - Lane 2's #671 and #699 and lane 4's #701 are on main now, so the main row covers them. - Lane 5's #682 (#648, 8b39cd1), `fix/692-raw-folder-paths` ab55c29 (it changes `scripts/family.py` too) and `fix/704-bootstrap-leg-moves` 4e2b211. - **A `.gitmodules` that names the variant too** (a `git mv members/<P> Members/<P>`, committed or staged; checklist candidate (e) below). The move puts the gitlink back at the row's path, but the section still gives `Members/<P>`. So `git submodule update` fails with `No url found for submodule path 'members/Atlas'`, and the gate refuses `member-uninitialized` (`$Q/v110/w646/gitmv-head.log`). On BASE the same remedy moved the gitlink and the section to `renamed-1` (`gitmv-base.log`). #639's validator prints the same incomplete lines for this state. A fix needs a `.gitmodules` edit that the validator does not print. - **A file the index holds at or under the row's path, beside the variant's gitlink** (decision 1). #574's remedy is kept: it takes the variant out, and the file stays at the row's path. For a file at the row's path, typed, it leaves HEAD with `100644 blob … members/IRRS`, the gate refuses `member-gitlink-absent` (exit 2), and `add` of another member then goes through (exit 0), as rv670b ran it; a file under the row's path was not run. The move would replace that file, or stop half done, so no remedy here can both keep the file and put the member back. This state predates this PR. - **A file or link the index holds at `members` itself** (a file `members`, or a committed link `members -> Members` left after a hand rename; checklist candidate (h) below). The rule asks about entries at or under a row's path, not above it. With every member at a variant and `members` a file in the index, `add` prints the move back, and rv670b typed it line by line: `git rm` takes out both variants (exit 0), then each `git update-index --add --cacheinfo` fails with exit 128, `'members/IRRS' appears as both a file and as a directory`. The next `add` prints the same move again, now from HEAD. A committed link behaves the same. #574's remedy also un-registered the member here, so it is not a regression. Adding `or row.startswith(name + "/")` to the rule would keep #574's remedy there, which leaves the member with no gitlink, as the entry above does. `scripts/family.py` is not changed for it here. - **Git older than 2.34 and `--sparse`.** `git rm` has no `--sparse` option before 2.34 (git v2.33.0's `builtin/rm.c`), and it skips every `skip-worktree` entry (`if (ce_skip_worktree(ce)) continue;`), so there the line without the flag was the same silent no-op. The printed `--sparse` stops with an unknown-option usage error, exit 129 (from reading that source; not run). #574's `git mv --sparse` has the same limit. The S1 test is skipped on git older than 2.34, as #574's sparse test is. - **PowerShell.** The printed lines are quoted for a POSIX shell (#532), so paste them into Git Bash. RV670 notes (N4) that in PowerShell the bare `160000,<commit>,members/IRRS` becomes an array passed as three arguments, which `--cacheinfo` also accepts. Not run, by RV670 or here. On Linux, dropping `'posix'` from these lines changes nothing (lane 3's mutant X3), since `shell_quoted` quotes for `os.name`, which is POSIX there. - **The closing `mv` inherits #639's N-1 and N-2** (rv611c), and its N-3: - N-1: where the variant directory is a live checkout with an absorbed gitdir, the `mv` leaves its `.git` file pointing nowhere; - N-2: the `mv`'s condition is stated by OS, not by a check a person can run, so on a case-folding Linux mount (WSL's `/mnt/c`) typing it moves the member's own checkout out, and `git submodule update --init --recursive` then puts it back; - N-3: an empty `../<variant>` directory that already exists is moved into. - **A variant under `members/` itself.** `members/irrs` for the row `members/IRRS` is invisible to `add`'s `_spelled_target`, while the validator's casefold check names it (RV670, N6). This predates this PR. - **Case folding beyond ASCII**, as in #574: project names are ASCII, and git's `icase` folds ASCII only. - **(e) `family.py add` and `validate-family.py`: the move back of a member's gitlink leaves `.gitmodules` naming the variant after a `git mv`.** After `git mv members/<P> Members/<P>`, git rewrites the section's `path` to `Members/<P>`. The index-only move both tools print puts the gitlink back at `members/<P>` and leaves the section where it is. `git submodule update --init` then fails with `No url found`, and the conditional `mv` would move the member's only checkout out of the holder. Both could also print `git config -f .gitmodules submodule.<name>.path members/<P>` and `git add .gitmodules` where a section gives a variant's path (evidence `$Q/v110/w646/gitmv-head.log`). - **(f) `validate-family.py`: its move back of a member's gitlink is a silent no-op where a sparse checkout keeps the variant off disk.** Its `git rm -r --cached --ignore-unmatch -- <variant>` has no `--sparse`, so it exits 0 and removes nothing, and its `git update-index --add` then leaves two gitlinks in the index. This is the validator's half of RV670's S1, which RV670 assigns to #459. The validator is a pinned template. - **(g) `validate-family.py` and `family.py add`: a file the index holds at or under a row's path, beside the member's gitlink at a variant, has no remedy that keeps the member.** `add` now keeps #574's remedy there, which takes the variant out. The validator asks `recorded_gitlink`, which skips any entry that is not mode 160000, so by reading its code (not run) it prints its move back for this state too: `update-index --add` would replace a file at the row's path without a word, and fail on files under it after its first line has taken the variant out. This state predates #670. - **(h) `family.py add` (and probably the validator, not run): a file or link the index holds at `members` itself is never named.** The move printed beside it stops at its `update-index` line after `git rm` has taken every variant out (above, under "Left out"). Nothing here fixes it; the coordinator places it. Review: lane 3's RV670 found 79754b9 READY with three SHOULDs; the fix round took S1-S3, N2 and N3, rv670b's re-review found it ready with nits, and a837a02 takes Brett Heap's ruling of 2026-10-16T14:11:42Z that the restore keeps the variant's own commit. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 a837a02 passed tests, tests-git-bash and SonarCloud; the head 6d7e4ff is a clean update-branch merge of main bf1f099, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree b46e560 over main 6794a9f: the family_add_member_variant, family_add_refusals, family_members_case_variants, repo_hygiene, agents_md_codes, cli_reference sets, 308 passed. Landed on Brett Heap's word of 2026-10-17T10:59:39Z, relayed by lane 3 on #473: "merge lane 5's approved v1.1.0 drafts". Closes #646 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…IR` is respelled or refused before install.sh's fetch, and the PATH note quotes the directory it prints and refuses one holding `:` or a newline (#667) (#726) #649's two leftovers (lane 5's checklist lines on #461). One commit, **a3cde999**, on main 35133f2 (#656 is in it). It changes `install.sh`, `openRepoShape` and the two suites `tests-git-bash` runs, `tests/test_install_sh.py` and `tests/test_openreposhape_command.py`, and nothing else. `git show --numstat`: `install.sh` +32/-4, `openRepoShape` +57/-1, `test_install_sh.py` +155/-2, `test_openreposhape_command.py` +216/-13. README, the handbook and `docs/cli.md` are unchanged: every sentence about `--install`, the PATH note and `$OPENREPOSHAPE_BIN_DIR` stays true (`export PATH=…` is still what they say). **1. install.sh applies #656's rule where it reads the variable** (`main`, before the symlink check and the fetch). - Before: `target="$bin_dir/openRepoShape"` was built from the raw value. With `D:\tools\bin`, the `preflight:` line and the four `done:` lines (and `install:` for an old release) said `D:\tools\bin/openRepoShape`, while the command's own lines said `/d/tools/bin/openRepoShape`. With no `cygpath`, the command's `--install` refused (exit 2) after install.sh had fetched it, and install.sh said only "the <version> openRepoShape's --install refused (exit 2)". - Now: a value that starts `[A-Za-z]:/` or `[A-Za-z]:\` is spelled with `cygpath -u --`, so every line names one directory. Where no `cygpath` answers, fails, or answers a drive spelling again, install.sh refuses with exit 2 before the symlink check and before any request. The `case` is a copy of `install_commands`'s, because install.sh is downloaded alone. - The refusal does not echo the value. A value that holds a newline would forge a line. **2. The PATH note quotes the directory it prints** (`export_path_line`, new, in `openRepoShape`). - Before: `export PATH="$dir:$PATH"`, the directory as it was between double quotes. For `a"b$HOME c` it printed `export PATH="…/a"b$HOME c:$PATH"`, which `eval` rejects (`unexpected EOF while looking for matching `"``). For ``a`echo X` `` it printed a line that RUNS `echo X` when pasted, and for `a$b` one that drops `$b`. - Now, for a directory holding a `"`, `$`, backquote, `\` or `!`, the line is `export PATH='<dir>':"$PATH"`, with `'\''` for an apostrophe. Every other directory keeps today's line byte for byte, a space or an apostrophe included (both are safe in double quotes). - `!` is in the set because an interactive bash reads it inside double quotes as history: `echo "a!b"` answers `bash: !b: event not found` at a prompt, and a person pastes this line at a prompt. A profile file never does. - Where it is called: `say " $(export_path_line "$dir")"`, in place of the old inline line. **3. `--install` refuses a directory holding `:` or a newline** (`install_commands`, beside #656's early checks), before anything is placed, with exit 2 and the remedy. - A `:` splits a `$PATH` whatever the quoting, and a newline breaks the printed line in two, so no line about `$PATH` can name them. - The newline check runs first, before the Windows spelling, and its message does not name the value, so no later refusal can echo a forged line. The colon check runs after the Windows spelling: a drive spelling holds a `:` by definition, is respelled without one, and refuses what it cannot respell. A `cygpath` answer that holds a `:` is refused there too. - The directory may come from `$HOME/.local/bin`; the colon refusal names it. - **install.sh respells and refuses only where respelling is impossible** (the issue offered "respell, or refuse"). It is #656's decision for the command, and install.sh must not answer one value differently: a test feeds each value through both (below). - **install.sh gets the drive-letter rule only.** The brief and the issue's first line name that rule. The `:` and newline refusals are the command's (issue line 2). Where install.sh is handed such a value, the command refuses after the fetch, as it always did. Named below as a checklist candidate. - **The line's shape is `export PATH='<dir>':"$PATH"`.** The directory is the only quoted part. The probe `a"b$HOME c` now prints `export PATH='…/a"b$HOME c':"$PATH"`, and `eval` of it sets `$PATH` to exactly the directory plus the old `$PATH`. - **Any backslash is quoted,** though `a\b` survives double quotes (`\b` is not special there). The issue lists `\`, and one rule is easier to hold than "a backslash before one of four characters". - **The apostrophes are walked, not replaced with `${dir//…}`.** How a pattern substitution reads a quote in its replacement has changed between bash releases, this runs on 3.2, and `sed` is not one of the programs `--install` uses (`INSTALL_NEEDS`). I could not run bash 3.2 here (5.2.21 only); the loop uses `${x%%"$q"*}` and `${x#*"$q"}`, which are long-standing 3.2 forms. - **`BIN_DIR_SPELLINGS` gained `lower-case`** (`d:\tools\bin`). #656's two tests now run over it too (+5 cases, 15 in all, all passing on both the base and the head), so `[A-Za-z]` is pinned in the command as well as in the copy. New: 56 cases. In `tests/test_openreposhape_command.py` (39 with #656's two extended): the PATH note's lines for 12 names, `export_path_line` alone for the same 12 (so a `"` and a `\` are held on Windows, where no file can be named with one), 3 through `bash -i`, 5 refusals (a colon, two directories, a drive-relative name, a newline, a drive spelling holding a newline), the `$HOME` default, a `cygpath` answer holding a colon, and +5 from the `lower-case` spelling. In `tests/test_install_sh.py` (17): one value through install.sh and the command, in 3 spellings × 4 `cygpath` states (12; where it spells, both place the command at the same directory and name no other; where it does not, both refuse with exit 2 and install.sh made no request); every `preflight:`, `install:` and `done:` line with the directory respelled, over the four endings (4); and the symlink check seeing the respelled directory (1). - **On the base 35133f2** (the head's two test files over the base's code): `47 failed, 19 passed` in the new selection. The 19 are #656's own cases and the plain, space and apostrophe names, which keep today's line. - **On the head a3cde99, dirty=0:** - `tests/test_openreposhape_command.py tests/test_install_sh.py tests/test_install_sh_filter_repo.py tests/test_openreposhape_package.py`: **276 passed** in 589.67s (the machine's load average was 115). - `tests/test_repo_hygiene.py tests/test_workflow_concurrency.py tests/test_release_workflow.py tests/test_readme_sections.py tests/test_cli_reference.py tests/test_setup_by_hand_commands.py tests/test_readme_windows_quoting_sentence.py tests/test_gh_guard.py`: **250 passed** in 175.17s. - **The three files `tests-git-bash` runs, with Windows' `write_text` emulated** (a plugin makes every `Path.write_text` write CRLF): **218 passed** in 326.60s. A probe test takes the plugin's effect: 1 passed with it, 1 failed without. - `actionlint`: not run, no workflow is touched (`tests.yml` is as on main). Each row reverts one arm, refusal or hunk in a throwaway clone and runs the new cases and #656's (66 selected). The first column is how many failed. | Mutation | Failed | Where | |---|---|---| | install.sh: the whole drive block off | 17 | parity (12), line cases (4), symlink | | install.sh: no backslash arm | 13 | parity `backslash` and `lower-case`, line cases, symlink | | install.sh: no slash arm | 4 | parity `slash` | | install.sh: `[A-Z]` for `[A-Za-z]` | 4 | parity `lower-case` | | install.sh: no `cygpath` call | 8 | parity `spells` (3), line cases, symlink | | install.sh: no refusal | 9 | parity, every state but `spells` | | install.sh: no drive-answer refusal | 3 | parity `again` | | install.sh: no `|| spelled=""` guard | 3 | parity `fails` (set -e exits silently) | | install.sh: `cygpath -u` without `--` | 8 | parity `spells`, line cases, symlink | | install.sh: the block moved after the symlink check | 8 | line cases, symlink, parity `spells` | | install.sh: the block moved after the release lookup | 17 | parity (12: a request was made, or the target was named raw), line cases (4), symlink | | install.sh: the refusal echoes the value | 9 | parity refusals (typed value in stderr) | | command: no newline refusal | 2 | `a-newline`, `a-drive-spelling-and-a-newline` | | command: newline checked after the drive arm | 1 | `a-drive-spelling-and-a-newline` (the value would be echoed) | | command: no colon refusal | 5 | colon (3), `$HOME` default, respelled answer | | command: colon checked before the drive arm | 31 | #656's cases, parity, respelled answer | | command: `"` not quoted | 2 | `a-double-quote`, both tests | | command: `$` not quoted | 5 | `a-dollar`, `an-apostrophe-and-a-dollar`, both tests, `bash -i` | | command: backquote not quoted | 2 | `a-backquote`, both tests | | command: `\` not quoted | 4 | `a-backslash`, `a-trailing-backslash`, both tests | | command: `!` not quoted | 5 | `a-bang`, `two-apostrophes-and-a-bang`, both tests, `bash -i` | | command: apostrophes not walked | 4 | the two apostrophe-with-special names, both tests | | command: an apostrophe alone forces single quotes | 2 | `an-apostrophe`, both tests (today's line kept) | | command: always single quotes | 9 | the plain, space and apostrophe names in both tests (6), and #656's `not-on-path` cases (3) | | command: the old inline line at the call site | 11 | the 9 e2e cases with a special character, 2 of `bash -i` | All 25 mutations are killed. A first version of "an apostrophe alone forces single quotes" was a syntax error, so every case failed; it was rewritten with `\'` and re-run, and the row is that run. On the base (35133f2 code) and on the head: - **install.sh, `D:\tools\bin`, a `cygpath` that spells it:** base lines `preflight: D:\tools\bin/openRepoShape --preflight` and `done: … installed at D:\tools\bin/openRepoShape, but the preflight did not run to the end (exit 127)` (the exit 127 is Linux: the drive spelling resolves only in Git Bash). Head: `preflight: <dir>/openRepoShape --preflight` and `done: … installed at <dir>/openRepoShape, pinned to the release tag, and this machine is ready`, exit 0. - **install.sh, no `cygpath`:** base exit 2 after 2 requests (the redirect and the fetch), with the command's refusal. Head: exit 2, 0 requests, install.sh's own refusal. - **The PATH note:** base `a"b$HOME c` → `eval` fails with `unexpected EOF`; `a$b` → `$PATH` gets `…/a`; ``a`echo X` `` → `$PATH` gets `…/aX` (the backquote ran). Head: all three give exactly the directory plus the old `$PATH`. A backslash, a bang, an apostrophe, a space and a plain name keep or gain the right line (the first two single-quoted, the last three today's line). - **A directory holding `:` or a newline:** base exit 0 and the command placed under it, with a line `eval` cannot use. Head exit 2, nothing placed. - **Needs Windows to be real:** `cygpath -u` on a real drive path, a real `D:`. Not faked as real; the PR's `tests-git-bash` run checks it through the tests (below). - origin/main de92614 (adds #698, `tests/test_doctor_next_is_a_command.py` only): clean. - #630 `l3-563-docs` 5b140bd: conflicts in `docs/handbook.html`, and the same single file conflicts between #630 and main de92614 alone (#630 is lane 3's to refresh). #630 touches none of this PR's four files, so my change adds no region. - Lane 3's `l3-668`, `l3-669`, `l3-677`, `l3-678`; lane 4's `fix/610`, `fix/645`, `fix/646`, `fix/648`, `fix/650`, `fix/651`, `fix/653`, `fix/655`, `fix/658`, `fix/659`, `fix/664`, `fix/696`: clean, and none touches `install.sh`, `openRepoShape` or the five installer and launcher suites. - Checked on Linux: CRLF `write_text` (218 passed). No new assert compares a path bash printed with a Python path (the PATH-note line is built from `bash_path(tmp_path)`, as #656's tests are). - Not checked: on `windows-latest`, Git Bash's real `cygpath` for the new install.sh cases (3 spellings, through `put_cygpath`, which puts a `#!/bin/sh` wrapper around `shutil.which("cygpath")` on that `$PATH`, as `put_on_path` does for `mktemp`); the 12 PATH-note names that can be created there (`a$b`, a backquote, `!`, a space, an apostrophe); the `bash -s` runs that carry the script as bytes and the directory in the environment; and a newline or a `:` in `$OPENREPOSHAPE_BIN_DIR` crossing the environment into Git Bash. The names with `"` or `\`, the `bash -i` cases, the `$HOME` default and the respelled-colon case skip on Windows; `export_path_line` is still run for the `"` and `\` names there, by environment, not by a file name. If one fails there, its log shows the typed value and the printed line. - bash 3.2 (macOS's `/bin/bash`) was not available: only the loop's forms are claimed for it. - **install.sh's own `:` and newline refusals.** See candidate 1. - **Release notes.** `--install` now refuses a `:` or newline in the directory, where it used to place the command and print an unusable line: a visible change for `docs/release-notes/v1.1.0.md` (lane 3's #630). Not edited here. - README, the handbook and `docs/cli.md`: no sentence became false. 1. **install.sh learns of a `:` or a newline in `$OPENREPOSHAPE_BIN_DIR` only after its fetch.** Handed such a value, install.sh fetches the launcher, and the command refuses with exit 2; install.sh says "the <version> openRepoShape's --install refused (exit 2); its own refusal above says why". A release older than this change does not refuse at all. Copying the two `case` arms into `main`, before the symlink check, would make install.sh refuse first and name the variable, as it now does for the drive spelling. 2. **A directory holding another control character** (a carriage return, an escape) is still echoed as typed by the refusals and by the note's `is not on $PATH` line; only the newline is refused first. It cannot forge a line, but an escape sequence reaches the terminal. Review: rv667, lane 5's independent review, approved a3cde99 with nits, and codex reviewed it. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 a3cde99 passed tests, tests-git-bash and SonarCloud; the head a22b94c is a clean update-branch merge of main bf1f099, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 7e21cd2 over main dc7d3fb: the install_sh, openreposhape_command, install_sh_filter_repo, openreposhape_package, repo_hygiene, agents_md_codes, cli_reference sets, 422 passed. Landed on Brett Heap's word of 2026-10-17T10:59:39Z, relayed by lane 3 on #473: "merge lane 5's approved v1.1.0 drafts". Closes #667 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e no bytecode under templates/, the launcher guard reads family.py's siblings child, the quoting-sentence inventory follows an aliased quoter, a suffixless tool and each tool's calling functions, and an S5778 pytest.raises builds its argument first (#665) (#724) Four guards held less than they said. One commit, `151a0861`, on `8062810d`. No shipped behaviour changes except one environment variable in the generator's `--help` children. No `--help` text changes, so `docs/cli.md` is not regenerated. No quoting changes. 1. **`scripts/render-cli-reference.py`: `child_env()` sets `PYTHONDONTWRITEBYTECODE=1`.** The template's `siblings.py` does `from bootstrap import ...` from its own directory, so the generator's `--help` child wrote `templates/family-root/scripts/__pycache__/bootstrap.cpython-312.pyc` (ignored by git, so silent). A person who runs the generator now gets the same tree as the test that runs it. The docstring says why. `tests/test_cli_reference.py` gets one test: the variable is in `child_env()` whatever the caller had, and an interpreter started under that environment reports `sys.dont_write_bytecode` as true. 2. **`tests/test_validator_child_encoding.py`: the launcher guard sees `family.py`.** `interpreter_runs` accepts `[sys.executable or PYTHON, ...]` as well as `[sys.executable, ...]`, and `LAUNCHERS` gains `"scripts/family.py": "inherited_child_environment"`. Dropping `env=` from `family.py siblings`' child (#214) now fails the guard. 3. **`tests/test_readme_windows_quoting_sentence.py`: the inventory holds what it claims.** It follows `from repo_shape import shell_line as joined` (and `joined = shell_line`, however many steps). It scans every file `git ls-files` lists (tracked, or new and not ignored) that is a `.py` file or starts with a `python` shebang, which adds the root `bootstrap`. It pins functions, not files, for the two tools the sentence names by particular lines: `scripts/family.py` (`_print_next`, `_switch_command`, `_add_command`, `_print_next_steps`, `_checked_mount_path`) and `setup-project.py` (`family_next_command`). `shape-doctor.py` stays every line, because the sentence says "`shape-doctor.py`'s lines". Only the bodies of `shell_quoted` and `shell_line` in `scripts/repo_shape.py` are exempt, by name; the rest of that file is not. Nothing about quoting changes: the Git Bash dialect (option (a)) is its own v1.1.1 issue, and when it moves `NAMED`, this table moves with it. 4. **`tests/test_bump_leg_paste_lines.py`: Sonar S5778.** `placement = bump_leg_module.leg_placement(leg)` before the `with pytest.raises(LegNotAsFound)` in `test_pasting_the_status_line_reads_that_leg`, as the test above it does. Five lines; the hunk is :401-405, away from #657's hunks. **The reproduction (`.pyc`).** In a fresh shared clone at `8062810d`, with `PYTHONDONTWRITEBYTECODE` unset, `python3 scripts/render-cli-reference.py --check` printed `[ok] docs/cli.md is what the generator produces` and left five `.pyc` files, among them `templates/family-root/scripts/__pycache__/bootstrap.cpython-312.pyc` at 11533 bytes (the size the issue reports). The same command on `151a0861` printed the same `[ok]` line and left none (`find . -name '*.pyc'` found 0). **Mutants.** Each is applied to a copy at the head's test files; "old" is the same mutant under `8062810d`'s own tests. | Mutant | New tests | Old tests | |---|---|---| | M0: drop `PYTHONDONTWRITEBYTECODE` from `child_env()` (the base's behaviour) | `test_child_env_leaves_no_bytecode_in_the_trees_it_runs` fails: `KeyError: 'PYTHONDONTWRITEBYTECODE'` | none to run | | M1: drop `env=` from `family.py`'s `siblings` child (`scripts/family.py:6543`) | `test_the_tools_that_run_the_projects_validators_give_them_an_environment[scripts/family.py]` fails: ``scripts/family.py:6543 runs the interpreter without `env=inherited_child_environment()` `` | 55 passed (survives) | | V2: `from repo_shape import shell_line as joined` and a call, in `update-shape.py` | `test_the_tools_that_quote_for_the_host_...` fails: files are `[family, setup-project, shape-doctor, update-shape]` | 3 passed (survives) | | V3: `family.py`'s `_switch_command` calls `shell_line(..., "posix")` | `test_the_functions_that_quote_for_the_host_...[scripts/family.py]` fails: `_switch_command` missing from the calling functions | 3 passed (survives) | | V5: a host-quoter call in the suffixless `bootstrap` | the file test fails: `bootstrap` is now among the files | 3 passed (survives) | | V6 (the exemption): a third function of `scripts/repo_shape.py` calls `shell_line` | the file test fails: `scripts/repo_shape.py` is now among the files | 3 passed (survives) | Each new failure is one test, and the other tests of the file stay green (`1 failed, 8 passed`; `1 failed, 55 passed`). V2, V3 and V5 are R588's mutants as the issue describes them. On `151a0861`, one run, `run9.sh` (gh stub first on PATH, `GH_TOKEN` bogus, empty `GH_CONFIG_DIR`): `tests/test_cli_reference.py tests/test_validator_child_encoding.py tests/test_readme_windows_quoting_sentence.py tests/test_bump_leg_paste_lines.py tests/test_family_siblings_placement.py tests/test_repo_hygiene.py`: **227 passed in 698.59s**. Per file: `test_cli_reference.py` 23 (one new), `test_validator_child_encoding.py` 56 (one new case, `[scripts/family.py]`), `test_readme_windows_quoting_sentence.py` 9 (6 new: the `scripts/family.py` and `setup-project.py` function pins, the alias, the method name, the exemption, the suffixless file), `test_bump_leg_paste_lines.py` 33, `test_family_siblings_placement.py` and `test_repo_hygiene.py` 106 together. - **origin/main `35133f29`** (includes #657 as `78fe3fff`, which edits `tests/test_bump_leg_paste_lines.py` at :53, :235-244 and `setup-project.py`): clean. On the predicted merge commit, `test_readme_windows_quoting_sentence.py` and `test_validator_child_encoding.py` pass (65 passed), and so does `test_bump_leg_paste_lines.py` (33 passed). `family_next_command` is still the only function in `setup-project.py` that calls `shell_word`. - **#630** (`5b140bd3`): conflicts in `docs/handbook.html` only, and it does so against `8062810d` and `35133f29` without this commit, so not this change's. It changes `scripts/render-cli-reference.py`, `docs/cli.md` and `tests/test_repo_hygiene.py`; none of those hunks overlaps mine, and `merge-tree` reports no conflict in them. - **Lane 4's `family.py` work:** the heads I could fetch are #670 (`pr670`) and #671; `fix/664-surviving-mutants` and `fix/696-setup-family-folder` are on origin too. Clean with each. Lane 4's other `family.py` branches (#684, #686, #688, #689, #693, #674) are not on origin and not in the shared `.git`, so I could not check them. Predicted merges with #670, #671 and lane 5's `fix/646` pass the two guards' files (65 passed each). - **Other open heads touching a tool the inventory reads:** #703 and `fix/696` (`setup-project.py`), #699 (`adopt-project.py`), #700, #701, #702 and #705 (`shape-doctor.py`), `fix/664`: clean, and the inventory file passes on each predicted merge (9 passed each). #682, #698 and #706: clean. - **No quoting change.** The Git Bash dialect (option (a)) is a separate v1.1.1 issue; the inventory only holds what the sentence says today. - **`docs/release-notes/v1.0.0.md:532`** says the `.pyc` is left by `test_cli_reference.py`'s children. That is true of v1.0.0 and I left it. The v1.1.0 notes (#630) may want one line. - **`--others` in the file listing.** A new, untracked tool is scanned before it is added, as `rglob` did; a tracked file deleted from the work tree is skipped. - **A quoter handed on as a value is not a call.** `map(shell_quoted, words)` or `functools.partial(shell_line)` is not seen by the inventory (said in `host_calls`' docstring). Nothing does it today. - **The function pin counts functions, not calls.** A function with two host-dialect calls where only one is switched to POSIX still passes. Today the one-call functions are `_switch_command`, `_print_next`, `_checked_mount_path`. - **`_print_next_steps` prints a `cd {shell_quoted(work)}` line** the sentence's list (`NEXT`, `switch -c`, `add`, `git rm -r --cached`) does not name. It belongs to the Git Bash dialect issue. - **The launcher guard's `LAUNCHERS` still has no row for `src/openreposhape/cli.py` (`run_child([sys.executable, ...], env)`) or `setup-project.py` (its own `run` with a `PYTHON` variable).** Both spell the child differently from `subprocess.run([sys.executable, ...])`. Review: lane 5's independent review approved 151a086 with nits only, and codex reviewed it. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 151a086 passed tests, tests-git-bash and SonarCloud; the head 949fd4d is a clean update-branch merge of main bf1f099, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 9cd4767 over main 974d482: the bump_leg_paste_lines, cli_reference, readme_windows_quoting_sentence, validator_child_encoding, adopt_plan_leg_path_help, repo_hygiene, agents_md_codes sets, 290 passed. Landed on Brett Heap's word of 2026-10-17T10:59:39Z, relayed by lane 3 on #473: "merge lane 5's approved v1.1.0 drafts". Closes #665 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-3 (openRepoShape-3)
Claim: #679 (comment)
Closes #679.
What changed
Test-only, in
tests/test_doctor_next_is_a_command.py.shape-doctor.pyis not touched.The #277 guard read every string constant alone and wanted a blank after the
#, so three spellings of the defect it guards against reached a NEXT unseen. It now sees each of them:#.TAIL(:1161) is(?:[ \t]{2,}|\t)#, two or more blanks or a tab and then#, with or without a blank after it. An interactive zsh pastes#xas an argument.+.pieces_of(:1179) reads a string expression whole: a+chain and an f-string are one string, and each part that is not a constant is a gap.cmd + " " + "# x"is# xafter a gap.tail_runs(:1203) takes each run of constant text between two gaps and flags it when it holds aTAIL, or opens with#straight after a gap, because that gap may be the blanks (f"{cmd}{pad}# x").tails_in(:1220) walks the tree as before, reads each string expression once (the nodespieces_ofhas read go inparts, so a piece is not read again alone), and reports a run, not a constant. A run is the text between two gaps, soTAIL_ALLOWED(:1169) names the same text it did and is unchanged. The#flag of a format spec (f"{x:{w}#x}") is not a tail.The test (
:1271):unspaced,split,led,:1297-1302) and aquietdef (:1303) that must stay unflagged: a#with nothing before it, one blank before#, and a format spec's#;reportyields are exactly[TAIL_ALLOWED["report"]], where it wasin. With the two asserts before it, the real file yields exactly the allowed pair.Claims in the issue, re-checked on main c6122e8
All hold: the real file yields only
placement_plan_leg's one YAML comment line (shape-doctor.py:6598) andreport's one line (:8865); the tab at:2517is apartitionseparator; no string in the file opens with#after a gap.Tested
Mutation checks, in scratch clones, not in the branch:
TAILandtails_inand through the new ones: the old pair finds none ofcommand + " #x",command + " " + "# x"andf"{command}{pad}# x"; the new pair finds#x,# xand# x. The controlf"{command} # a tail"is found by both.TAILandtails_in: fails at the sample assert, "Right contains 3 more items, first extra item: ('led', '# x')".shape-doctor.pywith a def appended: a rawcommand + " # x"fails the test ([(8995, 'mutant_next', ' # x')]), and so does each of the three new spellings. Withshape-doctor.pyuntouched the test passes.shape-doctor.py(9 open PRs listed on 2026-10-16), andorigin/mainyields the allowed pair, so the guard passes on main and on the PRs now open.Pinned shape copies
None touched: no
repo_shape.py, assembly-root or family-root script changes, so noupstream-changedrow.Lands after
Nothing.
🤖 Generated with Claude Code
Summary by Sourcery
Strengthen the test-only NEXT-command guard so it catches additional comment-tail spellings without flagging legitimate hash text.
Bug Fixes:
#space, tails split across concatenated strings, and tails beginning after formatted values.Enhancements:
Tests: