Skip to content

fix(standards): unblock commits, repair the lockfile, gate it in CI - #804

Merged
hyperpolymath merged 5 commits into
mainfrom
fix/unblock-standards-precommit-20260915
Sep 15, 2026
Merged

hyperpolymath merged 5 commits into
mainfrom
fix/unblock-standards-precommit-20260915

Conversation

@hyperpolymath

@hyperpolymath hyperpolymath commented Sep 15, 2026 •

Copy link
Copy Markdown
Owner

What this is

Four commits that take standards from "cannot accept any commit through its own
hooks"
to "the lockfile is repaired and a gate stops it drifting again".

They are ordered because each one unblocks the next.

commit effect
1 8b5c9f2 unblocks commits — two staged-mode validator defects
2 e4f6e55 re-keys codeql-action — un-poisons every caller
3 a85c0a1 onboards check-suite-monitor.yml, never locked at all
4 75fea61 adds the uses ⊆ actions.lock gate, in CI as well as pre-commit

1 — The deadlock (8b5c9f2)

Two gates in .githooks/pre-commit could not both be satisfied:

  • build-registry.sh --check failed every commit until you regenerated and staged
    .machine_readable/REGISTRY.a2ml;
  • validate-a2ml.sh then rejected that very file.

No ordering satisfied both. --no-verify was the only exit, which is why nothing in
.githooks was really enforcing anything.

Separately, validate-spdx.sh applied its extension allowlist in scan mode but not in
staged 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 (anchored
regex, 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-action from cdf488f5 to b96794f0 inside
codeql-reusable.yml, scorecard-reusable.yml and hypatia-scan-reusable.yml and never
regenerated the lockfile. So every repo calling those three failed — failure, not
startup_failure, 0 jobs, name == path, and no reason in either the REST or the GraphQL
payload
; only the run page says why. actionlint passes on the files either way.

One lockfile commit repairs all of them.

3 — A workflow that was never locked (a85c0a1)

check-suite-monitor.yml had no section in the lockfile at all. Same class, different
cause: not a bump that skipped the lock, but a workflow added without one.

security-gate-pr-target.yml is the other unlocked workflow and is deliberately left
alone
— see doctrine below.

4 — The gate (75fea61)

Every validator in .githooks ran only in pre-commit, and no workflow invoked any of
them. 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-local

Measured on v0.1.6, and both findings are why the new check is hand-written:

  • 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 suppresses it, but then the tool stops descending into local
    composite actions and misreports their real dependencies as stale.
  • Its coverage is repo-scoped, not SHA-exact. Bump one of a workflow's two refs to the
    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

mutant rc
M1 partial bump — the one the tool misses 1 KILLED
M2 full Dependabot-shaped bump 1 KILLED
M3 new unlocked action added 1 KILLED
M4 lockfile key deleted 1 KILLED
M6 empty lockfile (parser control) 1 KILLED
N1 case flip Swatinem → SWATINEM 0 green, correct
N2 reusable-workflow ref bumped (out of scope) 0 green, by design

The 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:

  • 215 pre-existing SPDX header failures in scan mode (unchanged by this PR — scan mode
    was already red at HEAD, and my patched output is byte-identical).
  • validate-a2ml.sh's agent-id / pedigree checks match 0 of 222 tracked files in
    both syntaxes — a requirement nothing satisfies.
  • run_validator returns 0 when a validator file is missing — a silent skip that reads green.
  • gh actions-lock prepends a duplicate "managed by gh actions-lock" banner above the SPDX
    line on every run (its idempotence check reads only line 1; scorecard-reusable.yml already
    carries 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

  • Membership is global, not per-workflow-section. The lockfile groups keys under a
    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.yml was caught here
    only because github-script was globally absent -- had it used checkout alone, it
    would have gone undetected. Narrowing to per-section membership is a separate change.
  • Tag refs are out of scope (validate-sha-pins.sh owns that question), as are
    reusable-workflow refs, which the lockfile keys no entry for at all.

Control

Every lockfile commit was made with the *.yml diff empty — verified with
git diff --stat -- '.github/workflows/*.yml' — because this tool is known to de-pin SHAs to
tags and invent invalid local-action refs when it rewrites workflows.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HfgwLCdKNd5iZVo6VTiSim

`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
@coderabbitai

coderabbitai Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 99562238-0dbd-462c-bd73-4ce569c14d46

📥 Commits

Reviewing files that changed from the base of the PR and between 317101e and 8b5c9f2.

📒 Files selected for processing (3)
  • .githooks/validate-a2ml.sh
  • .githooks/validate-spdx.sh
  • .machine_readable/REGISTRY.a2ml

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)
  • GitHub Check: Repo self-tests
⚠️ CI failures not shown inline (6)

GitHub Actions: Secret Scanner / 0_scan _ rust-secrets.txt: fix(hooks): unblock commits blocked by staged-mode validator defects

Conclusion: failure

View job details

##[group]Run TODAY="${RUST_TODAY:-$(date -u +%Y-%m-%d)}"
 �[36;1mTODAY="${RUST_TODAY:-$(date -u +%Y-%m-%d)}"�[0m
 �[36;1m�[0m
 �[36;1m# An unparseable cutoff would pick the warn branch forever, silently�[0m
 �[36;1m# disarming the widened scan. Refuse to run instead.�[0m
 �[36;1mrequire_date() {�[0m
 �[36;1m  case "$2" in�[0m
 �[36;1m    [0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]) : ;;�[0m
 �[36;1m    *) echo "::error::rust-secrets: $1='$2' is not YYYY-MM-DD."�[0m

GitHub Actions: Secret Scanner / scan _ rust-secrets: fix(hooks): unblock commits blocked by staged-mode validator defects

Conclusion: failure

View job details

##[group]Run TODAY="${RUST_TODAY:-$(date -u +%Y-%m-%d)}"
 �[36;1mTODAY="${RUST_TODAY:-$(date -u +%Y-%m-%d)}"�[0m
 �[36;1m�[0m
 �[36;1m# An unparseable cutoff would pick the warn branch forever, silently�[0m
 �[36;1m# disarming the widened scan. Refuse to run instead.�[0m
 �[36;1mrequire_date() {�[0m
 �[36;1m  case "$2" in�[0m
 �[36;1m    [0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]) : ;;�[0m
 �[36;1m    *) echo "::error::rust-secrets: $1='$2' is not YYYY-MM-DD."�[0m

GitHub Actions: Secret Scanner / 1_scan _ shell-secrets.txt: fix(hooks): unblock commits blocked by staged-mode validator defects

Conclusion: failure

View job details

##[group]Run # Patterns: an `export FOO=` or `FOO=` with a quoted literal of meaningful length.
 �[36;1m# Patterns: an `export FOO=` or `FOO=` with a quoted literal of meaningful length.�[0m
 �[36;1m# Restricted to *_TOKEN / *_KEY / *_SECRET / PASSWORD to keep false-positives low.�[0m
 �[36;1mPATTERNS=(�[0m
 �[36;1m  '(export[[:space:]]+)?[A-Z_]*TOKEN[A-Z_]*=["'"'"'][A-Za-z0-9_./+=-]{20,}["'"'"']'�[0m
 �[36;1m  '(export[[:space:]]+)?[A-Z_]*API_KEY[A-Z_]*=["'"'"'][A-Za-z0-9_./+=-]{20,}["'"'"']'�[0m
 �[36;1m  '(export[[:space:]]+)?[A-Z_]*SECRET[A-Z_]*=["'"'"'][A-Za-z0-9_./+=-]{16,}["'"'"']'�[0m
 �[36;1m  '(export[[:space:]]+)?***"'"'"'][^"'"'"']{6,}["'"'"']'�[0m
 �[36;1m)�[0m
 �[36;1m�[0m
 �[36;1m# Inline pragma patterns — suppress a hit when found on the same or�[0m
 �[36;1m# immediately preceding line.�[0m
 �[36;1mPRAGMA_RE='(scanner-allow:[[:space:]]*shell-secrets|hypatia:[[:space:]]*allow[[:space:]]+security_errors/secret_detected)'�[0m
 �[36;1m�[0m
 �[36;1m# Param-expansion RHS pattern — assignments whose value is a variable�[0m
 �[36;1m# reference rather than a literal are never real secrets.�[0m
 �[36;1m# Matches: ="$VAR"  ="${VAR}"  ="${VAR:-…}"  ="${VAR:?…}"  ='${VAR}'  =$VAR�[0m
 �[36;1mPARAM_EXPANSION_RE='=['"'"'"'"'"']?\$\{?[A-Za-z_][A-Za-z0-9_]*(:[?-][^}]*)?\}?['"'"'"'"'"']?[[:space:]]*(#.*)?$'�[0m
 �[36;1m�[0m
 �[36;1m# Load per-repo ignore globs from .shell-secrets-ignore if present.�[0m
 �[36;1mIGNORE_GLOBS=()�[0m
 �[36;1mif [[ -f .shell-secrets-ignore ]]; then�[0m
 �[36;1m  while IFS= read -r line || [[ -n "$line" ]]; do�[0m
 �[36;1m    # Skip blank lines and comments�[0m
 �[36;1m    [[ -z "$line" || "$line" == \#* ]] && continue�[0m
 �[36;1m    IGNORE_GLOBS+=("$line")�[0m
 �[36;1m  done < .shell-secrets-ignore�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1m# is_ignored <filepath> — returns 0 (true) if path matches any ignore glob.�[0m
 �[36;1mis_ignored() {�[0m
 �[36;1m  local path="$1"�[0m
 �[36;1m  for glob in "${IGNORE_GLOBS[@]}"; do�[0m
 �[36;1m    #...

GitHub Actions: Secret Scanner / scan _ shell-secrets: fix(hooks): unblock commits blocked by staged-mode validator defects

Conclusion: failure

View job details

##[group]Run # Patterns: an `export FOO=` or `FOO=` with a quoted literal of meaningful length.
 �[36;1m# Patterns: an `export FOO=` or `FOO=` with a quoted literal of meaningful length.�[0m
 �[36;1m# Restricted to *_TOKEN / *_KEY / *_SECRET / PASSWORD to keep false-positives low.�[0m
 �[36;1mPATTERNS=(�[0m
 �[36;1m  '(export[[:space:]]+)?[A-Z_]*TOKEN[A-Z_]*=["'"'"'][A-Za-z0-9_./+=-]{20,}["'"'"']'�[0m
 �[36;1m  '(export[[:space:]]+)?[A-Z_]*API_KEY[A-Z_]*=["'"'"'][A-Za-z0-9_./+=-]{20,}["'"'"']'�[0m
 �[36;1m  '(export[[:space:]]+)?[A-Z_]*SECRET[A-Z_]*=["'"'"'][A-Za-z0-9_./+=-]{16,}["'"'"']'�[0m
 �[36;1m  '(export[[:space:]]+)?***"'"'"'][^"'"'"']{6,}["'"'"']'�[0m
 �[36;1m)�[0m
 �[36;1m�[0m
 �[36;1m# Inline pragma patterns — suppress a hit when found on the same or�[0m
 �[36;1m# immediately preceding line.�[0m
 �[36;1mPRAGMA_RE='(scanner-allow:[[:space:]]*shell-secrets|hypatia:[[:space:]]*allow[[:space:]]+security_errors/secret_detected)'�[0m
 �[36;1m�[0m
 �[36;1m# Param-expansion RHS pattern — assignments whose value is a variable�[0m
 �[36;1m# reference rather than a literal are never real secrets.�[0m
 �[36;1m# Matches: ="$VAR"  ="${VAR}"  ="${VAR:-…}"  ="${VAR:?…}"  ='${VAR}'  =$VAR�[0m
 �[36;1mPARAM_EXPANSION_RE='=['"'"'"'"'"']?\$\{?[A-Za-z_][A-Za-z0-9_]*(:[?-][^}]*)?\}?['"'"'"'"'"']?[[:space:]]*(#.*)?$'�[0m
 �[36;1m�[0m
 �[36;1m# Load per-repo ignore globs from .shell-secrets-ignore if present.�[0m
 �[36;1mIGNORE_GLOBS=()�[0m
 �[36;1mif [[ -f .shell-secrets-ignore ]]; then�[0m
 �[36;1m  while IFS= read -r line || [[ -n "$line" ]]; do�[0m
 �[36;1m    # Skip blank lines and comments�[0m
 �[36;1m    [[ -z "$line" || "$line" == \#* ]] && continue�[0m
 �[36;1m    IGNORE_GLOBS+=("$line")�[0m
 �[36;1m  done < .shell-secrets-ignore�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1m# is_ignored <filepath> — returns 0 (true) if path matches any ignore glob.�[0m
 �[36;1mis_ignored() {�[0m
 �[36;1m  local path="$1"�[0m
 �[36;1m  for glob in "${IGNORE_GLOBS[@]}"; do�[0m
 �[36;1m    #...

GitHub Actions: Secret Scanner / 2_scan _ gitleaks.txt: fix(hooks): unblock commits blocked by staged-mode validator defects

Conclusion: failure

View job details

##[group]Run set -euo pipefail
 �[36;1mset -euo pipefail�[0m
 �[36;1m�[0m
 �[36;1m# fetch-depth: 0 on the checkout is load-bearing HERE. If it ever�[0m
 �[36;1m# regresses to the default depth-1 clone, detect would walk a single�[0m
 �[36;1m# commit, find nothing and report a pass — a gate that cannot fail.�[0m
 �[36;1m# Assert completeness from git itself: gitleaks' own "scanned N�[0m
 �[36;1m# commits" line under-reports and is not proof of depth.�[0m
 �[36;1mif [ "$(git rev-parse --is-shallow-repository)" != "false" ]; then�[0m
 �[36;1m  echo "::error::checkout is shallow -- a history scan here would be vacuous; refusing to report a pass"�[0m

GitHub Actions: Secret Scanner / scan _ gitleaks: fix(hooks): unblock commits blocked by staged-mode validator defects

Conclusion: failure

View job details

##[group]Run set -euo pipefail
 �[36;1mset -euo pipefail�[0m
 �[36;1m�[0m
 �[36;1m# fetch-depth: 0 on the checkout is load-bearing HERE. If it ever�[0m
 �[36;1m# regresses to the default depth-1 clone, detect would walk a single�[0m
 �[36;1m# commit, find nothing and report a pass — a gate that cannot fail.�[0m
 �[36;1m# Assert completeness from git itself: gitleaks' own "scanned N�[0m
 �[36;1m# commits" line under-reports and is not proof of depth.�[0m
 �[36;1mif [ "$(git rev-parse --is-shallow-repository)" != "false" ]; then�[0m
 �[36;1m  echo "::error::checkout is shallow -- a history scan here would be vacuous; refusing to report a pass"�[0m
🔇 Additional comments (3)
.githooks/validate-a2ml.sh (1)

14-22: LGTM!

.githooks/validate-spdx.sh (1)

10-25: LGTM!

Also applies to: 41-41

.machine_readable/REGISTRY.a2ml (1)

66-66: LGTM!

Also applies to: 129-129, 210-210


📝 Summary

Summary by CodeRabbit

  • Bug Fixes

    • Validation now recognises generated manifests and avoids applying manual-edit checks to them.
    • SPDX checks now apply only to supported source files, preventing irrelevant validation failures.
  • Chores

    • Updated the machine-readable registry index to reflect current source contents for three specifications.

Walkthrough

The A2ML validator now skips marked generated manifests. The SPDX validator now checks only recognised source files. The registry updates source hashes for three existing specifications.

Changes

Validation and registry updates

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 ⚠️ Warning 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.

❤️ Share

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.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Sep 15, 2026
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
coderabbitai[bot]
coderabbitai Bot previously approved these changes Sep 15, 2026
hyperpolymath and others added 2 commits September 15, 2026 09:17
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
@hyperpolymath hyperpolymath changed the title fix(hooks): unblock commits blocked by staged-mode validator defects fix(standards): unblock commits, repair the lockfile, gate it in CI Sep 15, 2026
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
@sonarqubecloud

Copy link
Copy Markdown

@hyperpolymath
hyperpolymath merged commit 4c9225a into main Sep 15, 2026
27 of 30 checks passed
@hyperpolymath
hyperpolymath deleted the fix/unblock-standards-precommit-20260915 branch September 15, 2026 09:22
hyperpolymath added a commit that referenced this pull request Sep 15, 2026
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
hyperpolymath added a commit that referenced this pull request Sep 15, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant