Skip to content

Commit 4dac1dd

Browse files
claudehyperpolymath
authored andcommitted
feat(wave8): tighten the gates — three advisory surfaces become real gates
Promotes the enforcement surfaces that could not block, each honestly (no manufactured red noise), each with a red-team proof it CAN fail. - hypatia-scan-reusable.yml: baseline-aware gate. When a caller commits .hypatia-baseline.json (+ ships scripts/apply-baseline.sh), findings are filtered through the baseline and any UNBASELINED finding >= high FAILS the job (apply-baseline.sh blocking mode — thresholds, expiry, downgrades all already implemented there). Repos without a baseline keep the honestly- labelled ADVISORY behaviour; the gate arms itself the moment a baseline lands (standards#399/#437/#446). - affinescript-verify.yml: SPLIT GATE replaces the blanket advisory. ADDED .affine files must compile — a failing added file fails the job (new code has no excuse); MODIFIED legacy ports stay advisory until the port backlog clears (LEGACY_BLOCKING flag to flip later); toolchain failures skip LOUDLY ("SKIPPED, not passed" — never a silent green claim). Job-level continue-on-error removed; verify step guards against a missing compiler checkout before cd. - registry-verify.yml: Mustfile enforcement now gates PRs (structural check + run-mustfile.sh) — previously the invariants executed only on push-to-main, so a breaking change was caught after merge, not before. - scripts/tests/wave8-gates-test.sh (11 assertions; Justfile: gates-test): unbaselined-high blocks / baselined passes / EXPIRED baseline entry stops suppressing / low severity below threshold; Mustfile gate passes on this repo and blocks on a failing critical invariant; workflow-shape checks (job-level continue-on-error gone, blocking branch present, loud skip). Licence/SPDX untouched (flag-only policy). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0114ps6mY5jAH4SzbGxeuYjc
1 parent 0bca951 commit 4dac1dd

5 files changed

Lines changed: 190 additions & 48 deletions

File tree

.github/workflows/affinescript-verify.yml

Lines changed: 74 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,20 @@ permissions:
2525
# compiler (hyperpolymath/affinescript). The compiler is pinned to a commit
2626
# SHA for reproducibility; bump COMPILER_REF deliberately.
2727
#
28-
# NON-BLOCKING (temporary): the initial ReScript->AffineScript port (PR #62)
29-
# was done without a compiler, so `affinescript check` currently reports
30-
# errors that need mechanical fixes. Until those are resolved this job
31-
# REPORTS failures (job summary + warnings) but exits green so it does not
32-
# block merges. Flip BLOCKING to "true" once the ports compile clean.
28+
# SPLIT GATE (Wave 8, standards#446 honest-gating): the initial
29+
# ReScript->AffineScript port (PR #62) predates the compiler, so some legacy
30+
# ports still fail `affinescript check`. Blindly flipping the whole job to
31+
# blocking would red every PR that touches a legacy port — manufactured noise.
32+
# Instead the gate distinguishes:
33+
# * ADDED .affine files (diff-filter=A) -> BLOCKING. New code has no excuse
34+
# not to compile; a failing added file fails this job.
35+
# * MODIFIED legacy files (diff-filter=CMR) -> advisory warnings until the
36+
# port backlog clears (flip LEGACY_BLOCKING=true then).
37+
# * Toolchain failures (compiler checkout/opam/build) -> advisory: the step
38+
# reports "toolchain unavailable" and the verify step SKIPS (loudly) rather
39+
# than blocking unrelated changes on OCaml infra flake.
3340
env:
34-
BLOCKING: "false"
41+
LEGACY_BLOCKING: "false"
3542
COMPILER_REPO: hyperpolymath/affinescript
3643
COMPILER_REF: d2875a552f1d389b4a60c4adfdc02ae53e36aca3
3744

@@ -40,12 +47,6 @@ jobs:
4047
timeout-minutes: 20
4148
name: AffineScript Verify
4249
runs-on: ubuntu-latest
43-
# advisory: see header note. continue-on-error keeps the
44-
# whole job advisory — including the compiler checkout/setup-ocaml/build
45-
# steps — so a toolchain/build problem cannot block merges or add
46-
# estate-wide red noise while the ports + build are sorted in follow-up.
47-
# Remove this (and flip BLOCKING=true) once the job is reliably green.
48-
continue-on-error: true
4950
permissions:
5051
contents: read
5152
steps:
@@ -66,15 +67,20 @@ jobs:
6667
|| printf '%s' "$BASE" | grep -qE '^0+$'; then
6768
BASE="$(git rev-parse HEAD^ 2>/dev/null || git rev-parse HEAD)"
6869
fi
69-
FILES="$(git diff --name-only --diff-filter=ACMR "$BASE" HEAD -- '*.affine' || true)"
70+
# Split-gate inputs: ADDED files gate hard; modified legacy files are
71+
# advisory until the port backlog clears (see header).
72+
ADDED="$(git diff --name-only --diff-filter=A "$BASE" HEAD -- '*.affine' || true)"
73+
MODIFIED="$(git diff --name-only --diff-filter=CMR "$BASE" HEAD -- '*.affine' || true)"
74+
FILES="$(printf '%s\n%s\n' "$ADDED" "$MODIFIED" | sed '/^$/d')"
7075
if [ -z "$FILES" ]; then
7176
echo "any=false" >> "$GITHUB_OUTPUT"
7277
echo "No changed .affine files — nothing to verify."
7378
else
7479
echo "any=true" >> "$GITHUB_OUTPUT"
75-
echo "Changed .affine files:"
80+
echo "Changed .affine files (added gate hard; modified advisory):"
7681
echo "$FILES"
7782
{ echo 'files<<EOF'; echo "$FILES"; echo 'EOF'; } >> "$GITHUB_OUTPUT"
83+
{ echo 'added<<EOF'; echo "$ADDED"; echo 'EOF'; } >> "$GITHUB_OUTPUT"
7884
fi
7985
8086
- name: Checkout AffineScript compiler
@@ -98,61 +104,86 @@ jobs:
98104
ocaml-compiler: "5.1"
99105

100106
- name: Build compiler
107+
id: build
101108
if: steps.changed.outputs.any == 'true'
102-
# advisory: compiler build failures are reported by this job, not yet
103-
# merge-blocking, until the report-only porting phase ends.
109+
# advisory: toolchain failures are infra flake, not code failures —
110+
# they must not block unrelated changes. The verify step below skips
111+
# LOUDLY (never a silent green claim) when the compiler is unavailable.
104112
continue-on-error: true
105113
working-directory: .affinescript-compiler
106114
run: |
107115
opam install . --deps-only
108116
opam exec -- dune build
117+
echo "ok=true" >> "$GITHUB_OUTPUT"
109118
110-
- name: Verify changed .affine files
119+
- name: Verify changed .affine files (added files gate; legacy advisory)
111120
if: steps.changed.outputs.any == 'true'
112-
# advisory: verification findings are emitted as warnings and job
113-
# summary entries until BLOCKING is intentionally enabled.
114-
continue-on-error: true
115-
working-directory: .affinescript-compiler
121+
# No working-directory: if the (advisory) compiler checkout failed, the
122+
# dir may not exist — the guard below must run BEFORE any cd.
116123
run: |
117124
set -u
118-
rc=0
119-
failed=""
125+
if [ "${{ steps.build.outputs.ok }}" != "true" ] || [ ! -d .affinescript-compiler ]; then
126+
echo "::warning::AffineScript toolchain unavailable (compiler checkout/opam/build failed) — verification SKIPPED, not passed."
127+
{
128+
echo "## AffineScript Verify"
129+
echo "⚠️ **SKIPPED — toolchain unavailable.** The compiler did not build;"
130+
echo "no \`.affine\` file was verified. This is an infra flake, not a pass."
131+
} >> "$GITHUB_STEP_SUMMARY"
132+
exit 0
133+
fi
134+
cd .affinescript-compiler
135+
# is_added <file>: is this file in the added set? (added files gate hard)
136+
is_added() {
137+
case $'\n'"${ADDED_SET}"$'\n' in *$'\n'"$1"$'\n'*) return 0 ;; *) return 1 ;; esac
138+
}
139+
ADDED_SET="$(cat <<'EOF'
140+
${{ steps.changed.outputs.added }}
141+
EOF
142+
)"
143+
ADDED_SET="$(printf '%s\n' "$ADDED_SET" | sed 's/^[[:space:]]*//' | sed '/^$/d')"
144+
added_fail=""
145+
legacy_fail=""
120146
while IFS= read -r f; do
121147
[ -z "$f" ] && continue
122148
abs="$GITHUB_WORKSPACE/$f"
123149
echo "::group::check $f"
124150
if opam exec -- dune exec affinescript -- check "$abs" 2>&1; then
125151
echo "✅ $f"
152+
elif is_added "$f"; then
153+
echo "::error file=$f::AffineScript check failed (ADDED file — blocking)"
154+
echo "❌ $f failed (added file — blocking)"
155+
added_fail="$added_fail$f"$'\n'
126156
else
127-
echo "::warning file=$f::AffineScript check failed"
128-
echo "❌ $f failed AffineScript check"
129-
failed="$failed$f"$'\n'
130-
rc=1
157+
echo "::warning file=$f::AffineScript check failed (legacy port — advisory until backlog clears)"
158+
echo "⚠️ $f failed (legacy port — advisory)"
159+
legacy_fail="$legacy_fail$f"$'\n'
131160
fi
132161
echo "::endgroup::"
133162
done <<'EOF'
134163
${{ steps.changed.outputs.files }}
135164
EOF
136165
137166
{
138-
echo "## AffineScript Verify"
139-
if [ "$rc" -eq 0 ]; then
167+
echo "## AffineScript Verify (split gate)"
168+
if [ -z "$added_fail" ] && [ -z "$legacy_fail" ]; then
140169
echo "All changed \`.affine\` files passed \`affinescript check\`."
141-
else
142-
echo "The following changed \`.affine\` files failed \`affinescript check\`:"
143-
echo ""
144-
echo "$failed" | sed '/^$/d' | sed 's/^/- /'
145-
echo ""
146-
echo "_See the per-file groups in the job log for the compiler errors._"
170+
fi
171+
if [ -n "$added_fail" ]; then
172+
echo "### ❌ ADDED files failing (BLOCKING — new code must compile)"
173+
echo "$added_fail" | sed '/^$/d' | sed 's/^/- /'
174+
fi
175+
if [ -n "$legacy_fail" ]; then
176+
echo "### ⚠️ Legacy ports failing (advisory until the port backlog clears)"
177+
echo "$legacy_fail" | sed '/^$/d' | sed 's/^/- /'
147178
fi
148179
} >> "$GITHUB_STEP_SUMMARY"
149180
150-
if [ "$rc" -ne 0 ]; then
151-
if [ "$BLOCKING" = "true" ]; then
152-
echo "AffineScript verification failed (blocking)."
153-
exit 1
154-
fi
155-
echo "::warning::AffineScript verification found errors but is non-blocking (BLOCKING=false). See job summary."
156-
exit 0
181+
if [ -n "$added_fail" ]; then
182+
echo "AffineScript verification failed for ADDED files (blocking)."
183+
exit 1
184+
fi
185+
if [ -n "$legacy_fail" ] && [ "$LEGACY_BLOCKING" = "true" ]; then
186+
echo "Legacy port failures are blocking (LEGACY_BLOCKING=true)."
187+
exit 1
157188
fi
158-
echo "All changed .affine files passed AffineScript verification."
189+
echo "Verification complete (added files clean)."

.github/workflows/hypatia-scan-reusable.yml

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,14 +149,30 @@ jobs:
149149
panic-attack-findings.json
150150
retention-days: 90
151151

152+
- name: Gate on baseline (blocking when a baseline is committed)
153+
if: steps.scan.outputs.findings_count > 0
154+
run: |
155+
# Wave-8 gate promotion: when the calling repo commits a
156+
# .hypatia-baseline.json (schema: .machine_readable/hypatia-baseline.
157+
# schema.json) AND ships scripts/apply-baseline.sh, findings are
158+
# filtered against the baseline and any UNBASELINED finding at or
159+
# above the blocking threshold FAILS this job. Repos without a
160+
# baseline keep the honest ADVISORY behaviour below — the gate arms
161+
# itself the moment a baseline lands (standards#399/#437/#446).
162+
if [ -f scripts/apply-baseline.sh ] && [ -f .hypatia-baseline.json ]; then
163+
echo "Baseline present — running BLOCKING gate (threshold: high)."
164+
bash scripts/apply-baseline.sh hypatia-findings.json .hypatia-baseline.json blocking
165+
else
166+
echo "No committed baseline — gate stays advisory (see next step)."
167+
fi
168+
152169
- name: Check for critical issues (ADVISORY — does not gate)
153170
if: steps.scan.outputs.critical > 0
154171
run: |
155-
# This scan is ADVISORY / fix-forward: it never fails the build. The
156-
# label and summary state that explicitly so a green check carrying
157-
# critical findings is not mistaken for "no critical findings".
158-
# (Promotion to blocking is tracked with the baseline reconciliation,
159-
# standards#399/#437.)
172+
# This scan is ADVISORY / fix-forward WHEN NO BASELINE IS COMMITTED
173+
# (the step above becomes the blocking gate once .hypatia-baseline.json
174+
# lands). The label and summary state that explicitly so a green check
175+
# carrying critical findings is not mistaken for "no critical findings".
160176
echo "::warning title=Hypatia (ADVISORY, non-blocking)::${{ steps.scan.outputs.critical }} critical finding(s). This scan does not gate — review hypatia-findings.json and fix forward."
161177
{
162178
echo "### Hypatia scan — ADVISORY (does not gate)"

.github/workflows/registry-verify.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ jobs:
5252
exit 1
5353
fi
5454
55+
- name: Mustfile enforcement (structural + execute invariants)
56+
run: |
57+
# Wave-8 gate promotion: the Mustfile's invariants previously executed
58+
# only on push-to-main (boj-build.yml). PRs now gate on them too —
59+
# a change that breaks a MUST invariant fails before merge, not after.
60+
bash scripts/check-mustfile-structure.sh
61+
bash scripts/run-mustfile.sh
62+
5563
- name: Verify compliance dashboard is current (and pass-checks hold)
5664
run: |
5765
# --verify RUNS every pass-row's executable `check`: a claimed pass

Justfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ false-green-test:
4949
automation-test:
5050
@bash scripts/tests/wave1-automation-test.sh
5151

52+
# Wave-8 gate-promotion regression: baseline gate + Mustfile PR gate can fail
53+
gates-test:
54+
@bash scripts/tests/wave8-gates-test.sh
55+
5256
# Structural validation of the Mustfile contract (severity + run/verification per check)
5357
mustfile-check path=".machine_readable/contractiles/must/Mustfile.a2ml":
5458
@bash scripts/check-mustfile-structure.sh "{{path}}"

scripts/tests/wave8-gates-test.sh

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: MPL-2.0
3+
set -uo pipefail
4+
#
5+
# Wave-8 "tighten the gates" regression test.
6+
#
7+
# Every promoted gate must demonstrably BLOCK on bad input and PASS on good:
8+
# * apply-baseline.sh blocking mode: unbaselined high/critical finding fails;
9+
# baselined finding passes; EXPIRED baseline entry no longer suppresses.
10+
# * Mustfile PR gate: the exact commands registry-verify.yml runs succeed on
11+
# this repo, and a broken Mustfile fails them (via the wave-1 runner).
12+
13+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
14+
AB="$ROOT/scripts/apply-baseline.sh"
15+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
16+
17+
pass=0 fail=0
18+
ok() { echo "$1"; pass=$((pass + 1)); }
19+
bad() { echo "$1"; fail=$((fail + 1)); }
20+
expect() { if [ "$2" -eq "$1" ]; then ok "$3 (exit $2)"; else bad "$3 (wanted $1, got $2)"; fi; }
21+
22+
echo "== apply-baseline.sh blocking gate =="
23+
cat > "$TMP/findings.json" <<'EOF'
24+
[{"severity":"high","rule_module":"cicd_rules","type":"test_finding","file":"a/b.sh"}]
25+
EOF
26+
27+
# (1) unbaselined high finding in blocking mode -> exit 1
28+
printf '[]' > "$TMP/empty-baseline.json"
29+
bash "$AB" "$TMP/findings.json" "$TMP/empty-baseline.json" blocking >/dev/null 2>&1
30+
expect 1 $? "unbaselined high finding BLOCKS"
31+
32+
# (2) same finding, advisory mode -> exit 0 (reported, not gating)
33+
bash "$AB" "$TMP/findings.json" "$TMP/empty-baseline.json" advisory >/dev/null 2>&1
34+
expect 0 $? "advisory mode does not gate"
35+
36+
# (3) baselined finding (unexpired) -> suppressed, blocking passes
37+
cat > "$TMP/baseline.json" <<'EOF'
38+
[{"severity":"high","rule_module":"cicd_rules","type":"test_finding","file":"a/b.sh","expires_at":"9999-12-31","note":"acknowledged for test"}]
39+
EOF
40+
bash "$AB" "$TMP/findings.json" "$TMP/baseline.json" blocking >/dev/null 2>&1
41+
expect 0 $? "baselined finding passes blocking gate"
42+
43+
# (4) EXPIRED baseline entry no longer suppresses -> blocks again
44+
cat > "$TMP/expired.json" <<'EOF'
45+
[{"severity":"high","rule_module":"cicd_rules","type":"test_finding","file":"a/b.sh","expires_at":"2000-01-01","note":"expired"}]
46+
EOF
47+
bash "$AB" "$TMP/findings.json" "$TMP/expired.json" blocking >/dev/null 2>&1
48+
expect 1 $? "expired baseline entry stops suppressing (blocks)"
49+
50+
# (5) low-severity unbaselined finding stays below the blocking threshold
51+
cat > "$TMP/low.json" <<'EOF'
52+
[{"severity":"low","rule_module":"cicd_rules","type":"test_finding","file":"a/b.sh"}]
53+
EOF
54+
bash "$AB" "$TMP/low.json" "$TMP/empty-baseline.json" blocking >/dev/null 2>&1
55+
expect 0 $? "low-severity finding stays below blocking threshold"
56+
57+
echo "== Mustfile PR gate (exact registry-verify.yml commands) =="
58+
( cd "$ROOT" && bash scripts/check-mustfile-structure.sh >/dev/null 2>&1 )
59+
expect 0 $? "structural check passes on this repo"
60+
( cd "$ROOT" && bash scripts/run-mustfile.sh >/dev/null 2>&1 )
61+
expect 0 $? "invariant execution passes on this repo"
62+
# and it CAN fail: a Mustfile with a failing critical invariant blocks
63+
printf '### x\n- run: test -f /nonexistent-wave8\n- severity: critical\n' > "$TMP/bad-must.a2ml"
64+
( cd "$ROOT" && bash scripts/run-mustfile.sh "$TMP/bad-must.a2ml" >/dev/null 2>&1 )
65+
expect 1 $? "failing critical invariant blocks"
66+
67+
echo "== affinescript-verify workflow shape =="
68+
WF="$ROOT/.github/workflows/affinescript-verify.yml"
69+
# job-level continue-on-error removed (the job can now fail)
70+
if awk '/^ verify:/,/^ steps:/' "$WF" | grep -q 'continue-on-error: true'; then
71+
bad "job-level continue-on-error still present"
72+
else
73+
ok "job-level continue-on-error removed"
74+
fi
75+
# added-file failures exit 1 (blocking branch exists)
76+
grep -q 'exit 1' "$WF" && grep -q 'ADDED file — blocking' "$WF" \
77+
&& ok "added-file blocking branch present" || bad "added-file blocking branch missing"
78+
# toolchain-unavailable skip is loud (never silent green)
79+
grep -q 'SKIPPED, not passed' "$WF" && ok "toolchain skip is loud" || bad "toolchain skip not loud"
80+
81+
echo
82+
echo "Wave-8 gates regression: $pass passed, $fail failed"
83+
[ "$fail" -eq 0 ]

0 commit comments

Comments
 (0)