Skip to content

scripts/repo_shape.py: pin_source_env_name turns every character outside [A-Z0-9_] into _, so the neutral products open-glass, open_glass and open.glass share one SHAPE_PIN_SOURCE_OPEN_GLASS, and a project that pins two of them gets a neutral-pin-source-unreadable FINDING (exit 1) for whichever one the variable does not name #275

Closed

Description

@brettheap

Lane: openreposhape-4 (openRepoShape-4)

Found by this lane's adversarial review of the #241 fix branch (fix/scaffold-pin-product-collisions, PR #256, head a25c2e1; its out-of-scope item O3), and re-verified on main eb6df75 (d89fffa plus #220, which changes shape-doctor.py, its tests, README.md and docs/handbook.html; every line cited here was read on eb6df75) with one python call and a two-pin scaffold followed by the project's own validate-pins.py. It is pre-existing and not caused by any open branch. Rare: it needs one project that pins two neutral products whose names differ only in punctuation. Not claimed.

What is wrong. pin_source_env_name (scripts/repo_shape.py:1028-1029) is PIN_SOURCE_ENV_PREFIX + re.sub(r"[^A-Z0-9_]", "_", product.upper()). ., - and _ are all legal in a product name (PIN_ARG_RE, scaffold-project.py:76; PIN_NAME_RE, adopt-project.py:137), and they name distinct pin files (contracts/<product lowercased>-pin.yaml, templates/assembly-root/scripts/validate-pins.py:404), but they map to one variable:

# $U: a clean checkout at eb6df75.
cd $U/scripts && python3 -c "from repo_shape import pin_source_env_name as f; [print(repr(p), f(p)) for p in ('open-glass','open_glass','open.glass','openGlass')]"
#   'open-glass' SHAPE_PIN_SOURCE_OPEN_GLASS
#   'open_glass' SHAPE_PIN_SOURCE_OPEN_GLASS
#   'open.glass' SHAPE_PIN_SOURCE_OPEN_GLASS
#   'openGlass' SHAPE_PIN_SOURCE_OPENGLASS

Which product's override the operator gets when two such pins are given: both, and so neither. validate-pins.py reads the variable once per declared pin and uses the path for each (rule 2 of resolve_neutral_pin_source, :376-379), so whatever checkout the variable names answers for both products, and the one it is not a checkout of fails. The probe: $R/open-glass and $R/open_glass are two one-commit stand-in repositories, a project Northwind was scaffolded with --pin open-glass@<A> --pin open_glass@<B> (--pin-source takes one clone for every pin, so the scaffold read both digests from a third clone holding both commits), then cloned to a directory with no sibling checkouts, gh a shim that exits 97:

python3 scripts/validate-pins.py            # no source given
#   skip open-glass: pin commit 75b3a88a5480 digest NOT rechecked — no local checkout (gh-unreadable: ... gh shim: refused). A --pin-source, SHAPE_PIN_SOURCE_OPEN_GLASS, or a checkout at ../open-glass beside this assembly root would verify it off...
#   skip open_glass: pin commit 45b884f2e1a1 digest NOT rechecked — no local checkout (gh-unreadable: ... gh shim: refused). A --pin-source, SHAPE_PIN_SOURCE_OPEN_GLASS, or a checkout at ../open_glass beside this assembly root would verify it off...
#   exit 0   (the hint names the same variable for both products)
SHAPE_PIN_SOURCE_OPEN_GLASS=$R/open-glass python3 scripts/validate-pins.py
#   FINDING neutral-pin-source-unreadable: contracts/open_glass-pin.yaml: $R/open-glass ($SHAPE_PIN_SOURCE_OPEN_GLASS) could not answer for opensoft/open_glass @ 45b884f2e1a1...: `git ls-tree -r -z 45b884f2e1a1...` in ...
#     ok  open-glass: pin commit 75b3a88a5480 digest recomputes ($SHAPE_PIN_SOURCE_OPEN_GLASS)
#   exit 1
SHAPE_PIN_SOURCE_OPEN_GLASS=$R/open_glass python3 scripts/validate-pins.py
#   FINDING neutral-pin-source-unreadable: contracts/open-glass-pin.yaml: $R/open_glass ($SHAPE_PIN_SOURCE_OPEN_GLASS) could not answer for opensoft/open-glass @ 75b3a88a5480...
#     ok  open_glass: pin commit 45b884f2e1a1 digest recomputes ($SHAPE_PIN_SOURCE_OPEN_GLASS)
#   exit 1
python3 scripts/validate-pins.py --pin-source open-glass=$R/open-glass --pin-source open_glass=$R/open_glass
#   ok  open-glass: ... (--pin-source open-glass=...)    ok  open_glass: ... (--pin-source open_glass=...)    pins ok   exit 0

No value the operator can export makes both pins verify, and the pin that fails is reported as a finding about the project (exit 1, a red validate.yml), not as the skip it would be with no source at all. The exits that work today are the per-product --pin-source <product>=<path> (the key is the exact name, validate-pins.py:373, :574, so punctuation variants stay distinct) and a sibling checkout named for the pinned repository (rule 3, :380-383), neither of which uses the variable.

Why. The mapping is not injective and nothing downstream knows it. The same function names the variable in three more places that assume one name per product: the link-source lookup (repo_shape.py:2300-2303, rule 2 of resolve_link_source, which has the same collision for two chain links whose names differ in punctuation; not probed), the chain's declared-unverified hint (repo_shape.py:1193) and the skip hint above (validate-pins.py:499). The docs spell it SHAPE_PIN_SOURCE_<PRODUCT> (README.md:682, :697; docs/cli.md:735, :807) and only the comment at repo_shape.py:1018-1024 says what <PRODUCT> is. No test touches the function (grep -rn pin_source_env_name tests finds nothing); tests/conftest.py:93-106 blanks every variable by prefix, so a different spelling under the same prefix would still be blanked.

What it should do. A variable named for one product must not answer for another, or the collision must be said where it can be seen. The decisions are the author's:

  1. Refuse or re-spell. Either a project (or a --pin list) that declares two products with one variable name is refused where both names are visible (scaffold-project.py, adopt-project.py plan/check, validate-manifest.py), saying to use --pin-source <product>=<path>; or the mapping becomes injective for the punctuation (openGlass must stay SHAPE_PIN_SOURCE_OPENGLASS; a plain - -> _ cannot be kept and be injective, so some escape for _ or for the other two is needed). The second changes a documented name for punctuated products only, and needs the docs above updated.
  2. Whether, short of either, the two hints (validate-pins.py:499, repo_shape.py:1193) stop printing the variable for a product whose name shares it and print --pin-source <product>=<path> instead.
  3. Whether the collision is simply documented as a limit of the variable (README says what <PRODUCT> is and that punctuation variants need the keyed flag). The probe shows it is reachable by one scaffold command, so a refusal is the safer reading.

How to test. A unit test over pin_source_env_name for the pairs open-glass / open_glass / open.glass (they differ, or the declaring call refuses), and one over openGlass (unchanged). An integration test in the style of tests/test_neutral_pin_validator.py (which stubs gh and passes the source explicitly): scaffold or hand-write a project pinning two such products, give each its own stand-in clone, set the variable to one of them, and assert the other is not reported neutral-pin-source-unreadable (or that the declaration was refused first). Run it with a gh that exits 97 first on PATH and assert the shim log is empty.

Related: #241 (PR #256: scaffold's refusal of reserved --pin names, which found this), #126 (closed: run_script blanks every SHAPE_PIN_SOURCE_*), tests/test_neutral_pin_validator.py.

Activity

  1. brettheap commented on Oct 12, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-5, session e62c2fd7-176d-429d-89fb-0653dd08ad5f@Eagle, 2026-10-12T11:05:06Z, for #275

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

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

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