Repository navigation
Description
validate-pins.py should refuse an unsearchable contracts/ by name and not end in a PermissionError, name an unreadable pin by its path relative to the root and once, give a failed read the advice for its own cause, and spell the names and paths it prints. #633's _is_a is the ready fix for the first of these. The riders in the same file ride with it.
From lane 4's checklist triage of 2026-10-16 (T3), approved by Brett Heap at 2026-10-16T13:36:07Z ("approve all triage recommendations"). One pull request covers the lines below.
Parent: #459. Milestone: v1.1.1 (a follow-up filed after Brett Heap's v1.1.1 word of 2026-10-16T13:35:28Z).
Authored after the v1.1.0 work; lands after the v1.1.0 tag unless the release owner (lane 3) says otherwise.
Lands after:
- lane 2's Pinned shape copies: one bump, so AGENTS-shape.md is true of every root, the scaffold's chain WARNING offers only its own sources and spells
--pin-source, the root's not-used note spells it too, and THE LOCKSTEP RULE'sbumpruns (#610) #634 (ready): it editstemplates/assembly-root/scripts/validate-pins.pyandscripts/repo_shape.py, and spells--pin-sourcevalues with_shownwhile leaving the pin path alone (L4-144's remark). - lane 3's validate-pins.py and shape-doctor.py: one leg's gitlink off its pin beside another leg's
pin-digest-mismatchsays "First clear that finding" and the digest names no line, so everybump-leg.pyeither finding points at is rolled back and the one step that works, putting the gitlink back by hand, is never named #677 (filed, v1.1.0) and its draft pull request validate-pins.py and shape-doctor.py: one leg's gitlink off its pin beside another leg's digest or workflow reference names the step that comes first, that gitlink put back by hand, and then the other leg'sbump-leg.pyline, so neither finding says "First clear that finding" over a finding no line can clear first (#677) #702: the same pinnedvalidate-pins.py.
Effort: M: _is_a at five is_file() sites, one relative path, spelled names. Platform: all (the mode-000 route is POSIX; L4-103 was measured on Python 3.12, 3.13 and 3.14).
Files: templates/assembly-root/scripts/validate-pins.py and scripts/repo_shape.py (load_yaml), both pinned shape copies. The pull request re-pins both. Lane 4's #714 changes the same two files, and #714 lands after this one.
Lane openRepoShape-4 claims it at filing; see its CLAIMED comment.
Why this is filed. The input is exotic: an unsearchable contracts/ directory, a pin this user cannot open, a workflow named with a newline or a bidi control, a pin entry holding ESC. None is met by an ordinary repository, and each is a refusal or a stack trace on a root nobody else can open. Line numbers are at c6122e8 (#639) unless a line says another commit; main has moved since.
Lines
- L4-103 (P3): an unsearchable
contracts/still crashesvalidate-pins.py. Withcontracts/at mode 000 or r--, the validator ends inPermissionError, exit 1, at_load_leg_pin'sif not pin_path.is_file():(validate-pins.py:374; frames_run_all_checks:1378,_check_leg:807,_load_leg_pin:374) on Python 3.12 and 3.13. On 3.14 it isREFUSED pin-missing: leg 'spec' declares testorg/Atlas-spec at 'spec' but contracts/spec-pin.yaml does not exist, exit 2: not a pass, but it says a present file does not exist and gives the lockstep remedy. Main does the same; validate-pins.py (assembly-root template): a.github/workflows/directory that this user cannot list or search is aworkflow-unreadablerefusal naming it, exit 2, so the project's pins gate no longer ends in a PermissionError traceback, exit 1, where a workflow file it cannot open already was one (#434) #633 neither caused nor changed it. templates/assembly-root/scripts/validate-pins.py: a.github/workflows/directory that this user cannot list or search ends the run in a PermissionError traceback, exit 1, where a workflow file it cannot open is aworkflow-unreadablerefusal, exit 2 #434 itself names "theis_file()calls on the pin and the manifest" as the same shape, unprobed. Atac508957they are:374(leg pin),:823(shape pin),:855(shape copy, which on 3.14 reads "pinned but absent"),:1085(neutral pin) and:1352(manifest)._is_a, which validate-pins.py (assembly-root template): a.github/workflows/directory that this user cannot list or search is aworkflow-unreadablerefusal naming it, exit 2, so the project's pins gate no longer ends in a PermissionError traceback, exit 1, where a workflow file it cannot open already was one (#434) #633 adds, is the ready fix for all of them (R434 O1). (source) - L4-104 (P3): a pin this user cannot open spells its path absolute, twice.
contracts/spec-pin.yamlat mode 000 givesREFUSED yaml-unreadable: <absolute path>: [Errno 13] Permission denied: '<absolute path>'. That isrepo_shape.load_yaml's line (scripts/repo_shape.py:1006,Refusal("yaml-unreadable", f"{path}: {exc}")), where validate-pins.py (assembly-root template): a.github/workflows/directory that this user cannot list or search is aworkflow-unreadablerefusal naming it, exit 2, so the project's pins gate no longer ends in a PermissionError traceback, exit 1, where a workflow file it cannot open already was one (#434) #633's ownworkflow-unreadablelines are relative to the root and POSIX, and its tests assertstr(root) not in stderr(R434 O2). (source) - L4-106 (P3): names are quoted raw in
workflow-unreadabledetail lines. This predates validate-pins.py (assembly-root template): a.github/workflows/directory that this user cannot list or search is aworkflow-unreadablerefusal naming it, exit 2, so the project's pins gate no longer ends in a PermissionError traceback, exit 1, where a workflow file it cannot open already was one (#434) #633 (templates/assembly-root/scripts/validate-pins.py: a workflow file under .github/workflows/ that is not UTF-8 ends the run in a UnicodeDecodeError traceback, exit 1, with no refusal naming the file, so the project's pins gate (make validate, the validate workflow) dies on a Latin-1 comment where a non-UTF-8 pin is a cleanREFUSED yaml-unreadable, exit 2 #359's path) and is now also reachable through the r-- route. A.ymlwhose name holds a newline prints as two lines, so7 workflow file(s)is followed by eight lines. A bidi control (U+202E) or a C1 control (U+0085) prints raw to a UTF-8 stderr, so a name can display reversed. Undecodable bytes print as\udcXX. Measured under the default locale, underLC_ALL=C PYTHONUTF8=0and on 3.14: exit 2, no traceback, every name listed. Main's templates/assembly-root/scripts/validate-pins.py: a workflow file under .github/workflows/ that is not UTF-8 ends the run in a UnicodeDecodeError traceback, exit 1, with no refusal naming the file, so the project's pins gate (make validate, the validate workflow) dies on a Latin-1 comment where a non-UTF-8 pin is a cleanREFUSED yaml-unreadable, exit 2 #359 path does exactly the same for a mode-000 file so named in a readable directory (R434 O4). Where:validate-pins.py:225lists the names raw. (source) - L4-144 (P3): a control character in a pin ENTRY reaches stderr raw, in the pin's PATH.
neutral_product_pins: ["\x1b[31mRED"]is a list, so validate-repository-naming.py, validate-manifest.py and validate-pins.py: a project's ownneutral_product_pins:or a leg'snaming.referent_chain:that is not a list isvalidate-manifest.py's finding for that field in every gate that reads it, so5no longer endsmake validatein aTypeErrorand a scalaropenXdoxis no longer read as its characters (#443) #631 reads it as one pin, andvalidate-pins.pyrefusesREFUSED neutral-pin-missing: project.yaml declares a pin on '\x1b[31mRED' but contracts/<ESC>[31mred-pin.yaml does not exist, exit 2. The name isrepr'd, but the path is built atvalidate-pins.py:1084-1089(at5b29ee1;:1087onc6122e8) and carries the ESC byte (octal 033 inod -c). Main printedcontracts/<ESC>-pin.yamlfor the scalar spelling, which validate-repository-naming.py, validate-manifest.py and validate-pins.py: a project's ownneutral_product_pins:or a leg'snaming.referent_chain:that is not a list isvalidate-manifest.py's finding for that field in every gate that reads it, so5no longer endsmake validatein aTypeErrorand a scalaropenXdoxis no longer read as its characters (#443) #631 fixes. The list-entry case is the "entries that are not names" class, and it sits beside lane 2's Pinned shape copies: AGENTS-shape.md'sCONTRACTS_DIRsentence is false for every scaffolded root, and the chain WARNING offers remedies the scaffold lacks (one shape bump before the v1.1.0 tag) #610 (Pinned shape copies: one bump, so AGENTS-shape.md is true of every root, the scaffold's chain WARNING offers only its own sources and spells--pin-source, the root's not-used note spells it too, and THE LOCKSTEP RULE'sbumpruns (#610) #634), which spells--pin-sourcevalues with_shownand leaves the pin path alone (R443 O4). (source)
Rides along (backlog; tick it here if the same change covers it)
- L4-105: the file-level
cannot be readadvice is not cause-aware. That is templates/assembly-root/scripts/validate-pins.py: a workflow file under .github/workflows/ that is not UTF-8 ends the run in a UnicodeDecodeError traceback, exit 1, with no refusal naming the file, so the project's pins gate (make validate, the validate workflow) dies on a Latin-1 comment where a non-UTF-8 pin is a cleanREFUSED yaml-unreadable, exit 2 #359's sentence, which validate-pins.py (assembly-root template): a.github/workflows/directory that this user cannot list or search is aworkflow-unreadablerefusal naming it, exit 2, so the project's pins gate no longer ends in a PermissionError traceback, exit 1, where a workflow file it cannot open already was one (#434) #633 extends. A childstator read that fails with EIO or ENAMETOOLONG is told "this user cannot open it, so restore read access to it". The directory's refusal in validate-pins.py (assembly-root template): a.github/workflows/directory that this user cannot list or search is aworkflow-unreadablerefusal naming it, exit 2, so the project's pins gate no longer ends in a PermissionError traceback, exit 1, where a workflow file it cannot open already was one (#434) #633, andbump-leg.py's_cannot_open_advice(bump-leg.py: a workflow file, a.github/workflows/directory or a leg pin that this user cannot open ends the run in a PermissionError traceback, exit 1, under--dry-runas well, becausecmd_bumpandplan_workflowsread them with no handler andmain()catchesRefusalandCommandFailedonly, so no refusal names the file or says to make it readable #383), give a non-permission cause its own advice (R434 O3). Where:validate-pins.py:219says 'restore read access to it' whatever the cause. (source) - L4-143: cosmetic: a pin source for a project that now declares nothing gets a note that does not point at the finding. With
neutral_product_pins: openXdox(a scalar, so the field declares no pin) and--pin-source openXdox=../nowhere,validate-pins.pyadds--pin-source openXdox=../nowhere: not used — project.yaml declares no neutral-product pin.under the pins sentence (templates/assembly-root/scripts/validate-pins.py:1353, at5b29ee1). It is true under the "declares nothing" rule; a note that pointed at the finding would read better (R443 O3). (source) - L4-102:
PinSourcesstill keys--pin-sourceproducts bycasefold(), inPinSources.__init__andresolve_neutral_pin_source(validate-pins.py:934and:948). scripts/repo_shape.py: the chain readers identify a link, a declared pin and a--link-sourcekey bycasefold()(twelve calls,:1207to:2428), where #275's fix (PR #409, 6494714) makes a product's identitypin_product_key(lower(), the pin file's own fold), soopenßandopenssare two products to the pin validator and one link to the chain: a holder's tree that declaresopenssverifies a chain that saysopenß, a project that pinsopenssverifies a chain that begins atopenß, a--link-source openss=answers for the linkopenß,link_pins_from_treeskeeps one entry for the pair, and a chain that names both is refused as a cycle #444 (repo_shape.py and validate-repository-naming.py: a chain link, a declared pin and a--link-sourcekey are one product bypin_product_key, the pin file's own fold, so a tree declaringopenssno longer verifies a chain that saysopenß, a chain naming both is two links and not a cycle, and--link-source openss=no longer answers foropenß(#444) #638) moves the chain readers'LinkSourcestopin_product_key(), which islower(), soopenßandopenssare two links there. But--pin-source openss=still answers foropenß, so the two copied tools read[PRODUCT=]PATHone way only over ASCII. A pin's digest stops a wrong source from passing, so nothing wrong is accepted: the source is simply looked up under the other spelling. The fix ispin_product_keyin both places, as repo_shape.py and validate-repository-naming.py: a chain link, a declared pin and a--link-sourcekey are one product bypin_product_key, the pin file's own fold, so a tree declaringopenssno longer verifies a chain that saysopenß, a chain naming both is two links and not a cycle, and--link-source openss=no longer answers foropenß(#444) #638 did forLinkSources(R444 O1). (source)
What is expected
- An unsearchable
contracts/is refused by name, in one place, at all fiveis_file()sites, on every Python the repository supports: not aPermissionErroron 3.12 and 3.13, and not "does not exist" with the lockstep remedy on 3.14. - An unreadable pin is named by its path relative to the root, once, in POSIX spelling, as validate-pins.py (assembly-root template): a
.github/workflows/directory that this user cannot list or search is aworkflow-unreadablerefusal naming it, exit 2, so the project's pins gate no longer ends in a PermissionError traceback, exit 1, where a workflow file it cannot open already was one (#434) #633's ownworkflow-unreadablelines are. - A read that fails for another cause than permission gets that cause's advice.
- Every name and path this file prints is spelled (a newline, a bidi control, a C1 control, ESC, an undecodable byte), with one test row for each print.
PinSourceskeys bypin_product_key.- The pull request says which of L4-143 and L4-102 it took.
Not a duplicate: lane 3's #677 and #697 touch validate-pins.py and the doctor's lost-repo_shape.py row, and not these lines; lane 2's #634 spells --pin-source values only. No issue numbered #650 to #700 cites L4-102 to L4-106, L4-143 or L4-144 (read at 13:17Z).
Each line above is a line of #459's checklist comments (issuecomment-6214491417, issuecomment-6214788046 and issuecomment-6213851688). Tick it there too when this closes.
Lane: openreposhape-4 (openRepoShape-4)
Activity
brettheap commentedon Oct 16, 2026
CLAIMED — lane openRepoShape-4, session 11c6c0ef-495a-4a05-94cf-811b7b0a1012@Eagle, 2026-10-16T14:00:27Z, for #712
Logged in
lanes/log/openRepoShape-4.mdatbefa998bd181f823c6bb24093d29dfa255dac39f.The three reads (lane-collision-protocol Rule 1):
validate-pins.py: a leg checkout this user cannot search is `pin-subm…
Merge main bad05d2 into fix/712-validate-pins-contracts
tests: the sibling row at mode 000 stubs `tree_digest_from_gh`, so no…
Merge main (9f235da, #827 for #712 landed) into fix/714-legs-not-a-li…
Merge main (ca47b71) into fix/847-doctor-unreadable-pin
shape-doctor.py: the `legs`, `manifest kinds` and `shape currency` ro…
the assembly root's validate-manifest.py and bootstrap.py: a path thi…
validate-pins.py: a `--pin-source` key is the longest declared pin be…