Skip to content

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

Draft
brettheap wants to merge 10 commits into
mainfrom
fix/1150-index-gitmodules-stacked

Conversation

@brettheap

@brettheap brettheap commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Goal

#1150: after this PR the family gate's check for values git dies on (gitmodules-value-refused) reads the index's .gitmodules beside the work tree's, so each remedy that says to stage the file ends with the line that stages it (a later value the work tree alone gives too, once a line above stages the file) and a value only the index gives is named; an index file git cannot parse is refused as gitmodules-unreadable instead of read as nothing; and the checks read no file while a merge leaves .gitmodules unmerged, as git reads none then, so gitlink-unnamed and the init stop ask nothing in that state.

This is a DRAFT on main. It is the follow-up lane 4 filed as #1150 from RV1124's review of #1124 (pull/1124#issuecomment-6321372469): the review's N1, N2 and N6 were outside #1124's own review round, each is older than #1124, and each was measured again before this change. #1124 has landed as dea964aa1, and this branch was restacked onto it (see "The restack"). Fix round 1 answers rv1227's review (issuecomment-6374575208) and merges main e04826db4 (#1208). The one script is templates/family-root/scripts/validate-family.py; with its tests the change is four files, +1032 and -78 lines against main 50869f384 (git diff --shortstat origin/main HEAD -- <the four files>).

Fix round 1: rv1227's review

rv1227's independent review (issuecomment-6374575208, PARTIAL at b031f4938, NOT READY) found M1, M2, S1, S2 and N1 to N6. Each was measured again before the change: git 2.43.0, offline through lane 5's run9.sh, on the branch merged with main e04826db4 (d95308e95). The probe is v124/n1150/d/probes/test_n1150d_probe.py in lane 5's scratch, never committed; its logs are in v124/n1150/d/probes-out/merged-d95308e/.

  • M1, the merge of main. d95308e95 merges main e04826db4 (test_validate_family_index_gitmodules.py and validate-family.py: a row pins path-twice's wording of whose file it puts back, the index's or HEAD's as the index holds none, a row pins the value check's reading of HEAD's where the deletion of .gitmodules is staged, and _listed_as_git_reads's docstring says it returns nothing where the work tree's file is on disk (#1181) #1208); its tree is a151d1110, the one git merge-tree predicted. test_validate_family_index_gitmodules.py and validate-family.py: a row pins path-twice's wording of whose file it puts back, the index's or HEAD's as the index holds none, a row pins the value check's reading of HEAD's where the deletion of .gitmodules is staged, and _listed_as_git_reads's docstring says it returns nothing where the work tree's file is on disk (#1181) #1208's new row test_1077_a_value_head_gives_where_the_deletion_is_staged_puts_head_s_back pinned the finding's lines ending at --unset-all and typed git add by hand.
    • Measured (m1-*.log): update = !true committed, the deletion of .gitmodules staged. The three printed lines (git checkout HEAD -- .gitmodules, the --unset-all, git add -- .gitmodules) typed in order, then a plain git commit: exit 0, HEAD holds no value, the index and the work tree equal it, git status is clean, and the gate passes. Typed without the stage line, the commit exits 1 (no changes added to commit) and HEAD keeps !true. The merged answer is right.
    • Change (8938d17df): the row expects git add -- .gitmodules last and types no git add by hand.
  • M2, the reads that turn on "no section" while .gitmodules is unmerged.
    • Measured (m2-*.log): with .gitmodules at stages 1 to 3, git reads no file. git submodule status and git submodule update --init exit 128 at every gitlink (no submodule mapping found, No url found for submodule path 'tools/x', and 'members/IRRS' for the whole update), and a plain clone of HEAD checks tools/x out (exit 0). The head said gitlink-unnamed of tools/x (no section gives it, a plain clone stops there) and, for a member not checked out, member-uninitialized said to write back a section the file gave. The family bootstrap, in the second state, says NOT CHECKED OUT, and not for want of a credential: members/IRRS (git reads no .gitmodules while a merge leaves it unmerged). Resolve the merge in .gitmodules and `git add .gitmodules`, then run this again.
    • Change: _check_gitlinks_elsewhere (gitlink-unnamed) and _init_stop_said ask nothing while .gitmodules is unmerged, as no section is read then. gitlink-unnamed says what a clone does, and which section the commit after the merge records is the merge's to say; resolved and staged, the gitlink is asked about again. member-uninitialized gives the plain update, whose make bootstrap names the cure.
    • The hand mount's two reads keep reading none, since that is what git's own git rm reads there (measured): a plain git rm of a clone whose .git is a directory, at a path a section gives, dies (could not lookup name for submodule 'members/IRQS', exit 128), and git rm --cached, the line _hand_cached prints because it reads none, exits 0 (main read the work tree's file and printed the plain line, which dies). With the work tree's file deleted, the plain git rm exits 0 and takes no section, so _plain_rm_said puts nothing back; HEAD's put back would resolve the merge to HEAD's side. _given_as_git_reads' docstring says so.
  • S1, a value the work tree alone gives after a staging line.
    • Change: _value_taken_out takes again, set by _check_gitmodules_values once a finding before it ends with the stage line: a later finding whose value the work tree's file alone gives then ends with git add -- .gitmodules too, and says why (STAGED_ABOVE: "A line above stages the work tree's .gitmodules, this value with it, so the lines below end staging it again.").
  • S2, a sparse checkout that leaves .gitmodules out: kept out of this PR, as a Limit and the follow-up validate-family.py: in a sparse checkout that leaves .gitmodules out, the put-back line git checkout -- .gitmodules matches no file and every git add -- .gitmodules the findings print exits 1, so no printed line set cures a finding there #1242.
    • Measured (s2-*.log): git checkout --ignore-skip-worktree-bits -- .gitmodules puts the index's file back (exit 0, the bit cleared), the --unset-all then exits 0, but git add -- .gitmodules still exits 1 (outside of your sparse-checkout definition); git add --sparse -- .gitmodules exits 0, and the lines so typed end well.
    • Why not here: the fix is a sparse-aware put-back and a sparse-aware stage line, and the stage line is one constant (STAGE_LINE) that about ten findings print, most of them not this PR's. Fixing only this PR's two findings would leave the others failing in the same state.
  • N1. _blob_paths and _blob_sections say they refuse an index's file git cannot parse (gitmodules-unreadable, through _blob_listed) and read none for no spec.
  • N2. A row for the refusal with the work tree's file on disk and clean (rv1227's e1b).
  • N3. A row for a real modify/delete conflict (stages 1 and 2), and an [on-disk] id for the sections row (rv1227's e2f). A value git dies on cannot stand in for the section in the modify/delete row: with one in the work tree's file, git merge itself dies (invalid value, exit 128, measured: n3-*.log).
  • N4. Where the index's file gives the key another value than the work tree's, the finding names it (The index's file, which a plain `git commit` records, gives it the value `!false`., INDEX_GIVES, _values_said) in place of "gives it too".
  • N5. GIVEN_WHERE's comment says what (False, False) is.
  • N6. In Limits.

What was measured before the change (the first round)

All probes are offline: run9.sh puts a stub gh first on PATH and sets GIT_SSH_COMMAND=false and GIT_CONFIG_GLOBAL=/dev/null, and git is 2.43.0. The probe is tests/test_n1150_probe.py, a scratch file that is not committed; its logs are probe-main.log (main 961640c6, 2026-10-22T20:08Z) and probe-parent.log (#1124's head 8dfa32bd, 20:09Z), in lane 5's scratch v124/n1150/. The scratch families have gitlinks at members/IRRS and members/IRSS. Paths such as mut/..., logs/... and probe-*.log below are in that scratch folder.

#1124's script was main's script in the first round. validate-family.py is blob dfb50769f at 8dfa32bd, at dea964aa1 and at origin/main (50ce8ab15), and git diff 8dfa32bd dea964aa1 over validate-family.py and the two older test files this PR changes is empty. So the column "on #1124's head" below is main's script as it stands.

N1, the index's file beside the work tree's on disk.

N2, an index file with a line git cannot parse.

N6, an unmerged .gitmodules (stages 1 to 3 in the index, none at stage 0).

Why the branch was cut on #1124. Every item above holds on main 961640c6 too, but the code each change touches (_blob_listed, READ_HERE, _listed_as_git_reads, the "then stage" ending) was #1124's, and a branch on main would have rewritten the hunks #1124 rewrites.

What changed (the first round)

templates/family-root/scripts/validate-family.py (+195 and -33 lines against dea964aa1):

  • N1: the index's file is read beside the work tree's.
  • N2: an index file git cannot parse is gitmodules-unreadable. _blob_listed refuses with that code, as _head_listed does for HEAD's, naming the index's file and git's own bad config line text, and its remediation says to mend the file, stage it with git add -- .gitmodules, and run again. git config --blob prints the entries before such a line and then exits 128; the old read returned nothing on that 128 and so dropped them. This is the second of the two options validate-family.py: the check for values git dies on never reads the index's .gitmodules where the work tree's is on disk, an index blob with a line git cannot parse after a value is family ok, and an unmerged .gitmodules is read as the work tree's or HEAD's where git reads no file (follow-ups from RV1124's review of #1124) #1150 names (the refusal), not the first (reading up to the line as bootstrap does); it is the same code HEAD's file already gets (P9h).
  • N6: no file is read while a merge leaves .gitmodules unmerged. _gitmodules_unmerged (git ls-files -s, no stage 0) is the test, as bootstrap's gitmodules_source reads it.
    • The value check reads nothing then (UNMERGED_HERE: "the file git reads here, which is none while a merge leaves .gitmodules unmerged") and keeps HEAD's, which a clone reads. Its remedy is "Resolve the merge in .gitmodules by hand, leaving that key out, then stage and commit .gitmodules:" and the stage line, with no put-back line.
    • _sections_as_git_reads returns no section while unmerged, so gitmodules-path-twice and gitmodules-path-unnamed read none and print no git checkout HEAD -- .gitmodules.
    • One finding changes: P6b. A value only the work tree's file gives, on disk, while .gitmodules is unmerged and HEAD's is clean, was gitmodules-value-refused and is family ok now, until the merge is resolved and staged. Git reads no file there either, so the value is no finding yet. After git add, it is named as N1's. See Limits.
  • The module docstring's WHAT IS CHECKED bullets and the docstrings of the functions above say all of this.

tests/test_validate_family_index_value_unmerged.py (new, +410 lines, 9 rows in 7 functions). Every row types only the printed git lines, in order, and does by hand only what the text says in words; then a plain git commit is made, and the row asserts that HEAD is as it should be, that the gate passes in every mode, and that a plain clone checks both members out (ends_well). The premises are measured in each row: git's own exit code, and a plain clone of the commit that records the staged file.

  • P1: test_a_value_the_work_tree_s_file_and_the_index_s_give_ends_staging_it.
  • P1b: test_a_value_the_index_s_file_alone_gives_is_named_beside_a_clean_one (a plain clone of the commit that records it dies, exit 128).
  • The M3 row: test_a_value_here_and_in_head_ends_staging_it_with_no_index_s_file. HEAD's file and the work tree's give update = !true, and git rm --cached has taken .gitmodules out of the index. See "Mutants".
  • P9 and P9t: test_an_index_s_file_git_cannot_parse_is_refused_unreadable ([a-value], [a-section]): git's submodule status exits 128 and 0, the gate refuses with exit 2 in all three modes, the remediation's git checkout and git add lines are typed, and then the finding the file gave before the bad line is the one left.
  • P6a: test_a_value_head_gives_while_a_merge_leaves_the_file_unmerged ([deleted], [on-disk]).
  • P6b: test_a_value_the_work_tree_alone_gives_while_unmerged_waits_for_git: every mode exits 0 while unmerged; after git add, the finding is N1's and its lines, typed, end well.
  • Sections: test_no_section_is_read_while_a_merge_leaves_the_file_unmerged: no finding and no git checkout HEAD while unmerged; resolved to HEAD's side and staged, gitmodules-path-unnamed names the stale section.

tests/test_family_gitmodules_values.py (+27 and -14 lines) and tests/test_validate_family_index_gitmodules.py (+11 and -12 lines): older rows re-pinned to the stage line and the refusal.

  • taken_out, the expected finding every row of the first file builds, ends with STAGE wherever HEAD's file gives the value, and WHERE["head-only"] ends ":" and the stage line.
  • test_the_index_s_file_cut_short_is_put_back_from_the_index had as its premise that the gate cannot name the cut line, because it read the index's file as nothing. The premise is now the refusal: exit 2, REFUSED gitmodules-unreadable: the index's .gitmodules could not be read as git reads it: , and bad config line named. The row's cure is unchanged.
  • Two test_1077_ rows (the index alone gives a value; a url git dies on is written back) expect the stage line last and no longer type git add by hand.

Rows that fail on main (fix round 1)

Run on 2026-10-24 at this head, 0d65ea80c, with validate-family.py as main 50869f384 holds it (blob 172647b3e, sha256 30abb4ea28e6, unchanged since e04826db4) written into the worktree, then the head's script (blob 18a0942d4, sha256 30dc80fe29de) restored and its sha256 checked, git status clean after. The logs are in lane 5's scratch v124/n1150/d/mut/ (summary.log holds the runner's lines).

The three test files in full: 39 failed, 142 passed (main-full.log, 218.81s; the failed ids are listed in main-full-failed.txt). They are the 29 rows that failed in the first round's comparison, with main's script then (blob dfb50769f, see below), plus the 10 ids this round adds or re-pins. The 142 that pass are rows that name nothing this PR changes, as many as in the first round's comparison.

The 10 ids of the fix round, run on their own: all fail (main.log, 11 ids, 11 failed in 26.01s; the 11th is the sections row's [deleted] id, which the first round added).

row (fix round item) on main's script
test_1077_a_value_head_gives_where_the_deletion_is_staged_puts_head_s_back (M1, #1208's row re-pinned) the lines end at the --unset-all; the row expects git add -- .gitmodules last
test_a_value_the_work_tree_alone_gives_after_a_staging_one_stages_again[staged], [committed] (S1) the second finding has no STAGED_ABOVE sentence and ends at its --unset-all, with no stage line
test_another_value_the_index_s_file_gives_the_key_is_named (N4) the finding ends at its --unset-all; the row expects the index's value !false named and the stage line last
test_an_index_s_file_git_cannot_parse_beside_a_clean_one_is_refused (N2) family ok, exit 0, where the row expects exit 2 and gitmodules-unreadable
test_no_section_is_read_while_a_merge_leaves_the_file_unmerged[on-disk] and test_no_section_is_read_in_a_modify_delete_conflict (N3) one finding, exit 1, whose lines take the stale members/Old section out, where the row expects every mode to pass while unmerged
test_no_gitlink_outside_members_is_asked_about_while_unmerged[deleted], [on-disk] (M2) one finding, exit 1, whose lines end with --unset-all submodule.tools/x.url and the stage line, where the row expects every mode to pass while unmerged
test_a_member_not_checked_out_while_unmerged_is_told_the_plain_update (M2) member-uninitialized's remediation says to write the section for members/IRRS back, where the row expects the plain update

Rows that fail on main (the first round)

The three test files at the first round's head, b031f4938, were run against validate-family.py as main holds it (blob dfb50769f, #1124's script, written into the worktree and restored, sha256 844c1bee1d62 checked; the run is at 4b70f2ec6, whose four files are byte-identical to b031f4938's): 29 failed, 142 passed (mut/parent-full-4b70f2e.log). The 142 are rows that do not name anything this PR changes.

rows count on main
test_a_value_git_dies_on_fails_bootstrap_and_every_mode_of_the_gate (8 ids), test_a_holder_with_every_member_checked_out_fails_on_it_too, test_a_value_git_dies_on_in_the_index_s_file_is_named_and_mended, test_two_keys_with_no_gitmodules_in_the_work_tree_put_it_back_once, test_a_url_no_row_gives_one_for_is_written_back_as_a_placeholder, test_a_url_or_path_left_with_no_value_is_written_back_as_add_writes_it (3 ids) 15 the finding ends at its last key line; the row expects git add -- .gitmodules after it
test_the_gate_says_which_file_gives_it[head-only] 1 "Stage and commit the work tree's .gitmodules, which does not give it." with no line
test_the_index_s_file_cut_short_is_put_back_from_the_index (2 ids) 2 family ok, exit 0, where the row expects exit 2 and gitmodules-unreadable
the two test_1077_ rows 2 the lines end with the --unset-all or the url line, and no stage line
new: all 9 rows of tests/test_validate_family_index_value_unmerged.py 9 P1 and the M3 row: the finding has no stage line (P1's also lacks the "gives it too" sentence). P1b, P9 and P9t: the gate passes, exit 0, where the row expects a finding or a refusal. P6b and the sections row: the gate gives a finding where the row expects exit 0. P6a (2 ids): the finding says "here and in HEAD", says nothing of the merge, and with the file deleted puts HEAD's back

At the first fold (442615eb3), the same comparison over the 15-row selection the mutant runner uses gave 14 failed, 1 passed (mut/parent.log). The M3 row alone, run on the tree before it was committed: 1 passed on the head's script, 1 failed with #1124's script and 1 failed with main's script 961640c6 (mut/row1-head.log, row1-parent.log, row1-main.log).

Mutants (fix round 1)

Run on 2026-10-24 at this head, 0d65ea80c, with git 2.43.0. Each of the 10 was applied alone to validate-family.py in the worktree (v124/n1150/d/mut/mutants.py in lane 5's scratch: one exact replacement, checked to occur once in the committed script), its rows were run through run9.sh, and the head's script was restored with sha256 30dc80fe29de checked; git status was clean after each (d/mut/summary.log, one log per mutant beside it). No other test run or gate used the worktree while a mutant was applied. R1 to R5 are rv1227's own (v129/rv1227/probes/mutants.py): R1, R2, R4 and R5 byte for byte, and R3 the same change moved to the line N4 rewrote. D1 to D5 are one for each new behaviour of this round. 10 of 10 are killed. Each ran over its own rows (the -k selections in d/mut/run-mut.sh), not the full files.

mutant what it changes result
R1 _gitmodules_unmerged is true only with all three of stages 1 to 3, so not in a modify/delete conflict's two killed, 1 failed: test_no_section_is_read_in_a_modify_delete_conflict
R2 _sections_as_git_reads reads the work tree's file on disk before it asks whether .gitmodules is unmerged killed, 2 failed: the sections row's [on-disk] id and the modify/delete row (the [deleted] id passes)
R3 the INDEX_TOO sentence is said wherever the work tree's file gives the value, where HEAD's gives it too (where[0] for where == (True, False)) killed, 1 failed: test_a_value_the_work_tree_alone_gives_after_a_staging_one_stages_again[committed]
R4 where the work tree's file does not give a value HEAD's gives, the remedy ends "Stage and commit the work tree's .gitmodules, which does not give it." with no stage line killed, 1 failed: test_the_gate_says_which_file_gives_it[head-only]
R5 _index_listed reads an index file git cannot parse as nothing where the work tree's file is on disk: the refusal is swallowed killed, 1 failed: test_an_index_s_file_git_cannot_parse_beside_a_clean_one_is_refused (N2)
D1 S1: again is never passed (again=False), so no later finding stages the file again killed, 2 failed: the S1 row, [staged] and [committed]
D2 S1: staging is carried only by a value HEAD's file gives (committed), not by a staged one killed, 1 failed: the S1 row's [staged] id
D3 N4: "gives it too" (INDEX_TOO) for any value the index's file gives the key, not only the same value killed, 1 failed: test_another_value_the_index_s_file_gives_the_key_is_named
D4 M2: _check_gitlinks_elsewhere (gitlink-unnamed) asks while .gitmodules is unmerged killed, 2 failed: test_no_gitlink_outside_members_is_asked_about_while_unmerged, [deleted] and [on-disk]
D5 M2: _init_stop_said asks while .gitmodules is unmerged killed, 1 failed: test_a_member_not_checked_out_while_unmerged_is_told_the_plain_update

Mutants (the first round)

Each of the 13 was applied alone in the worktree (mut/mutants.py: one exact replacement, checked to apply once), the rows run through run9.sh, and the script restored with sha256 844c1bee1d62 checked; git status was clean after (dirty after: 0). 13 of 13 are killed. Twelve of them (all but M3) ran at 442615eb3 over a 15-row selection: the 8 rows the new file had then, plus the 7 older rows the change re-pinned (test_a_value_git_dies_on_fails_bootstrap_and_every_mode_of_the_gate[update-command], both ids of test_the_gate_says_which_file_gives_it, both of test_the_index_s_file_cut_short_is_put_back_from_the_index, and the two test_1077_ rows). No later commit of the first round changed the script (blob b81da47ad at 442615eb3, 4b70f2ec6 and b031f4938).

mutant what it changes result
M1 _index_listed returns nothing: the index's file is never read beside the work tree's killed, 4 failed: P1, P1b, P9 [a-value], P6b
M2 staged=not present or (name, key) in in_index cut to staged=not present killed, 3 failed: P1, P9 [a-value], P6b
M3 if committed or staged: cut to if staged: in _value_taken_out survived at 442615eb3: 15 passed over the selection, and 170 passed over the three full files. Killed at 4b70f2ec6: 1 failed (the M3 row), 170 passed, over the three full files
M4 the loop over the keys leaves out the index's killed, 1 failed: P1b
M5 the INDEX_TOO sentence is not said killed, 2 failed: P1, P6b
M6 "Stage and commit" for every case killed, 1 failed: P1b
M7 _value_not_here ends with a full stop, no stage line killed, 2 failed: P1b, test_the_gate_says_which_file_gives_it[head-only]
M8 _blob_listed returns nothing on the 128, as #1124's did killed, 4 failed: P9 [a-value] and [a-section], both ids of the cut-short row
M9 _gitmodules_unmerged is never true killed, 4 failed: P6a (2 ids), P6b, the sections row
M10 _gitmodules_unmerged is true with a stage 0 entry too (bool(stages)) killed, 13 failed
M11 _sections_as_git_reads still reads a section while unmerged killed, 1 failed: the sections row
M12 the value check still reads its file while unmerged killed, 3 failed: P6a (2 ids), P6b
M13 _index_listed reads the blob without first asking whether the index holds one at stage 0 killed, 2 failed: P6a [on-disk], P6b

M3, why it survived and what kills it. M3 differs from the head only where the work tree's file is on disk and gives the value, HEAD's gives it, and the index's does not. Every existing row with HEAD's value also had it in the index, or no work tree file, so staged was true anyway. The M3 row has that state. Under M3 the finding ends at the --unset-all line, a plain git commit after it records .gitmodules gone from HEAD (ls-tree HEAD .gitmodules is empty, ?? .gitmodules), and the gate then finds member-gitmodules-absent, exit 1 (mut/probe-M3.log). On the head, HEAD keeps .gitmodules, git status is clean and the gate is family ok (mut/probe-head.log). The row is the only change of content added since the fold (4b70f2ec6, +30 lines).

Runs (fix round 1)

All through lane 5's run9.sh, each START line recording the head and dirty=0, with the stub gh, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null and EMAIL exported. The logs are in lane 5's scratch v124/n1150/d/logs/. The full suite was not run. The runs with main's script and with the mutants are in their own sections above.

At this head, 0d65ea80c (2026-10-23T22:03Z to 22:16Z):

run git result log
the three test files, tests/test_path_guard_hygiene.py, tests/test_repo_hygiene.py, tests/test_agents_md_codes.py, tests/test_cli_reference.py, tests/test_pinned_copy_text.py 2.43.0 560 passed in 329.42s gate243-e-0d65ea8.log
the three test files 2.47.1 181 passed in 267.16s gate247-e-0d65ea8.log
nine files that pin the script's texts elsewhere, in four runs of under 100 rows: tests/test_family_holder_remedies_compose.py, tests/test_family_unnamed_gitlink.py and tests/test_validate_family_remedies_typed.py; tests/test_family_holder_bootstrap_texts.py and tests/test_family_hand_mount_git_rm_lines.py; tests/test_validate_family_typed_states.py and tests/test_validate_family_unnamed_mounts.py; tests/test_family_bootstrap_gate_order.py and tests/test_family_head_lost_gitmodules.py 2.43.0 330 passed (89, 86, 83 and 72) disc9-A.log to disc9-D.log

At 8938d17df, the M1 commit (2026-10-23T21:41Z to 21:52Z):

run git result log
the three test files and the same five hygiene files 2.43.0 551 passed in 417.67s gate243-m1-8938d17.log
the three test files 2.47.1 172 passed in 264.09s gate247-m1-8938d17.log

Before 0d65ea80c was committed, its tree ran on the work tree it was then committed from, byte for byte (dirty=2, so these are not gates): the new test file, 18 passed in 85.45s (disc-new-e.log); the two older test files, 163 passed in 203.44s (disc-old2-e.log).

No row failed in any of these runs.

Runs (the first round)

All through lane 5's run9.sh, each START line recording the head and dirty=0, with the stub gh, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null and EMAIL exported. The full suite was not run.

At the first round's head, b031f4938 (2026-10-23T18:24Z to 18:34Z; logs in lane 5's scratch v129/g1150/):

run git result log
the three test files, tests/test_pinned_copy_text.py, tests/test_template_text_placeholders.py, tests/test_repo_hygiene.py, tests/test_agents_md_codes.py, tests/test_cli_reference.py 2.43.0 571 passed in 360.06s gate243.log
the three test files 2.47.1 171 passed in 232.96s gate247.log

At 4b70f2ec6 (2026-10-23T12:10Z to 12:15Z; its four files are b031f4938's byte for byte; v124/n1150/logs/):

run git result log
the three test files and tests/test_repo_hygiene.py 2.43.0 271 passed in 271.14s gate243-4b70f2e.log
the three test files 2.47.1 171 passed in 269.78s gate247-4b70f2e.log

At f78107c38, a local WIP commit that was folded into 442615eb3 before any push (its tree equals the fold's; 2026-10-22T22:18Z): the three test files, 2.43.0, 170 passed in 230.25s (upd-f78107c.log).

At 442615eb3, the fold (2026-10-22T22:24Z to 22:54Z; same script and same two older test files, the new file one row shorter; these ran on #1124's head as the base, before the restack):

run git result log
the 60 files of tests/test_family*.py and tests/test_validate_family*.py, shard A (30 files) 2.43.0 1164 passed, 4 skipped in 1275.69s (the 4 are tests/test_family_operation_reftable.py rows, which need git 2.45 or later) gate-shardA.log
the same, shard B (30 files) 2.43.0 1480 passed, 20 skipped in 1588.90s (16 and 3 in tests/test_family_init_values.py, 1 in tests/test_family_member_make_jobserver.py) gate-shardB.log
the three test files 2.47.1 170 passed in 182.05s gate247.log
tests/test_path_guard_hygiene.py, tests/test_repo_hygiene.py, tests/test_agents_md_codes.py, tests/test_cli_reference.py, tests/test_pinned_copy_text.py, on a scratch merge 7aefff483 of the fold and main 961640c68 (the stack lacked the first file) 2.43.0 307 passed in 45.99s hyg.log

No failure in any run.

The restack

The branch was cut on #1124's head 8dfa32bd. After #1124 landed, lane 5's restack3.sh restacked it with three merges; nothing was amended, rebased or forced. First #1124's final head 2d3bea2df (merge 1004b6a73). Then its squash dea964aa1449b62d9c2bbafc972bbc344631e0b2, taking the branch's side for the three files that conflicted, validate-family.py, tests/test_family_gitmodules_values.py and tests/test_validate_family_index_gitmodules.py (merge b031f4938fa8604969e523a093da2735d7fb3523). Then main, which was dea964aa1 itself when the restack ran at 18:22Z (that commit is dated 18:16Z and the next main commit 18:32Z), so the third merge found nothing to add and made no commit. The push was a fast-forward from 4b70f2ec6, and git ls-remote reads b031f4938 on fix/1150-index-gitmodules-stacked. The four files at b031f4938 are byte-identical to 4b70f2ec6's (an empty git diff 4b70f2ec6 b031f4938 over them), and against dea964aa1 the three older ones differ by this PR's lines alone, so the mutants, the rows on #1124's script and the runs at 4b70f2ec6 describe b031f4938's bytes.

Merges

Read at 2026-10-24T14:16:57Z with git 2.43.0, origin/main at 50869f384 (#1200) and the branch at 0d65ea80c (v124/n1150/d/logs/merge-trees-f-0d65ea8.log in lane 5's scratch). The branch holds main up to e04826db4 (#1208, merged in fix round 1 as d95308e95). Main's two commits since then touch none of this PR's four files (git diff --name-only e04826db4 origin/main -- <the four files> is empty), so main is not merged again: 0ed3e57f8 (#1121) changes adopt-project.py and tests/test_adopt_next_make_reads_first.py, and 50869f384 (#1200) changes shape-doctor.py and tests/test_doctor_unreadable_paths.py.

Limits

  • P6b changes a finding. Unmerged, the work tree's file on disk giving a value HEAD's does not, was gitmodules-value-refused on main and on validate-family.py: gitmodules-path-twice and the check for values 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) #1124's head and is family ok (exit 0, every mode) now, until the merge is resolved and staged; the row holds it, and lane 5's coordinator accepted it. Git reads no file in that state either (submodule status exits 128, no submodule mapping found): it dies at the first gitlink, not on the value. After git add, the finding comes back, with N1's lines.
  • An unmerged .gitmodules is still no finding of its own. The gate is family ok while git's submodule status exits 128, and since the fix round (M2) it also asks no question of a gitlink outside members/ and says no init stop while the file is unmerged: member-uninitialized gives the plain update, which exits 128 until the merge is resolved, and its make bootstrap names that cure. validate-family.py: the check for values git dies on never reads the index's .gitmodules where the work tree's is on disk, an index blob with a line git cannot parse after a value is family ok, and an unmerged .gitmodules is read as the work tree's or HEAD's where git reads no file (follow-ups from RV1124's review of #1124) #1150 cites v1.1.1's release notes, which list the unmerged case as a known limit; this PR makes the checks agree with git on which file is read and adds no finding for the state. No documentation file is changed by this PR, and whether a page that describes the limit should change was not checked.
  • A line set that ends in a commit cannot be committed while a merge leaves .gitmodules unmerged: git refuses any commit then (Committing is not possible because you have unmerged files, exit 128). The value check's own remedy there says to resolve the merge first; the others say nothing of it.
  • A hand mount's plain git rm dies while .gitmodules is unmerged and on disk (measured, git 2.43.0, m2-hand-checkout-section-unmerged-on-disk.log): Cannot change unmerged .gitmodules, resolve merge conflicts first, exit 128, after git removed the checkout from disk; the gitlink stays in the index, and its store under .git/modules is kept. Main prints the same plain git rm in that state, and _hand_cached asks no question of the merge. Not changed here.
  • A sparse checkout that leaves .gitmodules out (rv1227's S2), measured: the put-back git checkout -- .gitmodules exits 1 (did not match any file(s)), so the --unset-all after it exits 5, and git add -- .gitmodules exits 1 (outside of your sparse-checkout definition) even once the file is back; a plain git commit after the lines records what the index held. --ignore-skip-worktree-bits and git add --sparse work there. This holds for every finding that prints a put-back or a stage line, as on main. The follow-up is validate-family.py: in a sparse checkout that leaves .gitmodules out, the put-back line git checkout -- .gitmodules matches no file and every git add -- .gitmodules the findings print exits 1, so no printed line set cures a finding there #1242.
  • A real content conflict never reaches the unmerged code (rv1227's N6). It leaves conflict markers in the work tree's file, so the gate refuses gitmodules-unreadable on that file first, as main does (rv1227's e2b). The unmerged path is reached by a merge resolved by hand and not yet staged (the rows' synthetic stages 1 to 3), or by a modify/delete conflict (stages 1 and 2, or 1 and 3).
  • Not measured: member-gitlink-removal-staged while .gitmodules is unmerged. Its put-back (_section_back, through _gitmodules_put_back) finds no index entry at stage 0 there, so by reading it falls to HEAD's file, git checkout HEAD -- .gitmodules, "HEAD's, as the index holds none", which resolves the merge to HEAD's side. No row runs that state.
  • What ran where. At this head, the three test files and the five hygiene files ran on git 2.43.0, with the nine files that pin the script's texts elsewhere, and the three test files ran on 2.47.1. The 60 files of tests/test_family*.py and tests/test_validate_family*.py ran at 442615eb3 alone, before the restack and fix round 1, on git 2.43.0, and the 4 reftable rows they skip there ran on no git. Each fix-round mutant ran over its own rows, not the full files. Main's commits since e04826db4 touch none of the four files and are not merged, and no gate ran on the predicted merge with main 50869f384 or with either open pull request's head: those were merge-treed only. The full suite was not run.

Closes #1150

Lane: openreposhape-5 (openRepoShape-5)

🤖 Generated with Claude Code

Summary by Sourcery

Align family validation with Git’s effective .gitmodules reads across the work tree, index, HEAD, and unmerged states.

New Features:

  • Make family validation inspect the staged .gitmodules alongside the work-tree and HEAD versions, identifying values that a commit would record.
  • Handle unmerged .gitmodules states consistently with Git by treating them as unreadable by the checks until the merge is resolved.
  • Report unparsable indexed .gitmodules files as gitmodules-unreadable with remediation guidance.

Bug Fixes:

  • Prevent validation from overlooking Git-failing values present only in the index or from treating unparsable indexed configuration as empty.
  • Avoid producing misleading section and value findings while .gitmodules remains unmerged.

Enhancements:

  • Ensure generated remedies that modify .gitmodules include the required git add -- .gitmodules staging command and accurately describe indexed values and merge states.

Tests:

  • Add comprehensive coverage for indexed values, unparsable .gitmodules content, unmerged conflicts, staged remedies, and related section checks.

Brett Heap and others added 7 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>
@brettheap brettheap added this to the v1.2.1 milestone Oct 23, 2026
@sourcery-ai

sourcery-ai Bot commented Oct 23, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates family validation to mirror Git’s .gitmodules behavior across the work tree, stage-0 index, HEAD, malformed index blobs, and unmerged merge stages, while making remediation commands fully actionable and adding comprehensive regression tests.

Sequence diagram for staged and work-tree .gitmodules value validation

sequenceDiagram
    participant Gate as FamilyGate
    participant WT as WorkTree
    participant Index as Stage0Index
    participant Head as HEAD
    participant Git as Git

    Gate->>WT: _gitmodules_listed()
    Gate->>Head: _head_listed()
    Gate->>Git: _gitmodules_unmerged()
    alt work-tree .gitmodules exists
        Gate->>Index: _index_listed()
        Index->>Git: rev-parse --verify --quiet :.gitmodules
        Index->>Git: config --blob : .gitmodules --list
        Gate->>Gate: Compare work-tree, index, and HEAD values
        Gate-->>Gate: Add INDEX_TOO when index also gives value
    else work-tree .gitmodules is absent
        Gate->>Index: _index_listed()
        Index->>Git: config --blob :.gitmodules --list
        Gate-->>Gate: Validate the index file Git reads here
    end
    Gate-->>Gate: Emit gitmodules-value-refused
    Gate-->>WT: Remediation ends with git add -- .gitmodules
Loading

State diagram for unmerged .gitmodules handling

stateDiagram-v2
    [*] --> Stage0
    [*] --> Unmerged
    Stage0 --> WorkTreeRead: .gitmodules on disk
    Stage0 --> IndexRead: no work-tree file
    IndexRead --> Unreadable: git config --blob fails
    IndexRead --> Valid: blob parses
    WorkTreeRead --> ValueCheck
    Valid --> ValueCheck
    Unmerged --> NoFileRead: no stage-0 entry
    NoFileRead --> AwaitResolution: no value or section read
    AwaitResolution --> Stage0: git add -- .gitmodules
    Unreadable --> Repair: mend file and stage it
    Repair --> Stage0
    ValueCheck --> [*]
Loading

Flow diagram for Git-aware .gitmodules validation

flowchart TD
    A["Check .gitmodules"] --> B{".gitmodules unmerged?"}
    B -->|Yes| C["Read no file"]
    C --> D["Use HEAD only for comparison"]
    D --> E["Resolve merge, then git add -- .gitmodules"]
    B -->|No| F{"Work-tree file exists?"}
    F -->|Yes| G["Read work-tree file"]
    F -->|No| H["Read stage-0 index file"]
    H --> I{"Index blob parseable?"}
    I -->|No| J["Refuse as gitmodules-unreadable"]
    I -->|Yes| K["Compare values with HEAD"]
    G --> K
    K --> L["Report gitmodules-value-refused when Git would die"]
    L --> M["Apply remediation and git add -- .gitmodules"]
Loading

File-Level Changes

Change Details Files
Align .gitmodules value and section checks with Git’s index and merge-state read semantics.
  • Add stage-0 index reading alongside the work tree and HEAD, including values present only in the index.
  • Treat unmerged index entries as no readable .gitmodules and suppress value/section reads until resolution.
  • Append git add -- .gitmodules to applicable remediation commands and describe index-only findings accurately.
templates/family-root/scripts/validate-family.py
Refuse malformed index .gitmodules files instead of silently treating them as empty.
  • Propagate a gitmodules-unreadable refusal with Git’s parse diagnostics and remediation instructions.
  • Preserve and validate entries preceding the malformed line after the file is repaired.
templates/family-root/scripts/validate-family.py
tests/test_family_gitmodules_values.py
Expand regression coverage for index-only, malformed, and unmerged .gitmodules states.
  • Add scenarios covering index/work-tree divergence, malformed index blobs, merge stages, section handling, remediation typing, commits, and clone behavior.
  • Update existing expectations for refusal status and staging commands.
tests/test_validate_family_index_value_unmerged.py
tests/test_family_gitmodules_values.py
tests/test_validate_family_index_gitmodules.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1150 When the work-tree .gitmodules is on disk, check the index copy as well, report values found only in the index or in both the work tree and index, and include the staging command in remedies that require staging. ✅
#1150 Refuse an index .gitmodules blob that Git cannot parse instead of treating it as empty, reporting it as gitmodules-unreadable. ✅
#1150 When .gitmodules is unmerged in the index, read no work-tree, index, or HEAD copy in the affected checks, matching Git’s behavior, and provide a remedy to resolve and stage the merge. ✅

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 #1227 (#1150) for lane openRepoShape-5

PARTIAL at b031f4938 (stopped at the usage limit)

The usage limit stopped the review before the mutants and the rows-on-main run. What did run already gives two MUSTs.

All probes ran offline through run9.sh: a stub gh, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null, git 2.43.0 unless stated otherwise. The probe file is v129/rv1227/probes/test_rv1227_probe.ran1.py (14 states, never committed). Its logs are in v129/rv1227/out/head/ and out/main/. The second set ran with main's script, blob dfb50769f, swapped in; the head's script was put back after (sha256 844c1bee1d62 checked, git status clean).

MUST

M1. The PR no longer merges cleanly in behaviour with current main: one row fails.

M2. While .gitmodules is unmerged, every check reading through _given_as_git_reads now finds no section, so a correct finding becomes a false one.

  • Where: validate-family.py:3010 (_sections_as_git_reads returns {}). Its callers are _given_as_git_reads at 2988, gitlink-unnamed at 2962, the hand-mount checks at 3108 and 3257, and _init_stop_said through sections at 4368/4393.
  • Probe e2d: tools/x is mounted with its section and committed. Then .gitmodules is put at stages 1, 2 and 3, with the work tree's file on disk as HEAD's.
    • Main's script gives gitmodules-path-unnamed. Its four lines typed, then a plain commit, end family ok in all three modes.
    • The head gives, in every mode: FINDING gitlink-unnamed: tools/x is a gitlink ... that no section of .gitmodules gives, so in a plain clone of this holder git submodule update --init stops there (No url found for submodule path, exit 128) ... and a recursive clone checks out nothing at it.
    • That is false: a plain clone reads HEAD's file, which gives tools/x a url.
    • Its remedy says "commit that". After git rm --cached -- tools/x, the commit exits 128 (Committing is not possible because you have unmerged files), and the next run says "Commit that removal" again.
  • Probe e2e: a member deinit'd, .gitmodules unmerged.
    • The head's member-uninitialized now says to "Write the section of .gitmodules for members/IRRS back as family.py add writes one ... and commit .gitmodules".
    • That section is in the work tree's file and in HEAD's; what blocks git is the unresolved merge. Main gave the plain remedy.
  • The body's What changed and Limits name only gitmodules-path-twice and gitmodules-path-unnamed as reading no section while unmerged.
  • Fix: while unmerged, have _check_gitlinks_elsewhere, the hand-mount checks and _init_stop_said ask nothing, or read HEAD's file (what a clone reads), instead of reading "no section". Add a row for e2d's state.

SHOULD

S1. A staged value followed by a work-tree-only value still ends in HEAD (validate-family.py:2116-2129).

  • Probe e4o1: IRRS has update = !true in the work tree's file and staged. IRSS has update = !false in the work tree's file alone. HEAD is clean.
    • The IRRS finding ends git add -- .gitmodules. The IRSS finding comes after it and ends at --unset-all with no stage line.
    • Typed in order, then a plain commit: HEAD gives update = !false for IRSS. The next run says in HEAD, which a clone reads (exit 1).
    • Main recorded IRRS's !true in the same state. So N1 is moved, not closed: the gate's own git add now stages a value that was never staged.
  • Probe e4o2: IRRS's value committed instead of staged gives the same result, and HEAD gains !false.
  • Control (e4o3): in the reverse order, the lines typed then a plain commit leave HEAD clean.
  • Fix: once any finding of this check prints STAGE_LINE, end each later work-tree finding with it too (or print the staging findings last). Add e4o1's row.

S2. In a sparse checkout that leaves .gitmodules out (non-cone /* !/.gitmodules), none of the printed lines works.

  • Probe e3a: a value is staged in the index's file.
    • git checkout -- .gitmodules exits 1 (did not match any file(s)).
    • --unset-all exits 5.
    • The new git add -- .gitmodules exits 1 (outside of your sparse-checkout definition).
    • A plain commit then records !true in HEAD. Main fails the same way on its two lines.
  • Probe e3b: the index's file git cannot parse.
    • The head now refuses it correctly (exit 2; main said family ok).
    • The remediation's git checkout -- .gitmodules (validate-family.py:4009) exits 1, and its git add exits 1.
  • The body's Limits says this is "not measured"; it is measured now.
  • Fix: print --ignore-skip-worktree-bits and git add --sparse where ls-files -t says S, as bootstrap does, or state the measured result in Limits.

NIT

  • N1. Stale docstrings. validate-family.py:3961-3974: the docstrings of _blob_paths and _blob_sections still say "none where git cannot read it". _blob_listed now raises gitmodules-unreadable.
  • N2. No row pins the refusal with the work tree's file on disk. Probe e1b: the index's file has a bad line, and the work tree's file is on disk and clean. The head refuses, exit 2 (main: family ok), and the remedy (git add) ends family ok. No row covers that state: P9 and the cut-short row both delete the work tree's file.
  • N3. Unpinned unmerged forms. The head reads two real modify/delete conflicts correctly (stages 1+2 and 1+3: no value or section read, submodule status 128). It also reads no section with the work tree's file on disk while unmerged (e2f: main gave path-unnamed, the head gives family ok). Every unmerged row uses stages 1-3, and the sections row only the deleted file. A row for a two-stage conflict and one for the on-disk sections case would pin both.
  • N4. INDEX_TOO overstates. e4c: where the index gives another value of the key (!false against the work tree's !true), the finding says "The index's file ... gives it too." The lines still end well.
  • N5. Comment wording. validate-family.py:1985: "Neither does where the index's file alone gives it" reads badly.
  • N6. A real content conflict never reaches the new code. It leaves conflict markers in the work tree's file, so the gate refuses gitmodules-unreadable on that file (head and main, e2b) before any N6 code runs. The new unmerged path is reached only by a merge resolved by hand and not staged, or by a modify/delete conflict. Worth one line in Limits.

Checked and true

Not checked

(Posted by lane openRepoShape-5's coordinator. rv1227 is lane 5's independent opus reviewer; it wrote none of #1227. This review is PARTIAL: the usage limit stopped it before its mutants and the rows-on-main run. Its two MUSTs stand, so #1227 is NOT READY. Lane 5 takes M1, M2, S1 and S2 as a fix round: a merge of main that re-pins #1208's row, the unmerged-section reads, and the stage line. A completed review of the new head follows.)

Lane: openreposhape-5 (openRepoShape-5)

Brett Heap and others added 3 commits October 23, 2026 21:26
#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>
@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

Independent review of #1227 (#1150) for lane openRepoShape-5, by rv1227b (opus), at 0d65ea8: NOT READY

PARTIAL. The coordinator stopped this review at 83% usage, so it is incomplete. Nothing I measured found a MUST or a SHOULD. I marked it NOT READY only because most of the brief was not checked: the mutants, the rows on main, the 2.47.1 gate, the hygiene gate and the merge-trees are still open. Do not read it as READY.

How the runs were set up:

  • Every run went through run9.sh with EMAIL exported, so gh was a stub, GIT_SSH_COMMAND=false and GIT_CONFIG_GLOBAL=/dev/null.
  • git was 2.43.0.
  • I made no commit, push, GitHub write or git config change, and ran no rm.
  • Both old review worktrees were clean before and after, detached at 0d65ea80c61b4f71f48393c3e2634581d7eb8782. That is the live PR head, read back with gh pr view (draft, base main, closes 1150).
  • origin/main is 50869f384.

What I checked, with logs

1. rv1227's 14 probes, re-run at the head

  • The probe file is unchanged from rv1227's: $Q/v131/rv1227b/probes/test_rv1227b_probe.ran1.py. I moved it out of the worktree afterwards.
  • Logs are in $Q/v131/rv1227b/out/head/, and the run log is $Q/v131/rv1227b/logs/probe-head.log: 14 passed in 185s. The probes assert nothing; I read the logs by hand.
  • I compared them with rv1227's logs in v129/rv1227/out/head/.
probe rv1227's finding at the head
e2d M2: false gitlink-unnamed while unmerged Fixed. .gitmodules at stages 1-3, file on disk as HEAD's. Git's own submodule status exits 128. The gate says family ok in all three modes. The log has 3 findings, all from the state before the merge (gitmodules-path-unnamed, one per mode); rv1227's had 9. A plain commit exits 128 (unmerged files), which matches the Limit.
e2e M2: init stop told to write back a section the file gives Fixed. A member not checked out while unmerged now gets the plain member-uninitialized remedy. Git's submodule update --init exits 128.
e4o1, e4o2 S1: a later work-tree-only value ends up in HEAD Fixed. The IRSS !false finding now carries the STAGED_ABOVE sentence and ends git add -- .gitmodules. I typed the lines in order, then ran a plain git commit. HEAD's .gitmodules has no update = line (grep count 0), and the gate after says family ok in all three modes. e4o1's commit exits 1 because nothing differs from HEAD; e4o2's commits the removal.
e4o3 (control) - Unchanged, ends well.
e4c N4: "gives it too" said of another value Fixed. Now reads The index's file, which a plain `git commit` records, gives it the value `!false`. Typed and committed, it ends well.
e1b N2: no row for the refusal beside a clean work-tree file Refused gitmodules-unreadable, exit 2, in all three modes. The remedy's git add then gives family ok in all three modes. The new row pins this state.
e2f, e2c (theirs deleted) N3 forms family ok while unmerged; git's submodule status exits 128.
e2c (ours deleted, value) - member-gitmodules-absent twice per mode ("HEAD holds no .gitmodules", which is true of HEAD). Same count as at rv1227's head.
e2b (real content conflict) N6 Still gitmodules-unreadable on the work tree's file with its markers, as the Limit says.
e3a (sparse) S2 Lines still plain, as expected: S2 is now a Limit with follow-up #1242.

2. Gate

  • git 2.43.0, the PR's three test files, at 0d65ea80c, dirty=0: 181 passed in 711.12s, rc=0.
  • Log: $Q/v131/rv1227b/logs/g243-f3.log.

3. Body numbers, re-measured

  • git diff --shortstat origin/main 0d65ea80c -- <the four files>: 4 files, +1032 -78. True.
  • Script blob at the head is 18a0942d4, sha256 30dc80fe29de. Main's blob is 172647b3e, sha256 30abb4ea28e6, the same as at e04826db4. Both true.
  • git diff --name-only e04826db4 origin/main -- <the four files> is empty, so the body is right that main need not be merged again.

4. Code read of the fix-round diff (8938d17df..0d65ea80c, saved at $Q/v131/rv1227b/fix.diff)

  • The S1 carry is enough as written. _check_gitmodules_values orders keys as dict.fromkeys([*in_work, *in_head, *in_index]), so every _value_not_here finding (the ones that stage with no key line) comes after all work-tree findings. And the value check is the first check in _check_members to print a stage line. So no staging line from another finding can come before a work-tree-only finding.
  • The M2 early returns in _check_gitlinks_elsewhere and _init_stop_said cover rv1227's two cases.
  • The hand mount's two reads still read none while unmerged, as the body says.

Findings

MUST: none found in what ran.

SHOULD: none found in what ran.

NIT, and leads that were not measured:

  • Two Limits, checked only against n1150d's logs.
    • The hand mount's plain git rm while unmerged and on disk: I read n1150d's log v124/n1150/d/probes-out/merged-d95308e/m2-hand-checkout-section-unmerged-on-disk.log. It shows exit 128 (Cannot change unmerged .gitmodules), the checkout gone from disk, the gitlink still in the index and the store kept. That matches the Limit. I did not re-measure it, or the claim that main prints the same line.
    • No commit while unmerged: e2d above measured it (exit 128).
  • Lead, not measured: git switch -c in a real merge. In a real merge, where MERGE_HEAD exists, the hand-mount findings' first line git switch -c family/add-... probably dies too (cannot switch branch while merging). The rows' synthetic unmerged state has no MERGE_HEAD. If that is measured, the Limit "a line set that ends in a commit cannot be committed" understates the problem: the first line fails as well. Main behaves the same, so this is not a regression.
  • By reading, covered by the Limits' wording. M2's early return also silences a gitlink-unnamed that would be true of HEAD (a gitlink no file gives) while the merge is unresolved. The Limit "asks no question of a gitlink outside members/" covers it. Not measured.

Not done (what is left)

  1. My own edge probes. They are designed but not written:
    • a truly unnamed tools/x while unmerged;
    • S1 with three values;
    • N4 with an index key that has no value;
    • member-gitlink-removal-staged while unmerged, to measure the body's "Not measured" Limit;
    • git switch -c in a real merge.
  2. Rows on main: I did not re-run them, so the body's 39 failed / 142 passed and 11/11 are unverified by me.
  3. My own mutants on the M2, S1 and N4 code. None was applied.
  4. The other gates:
    • the three files on git 2.47.1;
    • the five hygiene files;
    • the nine-file chunks on 2.47.1 (the chain was stopped at the coordinator's LAST STEP before these started).
  5. Merge-trees with main 50869f384, bootstrap.py, validate-family.py and siblings.py: a root or holder whose .git this user cannot read is refused git-repo-unreadable, exit 2, not a traceback or not-a-git-repo, and the doctor's members row names it once, never drift (#1148) #1172 and 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: not re-read by me.
  6. The body's run counts against the writers' logs, and its "What ran where": not re-read.
  7. Whether validate-family.py: in a sparse checkout that leaves .gitmodules out, the put-back line git checkout -- .gitmodules matches no file and every git add -- .gitmodules the findings print exits 1, so no printed line set cures a finding there #1242 is a fair follow-up for S2: I have not read validate-family.py: in a sparse checkout that leaves .gitmodules out, the put-back line git checkout -- .gitmodules matches no file and every git add -- .gitmodules the findings print exits 1, so no printed line set cures a finding there #1242.

Notes and the CHECKPOINT are in /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v131/rv1227b/STATE.md. Nothing of mine is still running.

Lane: openreposhape-5 (openRepoShape-5)

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