Skip to content

validate-family.py asks every members: row for its gitlink and never the reverse, so a gitlink under members/ that no row names (a member mounted by hand with git submodule add, or a row deleted by hand) and a .gitmodules section whose path no row names (what family.py remove leaves of two sections for one path, or a section for a case variant such as Members/Atlas, which add commits beside the member's own even after #317) all pass the holder's own gate, and the doctor calls the holder COMPLIANT #361

Closed

Description

@brettheap

Lane: openreposhape-4 (openRepoShape-4)

Found by three of this lane's adversarial reviews:

Re-verified on main c7c02fa and on a scratch merge of #318's head 0f2a0dd (which carries c7c02fa) with #317's head c23e2b0, with local bare remotes, an empty global git config, GIT_ALLOW_PROTOCOL=file and a gh shim that exits 97. The doctor's machine row asked the shim gh --version and gh auth status, which is #296's class; nothing else called it. Not claimed: macOS or Windows (row (c) is a string comparison and ran on Linux with core.ignorecase false and true alike), and what bootstrap.py does with an unnamed member, which was read, not run.

What is wrong. The holder's validator walks members: and asks each row for its gitlink, digest and identity (_check_members, templates/family-root/scripts/validate-family.py:573 at c7c02fa). Nothing walks the other way, from what members/ holds or what .gitmodules registers back to a row. #317 adds _check_gitmodules_paths, which finds two sections for one path, and its docstring says so plainly: "A section whose path no row names is another question, and not asked here." The doctor's family row runs this validator, and its members row reads the rows too. So:

U=<openRepoShape checkout>   # a clean checkout at c7c02fa
# Runs from a clean checkout of openRepoShape, in bash, on Linux (GNU coreutils). Nothing leaves the machine: members and
# holder live in local bare repositories under $R, git reads an empty global config, GIT_ALLOW_PROTOCOL=file makes any
# accidental https/ssh clone fail, and a `gh` that exits 97 stands first on PATH.
R=$(mktemp -d); mkdir $R/bin; printf '#!/bin/sh\nexit 97\n' > $R/bin/gh; chmod +x $R/bin/gh; touch $R/gitconfig
export PATH=$R/bin:$PATH GIT_CONFIG_GLOBAL=$R/gitconfig GIT_CONFIG_NOSYSTEM=1 GIT_ALLOW_PROTOCOL=file \
       GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t.invalid GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t.invalid
for P in Atlas Nova Zed; do
  python3 $U/scaffold-project.py --org testorg --project $P --elected-by 'Test Human' --elected-on 2026-10-12 \
      --local-remote-dir $R/remotes --work-dir $R/work >/dev/null
done
python3 $U/scripts/family.py init --org testorg --family Fam --created-by 'Test Human' --created-on 2026-10-12 \
    --local-remote-dir $R/fam --work-dir $R/h >/dev/null 2>&1
H=$R/h/Fam; cd $H
add() { python3 $U/scripts/family.py add --family-root $H --member "$1" --local-remote-dir $R/remotes; }
add testorg/Nova >/dev/null
cp -a $H $R/h/Fam3                 # a holder without Atlas yet, for (c)
add testorg/Atlas >/dev/null
cp -a $H $R/h/Fam2
# (a) a member mounted by hand, `git submodule add`, with no `members:` row:
git -c protocol.file.allow=always submodule add -q -- $R/remotes/Zed.git members/Zed && git commit -qm 'Zed, by hand'
echo "gitlinks: $(git ls-files -s -- members | cut -f2 | tr '\n' ' ')  rows: $(grep -c '^  - project:' family.yaml)"
python3 scripts/validate-family.py | tail -1; echo "validate: exit ${PIPESTATUS[0]}"
python3 $U/shape-doctor.py --root $H | tail -1
# (b) a section whose path no row names, and no gitlink (the state `remove` leaves of two sections for one path):
cd $R/h/Fam2
git config -f .gitmodules submodule.members/Old.path members/Old
git config -f .gitmodules submodule.members/Old.url /nowhere/old.git
git add .gitmodules && git commit -qm 'a section with no row'
python3 scripts/validate-family.py | tail -1; echo "validate: exit ${PIPESTATUS[0]}"
# (c) a section under another name for `Members/Atlas`, a case variant of the mount path, then `add`:
cd $R/h/Fam3
git config -f .gitmodules submodule.ghost.path Members/Atlas
git config -f .gitmodules submodule.ghost.url /nowhere/ghost.git
git add .gitmodules && git commit -qm 'a section for Members/Atlas'
python3 $U/scripts/family.py add --family-root $R/h/Fam3 --member testorg/Atlas --local-remote-dir $R/remotes \
    | grep committed; echo "add: exit ${PIPESTATUS[0]}"
git show HEAD:.gitmodules | grep -E '^\[|path'
python3 scripts/validate-family.py | tail -1; echo "validate: exit ${PIPESTATUS[0]}"

Output at c7c02fa (the merge of #318 and #317 prints the same lines, commit ids aside):

gitlinks: members/Atlas members/Nova members/Zed   rows: 2
family ok: Fam (fam), 2 member(s)
validate: exit 0
COMPLIANT   (exit 0)
family ok: Fam (fam), 2 member(s)
validate: exit 0
  committed 34fb99715a8e: .gitmodules, members/Atlas, family.yaml
add: exit 0
[submodule "members/Nova"]
	path = members/Nova
[submodule "ghost"]
	path = Members/Atlas
[submodule "members/Atlas"]
	path = members/Atlas
family ok: Fam (fam), 2 member(s)
validate: exit 0

What each leaves:

Why. Every check here starts from a row, so anything the rows do not mention is outside the question. The reverse question already has an implementation in the doctor: gitlinks_under (shape-doctor.py:1311 at c7c02fa) lists "each gitlink under the members directory that no row names". Only family_beside_project (#211) calls it, for a family.yaml committed beside a project.yaml, never for a holder.

What it should do. A holder whose members/ or .gitmodules carries something no row names fails its own gate. The decisions are the author's:

  1. Two findings, or one. A gitlink under members/ that no row's path names: read from the index at stage 0, else from HEAD, the order recorded_gitlink uses. And a .gitmodules section whose path no row names. Neither needs a member checked out, so whether they run under --no-members as well, like family.py add compares --member with the existing rows case-sensitively, so --member inkrouter/irrs on a family that already pins InkRouter/IRRS mounts the same repository again as members/irrs, both gates pass a family that pins one project twice, and bump --member irss's refusal recommends exactly that add #206's id check and family.py: add never reads .gitmodules, so a section under ANOTHER name whose path is already members/<P> (a "ghost" section) does not stop it: it exits 0 and commits two sections for one path, validate-family.py says family ok, and a recursive clone takes the LATER section, so with the ghost second it dies fetching the ghost's URL #300's section check, is the author's call. A private family's CI falls back to that mode.
  2. Case. Whether a path is compared to the rows with ASCII case folded, as family.py: add asks the holder's index for the mount path in any case, so an entry Members/Atlas/README is refused as member-path-taken before anything is cloned instead of leaving the member's 19 files staged under it, and a refused add says the holder is exactly as it was only when the whole index is as it was (#302) #318's _index_entries does on every disk and validate-family.py member-duplicate runs only with the members checked and compares project: exactly, so --no-members passes a family.yaml that names one row twice, and rows IRRS and irrs with distinct ids pass, though members/IRRS and members/irrs are one directory on macOS and Windows #331 proposes for member-duplicate, so that Members/Atlas is reported as the variant of members/Atlas it is. And whether add's member-path-registered folds case too, so that row (c) is refused before the mount rather than found after it. That reopens a ruling of family.py: add refuses member-path-registered when a .gitmodules section under another name already gives the mount path, and validate-family.py finds gitmodules-path-twice, so add no longer commits two sections for one path that a recursive clone resolves to the later one, and a holder that already has such a ghost section fails its own gate (#300) #317, so it is the coordinator's to make.
  3. The remedy each finding names. For a gitlink: family.py add cannot adopt an existing mount (it refuses member-path-taken), so the remedy is git rm --cached and then add, or the row restored from git. For a section: family.py: add refuses member-path-registered when a .gitmodules section under another name already gives the mount path, and validate-family.py finds gitmodules-path-twice, so add no longer commits two sections for one path that a recursive clone resolves to the later one, and a holder that already has such a ghost section fails its own gate (#300) #317's by-key removal.
  4. The doctor. Its family row reports whatever the holder's own validator finds, so a validator finding reaches it. Separately, should its members row list the unnamed gitlinks with gitlinks_under, as shape-doctor.py reads only project.yaml at a root that also carries a family.yaml, so an assembly root whose family.yaml declares two unmounted members is COMPLIANT, and its manifest kinds row says that kind has a registered validator, which is never run #211's row does? One reader either way.
  5. validate-family.py is a copied file, so the finding reaches existing holders through update-shape.py.

How to test. The repro's three rows as validator tests, each a finding (exit 1), plus a control that a holder whose every gitlink and section a row names still says family ok. Build the extra section in git's objects (bytes on stdin), as #317's tests do. The hand-mounted gitlink needs git submodule add from a local bare remote, as the existing family fixtures already do.

Verified at main c7c02fa and at the scratch merge of #318's head 0f2a0dd with #317's head c23e2b0 (validate-family.py sha256 7ec3eec0…, scripts/family.py 8339041e…); all three rows survive the merge.

Related: #300 (decision 2; closed by PR #317), #317 (_check_gitmodules_paths, and its exact comparison in member-path-registered), #299 (PR #315's SHOULD-2, whose first remedy produced row (a)), #211 (gitlinks_under), #318 (_index_entries, any case), #331 (the same _check_members, held by lane 4: a landing neighbour), #360 (remove leaving row (b)).

Activity

  1. brettheap commented on Oct 12, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-4, session e7cd5391-c790-49b0-a88f-a97d34b22582@Eagle, 2026-10-12T15:16:44Z, for #361

    Logged in lanes/log/openRepoShape-4.md at 1366a1a945497425c0d5b7e54d2f848a8649602e.

    The three reads (lane-collision-protocol Rule 1):

    1. existing `CLAIMED —` comments on opensoft/openRepoShape#361:
       none
    2. `gh pr list --repo opensoft/openRepoShape --state all --search "#361"`, kept where the row or its body names `#361`:
       none
    3. `git ls-remote --heads git@github.com:opensoft/openRepoShape` branches naming `361` as a whole token:
       none
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions