Skip to content

Commit 4620037

Browse files
ci(spark): SPARK Theatre Gate reusable workflow (#135) (#141)
## What Adds `.github/workflows/spark-theatre-gate.yml` — the estate **SPARK Theatre Gate**, a reusable (`workflow_call`) anti-theatre lint that also self-runs on `standards`. Implements **standards#135** ("standards CI lint: ... fail `SPARK_Mode On` with zero contracts"), sub-issue of the proof-debt epic **#124**. ## Rules Per SPARK_Mode-On Ada unit, only when the repo shows **no GNATprove evidence** (no workflow ref, no `gnatprove` build recipe, no gnatprove output artifact): - **T1 — header proof-claim without prover (hard fail, always).** A strong proof assertion (`formally verified`, `SPARK Proof Level`, `(Gold)`, `all preconditions and postconditions are verified`, `proving the invariant holds`) in the **file header banner**. Header-scoped on purpose so an incidental phrase in a body comment (e.g. an enum-literal description) does not false-fire. - **T2 — zero-contract SPARK_Mode (warn by default).** `SPARK_Mode (On)` with no `Pre`/`Post`/`Global`/`Depends`/`Contract_Cases`. Emitted as a warning so the ~13 repos mid Ada/SPARK→Rust/SPARK migration are not broken on rollout; escalates to a hard failure when a caller passes `enforce_zero_contract: true` (the #135 end-state). Genuine SPARK repos (echidna, stapeln, …) pass — GNATprove evidence suppresses both rules. The gate only bites a *regression* that re-introduces a hollow "verified" claim. ## Verification (local harness, mirrors the workflow scanner) | Target | Result | |---|---| | `standards` self-run | **PASS** | | `ambientops` after the safety_boundary demotion | **PASS** (T2 warn only, no T1) | | `ambientops` with pre-demotion safety_boundary banner restored | **T1-FAIL** (regression correctly caught) | | `ambientops/.../strategy_matrix.ads:53` enum comment "Formally verified" | not flagged (header-scoping works) | ## Rollout This PR lands the gate + self-test. Consumer adoption is a 3-line caller per repo (the ambientops caller ships with the item-2 ambientops PR). T2→hard-fail escalation is deferred per #135 until the migration tail clears. Refs #124 Refs #135 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4aed5f8 commit 4620037

1 file changed

Lines changed: 137 additions & 0 deletions

File tree

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# SPDX-License-Identifier: PMPL-1.0
2+
name: SPARK Theatre Gate
3+
4+
# Estate anti-theatre lint (hyperpolymath/standards#135, sub-issue of #124).
5+
#
6+
# Kills "SPARK proof theatre": an Ada unit that declares SPARK_Mode (On)
7+
# and banners itself "formally verified" / "(Gold)" / "SPARK Proof Level"
8+
# while no GNATprove run has ever discharged an obligation for it.
9+
#
10+
# Per SPARK_Mode-On compilation unit, with no repo-wide prover evidence:
11+
#
12+
# T1 HEADER PROOF-CLAIM WITHOUT PROVER (hard fail, always)
13+
# A strong proof assertion ("formally verified", "SPARK Proof
14+
# Level", "(Gold)", "all preconditions and postconditions are
15+
# verified", "proving the invariant holds") appears in the file
16+
# HEADER BANNER (the comment block before the first code token) of a
17+
# SPARK_Mode-On unit. Header-scoped on purpose: an incidental phrase
18+
# in a comment deep in the body (e.g. an enum-literal description) is
19+
# NOT a claim about the unit and must not trip the gate.
20+
#
21+
# T2 ZERO-CONTRACT SPARK_Mode (issue #135 verbatim; warn by default)
22+
# SPARK_Mode (On) with no Pre/Post/Global/Depends/Contract_Cases.
23+
# Hollow SPARK_Mode that proves nothing. Emitted as a warning so
24+
# repos mid Ada/SPARK->Rust/SPARK migration are not broken on
25+
# rollout; escalates to a hard failure when the caller passes
26+
# enforce_zero_contract: true (target end-state per #135).
27+
#
28+
# Genuine SPARK repos (echidna, stapeln, ...) pass: GNATprove runs in CI,
29+
# so "prover evidence" is present and neither T1 nor T2 fires. The gate
30+
# only bites a regression that re-introduces a hollow "verified" claim.
31+
#
32+
# Reusable: a consumer repo adds a 3-line caller (see ambientops
33+
# .github/workflows/spark-proof-gate.yml). It also self-runs on standards.
34+
35+
on:
36+
workflow_call:
37+
inputs:
38+
paths:
39+
description: "Space-separated roots to scan (default: repo root)."
40+
required: false
41+
type: string
42+
default: "."
43+
enforce_zero_contract:
44+
description: "Escalate T2 (zero-contract SPARK_Mode) from warning to hard failure."
45+
required: false
46+
type: boolean
47+
default: false
48+
push:
49+
branches: [main, master]
50+
pull_request:
51+
branches: [main, master]
52+
53+
concurrency:
54+
group: ${{ github.workflow }}-${{ github.ref }}
55+
cancel-in-progress: true
56+
57+
permissions:
58+
contents: read
59+
60+
jobs:
61+
spark-theatre-gate:
62+
name: SPARK Theatre Gate
63+
runs-on: ubuntu-latest
64+
steps:
65+
- name: Checkout
66+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4
67+
68+
- name: Scan for SPARK proof theatre
69+
env:
70+
SCAN_PATHS: ${{ inputs.paths || '.' }}
71+
ENFORCE_T2: ${{ inputs.enforce_zero_contract && 'on' || 'off' }}
72+
run: |
73+
set -uo pipefail
74+
75+
# --- Repo-wide: is GNATprove ever actually run? -------------------
76+
prover_evidence=0
77+
if grep -rIlE 'gnatprove' .github/workflows 2>/dev/null | grep -q .; then
78+
prover_evidence=1
79+
fi
80+
for bf in Justfile justfile Mustfile Dustfile; do
81+
[ -f "$bf" ] && grep -qI 'gnatprove' "$bf" && prover_evidence=1
82+
done
83+
if find . \( -name 'gnatprove.out' -o -name '*.spark' \
84+
-o -path '*/gnatprove/*.json' \) \
85+
-not -path '*/.git/*' 2>/dev/null | grep -q .; then
86+
prover_evidence=1
87+
fi
88+
echo "prover_evidence=$prover_evidence enforce_t2=$ENFORCE_T2"
89+
90+
claim_re='formally verified|SPARK Proof Level|\(Gold\)|all preconditions and postconditions are verified|proving the invariant holds'
91+
contract_re='Pre[[:space:]]*=>|Post[[:space:]]*=>|Global[[:space:]]*=>|Depends[[:space:]]*=>|Contract_Cases'
92+
sparkon_re='pragma[[:space:]]+SPARK_Mode[[:space:]]*\([[:space:]]*On[[:space:]]*\)|with[[:space:]]+SPARK_Mode[[:space:]]*=>[[:space:]]*On'
93+
94+
fail=0
95+
while IFS= read -r f; do
96+
[ -z "$f" ] && continue
97+
98+
# Active SPARK_Mode-On aspect, ignoring commented-out lines.
99+
grep -vE '^[[:space:]]*--' "$f" | grep -qE "$sparkon_re" || continue
100+
101+
# Header banner = the leading run of comment / blank lines up to
102+
# (but excluding) the first non-comment, non-blank code token.
103+
header="$(awk '
104+
/^[[:space:]]*--/ || /^[[:space:]]*$/ { print; next }
105+
{ exit }' "$f")"
106+
107+
has_header_claim=0
108+
printf '%s\n' "$header" | grep -qiE "$claim_re" && has_header_claim=1
109+
has_contract=0
110+
grep -qE "$contract_re" "$f" && has_contract=1
111+
112+
[ "$prover_evidence" -eq 1 ] && continue # prover runs: not theatre
113+
114+
if [ "$has_header_claim" -eq 1 ]; then
115+
echo "::error file=$f::T1 SPARK theatre: header banner claims formal proof but no GNATprove run exists in this repo. Wire gnatprove into CI, or remove the claim and set SPARK_Mode Off (see proven#24 / ambientops safety_boundary for the honest-demotion pattern)."
116+
fail=1
117+
elif [ "$has_contract" -eq 0 ]; then
118+
if [ "$ENFORCE_T2" = "on" ]; then
119+
echo "::error file=$f::T2 zero-contract SPARK_Mode (standards#135): SPARK_Mode (On) with no Pre/Post/Global/Depends/Contract_Cases and no prover. Add real contracts + a gnatprove gate, or set SPARK_Mode Off."
120+
fail=1
121+
else
122+
echo "::warning file=$f::T2 zero-contract SPARK_Mode (standards#135): SPARK_Mode (On) proves nothing here (no contracts, no prover). Demote to SPARK_Mode Off or add contracts. Escalates to a hard failure once enforce_zero_contract is set."
123+
fi
124+
fi
125+
done < <(
126+
for root in $SCAN_PATHS; do
127+
find "$root" \( -name '*.ads' -o -name '*.adb' \) \
128+
-not -path '*/.git/*' -not -path '*/obj/*' \
129+
-not -path '*/node_modules/*' 2>/dev/null
130+
done
131+
)
132+
133+
if [ "$fail" -ne 0 ]; then
134+
echo "::error::SPARK Theatre Gate FAILED — see annotations. Doctrine: a SPARK_Mode-On unit must either be genuinely proved (GNATprove in CI) or honestly demoted (SPARK_Mode Off + tracked obligation). No hollow 'verified' claims."
135+
exit 1
136+
fi
137+
echo "✓ SPARK Theatre Gate passed (no hollow header proof claims found)."

0 commit comments

Comments
 (0)