fix(standards): unblock commits, repair the lockfile, gate it in CI - #804
Conversation
`standards` could not accept ANY commit through its own pre-commit chain.
Two independent defects, both in staged mode only:
1. validate-spdx.sh — scan mode applies an extension allowlist; staged mode
applied NONE, so every staged file was judged by a rule written for source
files. The machine-generated .github/workflows/actions.lock ("Do not edit by
hand") carries no SPDX header and has any added header stripped on the next
regeneration, so this blocked EVERY commit touching the lockfile — which is
why a Dependabot-caused lockfile desync could sit unrepaired. Same fault hit
README.adoc and any other non-source file. Cure: one is_source_file()
predicate, applied in BOTH modes.
2. validate-a2ml.sh — the registry-drift gate in .githooks/pre-commit FAILS
every commit until you regenerate and stage .machine_readable/REGISTRY.a2ml,
and this validator then REJECTED that very file. No ordering satisfied both
gates; --no-verify was the only exit. Cure: skip files that declare
themselves generated. Narrow by construction — 2 of 222 tracked .a2ml files
carry the marker, and the pattern is anchored (^# GENERATED FILE ... DO NOT
EDIT BY HAND) so a prose mention cannot spoof the exemption.
Mutation-tested, 7 cases:
T1 lockfile alone rc=0 (was 1) cured
T2 real .sh missing SPDX rc=1 guard not disarmed
T3 full-scan mode rc=1 PRE-EXISTING at HEAD (215
errors); patched output is
byte-identical, diff clean
T4 REGISTRY.a2ml rc=0 (was 1) cured
T5 hand-written .a2ml, no fields rc=1 guard not disarmed
T6 MUTANT: prose mention of marker rc=1 spoof killed by the anchor
T7 MUTANT: exemption removed rc=1 proves the exemption acts
Known and deliberately not fixed here: 215 tracked files lack SPDX headers, so
the full-scan gate must stay non-blocking in CI until that backlog is cleared.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HfgwLCdKNd5iZVo6VTiSim
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📜 Recent review details⏰ Context from checks skipped due to timeout. (1)
|
| Layer / File(s) | Summary |
|---|---|
Validation rule updates .githooks/validate-a2ml.sh, .githooks/validate-spdx.sh |
Generated A2ML files with the defined header bypass validation. Staged non-source files no longer require SPDX headers. |
Registry source-hash updates .machine_readable/REGISTRY.a2ml |
The meta-a2ml, 0-ai-gatekeeper-protocol, and rhodium-standard-repositories entries receive updated source_hash values. No entries are added or removed. |
Priority: ⬇️ Low
Estimated code review effort: 2 (Simple) | ~10 minutes
Change: Bug fix
Suggested reviewers: joshuajewell
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 files. (1 skipped: 1… | Write docstrings for the functions missing them to satisfy the coverage threshold. |
✅ Passed checks (4 passed)
| Check name | Status | Explanation |
|---|---|---|
| Linked Issues check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
| Title check | ✅ Passed | The title clearly summarises the main changes: it unblocks commits, repairs the lockfile, and adds CI enforcement. |
| Description check | ✅ Passed | The description is directly related to the changeset and provides detailed context for the validator fixes, lockfile repair, CI gate, scope, and testing. |
Full details: Docstring Coverage
Explanation
Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 files. (1 skipped: 1 unsupported.)
- Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
- Create stacked PR
- Commit on current branch
🛠️ Fix failing CI checks 💡
- Create stacked PR
- Commit on current branch
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
A rabbit checks the headers bright
Generated pages skip the fight
Source files keep their SPDX sign
Three registry hashes now align
The burrow builds with cleaner light
Comment @coderabbitai help to get the list of available commands.
Dependabot bumped github/codeql-action from cdf488f5 to b96794f0 inside codeql-reusable.yml, scorecard-reusable.yml and hypatia-scan-reusable.yml on 09-12 but never regenerated .github/workflows/actions.lock. GitHub validates a reusable workflow against the CALLEE repo's own actions.lock, so every caller of those three reusables -- the whole 159-repo population -- failed with `failure` (not `startup_failure`), 0 jobs, and name == path. REST and GraphQL carry no reason for this; only the run page shows it. actionlint passes on the files either way, which is why it went unnoticed. Repair is lockfile-only and deliberately scoped to the three affected workflows, passed as explicit paths so the tool never sees the others: gh actions-lock <the three> --no-migrate-local-actions --no-narrow Two refs stay absent from the lock ON PURPOSE, not by oversight: denoland/setup-deno@22d081ff (governance-reusable.yml) Keying it would make a BANNED runtime a required lock key for every caller. Estate doctrine is bun-only; the cure is to remove the consumer, not to satisfy it. Tracked separately. hyperpolymath/a2ml-ecosystem/secrets-check-action@f7a40a4d A2ML is dead; do not connect new machinery to it. Control, per the known rewrite hazard in this tool: the *.yml diff is EMPTY. `gh actions-lock` in fix mode prepends a duplicate "managed by gh actions-lock" banner above the SPDX line (its idempotence check reads only line 1 -- scorecard-reusable.yml already carries the banner on both line 1 and line 3 from a previous run); those two cosmetic hunks were reverted so this commit touches the lockfile alone. Measured: `gh actions-lock --verify-local --no-migrate-local-actions` went from 4 of 45 workflows failing (5 stale) to 1 of 45 (2 stale). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HfgwLCdKNd5iZVo6VTiSim
check-suite-monitor.yml had NO section in .github/workflows/actions.lock at all -- it was never onboarded, so actions/github-script@3a2844b7 and actions/checkout@3d3c42e5 were unlocked. Same failure class as the codeql-action desync in the previous commit, different cause: not a Dependabot bump that skipped the lock, but a workflow added without one. security-gate-pr-target.yml is the other github-script consumer and is ALSO unlocked, but it is deliberately left alone: it uses the dead hyperpolymath/a2ml-ecosystem/secrets-check-action, and onboarding it would key that action into the lock. A2ML is dead; the cure is to remove the consumer, not to lock it in. Same reasoning as denoland/setup-deno. Lockfile-only; the *.yml diff is empty (the tool's duplicate banner insertion was reverted). The actions/checkout dependency record gains ref: v7.0.1 in place of a bare SHA -- a symbolic-ref annotation, not a de-pin: the commit field is unchanged and no workflow ref moved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HfgwLCdKNd5iZVo6VTiSim
Adds .githooks/validate-actions-lock.sh (uses subset-of actions.lock) and
wires it into BOTH .githooks/pre-commit and a new CI workflow.
CI is the placement that matters. Every validator in .githooks ran only
in pre-commit, and no workflow in this repo invoked any of them -- so
Dependabot, which never runs pre-commit, bypassed the entire suite. That
is precisely how the codeql-action bump reached three reusable workflows
without the lockfile, poisoning every caller. A pre-commit hook cannot
gate a bot.
Why not `gh actions-lock --verify-local`, measured on v0.1.6:
* It WRITES. It rewrote `uses: ./.github/actions/signed-push` to the
invalid `uses: $/.github/actions/signed-push` -- a ref that kills the
workflow at startup -- while in a mode its own help text calls
read-only and "ideal for pre-commit hooks".
* --no-migrate-local-actions stops that, but then the tool no longer
descends into local composite actions and misreports their real
dependencies as stale.
* Its coverage is REPO-scoped, not SHA-exact: bumping ONE of a
workflow's two refs to the same action leaves the old key still
referenced by the other, so nothing is stale, nothing is missing,
the check is green and the workflow is broken. That mutant survives
the tool and dies here.
Mutation-tested rather than merely run; 5 killed, 2 negative controls:
M1 partial bump (the one the tool misses) rc=1 KILLED
M2 full Dependabot-shaped bump rc=1 KILLED
M3 new unlocked action added rc=1 KILLED
M4 lockfile key deleted rc=1 KILLED
M6 empty lockfile (parser control) rc=1 KILLED
N1 case flip Swatinem -> SWATINEM rc=0 green
N2 reusable-workflow ref bumped (out of scope) rc=0 green
The script states its scope instead of implying it, counts what it
checked so a silent collapse to zero is visible, and fails closed if it
parses no keys -- an empty lockfile reports a PARSER failure, never
"every ref is missing".
Two refs are allow-listed as deliberately absent, in data with reasons,
not by pattern: denoland/setup-deno (banned runtime -- remove the
consumer, do not lock it in) and hyperpolymath/a2ml-ecosystem (dead).
An allow-list entry no longer used is reported stale, so it cannot rot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HfgwLCdKNd5iZVo6VTiSim
Two defects in the gate as first landed. 1. The `paths:` filter makes it unrequirable. A required check whose workflow is skipped by path filtering never reports a conclusion, so the PR sits on "Expected -- waiting for status" indefinitely: every PR that does not touch .github/** becomes permanently unmergeable. The gate is worth nothing until it is required, and it cannot be required with the filter on. The job is a checkout plus a few seconds of bash; it now runs on every PR and every push to main. 2. The validator scanned only .github/workflows/. The two dependencies of the local composite action .github/actions/signed-push/action.yml are keyed in the lockfile, and Dependabot bumps them like any other ref -- so a bump there drifted the lock with the gate looking the other way. Scanning .github/actions/*/action.yml closes that: the ref count goes 21 to 23, and a mutant bumping push-signed-commits inside the composite action now dies (it survived before). Also states the gate's own limit in its header: membership is global, not per-workflow-section, so a workflow carrying SHA-pinned refs but no lock section of its own is not detected here. No `uses:` ref moved, so actions.lock is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HfgwLCdKNd5iZVo6VTiSim
|
The read half of the estate spine, which this plan has said ships first since
it was written. One page answering the question the CI/CD campaign opened with:
which of 441 live repos are actually gated, actually reporting, and actually
pinned to something that exists.
WHAT LANDS
docs/ESTATE-BOARD.adoc 441 repos, one row each
.machine_readable/estate-board.json the same, machine-readable
.machine_readable/estate-board.tsv the same, joinable
.machine_readable/estate-residue-ledger.tsv the repair surface, 85 rows
scripts/spine/board.sh board.awk the generator
scripts/spine/verify-board.sh the verifier
Every cell carries its horizon and its measurement date. Disk-derived columns
aggregate UNION over files and MAX over counts across a repo's local checkouts,
never SUM -- a repo with four checkouts is one repo, not four.
MEASURED, NOT ASSERTED (origin/HEAD, 2026-09-15)
workflow soundness 8334 OK / 58 UNPARSEABLE / 7 NOJOBS of 8399 = 99.2%
reusable pin state 1965 pinned / 46 unpinned refs, canonicalising to
15 refs across 9 repos
Actions posture n=440, zero errors; no repo has an empty
patterns_allowed (floor 92); zero repos are
verified_allowed:false
residue ledger 85 rows, 69 PENDING, 16 DO-NOT-TOUCH
The residue is 60 unsound files plus 9 repos with unpinned reusable refs --
roughly 47 repos, not 441. That is the number the next campaign drives to zero.
VERIFIED, NOT MERELY PRESENT
scripts/spine/verify-board.sh: 16 assertions, all passing, including a
RED-THEN-GREEN fixture. A deliberately unsound workflow file and a
deliberately unpinned reusable ref are planted into a copy of the input set;
both must appear in the fixture ledger with the right class, and both must be
absent from the real one, BEFORE any "the board finds no X" claim is
admissible. Soundness is the two-limb test -- a non-empty jobs map AND a
trigger key -- never parseability, and never CI colour: a broken workflow
emits no check run, so a destroyed repo scores greener than a healthy one.
The verifier's JSON half recounts the board's own totals from the TSV with a
second instrument (jq reads the JSON, awk recounts the TSV) rather than
trusting the generator's arithmetic.
A BEHAVIOUR CHANGE TO A GATE, STATED AS ONE
.githooks/validate-spdx.sh drops *.json from is_source_file.
The test is `head -10 "$file" | grep -qE '^# SPDX-License-Identifier:'`.
JSON has no '#' comment syntax, so this rule was unsatisfiable by
construction, and it was never satisfied: both .machine_readable/*.json
files already on main -- hypatia-baseline.schema.json and
scorecard/scorecard.schema.json -- carry zero such headers. Removing the
extension restores truth rather than weakening a check, and it unblocks
every future commit touching a JSON file in this repo.
Proven both ways before the change was kept: the validator as it stands on
main exits 1 on this commit's file list, naming estate-board.json; with
*.json dropped it exits 0. Same defect class as the staged-mode filter bug
fixed in #804, one extension over.
.machine_readable/REGISTRY.a2ml was regenerated to clear build-registry
drift and is deliberately LEFT UNSTAGED -- staging it is what creates the
commit deadlock this repo has hit before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GEWfbxba7BJaTFRFhCGNF
…810) Ships the **ESTATE-BOARD** — the read half of the propagation spine, and the closure deliverable under owner ruling **CICD-R-W1**. Zero writes to any repo other than this one. ## What the board answers One page, one row per repo, for the question that started this campaign: *which of 441 repos are actually gated, actually reporting, and actually pinned to something that exists.* Every cell carries its horizon and its measurement date. Measured at `origin/HEAD`, whole population, every file parsed — not sampled: | | | |---|---| | repos on the board | **441** | | workflow files | **6,551** | | unsound workflow files | **60** | | unpinned reusable refs | **15** (canonicalised from 46 raw refs across 9 repos) | | repos with no local checkout | **8** | | residue ledger rows PENDING | **69** = 60 unsound files + 9 unpinned-reusable repos | The soundness test is the two-limb one, never parseability: a workflow is healthy iff `jobs` is a **non-empty map** *and* a trigger key is present. Parseability is not the test — a YAML file whose `jobs:` has been swallowed into a multi-line `name` scalar parses perfectly and is completely inert. ## ⚠ Behaviour change to a gate **This PR changes the behaviour of `.githooks/validate-spdx.sh`: it drops `*.json` from `is_source_file`.** That is a deliberate behaviour change to a gate, made under owner ruling **CICD-R-W6**, and it is called out here rather than buried in the diff. The rule was **unsatisfiable by construction**. The test is ```sh head -10 "$file" | grep -qE '^# SPDX-License-Identifier:' ``` and **JSON has no `#` comment syntax**, so no JSON file can ever satisfy it. **Control, measured on `main`:** the two `.machine_readable/*.json` files already committed — `hypatia-baseline.schema.json` and `scorecard/scorecard.schema.json` — carry **zero** SPDX headers between them. The rule has never been satisfiable and has never been satisfied. Removing `*.json` therefore **restores truth rather than weakening a check**, and it completes the repair #804 began, one extension over: that was the staged-mode filter bug; this is the same defect class in the extension allowlist itself. **Proven red-then-green, not asserted:** the validator as it stands on `main` exits **1**, naming `.machine_readable/estate-board.json`; with `*.json` dropped it exits **0**. That same probe also proved the `# SPDX-License-Identifier: MPL-2.0` headers added to `board.sh`, `board.awk` and `verify-board.sh` were both necessary and sufficient. ## The verifier, and the fixture that fired `scripts/spine/verify-board.sh` is committed alongside the generator and **was run**: **16 assertions passed, 0 failed, exit 0.** It is not a set of inert assertions. Two things are worth naming: - **Red-then-green fixture.** A known-unsound workflow (`ZZ-PLANTED-CONTROL.yml`) and a known unpinned reusable ref are planted, and the board must classify **both** correctly and ledger them — *and* both must be **absent** from the real ledger. Both fired. This satisfies §11.8 criteria 2 and 5 by measurement rather than by assertion. - **Independent recount, two instruments.** The JSON's own six totals are read with `jq`; the same six figures are recomputed from the TSV with `awk`. They agree (`rows=441 wf=6551 unsound=60 unpinned=15 nocheckout=8`). The `jq`-missing branch **fails** rather than skipping, so the JSON half of the verifier cannot pass vacuously. `verify-board.sh` uses `jq` and `awk` only — **no Python**, consistent with the estate-wide ban on tracked `.py` files. `board.sh` and `verify-board.sh` are committed **100755**; a suite committed `100644` passes every local run and dies in CI at exit 126 before one control runs. ## Files | File | Purpose | |---|---| | `docs/ESTATE-BOARD.adoc` | the board you read | | `.machine_readable/estate-board.json` | the same data, machine-readable | | `.machine_readable/estate-board.tsv` | one row per repo, 15 columns | | `.machine_readable/estate-residue-ledger.tsv` | 69 PENDING rows — the number that goes to zero | | `scripts/spine/board.sh` + `board.awk` | the generator | | `scripts/spine/verify-board.sh` | the verifier, 16 assertions | | `.githooks/validate-spdx.sh` | the gate change described above | ## Merge honesty `main` carries the `Optimus-Branch` ruleset with nine rule types, three of which **no PR in this backlog can satisfy**: `code_coverage` demands `minimum_coverage: 95` on a shell/AsciiDoc repo, `required_deployments` demands the `copilot` and `github-pages` environments, and the required `CodeQL` context is emitted only by GitHub default code-scanning setup. **This PR will land by admin bypass, and that is stated as a bypass rather than reported as a gate passed.** The commit is **signed** (`%G?` = `G`) and was made with **no `--no-verify`** — all nine pre-commit validators passed on their merits. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_014GEWfbxba7BJaTFRFhCGNF Co-authored-by: Claude Opus 5 <noreply@anthropic.com>



What this is
Four commits that take
standardsfrom "cannot accept any commit through its ownhooks" to "the lockfile is repaired and a gate stops it drifting again".
They are ordered because each one unblocks the next.
8b5c9f2e4f6e55codeql-action— un-poisons every callera85c0a1check-suite-monitor.yml, never locked at all75fea61uses ⊆ actions.lockgate, in CI as well as pre-commit1 — The deadlock (
8b5c9f2)Two gates in
.githooks/pre-commitcould not both be satisfied:build-registry.sh --checkfailed every commit until you regenerated and staged.machine_readable/REGISTRY.a2ml;validate-a2ml.shthen rejected that very file.No ordering satisfied both.
--no-verifywas the only exit, which is why nothing in.githookswas really enforcing anything.Separately,
validate-spdx.shapplied its extension allowlist in scan mode but not instaged mode, so any commit touching a non-source file — including the machine-generated
actions.lock— was judged by a rule written for source files and failed.Cures: exempt files whose own header says
GENERATED FILE ... DO NOT EDIT BY HAND(anchoredregex, so prose cannot spoof it), and apply the same extension predicate in both modes.
2 — The estate-wide poison (
e4f6e55)GitHub validates a reusable workflow against the callee repo's own
actions.lock.Dependabot bumped
github/codeql-actionfromcdf488f5tob96794f0insidecodeql-reusable.yml,scorecard-reusable.ymlandhypatia-scan-reusable.ymland neverregenerated the lockfile. So every repo calling those three failed —
failure, notstartup_failure, 0 jobs,name == path, and no reason in either the REST or the GraphQLpayload; only the run page says why.
actionlintpasses on the files either way.One lockfile commit repairs all of them.
3 — A workflow that was never locked (
a85c0a1)check-suite-monitor.ymlhad no section in the lockfile at all. Same class, differentcause: not a bump that skipped the lock, but a workflow added without one.
security-gate-pr-target.ymlis the other unlocked workflow and is deliberately leftalone — see doctrine below.
4 — The gate (
75fea61)Every validator in
.githooksran only in pre-commit, and no workflow invoked any ofthem. Dependabot never runs pre-commit, so it bypassed the entire suite. That is the actual
root cause, and it needs CI, not a hook.
Why not
gh actions-lock --verify-localMeasured on v0.1.6, and both findings are why the new check is hand-written:
uses: ./.github/actions/signed-pushto the invaliduses: $/.github/actions/signed-push— a ref that kills the workflow at startup — while ina mode its own help text calls read-only and "ideal for pre-commit hooks".
--no-migrate-local-actionssuppresses it, but then the tool stops descending into localcomposite actions and misreports their real dependencies as stale.
same action and the old key is still referenced by the other: nothing stale, nothing
missing, check green, workflow broken.
Mutation-tested, not merely run
Swatinem→SWATINEMThe script states its scope rather than implying it, prints how many refs it checked so a
silent collapse to zero is visible, and fails closed on a parse: an empty lockfile reports a
parser failure, never "every ref is missing".
Deliberately not fixed here
Two refs stay absent from the lockfile by doctrine, allow-listed in data with reasons:
denoland/setup-deno@22d081ff(governance-reusable.yml) — the estate is bun-only;deno is banned. Keying it would make a banned runtime a required lockfile key for every
caller. The cure is to remove the consumer, not to satisfy it.
hyperpolymath/a2ml-ecosystem/secrets-check-action@f7a40a4d(
security-gate-pr-target.yml) — A2ML is dead; do not connect new machinery to it.An allow-list entry that stops being used is reported as stale, so the list cannot rot.
Also untouched, and worth separate issues:
was already red at HEAD, and my patched output is byte-identical).
validate-a2ml.sh'sagent-id/pedigreechecks match 0 of 222 tracked files inboth syntaxes — a requirement nothing satisfies.
run_validatorreturns 0 when a validator file is missing — a silent skip that reads green.gh actions-lockprepends a duplicate "managed by gh actions-lock" banner above the SPDXline on every run (its idempotence check reads only line 1;
scorecard-reusable.ymlalreadycarries it on both line 1 and line 3). Those cosmetic hunks were reverted so each lockfile
commit touches the lockfile alone.
What this gate does NOT catch -- stated, not implied
section per workflow, which implies GitHub validates per section; this check only asks
whether a ref appears somewhere in the lock. A workflow carrying SHA-pinned refs but
no lock section of its own therefore passes.
check-suite-monitor.ymlwas caught hereonly because
github-scriptwas globally absent -- had it usedcheckoutalone, itwould have gone undetected. Narrowing to per-section membership is a separate change.
validate-sha-pins.showns that question), as arereusable-workflow refs, which the lockfile keys no entry for at all.
Control
Every lockfile commit was made with the
*.ymldiff empty — verified withgit diff --stat -- '.github/workflows/*.yml'— because this tool is known to de-pin SHAs totags and invent invalid local-action refs when it rewrites workflows.
🤖 Generated with Claude Code
https://claude.ai/code/session_01HfgwLCdKNd5iZVo6VTiSim