Repository navigation
family.py _checked_member's MEMBER_RE admits an owner or a project that begins with ., _ or - (testorg/_Zed, testorg/.Zed, testorg/-Zed, _org/Zed, .org/Zed), which the holder's own validate-family.py refuses with its QUALIFIED_RE, so add exits 0 with a commit whose new row its gate then reports as member-repository #366
Description
Lane: openreposhape-4 (openRepoShape-4)
Named as a follow-up by #299's writer (PR #315, head 58aa4e3): "MEMBER_RE (family.py:104, add only) is untouched; tightening it to the validator's QUALIFIED_RE shape is a follow-up". It comes out of this lane's adversarial reviews of #299: at 58aa4e3, out-of-scope item OOS-1, "MEMBER_RE admits . and .. as the project"; and at 2f04085, check (b), where . and .. segments became member-malformed with the class left as it was "so .github stays legal". 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 that exits 97 (never called). Not claimed: which of these names GitHub itself accepts for an owner or a repository. The point here is that two of this standard's own tools disagree.
What is wrong. The two patterns share one character class. The validator's also requires each segment to begin with a letter or a digit:
| pattern | where (at c7c02fa) | |
|---|---|---|
MEMBER_RE |
^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ |
scripts/family.py:104, _checked_member, the only check add makes of --member's shape |
QUALIFIED_RE |
^[A-Za-z0-9][A-Za-z0-9_.-]*/[A-Za-z0-9][A-Za-z0-9_.-]*$ |
templates/family-root/scripts/validate-family.py:68, a row's repository: (and the same pattern at templates/assembly-root/scripts/validate-manifest.py:72) |
So exactly these names pass MEMBER_RE and fail QUALIFIED_RE: a --member whose owner or project begins with ., _ or -. add already refuses three of them another way. A segment that is exactly . or .. is member-malformed (#299, PR #315). A value beginning with - is read by argparse as an option, and checked_value refuses a leading - as unsafe-value. Every other one is mounted, pinned and committed, and add writes the argument verbatim as the row's repository: (_member_row):
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/Atlas >/dev/null
for N in _Zed .Zed -Zed; do git clone -q --bare $R/remotes/Zed.git "$R/remotes/$N.git"; done # the same project under each name
i=0
for V in testorg/_Zed testorg/.Zed testorg/-Zed _org/Zed .org/Zed; do
i=$((i+1)); cp -a $H $R/h/copy$i; cd $R/h/copy$i
python3 $U/scripts/family.py add --family-root $R/h/copy$i --member "$V" --local-remote-dir $R/remotes >/dev/null
echo "add --member $V: exit $?"
python3 scripts/validate-family.py 2>&1 >/dev/null | grep '^FINDING'; echo " validate: exit ${PIPESTATUS[0]}"
doneOutput at c7c02fa (the merge of #318 and #317 prints the same):
add --member testorg/_Zed: exit 0
FINDING member-repository: _Zed: repository is 'testorg/_Zed', expected `<org>/<Name>`
validate: exit 1
add --member testorg/.Zed: exit 0
FINDING member-repository: .Zed: repository is 'testorg/.Zed', expected `<org>/<Name>`
validate: exit 1
add --member testorg/-Zed: exit 0
FINDING member-repository: -Zed: repository is 'testorg/-Zed', expected `<org>/<Name>`
validate: exit 1
add --member _org/Zed: exit 0
FINDING member-repository: Zed: repository is '_org/Zed', expected `<org>/<Name>`
validate: exit 1
add --member .org/Zed: exit 0
FINDING member-repository: Zed: repository is '.org/Zed', expected `<org>/<Name>`
validate: exit 1
The same holds for testorg/.github and testorg/... (each passes MEMBER_RE and fails QUALIFIED_RE; checked against the two patterns, not run through add). Names with a dot, underscore or hyphen anywhere after the first character (testorg/Ze.d, testorg/Zed_) pass both, and are not this issue. Nor is a trailing dot, which Windows drops from a file name: #299's re-check named that and left it to the forge.
Why. One rule has two spellings, and add writes whatever the looser one admits into a file the stricter one checks, so the commit add reports fails the gate the holder ships. _checked_member's comment keeps the class "for .github". But a row naming testorg/.github is refused by the validator too, so nothing a holder can keep depends on add admitting it.
What it should do. add refuses what the holder's validator refuses, before the member is read. The decisions are the author's:
MEMBER_REtakesQUALIFIED_RE's shape (the first character of each segment a letter or a digit), still refused asmember-malformedbefore the member is read. Then_checked_member's./..clause is redundant: keep it for its more specific sentence, or fold it in.- One definition, or a pinned equality.
QUALIFIED_REis already spelled twice, and both copies ship into roots.family.pyruns from the standard's checkout and imports fromrepo_shape.py. The validators import from the holder's own copy ofrepo_shape.py, so a constant moved there reaches existing holders only throughupdate-shape.py. Either way (a third spelling with a test that pins all three equal, or the move) is the author's call. - If the validator's rule is what should change instead (to admit
.github, say), both change together. Which rule is right is not this issue's question.
How to test. Parametrize add --member over the five spellings, using bare copies of a scaffolded member in a local remote directory as the repro does. Assert member-malformed, exit 2, an empty stdout (nothing was read) and a byte-identical holder. A control: testorg/Ze.d, or another inner ./_/-, still adds and validates.
Verified at main c7c02fa and at the scratch merge of #318's head 0f2a0dd with #317's head c23e2b0 (scripts/family.py sha256 8339041e…, validate-family.py 7ec3eec0…); neither PR touches either pattern, and the finding survives the merge.
Related: #299 / PR #315 (_checked_member's ./.. clause and its .github comment), #206 (the any-case member lookup), #330 (another fact add does not check and the validator then refuses: the member's id), #307 (family.py init writing values its own validator refuses, the same class for init).
Activity
brettheap commentedon Oct 12, 2026
CLAIMED — lane openRepoShape-4, session e7cd5391-c790-49b0-a88f-a97d34b22582@Eagle, 2026-10-12T15:23:04Z, for #366
Logged in
lanes/log/openRepoShape-4.mdat7b31f09f90f650706f12f6bbf026edd4bdb7e515.The three reads (lane-collision-protocol Rule 1):
Merge branch 'fix/family-member-re' (#399) into fix/family-add-dry-ru…
tests/test_family_dry_run.py: `add --dry-run` is held to `member-dupl…
Merge origin/main (53bee5b: #366 via #399, #390, #357, #289 via #352)…
Merge origin/main dd8d0ea into fix/adopt-root-kept-symlinks: #261's `…