Repository navigation
family.py: the add, bump and remove lines its remedies name begin with the interpreter and the script's path, quote the row's name, fill in the family's org, say the branch they run on, and member-already-present prints its remove (#1127) - #1201
brettheap wants to merge 6 commits into
Conversation
… and the `add` that `bump`'s `member-unknown` names carry `--family-root <holder>`, quoted for the host, so each runs as printed (#1005) `remove` and `add` require `--family-root`, as `bump` does. #927 gave every remedy that names `family.py bump` the holder and left these three, which typed as printed ended in `the following arguments are required: --family-root`: - `add`'s `member-duplicate-id`: `family.py remove --member <P>`. - `bump`'s own `member-duplicate-id`: `family.py remove --member <P>`. - `bump`'s `member-unknown`: `family.py add --member <org>/<P>`. Each now names `--family-root` and the resolved `family.root`, spelled by `shell_quoted` as #927's `bump` line and `_add_command`'s are. `<org>` stays a blank for the person to fill in. README's and the handbook's "Windows, natively" sentence names `remove` among the lines `family.py` quotes for the shell they are printed in, and the handbook's markers carry README's new section hash. The table in `tests/test_readme_windows_quoting_sentence.py` counts the new host-dialect calls in `_checked_member_id`, `_checked_bumped_id` and `cmd_bump`. `tests/test_remove_add_remedies_family_root.py` types each printed line from another folder, over holders in a plain folder, under `my work` and `Dana's work`, reached by a relative `--family-root` and through a symlink: the line names the holder whole, `family.py`'s own parser accepts it, and it runs as printed and does what the remedy says. `tests/test_family_add_member_id.py` and `tests/test_family_bump_to_checks.py` follow the new words, and the former now runs the `remove` line exactly as printed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…in with the interpreter and the script's path, quote the row's name, fill in the family's org, say the branch they run on, and `member-already-present` prints its `remove` (#1127) RV1117's follow-ups on #1117 (#1005), stacked on #1117 at 10dc011: the lines `bump`'s `member-unknown`, `add`'s `member-already-present` and both `member-duplicate-id` remedies print are now ONE spelling, `_family_line`: the interpreter, this file's path from where the run stood, the verb, `--family-root <holder>` and each value, every word quoted by `shell_quoted` for the reader's shell, as `init`'s NEXT STEPS' `add` already was (N2, N4). `member-unknown`'s `add` names the owner `--member` was typed with, else `family.yaml`'s `org:`, and leaves the `<org>` blank only where neither is a `--member` `add` takes (N3). Both refusals said before `_checked_branch` now say the line runs on a branch other than the holder's tracking branch, with the `git switch -c` that makes one and BRANCH_LEFT (N1, and N1b: `member-already-present`'s `bump` was refused there too). A command holding a backtick is printed without backticks around it (N6). `member-already-present` prints the `remove` line beside the `bump` line (N7). The README sentence's list of `family.py` lines is tied to NAMED and to the verbs `_family_line` is called with (N5). New tests/test_family_remedy_lines.py types each line as printed, from the tracking branch, and the existing tests that pinned a bare `family.py` read the new lead. Closes #1127 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…amily-root (#1005) One file conflicted, docs/handbook.html, and only at the seven `reads README.md, section Starting a project in a new organisation: blob ...` markers (the masthead, `start`, `partway`, `ci`, `legs`, `doublepin` and `park`), the known serialization of two README pull requests: - this branch changed that README section's "Windows, natively" sentence, which now names `remove` among the lines `family.py` quotes for the shell they are printed in, so its markers carried blob 2085e8ac35de0506c5ea51774a105e0e400956d7; - main's #1101 (7cd6690) added a paragraph to the same section, about the ssh, helper and askpass prompts a `--reuse-empty-repo` re-run can still wait on, so its markers carried blob ffaedeefe61720051098400cbf2212de50b923f3. README.md and the handbook's prose merged without a conflict, and the merged README holds both edits. Each of the seven markers is now the hash of that section of the merged README, b102a1cd7ec797cf9aa1daa80fbf3a5e60ae3b72, computed with the repository's own `readme_segments` and `readme_fingerprint` (tests/test_repo_hygiene.py). No other README section changed on either side, so no other marker moved, and no count, list or table in a test needed a new value: main adds no `shell_quoted` call to `family.py` or `setup-project.py`, and README's line count is unchanged by this branch (+2 -2). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Reviewer's GuideImplementationCentralize shell-safe family command construction and update remedies to include executable paths, branch guidance, known organization names, quoted member names, and the missing remove alternative.
TestsVerify the emitted remedy commands, their shell quoting and branch behavior, organization selection, and the README sentence’s coverage of printed command kinds.
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Independent review of #1201 (#1127) for lane openRepoShape-5 READY at No MUST. One SHOULD, which is a change to the PR body only and does not move the head. Three NITs for follow-ups. FindingsSHOULD-1: the Goal's "each runs as printed, including from the branch a fresh clone stands on" is not true of three lines a remedy printsThe title says "say the branch they run on" and the Goal says "each runs as printed, including from the branch a fresh clone stands on". I measured three printed lines that name no branch. Typed on the tracking branch, each is refused. My own probe file, not in the PR, shows it:
RV1117's N1 and #1127 item 1 already noted (a) and (b): "The two Fix (body only, enough to land):
Optional code fix: prefix NIT-1: mutant R5 survives because the fixture's ids equal the names
NIT-2: the rows check the interpreter's name but do not run it
NIT-3:
|
| run | git | result |
|---|---|---|
8 changed files + test_repo_hygiene, test_agents_md_codes, test_cli_reference |
2.43.0 | 510 passed in 358.62s |
the same 11 files, clean worktree (git version 2.47.1 is the log's first line) |
2.47.1 | 510 passed in 331.64s |
| the body's 9 related files | 2.43.0 | 443 passed in 313.21s |
predicted restack 455db1144: the 11 above + test_path_guard_hygiene, test_family_leg_path_by_hand_bytes, test_family_tracking_branch |
2.43.0 | 666 passed in 426.51s |
- 510 + 443 = 953 matches the body's 17-file-plus-trio run.
- An earlier 2.47.1 run also passed 510. An untracked probe file was in the worktree then, and
initwarns DIRTY on untracked files, so I re-ran it clean and report the clean run.
Rows that fail on #1117's head
- I put
10dc011c9'sscripts/family.py(blobbea9abd47) undereac177af9's tests in a second worktree and ran the body's 8-file set with-rA. - Result: 90 failed, 142 passed.
- Per file it matches the body's table exactly: 45/45, 18/21, 12/12, 6/14, 4/4, 3/6, 2/46, 0/84.
- All 90 are
AssertionErrorat a test file's assert; none is an import error. - Restored: sha256
b8199379...before and after,git statusclean.
Own mutants
Each was applied alone in the second worktree and restored, with the sha256 checked after every restore. After the last one, git status --porcelain --untracked-files=no was empty.
| id | mutation | result |
|---|---|---|
| R1 | _cited always prints the command bare |
killed, 11 of 12 rows |
| R2 | owner taken by partition instead of rpartition |
killed, 6 rows (a bare IRQS became IRQS/IRQS) |
| R3 | _family_line quotes its words for sh on every host |
killed by the NAMED row [scripts/family.py] |
| R4 | member-already-present loses its branch clause |
killed, 3 rows |
| R5 | branch suggested from the row's name, not its id | survived, 12 passed (NIT-1) |
| R6 | member-already-present's lines name the typed project, not the row's |
killed, 4 rows (3 in test_family.py) |
| R7 | README and the handbook both drop cd from the sentence |
killed, only by test_the_kinds_of_family_lines_are_the_ones_the_sentence_lists |
Lines typed as printed (probe tests/test_zz_rv1201_probe.py, kept outside the tree; 27 of 27 rows passed in the end)
member-unknown'sswitch -cthenadd, onmain. Typed through/bin/bash,/bin/dashand/usr/bin/zsh, withpython3from PATH, from a foldermy desk ü. Holders were underDana's workand undercafé ’q $HOME;x(with a U+2019 apostrophe). Theaddtyped alone isREFUSED family-on-tracking-branch. After the switch it exits 0, lands onfamily/add-irqs, and the holder's gate is green. 6 of 6.member-already-present'sbumpandremove, the same way. 6 of 6. The pin moved tomain~1, or the row was gone.- Quoting. A hand-edited, committed row
project: "IR$(touch PWNED)RS"is printed as--member 'IR$(touch PWNED)RS'. Typed in each of the three shells, noPWNEDfile is created, andfamily.pyrefuses it asunsafe-value. 3 of 3. - Detached holder. The clause's
switch -c, then theadd, exit 0; typed alone, theaddisREFUSED family-holder-detached.
The owner fill-in
- The PR's 7 params pass.
- My 6 further cases also hold:
org: "-Ink"gives<org>/IRQS.org: "Ink.Router"givesInk.Router/IRQS.- A typed
a/b/IRQSgivesInkRouter/IRQS. - A typed
inkrouter/IRQSis kept as typed. org: 123andorg: [a, b]give<org>/IRQS.
- The template writes
org: "{{ORG}}", quoted, soorg:is always a string unless edited by hand.
PowerShell, in process
- With
repo_shape.host_shell_dialectforced tontandPYTHONset topython,powershell_wordsreads_family_line's line as[python, ..\my desk\scripts\family.py, bump, --family-root, <holder>, --member, <name>, --to, x]. - That held for 3 Windows holders (an apostrophe, U+2019, a plain one) times 3 names (
IRRS,IR RS,O'Neil), and for_switch_command. - The interpreter is
repo_shape.PYTHON:pythonon Windows and under Git Bash,python3elsewhere. Nopyand nopython3on Windows, asvalidate-family.pyand_add_commandalready print it.
Claims checked by reading or a small script
_add_commandprints the same bytes as family.py: theremovethat bothmember-duplicate-idremedies name and theaddthatbump'smember-unknownnames carry--family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117's formula: 12 of 12 in-process cases.NAMED's counts (_family_line3,_add_command1,_unknown_members_repository2) match the code.- The
_family_lineverbs arebump,removeandadd. Those plus the hand-written kinds equal README's list of 7 kinds:NEXT,switch -c,add,cd,git rm -r --cached,bump,remove. - The body's "after" remediation text matches the head's output word for word, followed by
BRANCH_LEFT. - The wording "on a branch other than the holder's tracking branch, which
removerefuses" is the same asvalidate-family.py:1855-1858. - Asserts read on the syntax tree: the new file has 43, and the sentence file goes from 34 to 41. None of them joins conditions with
andoror. - The diff adds no non-ASCII line, and no added test line uses
ast.dump,repr(,sys.versionor!r. - The Limits line numbers are right: 558, 607 and 10386.
Docs
- No remedy text appears in README.md, docs/handbook.html, AGENTS.md or docs/*.md.
- The README sentence that pins the host quoters (README.md:159-162) and its handbook twin (docs/handbook.html:739-743) already list
add,bumpandremove, from family.py: theremovethat bothmember-duplicate-idremedies name and theaddthatbump'smember-unknownnames carry--family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117. - This PR adds no new kind of line, so it does not need to touch that sentence. The new
test_the_kinds_...row ties it to the code, and R7 shows a dropped kind fails there. git diff --name-only 10dc011c9 eac177af9names no file underdocs/.
Merge-trees
git merge-tree --write-tree ef619c74a eac177af9: exit 1, one conflict,docs/handbook.html. Against current maindea964aa1: the same.- The conflict is 7 hunks, every one a
reads README.md § Starting a project in a new organisationmarker: main has blobffaedeef..., the PR has2085e8ac.... The hunks are inmasthead,start,partway,ci,legs,doublepinandpark. scripts/family.pyauto-merges.- The conflict is family.py: the
removethat bothmember-duplicate-idremedies name and theaddthatbump'smember-unknownnames carry--family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117's own:- family.py: the
removethat bothmember-duplicate-idremedies name and theaddthatbump'smember-unknownnames carry--family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117 at10dc011c9againstef619c74a: exit 1,docs/handbook.html. - family.py: the
removethat bothmember-duplicate-idremedies name and theaddthatbump'smember-unknownnames carry--family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117 at219e57c28againstef619c74a: clean, treeec97278a. Againstdea964aa1: clean, treee6bda502.
- family.py: the
- Against family.py: the
removethat bothmember-duplicate-idremedies name and theaddthatbump'smember-unknownnames carry--family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117:10dc011c9witheac177af9is clean (tree0abd5f5d), and219e57c28witheac177af9is clean (tree3e1d8a93).
CI and PR state
- CI on
eac177af9:tests,tests-git-bash,packageand SonarCloud all SUCCESS. - The PR is OPEN, draft, MERGEABLE, CLEAN.
Not checked
- A real Windows host (PowerShell 5.1 or 7, or Git Bash). PowerShell was read in process only.
- macOS zsh, and Python 3.14, which CI runs; the bench runs 3.12.
- The full suite.
- The PR's own mutants M1-M16 and D1-D3, which I did not re-run.
- The heads of validate-manifest.py, validate-family.py, family.py and update-shape.py: an
id:, a date or arepository:ending in a line feed is refused by name, and a pin'ssource_repository:that is no path orowner/reponame is named as the pin's (#1139) #1166 and family.py and the family root's siblings.py: a holder's.gitmodulesthis user cannot read, as through a link into a folder it cannot search, isgitmodules-unreadable, never no section forremove's rollback or no url formake siblings(#1110) #1164, and the merge-trees the body lists for them. - Sonar beyond the check's conclusion.
- Lane 5's
v124/n1127/logs: I measured everything independently and did not read them. bump --dry-run's duplicate-idbumpline: it has none, only aremoveline.
(Posted by lane openRepoShape-5's coordinator. rv1201 is lane 5's independent opus reviewer; it wrote none of #1201. SHOULD-1 is fixed in the body: the Goal is narrowed and Limits names (a), (b) and (c). The three NITs and the optional code fix go to a follow-up. #1201 stays stacked on #1117 and takes the three-merge restack after #1117 lands.)
Lane: openreposhape-5 (openRepoShape-5)
…py: an `id:`, a date or a `repository:` ending in a line feed is refused by name, and a pin's `source_repository:` that is no path or `owner/repo` name is named as the pin's (#1139) (#1166) The value patterns the issue names refuse a value that ends in a line feed, by name, as #1126 (#1017) makes `COMMIT_RE`, `SHA256_RE` and `COMMIT_40` do: - `DATE_RE` and `QUALIFIED_RE` in `validate-manifest.py` and `validate-family.py`, `MEMBER_RE` in `scripts/family.py`, `DATE_RE` in `scripts/shape_advisory.py` and `REPOSITORY_RE` in `update-shape.py`; - `PROJECT_ID_RE`, read with `fullmatch` where the two validators and `family.py add` read an id from a file. A shape pin's `source_repository:` that is neither a path on this machine nor an `owner/repo` name is refused in the pin's name, not `--upstream`'s. Values without a line feed keep their outcome, and no refusal code is added or renamed. Two inputs do move from one existing code to another (see "Behaviour changed", below). **What this PR does not cover, and the title does not claim:** - the naming-policy patterns that `scripts/repo_shape.py` reads from `contracts/repository-naming.yaml`, which `validate-repository-naming.py` still reads with `.match`; - `update-upstream-not-a-repo`, which still says `--upstream` when a pin names a folder that exists and is not a repository. Both are follow-ups, with the other findings of the independent review, under "Follow-ups (not in this PR)". This does not need #1126 and does not touch its files or its test file. Measured on main `038dafba` (Python 3.12.3, git 2.43.0) by a probe of each tool, before any change. Each value is written in the file as a YAML double-quoted `"...\n"`, which the reader takes as the value and a line feed. | Read from | Value | On main | |---|---|---| | `project.yaml` (`validate-manifest.py`) | `id: "atlas\n"` | only `FINDING manifest-topic: topic is 'xf-project-atlas', expected 'xf-project-atlas\n' derived from id 'atlas\n'`, exit 1 | | `project.yaml` | `elected_on: "2026-09-02\n"` | `manifest ok`, exit 0 | | `project.yaml` | a leg's `repository: "testorg/Atlas-spec\n"` | `manifest ok`, exit 0 | | `family.yaml` (`validate-family.py`) | `id: "inkrouter\n"` | `family ok: InkRouter (inkrouter` and a line break, exit 0 | | `family.yaml` | `created_on: "2026-09-04\n"` | `family ok`, exit 0 | | `family.yaml` | a member's `repository: "InkRouter/IRRS\n"` | `FINDING member-repository-name: ... whose name is 'IRRS\n'`, exit 1 | | a member's `project.yaml`, read by `family.py add` | `id: "irqs\n"` | mounted and committed, exit 0, with the row `id: "irqs\n"`; the holder's gate then said `family ok` | | `contracts/shape-pin.yaml`, `update-shape.py check` with no `--upstream` | `source_repository: "opensoft/openRepoShape\n"` | `REFUSED unsafe-value: --upstream is 'opensoft/openRepoShape\n'`, exit 2 | | the same | `source_repository: "opensoft/open Repo"` | `REFUSED update-upstream-unresolvable: --upstream 'opensoft/open Repo' is neither ...`, exit 2 | | the same | `source_repository: "-opensoft/openRepoShape"` | `REFUSED unsafe-value: --upstream is '-opensoft/openRepoShape'`, exit 2 | | an adoption plan (`adopt-project.py check`) | `id: "northwind\n"` | `FINDING plan-manifest-invalid: id is 'northwind\n', which the project's own validate-manifest.py refuses: manifest-topic: ...`, exit 1; `execute` refused it, exit 2, before any leg | In-process on the same main: `repo_shape.PROJECT_ID_RE.match('atlas\n')`, `shape_advisory.DATE_RE.match('2026-09-02\n')` and `REPOSITORY_RE.match('a/b\n')` each matched, and `date.fromisoformat('2026-09-02\n')` raised `ValueError`. - **`DATE_RE` and `QUALIFIED_RE`** in `templates/assembly-root/scripts/validate-manifest.py` and `templates/family-root/scripts/validate-family.py` end in `\Z`. So do `MEMBER_RE` in `scripts/family.py` (the third spelling `tests/test_family_member_name.py` holds equal to the two `QUALIFIED_RE`), `DATE_RE` in `scripts/shape_advisory.py` and `REPOSITORY_RE` in `update-shape.py`. - **`PROJECT_ID_RE` keeps its `$`.** Every id refusal prints its text (`must match ^[a-z0-9][a-z0-9-]*$`, pinned in six test files), the validators run against each project's own copy of `repo_shape.py`, and its definition sits inside #1126's hunk. The three callers that read an id from a file ask it with `fullmatch` instead: `validate-manifest.py` (`manifest-id`), `validate-family.py` (`family-id`) and `family.py`'s reading of a member's `project.yaml` (`member-bad-id`, which `add` now says before anything is mounted). - **`update-shape.py`.** `open_upstream(spec, pin_path=None)` takes the pin the value was read from, which `prepare` passes when no `--upstream` was given. Then `update-upstream-unresolvable` says ``<root>/contracts/shape-pin.yaml: `source_repository:` is '...', which is neither a path on this machine nor an `owner/repo` name``, with a remedy to write the pin's field or pass `--upstream`, and `checked_value` names the same field. **Code kept:** `update-upstream-unresolvable`, the existing code for a value that is not an `owner/repo` name. A line feed now reaches it, since the pattern no longer takes one. Before, `checked_value` refused it as `unsafe-value`. - **One existing test line:** `tests/test_family_init_values.py` finds validate-family's `DATE_RE` line by its text, which now ends `\Z`. - **One existing stand-in:** the `temporaries` fixture in `tests/test_update_shape_unreadable.py` stands in for `open_upstream` with `def opening(spec)`, which the new two-argument call refused with `TypeError: ... takes 1 positional argument but 2 were given`. It now takes `pin_path=None` and hands it on to the real `open_upstream`, so `test_a_read_refused_in_prepare_closes_the_upstream[check|apply]` asks what it asked before. - **The rows** are in a new file of their own, `tests/test_value_pattern_line_break.py`. - **A second guard, in a second commit (`5102b4294`, a test only, no production file).** The first guard holds the four pattern names this PR ends in `\Z`, and nothing held `PROJECT_ID_RE`, which keeps its `$`. `test_every_reader_of_the_id_pattern_is_in_the_table` reads every tracked `.py` outside `tests/` with `ast` (it runs none) and holds each call of a method of `PROJECT_ID_RE` (`PROJECT_ID_RE.x(...)` or `repo_shape.PROJECT_ID_RE.x(...)`) to the table `ID_READERS`: - the three `fullmatch` readers of an id from a file (`validate-manifest.py`, `validate-family.py`, `scripts/family.py`); - the modules that still `match` it, each with its reason: argv `--id` in `scaffold-project.py` and `scripts/family.py`, where `checked_value` refuses a line feed after the pattern; argv `--id` in `adopt-project.py`, which the project's own `validate-manifest.py` refuses (`adopt-manifest-invalid`, below; `ID_READERS` still gives it the `checked_value` reason); and `adopt-project.py`'s two `plan-bad-id` readers, of the plan's `id:` and of the id derived from `legs.assembly`, which a line feed never passes (below). A new reader of `PROJECT_ID_RE`, `fullmatch` included, fails it by file and method until it is listed in `ID_READERS` with its reason. The table is exact both ways, so a reader taken away fails it too. **Left as they are, and why:** - the argv readers of `PROJECT_ID_RE` (`--id` in `scaffold-project.py`, `family.py init` and `adopt-project.py`). The first two hold the id to `checked_value` after the pattern, which refuses a line feed as `unsafe-value` naming `--id`, and `tests/test_derived_id_refusals.py` pins that answer for them. `adopt-project.py` never calls `checked_value` on `--id`: `plan` with `--id` set to `northwind` and a line feed exits 2 with `adopt-manifest-invalid`, in the words of the project's own `validate-manifest.py` (`manifest-id`), and writes no plan (lane 5's re-check, issuecomment-6371408915, NIT 2; measured on `plan` only); - `adopt-project.py`'s reading of a plan's `id:`: `check` and `execute` hold it to the project's own `validate-manifest.py`, which refused it on main (above) and now names it in `manifest-id`'s words. It is the same value, refused under the same code, `plan-manifest-invalid`, before any leg exists. The id derived from `legs.assembly` is read with `.match` too, and a line feed in `legs.assembly` is `unsafe-value` before that id is asked (the review measured both, on main and here alike). This keeps the open PRs on `adopt-project.py` (24 at 12:02Z) clear; - `PIN_ARG_RE`, and `NAME_RE`/`REF_RE`/`PATH_RE` in `setup-project.py`, as the issue says; - `update-upstream-not-a-repo` still says `--upstream`. A pin's value reaches it only when it names a path that exists, not through the pattern. **Behaviour changed, stated plainly, and what the issue's Verify means by "No refusal code changes":** the issue's Verify line reads as no code added or renamed, and none is. Two inputs do move from one existing code to another, exit 2 both before and after: - `update-shape.py --upstream "opensoft/openRepoShape<LF>"` was `unsafe-value` and is now `update-upstream-unresolvable`, still naming `--upstream`; - a pin's `source_repository:` that ends in a line feed, with no `--upstream`, was `unsafe-value` naming `--upstream` and is now `update-upstream-unresolvable` naming the pin's field. No test pinned either old answer: every gate below passed. `family.py add --member` is stripped before `MEMBER_RE`, so `add` answers as before. **Not changed, seen while measuring:** with `--upstream` passed, `update-shape.py` does not read the pin's `source_repository:` through `REPOSITORY_RE` at all, and it keeps the value as the upstream's repository name. `validate-family.py` does not hold a member row's `id:` to `PROJECT_ID_RE`; it compares the row with the member's manifest. Neither is a `$` question. Both are under "Follow-ups (not in this PR)". All in `tests/test_value_pattern_line_break.py`, run against main's production code: the five production files swapped in place from `origin/main` (`961640c68`), then put back with their sha256 matching (`dirty=5` in the run's START line is those five files). On the head `5102b4294` (30 rows) the result is **20 failed, 10 passed in 23.12s, git 2.43.0**. The 10 that pass are baselines: the seven `test_a_pattern_takes_its_value` rows, the spelling-table guard and the two double-quoted-without-a-line-feed rows. The first 19 are the first commit's rows (19 failed, 10 passed in 22.12s on its 29 rows at `038dafba`). The independent review reproduced them from main's bytes of the same five files (19 failed, 10 passed in 26.78s, the same 19 ids). The 20th, `test_every_reader_of_the_id_pattern_is_in_the_table`, is the second commit's: it fails on main because the three readers of an id from a file are `.match` there, not `.fullmatch`. - `test_a_pattern_refuses_its_value_and_a_line_feed[manifest-DATE_RE]` - `test_a_pattern_refuses_its_value_and_a_line_feed[manifest-QUALIFIED_RE]` - `test_a_pattern_refuses_its_value_and_a_line_feed[family-DATE_RE]` - `test_a_pattern_refuses_its_value_and_a_line_feed[family-QUALIFIED_RE]` - `test_a_pattern_refuses_its_value_and_a_line_feed[family.py-MEMBER_RE]` - `test_a_pattern_refuses_its_value_and_a_line_feed[shape_advisory-DATE_RE]` - `test_a_pattern_refuses_its_value_and_a_line_feed[update-shape-REPOSITORY_RE]` - `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[id]` - `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[elected_on]` - `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[leg-repository]` - `test_adopt_check_names_a_plan_id_that_ends_in_a_line_break_as_an_id` - `test_a_family_value_that_ends_in_a_line_break_is_a_finding[id]` - `test_a_family_value_that_ends_in_a_line_break_is_a_finding[created_on]` - `test_a_family_value_that_ends_in_a_line_break_is_a_finding[member-repository]` - `test_add_refuses_a_member_whose_id_ends_in_a_line_break` - `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]` - `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[space]` - `test_a_pins_source_checked_value_refuses_is_named_as_the_pins` - `test_an_upstream_passed_with_a_line_feed_is_named_as_passed` - `test_every_reader_of_the_id_pattern_is_in_the_table` **13 of 13 killed.** Each mutant was applied alone to one file of the branch's production bytes (the same bytes as the first commit and as the head, which adds a test only), the rows were run, and the file was put back with its sha256 checked (`restored=yes` for all 13). Each run is `tests/test_value_pattern_line_break.py` plus `tests/test_family_member_name.py::test_every_spelling_of_the_member_name_rule_is_the_same_pattern`, git 2.43.0. | Mutant | File | Killed by | |---|---|---| | M1 `DATE_RE` back to `$` | `validate-manifest.py` | 2: `test_a_pattern_refuses_its_value_and_a_line_feed[manifest-DATE_RE]`, `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[elected_on]` | | M2 `QUALIFIED_RE` back to `$` | `validate-manifest.py` | 3: `...[manifest-QUALIFIED_RE]`, `...[leg-repository]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` | | M3 the id read with `.match` | `validate-manifest.py` | 2: `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[id]`, `test_adopt_check_names_a_plan_id_that_ends_in_a_line_break_as_an_id` | | M4 `DATE_RE` back to `$` | `validate-family.py` | 2: `...[family-DATE_RE]`, `test_a_family_value_that_ends_in_a_line_break_is_a_finding[created_on]` | | M5 `QUALIFIED_RE` back to `$` | `validate-family.py` | 3: `...[family-QUALIFIED_RE]`, `...[member-repository]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` | | M6 the id read with `.match` | `validate-family.py` | 1: `test_a_family_value_that_ends_in_a_line_break_is_a_finding[id]` | | M7 `MEMBER_RE` back to `$` | `scripts/family.py` | 2: `...[family.py-MEMBER_RE]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` | | M8 a member's id read with `.match` | `scripts/family.py` | 1: `test_add_refuses_a_member_whose_id_ends_in_a_line_break` | | M9 `DATE_RE` back to `$` | `scripts/shape_advisory.py` | 1: `...[shape_advisory-DATE_RE]` (its pattern row alone: `date.fromisoformat` refuses the line feed after the pattern, so no tool row sees it) | | M10 `REPOSITORY_RE` back to `$` | `update-shape.py` | 3: `...[update-shape-REPOSITORY_RE]`, `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]`, `test_an_upstream_passed_with_a_line_feed_is_named_as_passed` | | M11 `prepare` passes no pin | `update-shape.py` | 3: `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]` and `[space]`, `test_a_pins_source_checked_value_refuses_is_named_as_the_pins` | | M12 `checked_value` names `--upstream` again | `update-shape.py` | 1: `test_a_pins_source_checked_value_refuses_is_named_as_the_pins` | | M13 the pin's refusal sentence removed | `update-shape.py` | 2: `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]` and `[space]` | No mutant is added for the stand-in in `tests/test_update_shape_unreadable.py`: it is a test change. The independent review re-ran M3, M8, M11 and M12, and one mutant of its own (X1: `prepare` names the pin even when `--upstream` is passed). All five were killed, and each file was put back with its sha256 checked. The other nine of the 13 are this writer's runs alone. **The second commit's guard: 6 of 6 probes caught.** Each was applied alone to one tracked file at `5102b4294`, `test_every_reader_of_the_id_pattern_is_in_the_table` was run, and the file was put back with its sha256 checked (`restored=yes` for all six). The first commit's guard, `test_every_spelling_of_a_value_pattern_is_in_the_table`, was run with each plant too: it passes every one of them, which is the gap the second guard closes. | Probe | File | The new guard | The first guard | |---|---|---|---| | P1 a new `PROJECT_ID_RE.match(value)` reader appended | `update-shape.py` | failed: `('update-shape.py', 'match'): 1` is more than the table holds | passed | | P2 a new `repo_shape.PROJECT_ID_RE.search(value)` reader appended | `scripts/shape_advisory.py` | failed | passed | | P3 `fullmatch` back to `match` | `templates/assembly-root/scripts/validate-manifest.py` | failed | passed | | P4 `fullmatch` back to `match` | `templates/family-root/scripts/validate-family.py` | failed | passed | | P5 `fullmatch` back to `match` | `scripts/family.py` | failed | passed | | P6 main's bytes of those three files together (the 20th row above) | the three | failed | not run | **The second commit `5102b4294` (dirty=0)**, Python 3.12.3, with the same stub `gh`, bogus token and no global git config: | Run | Git | Result | |---|---|---| | `tests/test_value_pattern_line_break.py` | 2.43.0 | **30 passed** in 23.00s | | `tests/test_value_pattern_line_break.py` | 2.47.1 | **30 passed** in 23.30s | | `tests/test_pinned_copy_text.py`, `tests/test_path_guard_hygiene.py`, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py` | 2.43.0 | **306 passed** in 61.54s | That commit changes only the test file (62 lines added, no assert that joins conditions with `and` or `or`, ASCII), so the first commit's runs below are not repeated: they hold the production files and every other test file, and those are unchanged. CI on `5102b4294`, and an independent look at the second commit, were not done when this was written. **The independent review of the first commit** (rv1166c, READY, read-only) reports these gates on `2593952c9`, all passing: the stand-in gate's 6 files (298 passed), the new file on 2.47.1 (29 passed), the five hygiene files (306 passed), 15 more test files that name a changed function, pattern or code (1611 passed, 19 skipped) and `tests/test_shape_doctor.py` (445 passed, 3 skipped). It also read CI on that head at about 20:04Z: `tests` (the full suite on 3.14), `tests-git-bash`, `package` and SonarCloud all passed. **The first commit's gates ran on `2593952c` (dirty=0)**, Python 3.12.3, with the real `gh` unreachable (a stub first on `PATH`, a bogus token) and no global git config: | Run | Git | Result | |---|---|---| | the files that held every failure and setup error of the shards below: `tests/test_family.py`, `tests/test_update_shape_pr_base.py`, `tests/test_update_shape_unreadable.py`, `tests/test_update_shape_header_comments.py`, `tests/test_update_shape_messages.py` and the new `tests/test_value_pattern_line_break.py` | 2.43.0 | **298 passed** in 343.60s | | `tests/test_value_pattern_line_break.py` | 2.47.1 | **29 passed** in 21.31s | | `tests/test_pinned_copy_text.py`, `tests/test_path_guard_hygiene.py`, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py` | 2.43.0 | **306 passed** in 69.12s | **The two shards, on the uncommitted tree** holding the same production bytes as the first commit (the 7 files' sha256 checked before and after), git 2.43.0: the validators' test files, the `REPOSITORY_RE`/`source_repository` files and the `update-shape.py`, `shape_advisory.py` and `family.py` files, 125 in all. - shard 1 (62 files): 4 failed, 5290 passed, 13 skipped in 56 min; - shard 2 (63 files): 3 failed, 4 setup errors, 3761 passed, 29 skipped in 50 min. Every failure and setup error is in the five existing files of the first row of the table above, which all pass on the first commit: - 2 were real: `test_a_read_refused_in_prepare_closes_the_upstream[check|apply]`, the `opening(spec)` stand-in under the two-argument call. The stand-in is fixed in the first commit. - The other 5 failures and the 4 setup errors were `update-conflict` on `scripts/validate-family.py` ("not a verbatim copy ... at the pinned commit"). This is an artifact of an uncommitted tree: `tests/test_family.py`'s `upstream_clone` commits the working tree into its clone, while `family.py init` pins the holder at the real `HEAD`, so an uncommitted `templates/family-root/` file is never verbatim at its pin. On the first commit they pass. Every other file of the two shards passed on these production bytes, so the shards were not re-run. `git merge-tree --write-tree <other> 5102b42` (git 2.43.0) at 2026-10-23T12:08Z, after re-fetching main (still `961640c6`; read again at 12:10Z, just before the push) and re-listing the open PRs at 12:02Z (55 open). Main has moved from the `038dafba` that this body measured to `961640c6` by one file, `docs/release-notes/v1.2.0.md`. Each open PR that touches one of this PR's 8 paths, at its current head, plus #1126: | Against | Head | Shares | Result | |---|---|---|---| | origin/main | `961640c6` | | clean, tree `4613729516ff` | | #1098 | `dad7961a` | `scripts/family.py` | clean, tree `4d1ff9517588` | | #1117 | `10dc011c` | `scripts/family.py` | `scripts/family.py` merges clean; conflict in `docs/handbook.html` only, which this PR does not touch (see below) | | #1124 | `8dfa32bd` | `validate-family.py` | clean, tree `cb2ec1073646` | | #1126 | `4c1f80cf` | none (its `scripts/repo_shape.py` and tests) | clean, tree `e82c8945ac37` | | #1133 | `a7bfee1b` | `update-shape.py` | clean, tree `a710870d1ffc` | | #1144 | `89486f69` | `scripts/shape_advisory.py` | clean, tree `2d3e3e1ef43d` | | #1147 | `b75babee` | `validate-manifest.py` | clean, tree `a82ac3b47a3f` | | #1159 | `66e68f94` | `scripts/shape_advisory.py` | clean, tree `4abeb7b38665` | | #1164 | `74096eb4` | `scripts/family.py` | clean, tree `ee285e1cb8cf` | | #1172 | `5aa0f116` | `validate-family.py` | clean, tree `aa32d41e6e2c` | | #1187 | `cc8a87ee` | `update-shape.py`, `tests/test_update_shape_unreadable.py` | clean, tree `25711eb1cac4` | | #1201 | `eac177af` | `scripts/family.py` | `scripts/family.py` merges clean; conflict in `docs/handbook.html` only (see below) | - #1117 and #1201 conflict in `docs/handbook.html` alone. #1201 is stacked on #1117 (their merge-base is #1117's head `10dc011c`). Each of them against origin/main alone gives the same conflict in the same file, so it is theirs: #1117's, from its older base `6da757cb`, which #1201 inherits. - #1187 and #1201 are new since the first merge-trees. #1187 shares `tests/test_update_shape_unreadable.py` with this PR; its diff adds or removes no line that names `open_upstream`, `def opening` or `REPOSITORY_RE`, so it brings no one-argument stand-in for the two-argument call. That was read for all 55 open heads at 12:12Z; only this PR's own lines name them. - No other open PR adds or removes a call of a method of `PROJECT_ID_RE`, which the second guard counts: the lines each of the 55 open heads adds or removes outside `tests/`, `docs/` and `*.md` were read at 12:04Z. Only this PR's own name it, and #1201's one new `MEMBER_RE.fullmatch` is another pattern. So a sibling landing first does not move the guard's table. Each is a candidate for its own issue. They come from the independent review (rv1166c, READY at `2593952c9`, issuecomment-6352173084), which measured each of them on main and at the head unless the item says it did not. The review's finding numbers are in brackets. - **The naming-policy patterns [1].** `scripts/repo_shape.py` reads the families, the roles and the topic pattern of `contracts/repository-naming.yaml` with `.match` (`family["_re"].match(name)`, `family["_split_re"].match(name)`, `role["_re"].match(name)`, and `topic_pattern`, which defaults to `^xf-project-[a-z0-9-]+$`). `validate-repository-naming.py --project project.yaml` runs in the root template's `Makefile` and `validate.yml`. At the head, and on main, it exits 0 on a leg `repository: "testorg/Atlas\n"` and prints the line feed inside its table; `-spec` and `-code` the same. The project's gate as a whole still fails, because `validate-manifest.py` now refuses that manifest. - **`update-upstream-not-a-repo` with a pin [3].** With no `--upstream`, a pin whose `source_repository:` names a folder that exists and is no repository (the review measured `contracts` and `scripts/repo_shape.py`) is `REFUSED update-upstream-not-a-repo: --upstream contracts exists but is not a git repository`, exit 2, on main and here. `pin_path` is in scope at `update-shape.py:686-687`, so that refusal can name the pin as `update-upstream-unresolvable` now does. - **A pin's line feed when `--upstream` is passed [9].** `update-shape.py check --upstream <clone>` over a pin whose `source_repository:` ends in a line feed exits 0, `nothing to do`. The value is kept as `upstream.repository`, which reaches the README shape line's check, `apply`'s commit subject and the pull request words (`update-shape.py:2525`, `3017` and `5121`). The review measured `check`, not `apply`. It belongs with the previous item. - **A member row's `id:` [4].** On main, `add` of a member whose `project.yaml` reads `id: "irqs\n"` commits the row `id: "irqs\n"`, and the holder's `validate-family.py` then says `family ok`. Under mutant M8 (this branch with main's `.match` in `add`) the same row is written, and the gate still says `family ok`. By code reading, not measured end to end: `validate-family.py` compares a row's id with the member manifest's by `str` equality (`member-id-mismatch`, `validate-family.py:1608`), and `_check_member_ids_unique` does the same, so neither asks `PROJECT_ID_RE` of a row. This PR makes `add` refuse such a member, so no new holder gets such a row. - **Values that no pattern reads [8].** The review's sweep found these `manifest ok` or `family ok`, exit 0, with a trailing line feed, at the head and on main alike: - in `project.yaml`: `name`, a spec or code leg's `path`, `shape.repository` and `shape.digests.digest_algorithm`; - in `family.yaml`: `org`, the family's own `repository`, `tracking_branch`, and the shape's and each member pin's `digest_algorithm`. No `^...$` pattern reads them, so they are not this PR's. A leg's `path` is checked only by `_leg_path_findings`, which asks no pattern, and the family's `tracking_branch` is refused in `project.yaml` (`manifest-tracking-branch`, U+000A) but not in `family.yaml`. `shape.commit` and `tree_sha256` with a line feed are also `manifest ok`; that is #1126's. The first commit's message says "the three readers of an id from a file ask it with `fullmatch` (`manifest-id`, `family-id`, `member-bad-id`)". That is true of those three, but it reads as if there were no others [2]. What the review measured instead: - `adopt-project.py` still reads the id derived from `legs.assembly` (`:6780`) and the plan's `id:` (`:6791`), both `plan-bad-id`, with `.match`; - neither lets a line feed through. A plan `id: "northwind\n"` is `plan-manifest-invalid`, now in `manifest-id`'s words. A plan with no `id:` and `legs.assembly: "NorthWind\n"` is `REFUSED unsafe-value: legs.assembly is 'NorthWind\n'`, exit 2, before the derived id is asked, on main and here alike. So it is the wording alone that overstates. The second commit's `ID_READERS` lists both readers with that reason. The title's pin clause is the narrower one (the pin's name only where the value is no path and no `owner/repo` name), and two inputs change from `unsafe-value` to `update-upstream-unresolvable` (see "Behaviour changed", above). Review: rv1166 and rv1166c, lane 5's independent opus reviewers, at `2593952c` (issuecomment-6352173084); then rc1166b, lane 5's independent sonnet re-checker, which wrote none of #1166: READY at `5102b429`, H0 of this landing (issuecomment-6371408915), with no MUST or SHOULD; its NITs 1 and 2 are corrected in the body. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 5102b42 passed tests, tests-git-bash and SonarCloud; the head 5c8afef is a clean update-branch merge of main 93d2931, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 3c89065 over main 11560eb: the family_init_values, update_shape_unreadable, value_pattern_line_break, repo_hygiene, agents_md_codes, cli_reference sets, 365 passed, 19 skipped. Landed on Brett Heap's word of 2026-10-23T13:10:26Z, relayed to every lane by lane 3 (#473 issuecomment-6365697123): "merge v1.2.1 PRs once reviewed and green". Closes #1139 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ready-here` builds its clause before it raises, so "Remediation: " is no fourth bare copy, and two rows pin that each dry run's clause names its branch from the row's id (#1228) rv1264's two NITs on #1264, both at b54520c: - NIT-1: the (c) hunk wrote `"Remediation: "` beside `_branch_clause`'s call, a fourth bare copy of that literal after #1201's three, which SonarCloud reports on #1264 as python:S1192. `_landing` now holds the clause in a local and says `f"Remediation: {clause}"`. The refusal's words are byte for byte what they were, for a plain folder and for one named with a backtick (probed before and after, same script path). - NIT-2: mutants M10a and M10b, the dry runs' clause branch built from the row's name and not its `id:`, survived because every fixture row's id is its name lowercased. Two rows rename a row by hand (`IRRS` to `Orrs`, `IRSS` to `Osss`) and assert the clause's `switch -c` names `family/remove-irrs` and `family/remove-irss`. The edit those rows and the `IR RS` row share is now `renamed_by_hand`. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e for: scripts/family.py tests/test_family_add_member_id.py tests/test_family_bump_to_checks.py tests/test_readme_windows_quoting_sentence.py tests/test_remove_add_remedies_family_root.py Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|



Goal
#1127: the
add,bumpandremovelines thatfamily.py's remedies name are one spelling,_family_line, and each line a realadd,bumporremoveprints runs as printed, including from the branch a fresh clone stands on, as do the dry runs'member-unknownandmember-already-presentlines (three other lines are in Limits). A line begins with the interpreter and the script's path, quotes the row's name, names the family's org where it is known, and says which branch it runs on, andmember-already-presentprints itsremovebeside itsbump.Was stacked on #1117 at
10dc011c9, and is restacked after #1117 landed as9252ef20(see "Restacked after #1117 landed"). Before that, its base was #1117's branch,fix/1005-family-remove-add-family-root, and it showed one commit,eac177af9: 9 files, 638 insertions and 74 deletions (git diff --stat 10dc011c9 eac177af9). It answers the seven follow-ups of RV1117's review of #1117 (#1005), N1 to N7 of #1127, and one more that measuring found, N1b.What was measured before the change
Main is
961640c6and #1117's head is10dc011c9. The probe istests/test_zz_probe1127.py, run untracked throughrun9.sh(stubgh, bogus token, no global git config, no ssh) on each, from 20:13Z to 20:16Z on 2026-10-22. Its scripts and logs are in lane 5's scratchv124/n1127/(probe/,logs/probe-main.log,logs/probe-parent.log,logs/n5-measure-main.log,logs/n5-measure-parent.log).member-unknown'sadd, typed on the tracking branch--family-root, so typed from a holder it exits 2 with argparse'sthe following arguments are required: --family-root, on any branchadd --family-root <holder> --member <org>/IRQSfrom a holder onmainexits 2family-on-tracking-branchmember-already-present'sbump, typed on the tracking branchadd --member InkRouter/IRRSonmainis refusedmember-already-present; itsbumpline, with a real commit, exits 2family-on-tracking-branch.addasks_checked_add_targetbefore_checked_branchfamily.pymember-unknownandmember-already-presentlines startfamily.pyremovelines<org>left blankfamily.yamlsaysorg: InkRouter, the line says<org>/IRQS--memberprinted rawIR RS, thebumpline reads--member IR RS, whichshlexsplits intoIRandRSbumpfrom README and the handbook leavestests/test_readme_windows_quoting_sentence.pyat 44 passedremoveleaves it at 44 passedfamily.py bump --family-root '<...>/a, cut at the path's backtickremovetakes it out, with no linemember-already-presentremedy says "removetakes it out" and prints noremovelineSo both kinds are present. N1 exists only in #1117's
addline, since main's has no--family-rootto be refused for. The other seven (N1b and N2 to N7) hold on both, and #1117 rewrites three of the lines they concern. That is why this is stacked on #1117.What changed
Item by item, with the row that pins it and the mutants aimed at it (the results are in the Mutants section; each run stopped at its first failing row):
member-unknown's remedy opens with the branch clause and thegit switch -c; typed in order from the tracking branch, theswitch -cthen theaddexit 0test_member_unknowns_add_runs_as_printed_from_the_tracking_branchmember-already-present'sbumpandremovetest_member_already_presents_lines_run_as_printed_from_the_tracking_branch(bump,remove)family.yaml'sorg:, else the blanktest_member_unknowns_add_names_the_owner_where_it_is_known(7 params)test_a_rows_name_with_a_space_is_one_word_in_both_linesNAMEDand to the verbs_family_lineis called withtest_the_kinds_of_family_lines_are_the_ones_the_sentence_liststest_a_command_holding_a_backtick_is_printed_baremember-already-presentprints theremoveline[remove]param of the N1b rowThe code, in
scripts/family.py:_family_line(verb, holder, *words)(N2, N4). It prints<python> <this file's path from where the run stood> <verb> --family-root <holder> <words>, every word spelled byshell_quotedfor the reader's shell. That is howinit's NEXT STEPS'addwas already printed (_add_command, family.py and siblings.py print commands with unquoted paths, so under a folder with a space themember-git-dir-cachedremedy'srm -rfwould delete the wrong directory, andadd's push line fails #486), now shared._add_commandis built on it and prints the same bytes.bump'smember-unknown(itsadd),add'smember-already-present(itsbumpandremove),add'smember-duplicate-id(itsremoveandbump) andbump'smember-duplicate-id(itsremove).project:is passed as a word, so a rowIR RSprints'IR RS', one word (N4).<commit>,<org>/...) is added after the line, bare: quoted, it would read as a value._off_the_tracking_branch(N1, N1b).member-unknownandmember-already-presentare refused before_checked_branchreads the branch, so a person on the tracking branch, whereinitand a fresh clone leave them, typed a line that was refused. Each remedy now opens withon a branch other than the holder's tracking branch, which <verb> refuse(s) (<git -C holder switch -c branch> makes one),, and ends withBRANCH_LEFT(family.py and validate-family.py: thegit switch -c family/<act>-<member>remedy fails when an earlier act left that branch, and the docs spell the branch word two ways #607).member-unknown:`add` refuses, branchfamily/add-<project>.member-already-present:`bump` and `remove` refuse, branchfamily/bump-<id>. The issue named only theadd(N1); this is N1b.member-duplicate-idremedies name no branch: a realaddorbumpasks them after_checked_branch.member-unknown'sadd,_unknown_members_repository(N3). The owner--memberwas typed with (<org>/<project>), elsefamily.yaml'sorg:, else the blank<org>. Each is used only whereMEMBER_RE.fullmatch("<owner>/<project>"), so an empty org or an org holding a space leaves the blank, and a typed owneraddrefuses falls through to the family's org. The value is one word for the shell._cited(N6). Every shell reads the line the same either way.member-already-presentprints theremoveline beside thebumpline (N7).`<bump line> --to <commit>` moves an existing member and `<remove line>` takes it out.The remedy of
bump --member IRQSon a family whoseorg:is InkRouter, before and after. The "after" is from the working tree's probe before the commit (logs/probe-wip.log), and the tests pin it on the head.BRANCH_LEFTfollows it.Tests:
tests/test_family_remedy_lines.py(new, 12 rows) types each line as printed. The holder is the suite's family, copied and put back on its tracking branch, astests/test_family_tracking_branch.pydoes, and the lines are typed from a folder with a space in its name, by the interpreter and the script path they print.member-unknown: the exact remediation; theaddtyped alone is refusedfamily-on-tracking-branch; itsswitch -cthen theaddexit 0, pin IRQS onfamily/add-irqs, leave the holder clean andvalidate-family.pygreen (N1, N2).member-already-present, forbumpand forremove: the same, with the pin moved tomain~1or the row gone (N1b, N7).addrefuses; no org; an empty org; an org holding a space; no org but a typed owner), each pinned to the exact line (N3).IR RS: both lines carry'IR RS'as one word (N4).ab: both remediations print bare commands, and theadd` runs (N6).tests/test_readme_windows_quoting_sentence.pyties the sentence's verbs to the code (N5).NAMEDis re-counted for the new helpers (_family_line3,_add_command1,_unknown_members_repository2). A newFAMILY_LINESsays what each function prints, and a newfamily_line_verbsreads each_family_linecall's verb from the source.test_the_kinds_of_family_lines_are_the_ones_the_sentence_listschecks those kinds against the verbs README lists forfamily.py(NEXT,switch -c,add,cd,git rm -r --cached,bump,remove), no more and no fewer, and refuses a verb that is not a string constant. A second test checks the reader on a small source. README and the handbook are not touched.family_lead(cwd=None)is a new helper intests/test_family_printed_quoting.py: the interpreter and the path tofamily.pyfrom where the refused run stood.tests/test_bump_remedies_family_root.py,tests/test_family.py,tests/test_family_add_member_id.py,tests/test_family_bump_to_checks.pyandtests/test_remove_add_remedies_family_root.pyexpect it where they expected a barefamily.py; that last file'sbump-member-unknowncase now expects the org filled in.andoror. Read on the syntax tree: the new file has 43 asserts and the sentence file gains 7, none of them composite. The diff adds no non-ASCII byte.Rows that fail on #1117's head
10dc011c9'sfamily.pywas put under the committed tests ofeac177af9(git show 10dc011c9:scripts/family.py > scripts/family.py), withtests/test_family.pynarrowed to its one changed test, and the 8 changed or new files run throughrun9.shwith-rA: 90 failed, 142 passed in 148.64s (logs/rows-parent.log; the failing ids are inlogs/rows-parent-failed.txt).family.pywas restored and its sha256 checked afterwards. The same 232 rows pass on the head, inside the gates below. The rows were not run against main'sfamily.py: this PR's base is #1117's branch.tests/test_remove_add_remedies_family_root.pytest_the_line_a_remedy_names_carries_the_holder_whole,..._parsesand..._runs_as_printed_and_does_what_it_says, 15 each: 5 holders (plain,my work,Dana's work,relative,symlink) x the casesadd-member-duplicate-id,bump-member-duplicate-id,bump-member-unknowntests/test_bump_remedies_family_root.pytest_the_bump_a_remedy_names_carries_the_holder_whole,..._parsesand..._runs_as_printed, 6 each: 3 holders (plain,my work,Dana's work) xmember-already-present,member-duplicate-idtests/test_family_remedy_lines.py(new)test_member_unknowns_add_runs_as_printed_from_the_tracking_branch;test_member_already_presents_lines_run_as_printed_from_the_tracking_branch[bump]and[remove];test_member_unknowns_add_names_the_owner_where_it_is_known(all 7 params);test_a_rows_name_with_a_space_is_one_word_in_both_lines;test_a_command_holding_a_backtick_is_printed_baretests/test_family_add_member_id.pytest_a_linked_manifest_with_a_pinned_id_is_refused_by_the_mount_unwound,test_a_member_whose_id_changes_between_the_probe_and_the_mount_is_unwound,test_a_renamed_repository_is_refused_before_anything_is_mounted,test_a_section_already_at_the_mount_path_is_refused_first_then_the_id, andtest_each_command_the_refusal_names_runs_on_the_row_it_names[bump]and[remove]tests/test_family.pytest_add_refuses_a_spelling_of_a_member_the_family_already_pins[INKROUTER/IRRS],[InkRouter/irrs],[OtherOrg/Irrs],[inkrouter/irrs]tests/test_family_bump_to_checks.pytest_a_duplicate_id_behind_a_tag_is_refused_after_the_peel_line,test_a_duplicate_id_read_through_a_linked_manifest_is_put_back,test_bump_to_a_commit_recording_another_rows_id_is_refused_aliketests/test_readme_windows_quoting_sentence.pytest_the_functions_that_quote_for_the_host_are_the_ones_the_sentence_names[scripts/family.py](NAMEDno longer matches #1117'sfamily.py:_add_commandis counted 3 there, whereNAMEDnow says 1, and four more functions are counted) and the newtest_the_kinds_of_family_lines_are_the_ones_the_sentence_liststests/test_family_printed_quoting.pyfamily_leadhelper onlyMutants
19 of 19 killed. Each was applied alone to the committed tree with
mut/mutants1127.py(lane 5's scratchv124/n1127/mut/), run throughrun9.shwith-x -qon the named file, and restored byte for byte, with the sha256 checked by an assert in the driver.git status --porcelain --untracked-files=nowas empty after the last one (logs/mutants.log). With-xa run stops at its first failing row, so the table names that row, not every row that would fail. Every failure is anAssertionErrorat an assert of the named file, read inlogs/mut-<id>.log, never an import or syntax error. No survivor._family_linedrops the interpreter and the scripttest_member_unknowns_add_runs_as_printed_from_the_tracking_branch_family_linetest_a_rows_name_with_a_space_is_one_word_in_both_lines_family_linetest_a_command_holding_a_backtick_is_printed_bare_citedalways backtickstest_a_command_holding_a_backtick_is_printed_baretest_member_unknowns_add_runs_as_printed_from_the_tracking_branchtest_member_unknowns_add_runs_as_printed_from_the_tracking_branchtest_member_unknowns_add_names_the_owner_where_it_is_known[the-typed-owner]MEMBER_RE...owner_where_it_is_known[a-typed-owner-add-refuses]...owner_where_it_is_known[the-typed-owner]removeline inmember-already-present(#1117's words)test_member_already_presents_lines_run_as_printed_from_the_tracking_branch[bump]BRANCH_LEFTdropped frommember-unknowntest_member_unknowns_add_runs_as_printed_from_the_tracking_branchfamily.py)test_member_unknowns_add_runs_as_printed_from_the_tracking_branchmember-unknown'sswitch -csuggestsbump's branchtest_member_unknowns_add_runs_as_printed_from_the_tracking_branchmember-already-present'sswitch -csuggestsadd's branchtest_member_already_presents_lines_run_as_printed_from_the_tracking_branch[bump]member-unknown's clause names the wrong verbtest_member_unknowns_add_runs_as_printed_from_the_tracking_branchmember-already-present's row name raw in its linestest_a_rows_name_with_a_space_is_one_word_in_both_linesremovefrom the sentence (N5)test_the_kinds_of_family_lines_are_the_ones_the_sentence_lists_family_linecall of a verb the sentence does not list (N5)test_the_kinds_of_family_lines_are_the_ones_the_sentence_lists_family_linecall whose verb is not a constant (N5)test_the_kinds_of_family_lines_are_the_ones_the_sentence_listsM1 to M16 ran on
tests/test_family_remedy_lines.py, D1 to D3 ontests/test_readme_windows_quoting_sentence.py. D1 is the N5 measurement made into a mutant: on #1117's head the same documentation edit left that file at 44 passed.Runs
All through lane 5's
run9.shwithEMAILexported, run bygate-n1127.shfrom the main loop. EverySTARTline readshead=eac177af934b9606db83e5a31e62cd5a98b5270canddirty=0, or, for the hygiene run, the scratch commit below. The 17 files:tests/test_family_remedy_lines.py,tests/test_bump_remedies_family_root.py,tests/test_family_add_member_id.py,tests/test_family_bump_to_checks.py,tests/test_family_printed_quoting.py,tests/test_readme_windows_quoting_sentence.py,tests/test_remove_add_remedies_family_root.py,tests/test_family.py(the 8 this commit changes or adds), andtests/test_family_init_landing.py,tests/test_git_bash_dialect.py,tests/test_validate_family_branch_step.py,tests/test_validate_family_remedies_typed.py,tests/test_validate_family_duplicates.py,tests/test_family_dry_run.py,tests/test_family_members_not_a_list.py,tests/test_family_switch_branch_exists.py,tests/test_family_tracking_branch.py(the 9 that name the changed functions or codes).tests/test_repo_hygiene.py,tests/test_agents_md_codes.py,tests/test_cli_reference.py)logs/gate243.loggit version 2.47.1is the log's first line)logs/gate247.logtests/test_path_guard_hygiene.py, in the scratch worktreen1127-hyg-scratch: main961640c68with thisfamily.pycommitted there asea72f4b98(blobab41b42ec, the same aseac177af9's)logs/hyg243.logThe hygiene run is on a scratch commit because #1117's base,
6da757cbe, predates #1111'stests/test_path_guard_hygiene.py: the file is not ineac177af9, and it is in main. No failure and no skip in any run. The full suite was not run.Merge-trees
Read again on 2026-10-23 at about 11:10Z, with git 2.43.0 and no global git config, after
git ls-remoteread main at961640c68, #1117's branch at10dc011c9and this branch ateac177af9:git merge-tree --write-tree <other> eac177af9origin/main(961640c68)docs/handbook.html10dc011c90abd5f5d4fc2dad7961a984ef018a9b5082593952c95docs/handbook.html74096eb47833docs/handbook.htmlremovethat bothmember-duplicate-idremedies name and theaddthatbump'smember-unknownnames carry--family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117's own, and this commit does not touch the handbook. family.py: theremovethat bothmember-duplicate-idremedies name and theaddthatbump'smember-unknownnames carry--family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117 alone (git merge-tree --write-tree <other> 10dc011c9) conflicts indocs/handbook.htmland nowhere else against main, validate-manifest.py, validate-family.py, family.py and update-shape.py: anid:, a date or arepository:ending in a line feed is refused by name, and a pin'ssource_repository:that is no path orowner/reponame is named as the pin's (#1139) #1166 and family.py and the family root's siblings.py: a holder's.gitmodulesthis user cannot read, as through a link into a folder it cannot search, isgitmodules-unreadable, never no section forremove's rollback or no url formake siblings(#1110) #1164, and is clean against family.py: a failed remove's by-hand lines spell a leg mounted at a path that is not UTF-8 as a printf word of octal escapes ending in a slash, sogit -C <member>/<leg>andsubmodule update -- <leg>typed in any POSIX shell name that leg (#1086) #1098.git diff --name-only 10dc011c9 eac177af9names no file underdocs/..gitmodulesthis user cannot read, as through a link into a folder it cannot search, isgitmodules-unreadable, never no section forremove's rollback or no url formake siblings(#1110) #1164's head moved to74096eb47833, from3162b523be, after the gate run. The gate run's merge-tree was at3162b523beand read the same: one conflict,docs/handbook.html.gh pr list --state open --limit 100 --json number,headRefName,headRefOid,files. Four touchscripts/family.py: validate-manifest.py, validate-family.py, family.py and update-shape.py: anid:, a date or arepository:ending in a line feed is refused by name, and a pin'ssource_repository:that is no path orowner/reponame is named as the pin's (#1139) #1166, family.py and the family root's siblings.py: a holder's.gitmodulesthis user cannot read, as through a link into a folder it cannot search, isgitmodules-unreadable, never no section forremove's rollback or no url formake siblings(#1110) #1164, family.py: theremovethat bothmember-duplicate-idremedies name and theaddthatbump'smember-unknownnames carry--family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117 and family.py: a failed remove's by-hand lines spell a leg mounted at a path that is not UTF-8 as a printf word of octal escapes ending in a slash, sogit -C <member>/<leg>andsubmodule update -- <leg>typed in any POSIX shell name that leg (#1086) #1098. Of this commit's 9 files, family.py: theremovethat bothmember-duplicate-idremedies name and theaddthatbump'smember-unknownnames carry--family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117 touches 5 (scripts/family.pyand four test files); no other PR touches a test file of this commit's. Every one of those heads is above.Restacked after #1117 landed
#1117 landed on 2026-11-08 at 19:15:56Z as
9252ef208, from its final head8cb9c729a(a merge of main into its branch after219e57c28). Lane 5's pilot restacked this branch at 19:31Z with two real merges and no rebase:85909e973merges family.py: theremovethat bothmember-duplicate-idremedies name and theaddthatbump'smember-unknownnames carry--family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117's final head8cb9c729a.24b6a0af6merges family.py: theremovethat bothmember-duplicate-idremedies name and theaddthatbump'smember-unknownnames carry--family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117's squash9252ef208, taking this branch's side for the five files the squash and the branch both carry:scripts/family.py,tests/test_family_add_member_id.py,tests/test_family_bump_to_checks.py,tests/test_readme_windows_quoting_sentence.pyandtests/test_remove_add_remedies_family_root.py.No third merge was needed: main was still
9252ef208. The new head24b6a0af6differs from main in exactly this PR's 9 files, 638 insertions and 74 deletions. That change is byte for byte the one rv1201 found READY:git diff 10dc011c9 eac177af9andgit diff 9252ef208 24b6a0af6have the same+and-lines and the same patch-id,ceddd1c4ddae. Its tree,85563cdb, is the one the pilot's dry run predicted.Limits
initremedies are left as follow-ups.scripts/family.pylines 558 (Family.open'sfamily-root-missing) and 10386 (cmd_siblings'sfamily-root-missing) say`family.py init --org <org> --family <Name>`with a barefamily.py._family_linealways prints--family-root, whichinitdoes not take, so they need a spelling of their own.member-ambiguous's remedy is left as a follow-up. It is raised at line 607 and says "removetakes the extra one out" with no line to type, the shape N7 had inmember-already-present. It is not in the issue.add --dry-run'smember-duplicate-id: under--dry-run,cmd_addskips_checked_branchbut still asks_checked_member_id, and theremoveline its remedy prints, typed on the tracking branch, isREFUSED family-on-tracking-branch. (b)bump --dry-run'smember-duplicate-id, the same way through_bump_dry_run. (c)init'sfamily-holder-already-hereprints_add_command's line between plain backticks, with no branch clause and not through_cited, so it is refused typed on the holderinitjust made, and a folder name that holds a backtick ends the span early. Each chain still ends in a refusal that prints its ownswitch -c. They are left as follow-ups, with rv1201's three NITs.org:is the likeliest owner, not a certain one. A member may live in another organisation (MEMBER_RE), and theaddof a repository that is not there is refused before it touches the holder.<org>stays blank where no owner is a--memberthataddtakes.removethat bothmember-duplicate-idremedies name and theaddthatbump'smember-unknownnames carry--family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117's head is expected to move before it lands (its handbook conflict with main is its own to mend). When it does, this branch takes a merge of the new head, never a rebase, and the gates run again. It did: see "Restacked after family.py: theremovethat bothmember-duplicate-idremedies name and theaddthatbump'smember-unknownnames carry--family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117 landed".ast.dump,repr(orsys.version, so no assert reads text that depends on the Python version.removethat bothmember-duplicate-idremedies name and theaddthatbump'smember-unknownnames carry--family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117, under his v1.2.1 merge word of 2026-10-23T13:10:26Z.Closes #1127
Claim: #1127 (comment)
Lane: openreposhape-5 (openRepoShape-5)
🤖 Generated with Claude Code
Summary by Sourcery
Make family.py remediation guidance directly executable and consistent across membership errors.
New Features:
Bug Fixes:
Enhancements:
Tests: