Skip to content

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

Merged
brettheap merged 3 commits into
mainfrom
fix/1077-gitmodules-checks-read-the-file-git-reads
Oct 23, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

What this does

RV1069's review of #1069 (the PR for #1036) left four follow-ups, recorded in #1077's body (NIT 1, NIT 2) and in issuecomment-6303768948 (NIT 3, NIT 4). Each was measured on main before this change. The only script changed is templates/family-root/scripts/validate-family.py, which has no pinned copy.

  • NIT 1, gitmodules-path-twice read the work tree's .gitmodules alone. A second section for members/IRRS, staged in the index's file with 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.
  • 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 says "the index's file, which git reads here" where that is the one read.
    • Its lines put that file back, not HEAD's. They end "then stage", or "then stage and commit" where HEAD's gives the value too.
    • Where HEAD's file alone gives the value and the work tree's is gone, it says "Commit the index's .gitmodules, which git reads here and does not give it", with no line, so a fix staged there is kept.
    • 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 before (the work tree's file, then HEAD's, then the one read in its place), so with no git to run, git-not-runnable names what it named.
  • NIT 3, a plain git commit after a section's lines. Where HEAD's file held the section, gitmodules-path-unnamed's lines ended "then commit .gitmodules" and staged nothing. 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) now prints git add -- .gitmodules after the keys, and says "then stage and commit" where HEAD holds the section.
  • NIT 4, a test gap. No row covered the case-variant branch's stage line where HEAD holds no section of the name. RV1069's probe test_r2_stray_not_in_head is that row now.

New rows: tests/test_validate_family_index_gitmodules.py

Every row types only the printed lines and does by hand only what the text says in words, then makes a plain git commit. The gate and a plain clone then pass.

  • N1: a second section in the index's file alone.
  • N2: a value in a named member's section, in the index's file alone.
  • N2b: HEAD's file gives the value, and the index's is mended.
  • N2c: as N2b, with OTHER's section out of both files, so member-gitmodules-absent puts the index's file back first.
  • N3: a section HEAD holds (a stale one, or tools/x with its gitlink), with the file on disk, deleted, or its deletion staged.
  • N4: the stray ghost (RV1069's probe), on disk and deleted.
  • Fix round 1: path-twice's put-back is recorded, so gitmodules-path-unnamed after it prints none (RV1124's P7, on deleted and deletion-staged); and the value's url is written back from the file git reads, not HEAD's (RV1124's P10). See the fix-round section below.

Before and after. On main's script the file gives 10 failed and 2 passed. The 2 are N4's rows, which pass there by design (NIT 4 is a test gap) and kill the case-variant mutant M8 below. On this head it gives 12 passed. After fix round 1 the file has 15 rows: on main's script it gives 13 failed and 2 passed (the 2 are N4's), and on this head 15 passed.

Older rows changed, and why

Each one pinned the behaviour #1077 names as the defect.

  • "then commit" with no stage line (NIT 3).
    • SECTION_REMEDY and a new STAGE constant (test_validate_family_unnamed_mounts.py), and its rows (b) and (c), plus the two-sections row.
    • THEN_COMMIT and section_first, which always ends with the stage line now (test_family_unnamed_gitlink.py), and its two-sections and control rows.
    • Two removal(...) assertions in test_validate_family_remedies_typed.py.
    • ghost_out, R1's replace anchor and R6 in test_family_hand_mount_git_rm_lines.py.
    • Each section in these rows is committed, so HEAD holds it: a plain commit after the old lines left it there.
  • Path-twice's ending (NIT 1 and NIT 3). test_family_add_refusals.py's path-twice row ends "then stage and commit": the commit records the index's file, not the work tree's hand edit.
  • HEAD's file put back where git reads the index's (NIT 2).
    • taken_out(put_back=...) and the two gone-file value rows in test_family_gitmodules_values.py. These now read HERE_AND_HEAD; one is renamed ..._put_it_back_once, since its old name said HEAD's file is put back.
    • The first line of test_round1_nit_6 in test_family_holder_remedies_compose.py.
    • The value row in test_family_holder_bootstrap_texts.py, renamed test_a_value_finding_that_put_the_file_back_is_not_put_back_over.
    • These are now git checkout -- .gitmodules, the index's file.
  • validate-family.py (family root): gitmodules-path-unnamed reads only the work tree's .gitmodules, so where that file is gone, a section the index still holds passes the gate and is committed #1036's R5 control (test_1036_control_a_value_git_dies_on_keeps_what_it_read).
    • Round 1 put back HEAD's file, which dropped IRQS's section that only the index's file gave.
    • The value's lines put back the index's file now, so the commit after round 1 records that section, as it does with the file on disk. The next run names it, in every mode.
    • The row now types round 2 as well, then a plain git commit; nothing is left, and the gate and a plain clone pass.
    • What the control pins is unchanged: where git reads a file that gives a value git dies on, gitmodules-path-unnamed reads nothing from it.

Gates

Every run used /usr/bin/python3 -m pytest -p no:cacheprovider with:

  • EMAIL set;

  • GIT_ALLOW_PROTOCOL, GIT_SSH_COMMAND and the LANES_* variables unset;

  • an empty GIT_CONFIG_GLOBAL and GIT_CONFIG_NOSYSTEM=1;

  • test author and committer identities;

  • a stub gh first on PATH, GH_TOKEN=bogus and an empty GH_CONFIG_DIR.

  • Targeted set: 78 files.

    • The files that name gitmodules-path-twice, gitmodules-value-refused, gitmodules-path-unnamed or validate-family in the head's tree, plus every test file that imports from a test module this changes.
    • This includes test_pinned_copy_text, test_repo_hygiene, test_cli_reference and test_agents_md_codes.
    • Result: 0 failed in all 78 files, 4390 passed and 34 skipped.
    • Shards 0 to 4 and 6 to 9 (70 files) were run at 0cd1c3f5, the three WIP commits this head squashes (the same tree, and the same sha256 of the script): 3379 passed, 25 skipped.
    • Shard 5 (8 files) was run last, at this head (672845cc): 1011 passed, 9 skipped, 0 failed. Two earlier runs of it were cut by checkpoints, at 49% and at 28%, with 0 failures both times. Its files are test_doctor_legs_members_next.py, test_family_add_spelling_leftovers.py, test_family_docs_one_spelling.py, test_family_makefile_siblings.py, test_family_remove_gitmodules.py, test_shape_doctor.py, test_update_shape_kept_helper.py and test_validate_family_unreadable.py.
    • The PR's CI tests job runs the full suite.
  • Older rows. The 33 rows the change moved, which failed on the new script before their update, now pass with the new rows file (45 passed).

Mutants

Each mutant was run against the new rows file and the older rows that pin the changed paths. The file was restored after each one, and its sha256 checked against the head's.

Mutant Rows that failed
M1: the value check reads the work tree's file alone 8 (N2, N2b, N2c, two gitmodules_values rows, round1 NIT 6, the bootstrap_texts row, R5)
M2: path-twice reads the work tree's file alone 1 (N1)
M3: no "commit the index's" branch where HEAD's alone gives the value 2 (N2b, N2c)
M4: the value's put-back is HEAD's, as before 6
M5: the value's text does not say to stage the file 1 (N2)
M6: no stage line in members/ 7
M7: no stage line outside members/ 3
M8: no stage line in the case-variant branch (NIT 4) 2 (N4 on disk, deleted)
M9: "then commit" where HEAD holds the section 10
M10: path-twice names no put-back 1 (N1)
M11: path-twice ends "then commit" 3
M12: member-gitmodules-absent puts nothing back after any value finding 1 (N2c)
M13: the file git reads is asked before HEAD's 3 (the without_git rows)

Merge-trees

git merge-tree --write-tree against origin/main 823624d is clean, and was clean against 6da757c, the base the gates ran on. Main has since taken #1101 and #1102; neither touches a file of this PR. No open PR touches these files. #1109, the closest in subject, merges clean too.

Follow-up, not in this PR

RV1124 measured the suspected counterpart of NIT 3 (its N1) and two more older defects (N2 and N6). All three are older than this PR and self-correcting or known limits; the lane files them as the follow-up #1150, and none is fixed here.

  • N1: the work tree's .gitmodules is on disk, and both it and the index's give a value HEAD's does not. The value's lines say nothing of staging, so a plain git commit after them records the index's value; the next run names it in HEAD. Main prints the same text and gives the same result.
  • N2: an index blob with a line git cannot parse after a value is family ok (_blob_listed returns nothing on git's exit 128).
  • N6: an unmerged .gitmodules (stages 1 to 3), which v1.1.1's release notes list as a known limit.

Fix round 1: RV1124's review of 672845cc (S1, N3, N4, N5)

RV1124's review of this PR at 672845cc found one SHOULD and six NITs. This round fixes S1, N3, N4 and N5, in one commit on top of it (8dfa32bd), with no amend and no force. N1, N2 and N6 are left to the follow-up #1150.

  • S1 (SHOULD): tests/test_path_guard_hygiene.py and shape-doctor.py: a bare is_file/is_dir/exists guard in a shipped tool fails by file and line unless its _is_a or a reasoned allowlist covers it, and a project.yaml linked into an unsearchable folder is yaml-unreadable (#430) #1111's test_path_guard_hygiene.py failed the new guard. _listed_as_git_reads opened with a bare os.path.lexists(root / GITMODULES). Its one caller, _check_gitmodules_values, asks it only where the work tree's file is gone (if present else _listed_as_git_reads(root)), and _gitmodules_put_back, which the function calls next, asks the same lexists and is on the test's allowlist. The guard is dropped; the function is spec = _gitmodules_put_back(root)[0] and the blob read. On the scratch merge of main 038dafba9 and this head, test_path_guard_hygiene.py gives 49 passed, with no failure for this file and no stale BENIGN entry.
  • N3: two behaviours had no row. Mutants A and B survived all 439 rows of the PR's nine files. Two rows, written from RV1124's probes P10 and P7:
    • test_1077_a_url_git_dies_on_is_written_back_as_the_file_git_reads_gave_it (P10): the index's file gives a url that is not HEAD's, then a url with no value; the line that writes the url back takes it from the index's file.
    • test_1077_path_twice_records_its_put_back_for_the_next_finding (P7, on deleted and deletion-staged): HEAD's file holds second and a stale members/Old; path-twice's put-back is recorded, so gitmodules-path-unnamed after it prints none, and typed in order then a plain git commit, HEAD loses both.
  • N4: path-twice's wording. "back from HEAD's, as the index holds none first" put "first" on the wrong phrase. It says "back first from {whose}, {put_back}" now. Only TWICE_PUT_BACK in the new rows file pinned the old phrase.
  • N5: a stale docstring. _gitlink_out_first said its text ends "then commit .gitmodules"; it now says it ends as _section_then does, "then stage and commit" or "then stage".

Rows. The new rows file has 15 rows. On main's script (6da757cb) it gives 13 failed and 2 passed (the 2 are N4's, by design). On 672845cc it gives 14 passed and 1 failed: the first row (test_1077_a_second_section_the_index_alone_gives_is_path_twice) fails on N4's old wording. The P7 and P10 rows pass on 672845cc, whose code was right; they kill the mutants below. On this head, 15 passed.

Mutants. Each ran in its own worktree over the new rows file, and the script was restored after each and its sha256 checked.

Mutant Rows that failed
A: the value's write-back from HEAD's where the work tree's file is gone 1 (the P10 row: the last line wrote the HEAD url, not the staged one)
B: path-twice names its put-back without recording it (_file_put_back(root, None)) 2 (the P7 rows: gitmodules-path-unnamed printed the put-back again)

Gate. On a scratch merge of main 038dafba9 and 2d4cb64b, whose tree is this head's: the usual environment (EMAIL, an empty GIT_CONFIG_GLOBAL, GIT_CONFIG_NOSYSTEM=1, test identities, a stub gh first on PATH, GH_TOKEN=bogus, an empty GH_CONFIG_DIR). 0 failed, 3458 passed and 27 skipped in all.

  • test_path_guard_hygiene.py: 49 passed.
  • The 64 files of every tests/test_family*.py and tests/test_validate_family*.py (the PR's nine among them), plus test_repo_hygiene.py, test_agents_md_codes.py, test_cli_reference.py, test_pinned_copy_text.py and test_doctor_validator_rows.py: 2964 passed and 24 skipped, in four shards (685; 997 and 20 skipped; 557 and 4 skipped; 725). Those four shards were cut by a checkpoint and re-run on the same merge with the same lists.
  • test_shape_doctor.py, which runs validate-family.py, in 8 shards by test id, each list non-empty: 445 passed and 3 skipped.

Merge-trees. git merge-tree --write-tree origin/main HEAD at 8dfa32bd against main 038dafba9 is clean (tree 23c7194f0b6a08f56b93bdc80a05924e90e703a4).

Closes #1077

🤖 Generated with Claude Code
Lane: openreposhape-4 (openRepoShape-4)

Summary by Sourcery

Align .gitmodules validation and remediation with Git’s effective file reads, while making all section fixes safe for ordinary commits.

Bug Fixes:

  • Make .gitmodules validation inspect the same worktree, index, or HEAD content that Git reads, detecting duplicate paths and invalid values in staged-only files.
  • Preserve Git-read .gitmodules content during remediation and ensure generated fixes retain valid staged changes.
  • Ensure section-removal guidance stages .gitmodules before committing so plain commits cannot leave invalid sections in HEAD.

Enhancements:

  • Add comprehensive regression coverage for index-only .gitmodules content, missing worktree files, remediation ordering, case variants, and plain-commit behavior.

Tests:

  • Expand validation tests to cover Git's index/HEAD fallback behavior, staged remediation commands, clone gates, and mutation scenarios.

…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>
@sourcery-ai

sourcery-ai Bot commented Oct 21, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates validate-family’s .gitmodules checks to follow Git’s work-tree/index/HEAD read precedence, preserves the file Git actually reads during remediation, and ensures every printed section fix stages the resulting file so a plain commit produces a valid repository, with comprehensive regression coverage for all four follow-ups.

Sequence diagram for Git-aware .gitmodules validation and remediation

sequenceDiagram
    participant Validator
    participant GitmodulesChecks
    participant Git
    participant Report

    Validator->>GitmodulesChecks: _check_members(root, members, no_members, report)
    GitmodulesChecks->>GitmodulesChecks: _check_gitmodules_values(root, members, report)
    GitmodulesChecks->>GitmodulesChecks: _listed_as_git_reads(root)
    alt .gitmodules exists in work tree
        GitmodulesChecks->>Git: _gitmodules_listed(root)
        Git-->>GitmodulesChecks: work tree file
    else work tree file is absent
        GitmodulesChecks->>Git: _gitmodules_put_back(root)
        Git-->>GitmodulesChecks: index or HEAD spec
        GitmodulesChecks->>Git: _blob_listed(root, spec)
        Git-->>GitmodulesChecks: index or HEAD file
    end
    GitmodulesChecks->>Report: finding gitmodules-value-refused
    GitmodulesChecks->>Report: _file_put_back(root, report)
    GitmodulesChecks->>GitmodulesChecks: _check_gitmodules_paths(root, sections, report)
    GitmodulesChecks->>Report: finding gitmodules-path-twice
    GitmodulesChecks->>Report: _file_put_back(root, report)
    GitmodulesChecks->>Report: print stage and commit .gitmodules
Loading

Flow diagram for .gitmodules read precedence and staging

flowchart TD
    A[validate-family checks .gitmodules] --> B{Work tree .gitmodules exists?}
    B -->|yes| C[Read work tree file]
    B -->|no| D[_listed_as_git_reads]
    D --> E{Index file available?}
    E -->|yes| F[Read index file]
    E -->|no| G[Read HEAD file]
    C --> H[_check_gitmodules_values]
    F --> H
    G --> H
    H --> I{Value git dies on?}
    I -->|yes| J[_file_put_back]
    J --> K[Print git config remediation]
    K --> L{HEAD also gives value?}
    L -->|yes| M[Print then stage and commit .gitmodules]
    L -->|no| N[Print commit the index's .gitmodules]
    I -->|no| O[_check_gitmodules_paths]
    O --> P{Duplicate path?}
    P -->|yes| Q[_file_put_back if needed]
    Q --> M
    P -->|no| R[Continue validation]
Loading

File-Level Changes

Change Details Files
Align .gitmodules validation with Git’s file-selection behavior when the work-tree file is absent.
  • Added shared logic to inspect the work-tree file, then the index, then HEAD, including Git-readable blob handling and refusal cases.
  • Updated duplicate-path and invalid-value checks to report the actual file Git reads.
  • Preserved staged index edits during remediation by recording a single put-back and avoiding later overwrites.
  • Adjusted member-gitmodules-absent coordination so it restores the file only when needed.
templates/family-root/scripts/validate-family.py
Make generated remediation commands safe for a subsequent plain commit.
  • Added git add -- .gitmodules to section-removal remedies in all branches.
  • Changed affected messages to distinguish staging alone from staging and committing.
  • Updated duplicate-path remedies to restore the Git-read file before editing and then stage and commit it.
templates/family-root/scripts/validate-family.py
tests/test_family_add_refusals.py
tests/test_family_hand_mount_git_rm_lines.py
tests/test_family_unnamed_gitlink.py
tests/test_validate_family_remedies_typed.py
tests/test_validate_family_unnamed_mounts.py
Expand regression coverage for index-only .gitmodules states and commit/remediation ordering.
  • Added N1–N4 scenarios covering index-only sections, invalid values, HEAD-only values, missing files, stale sections, gitlinks, and case variants.
  • Each scenario types only printed commands, performs a plain commit, and verifies the gate, clone, HEAD contents, and clean status.
  • Updated existing expectations and composed-remedy tests for index put-backs and mandatory staging.
tests/test_validate_family_index_gitmodules.py
tests/test_family_gitmodules_values.py
tests/test_family_hand_mount_git_rm_lines.py
tests/test_family_holder_bootstrap_texts.py
tests/test_family_holder_remedies_compose.py
tests/test_family_unnamed_gitlink.py
tests/test_validate_family_remedies_typed.py
tests/test_validate_family_unnamed_mounts.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1077 Make gitmodules-path-twice inspect the .gitmodules contents Git reads: the work-tree file when present, otherwise the index's file, then HEAD's. ✅
#1077 Make the check for values Git refuses to read inspect the same Git-selected .gitmodules file as well as HEAD's, and ensure its remedies preserve and stage the file Git reads. ✅

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

RV1124: independent review of #1124 at 672845cc: NOT READY

One SHOULD (S1): #1111's hygiene test fails this PR's new guard once #1111 is on main. The rest are NITs, with the suspected follow-up measured and graded below. The change itself reads the file git reads in every state I measured, and its lines type cleanly.

What I measured

Which .gitmodules git 2.43.0 reads, against what the script now reads. Each state is a scratch repository with gitlinks at members/W, members/I and members/H. The work tree's file maps W, the index's maps I and HEAD's maps H. "git" is the path git submodule status -- <path> maps. The script's columns are _listed_as_git_reads (the value check) and _sections_as_git_reads (path-twice and path-unnamed), loaded from the PR's script.

State git reads value check sections put-back line
only in the work tree W W W none
only in the index I I (:.gitmodules) I git checkout -- .gitmodules
only in HEAD H H (HEAD:.gitmodules) H git checkout HEAD -- .gitmodules
work tree's differs from the index's W W W none
deleted in the work tree I I I git checkout -- .gitmodules
a symlink to a file W (through it) W W none
a dangling symlink, or a directory nothing gitmodules-unreadable refusal same none
unmerged (stages 1-3), file on disk nothing W W none
unmerged, file deleted nothing H H git checkout HEAD -- .gitmodules
  • The script agrees with git in every state but the unmerged ones (N6), which v1.1.1's release notes already list as a known limit.
  • Git's file_exists is an lstat, so os.path.lexists is the right test. A symlinked .gitmodules is read and written through by git, but git add -- .gitmodules refuses it (invalid path '.gitmodules', exit 128). The stage line cannot be typed there, but git will not track such a file at all, and the old "then commit" could not either. No finding.

Remedies typed as printed, then a plain git commit. These are probe rows in my scratch copy, not in the PR.

  • P2: path-twice where HEAD's file is the source (deletion staged). Gate, git submodule update and a plain clone all pass, exit 0.
  • P3: a plain git rm members/IRRS, then a value staged in the index's file in IRSS's section, the work tree's deleted.
    • On main the value is missed.
    • On the PR the value finding puts back the index's file, and the removal writes MEMBER's section back "in the file a line above put back".
    • Typed in order and committed, the gate passes in every mode and a plain clone checks both out.
  • P5: a value in HEAD's alone, the index's mended but with a stale members/Old section, the work tree's deleted.
    • The PR says "Commit the index's", and path-unnamed puts the index's file back itself.
    • Typed and committed: HEAD gives two members, no value and no Old, and the gate exits 0.
  • P7: path-twice (second) and a stale section in HEAD's file, deletion staged. Typed in order: HEAD loses both, and the gate exits 0 in both modes.

Copies. templates/family-root/scripts/validate-family.py is the only copy in the tree. No 64-hex digest appears in templates/ or tests/: contracts/shape-pin.yaml is filled from placeholders at scaffold time. test_pinned_copy_text, test_agents_md_codes, test_cli_reference and test_doctor_validator_rows pass (below).

Rows on main's script. The new file gives 10 failed and 2 passed, as claimed. The 2 are the N4 rows. The failures are for the right reasons:

  • N1 and N2 are family ok, exit 0.
  • N2b and N2c fail on text: main puts back HEAD's.
  • The six N3 rows fail commands(found[0])[-1] == "git add -- .gitmodules".

Findings

S1 (SHOULD): #1111's test_path_guard_hygiene.py fails the new guard in _listed_as_git_reads.

N1 (NIT, follow-up): the suspected follow-up is real, pre-existing and self-correcting.

  • P1 setup: update = !true in IRRS's section, in the work tree's file on disk and staged; HEAD's does not give it.
  • Round 1 prints only git config -f .gitmodules --unset-all submodule.members/IRRS.update, with no stage word. After it and a plain git commit:
    • HEAD gives !true;
    • a plain clone's git submodule update --init --recursive exits 128 (fatal: invalid value for 'submodule.members/IRRS.update');
    • the next gate run says "in HEAD ... Stage and commit the work tree's .gitmodules".
  • Main's script prints the same text and gives the same result, so this is not a regression. CI's gate would refuse such a commit.
  • P1b, a sibling: the index alone gives the value and the work tree's file is clean on disk. The gate is family ok in --no-members (exit 0), and a plain commit records the value in HEAD.
  • Fix, in a follow-up issue: where the work tree's file is on disk, read the index's too (_blob_listed(root, ":.gitmodules")). Where it gives the value, end the lines with git add -- .gitmodules and "then stage" (or "then stage and commit" where HEAD gives it). Printing the stage line as a line, as _section_out now does, would also give N2's "then stage" a line to type.

N2 (NIT, pre-existing): an index blob with a line git cannot parse, after a value, is family ok.

N3 (NIT): two of the new behaviours are pinned by no row (mutants A and B below survive all 439 rows of the PR's 9 files).

  • (a) The value's write-back reads the file git reads.
    • P10: the index's IRRS section gives url = https://github.com/InkRouter/IRRS.git, then a url with no value, the work tree's deleted.
    • The PR writes back that url. Mutant A writes back HEAD's url, which silently undoes the staged change.
  • (b) path-twice records its put-back.
    • Under mutant B, P7's path-unnamed prints git checkout HEAD -- .gitmodules again, and the commit puts second back in HEAD.
  • Fix: add P7 and P10 as rows. Each kills its mutant here.

N4 (NIT): path-twice's wording where HEAD's file is the source.

  • The text reads "back from HEAD's, as the index holds none first, git checkout HEAD -- .gitmodules". The word "first" lands on "the index holds none".
  • Fix: "put .gitmodules, gone from the work tree, back first from {whose}, ..." (validate-family.py:2318).

N5 (NIT): a docstring this PR made stale. _gitlink_out_first still says "It ends as the section's finding does, then commit .gitmodules, but where HEAD's .gitmodules holds no section of its name ... staged" (validate-family.py:3368-3372). Every branch stages now.

N6 (NIT, pre-existing): an unmerged .gitmodules.

Mutants (mine)

Each mutant ran in its own worktree of the scratch merge, with -x, over the PR's 9 test files (439 rows). Each file was restored afterwards, and its sha256 checked: d0b4c63d... is the head's.

Mutant Result
A: the value's write-back from HEAD's where the work tree's file is gone (as before) SURVIVED, 439 passed; P10 kills it
B: path-twice names its put-back without recording it (_file_put_back(root, None)) SURVIVED, 439 passed; P7 kills it
C: path-twice and path-unnamed read nothing where HEAD's alone gives a value (old refused gating) killed: test_a_value_head_s_file_alone_gives_leaves_the_work_tree_s_read
D: READ_HERE names the index's file "the work tree" killed: N2
E: the value check reads HEAD's, never the index's killed: N2
F: the value's lines always say "then stage" killed: test_the_gate_says_which_file_gives_it[work-tree-only]
I: the value's put-back is not recorded killed: test_two_keys_with_no_gitmodules_in_the_work_tree_put_it_back_once
P: the value's lines never say "commit" killed: test_a_value_git_dies_on_fails_bootstrap_and_every_mode_of_the_gate[update-None]

Merge-trees

Gate

The gate ran on scratch merge e8d7f845 (main 4ed7c7bf + 672845cc), with /usr/bin/python3 -m pytest -p no:cacheprovider and the landing gate's environment.

Lane: openreposhape-4 (openRepoShape-4)

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

Copy link
Copy Markdown
Contributor Author

RV1124b: independent review of #1124 at 8dfa32bd: READY

No MUST and no SHOULD. RV1124's S1 is fixed, and N3, N4 and N5 are done as the fix round says. Two mutants survive all 442 rows of the PR's nine test files, and one docstring line overstates; all three are NITs and can go to a follow-up.

What I measured

Everything ran on scratch merge 338635f4 of origin/main 038dafba9 and 8dfa32bd (tree 23c7194f0b6a), with /usr/bin/python3 -m pytest -p no:cacheprovider and the landing gate's environment: EMAIL set, GIT_ALLOW_PROTOCOL, GIT_SSH_COMMAND and LANES_* unset, an empty GIT_CONFIG_GLOBAL, GIT_CONFIG_NOSYSTEM=1, test identities, a stub gh first on PATH, GH_TOKEN=bogus and an empty GH_CONFIG_DIR. Git is 2.43.0.

S1: _listed_as_git_reads asks no guard of its own.

  • Its only caller is still _check_gitmodules_values (validate-family.py:2052, (on_disk, "") if present else _listed_as_git_reads(root)), so it is asked only where os.path.lexists(root / ".gitmodules") is False. That is the predicate the dropped guard tested, and _gitmodules_put_back asks it again. No test calls the function.
  • Behaviour. A probe loaded both scripts, 672845cc and 8dfa32bd, and compared _check_gitmodules_values in 12 states: its return value, its findings, Report.put_back, and _listed_as_git_reads itself where the file is gone. The states are RV1124's (only in the work tree, only in the index, only in HEAD, the work tree's differing from the index's, deleted from the work tree, a symlink to a file, a dangling symlink, a directory, unmerged on disk, unmerged and deleted), plus no file at all, plus deleted from disk and not staged. All 12 agree. The file each reads matches RV1124's table.
  • test_path_guard_hygiene.py on the merge: 49 passed. That includes test_every_allowlist_entry_names_a_guard_that_is_there, so no BENIGN entry is stale, and no entry names _listed_as_git_reads. With 672845cc's script in its place, test_no_bare_guard_outside_a_stat_helper_or_the_allowlist[templates/family-root/scripts/validate-family.py] fails (1 failed, 48 passed), so the test pins S1.

N3: the new rows file (15 rows).

  • At this head: 15 passed.
  • At 672845cc's script: 14 passed, 1 failed. The one is test_1077_a_second_section_the_index_alone_gives_is_path_twice (N4's old wording); the P7 and P10 rows pass there.
  • At main's script: 13 failed, 2 passed (the 2 are N4's rows).
  • All three match the body.

N4 and N5.

  • Path-twice now says "put .gitmodules, gone from the work tree, back first from {whose}, {put_back}, so that every other section stays, and take each other out by hand". The only pin is TWICE_PUT_BACK in the new rows file, which covers the index's case. PUT_BACK_THEN (test_family_holder_remedies_compose.py:135) pins another finding's wording (script line 1074), which this round leaves alone.
  • _gitlink_out_first's docstring now agrees with _section_then, which it calls. No "then commit .gitmodules" is left in the script.

Nothing else moved. 672845cc..8dfa32bd touches the script (+12/-12: N4's string, N5's docstring, S1's docstring and the two dropped lines) and the new rows file (TWICE_PUT_BACK, STALE, and the P7 and P10 rows). I found no behaviour change the body does not name.

Mutants

Each ran in its own worktree of the scratch merge. Each script was restored afterwards, and its sha256 checked against the head's (63f4b9d5...).

Mutant Result
A: the value's write-back reads HEAD's where the work tree's file is gone (here if present else there) killed by the P10 row (1 of 15)
B: path-twice names its put-back without recording it (_file_put_back(root, None)) killed by both P7 rows (2 of 15)
S1a: the bare lexists guard back (672845cc's script) killed by test_path_guard_hygiene.py
S1b: the caller always asks _listed_as_git_reads, dropping present survived the new rows file; killed by test_family_gitmodules_values.py::test_a_value_git_dies_on_fails_bootstrap_and_every_mode_of_the_gate[update-None]
S1c: _listed_as_git_reads always reads :.gitmodules, with no fallback to HEAD's SURVIVED all 442 rows (15, then 427 with -x): NIT 2
S1d: _listed_as_git_reads returns b"" killed (2 of 15)
S1e: _listed_as_git_reads returns spec "" killed (3 of 15)
PT4: path-twice records a put-back for every path, not only one given twice killed (9 of 15)
PT7: path-twice always says "from the index's" SURVIVED all 442 rows (15, then 427 with -x): NIT 1

Merge-trees

Gate

  • tests/test_family*.py and tests/test_validate_family*.py (59 files, the PR's 9 among them), plus test_repo_hygiene.py and test_path_guard_hygiene.py: 61 files in 6 shards, 2785 passed, 24 skipped, 0 failed (365; 474; 366 and 4 skipped; 499 and 19 skipped; 553; 528 and 1 skipped).
  • test_shape_doctor.py in 8 shards of 56 ids from a bash script (mapfile -t, every list checked non-empty, no timeout): 445 passed, 3 skipped, 0 failed (56 in each shard but the last, 53 and 3 skipped). The 448 ids came from --collect-only -q; the 8 lists, concatenated, equal that list (cmp), with no id twice. 25 ids hold spaces.

Findings

NIT 1: path-twice's wording where HEAD's file is the source is pinned by no row.

  • Mutant PT7, which always says "back first from the index's", survives all 442 rows. The P7 row on deletion-staged checks only that the put-back line is in the text.
  • Fix: in test_1077_path_twice_records_its_put_back_for_the_next_finding, assert f"back first from {GONE[how][0]}, `{put_back}`" in twice, which pins "HEAD's, as the index holds none" on deletion-staged.

NIT 2: no row pins the value check's fallback to HEAD's where the index holds no .gitmodules.

  • Mutant S1c reads :.gitmodules alone and survives all 442 rows. With the deletion staged and HEAD's file giving a value, it reads nothing as the file git reads. It would then say "Commit the index's .gitmodules, which git reads here and does not give it", and that commit records the deletion. The head reads HEAD's there, as git does: the state probe's "only in HEAD" puts back git checkout HEAD -- .gitmodules and names the value.
  • Fix: add a row in which HEAD's file gives update = !true in a member's section and the deletion is staged (gone(holder, env, "deletion-staged")). Assert that the first line is git checkout HEAD -- .gitmodules and that the text ends "then stage and commit .gitmodules"; type the lines in order, make a plain git commit, and check that HEAD gives no value and the gate passes.

NIT 3: _listed_as_git_reads's docstring opens by saying it returns .gitmodules "as git reads it".

  • Where the work tree's file is on disk, it now returns nothing. The later sentence, "The caller asks it only where the work tree's file is gone", covers today's one caller, but a new caller could take the first line at its word.
  • Fix: open with "where the work tree's .gitmodules is gone from disk," or say it returns nothing where the work tree's file is on disk.

Not run: RV1124's P1 to P10 probes, which the brief left optional.

Lane: openreposhape-4 (openRepoShape-4)

brettheap pushed a commit that referenced this pull request Oct 23, 2026
…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>
@brettheap brettheap added this to the v1.2.1 milestone Oct 23, 2026
@brettheap
brettheap marked this pull request as ready for review October 23, 2026 18:15
@sourcery-ai

sourcery-ai Bot commented Oct 23, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 17 hours and 31 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit dea964a into main Oct 23, 2026
5 checks passed
@brettheap
brettheap deleted the fix/1077-gitmodules-checks-read-the-file-git-reads branch October 23, 2026 18:16
brettheap pushed a commit that referenced this pull request Oct 23, 2026
Restack of #1208 (see #1181) after #1124 squash-landed as dea964a: merge one of three. The parent's final head, whose tree is the squash's tree.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 23, 2026
Restack of #1208 (see #1181): merge two of three. The squash's tree equals the parent's final head's tree, so the conflicts in #1124's two files that this branch also changes (validate-family.py and test_validate_family_index_gitmodules.py) take this branch's side, which holds the squash's content plus #1208's change.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 23, 2026
Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 23, 2026
…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 added a commit that referenced this pull request Oct 23, 2026
…w 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)

It follows #1124 (for #1077), which landed as `dea964a`; W1181r restacked it onto main with only its own change (#1208 issuecomment-6371837537). It is the follow-up of RV1124b's review of #1124 (see #1150 for the neighbouring follow-ups from the first review).

## What changed

- `tests/test_validate_family_index_gitmodules.py`
  - **PT7 (NIT 1).** In `test_1077_path_twice_records_its_put_back_for_the_next_finding`, after the existing `` `{put_back}` `` assertion, a second assertion pins the whole put-back wording: `back first from {GONE[how][0]}, `{put_back}`, so that every other section stays`. It rides on the two existing rows, `[deleted]` (the index's) and `[deletion-staged]` (HEAD's, as the index holds none). The row's docstring says path-twice names whose file it puts back.
  - **S1c (NIT 2).** A new row, `test_1077_a_value_head_gives_where_the_deletion_is_staged_puts_head_s_back`. `update = !true` is committed in `members/IRRS`'s section and the deletion of `.gitmodules` is staged, so the index holds no file and git reads HEAD's. The row checks the premise (`git submodule status` exits 128), then that `found_alike(holder, *MODES)` returns the one finding as the issue gives it, whole. It types the two lines in order, runs `git add -- .gitmodules` and a plain `git commit`, and checks that `!true` is not in HEAD, `head_paths` is `{ROW, OTHER_ROW}`, `git status --porcelain` is empty, and `ends_well` passes (the gate in every mode, and a plain clone).
- `templates/family-root/scripts/validate-family.py` (NIT 3): the docstring of `_listed_as_git_reads` only. It opens "Where the work tree's .gitmodules is gone from disk, ..." and says it returns `(b'', '')` where the work tree's file is on disk. The script has no pinned copy and no checked-in digest. Its AST is equal to `8dfa32bd7`'s once that one docstring is dropped (sha256 `63f4b9d5...` there, `30abb4ea...` here).

## Counts

- The rows file has 16 rows: 15 old and 1 new. PT7's assertion adds no row.
- On this head: 16 passed.
- Gate, 14 files, one pytest run on this head: **777 passed** (exit 0). The files are the rows file, `tests/test_repo_hygiene.py`, the eight other test files #1124 changed (`test_family_add_refusals`, `test_family_gitmodules_values`, `test_family_hand_mount_git_rm_lines`, `test_family_holder_bootstrap_texts`, `test_family_holder_remedies_compose`, `test_family_unnamed_gitlink`, `test_validate_family_remedies_typed`, `test_validate_family_unnamed_mounts`), the three more that name the functions and codes it touched (`test_family_add_spelling_leftovers`, `test_family_bootstrap_gate_order`, `test_validate_family_typed_states`), and `test_pinned_copy_text.py`.
- The same 14 files on a scratch merge commit of `origin/main` (`961640c68`) and this head: **778 passed** (exit 0). The one extra id is a new row in `test_repo_hygiene.py` on `main`.

## Mutants

Each mutant was applied to `validate-family.py` in a scratch worktree of this head, run against the new rows file (16 rows) and a copy of #1124's original 15-row file (31 rows together), and restored with its sha256 checked (`30abb4ea...` before and after).

| Mutant | Edit | New file (16 rows) | #1124's 15 old rows |
|---|---|---|---|
| PT7 | path-twice's `f"from {whose}, ` written `f"from the index's, ` | 1 failed: `[deletion-staged]`; `[deleted]` passes | 15 passed (survives) |
| S1c | `_listed_as_git_reads` reads `spec = f":{GITMODULES}"` alone, with no fallback to HEAD's | 1 failed: the new S1c row | 15 passed (survives) |

## Merge-trees

`git merge-tree --write-tree` of each ref against this head was clean:

- `origin/main` (`961640c68`)
- #1124's head (`8dfa32bd7`)
- #1166's head (`5102b4294`) and #1172's head (`5aa0f116d`), the two other open PRs that touch `validate-family.py`; and the head of #1172's base branch (`b75babee3`)

No other open PR touches `tests/test_validate_family_index_gitmodules.py`.

Closes #1181

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Nov 8, 2026
…py: an `id:`, a date or a `repository:` ending in a line feed is refused by name, and a pin's `source_repository:` that is no path or `owner/repo` name is named as the pin's (#1139) (#1166)

The value patterns the issue names refuse a value that ends in a line feed, by name, as #1126 (#1017) makes `COMMIT_RE`, `SHA256_RE` and `COMMIT_40` do:
- `DATE_RE` and `QUALIFIED_RE` in `validate-manifest.py` and `validate-family.py`, `MEMBER_RE` in `scripts/family.py`, `DATE_RE` in `scripts/shape_advisory.py` and `REPOSITORY_RE` in `update-shape.py`;
- `PROJECT_ID_RE`, read with `fullmatch` where the two validators and `family.py add` read an id from a file.

A shape pin's `source_repository:` that is neither a path on this machine nor an `owner/repo` name is refused in the pin's name, not `--upstream`'s. Values without a line feed keep their outcome, and no refusal code is added or renamed. Two inputs do move from one existing code to another (see "Behaviour changed", below).

**What this PR does not cover, and the title does not claim:**
- the naming-policy patterns that `scripts/repo_shape.py` reads from `contracts/repository-naming.yaml`, which `validate-repository-naming.py` still reads with `.match`;
- `update-upstream-not-a-repo`, which still says `--upstream` when a pin names a folder that exists and is not a repository.

Both are follow-ups, with the other findings of the independent review, under "Follow-ups (not in this PR)".

This does not need #1126 and does not touch its files or its test file.

Measured on main `038dafba` (Python 3.12.3, git 2.43.0) by a probe of each tool, before any change. Each value is written in the file as a YAML double-quoted `"...\n"`, which the reader takes as the value and a line feed.

| Read from | Value | On main |
|---|---|---|
| `project.yaml` (`validate-manifest.py`) | `id: "atlas\n"` | only `FINDING manifest-topic: topic is 'xf-project-atlas', expected 'xf-project-atlas\n' derived from id 'atlas\n'`, exit 1 |
| `project.yaml` | `elected_on: "2026-09-02\n"` | `manifest ok`, exit 0 |
| `project.yaml` | a leg's `repository: "testorg/Atlas-spec\n"` | `manifest ok`, exit 0 |
| `family.yaml` (`validate-family.py`) | `id: "inkrouter\n"` | `family ok: InkRouter (inkrouter` and a line break, exit 0 |
| `family.yaml` | `created_on: "2026-09-04\n"` | `family ok`, exit 0 |
| `family.yaml` | a member's `repository: "InkRouter/IRRS\n"` | `FINDING member-repository-name: ... whose name is 'IRRS\n'`, exit 1 |
| a member's `project.yaml`, read by `family.py add` | `id: "irqs\n"` | mounted and committed, exit 0, with the row `id: "irqs\n"`; the holder's gate then said `family ok` |
| `contracts/shape-pin.yaml`, `update-shape.py check` with no `--upstream` | `source_repository: "opensoft/openRepoShape\n"` | `REFUSED unsafe-value: --upstream is 'opensoft/openRepoShape\n'`, exit 2 |
| the same | `source_repository: "opensoft/open Repo"` | `REFUSED update-upstream-unresolvable: --upstream 'opensoft/open Repo' is neither ...`, exit 2 |
| the same | `source_repository: "-opensoft/openRepoShape"` | `REFUSED unsafe-value: --upstream is '-opensoft/openRepoShape'`, exit 2 |
| an adoption plan (`adopt-project.py check`) | `id: "northwind\n"` | `FINDING plan-manifest-invalid: id is 'northwind\n', which the project's own validate-manifest.py refuses: manifest-topic: ...`, exit 1; `execute` refused it, exit 2, before any leg |

In-process on the same main: `repo_shape.PROJECT_ID_RE.match('atlas\n')`, `shape_advisory.DATE_RE.match('2026-09-02\n')` and `REPOSITORY_RE.match('a/b\n')` each matched, and `date.fromisoformat('2026-09-02\n')` raised `ValueError`.

- **`DATE_RE` and `QUALIFIED_RE`** in `templates/assembly-root/scripts/validate-manifest.py` and `templates/family-root/scripts/validate-family.py` end in `\Z`. So do `MEMBER_RE` in `scripts/family.py` (the third spelling `tests/test_family_member_name.py` holds equal to the two `QUALIFIED_RE`), `DATE_RE` in `scripts/shape_advisory.py` and `REPOSITORY_RE` in `update-shape.py`.
- **`PROJECT_ID_RE` keeps its `$`.** Every id refusal prints its text (`must match ^[a-z0-9][a-z0-9-]*$`, pinned in six test files), the validators run against each project's own copy of `repo_shape.py`, and its definition sits inside #1126's hunk. The three callers that read an id from a file ask it with `fullmatch` instead: `validate-manifest.py` (`manifest-id`), `validate-family.py` (`family-id`) and `family.py`'s reading of a member's `project.yaml` (`member-bad-id`, which `add` now says before anything is mounted).
- **`update-shape.py`.** `open_upstream(spec, pin_path=None)` takes the pin the value was read from, which `prepare` passes when no `--upstream` was given. Then `update-upstream-unresolvable` says ``<root>/contracts/shape-pin.yaml: `source_repository:` is '...', which is neither a path on this machine nor an `owner/repo` name``, with a remedy to write the pin's field or pass `--upstream`, and `checked_value` names the same field. **Code kept:** `update-upstream-unresolvable`, the existing code for a value that is not an `owner/repo` name. A line feed now reaches it, since the pattern no longer takes one. Before, `checked_value` refused it as `unsafe-value`.
- **One existing test line:** `tests/test_family_init_values.py` finds validate-family's `DATE_RE` line by its text, which now ends `\Z`.
- **One existing stand-in:** the `temporaries` fixture in `tests/test_update_shape_unreadable.py` stands in for `open_upstream` with `def opening(spec)`, which the new two-argument call refused with `TypeError: ... takes 1 positional argument but 2 were given`. It now takes `pin_path=None` and hands it on to the real `open_upstream`, so `test_a_read_refused_in_prepare_closes_the_upstream[check|apply]` asks what it asked before.
- **The rows** are in a new file of their own, `tests/test_value_pattern_line_break.py`.
- **A second guard, in a second commit (`5102b4294`, a test only, no production file).** The first guard holds the four pattern names this PR ends in `\Z`, and nothing held `PROJECT_ID_RE`, which keeps its `$`. `test_every_reader_of_the_id_pattern_is_in_the_table` reads every tracked `.py` outside `tests/` with `ast` (it runs none) and holds each call of a method of `PROJECT_ID_RE` (`PROJECT_ID_RE.x(...)` or `repo_shape.PROJECT_ID_RE.x(...)`) to the table `ID_READERS`:
  - the three `fullmatch` readers of an id from a file (`validate-manifest.py`, `validate-family.py`, `scripts/family.py`);
  - the modules that still `match` it, each with its reason: argv `--id` in `scaffold-project.py` and `scripts/family.py`, where `checked_value` refuses a line feed after the pattern; argv `--id` in `adopt-project.py`, which the project's own `validate-manifest.py` refuses (`adopt-manifest-invalid`, below; `ID_READERS` still gives it the `checked_value` reason); and `adopt-project.py`'s two `plan-bad-id` readers, of the plan's `id:` and of the id derived from `legs.assembly`, which a line feed never passes (below).

  A new reader of `PROJECT_ID_RE`, `fullmatch` included, fails it by file and method until it is listed in `ID_READERS` with its reason. The table is exact both ways, so a reader taken away fails it too.

**Left as they are, and why:**
- the argv readers of `PROJECT_ID_RE` (`--id` in `scaffold-project.py`, `family.py init` and `adopt-project.py`). The first two hold the id to `checked_value` after the pattern, which refuses a line feed as `unsafe-value` naming `--id`, and `tests/test_derived_id_refusals.py` pins that answer for them. `adopt-project.py` never calls `checked_value` on `--id`: `plan` with `--id` set to `northwind` and a line feed exits 2 with `adopt-manifest-invalid`, in the words of the project's own `validate-manifest.py` (`manifest-id`), and writes no plan (lane 5's re-check, issuecomment-6371408915, NIT 2; measured on `plan` only);
- `adopt-project.py`'s reading of a plan's `id:`: `check` and `execute` hold it to the project's own `validate-manifest.py`, which refused it on main (above) and now names it in `manifest-id`'s words. It is the same value, refused under the same code, `plan-manifest-invalid`, before any leg exists. The id derived from `legs.assembly` is read with `.match` too, and a line feed in `legs.assembly` is `unsafe-value` before that id is asked (the review measured both, on main and here alike). This keeps the open PRs on `adopt-project.py` (24 at 12:02Z) clear;
- `PIN_ARG_RE`, and `NAME_RE`/`REF_RE`/`PATH_RE` in `setup-project.py`, as the issue says;
- `update-upstream-not-a-repo` still says `--upstream`. A pin's value reaches it only when it names a path that exists, not through the pattern.

**Behaviour changed, stated plainly, and what the issue's Verify means by "No refusal code changes":** the issue's Verify line reads as no code added or renamed, and none is. Two inputs do move from one existing code to another, exit 2 both before and after:
- `update-shape.py --upstream "opensoft/openRepoShape<LF>"` was `unsafe-value` and is now `update-upstream-unresolvable`, still naming `--upstream`;
- a pin's `source_repository:` that ends in a line feed, with no `--upstream`, was `unsafe-value` naming `--upstream` and is now `update-upstream-unresolvable` naming the pin's field.

No test pinned either old answer: every gate below passed. `family.py add --member` is stripped before `MEMBER_RE`, so `add` answers as before.

**Not changed, seen while measuring:** with `--upstream` passed, `update-shape.py` does not read the pin's `source_repository:` through `REPOSITORY_RE` at all, and it keeps the value as the upstream's repository name. `validate-family.py` does not hold a member row's `id:` to `PROJECT_ID_RE`; it compares the row with the member's manifest. Neither is a `$` question. Both are under "Follow-ups (not in this PR)".

All in `tests/test_value_pattern_line_break.py`, run against main's production code: the five production files swapped in place from `origin/main` (`961640c68`), then put back with their sha256 matching (`dirty=5` in the run's START line is those five files). On the head `5102b4294` (30 rows) the result is **20 failed, 10 passed in 23.12s, git 2.43.0**. The 10 that pass are baselines: the seven `test_a_pattern_takes_its_value` rows, the spelling-table guard and the two double-quoted-without-a-line-feed rows.

The first 19 are the first commit's rows (19 failed, 10 passed in 22.12s on its 29 rows at `038dafba`). The independent review reproduced them from main's bytes of the same five files (19 failed, 10 passed in 26.78s, the same 19 ids). The 20th, `test_every_reader_of_the_id_pattern_is_in_the_table`, is the second commit's: it fails on main because the three readers of an id from a file are `.match` there, not `.fullmatch`.

- `test_a_pattern_refuses_its_value_and_a_line_feed[manifest-DATE_RE]`
- `test_a_pattern_refuses_its_value_and_a_line_feed[manifest-QUALIFIED_RE]`
- `test_a_pattern_refuses_its_value_and_a_line_feed[family-DATE_RE]`
- `test_a_pattern_refuses_its_value_and_a_line_feed[family-QUALIFIED_RE]`
- `test_a_pattern_refuses_its_value_and_a_line_feed[family.py-MEMBER_RE]`
- `test_a_pattern_refuses_its_value_and_a_line_feed[shape_advisory-DATE_RE]`
- `test_a_pattern_refuses_its_value_and_a_line_feed[update-shape-REPOSITORY_RE]`
- `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[id]`
- `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[elected_on]`
- `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[leg-repository]`
- `test_adopt_check_names_a_plan_id_that_ends_in_a_line_break_as_an_id`
- `test_a_family_value_that_ends_in_a_line_break_is_a_finding[id]`
- `test_a_family_value_that_ends_in_a_line_break_is_a_finding[created_on]`
- `test_a_family_value_that_ends_in_a_line_break_is_a_finding[member-repository]`
- `test_add_refuses_a_member_whose_id_ends_in_a_line_break`
- `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]`
- `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[space]`
- `test_a_pins_source_checked_value_refuses_is_named_as_the_pins`
- `test_an_upstream_passed_with_a_line_feed_is_named_as_passed`
- `test_every_reader_of_the_id_pattern_is_in_the_table`

**13 of 13 killed.** Each mutant was applied alone to one file of the branch's production bytes (the same bytes as the first commit and as the head, which adds a test only), the rows were run, and the file was put back with its sha256 checked (`restored=yes` for all 13). Each run is `tests/test_value_pattern_line_break.py` plus `tests/test_family_member_name.py::test_every_spelling_of_the_member_name_rule_is_the_same_pattern`, git 2.43.0.

| Mutant | File | Killed by |
|---|---|---|
| M1 `DATE_RE` back to `$` | `validate-manifest.py` | 2: `test_a_pattern_refuses_its_value_and_a_line_feed[manifest-DATE_RE]`, `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[elected_on]` |
| M2 `QUALIFIED_RE` back to `$` | `validate-manifest.py` | 3: `...[manifest-QUALIFIED_RE]`, `...[leg-repository]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` |
| M3 the id read with `.match` | `validate-manifest.py` | 2: `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[id]`, `test_adopt_check_names_a_plan_id_that_ends_in_a_line_break_as_an_id` |
| M4 `DATE_RE` back to `$` | `validate-family.py` | 2: `...[family-DATE_RE]`, `test_a_family_value_that_ends_in_a_line_break_is_a_finding[created_on]` |
| M5 `QUALIFIED_RE` back to `$` | `validate-family.py` | 3: `...[family-QUALIFIED_RE]`, `...[member-repository]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` |
| M6 the id read with `.match` | `validate-family.py` | 1: `test_a_family_value_that_ends_in_a_line_break_is_a_finding[id]` |
| M7 `MEMBER_RE` back to `$` | `scripts/family.py` | 2: `...[family.py-MEMBER_RE]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` |
| M8 a member's id read with `.match` | `scripts/family.py` | 1: `test_add_refuses_a_member_whose_id_ends_in_a_line_break` |
| M9 `DATE_RE` back to `$` | `scripts/shape_advisory.py` | 1: `...[shape_advisory-DATE_RE]` (its pattern row alone: `date.fromisoformat` refuses the line feed after the pattern, so no tool row sees it) |
| M10 `REPOSITORY_RE` back to `$` | `update-shape.py` | 3: `...[update-shape-REPOSITORY_RE]`, `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]`, `test_an_upstream_passed_with_a_line_feed_is_named_as_passed` |
| M11 `prepare` passes no pin | `update-shape.py` | 3: `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]` and `[space]`, `test_a_pins_source_checked_value_refuses_is_named_as_the_pins` |
| M12 `checked_value` names `--upstream` again | `update-shape.py` | 1: `test_a_pins_source_checked_value_refuses_is_named_as_the_pins` |
| M13 the pin's refusal sentence removed | `update-shape.py` | 2: `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]` and `[space]` |

No mutant is added for the stand-in in `tests/test_update_shape_unreadable.py`: it is a test change.

The independent review re-ran M3, M8, M11 and M12, and one mutant of its own (X1: `prepare` names the pin even when `--upstream` is passed). All five were killed, and each file was put back with its sha256 checked. The other nine of the 13 are this writer's runs alone.

**The second commit's guard: 6 of 6 probes caught.** Each was applied alone to one tracked file at `5102b4294`, `test_every_reader_of_the_id_pattern_is_in_the_table` was run, and the file was put back with its sha256 checked (`restored=yes` for all six). The first commit's guard, `test_every_spelling_of_a_value_pattern_is_in_the_table`, was run with each plant too: it passes every one of them, which is the gap the second guard closes.

| Probe | File | The new guard | The first guard |
|---|---|---|---|
| P1 a new `PROJECT_ID_RE.match(value)` reader appended | `update-shape.py` | failed: `('update-shape.py', 'match'): 1` is more than the table holds | passed |
| P2 a new `repo_shape.PROJECT_ID_RE.search(value)` reader appended | `scripts/shape_advisory.py` | failed | passed |
| P3 `fullmatch` back to `match` | `templates/assembly-root/scripts/validate-manifest.py` | failed | passed |
| P4 `fullmatch` back to `match` | `templates/family-root/scripts/validate-family.py` | failed | passed |
| P5 `fullmatch` back to `match` | `scripts/family.py` | failed | passed |
| P6 main's bytes of those three files together (the 20th row above) | the three | failed | not run |

**The second commit `5102b4294` (dirty=0)**, Python 3.12.3, with the same stub `gh`, bogus token and no global git config:

| Run | Git | Result |
|---|---|---|
| `tests/test_value_pattern_line_break.py` | 2.43.0 | **30 passed** in 23.00s |
| `tests/test_value_pattern_line_break.py` | 2.47.1 | **30 passed** in 23.30s |
| `tests/test_pinned_copy_text.py`, `tests/test_path_guard_hygiene.py`, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py` | 2.43.0 | **306 passed** in 61.54s |

That commit changes only the test file (62 lines added, no assert that joins conditions with `and` or `or`, ASCII), so the first commit's runs below are not repeated: they hold the production files and every other test file, and those are unchanged. CI on `5102b4294`, and an independent look at the second commit, were not done when this was written.

**The independent review of the first commit** (rv1166c, READY, read-only) reports these gates on `2593952c9`, all passing: the stand-in gate's 6 files (298 passed), the new file on 2.47.1 (29 passed), the five hygiene files (306 passed), 15 more test files that name a changed function, pattern or code (1611 passed, 19 skipped) and `tests/test_shape_doctor.py` (445 passed, 3 skipped). It also read CI on that head at about 20:04Z: `tests` (the full suite on 3.14), `tests-git-bash`, `package` and SonarCloud all passed.

**The first commit's gates ran on `2593952c` (dirty=0)**, Python 3.12.3, with the real `gh` unreachable (a stub first on `PATH`, a bogus token) and no global git config:

| Run | Git | Result |
|---|---|---|
| the files that held every failure and setup error of the shards below: `tests/test_family.py`, `tests/test_update_shape_pr_base.py`, `tests/test_update_shape_unreadable.py`, `tests/test_update_shape_header_comments.py`, `tests/test_update_shape_messages.py` and the new `tests/test_value_pattern_line_break.py` | 2.43.0 | **298 passed** in 343.60s |
| `tests/test_value_pattern_line_break.py` | 2.47.1 | **29 passed** in 21.31s |
| `tests/test_pinned_copy_text.py`, `tests/test_path_guard_hygiene.py`, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py` | 2.43.0 | **306 passed** in 69.12s |

**The two shards, on the uncommitted tree** holding the same production bytes as the first commit (the 7 files' sha256 checked before and after), git 2.43.0: the validators' test files, the `REPOSITORY_RE`/`source_repository` files and the `update-shape.py`, `shape_advisory.py` and `family.py` files, 125 in all.

- shard 1 (62 files): 4 failed, 5290 passed, 13 skipped in 56 min;
- shard 2 (63 files): 3 failed, 4 setup errors, 3761 passed, 29 skipped in 50 min.

Every failure and setup error is in the five existing files of the first row of the table above, which all pass on the first commit:
- 2 were real: `test_a_read_refused_in_prepare_closes_the_upstream[check|apply]`, the `opening(spec)` stand-in under the two-argument call. The stand-in is fixed in the first commit.
- The other 5 failures and the 4 setup errors were `update-conflict` on `scripts/validate-family.py` ("not a verbatim copy ... at the pinned commit"). This is an artifact of an uncommitted tree: `tests/test_family.py`'s `upstream_clone` commits the working tree into its clone, while `family.py init` pins the holder at the real `HEAD`, so an uncommitted `templates/family-root/` file is never verbatim at its pin. On the first commit they pass.

Every other file of the two shards passed on these production bytes, so the shards were not re-run.

`git merge-tree --write-tree <other> 5102b42` (git 2.43.0) at 2026-10-23T12:08Z, after re-fetching main (still `961640c6`; read again at 12:10Z, just before the push) and re-listing the open PRs at 12:02Z (55 open). Main has moved from the `038dafba` that this body measured to `961640c6` by one file, `docs/release-notes/v1.2.0.md`. Each open PR that touches one of this PR's 8 paths, at its current head, plus #1126:

| Against | Head | Shares | Result |
|---|---|---|---|
| origin/main | `961640c6` | | clean, tree `4613729516ff` |
| #1098 | `dad7961a` | `scripts/family.py` | clean, tree `4d1ff9517588` |
| #1117 | `10dc011c` | `scripts/family.py` | `scripts/family.py` merges clean; conflict in `docs/handbook.html` only, which this PR does not touch (see below) |
| #1124 | `8dfa32bd` | `validate-family.py` | clean, tree `cb2ec1073646` |
| #1126 | `4c1f80cf` | none (its `scripts/repo_shape.py` and tests) | clean, tree `e82c8945ac37` |
| #1133 | `a7bfee1b` | `update-shape.py` | clean, tree `a710870d1ffc` |
| #1144 | `89486f69` | `scripts/shape_advisory.py` | clean, tree `2d3e3e1ef43d` |
| #1147 | `b75babee` | `validate-manifest.py` | clean, tree `a82ac3b47a3f` |
| #1159 | `66e68f94` | `scripts/shape_advisory.py` | clean, tree `4abeb7b38665` |
| #1164 | `74096eb4` | `scripts/family.py` | clean, tree `ee285e1cb8cf` |
| #1172 | `5aa0f116` | `validate-family.py` | clean, tree `aa32d41e6e2c` |
| #1187 | `cc8a87ee` | `update-shape.py`, `tests/test_update_shape_unreadable.py` | clean, tree `25711eb1cac4` |
| #1201 | `eac177af` | `scripts/family.py` | `scripts/family.py` merges clean; conflict in `docs/handbook.html` only (see below) |

- #1117 and #1201 conflict in `docs/handbook.html` alone. #1201 is stacked on #1117 (their merge-base is #1117's head `10dc011c`). Each of them against origin/main alone gives the same conflict in the same file, so it is theirs: #1117's, from its older base `6da757cb`, which #1201 inherits.
- #1187 and #1201 are new since the first merge-trees. #1187 shares `tests/test_update_shape_unreadable.py` with this PR; its diff adds or removes no line that names `open_upstream`, `def opening` or `REPOSITORY_RE`, so it brings no one-argument stand-in for the two-argument call. That was read for all 55 open heads at 12:12Z; only this PR's own lines name them.
- No other open PR adds or removes a call of a method of `PROJECT_ID_RE`, which the second guard counts: the lines each of the 55 open heads adds or removes outside `tests/`, `docs/` and `*.md` were read at 12:04Z. Only this PR's own name it, and #1201's one new `MEMBER_RE.fullmatch` is another pattern. So a sibling landing first does not move the guard's table.

Each is a candidate for its own issue. They come from the independent review (rv1166c, READY at `2593952c9`, issuecomment-6352173084), which measured each of them on main and at the head unless the item says it did not. The review's finding numbers are in brackets.

- **The naming-policy patterns [1].** `scripts/repo_shape.py` reads the families, the roles and the topic pattern of `contracts/repository-naming.yaml` with `.match` (`family["_re"].match(name)`, `family["_split_re"].match(name)`, `role["_re"].match(name)`, and `topic_pattern`, which defaults to `^xf-project-[a-z0-9-]+$`). `validate-repository-naming.py --project project.yaml` runs in the root template's `Makefile` and `validate.yml`. At the head, and on main, it exits 0 on a leg `repository: "testorg/Atlas\n"` and prints the line feed inside its table; `-spec` and `-code` the same. The project's gate as a whole still fails, because `validate-manifest.py` now refuses that manifest.
- **`update-upstream-not-a-repo` with a pin [3].** With no `--upstream`, a pin whose `source_repository:` names a folder that exists and is no repository (the review measured `contracts` and `scripts/repo_shape.py`) is `REFUSED update-upstream-not-a-repo: --upstream contracts exists but is not a git repository`, exit 2, on main and here. `pin_path` is in scope at `update-shape.py:686-687`, so that refusal can name the pin as `update-upstream-unresolvable` now does.
- **A pin's line feed when `--upstream` is passed [9].** `update-shape.py check --upstream <clone>` over a pin whose `source_repository:` ends in a line feed exits 0, `nothing to do`. The value is kept as `upstream.repository`, which reaches the README shape line's check, `apply`'s commit subject and the pull request words (`update-shape.py:2525`, `3017` and `5121`). The review measured `check`, not `apply`. It belongs with the previous item.
- **A member row's `id:` [4].** On main, `add` of a member whose `project.yaml` reads `id: "irqs\n"` commits the row `id: "irqs\n"`, and the holder's `validate-family.py` then says `family ok`. Under mutant M8 (this branch with main's `.match` in `add`) the same row is written, and the gate still says `family ok`. By code reading, not measured end to end: `validate-family.py` compares a row's id with the member manifest's by `str` equality (`member-id-mismatch`, `validate-family.py:1608`), and `_check_member_ids_unique` does the same, so neither asks `PROJECT_ID_RE` of a row. This PR makes `add` refuse such a member, so no new holder gets such a row.
- **Values that no pattern reads [8].** The review's sweep found these `manifest ok` or `family ok`, exit 0, with a trailing line feed, at the head and on main alike:
  - in `project.yaml`: `name`, a spec or code leg's `path`, `shape.repository` and `shape.digests.digest_algorithm`;
  - in `family.yaml`: `org`, the family's own `repository`, `tracking_branch`, and the shape's and each member pin's `digest_algorithm`.

  No `^...$` pattern reads them, so they are not this PR's. A leg's `path` is checked only by `_leg_path_findings`, which asks no pattern, and the family's `tracking_branch` is refused in `project.yaml` (`manifest-tracking-branch`, U+000A) but not in `family.yaml`. `shape.commit` and `tree_sha256` with a line feed are also `manifest ok`; that is #1126's.

The first commit's message says "the three readers of an id from a file ask it with `fullmatch` (`manifest-id`, `family-id`, `member-bad-id`)". That is true of those three, but it reads as if there were no others [2]. What the review measured instead:
- `adopt-project.py` still reads the id derived from `legs.assembly` (`:6780`) and the plan's `id:` (`:6791`), both `plan-bad-id`, with `.match`;
- neither lets a line feed through. A plan `id: "northwind\n"` is `plan-manifest-invalid`, now in `manifest-id`'s words. A plan with no `id:` and `legs.assembly: "NorthWind\n"` is `REFUSED unsafe-value: legs.assembly is 'NorthWind\n'`, exit 2, before the derived id is asked, on main and here alike.

So it is the wording alone that overstates. The second commit's `ID_READERS` lists both readers with that reason.

The title's pin clause is the narrower one (the pin's name only where the value is no path and no `owner/repo` name), and two inputs change from `unsafe-value` to `update-upstream-unresolvable` (see "Behaviour changed", above).

Review: rv1166 and rv1166c, lane 5's independent opus reviewers, at `2593952c` (issuecomment-6352173084); then rc1166b, lane 5's independent sonnet re-checker, which wrote none of #1166: READY at `5102b429`, H0 of this landing (issuecomment-6371408915), with no MUST or SHOULD; its NITs 1 and 2 are corrected in the body. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 5102b42 passed tests, tests-git-bash and SonarCloud; the head 5c8afef is a clean update-branch merge of main 93d2931, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 3c89065 over main 11560eb: the family_init_values, update_shape_unreadable, value_pattern_line_break, repo_hygiene, agents_md_codes, cli_reference sets, 365 passed, 19 skipped. Landed on Brett Heap's word of 2026-10-23T13:10:26Z, relayed to every lane by lane 3 (#473 issuecomment-6365697123): "merge v1.2.1 PRs once reviewed and green".

Closes #1139

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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