|
| 1 | +# SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
| 3 | +# |
| 4 | +# lessons.a2ml — proof-engineering lessons surfaced through real failures. |
| 5 | +# Read at session start by any AI agent touching formal-verification code |
| 6 | +# (Coq, Idris2, Lean4, Agda). Each lesson is named, has a reproducer, and |
| 7 | +# tells the agent what to do (and what NOT to do) when it hits the pattern. |
| 8 | +# |
| 9 | +# Human-readable companion: |
| 10 | +# docs/sessions/SESSION-2026-05-20-proof-engineering-lessons.adoc |
| 11 | +# |
| 12 | +# Reference: ADR-002 in standards/agentic-a2ml/docs/ |
| 13 | + |
| 14 | +[metadata] |
| 15 | +version = "1.0.0" |
| 16 | +last-updated = "2026-05-20" |
| 17 | +surfaced-by = "standards#124 (estate proof-debt epic), single day's work 2026-05-20" |
| 18 | +surfaced-via-prs = ["proof-of-work#63", "ephapax#92", "ephapax#95", "ephapax#98", "ephapax#102", "ephapax#103"] |
| 19 | + |
| 20 | +# ============================================================================ |
| 21 | +# LESSON 1: in-file proof-status comments are NOT authoritative |
| 22 | +# ============================================================================ |
| 23 | + |
| 24 | +[[lesson]] |
| 25 | +id = "build-is-the-only-oracle" |
| 26 | +title = "In-file proof-status comments are NOT authoritative — build is the only oracle" |
| 27 | +severity = "high" |
| 28 | +applies-to = ["coq", "idris2", "lean4", "agda"] |
| 29 | + |
| 30 | +pattern = """ |
| 31 | +A source file carries a comment block claiming proof status: |
| 32 | + Qed. |
| 33 | + (* PROOF STATUS — FULLY CLOSED. Zero Admitted. ... *) |
| 34 | +The agent reads the comment, summarises it into downstream docs |
| 35 | +(ROADMAP, PROOF-NEEDS, STATE.a2ml) WITHOUT running the build. |
| 36 | +If the comment was ever wrong or silently drifted, the lie propagates. |
| 37 | +""" |
| 38 | + |
| 39 | +bit-us-where = "ephapax/formal/Semantics.v L3328 claimed 'FULLY CLOSED (2026-04-27). Zero Admitted.' with Qed.; coqc 8.18.0 rejected it. ephapax#87 propagated the lie into ROADMAP + PROOF-NEEDS. ephapax/.machine_readable/6a2/STATE.a2ml claimed '67 Qed, 0 Admitted' — also false." |
| 40 | + |
| 41 | +do = "Before citing or summarising any in-file proof-status claim, RUN THE BUILD:\n Coq: cd formal && coq_makefile -f _CoqProject -o Makefile.coq && make -f Makefile.coq\n Idris2: idris2 --build <pkg>.ipkg\n Lean4: lake build\nGreen build supports the claim. Red build invalidates the claim regardless of what the comment says." |
| 42 | + |
| 43 | +dont = "Don't trust the comment. Don't propagate it into docs without running the build. Don't summarise 'Qed' as 'proven' without verification." |
| 44 | + |
| 45 | +fixed-by-pr = ["ephapax#92", "ephapax#103"] |
| 46 | + |
| 47 | +# ============================================================================ |
| 48 | +# LESSON 2: "Theorem name in formal/" can be vacuous |
| 49 | +# ============================================================================ |
| 50 | + |
| 51 | +[[lesson]] |
| 52 | +id = "vacuous-wrapper-theorems" |
| 53 | +title = "A theorem can be vacuous — check the body, not the header docstring" |
| 54 | +severity = "high" |
| 55 | +applies-to = ["coq", "idris2", "lean4", "agda"] |
| 56 | + |
| 57 | +pattern = """ |
| 58 | +A formalisation file carries: |
| 59 | + ||| Region Safety: a RegionBlock proves its result doesn't reference r. |
| 60 | + public export |
| 61 | + 0 regionSafetyExtract : |
| 62 | + (r : RegionId) -> ... -> NoRegionInType r t -> ... -> NoRegionInType r t |
| 63 | + regionSafetyExtract r ctx t ne lc = ne |
| 64 | +Header claims something substantive. Body is = ne (return input |
| 65 | +unchanged). The theorem is sound (no escape hatches) but content-free. |
| 66 | +""" |
| 67 | + |
| 68 | +bit-us-where = "ephapax/src/formal/Ephapax/Formal/RegionLinear.idr: regionSafetyExtract and noGCExtract both have tautological bodies. ROADMAP cited them as 'regionSafetyTheorem' / 'noGCTheorem' complete. Vacuous. Also seen earlier in vcl-ut (L2/L3/L5 predicates vacuously inhabited by AllParameterised)." |
| 69 | + |
| 70 | +do = """ |
| 71 | +When reviewing a 'Theorem complete' claim: |
| 72 | + 1. Find the theorem in source. |
| 73 | + 2. Look at the BODY, not the header docstring. |
| 74 | + 3. If the body is `= <one of the inputs>`, the theorem is tautological — flag it. |
| 75 | + 4. If the body is `= absurd <constructor>` over a no-inhabitant ctor, the theorem |
| 76 | + is narrow — check the header doesn't overstate. |
| 77 | + 5. If the body is `Refl` with textually-identical LHS/RHS, same — vacuous. |
| 78 | +A real theorem chains supporting lemmas, does non-trivial case analysis, |
| 79 | +or constructs evidence. |
| 80 | +""" |
| 81 | + |
| 82 | +dont = "Don't conclude 'theorem complete' from the type signature alone. Don't trust grep for theorem names — the name is a label, not a proof." |
| 83 | + |
| 84 | +fixed-by-pr = ["ephapax#95"] # E3/E4 honestly OWED in the seam |
| 85 | + |
| 86 | +# ============================================================================ |
| 87 | +# LESSON 3: Show. + Show Existentials. for Coq diagnostics |
| 88 | +# ============================================================================ |
| 89 | + |
| 90 | +[[lesson]] |
| 91 | +id = "coq-show-existentials-recipe" |
| 92 | +title = "Show. Show Existentials. is the precise diagnostic for unfinished Coq proofs" |
| 93 | +severity = "medium" |
| 94 | +applies-to = ["coq"] |
| 95 | + |
| 96 | +pattern = """ |
| 97 | +A Coq proof script fails with: |
| 98 | + Error: (in proof X): Attempt to save an incomplete proof |
| 99 | + (there are remaining open goals). |
| 100 | +Natural assumption: 'one more tactic'. Often the reality: the script |
| 101 | +is closing zero goals, not one off from closure. |
| 102 | +""" |
| 103 | + |
| 104 | +bit-us-where = "ephapax/formal/Semantics.v::preservation — coqc rejected Qed.; in-file comment claimed 'Only ONE case remains open'. Actual count via Show: 910 goals = 35 step rules × 26 typing rules. The try solve chain was closing ZERO." |
| 105 | + |
| 106 | +do = """ |
| 107 | +Before adding tactics blindly: |
| 108 | + end). |
| 109 | + Show. (* prints current goal *) |
| 110 | + Show Existentials. (* lists every unresolved metavariable + context *) |
| 111 | + Admitted. |
| 112 | +Then build. coqc prints the goal count at the top + every existential. |
| 113 | +Restore the Admitted. afterwards. Count tells you whether you need one |
| 114 | +more tactic or a structural rewrite (see lesson 'induction-doesnt-substitute'). |
| 115 | +""" |
| 116 | + |
| 117 | +dont = "Don't add 'try solve [...]' lines hoping for the best. Don't trust comment claims about which case is left open without running Show." |
| 118 | + |
| 119 | +# ============================================================================ |
| 120 | +# LESSON 4: induction doesn't substitute outer expression slots |
| 121 | +# ============================================================================ |
| 122 | + |
| 123 | +[[lesson]] |
| 124 | +id = "induction-doesnt-substitute-outer-slots" |
| 125 | +title = "induction Hstep doesn't substitute outer expression slots — remember (cfg) as cfg eqn:Hcfg is the standard fix" |
| 126 | +severity = "high" |
| 127 | +applies-to = ["coq"] |
| 128 | + |
| 129 | +pattern = """ |
| 130 | +A preservation-style theorem: |
| 131 | + Theorem preservation : |
| 132 | + forall mu R e mu' R' e', |
| 133 | + step (mu, R, e) (mu', R', e') -> |
| 134 | + forall G T G', has_type R G e T G' -> |
| 135 | + exists G_out, has_type R' G e' T G_out. |
| 136 | + Proof. |
| 137 | + intros mu R e mu' R' e' Hstep. |
| 138 | + induction Hstep; intros G0 T0 G0' Htype; |
| 139 | + inversion Htype; subst; ... |
| 140 | + |
| 141 | +After `induction Hstep`, the outer `e` STAYS ABSTRACT. inversion Htype |
| 142 | +then produces ALL typing arms (m arms), not just the diagonal matching |
| 143 | +the step's expression. For n step rules × m typing rules you get n × m |
| 144 | +cross-cases. Trivial 'try solve [exfalso; discriminate]' fails because |
| 145 | +no discriminating equation is in scope. |
| 146 | +""" |
| 147 | + |
| 148 | +bit-us-where = "ephapax/formal/Semantics.v::preservation — 35 step rules × 26 typing rules = 910 open goals. Existing try solve chain closed ZERO." |
| 149 | + |
| 150 | +do = """ |
| 151 | +Add `remember` BEFORE induction: |
| 152 | + intros mu R e mu' R' e' Hstep. |
| 153 | + remember (mu, R, e) as cfg eqn:Hcfg. |
| 154 | + remember (mu', R', e') as cfg' eqn:Hcfg'. |
| 155 | + induction Hstep; intros G0 T0 G0' Htype; |
| 156 | + inversion Hcfg; subst; |
| 157 | + inversion Hcfg'; subst; |
| 158 | + inversion Htype; subst; |
| 159 | + try solve [...]. |
| 160 | +Now `Hcfg` after induction becomes (e.g. for S_StringNew): |
| 161 | + (mu0, R0, EStringNew r s) = (mu, R, e) |
| 162 | +inversion Hcfg; subst substitutes e := EStringNew r s everywhere |
| 163 | +including Htype. Then inversion Htype only generates the diagonal arm; |
| 164 | +the m-1 cross-arms are eliminated by constructor mismatch. |
| 165 | +ephapax#102 used this and reduced 910 → 29 (97%). |
| 166 | +""" |
| 167 | + |
| 168 | +dont = """ |
| 169 | +ANTI-PATTERN: `remember e_typed as e_orig eqn:He_orig` from `type of |
| 170 | +Htype`. This remembers the TYPING's expression (already abstract) — same |
| 171 | +shape as before. Solves nothing. Remember the CONFIG's expression slot |
| 172 | +(what induction Hstep substitutes for), not the typing's. |
| 173 | + |
| 174 | +Also don't: |
| 175 | +- Replace induction with inversion — loses IHs for congruence cases. |
| 176 | +- Use dependent induction without thinking — can over-substitute and |
| 177 | + break the IH for congruence-rule cases too. |
| 178 | +""" |
| 179 | + |
| 180 | +when-to-apply = "Any preservation-style proof where (a) the induction is on a relation with expression slots in its config, (b) the outer theorem quantifies over those slots, (c) typing-side inversion needs the expression form pinned." |
| 181 | + |
| 182 | +fixed-by-pr = ["ephapax#102"] |
| 183 | +human-readable-doc = "docs/sessions/SESSION-2026-05-20-proof-engineering-lessons.adoc" |
| 184 | + |
| 185 | +# ============================================================================ |
| 186 | +# CROSS-CUTTING NOTES |
| 187 | +# ============================================================================ |
| 188 | + |
| 189 | +[notes] |
| 190 | +estate-pattern = "All four lessons cluster around the same root: claimed-but-unverified proof state. Lesson 1 = claims propagated without verification; Lesson 2 = claims look substantive but lack content; Lesson 3 = the precise way to verify a claim; Lesson 4 = the fix for a specific failure mode that triggers Lesson 1." |
| 191 | + |
| 192 | +related-memory = [ |
| 193 | + "feedback_foundation_first", # build-is-the-only-oracle rule |
| 194 | + "project_vclut_hole_deeper_than_documented", # vacuous predicates analog |
| 195 | + "project_proof_debt_rust_spark_audit_2026_05_18" # epic context |
| 196 | +] |
0 commit comments