Skip to content

shape-doctor.py: a leg or member whose git directory's HEAD this user cannot open is named in the gate's words, spec: .git/modules/spec/HEAD: cannot be read (<reason>) or spec/.git/HEAD: ..., so the legs and members rows read INVALID with the gate's line and never DRIFTED, and placement never calls the leg "not a git repository" (#1222) - #1248

Draft
brettheap wants to merge 8 commits into
mainfrom
l3-1222-doctor-head-unreadable

Conversation

@brettheap

@brettheap brettheap commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

What changed

A v1.2.1 follow-up from lane 3's independent reviews (#1222). It lands under Brett Heap's v1.2.1 merge word (2026-10-23T13:10:26Z) once reviewed and green.

It completes #1222 after #1239, the gate half. This branch MERGES #1239's head fff42145 with a merge commit (bfbeed4a, onto main 50869f38, where #1200 landed), so its tree holds the gate it mirrors, and then merges main three times, each time without conflict: bd3e9c14 (bd68c0cf), which brings #1109's change to shape-doctor.py; b1f45fa7 (2dd144f5), which brings #1144's change to shape-doctor.py and scripts/shape_advisory.py; and 81eaf757 (2fe16fb1), which brings #1147 (51a9c5f1): root_git_read, _refuse_an_unreadable_root and tests/test_root_git_unreadable.py. It carries #1239's commits until #1239 lands: until then its diff also shows templates/assembly-root/scripts/validate-pins.py and tests/test_validate_pins_spelled_and_named.py. Lane 3 lands #1239 first, then refreshes this branch. The doctor half is three commits, shape-doctor.py (+71 -9), tests/test_doctor_unreadable_paths.py (+372 -32) and tests/test_root_git_unreadable.py (+124 -3) against main:

  • 930373c9: regular_file_opens, the open of HEAD in mount_read, and the rows below;
  • 69b96f48: the parity row asserts the doctor's (populated, own_git) before it unpacks the refusal, so at main it fails on the answer, not as a TypeError; and the directory and FIFO HEAD rows use made_directory, made_fifo and POSIX_NODES from tests/test_validate_pins_spelled_and_named.py, in place of local copies;
  • 1b747f7a: the refresh round, rv1248's S1, N3 and N4: root_git_read opens the root's own .git/HEAD as the gate does. See "Refresh round" below.

The "Measured", "Tests" and "Mutants" sections are the first round's, at 2dd144f5. This round's are under "Refresh round".

The defect. mount_read, the one question the legs, leg shape files, placement and members rows share, asked a mount's git directory's HEAD by stat alone (#1174). A HEAD at mode 000 stats, so git, run in the mount, could not read it:

The change.

Decided here.

Measured

A probe scaffolds a real assembly root, and a family holder, from the tree under test, sets the HEAD, and runs shape-doctor.py --json and the root's own scripts/validate-pins.py, each bounded at 60 s. "Main" is 50869f38; mount_read and the helpers it calls are byte-identical at bd3e9c14 and at b1f45fa7 (refused_read through the end of mount_read: 8673 bytes, sha256 693721576ad5ec3d... at all three). "Here" is this head, 2dd144f5; the probe gave the same rows at 69b96f48, commits and paths aside. Main's gate has no #1239.

  • A gitfile leg, .git/modules/spec/HEAD at 000.
    • Main: DRIFTED (1 leg(s) not at the pin); legs: spec: populated, but git cannot read a HEAD in it, with the lockstep gate as the NEXT; placement: spec: it is not a git repository; the gate: pin-commit-unresolvable.
    • Here: INVALID (pins); legs: spec: .git/modules/spec/HEAD: cannot be read (Permission denied), so no HEAD in it is compared with the pin, with this doctor again as the NEXT; placement says the same line; the gate: REFUSED pin-submodule-unreadable: .git/modules/spec/HEAD: cannot be read (Permission denied).
  • A .git-directory leg, spec/.git/HEAD at 000.
    • Main: DRIFTED; legs: spec: checked out at bb2ce64e53b0, pinned at 5434824945fd, the ROOT's HEAD, with git -C <root> submodule update --init --recursive --checkout -- spec as the NEXT; placement: spec: it is not a repository of its own -- `git` answers from <root>; the gate: pin-commit-unresolvable.
    • Here: INVALID (pins); legs and placement: spec/.git/HEAD: cannot be read (Permission denied); the gate: REFUSED pin-submodule-unreadable: spec/.git/HEAD: cannot be read (Permission denied).
  • A member, .git/modules/members/IRRS/HEAD at 000.
    • Main: DRIFTED (a member is not at its pin); members/IRRS: checked out at ?, pinned at f9ec4e17a5db.
    • Here: INVALID (family); members/IRRS: .git/modules/members/IRRS/HEAD: cannot be read (Permission denied), so no HEAD in it is compared with the pin, with this doctor again as the NEXT.
  • The controls, the same on both trees.
    • A readable HEAD: COMPLIANT, and the gate passes.
    • No HEAD: git's answer, DRIFTED with spec: populated, but git cannot read a HEAD in it, and the gate's pin-commit-unresolvable.
    • A FIFO HEAD, on both layouts: the doctor and the gate each HUNG past 60 s. Here ps shows the gate's git rev-parse in wait_for_partner, git's own open of the FIFO, and the doctor's python in poll_schedule_timeout, not in an open of its own.

Tests

tests/test_doctor_unreadable_paths.py gains 24 rows:

  • the two HEAD cells at mode 000 in GIT_DIR_CELLS, and a member's HEAD at 000;
  • the in-process open refused, for legs with a gitfile (a permission and an I/O error), a leg's own .git directory, and a member;
  • a git directory with no HEAD, left to git;
  • and the parity row: eight HEAD shapes on both leg layouts, 16 rows.

One pytest at a time on Python 3.12.3, at this head 2dd144f5, with -rs, under a stub gh (exit 97) first on PATH, then git-filter-repo==2.47.0 (CI's pin) from a venv of its own, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false and GIT_CONFIG_GLOBAL=/dev/null:

Mutants

Each mutant is applied to shape-doctor.py, run against the #1222 rows of tests/test_doctor_unreadable_paths.py (-k "head or git_directory or left_to_git or git_dir", 52 rows), then restored and checked with cmp (clean each time). They were run at 930373c9. The two merges of main left the code they touch byte-identical: refused_read through the end of mount_read is 10349 bytes with sha256 7a91fa445d7a7264... at 930373c9, at 69b96f48 and at this head 2dd144f5. So they were not run again.

  • before, main's shape-doctor.py swapped in: 13 failed, 39 passed. The 13 are exactly the new refusal rows; the controls pass.
  • m1, the open dropped: 13 failed. KILLED.
  • m2, the line names the git directory, never HEAD: 13 failed. KILLED.
  • m3, HEAD opened whatever it is, with no regular_file_opens: 6 failed. KILLED.
  • m4, an open that finds nothing raised as a refusal: 2 failed. KILLED.
  • m5, no is_a guard before the name and the open: 11 failed. KILLED.
  • m6, the helper opens what is no regular file: 4 failed. KILLED.

Refresh round (rv1248's S1, N3, N4)

This round is where root_git_read gets the open. #1147 landed on main as 51a9c5f1, so main has root_git_read, which asked the root's own .git/HEAD by stat alone. rv1248 (#1248 issuecomment-6723136323) found 2dd144f5 READY with no MUST, and said its S1 becomes a MUST at this refresh. This round merges main 81eaf757 (merge commit 2fe16fb1, tree 918ac27d, the tree merge-tree predicted) and adds one commit, 1b747f7a:

Measured. A probe scaffolds a real assembly root from the tree under test, sets the root's .git/HEAD, and runs shape-doctor.py --root <root> --json and the root's own scripts/validate-pins.py, each bounded at 60 s, as uid 1000 with real modes. "Before" is the merge 2fe16fb1; "after" is this head.

  • The root's .git/HEAD at 000.
    • Before: the doctor says DRIFTED (1 leg(s) not at the pin), and legs says spec: no gitlink recorded, so this leg is not mounted as a submodule at all; code: no gitlink recorded, .... The gate exits 2: REFUSED git-repo-unreadable: .git/HEAD: cannot be read (Permission denied).
    • After: the doctor says INVALID (pins), and legs says .git/HEAD: cannot be read (Permission denied), so no gitlink is read and no leg is compared with its pin. The gate is unchanged.
    • placement is ok both times: each leg's repository under .git/modules/ has a HEAD of its own, which git reads.
  • The root's .git/HEAD a symbolic link to a file at 000: the same as at 000, before and after.
  • The controls, the same before and after.
    • Readable: COMPLIANT, and the gate exits 0.
    • No HEAD, and a HEAD that is a directory: git's answer. The doctor says DRIFTED with no gitlink recorded, the gate pin-gitlink-absent, and neither tool refuses it.
    • A FIFO HEAD: the doctor and the gate each HUNG past 60 s, in git's own open. ps shows git rev-parse --show-toplevel and git ls-files -s in wait_for_partner, not either tool's own open. This is the FIFO case under "Out of scope".

Rows. 49 net new: 143 rows in the two files at 2fe16fb1, 192 here.

  • tests/test_root_git_unreadable.py, 9 rows:
    • the root's .git/HEAD at 000, a mode row that runs the doctor and the gate and is COMPLIANT once the mode is put back;
    • its open refused in process, for a permission and for an I/O error, with the doctor and the gate each giving the advice for the cause;
    • three open rows in the doctor-and-gate sentence row: the git directory, and a gitfile naming one inside and outside the root;
    • a HEAD gone at the open, left to git;
    • a directory HEAD among the absent controls, for both rows that read them.
  • tests/test_doctor_unreadable_paths.py, 40 rows:
    • the parity row of root_git_read against _refuse_an_unreadable_root, over three root layouts (a git directory, a gitfile naming one inside the root, one naming one outside it) and nine HEAD shapes (readable, mode 000, the open refused, an I/O error, gone at the open, none, a directory, a FIFO, a link to a file at 000). For each, the doctor's line and the gate's refusal agree on whether the root is refused and on the file named, word for word;
    • the link shape added to the leg parity row;
    • the NUL gitdir: row, one per parity pair;
    • the regular_file_opens against _gitfile_opens row, over the nine shapes.

Before: the merge's shape-doctor.py swapped in under the new rows gives 18 failed, 174 passed in 81.66s. The 18 that fail are exactly the new refusal rows: the mode row, the two in-process stand-ins, the three open rows, and the 12 refusing cells of the root parity row. Every control, the N3 row and the N4 rows pass, as they should.

Mutants. Each mutant is applied to this head's shape-doctor.py alone, run against tests/test_root_git_unreadable.py with the three new rows of tests/test_doctor_unreadable_paths.py (98 rows, in one pytest), then restored and checked with cmp (clean each time).

  • m1, the open dropped: 18 failed, 80 passed. KILLED.
  • m2, HEAD named before its stat: 14 failed, 84 passed. KILLED.
  • m3, the regular-file guard dropped (head.open("rb") in place of regular_file_opens): 7 failed, 91 passed. KILLED.
  • m4, the refusal swallowed: 18 failed, 80 passed. KILLED.
  • m5 (N3, rv1248's m11), regular_file_opens's last True flipped to False: 1 failed, 97 passed. KILLED by the new parity row.

Gates. All runs are at this head 1b747f7a, one pytest at a time, on Python 3.12.3 and git 2.43.0, with -rs. The stub gh (exit 97) is first on PATH, then git-filter-repo==2.47.0 (CI's pin) from a venv of its own. The environment sets GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null and EMAIL.

  • tests/test_root_git_unreadable.py with tests/test_doctor_unreadable_paths.py, in one pytest, since the second imports from the first: 192 passed in 82.92s.
  • tests/test_validate_pins_spelled_and_named.py: 116 passed in 24.99s.
  • tests/test_shape_doctor.py, in the background with a log: 446 passed, 3 skipped in 677.94s. All three skips are test_shape_doctor.py:10516, "the PowerShell line is printed, and powershell.exe exists, only on a Windows host". No git filter-repo row skipped.
  • The guards (test_repo_hygiene, test_agents_md_codes, test_git_output_decoding, test_stdout_encoding, test_cli_reference, test_no_auto_maintenance): 343 passed, 3 skipped in 65.17s. The skips are test_stdout_encoding.py:285, "the default this is about is Windows' own", and twice test_no_auto_maintenance.py:222, "the session began with GIT_CONFIG_GLOBAL set", from the GIT_CONFIG_GLOBAL=/dev/null these runs set.
  • The pins row, since validate-pins.py is a pinned template copy (test_pinned_copy_text, test_template_text_placeholders, test_template_values_single_pass): 421 passed in 87.35s.

Merges

One gh pr list of the open PRs at 18:58Z: besides #1239, three others touch shape-doctor.py or templates/assembly-root/scripts/validate-pins.py, and none touches tests/test_root_git_unreadable.py or tests/test_doctor_unreadable_paths.py. git merge-tree --write-tree of this head 1b747f7a (tree 01432f28) with each head, read fresh:

The merge of main gave the tree merge-tree predicted for it: 81eaf757 gave 918ac27d (2fe16fb1).

Out of scope

Known limit (rv1248b's NIT A, kept as a limit)

Lane 5's delta check rv1248b found this refresh round READY, with no MUST or SHOULD (issuecomment-6726511606). It measured one two-fault case:

Closes #1222

Lane: openreposhape-3 (openRepoShape-3)

🤖 Generated with Claude Code

Brett Heap and others added 5 commits October 23, 2026 21:40
… under `.git/modules/<leg>` or in a `.git` directory, is refused `pin-submodule-unreadable` by name, where it ended `pin-commit-unresolvable` (#1222)

`_refuse_an_unreadable_git_dir` asked the `HEAD` of a leg's git directory
only by `stat`, which a `HEAD` at mode 000 passes, so git, which could not
read it, ended the run in `pin-commit-unresolvable` under THE LOCKSTEP
RULE. A `HEAD` that is there is now opened as `_gitfile_opens` opens a
gitfile: only where it is a regular file, and an open that finds nothing
is a git directory with no `HEAD`, left to git. One this user cannot open
is named, `.git/modules/spec/HEAD` or `spec/.git/HEAD`, with the leg
checkout's remedy.

The gate half of #1222; the doctor half waits for #1200.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alf of #1222, so this branch's tree holds it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er cannot open is named in the gate's words, `spec: .git/modules/spec/HEAD: cannot be read (<reason>)` or `spec/.git/HEAD: ...`, so the `legs` and `members` rows read INVALID with the gate's line and never DRIFTED, and `placement` never calls the leg "not a git repository" (#1222)

`mount_read` asked a mount's git directory's `HEAD` by `stat` alone
(#1174). A `HEAD` at mode 000 stats, so git in the mount could not read
it: the `legs` row read `spec: populated, but git cannot read a HEAD in
it`, or, for a `.git` directory, quoted the ROOT's HEAD as the leg's with
a checkout over it; the `members` row said `checked out at ?`; and the
verdict was DRIFTED. The gate's `_refuse_an_unreadable_git_dir` opens that
`HEAD` now (#1239), and the doctor asks it the same way:
`regular_file_opens`, the gate's `_gitfile_opens`, opens it only where it
is a regular file (a FIFO's open waits for a writer), an open that finds
nothing is a git directory with no `HEAD`, left to git, and a refused open
is named after the mount as the gate names it (`git_dir_named`).

tests/test_doctor_unreadable_paths.py: the two `HEAD` cells at mode 000 in
`GIT_DIR_CELLS`, a member's `HEAD` at 000, the in-process open refused for
legs (gitfile and `.git` directory) and a member, a git directory with no
`HEAD` left to git, and the parity row that holds `mount_read` and the
gate's `_refuse_an_unreadable_git_dir` to one answer for eight `HEAD`
shapes on both leg layouts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…carries #1109's (e9a1650) change to shape-doctor.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tor's `(populated, own_git)` before it unpacks the refusal, and the directory and FIFO `HEAD` rows use the gate test's own `made_directory`, `made_fifo` and `POSIX_NODES` (#1222)

At main the parity row's `line, error, listed = doctor[2]` failed as a
TypeError on None before its assertion of `doctor[:2]` could say what
the doctor answered; the assertion now comes first and prints the
answer. The `HEAD` that is a directory or a FIFO is made by the helpers
`tests/test_validate_pins_spelled_and_named.py` already owns for the
gate's own rows, under its one skip, in place of a local
`Path.mkdir`, `os.mkfifo` and `skipif`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap brettheap added this to the v1.2.1 milestone Nov 8, 2026
@sourcery-ai

sourcery-ai Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Implementation

Update the gate’s unreadable-git-directory check to open a present regular-file HEAD, report refused opens by the HEAD path, and leave absent or non-regular HEAD files to Git.

  • templates/assembly-root/scripts/validate-pins.py L408-411, L464-473, L479-482 (+1 small edits)

Add regular_file_opens and use it for mount and root HEAD checks so unreadable files produce the gate’s named refusal instead of misleading drift or placement results.

  • shape-doctor.py L3493-3507, L3569-3599, L3645-3655 (+3 small edits)

Tests

Exercise the gate’s refusal for an unreadable leg HEAD, including absent, non-regular, and NUL-path controls.

  • tests/test_validate_pins_spelled_and_named.py L46-53, L922-1004

Cover doctor mount and root unreadable-HEAD behavior, parity with the gate, and controls for absent, non-regular, linked, and NUL-path cases.

  • tests/test_doctor_unreadable_paths.py L687-706, L746-762, L820-1091 (+12 small edits)

Verify root HEAD refusal naming, gate parity, error advice, and absent/non-regular controls.

  • tests/test_root_git_unreadable.py L54-64, L351-385, L565-625 (+5 small edits)

Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

…carries #1144's change to shape-doctor.py and scripts/shape_advisory.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-11-08T15:08:08Z) For lane openRepoShape-3, as asked in #473 issuecomment-6722762955:

Independent review of #1248 (#1222) for lane openRepoShape-3, by lane 5's reviewer rv1248 (opus), at 2dd144f: READY

Summary. The doctor change does what RV1239's spec asked. A leg's or member's git-directory HEAD at mode 000 now gets INVALID with the gate's line, where main said DRIFTED. I found no MUST at this head. One SHOULD applies once #1147 is on main: the refresh must then add the same open to #1147's root_git_read, and I measured both the gap and the fix.

Scope: the doctor delta fff42145..2dd144f5, without main's merges: shape-doctor.py (+33/-1 in code: regular_file_opens and 4 lines in mount_read, plus docstrings) and tests/test_doctor_unreadable_paths.py. #1239's two files are byte-identical at the head and at fff42145.

The doctor's answer (measured)

The probes scaffold real roots and holders, set real modes, and run as uid 1000. Each tool is bounded at 60 s. Each tree is scaffolded from itself.

Case main 11560eb9 head 2dd144f5
gitfile leg, .git/modules/spec/HEAD 000 doctor: DRIFTED (1 leg(s) not at the pin), legs spec: populated, but git cannot read a HEAD in it, placement says "it is not a git repository"; gate: pin-commit-unresolvable doctor: INVALID (pins), legs and placement spec: .git/modules/spec/HEAD: cannot be read (Permission denied), NEXT is the doctor again; gate: REFUSED pin-submodule-unreadable: .git/modules/spec/HEAD: cannot be read (Permission denied)
leg with its own .git directory, spec/.git/HEAD 000 doctor: DRIFTED, spec: checked out at <root HEAD>, pinned at ..., NEXT submodule update --checkout; gate: pin-commit-unresolvable doctor: INVALID (pins), spec/.git/HEAD: cannot be read (Permission denied); gate: the same line, pin-submodule-unreadable
member, .git/modules/members/IRRS/HEAD 000 DRIFTED (a member is not at its pin), checked out at ? INVALID (family), the members row names the HEAD
root .git/HEAD 000 doctor: DRIFTED, no gitlink recorded for both legs; gate: pin-gitlink-absent the same as main: this is #1146, which this PR does not touch
readable controls (both leg layouts, holder) COMPLIANT, gate exit 0 COMPLIANT, gate exit 0

On #1147's root_git_read (lane 3's note):

Findings

MUST: none at this head.

SHOULD

NIT

Body claims checked

  • The 24 net new rows: 92 rows at the head, 68 at main, with 2 old rows renamed [head-stat].
  • refused_read through the end of mount_read: exactly 10349 bytes, sha256 7a91fa445d7a7264..., at the head; 8673 bytes, 693721576ad5ec3d..., at main.
  • The mutant counts m1-m6 reproduce exactly.
  • The only closing reference is Closes #1222.
  • No doc pins the changed lines; only the historic v1.2.0 release notes mention one.

New rows on main

I swapped main's shape-doctor.py (b1f45fa7) into rv1248-mut and ran the whole file: 13 failed, 79 passed, then restored the file and its sha256 (7b302c32...) checked OK.

  • The 13 that fail are every new refusal row: the 2 GIT_DIR_CELLS HEAD cells, member [head-000], the in-process head-open and head-open-io-error, own-directory [head-open], member [head-open], and the parity mode-000, open-refused and open-io-error rows on both leg layouts.
  • The new ids that pass on main are by design:
    • 2 old rows renamed [head-stat];
    • head-gone;
    • 10 parity controls (readable, no-head, gone-at-open, directory and FIFO, on both layouts).

Mutants

Each mutant was run alone on the whole file (92 rows), then restored with a sha256 check (OK every time).

Mutant Result
m1, the open dropped 13 failed, KILLED
m2, the line names the git directory, never HEAD 13 failed, KILLED
m3, head.open with no regular-file guard 6 failed, KILLED
m4, an open that finds nothing raised as a refusal 2 failed, KILLED
m5, no is_a guard before the name and the open 11 failed, KILLED
m6, the helper opens what is no regular file 4 failed, KILLED
m7, the name set after the open 13 failed, KILLED
m10, only PermissionError caught in the helper 2 failed, KILLED
m13, a refused open swallowed 13 failed, KILLED
m8, the name built as git_dir_named(gitdir) + "/HEAD" SURVIVED: it differs only when a gitfile names the root itself (gitdir: ..)
m9, the guard asks for a regular file SURVIVED: equivalent
m11, the return value flipped SURVIVED: equivalent (N3)

Gates at 2dd144f

All runs used Python 3.12.3, run10.sh, and git-filter-repo 2.47.0 from venv247.

  • git 2.43.0, test_shape_doctor + test_doctor_unsearchable_links + test_doctor_unreadable_paths in one run: 563 passed, 3 skipped, in 731.74 s.
  • git 2.47.1 (L/tools/git247/bin first on PATH), the same three files: 563 passed, 3 skipped, in 727.51 s.
  • The skips: all 3 in each run are test_shape_doctor.py:10516, the PowerShell line that is printed only on a Windows host. No git filter-repo row skipped.
  • Hygiene five, git 2.43.0: 379 passed in 61.32 s.
  • Hygiene five, git 2.47.1: 379 passed in 59.05 s.
  • CI: Sonar (0 issues), package and tests-git-bash pass. tests (job 128760632882) was still in progress at 15:06Z, running since 14:31:01Z.

Merge-trees

All heads were read fresh. git merge-tree --write-tree gave the same trees on 2.43.0 and 2.47.1. The head's own tree is dddb9e3c.

Logs, the probe results and STATE are in /workspace/projects/.lane-worktrees/openRepoShape-5/work/rv1248/: STATE.md, before.log, mut-m*.log, probe-{head,main,m1147,m1147-rootopen}.jsonl, g243doc.log, g247doc.log, g243hyg.log, g247hyg.log, rootfix-rows.log.

Worktrees left (no rm allowed):

  • /workspace/projects/openRepoShape-worktrees/rv1248-review and /workspace/projects/openRepoShape-worktrees/rv1248-mut: clean at 2dd144f5.
  • /workspace/projects/openRepoShape-worktrees/rv1248-main: clean at 11560eb9.
  • /workspace/projects/openRepoShape-worktrees/rv1248-m1147: HEAD 2dd144f5, files read-tree'd to 196424a0. It is dirty on purpose, and its doctor's sha256 was restored.

I made no commits, no pushes and no GitHub writes.

Lane: openreposhape-5 (openRepoShape-5)

Brett Heap and others added 2 commits November 8, 2026 18:48
…carries #1147's (51a9c5f) change to shape-doctor.py, validate-pins.py and tests/test_root_git_unreadable.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e its `stat` passes, as the gate does, so a root `HEAD` at mode 000 is `.git/HEAD: cannot be read (<reason>)` and `INVALID (pins)`, never `no gitlink recorded` and DRIFTED (rv1248's S1, N3, N4) (#1222)

S1 (rv1248's SHOULD, a MUST on a main that holds #1147). #1147's
`root_git_read` asked the root's `HEAD` by `stat` alone. A `HEAD` at mode
000 stats, so git could not read the root, the `legs` row said `no gitlink
recorded` of each leg and the verdict was `DRIFTED (1 leg(s) not at the
pin)`, while `validate-pins.py` (`_refuse_an_unreadable_root`, through
`_refuse_an_unreadable_git_dir` with #1239's open) refused
`git-repo-unreadable: .git/HEAD: cannot be read (Permission denied)`: the
#1146 defect again. `root_git_read` now names `HEAD` once its `stat` finds
it and opens it through `regular_file_opens`, only where it is a regular
file. A refused open is the root's line, so no gitlink is read and no leg is
compared with its pin, and the verdict is `INVALID (pins)`. A refused
`stat` still names the git directory; an absent `HEAD`, one gone at the
open, a directory and a FIFO are left to git as before. `named` starts from
the root's `.git` path, so `.git/HEAD` is spelled relative to the root, in
POSIX, on every platform, as the gate's `_named` spells it.

N3. `regular_file_opens` keeps its False/True answer, and its docstring now
says why: its two callers (`mount_read`, `root_git_read`) ask it only for
its raise, but it is the gate's `_gitfile_opens`, whose answer the gate
reads of a leg's gitfile (`initialized`, #919), so one question keeps one
answer in both files for any later caller here. A new parity row holds the
two to the same answer for each shape, so rv1248's m11 is no longer an
equivalent mutant.

N4. The leg parity row's `HEAD_SHAPES` gains a `HEAD` that links to a file
at mode 000, and a new row asks #1239's NUL `gitdir: ` line of both parity
pairs (a leg's gitfile, the root's gitfile): neither tool refuses it.

Rows: in `tests/test_root_git_unreadable.py`, the root `HEAD` at 000 (a mode
row, doctor and gate, COMPLIANT once put back), its open refused in process
for a permission and an I/O error (doctor and gate, each with its advice),
three open rows in the doctor-and-gate sentence row (the git directory, and
a gitfile naming one inside and outside the root), a `HEAD` gone at the
open, and a directory `HEAD` among the absent controls. In
`tests/test_doctor_unreadable_paths.py`, the parity row of `root_git_read`
against `_refuse_an_unreadable_root` over three root layouts and nine `HEAD`
shapes, the NUL row, and the `regular_file_opens` parity row.

Co-Authored-By: Claude Opus 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-08T20:11Z) For lane 3, as asked in #473 issuecomment-6725968150: lane 5's delta check of #1248's refresh round, 2fe16fb1..1b747f7a, by rv1248b (opus), which wrote none of it, is READY, with three NITs. NIT A is a measured two-fault case with a tested fix variant, for your call. The report follows as the reviewer wrote it.

Delta check of #1248 (#1222) for lane openRepoShape-3, by lane 5's reviewer rv1248b (opus), 2fe16fb..1b747f7: READY

Summary. The refresh round does what rv1248's S1, N3 and N4 asked. With the root's .git/HEAD at mode 000, the doctor said DRIFTED at the merge 2fe16fb1 and now says INVALID (pins), naming .git/HEAD in the gate's words. Every number lane 3 gave reproduces exactly. I found no MUST and no SHOULD, and three NITs.

All probes and gates ran as uid 1000 with real modes, through run10.sh, on Python 3.12.3 with git-filter-repo 2.47.0 (venv247). Every swapped file was restored and its sha256 checked. Both worktrees are clean at 1b747f7a.

Checks (measured)

1. The merge.

2. S1. Real processes, each bounded at 60 s, with the merge's doctor (sha256 79e04060) swapped in for "before":

Root's HEAD Before (merge 2fe16fb1) Head 1b747f7a
mode 000 doctor DRIFTED (1 leg(s) not at the pin), legs says no gitlink recorded twice doctor INVALID (pins), legs says .git/HEAD: cannot be read (Permission denied), so no gitlink is read and no leg is compared with its pin; placement ok; COMPLIANT once the mode is put back
link to a file at 000 same as at 000 same as at 000
gitfile naming .gitdir, its HEAD at 000 DRIFTED INVALID, naming .gitdir/HEAD
gitfile naming ../elsewhere, its HEAD at 000 DRIFTED INVALID, naming the absolute path
absent, or a directory DRIFTED no gitlink recorded; gate pin-gitlink-absent the same: left to git
FIFO (asked in process, 10 s alarm) both tools None, no open of HEAD the same
gone at the open (in process, the open raising ENOENT) both tools None the same
EIO at the open (in process) doctor None both tools name .git/HEAD: cannot be read (Input/output error)
  • In every case the gate is unchanged: REFUSED git-repo-unreadable: .git/HEAD: cannot be read (Permission denied), exit 2.
  • The Windows spelling, by reading. named = dotgit is rendered through relative_to(root).as_posix(), and ctx.root is resolved in main. With PureWindowsPath and the same expressions, the old bare Path(".git") / "HEAD" prints .git\HEAD and the new code prints .git/HEAD, as the gate's _named spells it. No CI job can test this: tests-git-bash runs only the launcher and installer suites.

3. N3. The new row test_the_doctor_opens_a_file_as_the_gate_opens_a_gitfile holds regular_file_opens and _gitfile_opens to the same answer over the nine shapes.

  • rv1248's m11, run verbatim (an open that finds nothing answers True), is now KILLED, by 1 row: N3[gone-at-open].

4. N4.

  • link-to-mode-000 is in HEAD_SHAPES. It runs in the leg parity row (2 rows), the root parity row (3) and the N3 row (1).
  • The NUL gitdir: row covers both parity pairs: [a-legs-gitfile] and [the-roots-gitfile].

5. The rows.

  • 143 ids at 2fe16fb1 (51 + 92) and 192 at the head (60 + 132): 49 new, none removed.
  • With the merge's doctor swapped in: 18 failed, 174 passed in 83.79 s. The 18 are exactly the new refusal rows:
    • the mode row;
    • the two in-process rows (permission and I/O error);
    • the three *-head-open rows;
    • the 12 refusing cells of the root parity row.
  • The other 31 new ids pass on the merge by design: the controls, the N3 and N4 rows, and the leg link rows.
  • At the head, the two files pass in full: 192, inside the gate runs below.

6. Mutants. Each was run alone on lane 3's 98-row selection: test_root_git_unreadable.py plus the root parity, NUL and N3 rows. Each was restored with a sha256 check, and no run had a skip. The unchanged head gave 98 passed.

The writer's five reproduce exactly:

Mutant Rows failed Result
m1, the open dropped 18 KILLED
m2, HEAD named before its stat 14 KILLED
m3, head.open with no regular-file guard 7 KILLED (6 directory and FIFO root parity cells, 1 absent-directory control)
m4, the refusal swallowed 18 KILLED
m5, the last True flipped to False 1 (N3[readable]) KILLED

Mine:

Mutant Rows failed Result
k2, the name set after the open 18 KILLED
k4, the open only for a root whose .git is a directory 10 KILLED
k5, the open only for a root whose .git is a gitfile 8 KILLED
k6, regular_file_opens answers False for what is no regular file 3 KILLED
k7, no open through a link 4, plus 2 in the leg parity row KILLED
k8, is_a no longer takes a NUL name as nothing there 2 (the NUL rows) KILLED
k1, named = Path(".git") (the Windows spelling reverted) 0 SURVIVED: equivalent on POSIX (NIT C)
k3, the guard asks for a regular file 0 SURVIVED: equivalent

7. Gates at 1b747f7a.

  • git 2.43.0 (/usr/bin/git), the two files with tests/test_shape_doctor.py in one pytest: 638 passed, 3 skipped, in 819.43 s.
  • git 2.47.1 (L/tools/git247/bin/git first on PATH), the same three files: 638 passed, 3 skipped, in 833.49 s. 638 is 192 + 446, which matches lane 3's 446 for test_shape_doctor.py.
  • The skips: all 3 in each run are test_shape_doctor.py:10516, the PowerShell line printed only on a Windows host. No git filter-repo row skipped.
  • The hygiene five (test_path_guard_hygiene, test_repo_hygiene, test_agents_md_codes, test_cli_reference, test_pinned_copy_text): 379 passed on 2.43.0 (67.44 s) and 379 passed on 2.47.1 (67.29 s), with no skips.
  • CI at the head: Sonar, package and tests-git-bash pass; tests was still running at 20:10Z.

8. Merge-trees. Heads were read fresh; each result is the same on 2.43.0 and 2.47.1, and every one exits 0. The head's own tree is 01432f28.

Merged with Tree
main 40cb8086, which moved twice during the review 39b967d7
main dff8fd4b ee56b44c
main 9252ef20 d3cd704f
#1239 fff42145 01432f28, the head's own
#1172 ba81fd7d a120638f
#1263 9ef8f0ab 016e1572
#1269 c792615f 36531a9b

Findings

MUST: none. SHOULD: none.

NIT A. A second refusal in a leg is now hidden behind the root's HEAD. The delta lets a refusal of the root's HEAD alone reach audit_leg's root-line branch. Its comment says "what mends the root's mends it", which does not hold for a HEAD. Measured with the root's .git/HEAD at 000 and also .git/modules/spec/HEAD (or .git/modules/spec) at 000:

NIT B. The N3 docstring's "one question with one answer in both files" holds for every path either tool's callers pass. Given a name holding a NUL directly, the doctor answers True and the gate raises ValueError. That cannot happen through either tool, because both callers ask is_a or _is_there first. Optional to say so.

NIT C. The named = dotgit change has no row that can fail on Linux (k1 survives), and CI has no Windows job for these rows. It is held by reading only.

Body claims checked.

  • The numbers against main are right: +71/-9, +372/-32 and +124/-3, and the delta is +304/-13.
  • The only closing reference is Closes #1222.
  • The only non-ASCII character is the attribution line's emoji.
  • rv1248's N1, the Sourcery block, is gone.
  • The 35 added asserts include no and/or composite.

Files are in /workspace/projects/.lane-worktrees/openRepoShape-5/work/rv1248b/:

  • STATE.md
  • before.log, before-failed.txt, new-ids.txt
  • probe-head.jsonl, probe-merge.jsonl, probe-variant.jsonl
  • mutchain.log, mut-*.log
  • g243doc.log, g247doc.log, g243hyg.log, g247hyg.log
  • variant-headnarrow.py

Both worktrees are detached at 1b747f7a, clean: /workspace/projects/openRepoShape-worktrees/rv1248-review and /workspace/projects/openRepoShape-worktrees/rv1248-mut. rv1248-m1147 and rv1248-main were not touched. Nothing is running. I made no commits, no pushes and no GitHub writes.

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