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 11 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 while a merge leaves .gitmodules unmerged the checks read no file git would read (neither the index's nor the work tree's), as git reads none then, so gitlink-unnamed and the init stop ask nothing in that state. The checks that ask about HEAD, which a clone reads, still read HEAD's file, and the staged-removal check can read the work tree's, as the Limits say.

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). Fix round 2 answers rv1227b's and rv1227c's review (issuecomment-6722728204): one SHOULD, test-only (see "Fix round 2"). The one script is templates/family-root/scripts/validate-family.py; with its tests the change is four files, +1128 and -78 lines (git diff --shortstat origin/main...HEAD -- <the four files>, against the merge base e04826db4 with main 0d5704b3d).

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.

Fix round 2: rv1227b's and rv1227c's review

The independent review (issuecomment-6722728204, at 0d65ea80c, NOT READY) found no MUST and one SHOULD, S1, which needs a test and no change to the script. validate-family.py is byte for byte 0d65ea80c's (blob 18a0942d4, sha256 30dc80fe29de). The one commit, 2d56c1906, changes tests/test_validate_family_index_value_unmerged.py alone (109 insertions and 13 deletions), and the branch is a fast-forward of 0d65ea80c. Git 2.43.0 unless a line says 2.47.1, offline through lane 5's run10.sh (stub gh, GH_TOKEN=bogus-l5, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null); the logs are in lane 5's folder work/n1150g/.

  • S1, the stage line carried to every later finding (test-only). The code is staging = staging or committed or staged in _check_gitmodules_values. rv1227c's mutants K6 (staging = committed or staged: the stage line is not carried past the next finding) and K8 (staging = staging or staged: a committed value's stage line does not carry) each passed all 181 rows of the three files, and its probes showed that each puts a value git dies on into HEAD (p2m: fetchRecurseSubmodules = sometimes; p8: update = !false). The S1 row pinned only the finding right after a value that is both staged and committed. Two rows are added, and one id; staged_again builds the later finding's text for the S1 row and the new rows, which the S1 row spelled out inline.
    • test_every_later_value_the_work_tree_alone_gives_stages_again (rv1227c's p2a, a third value). update = !true is staged for IRRS, then update = !false and fetchRecurseSubmodules = sometimes are given in IRSS's section, in the work tree's file alone. The gate gives three findings, and the second and third each carry STAGED_ABOVE and end with the stage line. The printed lines are typed in order and a plain git commit follows: HEAD is as it was, git status is clean, the gate passes in every mode and a plain clone checks both members out. K6 fails it.
    • test_a_committed_value_the_index_lacks_stages_the_later_ones_again (rv1227c's p8, a [committed-not-staged] id). update = !true is committed for IRRS, the index's file is then staged without it, and the work tree's file gives IRRS !true again and IRSS update = !false. IRRS's finding is committed and not staged, and its git add -- .gitmodules stages IRSS's value all the same, so IRSS's finding ends staging the file again. The same typed lines and checks. K8 fails it.
    • test_another_value_the_index_s_file_gives_the_key_is_named[this-value-and-another] (rv1227c's p3c, the optional id). The index's file gives IRRS update both !true and !false beside the work tree's !true, and the finding names both ("gives it the value !true and the value !false"). The N4 row is one parametrized row of two ids now: [another-value], the old row, and this one. K11 fails it.
  • The review's NITs. NIT 7 is the Goal above, and NIT 1 and NIT 2 are in Limits. NIT 1 and NIT 3 are follow-up issues validate-family.py: while .gitmodules is unmerged, member-gitlink-removal-staged prints git checkout HEAD -- .gitmodules "as the index holds none", and that line resolves the merge to HEAD's side, dropping the other side's stage and overwriting a hand resolution on disk #1254 and validate-family.py: a staged deletion of .gitmodules with the file still on disk is family ok, and a plain git commit then drops .gitmodules from HEAD, so every member's gitlink is left empty in a recursive clone #1255, not changes here; the Limits below name NIT 1's. NIT 4 (a gitlink outside members/ that no file gives gets no question while unmerged; true on main) is the existing Limit's. K1 and K5 (NIT 5) are not touched by this round and were not run again. NIT 6 is optional and unchanged.

Rows that fail on main (fix round 2). Run at 2d56c1906 with validate-family.py as main held it from e04826db4 to 11560eb9f (blob 172647b3e, sha256 30abb4ea28e6), and again with the script as main 0d5704b3d holds it (blob ee26aede6, sha256 07f843df31b7, which differs by 105b5daf9's 12 added and 3 removed lines). Each was written into the worktree, then the head's script was restored and its sha256 30dc80fe29de checked, git status clean after (mut/MAIN.log and mut/summary.log; mut/MAIN2.log and mut/summary2.log).

  • The three test files in full: 42 failed, 142 passed in 157.36s with the first, and 42 failed, 142 passed in 158.42s with the second, the same 42 ids in both, of 184 rows (147 + 16 + 21). Fix round 1's run on the same script gave 39 failed and 142 passed of 181; the 42 are those 39 and the 3 ids this round adds (the two new rows and [this-value-and-another]), and the 142 that pass are the same 142.
  • The new and changed ids all fail: test_every_later_value_the_work_tree_alone_gives_stages_again, test_a_committed_value_the_index_lacks_stages_the_later_ones_again and test_another_value_the_index_s_file_gives_the_key_is_named[this-value-and-another]; the old N4 id, [another-value], and the S1 row's [staged] and [committed] fail as before. Each new id fails at its first assertion, found == [...], at index 0: main's first finding ends at its --unset-all line, where the row expects git add -- .gitmodules last.

Mutants (fix round 2). Each was applied alone in a detached worktree at 2d56c1906 (work/n1150g/mut/mutants.py: rv1227c's file, one exact replacement checked to occur once), the three test files were run in full through run10.sh, the head's script was restored and its sha256 30dc80fe29de checked, and git status was clean after each (mut/summary.log, one log per mutant beside it). No other run used the worktree while a mutant was applied.

mutant what it changes result
K6 S1: staging = committed or staged (the stage line is not carried past the next finding) killed, 1 failed, 183 passed (247.87s): test_every_later_value_the_work_tree_alone_gives_stages_again (new)
K8 S1: staging = staging or staged (a committed value's stage line does not carry) killed, 1 failed, 183 passed (241.32s): test_a_committed_value_the_index_lacks_stages_the_later_ones_again (new)
K11 N4: set(staged) >= set(values) (the index's file giving the work tree's value and another says "gives it too") killed, 1 failed, 183 passed (256.51s): test_another_value_the_index_s_file_gives_the_key_is_named[this-value-and-another] (new id)
K2 M2: gitlink-unnamed asks while unmerged where the work tree's file is on disk killed, 1 failed, 183 passed (225.47s): test_no_gitlink_outside_members_is_asked_about_while_unmerged[on-disk]
K3 M2: the init stop asks while unmerged where the file is on disk killed, 1 failed, 183 passed (231.77s): test_a_member_not_checked_out_while_unmerged_is_told_the_plain_update
K7 S1: again is not cleared for a finding that stages anyway (again = again) killed, 1 failed, 183 passed (227.21s): test_two_keys_with_no_gitmodules_in_the_work_tree_put_it_back_once
K13 N4: INDEX_GIVES names the work tree's values killed, 2 failed, 182 passed (235.91s): both ids of test_another_value_the_index_s_file_gives_the_key_is_named

All seven are killed. K6, K8 and K11 survived the 181 rows of the first run (rv1227c); K2, K3, K7 and K13 were killed then and still are, by the same rows.

Runs (fix round 2). All through run10.sh, each START line recording the head and dirty=0; the full suite was not run.

run git filter-repo result log
the three test files 2.43.0 2.47.0 184 passed in 234.10s logs/g243-f3.log
the three test files 2.47.1 2.47.0 184 passed in 243.42s logs/g247-f3.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 2.43.0 2.47.0 379 passed in 60.45s logs/hyg243.log
the three test files and the same five, on the scratch merge 804213b71 of the head with main b66d8f0cc 2.43.0 2.47.0 563 passed in 318.46s logs/merged243c.log
the same, on 875f5aee3 with main 105b5daf9 2.43.0 2.47.0 563 passed in 287.58s logs/merged243b.log
the same, on fda589e96 with main 11560eb9f 2.43.0 2.47.0 563 passed in 309.07s logs/merged243.log

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 again at 2026-11-08T15:50Z by rv1227d:

The rest of this section was read at 2026-11-08T15:07:43Z with git 2.43.0: origin/main at 0d5704b3d and the branch at 2d56c1906 (work/n1150g/logs/merge-trees-final.log in lane 5's folder). The branch holds main up to e04826db4 (#1208, merged in fix round 1 as d95308e95). Main has moved three times while this round ran, and is at 0d5704b3d, 11 commits past e04826db4; one of the 11 touches this PR's four files: 105b5daf9 (#1166, landed) changes validate-family.py (the date and repository patterns end in \Z, an id is matched with fullmatch), at hunks away from this PR's, and merges clean. So main is not merged again.

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.
  • While any merge is in progress, the hand mount's first line, git switch -c, exits 128 (rv1227c's P5, measured; main is the same). Git says fatal: cannot switch branch while merging, in a modify/delete conflict on .gitmodules, in a conflict in another file, and after a clean git merge --no-commit. With .gitmodules unmerged its plain git rm exits 128 too (Cannot change unmerged .gitmodules), as the next bullet says. A line set that ends in a commit is not the only one that cannot be typed in a merge. Not changed here.
  • 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).
  • member-gitlink-removal-staged while .gitmodules is unmerged (rv1227c's P4, measured; main is the same). Where the work tree's file is deleted, its put-back (_section_back, through _gitmodules_put_back) finds no index entry at stage 0, so it falls to HEAD's file and prints git checkout HEAD -- .gitmodules, "from HEAD's, as the index holds none". Where the file is on disk and gives no section for the member, _section_back prints the same line itself, in its on-disk branch, with the SECTION_TAKEN words ("putting that file back as HEAD has it and dropping any other change to it"), and never calls _gitmodules_put_back. So a fix must guard both places (rv1227d's NIT 2). That reading is wrong while the index holds the file at its conflict stages, and the line does more than put a file back.
  • The checks that ask about HEAD still read HEAD's file while .gitmodules is unmerged, as the Goal says, since HEAD's is what a clone reads (rv1227d's NIT 1, issuecomment-6723609032):
    • the value check's HEAD side: a value HEAD gives is still named, "in HEAD, which a clone reads, not in the file git reads here" (test_a_value_head_gives_while_a_merge_leaves_the_file_unmerged pins it);
    • member-gitmodules-absent and member-gitmodules-url-absent (_check_gitlinks_registered);
    • the hand mount (member-gitlink-unnamed), which asks HEAD's sections whether a clone fetches the gitlink (_gitlink_unnamed_said), with its helpers _section_then, _gitlink_put_back and _move_section;
    • the staged-removal check, above, which also reads the work tree's file in its on-disk form.
      The script's unmerged guards read nothing from any file. This PR changes none of these reads.
  • What ran where. At this head (2d56c1906), the three test files (184) and the five hygiene files (379) ran on git 2.43.0, together on the scratch merges with main named under Merges (563), and the three test files ran on 2.47.1 (184). The script is byte for byte 0d65ea80c's, where the nine files that pin the script's texts elsewhere ran on 2.43.0 (330 passed); no file imports the changed test file, so they were not run again. 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. Fix round 2's mutants ran over the three files in full, and fix round 1's over their own rows. Main's commits since e04826db4 are not merged: the one that touches validate-family.py (105b5daf9) merges clean and the scratch merges ran, and no gate ran on the predicted merge with an 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

Make family validation follow Git’s .gitmodules index and merge-state behavior while producing complete, reliable remediation commands.

New Features:

  • Align .gitmodules value and section checks with Git’s index and merge-conflict read semantics.
  • Report unparsable indexed .gitmodules files as gitmodules-unreadable with actionable remediation.
  • Ensure generated remedies stage .gitmodules consistently, including subsequent findings after an earlier staging command.

Bug Fixes:

  • Prevent checks from using work-tree or HEAD .gitmodules content while the index contains an unmerged file.
  • Detect values present only in the staged index and accurately describe differing indexed values.
  • Avoid asking gitlink and initialization questions when Git reads no .gitmodules during an unresolved merge.

Tests:

  • Add coverage for indexed values, unparsable indexed files, unmerged and modify/delete conflicts, repeated staging remedies, and related gitlink behavior.
  • Update existing expectations for staging commands and unreadable-index handling.

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

Implementation

Update .gitmodules validation to account for the staged index file, refuse an unparsable index file, and match Git’s no-file behavior while the index has an unmerged entry; keep findings and remedies consistent when earlier remedies stage the work-tree file.

  • templates/family-root/scripts/validate-family.py L2133-2186, L2191-2226, L4071-4100 (+20 small edits)

Tests

Update existing value-validation tests to expect the explicit staging command and the new refusal for an unparsable indexed .gitmodules file.

  • tests/test_family_gitmodules_values.py L193-196, L802-805, L815-821 (+5 small edits)

Revise index-file tests to type the printed staging command and verify that an index-only value remedy leaves the committed configuration clean.

  • tests/test_validate_family_index_gitmodules.py L222-225, L287-291 (+7 small edits)

Add regression coverage for index-only and differing indexed values, unreadable indexed files, unmerged .gitmodules behavior, and remedies that must restage values introduced by earlier findings.

  • 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

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

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-11-08T14:29:53Z) Independent review of #1227 (#1150) for lane openRepoShape-5, by rv1227b and rv1227c (opus), at 0d65ea8: NOT READY

Why NOT READY: one SHOULD, and it needs only a test. The code is right in every state I measured. But two of my mutants on the S1 code survive all 181 rows of the PR's three files, and I showed with probes that each one puts a value git dies on into HEAD. Adding a row that kills K6 and K8 is all that stands between this PR and READY. No MUST was found.

Setup

  • Reviewer rv1227c. I wrote none of 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. I made no commit, push, GitHub write or git config change, ran no rm or rmdir, and started no subagent.
  • Every pytest run went through L/tools/run10.sh (stub gh, GH_TOKEN=bogus-l5, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null). The 2.47.1 leg had L/tools/git247/bin first on PATH, and each log prints its git --version.
  • git-filter-repo version: run10.sh was edited at 14:09:17Z (its mtime), which is when venv247 replaced venv.
    • 2.45.0: every run that started before that, which is all gates, probes and mutants not listed below.
    • 2.47.0: the four-file extra run (started 14:09:50Z), mutants K1 and K5, and the gate on predicted merge 2.
    • No file in any of these runs executes git filter-repo. Only test_path_guard_hygiene.py and test_repo_hygiene.py mention it, in text.
  • Heads: the PR head is 0d65ea80c, read back by ls-remote and gh pr view (draft, open). Main was bd3e9c148 until about 14:20Z, then 11560eb9f (see the merge-trees).
  • Worktrees: /workspace/projects/openRepoShape-worktrees/rv1227c-review and rv1227c-mut are both clean, detached at 0d65ea80c. Nothing of mine is running.
  • My folder is /workspace/projects/.lane-worktrees/openRepoShape-5/work/rv1227c/ (STATE.md, logs/, out/, mut/, probes/).

Carried forward from rv1227b (issuecomment-6385625905, measured at 0d65ea8 on git 2.43.0)

  • rv1227's 14 probes re-run at the head: 14 passed.
    • M2: e2d (false gitlink-unnamed while unmerged) and e2e (the init stop said to write back a section) are fixed.
    • S1: e4o1 and e4o2 are fixed (STAGED_ABOVE, then the stage line; HEAD is clean after the lines and a plain commit).
    • N4: e4c is fixed (it names the index's !false).
    • N2: e1b is refused, and its git add then gives family ok.
    • e2b (a real content conflict) is still gitmodules-unreadable, as the Limit says.
    • e3a (sparse) is still plain lines, as the S2 Limit says.
  • Gate: the three PR files, 181 passed in 711.12s.
  • Body numbers: +1032/-78 over 4 files. Script blob 18a0942d4, sha256 30dc80fe29de; main's blob 172647b3e, sha256 30abb4ea28e6.
  • Code read:
    • The S1 carry's ordering holds: findings for keys the work tree's file does not give always come after the work-tree findings.
    • The M2 early returns cover e2d and e2e.
    • The hand mount's two reads read none while unmerged.

What rv1227c did (items 1 to 7 of R2)

1. Edge probes. The probe file is probes/test_rv1227c_probe.py, never committed and moved out of the worktree after the runs. Each state ran on 2.43.0 and 2.47.1 with the same outcome. Logs are in out/head243/, out/head243b/, out/head243c/ and out/head247/. Main's script (blob 172647b3e) ran beside the head's by --root.

  • P1, a truly unnamed tools/x while .gitmodules is unmerged. No section in any file gives tools/x, so a plain clone of HEAD exits 128 at tools/x.
    • Head: family ok in all three modes, for both the on-disk and the deleted form.
    • Main: a true gitlink-unnamed.
    • After git add -- .gitmodules, the head names it. The git rm --cached line typed, then a plain commit, gives family ok, and a plain clone exits 0.
    • The Limit ("asks no question of a gitlink outside members/") covers this. See NIT 4.
  • P2, S1 with three values. All four orders end well at the head: HEAD equals the start and the gate passes in every mode.
    • p2a: staged, then two work-tree-only values. The second and third findings each carry STAGED_ABOVE and the stage line.
    • p2c: committed, then two work-tree-only values.
    • p2b: work-tree-only, work-tree-only, then staged.
    • p2d: a value in HEAD only (_value_not_here, printed last), then two work-tree-only values.
  • P3, N4 with a key that has no value. All three end well (typed, then a plain commit; HEAD unchanged; family ok).
    • p3a, a bare update in the index: "The index's file, which a plain git commit records, gives it no value."
    • p3b, the reverse: "...gives it the value !true."
    • p3c, the index gives !true and !false: "...gives it the value !true and the value !false."
  • P4, member-gitlink-removal-staged while unmerged. This measures the Limit the body marks "Not measured". Head and main print the same text and lines.
    • With the work tree's file deleted, or on disk without IRRS's section, the finding prints git checkout HEAD -- .gitmodules and says "as the index holds none". The line exits 0, puts HEAD's blob at stage 0 and drops the other side's stage 3. In the on-disk form it also overwrote a hand resolution (branch = resolved-by-hand) in the work tree.
    • With a hand resolution on disk that gives the path, only git reset is printed. .gitmodules stays UU.
    • See NIT 1.
  • P5, git switch -c in a real merge (MERGE_HEAD). In all four forms (modify/delete with nothing on disk, modify/delete with a checkout, another file's conflict, a clean merge --no-commit), the hand mount's first line exits 128: fatal: cannot switch branch while merging.
    • With .gitmodules unmerged, its plain git rm also exits 128 (Cannot change unmerged .gitmodules). The gitlink stays in the index and the path is gone from disk ( D).
    • Main is the same. See NIT 2.
  • Extra P6/P6b, a staged deletion of .gitmodules with the work tree's file on disk.
    • Clean (P6b): family ok on head and main. A plain commit drops .gitmodules from HEAD, and the next run gives two member-gitmodules-absent.
    • With a work-tree-only value (P6): one finding, --unset-all only. Typed and committed, the same loss follows.
    • This predates the PR. See NIT 3.
  • Extra P7, the body's M2 claim about the hand mount, measured. The state is a hand clone whose path a section gives, with .gitmodules unmerged.
    • The head prints git rm --cached -- members/IRQS, which exits 0.
    • Main prints a plain git rm, which exits 128 (could not lookup name for submodule 'members/IRQS'). True.

2. Rows on main. I wrote main's script into rv1227c-mut, ran the three files, then restored the head's script (sha256 30dc80fe29de, dirty=0). Log: logs/main-full.log.

  • 39 failed, 142 passed in 173.14s, as the body says. The 39 ids are in logs/main-full-failed.txt.
  • All 11 fix-round ids are among them, so 11 of 11 fail.
  • Row counts: 147 + 16 + 18 = 181 (logs/collect3.log).

3. Mutants. Each was applied alone in rv1227c-mut, the three files ran in full, the script was restored and its sha256 checked, and dirty=0 after each. Files: mut/mutants.py, mut/summary.log, one log per mutant beside them.

id change result
K2 M2: gitlink-unnamed asks while unmerged where the work tree's file is on disk killed, 1: test_no_gitlink_outside_members_is_asked_about_while_unmerged[on-disk]
K3 M2: the init stop asks while unmerged where the file is on disk killed, 1: test_a_member_not_checked_out_while_unmerged_is_told_the_plain_update
K7 S1: again is not cleared for a finding that stages anyway killed, 1: test_two_keys_with_no_gitmodules_in_the_work_tree_put_it_back_once
K13 N4: INDEX_GIVES names the work tree's values killed, 1: test_another_value_the_index_s_file_gives_the_key_is_named
K6 S1: staging = committed or staged (not carried past the next finding) survived, 181 passed
K8 S1: staging = staging or staged (a committed value's stage line does not carry) survived, 181 passed
K11 N4: set(staged) >= set(values) says "gives it too" survived, 181 passed
K1 _gitmodules_unmerged needs stage 1, so an add/add conflict (stages 2 and 3) is not unmerged survived, 181 passed
K5 _gitmodules_unmerged returns True where ls-files fails survived, 181 passed

4. Gates at 0d65ea8 (dirty=0 in every START line).

run git result log
the three PR files 2.47.1 181 passed, 239.89s logs/g247-f3.log
the hygiene five (test_path_guard_hygiene, test_repo_hygiene, test_agents_md_codes, test_cli_reference, test_pinned_copy_text) 2.43.0 379 passed logs/hyg243.log
the hygiene five 2.47.1 379 passed logs/hyg247.log
all nine "other" files, four chunks 2.47.1 89, 86, 83 and 72 passed (330, the body's counts) logs/n9a-247.log to logs/n9d-247.log
extra: five more test_validate_family_* files (unreadable, without_git, branch_step, duplicates, member_path_glob) 2.43.0 131 passed logs/x5-243.log
extra: test_family_switch_branch_exists, test_family_remove_gitmodules, test_refusal_default_remediation, test_family_docs_one_spelling 2.43.0 112 passed logs/x4-243.log
predicted merge with main bd3e9c148 (d306cb3b4, a scratch commit with no ref): the three files plus the hygiene five 2.43.0 560 passed logs/gpm243.log
predicted merge 2 with main 11560eb9f (55ce81cd9, a scratch commit with no ref): the same eight files 2.43.0 560 passed logs/gpm2-243.log
  • CI's tests job (the full suite) passed at 0d65ea80c on 2026-10-23T22:16Z (run 40032550510). Sonar passed.

5. Merge-trees (logs/merge-trees.log).

6. The body's counts and "What ran where".

  • Every count I could measure again is true:
    • +1032/-78 over 4 files against bd3e9c148;
    • the two blobs and their sha256s;
    • 181 = 147 + 16 + 18;
    • 560 = 181 + 379;
    • 551 = 172 + 379;
    • 181 on 2.47.1;
    • the nine files 330 (89, 86, 83, 72);
    • 39/142, and 11 of 11.
  • "What ran where" is accurate.
  • Could not check: the writers' own logs, and their D1-D5 and R1-R5 mutant results. Those were in the lost /tmp scratch. I ran my own set instead.
  • Stale, not wrong: "Merges" reads main 50869f384 and tree 776447116. Main is now 11560eb9f, with the results above.

7. #1242 as the follow-up for S2: fair.

Findings

MUST: none.

SHOULD

  • S1. The S1 fix's "every later finding" and its committed arm are not pinned. The code is at validate-family.py:2157, staging = staging or committed or staged.
    • K6 (staging not carried past the next finding) and K8 (a committed value does not carry) each pass all 181 rows.
    • K6, probe p2m: p2a's state, the K6 script's lines typed, then a plain commit. HEAD gains fetchRecurseSubmodules = sometimes, and the next run finds it "in HEAD", exit 1. The head's own lines end well (p2a).
    • K8, probe p8: IRRS !true committed, the index's file staged without it, and the work tree giving IRRS !true and IRSS !false. The K8 script's lines leave update = !false in HEAD. The head's lines end well.
    • The docstring promises the behaviour these mutants break ("every later finding that takes a key out of it stages it again"), and the S1 row pins only the next finding after a value that is both staged and committed.
    • Fix (test-only): give the S1 row a third value (p2a's: IRSS update = !false and fetchRecurseSubmodules = sometimes after IRRS staged) and a [committed-not-staged] id (p8's state). Show that K6 and K8 are killed.

NIT

  1. The "Not measured" Limit for member-gitlink-removal-staged while unmerged is now measured (P4), and the effect is wider than it says.
    • git checkout HEAD -- .gitmodules resolves the merge to HEAD's side and drops the other side's stage.
    • With the file on disk, it also overwrites a hand resolution in the work tree.
    • Its text says "as the index holds none" while the index holds conflict stages.
    • Main is identical. Suggest rewording the Limit, and a follow-up issue.
  2. P5: in any merge in progress, the hand mount's first line git switch -c exits 128 ("cannot switch branch while merging"), even after a clean --no-commit merge. With .gitmodules unmerged its git rm exits 128 too. The Limit "a line set that ends in a commit cannot be committed" understates this. Main is identical. One line in Limits.
  3. P6/P6b: a staged deletion of .gitmodules with the file on disk is family ok, and a plain commit drops .gitmodules from HEAD. Beside a work-tree-only value there is no stage line either; the M3 row covers only a value HEAD's file gives. This predates the PR and main is identical. Suggest a follow-up issue.
  4. P1: while unmerged, a gitlink outside members/ that no file gives gets no finding (a true one on main). The Limit covers it, the gitlink is asked about again after git add, and its lines then end well. Optionally the Limit could say that a true finding is not said in that state.
  5. The other survivors.
    • K11: an index file that gives the value and another is not pinned. The head names both (p3c); the mutant would say "gives it too".
    • K1: an add/add conflict (stages 2 and 3) is not pinned.
    • K5: an index ls-files cannot list is not pinned.
  6. P3a wording: "gives it no value" can read as "does not give it". It matches the file's existing "gives ... no value" convention, so this is optional.
  7. Body refresh before landing: "Merges" (main 50869f384, tree 776447116) and the new open PR validate-family.py: a member whose .git file this user cannot open, or whose git directory (its own .git, or the one its gitfile names) this user cannot search, is refused member-unreadable naming that part, and no longer member-commit-unresolvable, which said the pinned commit was missing and prescribed a fetch (#1221) #1246.
    • The Goal's "the checks read no file while a merge leaves .gitmodules unmerged" overstates: the staged-removal check and the hand mount's reading of HEAD's sections still read files, as the Limits say.
    • rv1227's leads are now resolved: the git switch -c lead (P5), M2 silencing a true gitlink-unnamed (P1), and the hand mount's plain git rm Limit (confirmed again in P5's checkout form).

Not done

  • The writers' D1-D5 and R1-R5 mutants were not re-run, and their logs are lost with /tmp.
  • The full suite was not run on the bench; CI's full suite passed at the head on 10-23.
  • The 60 tests/test_family* and tests/test_validate_family* files were not all run at the head.

Lane: openreposhape-5 (openRepoShape-5)

…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>
@sonarqubecloud

sonarqubecloud Bot commented Nov 8, 2026

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-11-08T15:51:20Z) Independent delta review of #1227 (#1150) fix round 2 for lane openRepoShape-5, by rv1227d (sonnet), 0d65ea8..2d56c19: READY

I found no MUST and no SHOULD. READY is for head 2d56c1906 as pushed. The Goal and Limits wording in N1 and N2 below should be corrected in the body edit the coordinator makes at landing anyway (the "Merges" section is stale). That edit needs no commit and no gate run.

What I checked, and what held

  • The delta. It is one commit, 2d56c1906, on 0d65ea80c: git diff --name-only names tests/test_validate_family_index_value_unmerged.py alone (+109/-13).
    • validate-family.py is blob 18a0942d4, sha256 30dc80fe29dea6ccc0c34031b31612e5e9b409bd9d2bfbecb8c85e4321630e40 on both sides.
    • The subject is 244 characters. Author and committer are Brett Heap <service@opensoft.dev>. There is no closing keyword, and the Co-Authored-By and Lane lines are present.
    • The PR head, the origin ref and gh all read 2d56c1906.
  • Rows. Collected 147 + 16 + 21 = 184 (collect.log). The new ids are:
    • test_every_later_value_the_work_tree_alone_gives_stages_again
    • test_a_committed_value_the_index_lacks_stages_the_later_ones_again
    • ...the_key_is_named[this-value-and-another]
  • staged_again(). I imported the test module and compared its output with the old inline S1 string: old == new: True. The S1 ids stay [staged] and [committed].
  • The new rows. Each types the printed lines in order, then plain git commit, then asserts HEAD equals the start, status --porcelain == b"", and ends_well. That covers the gate in every mode and a plain clone of both members.
  • S9073. No assert joins conditions with and or or. I parsed the file with ast and found no BoolOp inside any Assert.
  • pytest versions. The bench runs pytest 9.1.1 and CI pins 8.3.4. The parametrize(..., .values(), ids=<dict>) form already has a precedent at line 434 (BEFORE_THE_LINE), which passed CI at 0d65ea80c.
  • Mutants. Each ran alone in rv1227c-mut, from L/work/rv1227c/mut/mutants.py, over the three PR files in full. The script was restored each time and checked: sha256 30dc80fe29de, dirty=0. Logs are in work/rv1227d/mut/.
mutant result (git 2.43.0) killed by
K6 1 failed, 183 passed, 235.65s the new p2a function
K8 1 failed, 183 passed, 263.10s the new p8 function
K11 1 failed, 183 passed, 280.12s [this-value-and-another]
K2 1 failed, 183 passed, 269.06s ...unmerged[on-disk]
K3 1 failed, 183 passed, 269.73s ...told_the_plain_update
K7 1 failed, 183 passed, 276.43s ...put_it_back_once
K13 2 failed, 182 passed, 279.95s both N4 ids
  • The new ids on main's script. I ran the current origin/main script, blob ee26aede6, over the three files.
    • Result: 42 failed, 142 passed in 196.88s. The 42 ids are the same as n1150g's MAIN2-failed.txt.
    • All three new ids fail at assert found == [...], "At index 0 diff".
    • I restored the script (sha256 30dc80fe29de, dirty=0).
    • Main held blob 172647b3e at e04826db4 and 11560eb9f, then ee26aede6 from 105b5daf9. That is 12 lines added and 3 removed, as the body says.
    • 39 + 3 = 42, as the body says.
  • Gates (filter-repo 2.47.0 in every run).
run git result
the three PR files at 2d56c1906 2.43.0 184 passed, 239.14s
the three PR files at 2d56c1906 2.47.1 184 passed, 264.48s
predicted merge 7317354ef (main dffc42265 + 2d56c1906), three files + hygiene five 2.43.0 563 passed, 341.54s

NIT 1: the Goal's "except" clause and the new "Two reads" Limits bullet are incomplete.

  • The two reads they name are real and correctly described:
    • the hand mount, _check_gitlinks_named line 2780, reads _head_gitmodules;
    • the staged-removal check, _check_removals_staged line 3813, reads _head_listed.
  • But they are not the only reads of a file in the unmerged state. HEAD's file is also read by:
    • the value check itself, _check_gitmodules_values lines 2125, 2132 and 2142-2148. A test pins it: test_a_value_head_gives_while_a_merge_leaves_the_file_unmerged expects the finding "in HEAD, which a clone reads, not in the file git reads here".
    • _check_gitlinks_registered, line 3680 (member-gitmodules-absent and -url-absent).
    • the hand-mount helpers _section_then (3532), _gitlink_put_back (838) and _move_section (1059).
  • The only unmerged guards in the script are at lines 2126, 3019, 3080 and 4311, and all of those read nothing from a file.
  • Suggested wording: say the checks that ask about HEAD, which a clone reads, still read HEAD's file, and name the value check, member-gitmodules-absent, the hand mount and the staged-removal check. Do not say "two".

NIT 2: the P4 Limits bullet and #1254 describe one of the three forms.

NIT 3: two further survivors on the S1 clearing, text-only. These are my own mutants, outside the K-list. Both ran with 184 rows, 2.43.0, and the script was restored each time (sha256 30dc80fe29de, dirty=0).

  • X1 (again = again and not staged): 184 passed, 272.88s.
  • X2 (again = again and not committed): 184 passed, 280.43s.
  • I called _value_taken_out directly on the head and on the X1 copy, both under work/rv1227d/x1copy/, with committed=True, staged=False, again=True. X1 adds "A line above stages the work tree's .gitmodules, this value with it..." to a finding whose value HEAD already gives. The typed lines are identical.
  • No row has a committed-only or staged-only finding after a staging one. K7 is killed only by a row where both are true.
  • Optional: one id for each half. Same class as the earlier K1, K5 and K11 survivors.

NIT 4: smaller items.

Not done

  • I did not run the nine "other" files, the 60 tests/test_family* files or the full suite. The script is byte-identical to 0d65ea80c, and no file references the changed test module.
  • The hygiene five ran only inside the 563 merged run.
  • I read n1150g's logs but did not re-run its P4, P6 and P6b probes under pytest. I re-measured their git behaviour with plain git instead.

Housekeeping

  • I made no commit or push, no GitHub write, no rm, no git config, and started no subagent.
  • My first fetch lacked -c gc.auto=0 -c maintenance.auto=false. Later fetches had them. No harm.
  • One stray file, /tmp/null, came from a redirect typo.
  • I created two unreferenced scratch commits with commit-tree: a6ae7b51e and 7317354ef.
  • rv1227c-review and rv1227c-mut are both detached at 2d56c1906, clean, with script sha256 30dc80fe29de. n1150g-probe and n1150g-merged were not touched.
  • Nothing of mine is running.
  • I used neither ${(f)...} nor $(<file), per the coordinator's note.

Files (all under /workspace/projects/.lane-worktrees/openRepoShape-5/work/rv1227d/)

  • STATE.md
  • mut/ (summary and one log per mutant)
  • mutx/ (X1 and X2)
  • logs/ (g243, g247, main, merged243, main-failed.txt)
  • collect.log
  • predicted-merge.txt
  • scratch/ (P4 and P5 plain-git logs)
  • x1copy/
  • body.live.md

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