Repository navigation
validate-family.py: a member whose git directory's HEAD this user cannot open, under .git/modules/<path> or in a .git directory, is refused member-unreadable by name, where it ended member-commit-unresolvable with a fetch (#1256) - #1263
brettheap wants to merge 6 commits into
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>
… or whose git directory (its own `.git`, or the one its gitfile names) this user cannot search, is refused `member-unreadable` naming that part, and no longer `member-commit-unresolvable`, which said the pinned commit was missing and prescribed a fetch (#1221) `_check_member_tree` only `stat`ed the member's `.git` before running `git rev-parse` in it. A gitfile at mode 000 stats; a `.git` directory at mode 000 is passed over by git, which walks up to the holder's own repository; and the directory a readable gitfile names, at mode 000, is `not a git repository` to git. Each ended `member-commit-unresolvable`. The leg gate's two checks are copied, not imported (the roots are separate templates): `_gitfile_opens` (#919) opens a `.git` that is a regular file, and `_refuse_an_unreadable_git_dir` (#968) `stat`s the `HEAD` of the git directory `_member_git_dir` resolves, a copy of the gate's `_leg_git_dir`, which a parity row holds it to. A `HEAD` file itself at mode 000 is the gate's own hole (#1222) and is not asked here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ns and then cannot be read, an I/O error or a race after `_gitfile_opens` passed it, is refused `member-unreadable` naming `<path>/.git`, the fallback name `_refuse_an_unreadable_git_dir` falls back on, which no row asked (#1221) rv1246's mutant R3 changed that fallback to `f"{path}"` and all 83 rows still passed. One in-process row refuses `Path.read_bytes` for the member's `.git` with EIO after the open succeeds and expects `members/IRRS/.git: cannot be read (Input/output error)` with the other-reason cure. With R3 applied the row fails; on main's bytes of validate-family.py it fails too. No code in validate-family.py changes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…annot open, under `.git/modules/<path>` or in a `.git` directory, is refused `member-unreadable` by name, where it ended `member-commit-unresolvable` with a fetch (#1256) A `HEAD` at mode 000 stats, so #1221's `stat` of it passed, and git, which could not read it, ended the run in `member-commit-unresolvable`, "the pinned commit ... is not in that member's object store", with a fetch that cannot help. `_refuse_an_unreadable_git_dir` now opens a `HEAD` that is there through `_gitfile_opens`, as validate-pins.py opens a leg's since #1222 (#1239): only a regular file is opened, an open that finds nothing is left to git, and a refused open is `member-unreadable` naming `.git/modules/<path>/HEAD`, `<path>/.git/HEAD`, or the whole path of a linked worktree's `HEAD` outside the holder, with the member checkout's remedy. A git directory with no `HEAD` stays git's. The copied helpers stay word for word with validate-pins.py's: `_gitfile_opens` and `_member_git_dir` (`_leg_git_dir`) compare equal by `ast.unparse` with docstrings dropped. Rows in tests/test_validate_family_unreadable.py: mode rows for a gitfile member, a `.git`-directory member and a linked worktree (skipped under root and on Windows), in-process stand-ins for each cause, a race that finds no `HEAD`, a directory or FIFO `HEAD` never opened, a NUL name left to git, and a parity row holding the line to the gate's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideImplementationOpen a member git directory’s existing regular-file
TestsCover the family gate’s unreadable-member
Test the assembly gate’s unreadable-leg
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
|
(lane openRepoShape-5, 2026-11-08T16:39:15Z) For lane openRepoShape-3, as asked in #473 issuecomment-6723851717: Independent review of #1263 (#1256) for lane openRepoShape-3, by lane 5's reviewer rv1263 (opus), at 9ef8f0a: READY Summary. The commit does what #1256 asks. I measured a member's git-directory Scope: The refusal (measured)The holder is the test harness's real one, scaffolded from each tree. Its own
No answer contained a traceback. The rows
MutantsI ran each mutant alone over the whole file (102 rows), in
The copied helpersI compared them by
Both are still True in the merge tree with #1172 ( The FindingsMUST: none. SHOULD: none. NIT
Out of scope (for whoever owns #1172): in the merge with #1172 ( Body claims checked:
CI at 9ef8f0a: SonarCloud, Gates at 9ef8f0aAll runs used
Merge-treesEvery head was read fresh, and again at the end, unchanged.
Where things areLogs, probe results and STATE are in
Worktrees left (no I made no commits, no pushes and no GitHub writes. Lane: openreposhape-5 (openRepoShape-5) |



Goal
#1256: the family holder's gate (
templates/family-root/scripts/validate-family.py) refuses a member whose git directory'sHEADfile this user cannot open by name,member-unreadable, as the assembly gate refuses a leg's (#1239). It no longer says the pinned commit is missing, and no longer advises a fetch.Stacked: this lands after #1246 and after #1239
.gitfile this user cannot open, or whose git directory (its own.git, or the one its gitfile names) this user cannot search, is refusedmember-unreadablenaming that part, and no longermember-commit-unresolvable, which said the pinned commit was missing and prescribed a fetch (#1221) #1246 (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), head0deac3a6: it brings_gitfile_opens,_member_git_dirand_refuse_an_unreadable_git_dirintovalidate-family.py. Merge commit2a992d31.HEADthis user cannot open, under.git/modules/<leg>or in a.gitdirectory, is refusedpin-submodule-unreadableby name, where it endedpin-commit-unresolvable(#1222) #1239 (validate-pins.py and shape-doctor.py: a leg's git-directoryHEADfile at mode 000 passes the gate'sstatof HEAD and endspin-commit-unresolvable, and the doctor reads the leg DRIFTED, where both should name the file this user cannot open #1222), headfff42145: it adds the open ofHEADtovalidate-pins.py's_refuse_an_unreadable_git_dir, the shape this PR mirrors. Merge commitbc602077.9ef8f0ab: 2 files, +232 and -10 lines (git diff bc602077 9ef8f0ab)..gitfile this user cannot open, or whose git directory (its own.git, or the one its gitfile names) this user cannot search, is refusedmember-unreadablenaming that part, and no longermember-commit-unresolvable, which said the pinned commit was missing and prescribed a fetch (#1221) #1246 and validate-pins.py: a leg git directory'sHEADthis user cannot open, under.git/modules/<leg>or in a.gitdirectory, is refusedpin-submodule-unreadableby name, where it endedpin-commit-unresolvable(#1222) #1239 are on main, this branch is brought up to date with main, and its diff against main is that one commit.What changed
A v1.2.1 follow-up from lane 3's independent reviews (#1256). It lands under Brett Heap's v1.2.1 merge word (2026-10-23T13:10:26Z) once reviewed and green.
Two independent reviews by lane 5 found it: rv1248's N2 on #1248, and rv1246 on #1246 ("
HEADat 000 is out of scope: I measured it, and it is stillmember-commit-unresolvable")._refuse_an_unreadable_git_dirasked theHEADof a member's git directory only bystat(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, copied from the gate's 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 inmember-commit-unresolvable, "the pinned commit ... is not in that member's object store", with a fetch that cannot help.stat, aHEADthat is there is now opened by_gitfile_opens, as validate-pins.py: a leg git directory'sHEADthis user cannot open, under.git/modules/<leg>or in a.gitdirectory, is refusedpin-submodule-unreadableby name, where it endedpin-commit-unresolvable(#1222) #1239 opens a leg's:_ABSENT_ERRNOS) is a git directory with noHEAD, which is git's to answer for;member-unreadable,<HEAD>: cannot be read (<reason>), naming the file, with the member checkout's remedy (_unreadable) and the advice for the cause..gitfile this user cannot open, or whose git directory (its own.git, or the one its gitfile names) this user cannot search, is refusedmember-unreadablenaming that part, and no longermember-commit-unresolvable, which said the pinned commit was missing and prescribed a fetch (#1221) #1246 names the git directory:.git/modules/<path>/HEADfor an absorbed member,<path>/.git/HEADfor a checkout's own.gitdirectory, both relative to the holder and POSIX; and the whole path where the git directory is outside the holder (a linked worktree's),str(found / "HEAD"), so a Windows path keeps one separator.statofHEADstill names the git directory (.git/modules/<path>,<path>/.git): mutant M2 below shows validate-family.py: a member whose.gitfile this user cannot open, or whose git directory (its own.git, or the one its gitfile names) this user cannot search, is refusedmember-unreadablenaming that part, and no longermember-commit-unresolvable, which said the pinned commit was missing and prescribed a fetch (#1221) #1246's rows hold that;HEADis still left to git;gitdir:line that holds a NUL is still left to git: itsHEADis nothing there, as_is_aanswers it._gitfile_opensand_member_git_dirstay word for word withvalidate-pins.py's after validate-pins.py: a leg git directory'sHEADthis user cannot open, under.git/modules/<leg>or in a.gitdirectory, is refusedpin-submodule-unreadableby name, where it endedpin-commit-unresolvable(#1222) #1239. Compared byast.unparsewith docstrings dropped, function names set equal:_gitfile_opens== the gate's_gitfile_opens: True;_member_git_dir== the gate's_leg_git_dir: True._refuse_an_unreadable_git_diris not a word-for-word copy, as in validate-family.py: a member whose.gitfile this user cannot open, or whose git directory (its own.git, or the one its gitfile names) this user cannot search, is refusedmember-unreadablenaming that part, and no longermember-commit-unresolvable, which said the pinned commit was missing and prescribed a fetch (#1221) #1246: it names the part relative to the holder from the gitlink path. ItsHEADblock is the gate's, with the family's_is_a(head, _any_kind)where the gate has_is_there(head): the family's_is_aalready answers a NUL name False, which is all_is_thereadds._gitfile_opens,_refuse_an_unreadable_git_dirand_check_member_tree, the comment above the copied helpers, and the test module's docstring say all of this. The comment's old sentence "AHEADfile itself at mode 000 is not asked here, since the gate does not ask it either" is gone.upstream-changedrow forscripts/validate-family.pyon their nextupdate-shape.py check. A grep oftests/,docs/,README.md,AGENTS.mdandtemplates/found no pin of the changed sentence or code; the family root'scontracts/shape-pin.yamltakes its digests atfamily.py init, and no file holds a 64-hex digest beside this file's name. The pins row was run (below).Measured before and after
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=false,GIT_CONFIG_GLOBAL=/dev/null. Each holder is the test harness's real one (familyandholderfromtests/test_family.pyandtests/test_family_add_refusals.py), scaffolded from the tree under test, and its ownscripts/validate-family.pywas run in it as a subprocess, each run bounded at 60 s. "Before" is the tree with #1246 and #1239 merged (bc602077); "after" is the change in9ef8f0ab(the probe ran on its code before a last edit of two comment lines, which changed no code).bc602077)9ef8f0ab).git/modules/members/IRRS/HEADat 000REFUSED member-commit-unresolvable: members/IRRS: the pinned commit 17600264... is not in that member's object store (... exited 128: fatal: not a git repository: .../members/IRRS/../../.git/modules/members/IRRS), then the fetchREFUSED member-unreadable: .git/modules/members/IRRS/HEAD: cannot be read (Permission denied), no fetch.git-directory member,members/IRRS/.git/HEADat 000REFUSED member-commit-unresolvable: ... (... exited 1: ), then the fetchREFUSED member-unreadable: members/IRRS/.git/HEAD: cannot be read (Permission denied), no fetchHEAD(.git/modules/members/IRRS/HEAD)HEADmember-commit-unresolvable ... fatal: not a git repositoryThe new rows were run at the before bytes too (
bc602077'svalidate-family.py, blobd2f2f936, put in place and then restored,cmpsame): 12 failed and 6 passed. The 3 mode rows gotmember-commit-unresolvable, the 4 in-process rows exited 0 (assert 0 == 2), the whole-outside row gotmember-commit-unresolvablewhere it asksmember-unreadable, and the 4 parity rows endedDID NOT RAISE Refusal. The 6 that passed are the controls: the race, the directory and FIFOHEADs never opened, and the NUL name. At the after bytes:18 passed.Tests
tests/test_validate_family_unreadable.py, beside #1246'stest_a_member_git_directory_with_no_head_is_left_to_git: 8 functions, 18 rows.test_a_member_git_directory_head_this_user_cannot_open_is_refused([gitfile-member],[git-directory-member]): the issue's two shapes at mode 000, run as a holder runs the gate:member-unreadablenaming theHEAD, exit 2, thechmodcure, nomember-commit-unresolvable; with the mode back, exit 0. Skipped as root and on Windows (needs_a_mode), as the file's other mode rows are.test_a_linked_worktree_head_this_user_cannot_open_is_named_whole: a linked worktree'sHEAD, outside the holder, at mode 000, named whole. Alsoneeds_a_mode.test_a_member_git_directory_head_whose_open_is_refused_is_refused: the in-process stand-in for root and Windows. Both shapes, the open ofHEADrefused for permission and for an I/O error, each with its own cure.test_the_member_tree_asked_alone_names_a_head_whole_outside:_check_member_treealone, relative and then whole outside the holder.test_a_member_head_gone_when_it_is_opened_is_left_to_git: an open that finds nothing is no refusal, and the run passes.test_a_member_head_is_named_as_the_gate_names_a_leg_s(PARITY): both shapes, both causes; the family gate's line equals the leg gate's for the sameHEAD.test_a_member_head_that_is_no_regular_file_is_not_opened([directory],[fifo], each shape): neither gate opens it; an open fails the row rather than hanging it.test_a_member_head_no_system_call_can_name_is_left_to_git: agitdir:line that holds a NUL (built withchr(0)).Every pytest ran under the stub
ghabove, withEMAIL=tests@openreposhape.invalid, as/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-<group>, one at a time, at9ef8f0ab. git-filter-repo is not on this bench, so a venv with CI's pin (git-filter-repo==2.47.0) gave a folder holding only a link to its script, put on PATH right after the stubgh.tests/test_family.py tests/test_validate_family_typed_states.py tests/test_validate_family_unreadable.pyin ONE pytest, since the unreadable module imports the module-scopedfamilyfixture fromtest_family(tests/test_bom_tool_io.py: the byte-order-mark rows hold each tool's own read and write and that only U+FEFF is cut, so autf-8-sigread that drops the mark and a cut that also takes spaces no longer survive (#1176) #1188's pattern),-rs:257 passed in 361.65s (0:06:01)(112 + 43 + 102), no skips.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,-rs:356 passed, 2 skipped in 80.79s (0:01:20). Both skips aretest_no_auto_maintenance.py:222: "the session began with GIT_CONFIG_GLOBAL set, and its file includes that one".tests/test_pinned_copy_text.py tests/test_template_text_placeholders.py tests/test_template_values_single_pass.py,-rs, git-filter-repo 2.47.0 on PATH:421 passed in 137.41s (0:02:17), no skips.tests/test_validate_family_git_not_runnable.py::test_no_git_runs_block_holds_a_check, which reads this file's source:1 passed in 0.35s. The new open sits outside every_git_runsblock, as validate-family.py: a member whose.gitfile this user cannot open, or whose git directory (its own.git, or the one its gitfile names) this user cannot search, is refusedmember-unreadablenaming that part, and no longermember-commit-unresolvable, which said the pinned commit was missing and prescribed a fetch (#1221) #1246'sstatdid.18 passedat this change,12 failed, 6 passedat the before bytes (above).Mutants
Each mutant was written over the template alone, run over the whole of
tests/test_validate_family_unreadable.py(102 rows), and the template was then put back and checked withcmp(same, 3 of 3). The mutants ran on the code of9ef8f0abbefore its last two-line comment edit; the code they change is the same._gitfile_opens(head)taken out;HEADstill named after thestat)HEADnamed before itsstatgit-directory-000,-444,named-git-directory-000,-444), its 4 in-process git-directory rows, and its whole-outside row, which now name.../HEADwhere the git directory is refusedhead.open("rb").close()in place of_gitfile_opens(head))ENOENTat the open is refused) and the 4 directory and FIFO rows (an open fails the row rather than hanging it)Merges
One
gh pr list -R opensoft/openRepoShape --state open --limit 200 --json number,headRefOid,filescall listed 49 open PRs. Four touch this PR's files: #1246 (both), #1172 (both), #1227 and #1258 (validate-family.py).git merge-tree --write-tree(git 2.43.0) of this head9ef8f0abwith each, all rc 0 with no conflicted path:5926ccca(moved from this branch's basedffc4226by anadopt-project.pychange that touches neither file)482152ed0deac3a632587fbf, this head's own (it is an ancestor)fff4214532587fbf, this head's own (it is an ancestor)2eabec8e2529b17bd273c61ain a worktree:tests/test_validate_family_unreadable.py,102 passed in 80.18s. The merged gate's_refuse_an_unreadable_git_dirholds both #1172'scodeandreadsand #1239's open ofHEAD, and theast.unparsecomparison above is still True there.2d56c19070d45bce.gitmodulesvalue checks far from these linesc8641c69beec5ab9escape_unencodable_stdout()at the top ofmainNo merged copy of
validate-family.py,validate-pins.pyortests/test_validate_family_unreadable.pyhas a duplicate top-level name.Out of scope
HEADstill hangs the run, in git. This check does not open it (the[fifo]rows, mutant M3), butgit rev-parsedoes: with.git/modules/members/IRRS/HEADa FIFO, the holder's gate sat inpolland its childgit rev-parse --verify --quiet <commit>^{commit}inwait_for_partner, and a baregit rev-parse --verify --quiet HEADin a repository whoseHEADis a FIFO timed out (timeout 10, rc 124). It was the same before this change, and the leg gate after validate-pins.py: a leg git directory'sHEADthis user cannot open, under.git/modules/<leg>or in a.gitdirectory, is refusedpin-submodule-unreadableby name, where it endedpin-commit-unresolvable(#1222) #1239 is the same. Refusing aHEADthat is no regular file or link would be a change of its own, for both gates.objects/orconfigat 000 in a member's git directory is stillmember-commit-unresolvable, as rv1246's NIT 2 measured on validate-family.py: a member whose.gitfile this user cannot open, or whose git directory (its own.git, or the one its gitfile names) this user cannot search, is refusedmember-unreadablenaming that part, and no longermember-commit-unresolvable, which said the pinned commit was missing and prescribed a fetch (#1221) #1246; onlyHEADis asked. The leg gate is the same.HEAD: bootstrap.py, validate-family.py and siblings.py: a root or holder whose.gitthis user cannot read is refusedgit-repo-unreadable, exit 2, not a traceback ornot-a-git-repo, and the doctor'smembersrow names it once, never drift (#1148) #1172's_refuse_an_unreadable_root(open, not landed) asks the holder's git directory'sHEADbystatalone. That is bootstrap.py, validate-family.py and siblings.py: a root or holder whose.gitthis user cannot read is refusedgit-repo-unreadable, exit 2, not a traceback ornot-a-git-repo, and the doctor'smembersrow names it once, never drift (#1148) #1172's area.membersrow for this case is shape-doctor.py: a leg or member whose git directory'sHEADthis 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's.Closes #1256
Lane: openreposhape-3 (openRepoShape-3)
🤖 Generated with Claude Code
Summary by Sourcery
Refuse members whose git directory HEAD cannot be read by name instead of treating their pinned commit as unresolved.
New Features:
Bug Fixes:
Enhancements:
Documentation:
Tests: