mirror of
https://github.com/saymrwulf/ltl-accumulator-verified.git
synced 2026-09-03 19:53:48 +00:00
CLASS 15 — a Lean file where no phase was looking. The dead-file scan read Proofs/*.lean and gen/LTLAcc/*.lean and nothing else. A module at the verification root, or under any other gen/ subdirectory, was neither compiled nor rejected — while remaining importable by name, since LEAN_PATH contains both roots. That is a source of the corpus that no phase reads and no pin covers, which is exactly what the dead-file gate exists to forbid; it was simply looking in two places instead of everywhere. Now nothing may live in either root but the two enumerated sets. CLASS 9 — the instruments' own declaration surface. AxiomCheck.lean and Inventory.lean perform the audit and are therefore not corpus, so nothing inventoried what THEY declare. Inventory.lean now walks both: AxiomCheck by module index, and itself as the module still being elaborated, whose declarations are the ones the environment reports with no originating module. That is what makes the inventory cover the instrument that produces it rather than exempting itself. The policy is not "declare nothing" — this file legitimately declares its machinery. It is that an instrument may declare only inert definitions. An axiom here would widen the trusted base without appearing in any certificate's cone; a theorem here would be a claim no certificate covers and no allowlist pins. A flat ban on theorems was WRONG and was measured to be wrong: defining a function by well-founded recursion makes the elaborator emit its own obligations, and axiomCone._proof_1 rejected this very file. The distinction that holds is whether a theorem is a claim someone wrote or an artefact of a definition declared alongside it — an artefact's name extends the name of a constant declared with it. Observed surface: 18 declarations, 16 def and 2 generated obligations, no axiom, no standalone claim. The drivers are byte-pinned already, so this does not pin WHICH definitions they contain — that would add a thing to maintain without adding a thing to catch. It adds the property byte-pinning cannot give: that no instrument declares an axiom or a claim, whatever its bytes are. selftest_audit.sh: 10 cases -> 14. Case 12 uses an INDENTED axiom, because Phase 1's source grep catches an unindented one and the point is to reach the kernel-side walk standing behind it. TWO DEFECTS IN THE TEST HARNESS, found while adding the cases. · The scratch tree copied verification/ only, but the button also reads README.md and STATEMENT-MAP.md from the repository root. check.sh therefore ALWAYS died in Phase 3c in the scratch tree, which made every `if check.sh; then <attack not caught>` guard unfirable — check.sh could not pass in there even with no attack at all. Only the diagnostic greps were doing any work. The documents are now copied, and the negative test below proves the guard is live: with the driver-surface check disabled, check.sh PASSES a tree whose inventory driver declares `axiom driver_cheat : False`. · Case 9 was the last case when it was written and left its rogue gen file in place. Harmless then; the new cases inherited it. Cleaned up between the blocks rather than inside case 9, so that case still tests what it did. Also fixed while here: Phase 3b compared the compile manifest against Inventory.lean by grepping the WHOLE FILE for a backticked module name, so prose counted — a doc comment naming a module broke the count, and in the other direction a doc mention of a module missing from the array would have satisfied the presence check and hidden the omission. It now reads the arrays. Both new gates negative-tested by removal. Button green, self-test green.
235 lines
11 KiB
Text
235 lines
11 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
|
||
import Proofs.AxiomCheck
|
||
|
||
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]
|
||
|
||
/-- The audit INSTRUMENTS, as opposed to the corpus. They are Lean modules in
|
||
the audited tree, so what they declare is part of this repository's
|
||
surface — but they are not proofs, and nothing may rest on them.
|
||
|
||
`Proofs.AxiomCheck` is reachable here because this module imports it; this
|
||
module ITSELF has no module index while it is being elaborated, so its own
|
||
declarations are the ones the environment reports with no originating
|
||
module, and they are checked that way below. That is what makes this
|
||
inventory cover the instrument that produces it. -/
|
||
def driverModules : Array Name := #[`Proofs.AxiomCheck]
|
||
|
||
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}"
|
||
-- ── CLASS 9: the instruments' own declaration surface ────────────────────
|
||
-- The loop above walks the CORPUS. It says nothing about the two modules
|
||
-- that perform the audit, and until 2026-07-31 nothing else did either: an
|
||
-- `axiom` or a `theorem` added to Proofs.AxiomCheck or to this file was
|
||
-- invisible to every phase of the button. Both are covered here.
|
||
--
|
||
-- Proofs.AxiomCheck is reachable by module index because this module imports
|
||
-- it. THIS module has no index yet — it is still being elaborated — so its
|
||
-- own declarations are exactly those the environment reports with no
|
||
-- originating module, which is how the inventory covers the instrument that
|
||
-- produces it rather than exempting itself.
|
||
--
|
||
-- The policy is not "declare nothing": this file legitimately declares the
|
||
-- machinery above. The policy is that an instrument may declare only inert
|
||
-- definitions. An `axiom` here would widen the trusted base without
|
||
-- appearing in any certificate's cone; a `theorem` here would be a claim
|
||
-- that no certificate covers and no allowlist pins.
|
||
let mut drvIdxs : Array Nat := #[]
|
||
for m in driverModules do
|
||
match env.getModuleIdx? m with
|
||
| some i => drvIdxs := drvIdxs.push i
|
||
| none => throwError "INVENTORY ERROR: driver module {m} is not imported"
|
||
let mut drvNames : Std.HashSet Name := {}
|
||
let mut drvConsts : Array (Name × ConstantInfo) := #[]
|
||
for (n, ci) in env.constants.toList do
|
||
let here : Bool :=
|
||
match env.getModuleIdxFor? n with
|
||
| some i => drvIdxs.contains i
|
||
| none => true -- declared by this file, still being elaborated
|
||
if here then
|
||
drvNames := drvNames.insert n
|
||
drvConsts := drvConsts.push (n, ci)
|
||
let mut drv : Array String := #[]
|
||
for (n, ci) in drvConsts do
|
||
let k := kindOf ci
|
||
-- An AXIOM in an instrument is never acceptable: it would widen the trusted
|
||
-- base without appearing in any certificate's cone.
|
||
if k == "axiom" then
|
||
throwError "DRIVER SURFACE VIOLATION: {n} is an axiom declared by the audit \
|
||
infrastructure. An instrument may not widen the trusted base."
|
||
-- A THEOREM needs care rather than a flat ban. Defining a function by
|
||
-- well-founded recursion makes the elaborator emit its own proof
|
||
-- obligations — `LTLAccAudit.axiomCone._proof_1` is one, and a flat ban
|
||
-- rejected this very file. The distinction that matters is whether the
|
||
-- theorem is a CLAIM someone wrote or an ARTEFACT of a definition here:
|
||
-- an artefact's name extends the name of a constant declared alongside it,
|
||
-- a standalone claim's does not.
|
||
if k == "theorem" && !drvNames.contains n.getPrefix then
|
||
throwError "DRIVER SURFACE VIOLATION: {n} is a standalone theorem declared by \
|
||
the audit infrastructure. An instrument may declare definitions \
|
||
and whatever the elaborator generates for them — never a claim \
|
||
of its own, which no certificate covers and no allowlist pins."
|
||
drv := drv.push s!"DRV|{n}|{k}"
|
||
let drvSorted := drv.qsort (· < ·)
|
||
for l in drvSorted do
|
||
IO.println l
|
||
IO.println s!"DRV-COUNT|{drvSorted.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
|