Skip to content

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

Draft
brettheap wants to merge 6 commits into
mainfrom
fix/1127-family-followups-stacked

Conversation

@brettheap

@brettheap brettheap commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Goal

#1127: the add, bump and remove lines that family.py's remedies name are one spelling, _family_line, and each line a real add, bump or remove prints runs as printed, including from the branch a fresh clone stands on, as do the dry runs' member-unknown and member-already-present lines (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, and member-already-present prints its remove beside its bump.

Was stacked on #1117 at 10dc011c9, and is restacked after #1117 landed as 9252ef20 (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 961640c6 and #1117's head is 10dc011c9. The probe is tests/test_zz_probe1127.py, run untracked through run9.sh (stub gh, 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 scratch v124/n1127/ (probe/, logs/probe-main.log, logs/probe-parent.log, logs/n5-measure-main.log, logs/n5-measure-parent.log).

item on main on #1117's head
N1 member-unknown's add, typed on the tracking branch not in that form: the line has no --family-root, so typed from a holder it exits 2 with argparse's the following arguments are required: --family-root, on any branch holds: add --family-root <holder> --member <org>/IRQS from a holder on main exits 2 family-on-tracking-branch
N1b (found by measuring) member-already-present's bump, typed on the tracking branch holds holds: add --member InkRouter/IRRS on main is refused member-already-present; its bump line, with a real commit, exits 2 family-on-tracking-branch. add asks _checked_add_target before _checked_branch
N2 a bare family.py holds: the member-unknown and member-already-present lines start family.py holds, and so do #1117's two remove lines
N3 <org> left blank holds: family.yaml says org: InkRouter, the line says <org>/IRQS holds
N4 --member printed raw holds: with the row's project hand-edited to IR RS, the bump line reads --member IR RS, which shlex splits into IR and RS holds
N5 the README sentence's verbs tied to nothing holds: dropping bump from README and the handbook leaves tests/test_readme_windows_quoting_sentence.py at 44 passed holds: dropping remove leaves it at 44 passed
N6 a backtick in the holder's path holds: the first backticked span of the remedy is family.py bump --family-root '<...>/a, cut at the path's backtick holds
N7 remove takes it out, with no line holds: the member-already-present remedy says "remove takes it out" and prints no remove line holds, the same text

So both kinds are present. N1 exists only in #1117's add line, since main's has no --family-root to 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):

item what holds now pinned by mutants
N1 member-unknown's remedy opens with the branch clause and the git switch -c; typed in order from the tracking branch, the switch -c then the add exit 0 test_member_unknowns_add_runs_as_printed_from_the_tracking_branch M5, M11, M13, M15
N1b the same for member-already-present's bump and remove test_member_already_presents_lines_run_as_printed_from_the_tracking_branch (bump, remove) M14
N2 every line begins with the interpreter and the script's path the new file's rows, and the rows of five existing files that now read that lead M1, M12
N3 the owner is the typed one, else family.yaml's org:, else the blank test_member_unknowns_add_names_the_owner_where_it_is_known (7 params) M6, M7, M8, M9
N4 the row's name is one quoted word test_a_rows_name_with_a_space_is_one_word_in_both_lines M2, M16
N5 the README sentence's verbs are tied to NAMED and to the verbs _family_line is called with test_the_kinds_of_family_lines_are_the_ones_the_sentence_lists D1, D2, D3
N6 a command holding a backtick is printed bare test_a_command_holding_a_backtick_is_printed_bare M3, M4
N7 member-already-present prints the remove line the [remove] param of the N1b row M10

The code, in scripts/family.py:

  • One spelling for the lines, _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 by shell_quoted for the reader's shell. That is how init's NEXT STEPS' add was already printed (_add_command, family.py and siblings.py print commands with unquoted paths, so under a folder with a space the member-git-dir-cached remedy's rm -rf would delete the wrong directory, and add's push line fails #486), now shared. _add_command is built on it and prints the same bytes.
    • It is called for bump's member-unknown (its add), add's member-already-present (its bump and remove), add's member-duplicate-id (its remove and bump) and bump's member-duplicate-id (its remove).
    • The row's project: is passed as a word, so a row IR RS prints 'IR RS', one word (N4).
    • A placeholder (<commit>, <org>/...) is added after the line, bare: quoted, it would read as a value.
  • The branch the line runs on, _off_the_tracking_branch (N1, N1b). member-unknown and member-already-present are refused before _checked_branch reads the branch, so a person on the tracking branch, where init and a fresh clone leave them, typed a line that was refused. Each remedy now opens with on a branch other than the holder's tracking branch, which <verb> refuse(s) (<git -C holder switch -c branch> makes one), , and ends with BRANCH_LEFT (family.py and validate-family.py: the git 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, branch family/add-<project>.
    • member-already-present: `bump` and `remove` refuse, branch family/bump-<id>. The issue named only the add (N1); this is N1b.
    • The words are true on any branch: the refusal is said before the branch is read, and reading it there could refuse in its place. The two member-duplicate-id remedies name no branch: a real add or bump asks them after _checked_branch.
  • The owner of member-unknown's add, _unknown_members_repository (N3). The owner --member was typed with (<org>/<project>), else family.yaml's org:, else the blank <org>. Each is used only where MEMBER_RE.fullmatch("<owner>/<project>"), so an empty org or an org holding a space leaves the blank, and a typed owner add refuses falls through to the family's org. The value is one word for the shell.
  • A command holding a backtick is printed bare, _cited (N6). Every shell reads the line the same either way.
  • member-already-present prints the remove line beside the bump line (N7). `<bump line> --to <commit>` moves an existing member and `<remove line>` takes it out.

The remedy of bump --member IRQS on a family whose org: 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_LEFT follows it.

before (#1117's head):
Remediation: `family.py add --family-root <holder> --member <org>/IRQS` puts it in the family first.

after:
Remediation: on a branch other than the holder's tracking branch, which `add` refuses (`git -C <holder> switch -c family/add-irqs` makes one), `python3 <path to family.py> add --family-root <holder> --member InkRouter/IRQS` puts it in the family first.

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, as tests/test_family_tracking_branch.py does, 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; the add typed alone is refused family-on-tracking-branch; its switch -c then the add exit 0, pin IRQS on family/add-irqs, leave the holder clean and validate-family.py green (N1, N2).
    • member-already-present, for bump and for remove: the same, with the pin moved to main~1 or the row gone (N1b, N7).
    • The owner, 7 params (the family's org; a typed owner; a typed owner add refuses; no org; an empty org; an org holding a space; no org but a typed owner), each pinned to the exact line (N3).
    • A row IR RS: both lines carry 'IR RS' as one word (N4).
    • A holder under a folder ab: both remediations print bare commands, and the add` runs (N6).
  • tests/test_readme_windows_quoting_sentence.py ties the sentence's verbs to the code (N5). NAMED is re-counted for the new helpers (_family_line 3, _add_command 1, _unknown_members_repository 2). A new FAMILY_LINES says what each function prints, and a new family_line_verbs reads each _family_line call's verb from the source. test_the_kinds_of_family_lines_are_the_ones_the_sentence_lists checks those kinds against the verbs README lists for family.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.
  • Five existing test files read the new lead. family_lead(cwd=None) is a new helper in tests/test_family_printed_quoting.py: the interpreter and the path to family.py from 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.py and tests/test_remove_add_remedies_family_root.py expect it where they expected a bare family.py; that last file's bump-member-unknown case now expects the org filled in.
  • No assert joins conditions with and or or. 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's family.py was put under the committed tests of eac177af9 (git show 10dc011c9:scripts/family.py > scripts/family.py), with tests/test_family.py narrowed to its one changed test, and the 8 changed or new files run through run9.sh with -rA: 90 failed, 142 passed in 148.64s (logs/rows-parent.log; the failing ids are in logs/rows-parent-failed.txt). family.py was 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's family.py: this PR's base is #1117's branch.

file failed of run the rows
tests/test_remove_add_remedies_family_root.py 45 of 45 test_the_line_a_remedy_names_carries_the_holder_whole, ..._parses and ..._runs_as_printed_and_does_what_it_says, 15 each: 5 holders (plain, my work, Dana's work, relative, symlink) x the cases add-member-duplicate-id, bump-member-duplicate-id, bump-member-unknown
tests/test_bump_remedies_family_root.py 18 of 21 test_the_bump_a_remedy_names_carries_the_holder_whole, ..._parses and ..._runs_as_printed, 6 each: 3 holders (plain, my work, Dana's work) x member-already-present, member-duplicate-id
tests/test_family_remedy_lines.py (new) 12 of 12 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_bare
tests/test_family_add_member_id.py 6 of 14 test_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, and test_each_command_the_refusal_names_runs_on_the_row_it_names[bump] and [remove]
tests/test_family.py 4 of 4 test_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.py 3 of 6 test_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_alike
tests/test_readme_windows_quoting_sentence.py 2 of 46 test_the_functions_that_quote_for_the_host_are_the_ones_the_sentence_names[scripts/family.py] (NAMED no longer matches #1117's family.py: _add_command is counted 3 there, where NAMED now says 1, and four more functions are counted) and the new test_the_kinds_of_family_lines_are_the_ones_the_sentence_lists
tests/test_family_printed_quoting.py 0 of 84 gains the family_lead helper only

Mutants

19 of 19 killed. Each was applied alone to the committed tree with mut/mutants1127.py (lane 5's scratch v124/n1127/mut/), run through run9.sh with -x -q on the named file, and restored byte for byte, with the sha256 checked by an assert in the driver. git status --porcelain --untracked-files=no was empty after the last one (logs/mutants.log). With -x a run stops at its first failing row, so the table names that row, not every row that would fail. Every failure is an AssertionError at an assert of the named file, read in logs/mut-<id>.log, never an import or syntax error. No survivor.

mutant what it does result first failing row
M1 _family_line drops the interpreter and the script killed, rc 1 test_member_unknowns_add_runs_as_printed_from_the_tracking_branch
M2 words unquoted in _family_line killed, rc 1 test_a_rows_name_with_a_space_is_one_word_in_both_lines
M3 holder unquoted in _family_line killed, rc 1 test_a_command_holding_a_backtick_is_printed_bare
M4 _cited always backticks killed, rc 1 test_a_command_holding_a_backtick_is_printed_bare
M5 no branch clause killed, rc 1 test_member_unknowns_add_runs_as_printed_from_the_tracking_branch
M6 org blank always killed, rc 1 test_member_unknowns_add_runs_as_printed_from_the_tracking_branch
M7 typed owner ignored killed, rc 1 test_member_unknowns_add_names_the_owner_where_it_is_known[the-typed-owner]
M8 owner not checked against MEMBER_RE killed, rc 1 ...owner_where_it_is_known[a-typed-owner-add-refuses]
M9 typed owner after the family's org killed, rc 1 ...owner_where_it_is_known[the-typed-owner]
M10 no remove line in member-already-present (#1117's words) killed, rc 1 test_member_already_presents_lines_run_as_printed_from_the_tracking_branch[bump]
M11 BRANCH_LEFT dropped from member-unknown killed, rc 1 test_member_unknowns_add_runs_as_printed_from_the_tracking_branch
M12 script named bare (#1117's family.py) killed, rc 1 test_member_unknowns_add_runs_as_printed_from_the_tracking_branch
M13 member-unknown's switch -c suggests bump's branch killed, rc 1 test_member_unknowns_add_runs_as_printed_from_the_tracking_branch
M14 member-already-present's switch -c suggests add's branch killed, rc 1 test_member_already_presents_lines_run_as_printed_from_the_tracking_branch[bump]
M15 member-unknown's clause names the wrong verb killed, rc 1 test_member_unknowns_add_runs_as_printed_from_the_tracking_branch
M16 member-already-present's row name raw in its lines killed, rc 1 test_a_rows_name_with_a_space_is_one_word_in_both_lines
D1 README and the handbook drop remove from the sentence (N5) killed, rc 1 test_the_kinds_of_family_lines_are_the_ones_the_sentence_lists
D2 a _family_line call of a verb the sentence does not list (N5) killed, rc 1 test_the_kinds_of_family_lines_are_the_ones_the_sentence_lists
D3 a _family_line call whose verb is not a constant (N5) killed, rc 1 test_the_kinds_of_family_lines_are_the_ones_the_sentence_lists

M1 to M16 ran on tests/test_family_remedy_lines.py, D1 to D3 on tests/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.sh with EMAIL exported, run by gate-n1127.sh from the main loop. Every START line reads head=eac177af934b9606db83e5a31e62cd5a98b5270c and dirty=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), and tests/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).

run git result log
the 17 files and the trio (tests/test_repo_hygiene.py, tests/test_agents_md_codes.py, tests/test_cli_reference.py) 2.43 953 passed in 656.46s logs/gate243.log
the 17 files 2.47.1 (git version 2.47.1 is the log's first line) 783 passed in 613.17s logs/gate247.log
tests/test_path_guard_hygiene.py, in the scratch worktree n1127-hyg-scratch: main 961640c68 with this family.py committed there as ea72f4b98 (blob ab41b42ec, the same as eac177af9's) 2.43 49 passed in 16.91s logs/hyg243.log

The hygiene run is on a scratch commit because #1117's base, 6da757cbe, predates #1111's tests/test_path_guard_hygiene.py: the file is not in eac177af9, 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-remote read main at 961640c68, #1117's branch at 10dc011c9 and this branch at eac177af9:

git merge-tree --write-tree <other> eac177af9 exit result
origin/main (961640c68) 1 one conflict, docs/handbook.html
#1117, 10dc011c9 0 clean, tree 0abd5f5d4fc2
#1098, dad7961a98 0 clean, tree 4ef018a9b508
#1166, 2593952c95 1 one conflict, docs/handbook.html
#1164, 74096eb47833 1 one conflict, docs/handbook.html

Restacked after #1117 landed

#1117 landed on 2026-11-08 at 19:15:56Z as 9252ef208, from its final head 8cb9c729a (a merge of main into its branch after 219e57c28). Lane 5's pilot restacked this branch at 19:31Z with two real merges and no rebase:

  1. 85909e973 merges family.py: the remove that both member-duplicate-id remedies name and the add that bump's member-unknown names carry --family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117's final head 8cb9c729a.
  2. 24b6a0af6 merges family.py: the remove that both member-duplicate-id remedies name and the add that bump's member-unknown names carry --family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117's squash 9252ef208, 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.py and tests/test_remove_add_remedies_family_root.py.

No third merge was needed: main was still 9252ef208. The new head 24b6a0af6 differs 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 eac177af9 and git diff 9252ef208 24b6a0af6 have the same + and - lines and the same patch-id, ceddd1c4ddae. Its tree, 85563cdb, is the one the pilot's dry run predicted.

Limits

  • The init remedies are left as follow-ups. scripts/family.py lines 558 (Family.open's family-root-missing) and 10386 (cmd_siblings's family-root-missing) say `family.py init --org <org> --family <Name>` with a bare family.py. _family_line always prints --family-root, which init does 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 "remove takes the extra one out" with no line to type, the shape N7 had in member-already-present. It is not in the issue.
  • Three printed lines name no branch, so typed on the tracking branch they are refused (rv1201's review, issuecomment-6372152028, SHOULD-1). (a) add --dry-run's member-duplicate-id: under --dry-run, cmd_add skips _checked_branch but still asks _checked_member_id, and the remove line its remedy prints, typed on the tracking branch, is REFUSED family-on-tracking-branch. (b) bump --dry-run's member-duplicate-id, the same way through _bump_dry_run. (c) init's family-holder-already-here prints _add_command's line between plain backticks, with no branch clause and not through _cited, so it is refused typed on the holder init just made, and a folder name that holds a backtick ends the span early. Each chain still ends in a refusal that prints its own switch -c. They are left as follow-ups, with rv1201's three NITs.
  • The family's org: is the likeliest owner, not a certain one. A member may live in another organisation (MEMBER_RE), and the add of a repository that is not there is refused before it touches the holder. <org> stays blank where no owner is a --member that add takes.
  • The clause says "a branch other than the tracking branch". It is worded to be true on any branch, since the refusal is said before the branch is read, so a person already off the tracking branch reads a clause that does not apply to them.
  • family.py: the remove that both member-duplicate-id remedies name and the add that bump's member-unknown names 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: the remove that both member-duplicate-id remedies name and the add that bump's member-unknown names carry --family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117 landed".
  • The runs are on the bench's Python 3.12; CI runs 3.14. No test line this commit adds uses ast.dump, repr( or sys.version, so no assert reads text that depends on the Python version.
  • A post-tag DRAFT. Brett Heap ruled on 2026-10-21T10:32:04Z to leave the follow-ups out of v1.2.0. It is on the v1.2.1 milestone, and it lands after its base family.py: the remove that both member-duplicate-id remedies name and the add that bump's member-unknown names 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:

  • Standardize family remediation commands so they include the interpreter, script path, family root, and shell-safe arguments.
  • Provide actionable branch guidance and complete add, bump, and remove remedies for family membership errors.
  • Infer a likely member organization for unknown-member add commands when available.

Bug Fixes:

  • Ensure remediation commands remain executable when paths contain spaces or backticks and when member names contain spaces.
  • Include the missing remove command in member-already-present guidance.

Enhancements:

  • Tie family command-line quoting tests to the documented set of family.py command types.

Tests:

  • Add coverage verifying remediation commands parse and run from tracking branches, preserve owners and names, handle unusual paths, and cover the new remove guidance.

Brett Heap and others added 3 commits October 21, 2026 13:09
… 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>
@sourcery-ai

sourcery-ai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Implementation

Centralize 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.

  • scripts/family.py L2285-2336, L2341-2373, L9477-9490 (+6 small edits)

Tests

Verify the emitted remedy commands, their shell quoting and branch behavior, organization selection, and the README sentence’s coverage of printed command kinds.

  • tests/test_bump_remedies_family_root.py L51, L111-118, L223
  • tests/test_family.py L51, L543-547
  • tests/test_family_add_member_id.py L74-77, L86-90 (+2 small edits)
  • tests/test_family_bump_to_checks.py L63, L87-88, L98-99
  • tests/test_family_printed_quoting.py L108-118
  • tests/test_family_remedy_lines.py L1-373
  • tests/test_readme_windows_quoting_sentence.py L69-73, L79-94, L361-417 (+1 small edits)
  • tests/test_remove_add_remedies_family_root.py L219-226 (+6 small edits)

Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

Independent review of #1201 (#1127) for lane openRepoShape-5

READY at eac177af9

No MUST. One SHOULD, which is a change to the PR body only and does not move the head. Three NITs for follow-ups.

Findings

SHOULD-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 prints

The 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:

  • (a) add --dry-run's member-duplicate-id.
    • Under --dry-run, cmd_add skips _checked_branch (scripts/family.py:7586). It still asks _checked_member_id (:7606) before the dry run leaves (:7610).
    • So on main, add --dry-run --member InkRouter/IRRS2 (a renamed copy) is refused member-duplicate-id. Its remedy names no branch, and its remove line typed in bash is REFUSED family-on-tracking-branch (probe p6).
  • (b) bump --dry-run's member-duplicate-id.
    • _bump_dry_run asks _checked_bumped_id (:9245), and nothing asks the branch first.
    • Its remove line typed on main is REFUSED family-on-tracking-branch (probe p7).
  • (c) init's family-holder-already-here.
    • Its remedy (:2503) prints _add_command's line, which is now _family_line's spelling, between plain backticks. It has no branch clause and does not go through _cited.
    • Typed on the holder init just made, which stands on main, the add is REFUSED family-on-tracking-branch (probe p9[plain]).
    • Under a folder named ab, the first backticked span ends at the path's backtick: `` python3 .../family.py add --family-root '/.../a`` (probep9[ab]). That is N6's shape.

RV1117's N1 and #1127 item 1 already noted (a) and (b): "The two remove lines can be reached there only from --dry-run." The code comments at :6674-6675 and :8724-8725 say "A real add/bump asks this after _checked_branch". That is true of a real run only. None of the three is in Limits, which names only the family-root-missing init lines (:558, :10386) and member-ambiguous.

Fix (body only, enough to land):

  • Narrow the Goal to the real runs' remedies plus the dry runs of member-unknown and member-already-present.
  • Add a Limits bullet naming (a), (b) and (c) as follow-ups. Each chain still ends in a refusal that prints its own switch -c.

Optional code fix: prefix _off_the_tracking_branch and BRANCH_LEFT to the two member-duplicate-id remedies when args.dry_run is set, and print (c) through _cited with the clause.

NIT-1: mutant R5 survives because the fixture's ids equal the names

  • member-already-present builds its branch from the row's id:: _suggested_branch("bump", str(held.get("id") or name)) at scripts/family.py:6542.
  • Every fixture row's id is its project lowercased, so my mutant R5, which builds it from name, passes all 12 rows of tests/test_family_remedy_lines.py.
  • The mutant is harmless, since any branch other than the tracking branch works.
  • Fix: in test_a_rows_name_with_a_space_is_one_word_in_both_lines (tests/test_family_remedy_lines.py:318), also assert the switch -c line. That row's project is IR RS and its id is irrs, so the id gives family/bump-irrs and the name gives family/bump-member.

NIT-2: the rows check the interpreter's name but do not run it

  • typed() (tests/test_family_remedy_lines.py:152-157) asserts words[0] == PYTHON and then runs the script by sys.executable (run_script). The rows never look the printed interpreter up on PATH.
  • I typed the lines through real shells with python3 taken from PATH (below): 12 of 12 passed. So there is no defect, only a gap in what the rows prove.

NIT-3: remove lands on a branch named bump

  • member-already-present suggests one branch, family/bump-<id>, for both its bump and its remove lines. A remove made from it lands on a branch with bump in its name, where remove's own refusal would suggest family/remove-<id>.
  • It is harmless. Leave it, or mention it.

Restack watch (#1117, then three merges)

Checked and true

All runs used $Q/v110/run9.sh (stub gh, GH_TOKEN=bogus, empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null) with EMAIL=service@opensoft.dev. Every START line reads head=eac177af9... and dirty=0, except where noted.

Gates

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 init warns 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's scripts/family.py (blob bea9abd47) under eac177af9'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 AssertionError at a test file's assert; none is an import error.
  • Restored: sha256 b8199379... before and after, git status clean.

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's switch -c then add, on main. Typed through /bin/bash, /bin/dash and /usr/bin/zsh, with python3 from PATH, from a folder my desk ü. Holders were under Dana's work and under café ’q $HOME;x (with a U+2019 apostrophe). The add typed alone is REFUSED family-on-tracking-branch. After the switch it exits 0, lands on family/add-irqs, and the holder's gate is green. 6 of 6.
  • member-already-present's bump and remove, the same way. 6 of 6. The pin moved to main~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, no PWNED file is created, and family.py refuses it as unsafe-value. 3 of 3.
  • Detached holder. The clause's switch -c, then the add, exit 0; typed alone, the add is REFUSED 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" gives Ink.Router/IRQS.
    • A typed a/b/IRQS gives InkRouter/IRQS.
    • A typed inkrouter/IRQS is kept as typed.
    • org: 123 and org: [a, b] give <org>/IRQS.
  • The template writes org: "{{ORG}}", quoted, so org: is always a string unless edited by hand.

PowerShell, in process

  • With repo_shape.host_shell_dialect forced to nt and PYTHON set to python, powershell_words reads _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: python on Windows and under Git Bash, python3 elsewhere. No py and no python3 on Windows, as validate-family.py and _add_command already print it.

Claims checked by reading or a small script

  • _add_command prints the same bytes as family.py: the remove that both member-duplicate-id remedies name and the add that bump's member-unknown names 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_line 3, _add_command 1, _unknown_members_repository 2) match the code.
  • The _family_line verbs are bump, remove and add. 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 remove refuses" is the same as validate-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 and or or.
  • The diff adds no non-ASCII line, and no added test line uses ast.dump, repr(, sys.version or !r.
  • The Limits line numbers are right: 558, 607 and 10386.

Docs

Merge-trees

CI and PR state

  • CI on eac177af9: tests, tests-git-bash, package and SonarCloud all SUCCESS.
  • The PR is OPEN, draft, MERGEABLE, CLEAN.

Not checked

(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)

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-23T20:36:20Z) Follow-up filed: #1228, rv1201's SHOULD-1 code fix (three printed lines that name no branch) and its NITs 1 to 3 (issuecomment-6372152028); it applies once #1201 has landed. Nothing in it blocks #1201.

Lane: openreposhape-5 (openRepoShape-5)

brettheap added a commit that referenced this pull request Nov 8, 2026
…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>
brettheap pushed a commit that referenced this pull request Nov 8, 2026
…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>
Base automatically changed from fix/1005-family-remove-add-family-root to main November 8, 2026 19:15
Brett Heap and others added 2 commits November 8, 2026 19:31
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>
@sonarqubecloud

sonarqubecloud Bot commented Nov 8, 2026

Copy link
Copy Markdown

This branch has not been deployed

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

Labels

None yet

Projects

None yet

1 participant