mirror of
https://github.com/saymrwulf/ltl-accumulator-verified.git
synced 2026-09-03 19:53:48 +00:00
STATEMENT BINDING (Phase 3d). The coverage gate pins every constant's name,
kind and axiom cone, both directions, and none of selftest_audit.sh's nine
attacks defeat it. It is nevertheless blind to what a declaration SAYS — and
that is demonstrated here rather than argued:
Wrapping one branch of `LTLAcc.pinAccept`'s body in `id (…)` is
definitionally equal. Every downstream proof still compiles. The name, the
kind, the type and the axiom cone are unchanged. The inventory gate reports
"222 constants, environment == allowlist" — GREEN.
That edit is harmless by construction; the point is that nothing stood between
it and a genuinely vacuous redefinition of a specification. Proofs/Inventory.lean
now also emits, for every inventoried constant, its fully-elaborated TYPE, and
for every definition its fully-elaborated BODY — 266 lines over 222 constants.
Proof terms are deliberately absent: by proof irrelevance a theorem's content
is its statement. check.sh Phase 3d binds the SHA-256 and the block is
committed as AUDIT-MANIFEST.txt so a mismatch is DIFFED, not merely reported.
The existing gate is untouched, per the standing rule that the port flows FROM
this repo, not to it: INV lines are byte-identical, inventory_gate.sh is
unchanged, and all nine of its attacks still fail as before.
selftest_statements.sh replays the defeq edit as case 1, asserting BOTH that
the coverage gate passes it and that Phase 3d catches it — so if the coverage
gate ever grows to see this, the test says so instead of quietly re-labelling.
Cases 2-4 cover a hand-edited committed block, a truncated block, and a
constant inventoried without a statement.
FIDELITY PIN (unrelated, found while running the button). Phase 4 had been
failing since 2026-07-23: LIED_PIN_DIV expected 3,867 divergences between the
Lean model and the deployed consistency verifier, and observed 0. Cause is
pacta ddbb5a4, which restored the RFC 9162 2.1.4.2 Step-7 terminal `sn == 0`
condition; that one conjunct removes every divergence in the pinned
73,573-case family. KNOWN-GAPS gap 14 already recorded the closure on the day
it landed — only this constant was stale, so the button had been red for five
days with nobody running it. The pin now reads 0 with the history in a comment.
Nothing about the paper, public log entry 13, or the attested commit 172a1d0
changes; the historical divergence stays reproducible at the tagged pre-fix
commit.
KNOWN-GAPS gap 16 records what the binding does not buy: identity, not
meaning; an author who edits and re-pins in one commit is caught by review and
not by the script; and proof terms are unbound by design.
Button green end to end: ATTESTATION GREEN (Lean + fidelity).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
166 lines
7.4 KiB
Text
166 lines
7.4 KiB
Text
/- Environment-derived declaration inventory (Phase 3b of check.sh).
|
||
|
||
Review round 2 (GPT H1) proved the previous source-regex enumerator
|
||
evadable: attributed / private / indented / `instance` declarations
|
||
were invisible, and a nested `namespace Hidden theorem MTH` collided
|
||
with the basename of an audited declaration. This module replaces
|
||
source scanning entirely: the inventory is read from the compiled
|
||
Lean ENVIRONMENT, so it sees exactly what the kernel saw.
|
||
|
||
Design (fail-closed by construction):
|
||
· The corpus module list below must match check.sh's compile
|
||
manifest (check.sh verifies this textually, both directions).
|
||
A listed module that is not actually imported is an elaboration
|
||
ERROR here, not a silent skip.
|
||
· EVERY constant whose originating module is a corpus module is
|
||
emitted — fully qualified, NO filtering. Compiler-generated
|
||
auxiliaries (equation lemmas, match/eq/induct helpers, private
|
||
mangles) are emitted too and pinned in the allowlist; anything
|
||
new, renamed, or removed shows up as a diff. There is no name
|
||
shape that can hide.
|
||
· Each constant carries its declaration KIND and its full axiom
|
||
cone, computed by the independent walker below (not by
|
||
#print axioms — Phase 3 still runs #print axioms separately, so
|
||
the two cone computations cross-check each other in check.sh).
|
||
· Output lines are prefixed `INV|` and sorted, so check.sh can
|
||
extract them robustly from compiler chatter.
|
||
|
||
This file is audit INFRASTRUCTURE, not corpus: it is excluded from
|
||
the compile manifest (like AxiomCheck.lean) and its own constants
|
||
are not inventoried (they live in the current module, which has no
|
||
module index). It proves nothing and is imported by nothing. -/
|
||
import Lean
|
||
import LTLAcc.HashExternal
|
||
import Proofs.Basic
|
||
import Proofs.Completeness
|
||
import Proofs.Extract
|
||
import Proofs.Descent
|
||
import Proofs.Consistency
|
||
import Proofs.Binding3
|
||
import Proofs.Refactor
|
||
import Proofs.Theorem3
|
||
import Proofs.PinStore
|
||
|
||
open Lean
|
||
|
||
namespace LTLAccAudit
|
||
|
||
/-- Exactly check.sh's GEN_MODULES ++ PROOFS, as module names. -/
|
||
def corpusModules : Array Name :=
|
||
#[`LTLAcc.HashExternal,
|
||
`Proofs.Basic, `Proofs.Completeness, `Proofs.Extract, `Proofs.Descent,
|
||
`Proofs.Consistency, `Proofs.Binding3, `Proofs.Refactor,
|
||
`Proofs.Theorem3, `Proofs.PinStore]
|
||
|
||
def kindOf : ConstantInfo → String
|
||
| .axiomInfo _ => "axiom"
|
||
| .defnInfo _ => "def"
|
||
| .thmInfo _ => "theorem"
|
||
| .opaqueInfo _ => "opaque"
|
||
| .quotInfo _ => "quot"
|
||
| .inductInfo _ => "inductive"
|
||
| .ctorInfo _ => "ctor"
|
||
| .recInfo _ => "recursor"
|
||
|
||
/-- Proof/definition body of a constant. NOTE: `ConstantInfo.value?`
|
||
returns `none` for theorems on this toolchain (observed on
|
||
4.30.0-rc2), which would silently truncate every cone at the first
|
||
theorem — so we match constructors directly. The cross-check against
|
||
core `collectAxioms` below would catch any such truncation. -/
|
||
def valueOf : ConstantInfo → Option Expr
|
||
| .defnInfo v => some v.value
|
||
| .thmInfo v => some v.value
|
||
| .opaqueInfo v => some v.value
|
||
| _ => none
|
||
|
||
/-- Full axiom cone of `root`: transitive closure over types AND values.
|
||
Written independently of core's `CollectAxioms`; the `#eval` below
|
||
insists both agree on every constant, and Phase 3 of check.sh
|
||
additionally cross-checks the audited names against `#print axioms`
|
||
output. -/
|
||
def axiomCone (env : Environment) (root : Name) : Array Name := Id.run do
|
||
let mut visited : NameSet := {}
|
||
let mut axioms : Array Name := #[]
|
||
let mut stack : Array Name := #[root]
|
||
while h : stack.size > 0 do
|
||
let n := stack[stack.size - 1]'(by omega)
|
||
stack := stack.pop
|
||
unless visited.contains n do
|
||
visited := visited.insert n
|
||
if let some ci := env.find? n then
|
||
if ci matches .axiomInfo _ then
|
||
axioms := axioms.push n
|
||
stack := stack ++ ci.type.getUsedConstants
|
||
if let some v := valueOf ci then
|
||
stack := stack ++ v.getUsedConstants
|
||
return (axioms.qsort (fun a b => a.toString < b.toString))
|
||
|
||
/-- Whitespace-canonical: every whitespace run collapses to one space, so the
|
||
pretty-printer's line wrapping cannot perturb the digest. -/
|
||
def normWs (s : String) : String :=
|
||
(s.foldl (fun (acc : String × Bool) c =>
|
||
let c := if c.isWhitespace then ' ' else c
|
||
if c == ' ' then (if acc.2 then acc else (acc.1.push ' ', true))
|
||
else (acc.1.push c, false))
|
||
("", true)).1
|
||
|
||
/-- Fully-explicit (`pp.all`) rendering, whitespace-canonicalized. Implicit
|
||
arguments, instances and universe levels are all made visible, so two
|
||
statements that merely LOOK alike cannot share a rendering. -/
|
||
def ppAll (e : Expr) : MetaM String := do
|
||
let fmt ← withOptions (fun o => o.setBool `pp.all true) (Meta.ppExpr e)
|
||
return normWs fmt.pretty
|
||
|
||
#eval show MetaM Unit from do
|
||
let env ← getEnv
|
||
-- Resolve every corpus module to its index; a miss is a hard error.
|
||
let mut idxs : Array Nat := #[]
|
||
for m in corpusModules do
|
||
match env.getModuleIdx? m with
|
||
| some i => idxs := idxs.push i
|
||
| none => throwError "INVENTORY ERROR: corpus module {m} is not imported"
|
||
let mut lines : Array String := #[]
|
||
-- STATEMENT SURFACE (P1-a). The INV lines above record what each constant
|
||
-- IS and what it RESTS ON. They do not record what it SAYS: a theorem gutted
|
||
-- to a tautology keeps its name, its kind and its axiom cone, and a `def`
|
||
-- redefined to BE the thing it was meant to specify keeps all three too,
|
||
-- while the certificate stated against it silently becomes vacuous. So every
|
||
-- constant additionally contributes its fully-elaborated TYPE, and every
|
||
-- definition its fully-elaborated BODY. Proof terms are NOT emitted: by
|
||
-- proof irrelevance a theorem's content is its statement, and its term is
|
||
-- both enormous and irrelevant to what is being claimed.
|
||
let mut stmts : Array String := #[]
|
||
let mut nTypes := 0
|
||
for (n, ci) in env.constants.toList do
|
||
if let some i := env.getModuleIdxFor? n then
|
||
if idxs.contains i then
|
||
let cone := axiomCone env n
|
||
-- Cross-check against core's collector (the same machinery
|
||
-- `#print axioms` uses): any divergence is a hard error.
|
||
let coreCone := (← collectAxioms n).qsort (fun a b => a.toString < b.toString)
|
||
unless cone == coreCone do
|
||
throwError "INVENTORY ERROR: cone divergence on {n}: walker={cone} core={coreCone}"
|
||
let coneStr := ",".intercalate (cone.toList.map (·.toString))
|
||
lines := lines.push s!"INV|{n}|{kindOf ci}|{coneStr}"
|
||
stmts := stmts.push s!"STMT|{n}|{kindOf ci}|type={← ppAll ci.type}"
|
||
nTypes := nTypes + 1
|
||
match ci with
|
||
| .defnInfo v => stmts := stmts.push s!"STMT|{n}|{kindOf ci}|value={← ppAll v.value}"
|
||
| _ => pure ()
|
||
let sorted := lines.qsort (· < ·)
|
||
for l in sorted do
|
||
IO.println l
|
||
IO.println s!"INV-COUNT|{sorted.size}"
|
||
-- FAIL CLOSED: the statement surface must cover the inventory exactly. If
|
||
-- these ever diverge, some constant is inventoried but unbound — which is
|
||
-- precisely the gap this section exists to close.
|
||
let sortedStmts := stmts.qsort (· < ·)
|
||
unless nTypes == sorted.size do
|
||
throwError "INVENTORY ERROR: {sorted.size} constants inventoried but {nTypes} carry a statement"
|
||
IO.println "STMT-BEGIN"
|
||
for l in sortedStmts do
|
||
IO.println l
|
||
IO.println "STMT-END"
|
||
IO.println s!"STMT-COUNT|{sortedStmts.size}"
|
||
|
||
end LTLAccAudit
|