Skip to content

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

Closed

Description

@brettheap

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]}"
done

Output 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:

  1. MEMBER_RE takes QUALIFIED_RE's shape (the first character of each segment a letter or a digit), still refused as member-malformed before the member is read. Then _checked_member's ./.. clause is redundant: keep it for its more specific sentence, or fold it in.
  2. One definition, or a pinned equality. QUALIFIED_RE is already spelled twice, and both copies ship into roots. family.py runs from the standard's checkout and imports from repo_shape.py. The validators import from the holder's own copy of repo_shape.py, so a constant moved there reaches existing holders only through update-shape.py. Either way (a third spelling with a test that pins all three equal, or the move) is the author's call.
  3. 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

  1. brettheap commented on Oct 12, 2026

    @brettheap
    ContributorAuthor

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

    Logged in lanes/log/openRepoShape-4.md at 7b31f09f90f650706f12f6bbf026edd4bdb7e515.

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

    1. existing `CLAIMED —` comments on opensoft/openRepoShape#366:
       none
    2. `gh pr list --repo opensoft/openRepoShape --state all --search "#366"`, kept where the row or its body names `#366`:
       none
    3. `git ls-remote --heads git@github.com:opensoft/openRepoShape` branches naming `366` as a whole token:
       none
    
  2. added 2 commits that reference this issue on Oct 12, 2026
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