Skip to content

scripts/validate-repository-naming.py: _link_sources_from_args splits every --link-source value at its first = and nothing reports a value no chain link used, so a link tree under a directory named a=b, or behind a mistyped key, is silently skipped (declared-unverified, exit 0), and repo_shape.link_manifest_path lets a source this user cannot enter (keyed, SHAPE_PIN_SOURCE_<LINK> or a sibling checkout) end the naming gate in a PermissionError traceback before Python 3.14 #338

Closed

Description

@brettheap

Lane: openreposhape-4 (openRepoShape-4)

Found by this lane's adversarial review of #295 (PR #312, head eb0658f; its out-of-scope item O1, repeated as O2 at the fix round's head 4aae417), which asked that this twin be filed so that it does not live only in a commit body: #312's writer probed the = half and left it, giving reasons (its first commit's body, point 4). Re-verified on main c7c02fa by running the naming validator against link trees in a scratch directory. The traceback half is new here: it applies that review's SHOULD-1 (a named source under a parent that cannot be entered) to this twin. It was run under Python 3.12.3 and under CPython 3.10.21, 3.11.16, 3.12.14, 3.13.15, 3.14.7 and 3.15.0rc2. No open pull request touches scripts/validate-repository-naming.py or scripts/repo_shape.py; #312 changes the validate-pins.py copy only, so this survives the landing of lane 4's open PRs. Not claimed:

  • Windows, and Python 3.9 (the floor), which was not run; its pathlib swallows the same four errors as 3.10's.
  • The project's own gate (scripts/bootstrap.py and validate.yml run validate-repository-naming.py --project project.yaml, which reaches the same resolve_link_source) and the doctor's naming row: read, not run.
  • scaffold-project.py --referent-chain, which hands --pin-source to link_pins_from_trees as the default link source: read, not run. Its pin digest reads that same source first; see the scaffold-project.py --pin-source issue filed from the same batch.

What is wrong. Two faults sit in one parser and one lookup.

# $U: a clean checkout at c7c02fa. Nothing beside the working directory is named openXdox, and SHAPE_PIN_SOURCE_OPENXDOX is unset.
R=$(mktemp -d)
mkdir -p "$R/lx/openXdox" "$R/a=b/openXdox" "$R/locked/openXdox" "$R/cwd"
for d in "$R/lx/openXdox" "$R/a=b/openXdox" "$R/locked/openXdox"; do
    printf 'neutral_product_pins:\n  - openDox\n' > "$d/project.yaml"   # a link tree: openXdox declares openDox
done
chmod 000 "$R/locked"
cd "$R/cwd"
naming() { python3 "$U/scripts/validate-repository-naming.py" --pins openXdox --referent-chain openXdox,openDox codexDox "$@"; echo "exit $?"; }
naming --link-source "$R/lx/openXdox"                 # bare, no `=`
#   codexDox                         domain-descendant   also_matches project-leg/assembly
#   exit 0
naming --link-source "$R/a=b/openXdox"                # bare, under a directory named `a=b`
#   WARNING codexDox: declared-unverified: openXdox declaring a pin on openDox is a fact in openXdox's own tree, which is not
#     reachable here (a sibling checkout, SHAPE_PIN_SOURCE_OPENXDOX, or --link-source openXdox=<path> would let it be read). The recorded chain still classifies.
#   codexDox                         domain-descendant   also_matches project-leg/assembly
#   exit 0
naming --link-source "openXdx=$R/lx/openXdox"         # a mistyped key
#   (the same WARNING and line)
#   exit 0
naming --link-source "openXdox=$R/locked/openXdox"    # keyed, under a parent this user cannot enter
#   Traceback (most recent call last):
#     File "$U/scripts/validate-repository-naming.py", line 577, in main
#     File "$U/scripts/validate-repository-naming.py", line 457, in _classify_targets
#     File "$U/scripts/validate-repository-naming.py", line 112, in _link_pins
#     File "$U/scripts/repo_shape.py", line 2397, in link_pins_from_trees
#     File "$U/scripts/repo_shape.py", line 2380, in resolve_link_source
#     File "$U/scripts/repo_shape.py", line 2330, in link_manifest_path
#       if candidate.is_dir():
#     ...
#   PermissionError: [Errno 13] Permission denied: '$R/locked/openXdox'
#   exit 1
chmod 755 "$R/locked"

What each run shows:

  • The second run reads the value as {"a": Path("b/openXdox")}: a keyed source for a link no chain names, which is never looked up. The WARNING then tells a person who passed --link-source that --link-source openXdox=<path> "would let it be read".
  • The third run (the typo) is the same silence.
  • The fourth run crashes, though the module docstring says "A chain link whose tree could not be read prints a WARNING and changes no exit code, deliberately" and --help says "A link no source answers for is declared-unverified, which is a warning and not a finding".

The same traceback, exit 1, under Python 3.12.3:

  • SHAPE_PIN_SOURCE_OPENXDOX=$R/locked/openXdox with no flag.
  • No flag and no variable, with a sibling checkout ../openXdox at mode 000. This is the beside-the-project lookup every run makes. It ends in PermissionError: ... '../openXdox/project.yaml', because the directory itself answers is_dir() and its manifest does not answer is_file().
  • --link-source openXdox=<a 300-character name>, which ends in OSError: [Errno 36] File name too long.

Across interpreters, the keyed mode-000 run is that traceback (exit 1) on 3.10.21, 3.11.16, 3.12.14 and 3.13.15. On 3.14.7 and 3.15.0rc2 it is the declared-unverified WARNING with exit 0, which offers --link-source openXdox=<path> as the remedy to the person who passed exactly that. CI pins python-version: '3.x', so no CI job ever runs the traceback, while the standard's floor is 3.9.

Why. The split decides "keyed or bare" knowing nothing about which names are links, nothing reports a value that answered for nothing, and the lookup assumes "is it a directory" can only answer yes or no. Both files are COPIED into every assembly root (shape_materialize.COPIED_FROM_SHAPE) and digest-pinned in contracts/shape-pin.yaml, and a family holder copies repo_shape.py too. So a fix reaches a project only through update-shape.py, and a change to repo_shape.py puts an upstream-changed row in front of every project and holder.

That is the cost #312 weighed when it left this half. The names a key would be checked against are the chain links, known only once the targets are built (--referent-chain, or each leg's naming.referent_chain: read inside _targets_from_project, which classifies with the sources in the same pass). So #295's reading moves the parse behind target construction.

What it should do. An explicit link source is used, or said not to have been, or reported as unreadable; it never ends the run. The decisions are the author's:

  1. Which reading wins for a value holding =. validate-pins.py: a --pin-source value is PRODUCT=PATH only when the text before its first = names a declared neutral-product pin, so a bare stand-in under a=b/ recomputes the digest instead of falling to gh api and a skip, an explicit source that does not exist or is not a directory is the neutral-pin-source-unreadable finding instead of a FileNotFoundError traceback, and a value no pin used is noted (#295) #312's rule for --pin-source is the model: a key only when the text before the first = casefolds to a declared name, otherwise the whole value is a bare path; keyed wins over an existing whole-value directory; ./ forces the bare reading. Here "declared" would mean a chain link: the links of --referent-chain, or of every target's naming.referent_chain: under --project. Decide whether the same rule and the same ./ spelling apply, so that the two copied tools read [PRODUCT=]PATH one way, or a different one.
  2. A value nothing consumed. After classification, report each --link-source no link used, with why: no link has that name, a later value replaced it, or a more specific source answered. Report it as a note beside the WARNINGs, never a finding, since an unread link is a warning by design. validate-pins.py: a --pin-source value is PRODUCT=PATH only when the text before its first = names a declared neutral-product pin, so a bare stand-in under a=b/ recomputes the digest instead of falling to gh api and a skip, an explicit source that does not exist or is not a directory is the neutral-pin-source-unreadable finding instead of a FileNotFoundError traceback, and a value no pin used is noted (#295) #312's note lines are the precedent.
  3. A source that cannot answer. Cover every way a keyed or bare --link-source, a SHAPE_PIN_SOURCE_<LINK> or a sibling checkout can fail: a parent that cannot be entered, a name too long, a manifest that cannot be read. Each is reported with the system's reason (one stat() inside a guard, as validate-pins.py: a --pin-source value is PRODUCT=PATH only when the text before its first = names a declared neutral-product pin, so a bare stand-in under a=b/ recomputes the digest instead of falling to gh api and a skip, an explicit source that does not exist or is not a directory is the neutral-pin-source-unreadable finding instead of a FileNotFoundError traceback, and a value no pin used is noted (#295) #312's _digest_from_source does) on every Python from the floor up, and never as a traceback. Decide whether an EXPLICIT source that cannot answer stays a warning (this file's rule) or becomes a finding (templates/assembly-root/scripts/validate-pins.py: a bare --pin-source <path> whose path holds an = is read as <product>=<path> (raw.partition("=")), so a stand-in clone under a directory named a=b is never consulted and the pin's digest silently goes to gh api and a skip (pins ok, exit 0), or, when the text before the = is a declared pin's name, to a FileNotFoundError traceback #295's rule for --pin-source), and say which in --help.
  4. Where the code goes. Either in validate-repository-naming.py alone (its own parse and its own guarded lookup), or in a shared helper in repo_shape.py, which costs every project and holder an upstream-changed row. Either way a changed --help is rendered into docs/cli.md by scripts/render-cli-reference.py (tests/test_cli_reference.py fails otherwise).

How to test. In tests/test_naming_policy.py, beside its existing --link-source tests:

The traceback rows only bite on Python before 3.14, which CI's '3.x' does not run, so run them locally on the floor once.

Verified at main c7c02fa (scripts/validate-repository-naming.py sha256 362efbbd…, scripts/repo_shape.py c4c33094…); no open PR touches either file.

Related:

Activity

  1. brettheap commented on Oct 12, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-5, session 3c13126a-509b-418f-b102-a3a282837b54@Eagle, 2026-10-12T14:29:49Z, for #338

    Logged in lanes/log/openRepoShape-5.md at e1a9044d74ca06edbd294cda28dfb3097d44a945.

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

    1. existing `CLAIMED —` comments on opensoft/openRepoShape#338:
       none
    2. `gh pr list --repo opensoft/openRepoShape --state all --search "#338"`, kept where the row or its body names `#338`:
       none
    3. `git ls-remote --heads git@github.com:opensoft/openRepoShape` branches naming `338` 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