Repository navigation
validate-pins.py: a leg git directory's HEAD this user cannot open, under .git/modules/<leg> or in a .git directory, is refused pin-submodule-unreadable by name, where it ended pin-commit-unresolvable (#1222) - #1239
Conversation
… 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>
Reviewer's GuideThe gate now explicitly opens an existing regular Sequence diagram for validating a leg git directory HEADsequenceDiagram
participant Gate as validate-pins.py
participant GitDir as Leg git directory
participant HEAD as HEAD
participant Git as Git
Gate->>GitDir: _leg_git_dir(dotgit)
Gate->>HEAD: _is_there(head)
alt HEAD exists
Gate->>HEAD: _gitfile_opens(head)
alt HEAD is unreadable
HEAD-->>Gate: OSError
Gate-->>Gate: pin-submodule-unreadable
else HEAD is readable
HEAD-->>Gate: opened
end
else HEAD absent or unresolved
Gate->>Git: leave HEAD handling to Git
end
Flow diagram for unreadable submodule HEAD handlingflowchart TD
A[Locate leg git directory] --> B[Build git directory HEAD path]
B --> C{"_is_there(head)"}
C -- No --> D[Leave missing HEAD to Git]
C -- Yes --> E{"_gitfile_opens(head)"}
E -- Regular and readable --> F[Continue pin validation]
E -- Refused open --> G[Refuse with pin-submodule-unreadable]
G --> H[Name .git/modules/<leg>/HEAD or <leg>/.git/HEAD]
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
|
(lane openRepoShape-3, 2026-10-24T14:41:31Z) RV1239, independent review of #1239 at fff4214 (Claude Opus 5.5): READY for the code. Two body edits are needed before landing; they are not commits. Measured with my own probe. Each tree's gate ran in a project scaffolded by that tree, 45 s timeout. Main 50869f3 against this head:
Tests at the head: the 27 leg rows passed; test_path_guard_hygiene, test_repo_hygiene and test_no_auto_maintenance gave 280 passed, 2 skipped (test_no_auto_maintenance.py:222, GIT_CONFIG_GLOBAL set). The full six gate files were not re-run (window). Doctor parity: on main+head, tests/test_doctor_unreadable_paths.py gave 68 passed. Yet with HEAD at 000 the gate refuses .git/modules/spec/HEAD while the doctor's mount_read answers (True, True, None). The parity rows do not catch that split. The doctor half must:
Body: Lane: openreposhape-3 (openRepoShape-3) |
…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>



Goal
#1222, the gate half: a leg whose git directory's
HEADfile this user cannot open is refused by name,pin-submodule-unreadable, byvalidate-pins.py, so the gate no longer says the pinned commit is missing. The doctor half follows in #1248, now that #1200 has landed (50869f38); see "What the doctor half will need".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 is RV1200's N2, from the review of #1200. One script,
templates/assembly-root/scripts/validate-pins.py, and its test file: 2 files, +111 and -4 lines againste04826db._refuse_an_unreadable_git_dirasked theHEADof a leg's git directory only bystat(validate-pins.py and repo_shape.py after #919: gh's stderr that is not UTF-8 ends in a traceback, a leg's git directory this user cannot read is called an unresolvable pin with no reason, and two gh details still name the repository raw #968). Astatof a file at mode 000 works while its directory can be searched, so aHEADat 000 passed, andgit rev-parse, which could not read it, ended the run inpin-commit-unresolvableunder THE LOCKSTEP RULE.stat, aHEADthat is there is now opened by_gitfile_opens, the gate's validate-pins.py and repo_shape.py:--rootand gh's stderr are printed raw, a non-file in a pin's place is called absent, and a leg's.gitgitfile at mode 000 passes #919 open of a gitfile, so it is opened the same way:_ABSENT_ERRNOS) is a git directory with noHEAD, which is git's to answer for;pin-submodule-unreadable,<HEAD>: cannot be read (<reason>), naming the file (.git/modules/spec/HEADorspec/.git/HEAD), with the leg checkout's remedy and the advice for the cause, as the gate refuses an unreadable git directory today.statofHEADstill names the git directory (.git/modules/spec,spec/.git);HEADis still left to git;gitdir:line that holds a NUL is still left to git: itsHEADis nothing there, as_is_thereanswers it._gitfile_opensis reused rather than renamed or copied. Its docstring now says it also asks a git directory'sHEAD. A rename would move lines that other lanes' PRs may call, and a copy would repeat its body.if _is_there(head):. The NUL row pins that guard: without it, aValueErrorends the run (mutant m5)._gitfile_opens,_refuse_an_unreadable_git_dirand_check_pin_digestsay all of this, and so does the test module's docstring.upstream-changedrow forscripts/validate-pins.pyon their nextupdate-shape.py check. A grep oftests/found no byte pin or digest pin of this file's bytes. No file holds a 64-hex digest beside its name.test_pin_source_env_collisions.pycompares the copy with a blob it reads when it runs. The gate's six test files and the docs guards were run (below).Measured before the change
Python 3.12.3, git 2.43.0, uid 1000. Every run was offline: a stub
gh(exit 97) first on PATH,GH_TOKEN=bogus, an emptyGH_CONFIG_DIR,GIT_SSH_COMMAND=falseandGIT_CONFIG_GLOBAL=/dev/null. The projects are the test harness's real scaffolds (project), kept by one run with-o tmp_path_retention_policy=all. The gate was run aspython3 scripts/validate-pins.pyin the project, with the bytes at main (e04826db) and then this branch's.HEADat 000e04826db.git/modules/spec/HEADREFUSED pin-commit-unresolvable: spec: the pinned commit 1f482c9f... is not in that submodule's object store (... exited 128: fatal: not a git repository: .../Atlas/spec/../.git/modules/spec), then the lockstep remedy. Exit 2.REFUSED pin-submodule-unreadable: .git/modules/spec/HEAD: cannot be read (Permission denied), then the leg checkout's remedy and the permission advice. Exit 2..git-directory legspec/.git/HEADREFUSED pin-commit-unresolvable: spec: the pinned commit 1f482c9f... is not in that submodule's object store (... exited 1: ). Exit 2. Git, which cannot read thatHEAD, passes overspec/.gitand walks up to the root's repository:git -C spec rev-parse --show-toplevelprinted the root.REFUSED pin-submodule-unreadable: spec/.git/HEAD: cannot be read (Permission denied), with the same remedy. Exit 2.With
HEADreadable, this branch's gate exits 0 on both projects. Main's bytes exit 1 there, with one finding,shape-copy-driftforscripts/validate-pins.py. That finding comes from the probe, not from main: the projects were scaffolded from this branch's template, and the probe swapped main's bytes in.The new rows were also run at main's gate before the change: 6 failed and 4 passed. The 2 mode rows got
pin-commit-unresolvable, and the 4 in-process rows endedDID NOT RAISE Refusal. The 4 rows that passed are the controls.Tests
tests/test_validate_pins_spelled_and_named.py, +91 lines: 5 functions and 10 rows (116 collected at this head, 106 at main), beside #968's rows.test_a_leg_git_directory_head_this_user_cannot_open_is_refused([gitfile-leg],[git-directory-leg]): the issue's two shapes, at mode 000, run as a project runs the gate. The control comes first: readable, the same leg passes, exit 0. Then exactly onepin-submodule-unreadablenaming theHEAD, with the remedy and the permission advice, exit 2, and nothing written. Skipped as root and on Windows (CANNOT_RUN_AS_ROOT).test_a_leg_git_directory_head_whose_open_is_refused_is_refused_everywhere: the in-process stand-in for those hosts. Both shapes, with the open ofHEADrefused for permission and for an I/O error, each with its own advice.test_a_head_gone_when_it_is_opened_is_left_to_git: an open that finds nothing is no refusal, and the run finds nothing.test_a_head_that_is_no_regular_file_is_not_opened([directory],[fifo]): an open of either fails the row rather than hanging it.test_a_head_no_system_call_can_name_is_left_to_git: agitdir:line that holds a NUL (built withchr(0)).test_a_leg_git_directory_with_no_head_is_left_to_gitpasses unchanged.Every pytest ran under the stub
ghabove, withEMAIL=tests@openreposhape.invalid,/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-<name> -o tmp_path_retention_policy=failed, one at a time:tests/test_validate_pins_spelled_and_named.pyalone:115 passed in 31.89s. That was before the NUL row; the run below includes it.tests/test_validate_pins_spelled_and_named.py tests/test_validate_pins_named_and_spelled.py tests/test_validate_pins_lockstep_lines.py:482 passed in 137.27s (0:02:17).tests/test_validate_pins_unreadable_workflow.py tests/test_validate_pins_unreadable_workflows_dir.py tests/test_validate_pins_without_git.py:117 passed in 30.87s.tests/test_repo_hygiene.py tests/test_path_guard_hygiene.py tests/test_no_auto_maintenance.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_update_add_codes_documented.py:356 passed, 2 skipped in 91.80s (0:01:31)(both skips aretest_no_auto_maintenance.py:222: "the session began with GIT_CONFIG_GLOBAL set, and its file includes that one").spec: .git/modules/spec: cannot be read (<reason>), so thelegsandmembersrows never count it as drift,placementnever calls it "not a git repository", and no HEAD is read for it (#1174) #1200's headcdf879a8(merge commit5de67a00, tree13a849cd, a local worktree):tests/test_doctor_unreadable_paths.py, the whole file,68 passed in 48.91s. That includes shape-doctor.py: a leg or member whose git directory this user cannot read is named in the gate's words,spec: .git/modules/spec: cannot be read (<reason>), so thelegsandmembersrows never count it as drift,placementnever calls it "not a git repository", and no HEAD is read for it (#1174) #1200's parity rows against this gate's helpers:test_the_doctor_finds_a_git_directory_as_the_gate_finds_it(6 rows),test_a_git_directory_is_named_as_the_gate_names_it_after_its_mount(4 rows), and its git-directory mode and in-process rows.shape-doctor.pyis not changed, sotests/test_shape_doctor.pywas not run.Mutants
Each mutant was applied to the template, then the gate's leg rows were run (
-k "head or no_head or git_directory or gitfile or no_system_call"). The file was then restored, andcmpwith the saved good copy was clean each time._gitfile_opens(head)deleted)HEADHEADopened whatever it is, withhead.open("rb")in place of_gitfile_opens_is_thereguard, as a direct openif _is_there(head):guard made a bare statement, with the open keptValueError: embedded null byteMerges
git merge-tree --write-treeof this head (fff42145) with:origin/mainbd3e9c14: clean (47a964e8). Main moved since the branch was cut: its 5 new commits touchadopt-project.py,shape-doctor.py,docs/cli.mdand test files, none of this PR's two files. They include shape-doctor.py: a leg or member whose git directory this user cannot read is named in the gate's words,spec: .git/modules/spec: cannot be read (<reason>), so thelegsandmembersrows never count it as drift,placementnever calls it "not a git repository", and no HEAD is read for it (#1174) #1200's squash,50869f38.spec: .git/modules/spec: cannot be read (<reason>), so thelegsandmembersrows never count it as drift,placementnever calls it "not a git repository", and no HEAD is read for it (#1174) #1200 has landed (50869f38), so it is part oforigin/mainabove. Its headcdf879a8(shape-doctor.py,tests/test_doctor_unreadable_paths.py) merged clean (13a849cd) before it landed, and was tested above..gitthis user cannot read is named.git: cannot be read (<reason>), so the doctor never reads it as DRIFTED, and both pinned validators refuse it, exit 2 (#1146) #1147a2fc1da8(also changes_refuse_an_unreadable_git_dir: its signature, docstring head andraise): clean (58246391). In the merged tree, shape-doctor.py, validate-pins.py and validate-manifest.py: a root whose own.gitthis user cannot read is named.git: cannot be read (<reason>), so the doctor never reads it as DRIFTED, and both pinned validators refuse it, exit 2 (#1146) #1147's_refuse_an_unreadable_rootcalls this function for the root's own.git. So once both land, a root's.git/HEADat 000 is opened too, and refusedgit-repo-unreadablenaming.git/HEAD. shape-doctor.py, validate-pins.py and validate-manifest.py: a root whose own.gitthis user cannot read is named.git: cannot be read (<reason>), so the doctor never reads it as DRIFTED, and both pinned validators refuse it, exit 2 (#1146) #1147's tests were not run on that merge.What the doctor half will need (it follows in #1248, now that #1200 has landed)
At #1200's head,
mount_readasksis_a(gitdir / "HEAD", lambda _mode: True), which is astatand the gate's old check. To mirror this gate:HEADis there, name it as the gate does, after its mount, withgit_dir_named(root, rel, gitdir / "HEAD"):spec: .git/modules/spec/HEAD, orspec/.git/HEAD._gitfile_opensdoes. An open that finds nothing stays git's. A refused open iscannot_be_read(<that name>, exc).git_dir=False(leg shape files) still asks nothing.tests/test_doctor_unreadable_paths.pythen needs:GIT_DIR_CELLS:.git/modules/spec/HEADat 000, namedspec: .git/modules/spec/HEAD, andspec/.git/HEADat 000 on the own-directory leg, namedspec/.git/HEAD. Thelegsandmembersrows read INVALID with the gate's line and not DRIFTED, andplacementdoes not say "not a git repository";HEADrefused;_refuse_an_unreadable_git_dirand the doctor'smount_readover the same HEAD-open-refused shape and asserts that the doctor's line ends with the gate's detail, as the directory's parity row does now;Out of scope
HEADthis user cannot open is named in the gate's words,spec: .git/modules/spec/HEAD: cannot be read (<reason>)orspec/.git/HEAD: ..., so thelegsandmembersrows read INVALID with the gate's line and never DRIFTED, andplacementnever calls the leg "not a git repository" (#1222) #1248 now that shape-doctor.py: a leg or member whose git directory this user cannot read is named in the gate's words,spec: .git/modules/spec: cannot be read (<reason>), so thelegsandmembersrows never count it as drift,placementnever calls it "not a git repository", and no HEAD is read for it (#1174) #1200 has landed, as above.config,objects,refs,commondir). The issue's Direction says widening the check to them is a separate ruling.validate-family.py's member check, which is validate-family.py: a member whose.gitfile or git directory this user cannot read passes_checked_outand is refusedmember-commit-unresolvable, which says the pinned commit is missing, wherevalidate-pins.pynames the unreadable part #1221.pin-submodule-unreadableis the gate's existing code for a leg checkout it cannot read.Part of #1222
Lane: openreposhape-3 (openRepoShape-3)
🤖 Generated with Claude Code