Skip to content

validate-family.py: a member row's id: that is no project id is member-id-malformed in every mode, and so is a mounted member's own project.yaml id:, with the bump that writes the row from that file (#1204) - #1273

Draft
brettheap wants to merge 17 commits into
mainfrom
fix/1204-family-member-id-pattern

Conversation

@brettheap

@brettheap brettheap commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #1227. This branch is stacked on #1227 (2d56c190), merged onto main 81eaf757: its first commit, ccc2c2d2, is a real merge of #1227's head into main (clean; tree 1e564383, the tree git merge-tree --write-tree 81eaf757 2d56c190 predicted), made before any change of this PR's, as lane 3 asked #1204 to build after #1227. That merge is the BASE every "base" below names. #1204 lands after #1227, and as the later lander it takes the refresh for lane 3's #1246 and lane 1's #1172 (and #1256, which has no refs/pull/1256/head on origin today), all of which change templates/family-root/scripts/validate-family.py. This PR's own change keeps out of #1227's lines (the module docstring at 64-110, the .gitmodules readers and _init_stop_said) and out of the loader _run_checks, where #1172 adds _refuse_an_unreadable_root.

Closes #1204

Outside v1.2.0 by Brett Heap's ruling of 2026-10-21T10:32:04Z ("leave the follow-ups out of v1.2.0").

What

validate-family.py asked PROJECT_ID_RE of one id only, the family's own. A member row's id: met two checks, and neither is a pattern: the mounted project.yaml compared with it as text (member-id-mismatch), and _check_member_ids_unique. So under --no-members, the run a private family's CI falls back to, an id that ends in a line feed, holds a space or opens with - was family ok; and with the members checked, so was a row that spelled the same malformed text as its member's own project.yaml: the row a family.py add older than #1166 wrote for a member whose own id: read "irqs\n".

Measured on base ccc2c2d2 and head d56f2084 (the head is now 0657e396, whose validate-family.py has the same code: round 1 below changes a docstring), Python 3.12.3, git 2.43.0, in the probe environment (a stub gh, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null, GIT_SSH_COMMAND=false), on tests/test_family.py's offline family (InkRouter, members IRRS and IRSS; table 3 rewrites IRRS's id: irrs line). The holder row mounts IRQS from a copy of its remote whose project.yaml reads id: "irqs\n", with its row written as pre-#1166 add wrote it (id: "irqs\n", the pin and digest those of the mount), since #1166's add now refuses that member.

row id: / holder base --no-members base members checked head --no-members head members checked
"irrs\n" exit 0, family ok exit 1, member-id-mismatch exit 1, member-id-malformed exit 1, member-id-malformed alone
"irrs\r" exit 0, family ok exit 1, member-id-mismatch exit 1, member-id-malformed exit 1, member-id-malformed alone
"IRRS Capital" exit 0, family ok exit 1, member-id-mismatch exit 1, member-id-malformed exit 1, member-id-malformed alone
"-x" exit 0, family ok exit 1, member-id-mismatch exit 1, member-id-malformed exit 1, member-id-malformed alone
"0" (control) exit 0, family ok exit 1, member-id-mismatch exit 0, family ok exit 1, member-id-mismatch (unchanged)
irrs (control, the fixture) exit 0, family ok exit 0 exit 0, family ok exit 0
the pre-#1166 holder, IRQS "irqs\n" both sides exit 0, family ok, 3 members exit 0 (and --pins exit 0); note project.yaml id irqs and a raw line feed exit 1, the row's finding exit 1, the row's finding and the member's own (--pins the same)

The two findings at head, the holder's path written <holder>:

FINDING member-id-malformed: IRRS: the row records id 'irrs\n', which does not match ^[a-z0-9][a-z0-9-]*$, so it names no project. The member's own project.yaml `id:` is the source this row pins. From a checkout of openRepoShape, with the member checked out, and on a branch other than the holder's tracking branch, which `bump` refuses (`git -C <holder> switch -c family/bump-member` makes one), `python3 scripts/family.py bump --family-root <holder> --member IRRS --to a3ee599077780f964bf0c17acd5242ebfdf9f95b` writes the row's `id:` from that file at the commit the row pins, and the pin stays where it is. Where that file's `id:` is no project id either, `bump` refuses it as `member-bad-id`: mend it in the member's own repository first, and give `--to` that commit. If `git switch -c` says that branch already exists, [...]

FINDING member-id-malformed: members/IRQS/project.yaml: the member's own `id:` is 'irqs\n', which does not match ^[a-z0-9][a-z0-9-]*$, so the tree mounted at members/IRQS is no project, and `family.py add` and `bump` refuse it as `member-bad-id`. That file is the source every row pins, so it is mended in the member's own repository, whose validate-manifest.py refuses it as `manifest-id`; then, from a checkout of openRepoShape and on a branch other than the holder's tracking branch, which `bump` refuses (`git -C <holder> switch -c family/bump-member` makes one), `python3 scripts/family.py bump --family-root <holder> --member IRQS --to <commit>` pins the mended commit and writes the row's `id:` from it. If `git switch -c` says that branch already exists, [...]

([...] is BRANCH_LEFT, word for word, as every remedy that names a switch -c ends.)

Why

The decision. The issue's two checks, under one code of their own, member-id-malformed, as member-bad-id is add's; and the remedy is bump to the commit the row pins, not the issue's remove and then add.

Rejected:

  • The issue's remedy, family.py remove and then add, runnable as printed. Measured on the base, on a hand-edited row id: "irrs\n" (committed, holder on its work branch): remove --member IRRS exits 0 and prints its NOTE that the member's object store is left under .git/modules/members/IRRS on purpose; add --member InkRouter/IRRS --at <the pinned commit> then exits 2, REFUSED member-git-dir-cached: <holder>/.git/modules/members/IRRS still holds the object store of a previously removed IRRS, and git submodule add will not write over it. The two lines as printed do not run in a holder with the member checked out, add clones the member again, and without --at it moves the pin to the member's tip. bump --to <the pinned commit>, measured on the same row: exit 0, one commit, the row id: irrs, the gitlink unmoved, validate-family.py exit 0. bump reads the member's own project.yaml at that commit and asks its id as add does (member-bad-id), so it repairs the row from the source and refuses where the source itself is malformed.
  • A second code for the member's own file. One code, one sentence each: the finding names which file (IRRS: for the row, members/IRQS/project.yaml: for the member's own), and AGENTS.md names one code.
  • Skipping falsy ids, as _check_member_ids_unique does. id: "" is no id; it is refused (mutant M6 below).

How:

  • templates/family-root/scripts/validate-family.py: _is_project_id, _bump_to (the bump line, POSIX-quoted as THE LOCKSTEP RULE's) and _check_member_ids_well_formed, beside _check_member_ids_unique; one call in _check_members; _check_member_identity takes root (its one caller, _check_member_checkout, has it) and asks the member's own id. The module docstring's WHAT IS CHECKED gains one bullet after member-row's, away from validate-family.py: the check for values git dies on reads the index's .gitmodules beside the work tree's and its stage remedies end git add -- .gitmodules, an index file git cannot parse is refused, and an unmerged one is read as none (#1150) #1227's lines. Each remedy's prose ends with BRANCH_LEFT in the same string as its _switch_line, as tests/test_family_switch_branch_exists.py's guard reads the source. The --to of the row's remedy is the row's pin.commit where COMMIT_RE.fullmatch takes it, and <commit> otherwise.
  • AGENTS.md: item 4 of the family section names member-id-malformed, on its own line, so the file stays at its 494-line cap (tests/test_repo_hygiene.py), and tests/test_agents_md_codes.py holds the code to the tool. The passages tests/test_family_docs_one_spelling.py pins are untouched.
  • tests/test_value_pattern_line_break.py: ID_READERS counts 2 fullmatch readers in validate-family.py, the family's id and _is_project_id.

Rows

tests/test_validate_family_member_id_pattern.py is new, 44 rows (41 at d56f2084; round 1 below adds three). It imports the family fixture and its helpers from the files that own them (test_family, test_family_tracking_branch's on_main, test_validate_family_branch_step's BRANCH_LEFT and switch_argv, test_value_pattern_line_break's ID_PATTERN, a_member_whose_id_is, findings and rewritten).

row base ccc2c2d2 head 0657e396
test_a_row_id_the_pattern_refuses_is_found_under_no_members[line-feed], [carriage-return], [space], [leading-dash] (table 3) 4 failed 4 passed
test_with_members_checked_it_is_the_one_finding_not_a_mismatch[...], the same four, members checked and --pins 4 failed 4 passed
test_a_row_id_whose_text_is_no_id_is_found[bool], [float], [negative], [list], [mapping] 5 failed 5 passed
test_the_holder_add_wrote_before_1166_is_refused_in_every_mode failed passed
test_a_mounted_member_whose_own_id_is_malformed_is_found[equal-to-its-row], [row-well-formed] 2 failed 2 passed
test_the_members_own_id_is_asked_as_add_asks_it (the member's own id: 0) failed passed
test_the_rows_remedy_runs_as_printed_from_the_tracking_branch[plain], [space-and-apostrophe] (round 1: the holder's path holds a space and an apostrophe) 2 failed 2 passed
test_a_pin_that_is_no_commit_is_named_to_commit (round 1: a pin of abc is named --to <commit>) failed passed
test_the_members_remedy_pins_the_mended_commit_as_printed failed passed
test_add_refuses_exactly_the_ids_the_holders_own_validator_finds[...], 8 malformed ids 8 failed 8 passed
CONTROLS the same, [zero], [irrs], [hyphen], [digit-first] 4 passed 4 passed
CONTROLS test_a_well_formed_row_id_is_family_ok_under_no_members[zero], [irrs] 2 passed 2 passed
CONTROLS test_the_fixture_is_family_ok_in_every_mode[members], [no-members], [pins] 3 passed 3 passed
CONTROL test_a_well_formed_row_id_that_is_not_the_members_is_still_a_mismatch passed passed
CONTROLS test_a_row_id_that_reads_as_a_number_is_its_digits[2026], [zero] (a bare id: 0, round 1) 2 passed 2 passed
CONTROL test_a_row_spelled_as_an_earlier_one_is_member_duplicates_alone passed passed
CONTROLS test_a_row_with_no_id_is_still_member_id_absent_alone[null], [no-id] 2 passed 2 passed
total 29 failed, 15 passed in 37.38 s 44 passed in 35.67 s

To get the base column, the base's validate-family.py (blob 4683c3cf) was laid into a scratch clone at 0657e396, written from git cat-file blob. Every base failure is the defect: no member-id-malformed (exit 0 where the row asks 1, or member-id-mismatch where it asks the row's finding), and the three remedy rows find no finding to type. The clone's file was then written back from HEAD's blob, and its blob, 4e011252, equals HEAD's. (At d56f2084 the count was 27 failed, 14 passed of 41, in 27.67 s and 31.42 s.)

Mutants

10 mutants, 10 killed (and the four of r1204's 14 that survived d56f2084, R6, R7, R11 and R12, killed in round 1 below). Each ran alone in the scratch clone over the new file, and after each the file was written back from HEAD's blob and proved equal to it (c9a37c41). M1 to M9 ran at 9fc09305, whose validate-family.py is this head's blob; M10 survived there and is killed at d56f2084 by the control row added for it.

id mutant result
M1 the row check moved past the --no-members return (the issue's first) KILLED, 19 failed
M2 the mounted path's own-id check removed, source_bad = False (the issue's second) KILLED, 5 failed
M3 fullmatch put back to match in _is_project_id (the issue's third) KILLED, 8 failed (every line-feed row)
M4 member-id-mismatch said as well where an id is malformed KILLED, 5 failed
M5 the member's own id asked as str(id), not add's str(id or "") KILLED, 1 failed (..._asked_as_add_asks_it)
M6 a falsy row id skipped, as _check_member_ids_unique skips it KILLED, 1 failed ([empty])
M7 the row's remedy names --to <commit>, not the pinned commit KILLED, 1 failed (..._rows_remedy_runs_as_printed_...)
M8 id: null and no id: asked as text ('None') KILLED, 2 failed
M9 the switch line says family/remove-... for a bump KILLED, 1 failed (..._rows_remedy_runs_as_printed_...)
M10 a row spelled exactly as an earlier one asked too KILLED at d56f2084, 1 failed (..._member_duplicates_alone); survived at 9fc09305

Tests

The gate, call by call, Python 3.12.3, git 2.43.0, in the probe environment, one basetemp per call, at most 2 at once. Call 1 ran at 9fc09305; every other call at d56f2084, which adds one row to the new file. Round 1 re-ran the files it touches at 0657e396; those calls are in the next section.

call result
1. the new file, tests/test_family.py, tests/test_family_add_member_id.py, tests/test_value_pattern_line_break.py, and #1227's tests/test_family_gitmodules_values.py, tests/test_validate_family_index_gitmodules.py, tests/test_validate_family_index_value_unmerged.py 381 passed in 441.07 s
2. every tests/test_validate_family_*.py, 12 files (the new one and #1227's two included) 421 passed in 421.85 s
3. the pin call, tests/test_pinned_copy_text.py with every tests/test_shape_pin_*.py and tests/test_update_shape*.py (22 files, 977 ids), shard 1 of 4 (245 ids) 245 passed in 192.61 s
4. the pin call, shard 2 of 4 (245 ids) 241 passed, 4 skipped in 152.60 s
5. the pin call, shard 3 of 4 (245 ids) 232 passed, 13 skipped in 134.75 s
6. the pin call, shard 4 of 4 (242 ids) 237 passed, 5 skipped in 169.89 s
7. tests/test_scaffold_leg_path_collisions.py, tests/test_path_guard_hygiene.py, tests/test_no_auto_maintenance.py 283 passed, 2 skipped in 36.53 s
8. tests/test_agents_md_codes.py, tests/test_cli_reference.py, tests/test_repo_hygiene.py 171 passed in 41.48 s
9. tests/test_shape_doctor.py, shard 1 of 4 (113 ids) 113 passed in 171.78 s
10. tests/test_shape_doctor.py, shard 2 of 4 (113 ids) 113 passed in 254.46 s
11. tests/test_shape_doctor.py, shard 3 of 4 (113 ids) 109 passed, 4 skipped in 216.32 s
12. tests/test_shape_doctor.py, shard 4 of 4 (110 ids) 107 passed, 3 skipped in 88.53 s
13. tests/test_template_text_placeholders.py (its 007 and true member ids pass the holder's gate) 132 passed, 10 skipped in 29.53 s
14. the predicted merge with main dff8fd4b (tree c477f34d, in the scratch clone): the new file, tests/test_family_add_member_id.py, tests/test_value_pattern_line_break.py, main's new tests/test_remove_add_remedies_family_root.py, tests/test_agents_md_codes.py, tests/test_family_switch_branch_exists.py 203 passed in 167.18 s (refreshed in round 1 at main 27397a4b: 256 passed)

Call 5's 13 skips, listed with -rs in a re-run (232 passed, 13 skipped in 131.55 s), are all tests/test_update_shape_merged_away.py: "git filter-repo is not installed". The other calls ran -q, so their skips are counted, not named; each is a skipif or pytest.skip of the file's own (a platform, a privilege or a tool this bench lacks), and none is in a file this PR changes. Each id list was built with --collect-only and checked non-empty before its call.

Review round 1 (r1204: READY, seven NITs)

r1204 reviewed d56f2084 and found it READY: no MUST, no SHOULD, 27 rows failing on base and 41 passing at head, and 10 of its 14 mutants killed. Round 1 is commit 0657e396 on top of d56f2084. It answers NITs 1 to 3 and changes no behavior of validate-family.py: the only difference between its blob at d56f2084 (c9a37c41) and at 0657e396 (4e011252) is the docstring of _check_member_ids_well_formed (git diff --numstat d56f2084 0657e396: the validator 3 added, 2 removed; the new test file 63 added, 6 removed).

NIT 1: the docstring said member-pin-commit is printed "beside" the row's finding, but a refusal drops the finding. Reworded: "A pin that is no 40-hex commit is named --to <commit>; where members are checked, member-pin-commit refuses the run first, and this finding follows once the pin is mended." The first row below also holds that claim: with the members checked, the same row is exit 2, REFUSED member-pin-commit: IRRS: ..., with no member-id-malformed in stderr.

NIT 2: no row held the remedy's <commit> fallback or the holder's quoting. Two rows in tests/test_validate_family_member_id_pattern.py:

  • test_a_pin_that_is_no_commit_is_named_to_commit (new). --no-members over a row with id: "irrs\n" and pin.commit: "abc": exit 1, the one member-id-malformed finding, and the remedy's --to argument is the literal <commit>. Kills R12.
  • test_the_rows_remedy_runs_as_printed_from_the_tracking_branch is parametrized [plain] and [space-and-apostrophe]. In the second, the holder is moved to tmp_path / "it's a holder" before the hand edit, as tests/test_validate_family_branch_step.py gives _switch_line the holder it's a holder; the switch and the bump are split by shlex, typed as printed, and the bump exits 0 with the row's id: written, the pin where it was, the tree clean and the gate green in every mode. Kills R7: with the path unquoted, shlex.split raises No closing quotation.

NIT 3: no row read a bare id: 0, the one value where the row's str() and add's str(id or "") part. test_a_row_id_that_reads_as_a_number_is_its_digits is parametrized [2026] and [zero]. Each: the row reads as that number; under --no-members it is exit 0, family ok; with the members checked (IRRS's own id is irrs) it is exit 1 and exactly one finding, member-id-mismatch: members/IRRS: project.yaml declares id 'irrs' but family.yaml records 0 for IRRS. ... (2026 for the first). Kills R6 (the row check would refuse a bare 0) and R11 (the identity check would pass it with no finding).

The new rows, base vs head (the clone's validator laid from base ccc2c2d2, then from HEAD, as above):

row base head
test_a_pin_that_is_no_commit_is_named_to_commit failed passed
test_the_rows_remedy_runs_as_printed_from_the_tracking_branch[space-and-apostrophe] failed passed
CONTROL test_a_row_id_that_reads_as_a_number_is_its_digits[zero] passed passed

Mutants, r1204's own definitions (its tools/mutate.py), each alone in a scratch clone at 0657e396; after each, the file was written back from HEAD's blob and its blob, 4e011252, printed EQUAL to HEAD's:

id mutant d56f2084 (r1204) 0657e396
R6 the row check reads str(member_id or "") survived, 41 passed KILLED, 1 failed (..._its_digits[zero]), 43 passed
R7 _bump_to leaves the holder's path unquoted survived, 64 passed (new file and test_validate_family_branch_step.py) KILLED, 1 failed (..._rows_remedy_runs_as_printed_...[space-and-apostrophe]), 43 passed; with test_validate_family_branch_step.py, 1 failed, 66 passed
R11 the identity check's row question reads str(recorded_id or "") survived, 41 passed KILLED, 1 failed (..._its_digits[zero]), 43 passed
R12 the COMMIT_RE fallback to <commit> removed survived, 41 passed KILLED, 1 failed (test_a_pin_that_is_no_commit_is_named_to_commit: ['IRRS', '--to', 'abc'] == ['IRRS', '--to', '<commit>']), 43 passed

The gate at 0657e396, Python 3.12.3, git 2.43.0, in the probe environment, one basetemp per call, at most 2 at once:

call result
R1. the new file, tests/test_family.py, tests/test_family_add_member_id.py, tests/test_value_pattern_line_break.py, tests/test_validate_family_branch_step.py 224 passed in 224.02 s
R2. tests/test_scaffold_leg_path_collisions.py, tests/test_path_guard_hygiene.py, tests/test_no_auto_maintenance.py 283 passed, 2 skipped in 32.05 s
R3. the doc trio, tests/test_agents_md_codes.py, tests/test_cli_reference.py, tests/test_repo_hygiene.py 171 passed in 39.61 s
R4. the template-text scans, tests/test_template_text_placeholders.py, tests/test_template_values_single_pass.py, tests/test_stdout_encoding.py, tests/test_refusal_default_remediation.py, tests/test_pin_pattern_line_break.py (the docstring is template text) 435 passed, 13 skipped in 84.87 s
R5. the predicted merge with main 27397a4b (commit 8cfb52f6, tree 44248a29, in the scratch clone): the new file, tests/test_family_add_member_id.py, tests/test_value_pattern_line_break.py, tests/test_remove_add_remedies_family_root.py, tests/test_agents_md_codes.py, tests/test_family_switch_branch_exists.py, tests/test_readme_windows_quoting_sentence.py, tests/test_family_bump_to_checks.py 256 passed in 237.42 s

Not re-run, since this round changes a docstring and the new test file and nothing else: #1227's three test files of call 1, the pin call (calls 3 to 6) and tests/test_shape_doctor.py (calls 9 to 12). Their results above stand for d56f2084, whose code 0657e396 shares.

NITs 4, 5 and 7 stay as named.

  • NIT 4: the AGENTS.md line is 129 characters and covers the row only (the file is at its 494-line cap). Known limits, below.
  • NIT 5: under --no-members the row's remedy says "with the member checked out" and names no command for it. _lockstep's bump behaves the same way, and bump's own member-uninitialized names git submodule update --init and make bootstrap. Known limits, below.
  • NIT 7: git 2.47.1 was not run. Known limits, below.

NIT 6 is refreshed below: the Merge-tree section names the current main, #1172's head and #1246's head, and call 14 is re-run on the merge with main 27397a4b (R5). The "Summary by Sourcery" block GitHub appended to the live body describes #1227's change and has no closing keyword; this file carries none of it, and it stays out of the squash message.

Merge-tree

git merge-tree --write-tree of head 0657e396 with each head fetched on 2026-11-08 (scratch clone):

#1256 has no refs/pull/1256/head on origin, so it has no row here. #1227's head is still 2d56c190, so the stacking statement at the top holds and no restack is needed.

Known limits

  • Under --no-members, a member's own malformed id behind a well-formed row is not seen: that run reads no member. add and bump refuse such a member (member-bad-id), so only a hand edit, or an add older than validate-manifest.py, validate-family.py, family.py and update-shape.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, leaves one.
  • The row's remedy needs the member checked out. bump refuses one that is not (member-uninitialized, naming git submodule update --init and make bootstrap); a CI log under --no-members is read by a person who then runs it where the member can be checked out. Where the row's pin.commit is no 40-hex commit, the remedy says --to <commit>.
  • Where the pin is no commit and the members are checked, the run is refused first (member-pin-commit, exit 2) and the row's finding follows once the pin is mended; under --no-members the finding stands alone and names --to <commit>. That order predates this PR (r1204's NIT 1; the docstring now says it).
  • bump to the commit already pinned commits as "Bump IRRS to <sha> ... <sha> -> <sha>" (measured, committed 832d083cac1b: members/IRRS, family.yaml): its message does not say the row's id changed.
  • A member's own id: that reads as a positive number (id: 7) is accepted, as add accepts it (str(7)), though the member's own validate-manifest.py refuses it as no string. This PR asks add's question, unchanged.
  • A row with no project: is skipped, as _check_member_ids_unique skips it; member-project names that row only with the members checked (read, not measured; unchanged).
  • Measured with git 2.43.0 only (/usr/bin/git); the issue's git 2.47.1 was not run. Python 3.12.3 only; CI runs 3.14. The change asks no is_file, exists or stat (tests/test_path_guard_hygiene.py is in call 7).
  • AGENTS.md's item 4 line is 129 characters, to name the code without a 495th line.

Not in this PR

Lane: openreposhape-1 (openRepoShape-1)

🤖 Generated with Claude Code

Brett Heap and others added 16 commits October 21, 2026 14:59
…git dies on read the `.gitmodules` git reads, the index's where the work tree's is gone, and every section's lines end `git add -- .gitmodules`, so a plain `git commit` after them takes the section out of HEAD (#1077)

RV1069's review of #1069 (#1036) found four follow-ups, each measured on
main before this change.

NIT 1. `gitmodules-path-twice` read the work tree's file alone. A second
section for members/IRRS staged in the index's file, the work tree's
deleted, was `family ok` in every mode, and a clean clone of the commit
after it failed its update and its gate. It reads the sections as git
reads them now (`_sections_as_git_reads`), as `gitmodules-path-unnamed`
does since #1036, and where the work tree's file is gone it names the
line that puts back the file git reads first, recorded so no later
finding puts it back over the hand edit.

NIT 2. The value check read the work tree's file and HEAD's, never the
index's. An `update = !true` staged in the index's file alone, in a named
member's section, was `family ok` while git's own `submodule status`
exited 128. It reads the file git reads (`_listed_as_git_reads`) and
names the index's where that is the one read. Its lines put that file
back, not HEAD's, and end "then stage", or "then stage and commit" where
HEAD's gives the value too. Where HEAD's alone gives it, the text says to
commit the index's file, with no line, so a fix staged there is kept,
and `member-gitmodules-absent` then puts the file back itself
(`_check_gitlinks_registered` asks `Report.put_back` alone). Git is asked
in the order it was asked before, the work tree's file, then HEAD's, then
the one read in its place, so `git-not-runnable` names what it named.

NIT 3. Where HEAD's file held the section, `gitmodules-path-unnamed`'s
lines ended "then commit .gitmodules" and staged nothing, so a plain
`git commit` after them left the section in HEAD and a clone's gate
failed. Every branch, in members/, outside it and in another case, prints
`git add -- .gitmodules` after the keys and says "then stage and commit"
there; path-twice ends "then stage and commit" too.

NIT 4. A row now covers the case-variant branch where HEAD's file holds
no section of the name (RV1069's probe `test_r2_stray_not_in_head`).

tests/test_validate_family_index_gitmodules.py holds the new rows: each
types only the printed lines, and what the text says in words, then a
plain `git commit`. The older rows that pinned the old text, HEAD's
put-back and #1036's control are updated to the new behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…its own, so #1111's hygiene test passes, path-twice says "back first from" the file it puts back, and two rows pin the value's write-back and path-twice's recorded put-back (#1077)

Fix round 1 on #1124, from RV1124's review at 672845c.

S1. `_listed_as_git_reads` opened with a bare `os.path.lexists(root /
GITMODULES)`, which #1111's `test_path_guard_hygiene.py` fails by file
and line. Its one caller, `_check_gitmodules_values`, asks it only where
the work tree's file is gone, and `_gitmodules_put_back`, which the
function calls next, asks the same `lexists` (it is on the test's
allowlist), so the guard is dropped.

N3. Two behaviours had no row; mutants A and B survived all 439 rows of
the PR's nine files. A row for each, from RV1124's probes P10 and P7:
- the value's write-back reads the file git reads, not HEAD's (A);
- path-twice records its put-back, so `gitmodules-path-unnamed` after it
  prints none and a plain commit keeps neither `second` nor
  `members/Old` in HEAD (B), where the work tree's file is deleted or
  its deletion is staged.

N4. Path-twice said "back from HEAD's, as the index holds none first":
"first" sat on the wrong phrase. It says "back first from {whose}".

N5. `_gitlink_out_first`'s docstring said its text ends "then commit
.gitmodules"; every branch stages now, and it says which words each
branch ends with.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s `.gitmodules` beside the work tree's and its stage remedies end `git add -- .gitmodules`, an index file git cannot parse is refused, and an unmerged one is read as none (#1150)

RV1124's review of #1124 (pull/1124#issuecomment-6321372469) found three
things outside #1124's fix round, each older than #1124 and measured again
on main 961640c and on #1124's head 8dfa32b before this change:

- N1: where the work tree's `.gitmodules` is on disk, the check never read
  the index's, which a plain `git commit` records. A value in both was
  taken out of the work tree's by a line that staged nothing, so the
  commit after it recorded the value in HEAD; a value in the index's
  alone was `family ok`. The check reads the index's too now, names the
  latter ("in the index's file alone"), and every remedy that says to
  stage the file ends with the line that does.
- N2: an index's file with a line git cannot parse was read as nothing
  (`git config --blob` exits 128 there), so a value or a section before
  the line was never named. It is `gitmodules-unreadable` now, as HEAD's
  is.
- N6: while a merge leaves `.gitmodules` unmerged in the index, git's
  submodule commands read no file, and the checks read the work tree's or
  HEAD's. They read none now; a value HEAD's gives says to resolve the
  merge without the key, then stage and commit. One finding changes: a
  value only the work tree's file gives, on disk, while unmerged, HEAD's
  clean, is no finding until the merge is resolved and staged, as git
  reads no file until then.

Stacked on #1124 at 8dfa32b: every item also holds on main, but the code
each fix changes (`_blob_listed`, `READ_HERE`, `_listed_as_git_reads`, the
stage line) is #1124's.

Closes #1150

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ules` and the work tree's give, with no index's file, is pinned to end with the line that stages it, as HEAD's value, not the index's, is what adds that line (#1150)

Mutant M3 of 442615e (`if committed or staged:` cut to `if staged:` in
`_value_taken_out`) survived all 170 rows of the three changed test
files: in every row whose lines take out a value HEAD's file gives, the
index's file gives it too, or the work tree holds no `.gitmodules`, so
`staged` was true there anyway. The new row has the work
tree's file and HEAD's give `update = !true`, and `git rm --cached` has
taken `.gitmodules` out of the index. Under M3 the finding ends at the
`--unset-all` line, and the plain `git commit` after it records
`.gitmodules` gone from HEAD, and the gate then finds
`member-gitmodules-absent`, exit 1. The row fails under M3, on #1124's head
8dfa32b and on main 961640c, and passes on 442615e.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e for: templates/family-root/scripts/validate-family.py tests/test_family_gitmodules_values.py tests/test_validate_family_index_gitmodules.py

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1208 touches validate-family.py (a docstring) and
tests/test_validate_family_index_gitmodules.py (a new row); the text
merges cleanly. The new row's re-pin to this branch's stage line follows
in its own commit (rv1227's M1).

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ue HEAD gives where the deletion of `.gitmodules` is staged expects the stage line last and types no `git add` by hand, since this branch's lines end with it

rv1227's M1 on #1227. Main's #1208 added the row while its finding ended
at the `--unset-all` line; this branch's `_value_taken_out` ends every
line set that says to stage the file with `git add -- .gitmodules`.
Measured on the merge (git 2.43.0): the three printed lines typed in
order, then a plain `git commit`, leave HEAD without the value, the
index and the work tree equal to it and `git status` clean, and the gate
passes; typed without the stage line, the commit exits 1 (`no changes
added to commit`) and HEAD keeps the value.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and the init stop ask nothing, a value the work tree alone gives after a staging line is staged again, and another index value of the key is named, not "gives it too"

rv1227's review of #1227 (issuecomment-6374575208), its M2, S1, N1, N2,
N3, N4 and N5, each measured on git 2.43.0 on the branch merged with main
e04826d (d95308e) before this change:

- M2. With `.gitmodules` at conflict stages, git reads no file: `git
  submodule status` and `update --init` exit 128 at every gitlink, and a
  plain clone of HEAD checks tools/x out. `_sections_as_git_reads`' none
  made `gitlink-unnamed` say no section gave tools/x and a plain clone
  stopped there, and `member-uninitialized` say to write back a section
  the file gave. `_check_gitlinks_elsewhere` and `_init_stop_said` ask
  nothing while unmerged now. The hand mount's reads keep reading none,
  as git's own `git rm` does there: its plain `git rm` of a clone dies
  (`could not lookup name for submodule`, exit 128), and `--cached`, the
  line it prints, exits 0.
- S1. `_value_taken_out` takes `again`: after a finding whose lines end
  `git add -- .gitmodules`, a value the work tree's file alone gives ends
  with that line too, and says why (`STAGED_ABOVE`).
- N4. Where the index's file gives the key another value, the finding
  names it (`INDEX_GIVES`, `_values_said`) in place of "gives it too".
- N1, N5. `_blob_paths` and `_blob_sections` say they refuse an index's
  file git cannot parse; `GIVEN_WHERE`'s comment says `(False, False)`.

tests/test_validate_family_index_value_unmerged.py: rows for M2 (e2d,
two ids; e2e), S1 (e4o1, e4o2), N4 (e4c), N2 (e1b), N3 (a real
modify/delete conflict, and the sections row's on-disk id). S2, the
sparse checkout, is a Limit of #1227 and a follow-up issue.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e S1 fix carries to every later finding is pinned for a third value and for a committed value the index lacks, and N4's row also for an index file giving the value and another

rv1227c's independent review of #1227 (issuecomment-6722728204), its
SHOULD S1, test-only. Two mutants of the S1 code survived all 181 rows
of the PR's three files, and probes showed each puts a value git dies on
into HEAD: K6 (`staging = committed or staged`, the stage line not carried
past the next finding) and K8 (`staging = staging or staged`, a committed
value's stage line not carried). The S1 row pinned only the finding right
after a value that is both staged and committed. validate-family.py does
not change.

- test_every_later_value_the_work_tree_alone_gives_stages_again (new,
  rv1227c's p2a): `update = !true` staged for IRRS, then `update =
  !false` and `fetchRecurseSubmodules = sometimes` in IRSS's section, the
  work tree's file alone. The second and third findings each say
  `STAGED_ABOVE` and end with the stage line; typed in order, then a plain
  `git commit`, HEAD is as it was. K6 fails it.
- test_a_committed_value_the_index_lacks_stages_the_later_ones_again (new,
  rv1227c's p8): IRRS `!true` committed, the index's file staged without
  it, the work tree's giving IRRS `!true` and IRSS `!false`. IRRS's
  finding is committed and not staged, and its `git add` stages IRSS's
  value all the same, so IRSS's ends staging again. K8 fails it.
- test_another_value_the_index_s_file_gives_the_key_is_named gains an id
  (rv1227c's p3c, its K11): the index's file giving `!true` and `!false`
  beside the work tree's `!true` names both. The old id is
  `[another-value]` now, the new one `[this-value-and-another]`.
- `staged_again` builds the later finding's text for the S1 row and the
  new rows, which the S1 row spelled out inline.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s lane 3 asked)

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mber-id-malformed` in every mode, and so is a mounted member's own `project.yaml` `id:`, with the `bump` that writes the row from that file (#1204)

`PROJECT_ID_RE` was asked of the family's own id alone. A row's `id:` met
the mounted `project.yaml`, compared as text, and `_check_member_ids_unique`,
and neither is a pattern: under `--no-members` a row id of "irrs\n",
"irrs\r", "IRRS Capital" or "-x" was `family ok`, and the holder a
pre-#1166 `add` wrote for a member whose own id read "irqs\n" was
`family ok` in every mode.

`_check_member_ids_well_formed` asks `_is_project_id(str(id))` of every row
`_check_member_ids_unique` asks, from `_check_members` before the
`--no-members` return. `_check_member_identity` asks the member's own id as
`family.py add` asks it, `str(id or "")`, and says no `member-id-mismatch`
where either id is malformed. Both remedies are a `bump`: to the commit the
row pins, or to the commit that mends the member's own file. AGENTS.md names
the code; `ID_READERS` counts the new reader.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…se own `id:` reads as the number 0 is refused, as `family.py add` refuses it, so the gate's `str(id or "")` reading of the member's own id is held (#1204)

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… line makes the branch `family.py` suggests for `bump`, `family/bump-member` for an id no branch word spells (#1204)

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… as an earlier one, its `id:` malformed, is `member-duplicate`'s alone, as `_check_member_ids_unique` skips it (#1204)

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

sourcery-ai Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Implementation

Validate member-row and mounted-manifest IDs with one whole-string project-ID check, report malformed IDs without a redundant mismatch, and give bump-based repairs.

  • templates/family-root/scripts/validate-family.py L1588-1598, L1630-1645, L1922-2016 (+3 small edits)

Make .gitmodules validation account for index contents, unreadable index blobs, and unmerged files, while keeping staged repair commands accurate and avoiding section checks during unresolved merges.

  • templates/family-root/scripts/validate-family.py L2275-2328, L2333-2368, L4213-4242 (+22 small edits)

Documentation

Document that malformed member-row IDs produce the new member-id-malformed finding.

  • AGENTS.md L404

Tests

Exercise malformed member IDs, mounted manifest IDs, remediation commands, and the shared ID-pattern reader.

  • tests/test_validate_family_member_id_pattern.py L1-509
  • tests/test_value_pattern_line_break.py L167-169

Cover .gitmodules validation and repair behavior for staged values, unreadable index content, and unresolved merges.

  • tests/test_family_gitmodules_values.py L193-196, L802-805, L815-821 (+5 small edits)
  • tests/test_validate_family_index_gitmodules.py L222-225, L287-291 (+7 small edits)
  • tests/test_validate_family_index_value_unmerged.py L1-790

Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-11-08T20:43:46Z) Independent review r1204 of #1273 at d56f2084: READY, with no MUST and no SHOULD, and seven NITs. Lane 1 starts a small round for NITs 1-3 (a docstring, and rows for the <commit> fallback, a quoted holder path and a bare id: 0), then asks r1204 for a delta check. NITs 4, 5 and 7 stay as named, and NIT 6 is for landing.

r1204: independent review of #1273 (issue #1204) at d56f20840d22194fff581fbd8d8ba423871106f4

Verdict: READY

No MUST and no SHOULD. The fix does what the issue asks:

  • a row's id: gets PROJECT_ID_RE.fullmatch in every mode (--no-members, --pins, full);
  • with the members checked, the mounted project.yaml id: gets the same str(id or "") question that family.py add (_read_member_project) asks;
  • there is one code, member-id-malformed, and AGENTS.md names it;
  • member-id-mismatch is no longer printed where either id is malformed.

The writer had good cause to depart from the issue's remove-then-add remedy. I measured the issue's remedy on base, head and current main, and it does not run as printed. The PR's bump --to <the pinned commit> does run as printed when the member is checked out.

What I measured:

  • The 27 rows fail on base ccc2c2d2 and pass at head; the 14 controls pass on both.
  • 10 of my 14 mutants are killed. The 4 survivors are test gaps in edge cases, and each behaviour is correct as I measured it (NIT 2, NIT 3).
  • Every gate call passes, including on the predicted merge with current main 40cb8086.
  • All three merge-trees are clean.

There are seven NITs, below.

Review checkout: /workspace/projects/openRepoShape-worktrees/l1-r1204, detached at d56f2084, left clean (git status --porcelain --ignored prints nothing). Scratch folder: /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1204/.

Findings

MUST

None.

SHOULD

None.

NIT 1: a docstring claims member-pin-commit is printed "beside" the row's finding, but a refusal drops the finding

  • Where: templates/family-root/scripts/validate-family.py:1983, in the docstring of _check_member_ids_well_formed: "A pin that is no 40-hex commit is named --to <commit>, with member-pin-commit beside it where members are checked."
  • Evidence: probe e1 (tools/test_r1204_probe_edges.py). The row has id: "irrs\n" and pin.commit: "abc".
    • --no-members: exit 1, FINDING member-id-malformed: IRRS: ... --to <commit> .... This part of the sentence holds.
    • members checked: exit 2, and stderr is only REFUSED member-pin-commit: IRRS: pin.commit is 'abc', which is not 40 hex. .... The row's finding is not printed: the refusal drops every finding collected before it. That behaviour predates this PR, but the sentence says the two appear together.
  • Fix: reword, for example: "...is named --to <commit>; where members are checked, member-pin-commit refuses the run first, and this finding follows once the pin is mended."

NIT 2: no row covers the remedy's <commit> fallback or the holder's quoting, which the body and docstring both claim (mutants R12 and R7 survive)

  • Where: validate-family.py:1998 (the COMMIT_RE.fullmatch fallback) and :1943 (shlex.quote(str(root)) in _bump_to).
  • Evidence:
    • R12 deletes the COMMIT_RE check, so a pin of abc would be printed --to abc, which bump refuses as member-to-not-a-commit. It survives: 41 passed in 38.72s.
    • R7 prints the holder's path unquoted. It survives over the new file plus tests/test_validate_family_branch_step.py: 64 passed in 56.91s.
    • The current behaviour is correct as measured:
      • Probe e1 prints --to <commit>.
      • Probe e3 uses a holder under with space'q/InkRouter. It prints --family-root '<...>/with space'"'"'q/InkRouter'. Typed as printed, the bump exits 0 (committed ab8afb265613: members/IRRS, family.yaml) and the gate then says family ok.
    • This gap already exists for _lockstep's bump and the member-duplicate-id remove line, so it is a NIT, not a hold.
  • Fix: two cheap rows in tests/test_validate_family_member_id_pattern.py:
    1. --no-members over a row with pin.commit: "abc", asserting argv[argv.index("--to") + 1] == "<commit>";
    2. test_the_rows_remedy_runs_as_printed_from_the_tracking_branch parametrized with a holder under tmp_path / "it's a holder", as test_validate_family_branch_step.py:99 does for _switch_line.

NIT 3: no row reads a bare id: 0, the one value where the row's str() and add's str(id or "") differ (mutants R6 and R11 survive)

  • Where: validate-family.py:1994 (the row check, str(member_id)) and :1651 (the identity check, str(recorded_id)). The control at tests/test_validate_family_member_id_pattern.py:227 uses 2026 only.
  • Evidence:
    • R6 makes the row check read str(member_id or ""), so a bare id: 0 row would be refused. It survives: 41 passed in 37.47s.
    • R11 makes the identity check read str(recorded_id or ""), so a bare id: 0 row against member irrs would pass silently, with no mismatch. It survives: 41 passed in 37.24s.
    • Head reads 0 as its digits, which is consistent with the PR's own rule and with family.py's _member_id, which rewrites the row as str(value). Probe e2: the row parses as 0; --no-members gives exit 0, family ok; members checked gives exit 1, FINDING member-id-mismatch: members/IRRS: project.yaml declares id 'irrs' but family.yaml records 0 for IRRS.
  • Fix: parametrize test_a_row_id_that_reads_as_a_number_is_its_digits over 2026 and 0. Add a members-checked assertion for 0: the one member-id-mismatch, exit 1.

NIT 4: the AGENTS.md line is 129 characters and covers the row only

  • Where: AGENTS.md:404, "A row whose id: is no project id is member-id-malformed."
  • Evidence: awk 'NR==404{print length($0)}' AGENTS.md prints 129, and wc -l gives 494, which is the cap (tests/test_repo_hygiene.py:667). The body's Known limits names this. The same code also fires on a mounted member's own project.yaml id:, which the line does not mention.
  • Fix: optional. If a line is ever freed, rewrap it, and say "A row's id:, or a mounted member's own, that is no project id is member-id-malformed."

NIT 5: under --no-members, the row's remedy says "with the member checked out" but names no command for it

  • Where: validate-family.py:2005.
  • Evidence: probe ci. A fresh clone of the holder, with no member checked out, has a hand-edited row; the remedy is typed as printed:
    • the printed bump gives exit 2, REFUSED member-uninitialized: members/IRRS is not an initialized submodule checkout ... Remediation: git submodule update --init members/IRRS, or make bootstrap;
    • after that init, the same printed bump exits 0.
    • Measured at head and on the merge with main.
  • Assessment: the remedy can be recovered, the precondition is stated in words, and _lockstep's bump behaves the same way. The body lists it in Known limits.
  • Fix (optional): name git -C <holder> submodule update --init -- members/<P> (or make bootstrap) beside "with the member checked out".

NIT 6: the body's main-merge evidence is stale, and the live body carries a Sourcery summary that describes #1227's change

NIT 7: the issue's Verify asks for git 2.47.1; neither the writer nor I ran it

  • The body says so (Known limits). CI's runner git covers part of this.
  • The change runs no git of its own: it is a YAML read and a regex. The rows' git use (submodule add, bump) is what main's tests already do.

The fix against the issue (brief item 1)

  • A row's id:, in every mode:
    • _check_member_ids_well_formed is called from _check_members at :4586, before the --no-members return, which comes after the gitmodules and gitlink checks.
    • It asks _is_project_id(str(member_id)), which is PROJECT_ID_RE.fullmatch(text) is not None (:1934).
    • Rows cover all three modes: the --no-members rows, plus --pins in test_with_members_checked_... and in the holder row.
    • Mutants R2 (call removed), R10 (call under --no-members only) and R1 (match) are killed.
  • Which rows it asks: exactly the rows _check_member_ids_unique asks, which skips a non-mapping row, a row without a string project:, and a row that spells an earlier project: exactly. _member_project_name returns None for those same rows, so every row that reaches _check_member_identity has been asked. I checked this against the source (:715-733). That makes the identity check's silent return for a malformed row id (:1651) safe. The writer's M10 holds it (see the mutants).
  • The mounted member's own id:
    • source_bad = not _is_project_id(str(actual_id or "")), the same expression _read_member_project uses (project_id = str(manifest.get("id") or "") then fullmatch, scripts/family.py:1131-1135).
    • R3, which asks the row's id instead, is killed. So are the writer's M2 and M5.
    • Probe e4: a mounted project.yaml with no id: gives "the member's own id: is None", the same wording add uses for that case.
    • The claim that validate-manifest refuses such a file as manifest-id holds for every value source_bad accepts: _id_findings refuses a non-string or a non-match (templates/assembly-root/scripts/validate-manifest.py:840-852).
  • One code: member-id-malformed. AGENTS.md:404 names it, and tests/test_agents_md_codes.py passes (call 5).
  • No double report: member-id-mismatch is not printed when either id is malformed. R4 (mismatch printed again for a malformed row) is killed by 4 rows.
  • Signature change: _check_member_identity gains root. It has one caller (git grep finds only :1565).

Remedy measurements (brief item 2)

How I measured:

step base ccc2c2d2 head d56f2084 merge with main 40cb8086
validate-family.py --no-members exit 0, family ok exit 1, member-id-malformed (the row's) exit 1, the same
issue's remedy, family.py remove --family-root <h> --member IRRS exit 0; NOTE that member's object store is left behind ... <h>/.git/modules/members/IRRS and an rm -rf line; the store is still there (True) the same the same
then family.py add --family-root <h> --member InkRouter/IRRS --at <pin> exit 2, REFUSED member-git-dir-cached: <h>/.git/modules/members/IRRS still holds the object store of a previously removed IRRS, and git submodule add will not write over it the same the same
add without --at exit 2, member-git-dir-cached the same the same
gate after that exit 0, family ok ... 1 member(s): the member is gone and the gate passes the same the same
PR's remedy, family.py bump --family-root <h> --member IRRS --to <pin> typed (base prints none): exit 0, IRRS 53946f63a693 -> 53946f63a693, committed 40008eb513cc: members/IRRS, family.yaml as printed: exit 0, one commit, row id: 'irrs', pin and gitlink unmoved, status clean as printed: exit 0, committed a12158791ab0, row id: 'irrs', gitlink unmoved
gate after bump exit 0 members checked; exit 0 --no-members the same the same
CI holder (fresh clone, no member checked out): printed switch + bump (no finding on base) exit 2, member-uninitialized, naming git submodule update --init members/IRRS; after it, the same bump exits 0 the same

Conclusions:

  • The writer's account holds on all three trees. 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 changes no part of it.
  • The issue's two lines do not run as printed. Following them as far as add's refusal leaves a holder that has dropped the member and still passes the gate.
  • Convention: _bump_to spells the line exactly as _lockstep does: {PYTHON} scripts/family.py bump --family-root {shlex.quote(str(root))} --member {shlex.quote(project)} --to .... Both remedies end with BRANCH_LEFT in the same string as their _switch_line, and tests/test_family_switch_branch_exists.py passes on the merge (call 3).
    • The switch word for a malformed row id is member, matching family.py's _suggested_branch. For the member's own finding it is the row id or the project (family/bump-irss in e4).
  • README "Windows, natively" sentence: still true. validate-family.py calls no host quoter (shlex.quote and shlex.join only). tests/test_readme_windows_quoting_sentence.py passes on the merge with main (call 3).

Values that are not strings (brief item 3)

I parsed each spelling with the repository's own parse_yaml and asked the head's _check_member_ids_well_formed (tools/values.py):

spelled read as row check add's str(v or "")
2026 int 2026 ok ok
0 int 0 ok ("0") refused ("")
007 int 7 ok ("7") ok
true / false bool refused (True / False) refused
1.5 float refused refused
-3 int refused refused
[irrs] / {irrs: 1} list / dict refused refused
"" str '' refused refused
null / ~ / no id: None absent (left to member-id-absent, members checked) refused
yes, on, 0x1f, 1e3, 2026-01-01 str (this parser keeps them as text) ok ok
.inf, 1_000, +3 str refused refused
  • The row reading, str(), is the right one. It matches the text the identity check always compared, and it matches family.py's _member_id, which rewrites a row as str(value) and whose docstring says id: 2026 passes.
  • The mounted path reads exactly as add does.
  • The two readings part only at a bare 0. That case is correct at head, but no row holds it (NIT 3).
  • null and no id: stay with member-id-absent: members checked, and silent under --no-members. Rows [null] and [no-id] hold this.

The pinned copy (brief item 4)

The rows (brief item 5)

New file, base validator laid into the scratch clone at d56f2084, then written back from HEAD's blob:

  • laid ccc2c2d2...:templates/family-root/scripts/validate-family.py work tree 4683c3cf...
  • base: 27 failed, 14 passed in 32.74s
  • laid HEAD:... work tree c9a37c41... HEAD c9a37c41... EQUAL-TO-HEAD
  • head: 41 passed in 37.40s
row base head
test_a_row_id_the_pattern_refuses_is_found_under_no_members[line-feed, carriage-return, space, leading-dash] 4 failed 4 passed
test_with_members_checked_it_is_the_one_finding_not_a_mismatch[same four] 4 failed 4 passed
test_a_row_id_whose_text_is_no_id_is_found[bool, float, negative, list, mapping] 5 failed 5 passed
test_the_holder_add_wrote_before_1166_is_refused_in_every_mode failed passed
test_a_mounted_member_whose_own_id_is_malformed_is_found[equal-to-its-row, row-well-formed] 2 failed 2 passed
test_the_members_own_id_is_asked_as_add_asks_it failed passed
test_the_rows_remedy_runs_as_printed_from_the_tracking_branch failed passed
test_the_members_remedy_pins_the_mended_commit_as_printed failed passed
test_add_refuses_exactly_the_ids_the_holders_own_validator_finds[8 malformed] 8 failed 8 passed
CONTROLS test_add_refuses_...[zero, irrs, hyphen, digit-first] 4 passed 4 passed
CONTROLS test_a_well_formed_row_id_is_family_ok_under_no_members[zero, irrs] 2 passed 2 passed
CONTROLS test_the_fixture_is_family_ok_in_every_mode[members, no-members, pins] 3 passed 3 passed
CONTROL test_a_well_formed_row_id_that_is_not_the_members_is_still_a_mismatch passed passed
CONTROL test_a_row_id_that_reads_as_a_number_is_its_digits passed passed
CONTROL test_a_row_spelled_as_an_earlier_one_is_member_duplicates_alone passed passed
CONTROLS test_a_row_with_no_id_is_still_member_id_absent_alone[null, no-id] 2 passed 2 passed
total 27 failed, 14 passed 41 passed

Every base failure is the defect:

  • assert 0 == 1, with the family ok output (14);
  • (True, False) == (True, True), where add refuses and the gate finds nothing (8);
  • member-id-mismatch where the row's finding is wanted;
  • IndexError where the remedy row finds no finding to type.

Cases the rows miss: the <commit> fallback and a quoted holder path (NIT 2), and a bare 0 (NIT 3). All three are correct as measured, in probes e1, e3 and e2.

Mutants (brief item 6)

  • All mutants ran in the scratch clone _lane1-scratch/r1204/clone at d56f2084, one at a time, using tools/mutate.py.
  • After each one, a finally wrote the file back from HEAD's blob. Every restore printed restored: work tree c9a37c41009c98cf7bdfb00093fcd6179009634d HEAD c9a37c41009c98cf7bdfb00093fcd6179009634d EQUAL.
  • Each ran over the new file unless the table says otherwise.

Mine: 10 of 14 killed.

id mutant result
R1 _is_project_id: fullmatch changed to match KILLED, 8 failed (every line-feed row)
R2 the _check_member_ids_well_formed call removed KILLED, 24 failed
R3 the member's own id asked of the row's id KILLED, 8 failed
R4 elif source_bad: only, so a malformed row id also gets a mismatch KILLED, 4 failed
R5 the row's text .strip()ped before it is asked KILLED, 10 failed
R6 row check reads str(member_id or "") SURVIVED, 41 passed (NIT 3)
R7 _bump_to leaves the holder's path unquoted (new file + test_validate_family_branch_step.py) SURVIVED, 64 passed (NIT 2)
R8 the row's switch line words the project, not the row id KILLED, 1 failed (..._rows_remedy_runs_as_printed_...)
R9 the member's own remedy names the row's pin, not <commit> KILLED, 3 failed
R10 the row check made under --no-members only KILLED, 6 failed
R11 identity check's row question reads str(recorded_id or "") SURVIVED, 41 passed (NIT 3)
R12 the COMMIT_RE fallback to <commit> removed SURVIVED, 41 passed (NIT 2)
R13 the row check's non-mapping guard removed (new file + test_validate_family_duplicates.py + test_family_members_not_a_list.py) KILLED, 7 failed, all in the two older files: a traceback on a non-mapping row
R15 the member's own switch word is the project, not the row id KILLED, 1 failed (..._members_remedy_pins_the_mended_commit_...)

The writer's M10 (a row spelled as an earlier one is asked too) checks out:

  • at d56f2084: KILLED, 1 failed, 40 passed in 38.16s, by test_a_row_spelled_as_an_earlier_one_is_member_duplicates_alone;
  • with 9fc09305's test file laid in: SURVIVED, 40 passed in 39.73s.

The test file was then written back from HEAD's blob (18af8751..., EQUAL-TO-HEAD). The validator blob is c9a37c41 at 0ab49ee2, f2dade54, 9fc09305 and d56f2084, so the writer's M1 to M9 ran on this head's code, as the body says.

Gate calls (brief item 7), every summary line

  • Probe environment: stub gh first on PATH, GH_TOKEN=bogus-r1204, empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null, GIT_SSH_COMMAND=false, EMAIL=r1204@openreposhape.invalid, env -u LANES_LANE, TMPDIR in scratch, PYTHONDONTWRITEBYTECODE=1.
  • Python 3.12.3, git 2.43.0 (/usr/bin/git).
  • At most 2 of my pytest calls at once, each under 9 minutes; df -h /workspace (93-100G free) and df -i / (41-42%) checked before each.
call tree result
1a. the new file, tests/test_family.py, tests/test_family_add_member_id.py, tests/test_value_pattern_line_break.py head 198 passed in 225.61s
1b. #1227's tests/test_family_gitmodules_values.py, tests/test_validate_family_index_gitmodules.py, tests/test_validate_family_index_value_unmerged.py head 184 passed in 297.69s (1a + 1b = 382, the collected count)
2a. test_validate_family_{branch_step, duplicates, git_not_runnable, index_gitmodules, index_value_unmerged, member_id_pattern}.py head 232 passed in 354.36s
2b. test_validate_family_{member_path_glob, remedies_typed, typed_states, unnamed_mounts, unreadable, without_git}.py head 189 passed in 244.84s (2a + 2b = 421, all 12 files)
3. tests/test_readme_windows_quoting_sentence.py, tests/test_remove_add_remedies_family_root.py, tests/test_family_bump_to_checks.py, plus the new file, tests/test_value_pattern_line_break.py, tests/test_family_switch_branch_exists.py, tests/test_family_add_member_id.py, tests/test_agents_md_codes.py predicted merge with main 40cb8086 (bdb5ccb4, tree d0d49d5b) 253 passed in 230.23s
4. tests/test_scaffold_leg_path_collisions.py, tests/test_path_guard_hygiene.py, tests/test_no_auto_maintenance.py head 283 passed, 2 skipped in 42.35s; both skips tests/test_no_auto_maintenance.py:222: the session began with GIT_CONFIG_GLOBAL set
5. tests/test_agents_md_codes.py, tests/test_cli_reference.py, tests/test_repo_hygiene.py head 171 passed in 47.26s

Probes, in the scratch clone only, never committed:

  • test_r1204_probe_remedy.py: base 3 passed in 11.81s, head 3 passed in 11.75s, merge 3 passed in 13.29s;
  • test_r1204_probe_edges.py (head): 4 passed in 10.91s.

The 3.14-like sitecustomize shim was not needed: the change adds no is_file, exists, is_dir or stat call. _check_member_identity's _is_a was already there, and tests/test_path_guard_hygiene.py passes (call 4).

Merges (brief item 8)

git merge-tree --write-tree d56f2084 <other>:

Further checks:

The body (brief item 9)

What I did not cover

Lane: openreposhape-1 (openRepoShape-1)

…ember_ids_well_formed` says `member-pin-commit` refuses the run first where members are checked, and rows hold a pin that is no commit as `--to <commit>`, the holder's path quoted in the printed `bump`, and a bare `id: 0` as its digits (#1204)

Answers NITs 1-3 of review r1204 on #1273. No behavior of validate-family.py
changes; only the docstring does.

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

sonarqubecloud Bot commented Nov 8, 2026

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-11-08T21:07:50Z) Lane 1's round on r1204's review (issuecomment-6726873431): 0657e396 answers NITs 1-3. In validate-family.py, only a docstring changes.

Lane: openreposhape-1 (openRepoShape-1)

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-11-08T21:14:54Z) Independent delta check r1204b (r1204, resumed) of #1273 at 0657e396: READY, with no MUST, SHOULD or NIT. #1273 stays a draft while #1227 is open. It lands after #1227, from its place in the after-#1186 list, once CI's tests passes at its head.

r1204: delta review of #1273 at 0657e39642e95d1bec9da2180871827e0c8019a0

Verdict: READY

No MUST, SHOULD or NIT in this round.

Commit 0657e396, on top of d56f2084, settles my NITs 1 to 3 from round 1 (issuecomment-6726873431):

  • No behaviour change. The only change to validate-family.py is a docstring: the AST is equal once docstrings are blanked.
  • Mutants. The four survivors of round 1 (R6, R7, R11, R12) are now each killed by the row the round names.
  • The new rows assert what they claim. They fail on base where they should, and the control passes on both.
  • The body. The round section and the merge-tree lines match my measurements. Closes #1204 is the only closing form.

NITs 4, 5 and 7 stay open, as the body says. They were NITs and do not hold the PR. NIT 6 is refreshed: the merge-tree lines are current, and the Sourcery block is gone from the live body.

The review checkout /workspace/projects/openRepoShape-worktrees/l1-r1204 is now detached at 0657e396 and clean: git status --porcelain --ignored prints 0 lines.

Checks

1. The validate-family.py change is a docstring only.

  • git diff --numstat d56f2084 0657e396 gives 3 2 templates/family-root/scripts/validate-family.py and 63 6 tests/test_validate_family_member_id_pattern.py. The validator blob goes from c9a37c41 to 4e011252.
  • tools/ast_eq.py parses both blobs and blanks the first-statement string of the module and of every function and class: docstrings blanked: 134 / 134; AST equal once blanked: True; AST equal unblanked: False.

2. R6, R7, R11 and R12 are killed at 0657e396.

  • They are the same mutant definitions as round 1 (tools/mutate.py), applied one at a time in the scratch clone (switched to 0657e396), over the new file only.
  • After each, a finally wrote the file back from HEAD's blob and checked it against HEAD.
id mutant d56f2084 (round 1) 0657e396
R6 the row check reads str(member_id or "") survived, 41 passed KILLED, 1 failed, 43 passed in 40.76s: test_a_row_id_that_reads_as_a_number_is_its_digits[zero] (assert 1 == 0 under --no-members)
R7 _bump_to leaves the holder's path unquoted survived, 64 passed KILLED, 1 failed, 43 passed in 41.85s: test_the_rows_remedy_runs_as_printed_from_the_tracking_branch[space-and-apostrophe] (ValueError: No closing quotation)
R11 the identity check's row question reads str(recorded_id or "") survived, 41 passed KILLED, 1 failed, 43 passed in 40.58s: ..._its_digits[zero] (assert 0 == 1, members checked: the mismatch was dropped)
R12 the COMMIT_RE fallback to <commit> removed survived, 41 passed KILLED, 1 failed, 43 passed in 40.88s: test_a_pin_that_is_no_commit_is_named_to_commit (assert ['IRRS', '--to', 'abc'] == ['IRRS', '--to', '<commit>'])

Every restore printed restored: work tree 4e011252497e0dc276d092707b72c34117570078 HEAD 4e011252497e0dc276d092707b72c34117570078 EQUAL. With these four, my mutants stand at 14 of 14 killed.

3. The new rows assert what they claim, and the NIT 1 sentence matches probe e1.

  • test_a_pin_that_is_no_commit_is_named_to_commit:
    • Under --no-members, the one member-id-malformed finding, and the bump argv ends ['IRRS', '--to', '<commit>'].
    • With the members checked: exit 2, stderr starts with REFUSED member-pin-commit: IRRS: , and member-id-malformed is absent.
    • That is exactly what probe e1 measured at d56f2084 (exit 2, REFUSED member-pin-commit: IRRS: pin.commit is 'abc', which is not 40 hex. ..., the row's finding not printed).
    • It also matches the reworded docstring at validate-family.py:1982-1984: "...--to <commit>; where members are checked, member-pin-commit refuses the run first, and this finding follows once the pin is mended."
  • test_the_rows_remedy_runs_as_printed_from_the_tracking_branch[space-and-apostrophe]:
    • It moves the whole on_main holder to tmp_path / "it's a holder". The members' gitfiles and core.worktree are relative, so the move keeps them valid. It then runs the same assertions as [plain].
    • The switch's argv equals switch_argv(holder, "bump", "irrs\n"), the bump is typed as printed and exits 0, the row reads id: irrs with the pin unmoved, the tree is clean, and the gate is green in both modes.
    • R7 shows the quoting is held.
  • test_a_row_id_that_reads_as_a_number_is_its_digits[2026, zero]:
    • repr(value) == str(number) proves the row parses as an int, not a string.
    • --no-members: exit 0 and a family ok: last line.
    • Members checked: exactly FINDING member-id-mismatch: members/IRRS: project.yaml declares id 'irrs' but family.yaml records 0 for IRRS. ..., which is word for word probe e2's line at d56f2084.
  • Base against head for the round's rows: the base validator 4683c3cf was laid into the clone, then HEAD's 4e011252 was written back (EQUAL-TO-HEAD). Base gives 3 failed, 2 passed in 12.19s:
    • FAILED test_a_pin_that_is_no_commit_is_named_to_commit;
    • FAILED test_the_rows_remedy_runs_as_printed_from_the_tracking_branch[plain] and [space-and-apostrophe];
    • PASSED test_a_row_id_that_reads_as_a_number_is_its_digits[2026] and [zero], the controls.
    • At head all five pass (call D below). This matches the body's round table.

4. The body.

5. Merges. git merge-tree --write-tree 0657e396 <other>:

Run (probe environment, at 0657e396)

  • Probe environment: stub gh first on PATH, GH_TOKEN=bogus-r1204, empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null, GIT_SSH_COMMAND=false, EMAIL=r1204@openreposhape.invalid, env -u LANES_LANE, TMPDIR in scratch, PYTHONDONTWRITEBYTECODE=1.
  • Python 3.12.3, git 2.43.0. At most 2 of my pytest calls at once. df -h /workspace showed 85-86G free and df -i / showed 42%.
call result
D. tests/test_validate_family_member_id_pattern.py, tests/test_validate_family_branch_step.py, tests/test_family_add_member_id.py (review checkout) 81 passed in 84.54s
base rows (clone, base validator laid in) 3 failed, 2 passed in 12.19s, as expected (check 3)
R6, R7, R11, R12 (clone, new file) each 1 failed, 43 passed (check 2)

I ran nothing wider. The validator's code is unchanged (check 1), and the test change is confined to the one new file, which call D and the mutant runs cover.

Findings

  • MUST: none.
  • SHOULD: none.
  • NIT: none new. Round 1's NITs 4 (AGENTS.md line length and scope), 5 (no init command named under --no-members) and 7 (git 2.47.1 not run) remain, and the body's Known limits names each one.

What I did not cover

  • The writer's gate calls R1 to R5 (test_family.py, the scans, the doc trio, the template-text scans, and the predicted merge with main 27397a4b). I did not re-run them; the delta is a docstring plus rows in one test file.
  • git 2.47.1, CPython 3.14, Windows, and the PR's CI and SonarCloud status.

Lane: openreposhape-1 (openRepoShape-1)

This branch has not been deployed

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

Labels

None yet

Projects

None yet

1 participant