Skip to content

Commit ed4e223

Browse files
ci: wire the formal/ Coq proof gate into CI (Refs #513) (#709)
## Why `formal/` contains **20 machine-checked Coq/Rocq proofs**, and `formal/justfile` already implements a real, fail-closed check over them. **No workflow ever invoked it** — `grep` for coq/rocq across `.github/workflows/` returned nothing, so the entire corpus was unchecked in CI. This is the inverse of the usual fake-gate problem: the assurance already existed and was simply being thrown away. ## Verified before wiring Ran the existing `formal/justfile check` locally with `coqc` 8.20.1: - all 20 files check clean - every `Print Assumptions` reports **`Closed under the global context`** — the strongest result, meaning no dependency on anything outside the kernel - the justfile's own assertion fires: *"OK: K-1/K1Let/F-1 + P-2/P-3/F-3/F-4 mechanised; no axioms."* - only deprecation warnings (`app_length`, from the 8.18→8.20 skew), no errors Note `formal/README.adoc` documents Coq/Rocq **8.18**; the container here is **8.20**, on which the corpus was confirmed to pass. ## Design **Fail-closed by construction.** The gate does *not* probe for the prover and skip when absent. The pinned container guarantees `coqc`, so a missing prover is an infrastructure failure, not a silent pass. **No duplicated proof list.** `formal/justfile` stays the single source of truth for the list and its dependency order; the workflow parses it rather than re-stating it. A completeness guard fails the run if any `formal/*.v` on disk is not named there — so adding a proof without wiring it cannot silently go unchecked. ## Falsifier-tested Both guards were proven to actually fire, not merely to pass: | Falsifier | Result | |---|---| | Add an unwired `formal/ZZ_Falsifier.v` | guard fires, run fails | | Add a theorem proved via `Axiom cheat` | `Print Assumptions` emits `Axioms:`, grep catches it, run fails | ## Notes - Container pinned by digest (`coqorg/coq@sha256:e50d77c4…`) - `actions/checkout` SHA-pinned to match repo convention - SPDX header on line 1 and `permissions:` at column 0, per `workflow-linter.yml` - A cold full run takes over 2 minutes locally, hence `timeout-minutes: 30` Draft — this is the first time these proofs will run in CI. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents 3e34d7f + 8bea4c7 commit ed4e223

1 file changed

Lines changed: 96 additions & 0 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# Coq/Rocq proof gate for the `formal/` mechanised-metatheory track (issue #513).
3+
#
4+
# This gate is deliberately FAIL-CLOSED. It does NOT probe for the prover and
5+
# skip when absent — the container guarantees `coqc` exists, so a missing
6+
# prover is an infrastructure failure, not a silent pass.
7+
#
8+
# `formal/justfile` remains the single source of truth for the proof list and
9+
# its dependency order; this workflow parses that list rather than duplicating
10+
# it, and fails if any `formal/*.v` on disk is not named there.
11+
name: Coq Proof Gate
12+
on:
13+
pull_request:
14+
paths:
15+
- 'formal/**'
16+
- '.github/workflows/coq-proof-gate.yml'
17+
push:
18+
branches: [main]
19+
paths:
20+
- 'formal/**'
21+
- '.github/workflows/coq-proof-gate.yml'
22+
workflow_dispatch:
23+
permissions: read-all
24+
concurrency:
25+
group: ${{ github.workflow }}-${{ github.ref }}
26+
cancel-in-progress: true
27+
jobs:
28+
coq-proofs:
29+
runs-on: ubuntu-latest
30+
timeout-minutes: 30
31+
container:
32+
# coqorg/coq:8.20 — pinned by digest. `formal/README.adoc` documents 8.18;
33+
# the corpus was verified to check clean on 8.20.1 (deprecation warnings
34+
# for `app_length` only, no errors).
35+
image: coqorg/coq@sha256:e50d77c4c5a9aa0d76ae1b343d79c5f922da3a75054b79c5dc635895438e4674
36+
options: --user root
37+
steps:
38+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
39+
40+
- name: Record prover version
41+
run: coqc --version
42+
43+
- name: Extract the ordered proof list from formal/justfile
44+
id: list
45+
run: |
46+
set -euo pipefail
47+
cd formal
48+
list="$(sed -n '/for f in /,/; do/p' justfile \
49+
| tr '\n' ' ' \
50+
| sed 's/.*for f in //; s/; do.*//; s/\\//g' \
51+
| tr -s ' ')"
52+
if [ -z "${list// /}" ]; then
53+
echo "::error::could not parse the proof list out of formal/justfile"
54+
exit 1
55+
fi
56+
echo "count=$(printf '%s' "$list" | wc -w)"
57+
echo "list=$list" >> "$GITHUB_OUTPUT"
58+
59+
- name: Guard — every formal/*.v must be wired into the gate
60+
env:
61+
LIST: ${{ steps.list.outputs.list }}
62+
run: |
63+
set -euo pipefail
64+
cd formal
65+
missing=0
66+
for f in *.v; do
67+
b="${f%.v}"
68+
case " $LIST " in
69+
*" $b "*) ;;
70+
*) echo "::error file=formal/$f::proof file is not listed in formal/justfile — it would never be checked"; missing=1 ;;
71+
esac
72+
done
73+
[ "$missing" -eq 0 ] || exit 1
74+
echo "all on-disk proofs are wired into the gate"
75+
76+
- name: Type-check every proof and reject any axiom / Admitted
77+
env:
78+
LIST: ${{ steps.list.outputs.list }}
79+
run: |
80+
set -euo pipefail
81+
cd formal
82+
all=""
83+
for f in $LIST; do
84+
echo "== coqc $f.v =="
85+
o="$(coqc -Q . ASFormal "$f.v")"
86+
printf '%s\n' "$o"
87+
all+="$o"$'\n'
88+
done
89+
# `Print Assumptions` emits "Axioms:" when a theorem depends on an
90+
# axiom or an `Admitted` proof; "Closed under the global context"
91+
# is the clean result.
92+
if printf '%s' "$all" | grep -q "Axioms:"; then
93+
echo "::error::a proof depends on an axiom / Admitted"
94+
exit 1
95+
fi
96+
echo "OK: all proofs mechanised; no axioms."

0 commit comments

Comments
 (0)