Repository navigation
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
Description
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
pathlibswallows the same four errors as 3.10's. - The project's own gate (
scripts/bootstrap.pyandvalidate.ymlrunvalidate-repository-naming.py --project project.yaml, which reaches the sameresolve_link_source) and the doctor'snamingrow: read, not run. scaffold-project.py --referent-chain, which hands--pin-sourcetolink_pins_from_treesas the default link source: read, not run. Its pin digest reads that same source first; see thescaffold-project.py --pin-sourceissue filed from the same batch.
What is wrong. Two faults sit in one parser and one lookup.
- The parser.
_link_sources_from_args(scripts/validate-repository-naming.py:393-409at c7c02fa) reads every--link-sourcevalue withraw.partition("=")(:404). With an=, the value isPRODUCT=PATH, keyed by whatever precedes the first=; without one, it is the bare default.resolve_link_source(scripts/repo_shape.py:2356-2382) looks keyed sources up only by a chain link's name. Nothing afterwards asks whether a value was used. This is the parser 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 nameda=bis never consulted and the pin's digest silently goes togh apiand askip(pins ok, exit 0), or, when the text before the=is a declared pin's name, to aFileNotFoundErrortraceback #295 fixed invalidate-pins.py. - The lookup.
link_manifest_path(repo_shape.py:2327-2332) asksPath.is_dir()and thenPath.is_file()with no guard. Before Python 3.14 those swallow only ENOENT, ENOTDIR, EBADF and ELOOP, so any otherOSErrorescapes, and nothing above them catches one.
# $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-sourcethat--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
--helpsays "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/openXdoxwith no flag.- No flag and no variable, with a sibling checkout
../openXdoxat mode 000. This is the beside-the-project lookup every run makes. It ends inPermissionError: ... '../openXdox/project.yaml', because the directory itself answersis_dir()and its manifest does not answeris_file(). --link-source openXdox=<a 300-character name>, which ends inOSError: [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:
- Which reading wins for a value holding
=. validate-pins.py: a--pin-sourcevalue isPRODUCT=PATHonly when the text before its first=names a declared neutral-product pin, so a bare stand-in undera=b/recomputes the digest instead of falling togh apiand askip, an explicit source that does not exist or is not a directory is theneutral-pin-source-unreadablefinding instead of aFileNotFoundErrortraceback, and a value no pin used is noted (#295) #312's rule for--pin-sourceis 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'snaming.referent_chain:under--project. Decide whether the same rule and the same./spelling apply, so that the two copied tools read[PRODUCT=]PATHone way, or a different one. - A value nothing consumed. After classification, report each
--link-sourceno 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-sourcevalue isPRODUCT=PATHonly when the text before its first=names a declared neutral-product pin, so a bare stand-in undera=b/recomputes the digest instead of falling togh apiand askip, an explicit source that does not exist or is not a directory is theneutral-pin-source-unreadablefinding instead of aFileNotFoundErrortraceback, and a value no pin used is noted (#295) #312'snotelines are the precedent. - A source that cannot answer. Cover every way a keyed or bare
--link-source, aSHAPE_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 (onestat()inside a guard, as validate-pins.py: a--pin-sourcevalue isPRODUCT=PATHonly when the text before its first=names a declared neutral-product pin, so a bare stand-in undera=b/recomputes the digest instead of falling togh apiand askip, an explicit source that does not exist or is not a directory is theneutral-pin-source-unreadablefinding instead of aFileNotFoundErrortraceback, and a value no pin used is noted (#295) #312's_digest_from_sourcedoes) 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 nameda=bis never consulted and the pin's digest silently goes togh apiand askip(pins ok, exit 0), or, when the text before the=is a declared pin's name, to aFileNotFoundErrortraceback #295's rule for--pin-source), and say which in--help. - Where the code goes. Either in
validate-repository-naming.pyalone (its own parse and its own guarded lookup), or in a shared helper inrepo_shape.py, which costs every project and holder anupstream-changedrow. Either way a changed--helpis rendered intodocs/cli.mdbyscripts/render-cli-reference.py(tests/test_cli_reference.pyfails otherwise).
How to test. In tests/test_naming_policy.py, beside its existing --link-source tests:
- A link tree under a directory named
a=b, passed bare: the chain verifies and nodeclared-unverifiedWARNING appears. - A mistyped key beside a good bare source: the note names the typo and the exit is unchanged.
- A keyed source, a
SHAPE_PIN_SOURCE_OPENXDOXand a sibling checkout, each under a parent at mode 000: the reason is printed and nothing is a traceback. Skip these on Windows and as root, with the reason beside the skip, as validate-pins.py: a--pin-sourcevalue isPRODUCT=PATHonly when the text before its first=names a declared neutral-product pin, so a bare stand-in undera=b/recomputes the digest instead of falling togh apiand askip, an explicit source that does not exist or is not a directory is theneutral-pin-source-unreadablefinding instead of aFileNotFoundErrortraceback, and a value no pin used is noted (#295) #312'sSEARCH_DENIEDrows intests/test_neutral_pin_validator.pydo. - A 300-character name, which runs on every OS.
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:
- 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 nameda=bis never consulted and the pin's digest silently goes togh apiand askip(pins ok, exit 0), or, when the text before the=is a declared pin's name, to aFileNotFoundErrortraceback #295 (decision 4 there asked that this twin be decided or said why not; PR validate-pins.py: a--pin-sourcevalue isPRODUCT=PATHonly when the text before its first=names a declared neutral-product pin, so a bare stand-in undera=b/recomputes the digest instead of falling togh apiand askip, an explicit source that does not exist or is not a directory is theneutral-pin-source-unreadablefinding instead of aFileNotFoundErrortraceback, and a value no pin used is noted (#295) #312 said why not). - scripts/repo_shape.py:
pin_source_env_nameturns every character outside[A-Z0-9_]into_, so the neutral productsopen-glass,open_glassandopen.glassshare oneSHAPE_PIN_SOURCE_OPEN_GLASS, and a project that pins two of them gets aneutral-pin-source-unreadableFINDING (exit 1) for whichever one the variable does not name #275 (pin_source_env_namefolds distinct products onto oneSHAPE_PIN_SOURCE_*, the same variable this lookup reads). - Descendant referent follows the declared pin chain (ruled by Brett Heap 2026-09-05) #40 (the recorded chain this flag verifies).
- The
scaffold-project.py --pin-sourcetraceback issue filed from the same batch (the scaffold's own copy of the uncaught-OSErrorclass).
Activity
brettheap commentedon Oct 12, 2026
CLAIMED — lane openRepoShape-5, session 3c13126a-509b-418f-b102-a3a282837b54@Eagle, 2026-10-12T14:29:49Z, for #338
Logged in
lanes/log/openRepoShape-5.mdate1a9044d74ca06edbd294cda28dfb3097d44a945.The three reads (lane-collision-protocol Rule 1):
repo_shape.py, validate-repository-naming.py: a link manifest whose `…
repo_shape.py, scaffold-project.py: a link tree the scaffold reached …
scaffold-project.py, validate-pins.py, repo_shape.py: `open-glass` an…
tests/test_naming_policy.py: the scaffold's "reached but could not be…
Merge origin/main (9736424, #409's squash) into fix/link-source-values (
validate-repository-naming.py, repo_shape.py: a `--link-source` value…
Merge main cc8f2da into fix/adopt-plan-quote-yaml-keywords (#181)
docs/release-notes/v1.0.0.md: brought up to main d41cc29, so section …
validate-repository-naming.py, repo_shape.py, validate-pins.py: a bar…
validate-repository-naming.py, validate-manifest.py and validate-pins…