betrusted-ed25519-verified/verification/model-correspondence.py

260 lines
11 KiB
Python
Raw Normal View History

P2-c: classify and pin the extraction boundary Aeneas emits a *_Template.lean naming everything the extracted code needs from outside itself — the extraction's own statement of its boundary. extract.sh has always said, in prose, "after regenerating, diff the template against the hand-written file". Prose is not a gate, and the diff cannot be one: the two files legitimately differ in almost every line, holes and Aeneas comments against real definitions and modeling policy. MEASURING FIRST CHANGED WHAT THIS ITEM SHOULD BE. The TODO offered two options — enforce the diff, or pin both files — and the answer turned out to be neither. Both files were ALREADY byte-pinned by Phase 0b. And two further things stand here: the generated Funs.lean imports the model and CALLS these externals, so the Lean compiler enforces their TYPES wherever the extracted code uses them; and the per-certificate exact cones catch any external that becomes, or stops being, an assumption anything depends on. What none of those three sees is the CLASSIFICATION: for each name the extraction asks for, whether this repository answers with an ASSUMPTION or with a PROOF. That is the tier-A/B claim the documents make in prose — the curve calls and the three curve types resolve to proven definitions rather than axioms, because gen/CurveField/Funs.lean opens `namespace curve25519_dalek` and so defines the very names Aeneas asks for. Nothing checked it. A regeneration that renamed one, or a model that quietly answered one with an axiom instead, would have left the documents claiming a proof where the repository had an assumption. Phase 0d recomputes the classification with model-correspondence.py (namespace-aware, so a definition inside a namespace counts under its full name) and requires equality with the committed MODEL-CORRESPONDENCE.txt. UNRESOLVED — the extraction asking for something nothing here provides — is a hard failure. dalek 43 MODEL 8 PROVEN 3 EXTRA anza 38 MODEL 0 PROVEN 4 EXTRA (no CurveSig crate) risc0 36 MODEL 8 PROVEN 4 EXTRA betrusted 35 MODEL 8 PROVEN 4 EXTRA selftest-correspondence.sh, five cases, negative-tested by disabling the comparison. The case that matters is 2: a PROVEN external answered by an axiom instead. No name changes anywhere, every byte pin still matches, and it compiles, because the signature is unchanged — before Phase 0d nothing in the button could tell. Trap recorded for whoever extends it: case 3 first deleted the PROVEN rows, which was VACUOUS on anza, since anza has none — it removed nothing, the table still matched, and the case passed while testing nothing. It now deletes the first row whatever its verdict AND asserts the file changed. extract.sh now points at the gate instead of asking a human to look. Certified by a full sweep: both buttons, all four forks, purged trees, machine otherwise idle. 8/8 green.
2026-07-31 15:53:31 +00:00
#!/usr/bin/env python3
"""Classify every external the extraction declares.
For each gen/<dir>/<X>_Template.lean, Aeneas states what the extracted Rust
needs from outside. Each such name must be provided by exactly one of:
MODEL declared in the hand-written sibling gen/<dir>/<X>.lean: an
assumption, which the axiom gate and the per-certificate cones
then govern;
PROVEN resolved to a real definition in the proven corpus, because a
Round-7 F1: make model/template correspondence SEMANTIC, and fail closed THE DEFECT, as found. GPT-5.6 showed the textual classifier could report PROVEN for a name Lean resolves to an axiom: a definition appearing only inside a `/- -/` comment was read as real. Repairing that exposed something larger and already live. Aeneas wraps long declarations: axiom curve25519_dalek.edwards.EdwardsPoint.Insts.CoreOpsArithNegEdwardsPoint.neg The old scanner required keyword and name on one physical line, so it matched nothing there and SILENTLY DROPPED the declaration — no MODEL row, no PROVEN row, no failure. Nine to ten externals per fork had no row at all, and the tier-A/B `neg` was missing from every committed table while the trust documents claimed that class was machine-checked. Four pinned tables were wrong, in four repositories, with the buttons green over them. No attacker was required; Aeneas's own formatting did it. The lesson is not "write a better regex". The scanner was FAIL-OPEN: input it could not parse produced silence instead of a stop. A gate that drops what it cannot read is worse than no gate, because the green covers a gap that is invisible in the diff. THE REPAIR, in three layers that each do only what they honestly can. · DISCOVERY stays textual, because the template cannot be imported — it declares the same names as the model and they would clash. It now strips NESTED `/- -/` blocks, reads a name that wraps to a later line, tracks namespace/section/end, and FAILS CLOSED: an unparseable keyword exits 2 naming file and line. · Phase 0d keeps the cheap pre-compile comparison against the table. · Phase 2d is new and authoritative. After compilation it generates a temp Lean driver (the Phase 2b idiom, so no permanent module and no manifest churn), imports every non-template gen module, and asks env.find? what each external IS: kind and originating module. Model module -> MODEL; non-axiom in a generated module -> PROVEN; an axiom outside the model, or a name that does not resolve, is a failure. Lean's verdict must equal the committed table's, and the answer count must equal the question count so a truncated resolver cannot pass. THE TABLES were regenerated and verified three ways before being committed: an independently written second scanner agreed on 228/228 rows across the four forks; all 47 changed rows carry file:line evidence; and the PROVEN row was put to Lean directly — kind = def, module = CurveField.Funs. dalek 54 -> 64 rows anza 42 -> 51 risc0 48 -> 57 betrusted 47 -> 56 Also corrected: `from_residual` was recorded EXTRA in every fork; the template does demand it, so it is MODEL. anza carried a spurious `hash|EXTRA` row from a mis-parse. NEGATIVE-TESTED. With Phase 0d neutered so Phase 2d was the gate under test, a PROVEN row edited to MODEL produces: SEMANTIC DRIFT: ...neg — table says MODEL, Lean says PROVEN selftest-correspondence.sh grew 4 cases -> 7: GPT's comment counterexample, the wrapped declaration that was actually live, and a fail-closed check. WHAT THIS DOES NOT CLOSE. Phase 2d resolves names in the environment built from gen/. It establishes what a name IS — assumption or proof, and where — not that the extraction faithfully represents the Rust. That remains the Charon/Aeneas trust assumption and no gate here can close it. Certified: both buttons, all four forks, purged trees. 8/8 green, with 62/48/53/52 externals resolved by Lean and every verdict matching.
2026-08-02 00:24:15 +00:00
module of this repository declares it (namespace-aware).
P2-c: classify and pin the extraction boundary Aeneas emits a *_Template.lean naming everything the extracted code needs from outside itself — the extraction's own statement of its boundary. extract.sh has always said, in prose, "after regenerating, diff the template against the hand-written file". Prose is not a gate, and the diff cannot be one: the two files legitimately differ in almost every line, holes and Aeneas comments against real definitions and modeling policy. MEASURING FIRST CHANGED WHAT THIS ITEM SHOULD BE. The TODO offered two options — enforce the diff, or pin both files — and the answer turned out to be neither. Both files were ALREADY byte-pinned by Phase 0b. And two further things stand here: the generated Funs.lean imports the model and CALLS these externals, so the Lean compiler enforces their TYPES wherever the extracted code uses them; and the per-certificate exact cones catch any external that becomes, or stops being, an assumption anything depends on. What none of those three sees is the CLASSIFICATION: for each name the extraction asks for, whether this repository answers with an ASSUMPTION or with a PROOF. That is the tier-A/B claim the documents make in prose — the curve calls and the three curve types resolve to proven definitions rather than axioms, because gen/CurveField/Funs.lean opens `namespace curve25519_dalek` and so defines the very names Aeneas asks for. Nothing checked it. A regeneration that renamed one, or a model that quietly answered one with an axiom instead, would have left the documents claiming a proof where the repository had an assumption. Phase 0d recomputes the classification with model-correspondence.py (namespace-aware, so a definition inside a namespace counts under its full name) and requires equality with the committed MODEL-CORRESPONDENCE.txt. UNRESOLVED — the extraction asking for something nothing here provides — is a hard failure. dalek 43 MODEL 8 PROVEN 3 EXTRA anza 38 MODEL 0 PROVEN 4 EXTRA (no CurveSig crate) risc0 36 MODEL 8 PROVEN 4 EXTRA betrusted 35 MODEL 8 PROVEN 4 EXTRA selftest-correspondence.sh, five cases, negative-tested by disabling the comparison. The case that matters is 2: a PROVEN external answered by an axiom instead. No name changes anywhere, every byte pin still matches, and it compiles, because the signature is unchanged — before Phase 0d nothing in the button could tell. Trap recorded for whoever extends it: case 3 first deleted the PROVEN rows, which was VACUOUS on anza, since anza has none — it removed nothing, the table still matched, and the case passed while testing nothing. It now deletes the first row whatever its verdict AND asserts the file changed. extract.sh now points at the gate instead of asking a human to look. Certified by a full sweep: both buttons, all four forks, purged trees, machine otherwise idle. 8/8 green.
2026-07-31 15:53:31 +00:00
Anything else is drift: the extraction asks for something this repository does
not provide.
Round-7 F1: make model/template correspondence SEMANTIC, and fail closed THE DEFECT, as found. GPT-5.6 showed the textual classifier could report PROVEN for a name Lean resolves to an axiom: a definition appearing only inside a `/- -/` comment was read as real. Repairing that exposed something larger and already live. Aeneas wraps long declarations: axiom curve25519_dalek.edwards.EdwardsPoint.Insts.CoreOpsArithNegEdwardsPoint.neg The old scanner required keyword and name on one physical line, so it matched nothing there and SILENTLY DROPPED the declaration — no MODEL row, no PROVEN row, no failure. Nine to ten externals per fork had no row at all, and the tier-A/B `neg` was missing from every committed table while the trust documents claimed that class was machine-checked. Four pinned tables were wrong, in four repositories, with the buttons green over them. No attacker was required; Aeneas's own formatting did it. The lesson is not "write a better regex". The scanner was FAIL-OPEN: input it could not parse produced silence instead of a stop. A gate that drops what it cannot read is worse than no gate, because the green covers a gap that is invisible in the diff. THE REPAIR, in three layers that each do only what they honestly can. · DISCOVERY stays textual, because the template cannot be imported — it declares the same names as the model and they would clash. It now strips NESTED `/- -/` blocks, reads a name that wraps to a later line, tracks namespace/section/end, and FAILS CLOSED: an unparseable keyword exits 2 naming file and line. · Phase 0d keeps the cheap pre-compile comparison against the table. · Phase 2d is new and authoritative. After compilation it generates a temp Lean driver (the Phase 2b idiom, so no permanent module and no manifest churn), imports every non-template gen module, and asks env.find? what each external IS: kind and originating module. Model module -> MODEL; non-axiom in a generated module -> PROVEN; an axiom outside the model, or a name that does not resolve, is a failure. Lean's verdict must equal the committed table's, and the answer count must equal the question count so a truncated resolver cannot pass. THE TABLES were regenerated and verified three ways before being committed: an independently written second scanner agreed on 228/228 rows across the four forks; all 47 changed rows carry file:line evidence; and the PROVEN row was put to Lean directly — kind = def, module = CurveField.Funs. dalek 54 -> 64 rows anza 42 -> 51 risc0 48 -> 57 betrusted 47 -> 56 Also corrected: `from_residual` was recorded EXTRA in every fork; the template does demand it, so it is MODEL. anza carried a spurious `hash|EXTRA` row from a mis-parse. NEGATIVE-TESTED. With Phase 0d neutered so Phase 2d was the gate under test, a PROVEN row edited to MODEL produces: SEMANTIC DRIFT: ...neg — table says MODEL, Lean says PROVEN selftest-correspondence.sh grew 4 cases -> 7: GPT's comment counterexample, the wrapped declaration that was actually live, and a fail-closed check. WHAT THIS DOES NOT CLOSE. Phase 2d resolves names in the environment built from gen/. It establishes what a name IS — assumption or proof, and where — not that the extraction faithfully represents the Rust. That remains the Charon/Aeneas trust assumption and no gate here can close it. Certified: both buttons, all four forks, purged trees. 8/8 green, with 62/48/53/52 externals resolved by Lean and every verdict matching.
2026-08-02 00:24:15 +00:00
WHY THIS FILE WAS REWRITTEN 2026-08-01, round-7 external review
The first version matched declarations with a LINE-ORIENTED regex requiring the
keyword and the name on the same physical line, and it did not strip comments.
Both assumptions are false about Lean, and false about Aeneas's own output.
Three of the four forks contain, verbatim:
axiom
curve25519_dalek.edwards.EdwardsPoint.Insts.CoreOpsArithNegEdwardsPoint.neg
:
curve25519_dalek.edwards.EdwardsPoint -> Result ...
The old pattern matched nothing there, so that declaration was SILENTLY
DROPPED: no MODEL row, no PROVEN row, and no failure. Every committed
MODEL-CORRESPONDENCE.txt was missing it, and every button passed green over the
incomplete table. A reviewer separately showed that a definition appearing only
inside a `/- ... -/` comment was read as a real declaration, so the scanner
could also report PROVEN for a name Lean resolves to an axiom.
The lesson is not "write a better regex". It is that this scanner was
FAIL-OPEN: input it could not parse produced silence instead of a stop. A gate
that drops what it cannot read is worse than no gate, because the button prints
green across the gap and the gap is invisible in the diff.
This version therefore:
· strips comments first, including NESTED `/- ... -/` blocks, which Lean has
and which a non-greedy match would close at the first inner `-/`;
· allows a declaration's name to appear on a later line than its keyword;
· tracks `namespace` / `section` / `end` over the stripped text;
· FAILS CLOSED every declaration keyword must yield a name, or the scanner
exits non-zero naming file and line. Nothing is dropped, ever.
WHAT IT STILL IS NOT. This is a source scanner, not a semantic Lean query. It
cannot see `export`, aliases, or how Lean actually resolves a name at
elaboration. A PROVEN row is documentary evidence about the extraction
boundary; it is NOT a Lean-checked fact, and the trust documents must not claim
it is. What the estate relies on for soundness is kernel-side and
environment-derived Phase 2b's axiom gate, Phase 2c's inventory, and the
exact per-certificate cones of Phase 3/3b none of which consult this file.
P2-c: classify and pin the extraction boundary Aeneas emits a *_Template.lean naming everything the extracted code needs from outside itself — the extraction's own statement of its boundary. extract.sh has always said, in prose, "after regenerating, diff the template against the hand-written file". Prose is not a gate, and the diff cannot be one: the two files legitimately differ in almost every line, holes and Aeneas comments against real definitions and modeling policy. MEASURING FIRST CHANGED WHAT THIS ITEM SHOULD BE. The TODO offered two options — enforce the diff, or pin both files — and the answer turned out to be neither. Both files were ALREADY byte-pinned by Phase 0b. And two further things stand here: the generated Funs.lean imports the model and CALLS these externals, so the Lean compiler enforces their TYPES wherever the extracted code uses them; and the per-certificate exact cones catch any external that becomes, or stops being, an assumption anything depends on. What none of those three sees is the CLASSIFICATION: for each name the extraction asks for, whether this repository answers with an ASSUMPTION or with a PROOF. That is the tier-A/B claim the documents make in prose — the curve calls and the three curve types resolve to proven definitions rather than axioms, because gen/CurveField/Funs.lean opens `namespace curve25519_dalek` and so defines the very names Aeneas asks for. Nothing checked it. A regeneration that renamed one, or a model that quietly answered one with an axiom instead, would have left the documents claiming a proof where the repository had an assumption. Phase 0d recomputes the classification with model-correspondence.py (namespace-aware, so a definition inside a namespace counts under its full name) and requires equality with the committed MODEL-CORRESPONDENCE.txt. UNRESOLVED — the extraction asking for something nothing here provides — is a hard failure. dalek 43 MODEL 8 PROVEN 3 EXTRA anza 38 MODEL 0 PROVEN 4 EXTRA (no CurveSig crate) risc0 36 MODEL 8 PROVEN 4 EXTRA betrusted 35 MODEL 8 PROVEN 4 EXTRA selftest-correspondence.sh, five cases, negative-tested by disabling the comparison. The case that matters is 2: a PROVEN external answered by an axiom instead. No name changes anywhere, every byte pin still matches, and it compiles, because the signature is unchanged — before Phase 0d nothing in the button could tell. Trap recorded for whoever extends it: case 3 first deleted the PROVEN rows, which was VACUOUS on anza, since anza has none — it removed nothing, the table still matched, and the case passed while testing nothing. It now deletes the first row whatever its verdict AND asserts the file changed. extract.sh now points at the gate instead of asking a human to look. Certified by a full sweep: both buttons, all four forks, purged trees, machine otherwise idle. 8/8 green.
2026-07-31 15:53:31 +00:00
"""
Round-7 F1: make model/template correspondence SEMANTIC, and fail closed THE DEFECT, as found. GPT-5.6 showed the textual classifier could report PROVEN for a name Lean resolves to an axiom: a definition appearing only inside a `/- -/` comment was read as real. Repairing that exposed something larger and already live. Aeneas wraps long declarations: axiom curve25519_dalek.edwards.EdwardsPoint.Insts.CoreOpsArithNegEdwardsPoint.neg The old scanner required keyword and name on one physical line, so it matched nothing there and SILENTLY DROPPED the declaration — no MODEL row, no PROVEN row, no failure. Nine to ten externals per fork had no row at all, and the tier-A/B `neg` was missing from every committed table while the trust documents claimed that class was machine-checked. Four pinned tables were wrong, in four repositories, with the buttons green over them. No attacker was required; Aeneas's own formatting did it. The lesson is not "write a better regex". The scanner was FAIL-OPEN: input it could not parse produced silence instead of a stop. A gate that drops what it cannot read is worse than no gate, because the green covers a gap that is invisible in the diff. THE REPAIR, in three layers that each do only what they honestly can. · DISCOVERY stays textual, because the template cannot be imported — it declares the same names as the model and they would clash. It now strips NESTED `/- -/` blocks, reads a name that wraps to a later line, tracks namespace/section/end, and FAILS CLOSED: an unparseable keyword exits 2 naming file and line. · Phase 0d keeps the cheap pre-compile comparison against the table. · Phase 2d is new and authoritative. After compilation it generates a temp Lean driver (the Phase 2b idiom, so no permanent module and no manifest churn), imports every non-template gen module, and asks env.find? what each external IS: kind and originating module. Model module -> MODEL; non-axiom in a generated module -> PROVEN; an axiom outside the model, or a name that does not resolve, is a failure. Lean's verdict must equal the committed table's, and the answer count must equal the question count so a truncated resolver cannot pass. THE TABLES were regenerated and verified three ways before being committed: an independently written second scanner agreed on 228/228 rows across the four forks; all 47 changed rows carry file:line evidence; and the PROVEN row was put to Lean directly — kind = def, module = CurveField.Funs. dalek 54 -> 64 rows anza 42 -> 51 risc0 48 -> 57 betrusted 47 -> 56 Also corrected: `from_residual` was recorded EXTRA in every fork; the template does demand it, so it is MODEL. anza carried a spurious `hash|EXTRA` row from a mis-parse. NEGATIVE-TESTED. With Phase 0d neutered so Phase 2d was the gate under test, a PROVEN row edited to MODEL produces: SEMANTIC DRIFT: ...neg — table says MODEL, Lean says PROVEN selftest-correspondence.sh grew 4 cases -> 7: GPT's comment counterexample, the wrapped declaration that was actually live, and a fail-closed check. WHAT THIS DOES NOT CLOSE. Phase 2d resolves names in the environment built from gen/. It establishes what a name IS — assumption or proof, and where — not that the extraction faithfully represents the Rust. That remains the Charon/Aeneas trust assumption and no gate here can close it. Certified: both buttons, all four forks, purged trees. 8/8 green, with 62/48/53/52 externals resolved by Lean and every verdict matching.
2026-08-02 00:24:15 +00:00
import re
import sys
import os
import glob
KEYWORDS = ('axiom', 'def', 'abbrev', 'opaque', 'structure', 'inductive',
'instance', 'theorem', 'lemma')
# A declaration keyword opening a logical line, after any attributes and
# modifiers. The NAME is deliberately NOT part of this pattern: it may sit on a
# later line, which is precisely the case the previous scanner dropped.
KW = re.compile(
r'^[ \t]*(?:@\[[^\]]*\][ \t\n]*)*'
r'(?:private |protected |noncomputable |unsafe |partial |scoped |local )*'
r'(' + '|'.join(KEYWORDS) + r')(?=[ \t\n])',
re.M)
IDENT = re.compile(r"[ \t\n]*([A-Za-z_][A-Za-z0-9_.'!?]*)")
NS = re.compile(
r"^[ \t]*(namespace|section|end)(?:[ \t]+([A-Za-z_][A-Za-z0-9_.']*))?[ \t]*$",
re.M)
class ScanError(Exception):
"""Raised when a declaration cannot be parsed. Never swallowed."""
def strip_comments(text):
"""Remove Lean comments, preserving newlines so line numbers stay true.
Block comments NEST in Lean, so this needs a depth counter: a non-greedy
`/-.*?-/` would close the outer block at the first inner `-/` and leave the
tail of a nested comment looking like source.
"""
out, i, n, depth = [], 0, len(text), 0
while i < n:
if text.startswith('/-', i):
depth += 1
out.append(' ')
i += 2
continue
if text.startswith('-/', i):
if depth:
depth -= 1
out.append(' ')
i += 2
continue
if depth:
out.append('\n' if text[i] == '\n' else ' ')
i += 1
continue
if text.startswith('--', i):
j = text.find('\n', i)
if j < 0:
out.append(' ' * (n - i))
break
out.append(' ' * (j - i))
i = j
continue
out.append(text[i])
i += 1
return ''.join(out)
P2-c: classify and pin the extraction boundary Aeneas emits a *_Template.lean naming everything the extracted code needs from outside itself — the extraction's own statement of its boundary. extract.sh has always said, in prose, "after regenerating, diff the template against the hand-written file". Prose is not a gate, and the diff cannot be one: the two files legitimately differ in almost every line, holes and Aeneas comments against real definitions and modeling policy. MEASURING FIRST CHANGED WHAT THIS ITEM SHOULD BE. The TODO offered two options — enforce the diff, or pin both files — and the answer turned out to be neither. Both files were ALREADY byte-pinned by Phase 0b. And two further things stand here: the generated Funs.lean imports the model and CALLS these externals, so the Lean compiler enforces their TYPES wherever the extracted code uses them; and the per-certificate exact cones catch any external that becomes, or stops being, an assumption anything depends on. What none of those three sees is the CLASSIFICATION: for each name the extraction asks for, whether this repository answers with an ASSUMPTION or with a PROOF. That is the tier-A/B claim the documents make in prose — the curve calls and the three curve types resolve to proven definitions rather than axioms, because gen/CurveField/Funs.lean opens `namespace curve25519_dalek` and so defines the very names Aeneas asks for. Nothing checked it. A regeneration that renamed one, or a model that quietly answered one with an axiom instead, would have left the documents claiming a proof where the repository had an assumption. Phase 0d recomputes the classification with model-correspondence.py (namespace-aware, so a definition inside a namespace counts under its full name) and requires equality with the committed MODEL-CORRESPONDENCE.txt. UNRESOLVED — the extraction asking for something nothing here provides — is a hard failure. dalek 43 MODEL 8 PROVEN 3 EXTRA anza 38 MODEL 0 PROVEN 4 EXTRA (no CurveSig crate) risc0 36 MODEL 8 PROVEN 4 EXTRA betrusted 35 MODEL 8 PROVEN 4 EXTRA selftest-correspondence.sh, five cases, negative-tested by disabling the comparison. The case that matters is 2: a PROVEN external answered by an axiom instead. No name changes anywhere, every byte pin still matches, and it compiles, because the signature is unchanged — before Phase 0d nothing in the button could tell. Trap recorded for whoever extends it: case 3 first deleted the PROVEN rows, which was VACUOUS on anza, since anza has none — it removed nothing, the table still matched, and the case passed while testing nothing. It now deletes the first row whatever its verdict AND asserts the file changed. extract.sh now points at the gate instead of asking a human to look. Certified by a full sweep: both buttons, all four forks, purged trees, machine otherwise idle. 8/8 green.
2026-07-31 15:53:31 +00:00
def declared(path):
correspondence: a named section is not a namespace; an extra axiom is a failure Round-8 review (GPT-5.6, register key `section-prefix-bug`, CRITICAL). Reproduced here exactly before fixing. model-correspondence.py treated `namespace`, `section` and `end` as one event class and pushed a named section onto the fully-qualified-name prefix. Lean does not: `section Foo` opens a scope for `variable`/`open` and gives `end Foo` a label; it does not turn `bar` into `Foo.bar`. Given a template reading section Foo axiom bar : Nat end Foo the scanner reported `Foo.bar`, `--names` handed Phase 2d only `Foo.bar`, Lean resolved an unrelated `Foo.bar` definition elsewhere in the corpus, and the verdict came back PROVEN. The axiom the extraction ACTUALLY depends on was never queried. This survived both the fail-closed rewrite and the new Lean-semantic phase, in a scanner rewritten that same week specifically to stop dropping things. AND THE REASON IT STAYED SILENT, which is the half worth keeping. The real external did not vanish — it landed in the table as EXTRA, the one verdict that could not fail. A silent bucket beside a fail-closed parser is a slower way of dropping things. An extra AXIOM is now EXTRA-AXIOM and stops the button: the model exists to answer the template, so an assumption nothing asks for is either a parse we got wrong or an assumption nobody governs. Extra definitions stay tolerated; helpers in a model file are ordinary. That gate fired on the real corpora on its first run. Each fork's hand-maintained gen/CurveField/FunsExternal.lean carried AVX2/AVX512 backend axioms present in no template, no proof, no cone and no allowlist — dead assumptions in a pinned trusted-base file, reported as EXTRA and therefore invisible. extract.sh:16 confirms these files are never overwritten by extraction, so they were hand-written and are removed here: dalek 2, anza 3, risc0 4, betrusted 4 Nothing referenced them, so no certificate's cone changes; the trusted base simply gets smaller. Table rows 64->62, 51->48, 57->53, 56->52, and Phase 2d independently resolved 62/48/53/52 externals against the regenerated tables. GEN-MODEL.sha256 and HARNESS.sha256 both move: the model bytes changed, and the harness pins the table and the gen manifest themselves. Certified: round-10 sweep, 2h53m, ten instruments in each of four forks, 40/40 GREEN, 0 failing, 0 resource-limited. A full run was required — the --audit-only staleness gate correctly refused after a source change. Registered in formal-verification-control/review-findings.tsv as `section-prefix-bug` and `dead-model-axioms`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 19:29:54 +00:00
"""{fully-qualified name: declaration keyword} for one file.
Returns a MAPPING, not a set, because the keyword is load-bearing: an
`axiom` the template never asks for must stop the button, while an extra
`def` is an ordinary helper. Callers that only need names take `set(...)`.
Round-7 F1: make model/template correspondence SEMANTIC, and fail closed THE DEFECT, as found. GPT-5.6 showed the textual classifier could report PROVEN for a name Lean resolves to an axiom: a definition appearing only inside a `/- -/` comment was read as real. Repairing that exposed something larger and already live. Aeneas wraps long declarations: axiom curve25519_dalek.edwards.EdwardsPoint.Insts.CoreOpsArithNegEdwardsPoint.neg The old scanner required keyword and name on one physical line, so it matched nothing there and SILENTLY DROPPED the declaration — no MODEL row, no PROVEN row, no failure. Nine to ten externals per fork had no row at all, and the tier-A/B `neg` was missing from every committed table while the trust documents claimed that class was machine-checked. Four pinned tables were wrong, in four repositories, with the buttons green over them. No attacker was required; Aeneas's own formatting did it. The lesson is not "write a better regex". The scanner was FAIL-OPEN: input it could not parse produced silence instead of a stop. A gate that drops what it cannot read is worse than no gate, because the green covers a gap that is invisible in the diff. THE REPAIR, in three layers that each do only what they honestly can. · DISCOVERY stays textual, because the template cannot be imported — it declares the same names as the model and they would clash. It now strips NESTED `/- -/` blocks, reads a name that wraps to a later line, tracks namespace/section/end, and FAILS CLOSED: an unparseable keyword exits 2 naming file and line. · Phase 0d keeps the cheap pre-compile comparison against the table. · Phase 2d is new and authoritative. After compilation it generates a temp Lean driver (the Phase 2b idiom, so no permanent module and no manifest churn), imports every non-template gen module, and asks env.find? what each external IS: kind and originating module. Model module -> MODEL; non-axiom in a generated module -> PROVEN; an axiom outside the model, or a name that does not resolve, is a failure. Lean's verdict must equal the committed table's, and the answer count must equal the question count so a truncated resolver cannot pass. THE TABLES were regenerated and verified three ways before being committed: an independently written second scanner agreed on 228/228 rows across the four forks; all 47 changed rows carry file:line evidence; and the PROVEN row was put to Lean directly — kind = def, module = CurveField.Funs. dalek 54 -> 64 rows anza 42 -> 51 risc0 48 -> 57 betrusted 47 -> 56 Also corrected: `from_residual` was recorded EXTRA in every fork; the template does demand it, so it is MODEL. anza carried a spurious `hash|EXTRA` row from a mis-parse. NEGATIVE-TESTED. With Phase 0d neutered so Phase 2d was the gate under test, a PROVEN row edited to MODEL produces: SEMANTIC DRIFT: ...neg — table says MODEL, Lean says PROVEN selftest-correspondence.sh grew 4 cases -> 7: GPT's comment counterexample, the wrapped declaration that was actually live, and a fail-closed check. WHAT THIS DOES NOT CLOSE. Phase 2d resolves names in the environment built from gen/. It establishes what a name IS — assumption or proof, and where — not that the extraction faithfully represents the Rust. That remains the Charon/Aeneas trust assumption and no gate here can close it. Certified: both buttons, all four forks, purged trees. 8/8 green, with 62/48/53/52 externals resolved by Lean and every verdict matching.
2026-08-02 00:24:15 +00:00
Raises ScanError on any declaration keyword whose name cannot be read.
"""
raw = open(path, encoding='utf-8', errors='replace').read()
text = strip_comments(raw)
# Scope events by offset, so each declaration can be placed in its stack.
events = [(m.start(), m.group(1), m.group(2)) for m in NS.finditer(text)]
correspondence: a named section is not a namespace; an extra axiom is a failure Round-8 review (GPT-5.6, register key `section-prefix-bug`, CRITICAL). Reproduced here exactly before fixing. model-correspondence.py treated `namespace`, `section` and `end` as one event class and pushed a named section onto the fully-qualified-name prefix. Lean does not: `section Foo` opens a scope for `variable`/`open` and gives `end Foo` a label; it does not turn `bar` into `Foo.bar`. Given a template reading section Foo axiom bar : Nat end Foo the scanner reported `Foo.bar`, `--names` handed Phase 2d only `Foo.bar`, Lean resolved an unrelated `Foo.bar` definition elsewhere in the corpus, and the verdict came back PROVEN. The axiom the extraction ACTUALLY depends on was never queried. This survived both the fail-closed rewrite and the new Lean-semantic phase, in a scanner rewritten that same week specifically to stop dropping things. AND THE REASON IT STAYED SILENT, which is the half worth keeping. The real external did not vanish — it landed in the table as EXTRA, the one verdict that could not fail. A silent bucket beside a fail-closed parser is a slower way of dropping things. An extra AXIOM is now EXTRA-AXIOM and stops the button: the model exists to answer the template, so an assumption nothing asks for is either a parse we got wrong or an assumption nobody governs. Extra definitions stay tolerated; helpers in a model file are ordinary. That gate fired on the real corpora on its first run. Each fork's hand-maintained gen/CurveField/FunsExternal.lean carried AVX2/AVX512 backend axioms present in no template, no proof, no cone and no allowlist — dead assumptions in a pinned trusted-base file, reported as EXTRA and therefore invisible. extract.sh:16 confirms these files are never overwritten by extraction, so they were hand-written and are removed here: dalek 2, anza 3, risc0 4, betrusted 4 Nothing referenced them, so no certificate's cone changes; the trusted base simply gets smaller. Table rows 64->62, 51->48, 57->53, 56->52, and Phase 2d independently resolved 62/48/53/52 externals against the regenerated tables. GEN-MODEL.sha256 and HARNESS.sha256 both move: the model bytes changed, and the harness pins the table and the gen manifest themselves. Certified: round-10 sweep, 2h53m, ten instruments in each of four forks, 40/40 GREEN, 0 failing, 0 resource-limited. A full run was required — the --audit-only staleness gate correctly refused after a source change. Registered in formal-verification-control/review-findings.tsv as `section-prefix-bug` and `dead-model-axioms`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 19:29:54 +00:00
names = {}
Round-7 F1: make model/template correspondence SEMANTIC, and fail closed THE DEFECT, as found. GPT-5.6 showed the textual classifier could report PROVEN for a name Lean resolves to an axiom: a definition appearing only inside a `/- -/` comment was read as real. Repairing that exposed something larger and already live. Aeneas wraps long declarations: axiom curve25519_dalek.edwards.EdwardsPoint.Insts.CoreOpsArithNegEdwardsPoint.neg The old scanner required keyword and name on one physical line, so it matched nothing there and SILENTLY DROPPED the declaration — no MODEL row, no PROVEN row, no failure. Nine to ten externals per fork had no row at all, and the tier-A/B `neg` was missing from every committed table while the trust documents claimed that class was machine-checked. Four pinned tables were wrong, in four repositories, with the buttons green over them. No attacker was required; Aeneas's own formatting did it. The lesson is not "write a better regex". The scanner was FAIL-OPEN: input it could not parse produced silence instead of a stop. A gate that drops what it cannot read is worse than no gate, because the green covers a gap that is invisible in the diff. THE REPAIR, in three layers that each do only what they honestly can. · DISCOVERY stays textual, because the template cannot be imported — it declares the same names as the model and they would clash. It now strips NESTED `/- -/` blocks, reads a name that wraps to a later line, tracks namespace/section/end, and FAILS CLOSED: an unparseable keyword exits 2 naming file and line. · Phase 0d keeps the cheap pre-compile comparison against the table. · Phase 2d is new and authoritative. After compilation it generates a temp Lean driver (the Phase 2b idiom, so no permanent module and no manifest churn), imports every non-template gen module, and asks env.find? what each external IS: kind and originating module. Model module -> MODEL; non-axiom in a generated module -> PROVEN; an axiom outside the model, or a name that does not resolve, is a failure. Lean's verdict must equal the committed table's, and the answer count must equal the question count so a truncated resolver cannot pass. THE TABLES were regenerated and verified three ways before being committed: an independently written second scanner agreed on 228/228 rows across the four forks; all 47 changed rows carry file:line evidence; and the PROVEN row was put to Lean directly — kind = def, module = CurveField.Funs. dalek 54 -> 64 rows anza 42 -> 51 risc0 48 -> 57 betrusted 47 -> 56 Also corrected: `from_residual` was recorded EXTRA in every fork; the template does demand it, so it is MODEL. anza carried a spurious `hash|EXTRA` row from a mis-parse. NEGATIVE-TESTED. With Phase 0d neutered so Phase 2d was the gate under test, a PROVEN row edited to MODEL produces: SEMANTIC DRIFT: ...neg — table says MODEL, Lean says PROVEN selftest-correspondence.sh grew 4 cases -> 7: GPT's comment counterexample, the wrapped declaration that was actually live, and a fail-closed check. WHAT THIS DOES NOT CLOSE. Phase 2d resolves names in the environment built from gen/. It establishes what a name IS — assumption or proof, and where — not that the extraction faithfully represents the Rust. That remains the Charon/Aeneas trust assumption and no gate here can close it. Certified: both buttons, all four forks, purged trees. 8/8 green, with 62/48/53/52 externals resolved by Lean and every verdict matching.
2026-08-02 00:24:15 +00:00
for m in KW.finditer(text):
im = IDENT.match(text, m.end())
if not im:
line = text.count('\n', 0, m.start()) + 1
raise ScanError(
"%s:%d: `%s` with no parseable name. This scanner fails closed:"
" it will not drop a declaration it cannot read."
% (path, line, m.group(1)))
stack = []
for off, kind, arg in events:
if off > m.start():
break
correspondence: a named section is not a namespace; an extra axiom is a failure Round-8 review (GPT-5.6, register key `section-prefix-bug`, CRITICAL). Reproduced here exactly before fixing. model-correspondence.py treated `namespace`, `section` and `end` as one event class and pushed a named section onto the fully-qualified-name prefix. Lean does not: `section Foo` opens a scope for `variable`/`open` and gives `end Foo` a label; it does not turn `bar` into `Foo.bar`. Given a template reading section Foo axiom bar : Nat end Foo the scanner reported `Foo.bar`, `--names` handed Phase 2d only `Foo.bar`, Lean resolved an unrelated `Foo.bar` definition elsewhere in the corpus, and the verdict came back PROVEN. The axiom the extraction ACTUALLY depends on was never queried. This survived both the fail-closed rewrite and the new Lean-semantic phase, in a scanner rewritten that same week specifically to stop dropping things. AND THE REASON IT STAYED SILENT, which is the half worth keeping. The real external did not vanish — it landed in the table as EXTRA, the one verdict that could not fail. A silent bucket beside a fail-closed parser is a slower way of dropping things. An extra AXIOM is now EXTRA-AXIOM and stops the button: the model exists to answer the template, so an assumption nothing asks for is either a parse we got wrong or an assumption nobody governs. Extra definitions stay tolerated; helpers in a model file are ordinary. That gate fired on the real corpora on its first run. Each fork's hand-maintained gen/CurveField/FunsExternal.lean carried AVX2/AVX512 backend axioms present in no template, no proof, no cone and no allowlist — dead assumptions in a pinned trusted-base file, reported as EXTRA and therefore invisible. extract.sh:16 confirms these files are never overwritten by extraction, so they were hand-written and are removed here: dalek 2, anza 3, risc0 4, betrusted 4 Nothing referenced them, so no certificate's cone changes; the trusted base simply gets smaller. Table rows 64->62, 51->48, 57->53, 56->52, and Phase 2d independently resolved 62/48/53/52 externals against the regenerated tables. GEN-MODEL.sha256 and HARNESS.sha256 both move: the model bytes changed, and the harness pins the table and the gen manifest themselves. Certified: round-10 sweep, 2h53m, ten instruments in each of four forks, 40/40 GREEN, 0 failing, 0 resource-limited. A full run was required — the --audit-only staleness gate correctly refused after a source change. Registered in formal-verification-control/review-findings.tsv as `section-prefix-bug` and `dead-model-axioms`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 19:29:54 +00:00
if kind == 'namespace':
Round-7 F1: make model/template correspondence SEMANTIC, and fail closed THE DEFECT, as found. GPT-5.6 showed the textual classifier could report PROVEN for a name Lean resolves to an axiom: a definition appearing only inside a `/- -/` comment was read as real. Repairing that exposed something larger and already live. Aeneas wraps long declarations: axiom curve25519_dalek.edwards.EdwardsPoint.Insts.CoreOpsArithNegEdwardsPoint.neg The old scanner required keyword and name on one physical line, so it matched nothing there and SILENTLY DROPPED the declaration — no MODEL row, no PROVEN row, no failure. Nine to ten externals per fork had no row at all, and the tier-A/B `neg` was missing from every committed table while the trust documents claimed that class was machine-checked. Four pinned tables were wrong, in four repositories, with the buttons green over them. No attacker was required; Aeneas's own formatting did it. The lesson is not "write a better regex". The scanner was FAIL-OPEN: input it could not parse produced silence instead of a stop. A gate that drops what it cannot read is worse than no gate, because the green covers a gap that is invisible in the diff. THE REPAIR, in three layers that each do only what they honestly can. · DISCOVERY stays textual, because the template cannot be imported — it declares the same names as the model and they would clash. It now strips NESTED `/- -/` blocks, reads a name that wraps to a later line, tracks namespace/section/end, and FAILS CLOSED: an unparseable keyword exits 2 naming file and line. · Phase 0d keeps the cheap pre-compile comparison against the table. · Phase 2d is new and authoritative. After compilation it generates a temp Lean driver (the Phase 2b idiom, so no permanent module and no manifest churn), imports every non-template gen module, and asks env.find? what each external IS: kind and originating module. Model module -> MODEL; non-axiom in a generated module -> PROVEN; an axiom outside the model, or a name that does not resolve, is a failure. Lean's verdict must equal the committed table's, and the answer count must equal the question count so a truncated resolver cannot pass. THE TABLES were regenerated and verified three ways before being committed: an independently written second scanner agreed on 228/228 rows across the four forks; all 47 changed rows carry file:line evidence; and the PROVEN row was put to Lean directly — kind = def, module = CurveField.Funs. dalek 54 -> 64 rows anza 42 -> 51 risc0 48 -> 57 betrusted 47 -> 56 Also corrected: `from_residual` was recorded EXTRA in every fork; the template does demand it, so it is MODEL. anza carried a spurious `hash|EXTRA` row from a mis-parse. NEGATIVE-TESTED. With Phase 0d neutered so Phase 2d was the gate under test, a PROVEN row edited to MODEL produces: SEMANTIC DRIFT: ...neg — table says MODEL, Lean says PROVEN selftest-correspondence.sh grew 4 cases -> 7: GPT's comment counterexample, the wrapped declaration that was actually live, and a fail-closed check. WHAT THIS DOES NOT CLOSE. Phase 2d resolves names in the environment built from gen/. It establishes what a name IS — assumption or proof, and where — not that the extraction faithfully represents the Rust. That remains the Charon/Aeneas trust assumption and no gate here can close it. Certified: both buttons, all four forks, purged trees. 8/8 green, with 62/48/53/52 externals resolved by Lean and every verdict matching.
2026-08-02 00:24:15 +00:00
stack.append(arg)
correspondence: a named section is not a namespace; an extra axiom is a failure Round-8 review (GPT-5.6, register key `section-prefix-bug`, CRITICAL). Reproduced here exactly before fixing. model-correspondence.py treated `namespace`, `section` and `end` as one event class and pushed a named section onto the fully-qualified-name prefix. Lean does not: `section Foo` opens a scope for `variable`/`open` and gives `end Foo` a label; it does not turn `bar` into `Foo.bar`. Given a template reading section Foo axiom bar : Nat end Foo the scanner reported `Foo.bar`, `--names` handed Phase 2d only `Foo.bar`, Lean resolved an unrelated `Foo.bar` definition elsewhere in the corpus, and the verdict came back PROVEN. The axiom the extraction ACTUALLY depends on was never queried. This survived both the fail-closed rewrite and the new Lean-semantic phase, in a scanner rewritten that same week specifically to stop dropping things. AND THE REASON IT STAYED SILENT, which is the half worth keeping. The real external did not vanish — it landed in the table as EXTRA, the one verdict that could not fail. A silent bucket beside a fail-closed parser is a slower way of dropping things. An extra AXIOM is now EXTRA-AXIOM and stops the button: the model exists to answer the template, so an assumption nothing asks for is either a parse we got wrong or an assumption nobody governs. Extra definitions stay tolerated; helpers in a model file are ordinary. That gate fired on the real corpora on its first run. Each fork's hand-maintained gen/CurveField/FunsExternal.lean carried AVX2/AVX512 backend axioms present in no template, no proof, no cone and no allowlist — dead assumptions in a pinned trusted-base file, reported as EXTRA and therefore invisible. extract.sh:16 confirms these files are never overwritten by extraction, so they were hand-written and are removed here: dalek 2, anza 3, risc0 4, betrusted 4 Nothing referenced them, so no certificate's cone changes; the trusted base simply gets smaller. Table rows 64->62, 51->48, 57->53, 56->52, and Phase 2d independently resolved 62/48/53/52 externals against the regenerated tables. GEN-MODEL.sha256 and HARNESS.sha256 both move: the model bytes changed, and the harness pins the table and the gen manifest themselves. Certified: round-10 sweep, 2h53m, ten instruments in each of four forks, 40/40 GREEN, 0 failing, 0 resource-limited. A full run was required — the --audit-only staleness gate correctly refused after a source change. Registered in formal-verification-control/review-findings.tsv as `section-prefix-bug` and `dead-model-axioms`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 19:29:54 +00:00
elif kind == 'section':
# A NAMED SECTION DOES NOT QUALIFY DECLARATION NAMES. `section
# Foo` opens a scope for `variable`/`open` and gives `end Foo` a
# label to match; it does not make `bar` into `Foo.bar`. This
# line pushed `arg`, so a template reading
# section Foo
# axiom bar : Nat
# end Foo
# was reported as declaring `Foo.bar`. Round-8 review (GPT-5.6,
# register key `section-prefix-bug`) showed the consequence:
# `--names` handed Phase 2d only `Foo.bar`, Lean happily
# resolved an unrelated `Foo.bar` definition elsewhere in the
# corpus and returned PROVEN, and the axiom the extraction
# ACTUALLY depends on was never queried at all. The scanner had
# been rewritten that same week specifically to be fail-closed.
# None appends a frame so `end` still balances, and the
# comprehension below drops it from the prefix.
stack.append(None)
Round-7 F1: make model/template correspondence SEMANTIC, and fail closed THE DEFECT, as found. GPT-5.6 showed the textual classifier could report PROVEN for a name Lean resolves to an axiom: a definition appearing only inside a `/- -/` comment was read as real. Repairing that exposed something larger and already live. Aeneas wraps long declarations: axiom curve25519_dalek.edwards.EdwardsPoint.Insts.CoreOpsArithNegEdwardsPoint.neg The old scanner required keyword and name on one physical line, so it matched nothing there and SILENTLY DROPPED the declaration — no MODEL row, no PROVEN row, no failure. Nine to ten externals per fork had no row at all, and the tier-A/B `neg` was missing from every committed table while the trust documents claimed that class was machine-checked. Four pinned tables were wrong, in four repositories, with the buttons green over them. No attacker was required; Aeneas's own formatting did it. The lesson is not "write a better regex". The scanner was FAIL-OPEN: input it could not parse produced silence instead of a stop. A gate that drops what it cannot read is worse than no gate, because the green covers a gap that is invisible in the diff. THE REPAIR, in three layers that each do only what they honestly can. · DISCOVERY stays textual, because the template cannot be imported — it declares the same names as the model and they would clash. It now strips NESTED `/- -/` blocks, reads a name that wraps to a later line, tracks namespace/section/end, and FAILS CLOSED: an unparseable keyword exits 2 naming file and line. · Phase 0d keeps the cheap pre-compile comparison against the table. · Phase 2d is new and authoritative. After compilation it generates a temp Lean driver (the Phase 2b idiom, so no permanent module and no manifest churn), imports every non-template gen module, and asks env.find? what each external IS: kind and originating module. Model module -> MODEL; non-axiom in a generated module -> PROVEN; an axiom outside the model, or a name that does not resolve, is a failure. Lean's verdict must equal the committed table's, and the answer count must equal the question count so a truncated resolver cannot pass. THE TABLES were regenerated and verified three ways before being committed: an independently written second scanner agreed on 228/228 rows across the four forks; all 47 changed rows carry file:line evidence; and the PROVEN row was put to Lean directly — kind = def, module = CurveField.Funs. dalek 54 -> 64 rows anza 42 -> 51 risc0 48 -> 57 betrusted 47 -> 56 Also corrected: `from_residual` was recorded EXTRA in every fork; the template does demand it, so it is MODEL. anza carried a spurious `hash|EXTRA` row from a mis-parse. NEGATIVE-TESTED. With Phase 0d neutered so Phase 2d was the gate under test, a PROVEN row edited to MODEL produces: SEMANTIC DRIFT: ...neg — table says MODEL, Lean says PROVEN selftest-correspondence.sh grew 4 cases -> 7: GPT's comment counterexample, the wrapped declaration that was actually live, and a fail-closed check. WHAT THIS DOES NOT CLOSE. Phase 2d resolves names in the environment built from gen/. It establishes what a name IS — assumption or proof, and where — not that the extraction faithfully represents the Rust. That remains the Charon/Aeneas trust assumption and no gate here can close it. Certified: both buttons, all four forks, purged trees. 8/8 green, with 62/48/53/52 externals resolved by Lean and every verdict matching.
2026-08-02 00:24:15 +00:00
elif stack:
P2-c: classify and pin the extraction boundary Aeneas emits a *_Template.lean naming everything the extracted code needs from outside itself — the extraction's own statement of its boundary. extract.sh has always said, in prose, "after regenerating, diff the template against the hand-written file". Prose is not a gate, and the diff cannot be one: the two files legitimately differ in almost every line, holes and Aeneas comments against real definitions and modeling policy. MEASURING FIRST CHANGED WHAT THIS ITEM SHOULD BE. The TODO offered two options — enforce the diff, or pin both files — and the answer turned out to be neither. Both files were ALREADY byte-pinned by Phase 0b. And two further things stand here: the generated Funs.lean imports the model and CALLS these externals, so the Lean compiler enforces their TYPES wherever the extracted code uses them; and the per-certificate exact cones catch any external that becomes, or stops being, an assumption anything depends on. What none of those three sees is the CLASSIFICATION: for each name the extraction asks for, whether this repository answers with an ASSUMPTION or with a PROOF. That is the tier-A/B claim the documents make in prose — the curve calls and the three curve types resolve to proven definitions rather than axioms, because gen/CurveField/Funs.lean opens `namespace curve25519_dalek` and so defines the very names Aeneas asks for. Nothing checked it. A regeneration that renamed one, or a model that quietly answered one with an axiom instead, would have left the documents claiming a proof where the repository had an assumption. Phase 0d recomputes the classification with model-correspondence.py (namespace-aware, so a definition inside a namespace counts under its full name) and requires equality with the committed MODEL-CORRESPONDENCE.txt. UNRESOLVED — the extraction asking for something nothing here provides — is a hard failure. dalek 43 MODEL 8 PROVEN 3 EXTRA anza 38 MODEL 0 PROVEN 4 EXTRA (no CurveSig crate) risc0 36 MODEL 8 PROVEN 4 EXTRA betrusted 35 MODEL 8 PROVEN 4 EXTRA selftest-correspondence.sh, five cases, negative-tested by disabling the comparison. The case that matters is 2: a PROVEN external answered by an axiom instead. No name changes anywhere, every byte pin still matches, and it compiles, because the signature is unchanged — before Phase 0d nothing in the button could tell. Trap recorded for whoever extends it: case 3 first deleted the PROVEN rows, which was VACUOUS on anza, since anza has none — it removed nothing, the table still matched, and the case passed while testing nothing. It now deletes the first row whatever its verdict AND asserts the file changed. extract.sh now points at the gate instead of asking a human to look. Certified by a full sweep: both buttons, all four forks, purged trees, machine otherwise idle. 8/8 green.
2026-07-31 15:53:31 +00:00
stack.pop()
Round-7 F1: make model/template correspondence SEMANTIC, and fail closed THE DEFECT, as found. GPT-5.6 showed the textual classifier could report PROVEN for a name Lean resolves to an axiom: a definition appearing only inside a `/- -/` comment was read as real. Repairing that exposed something larger and already live. Aeneas wraps long declarations: axiom curve25519_dalek.edwards.EdwardsPoint.Insts.CoreOpsArithNegEdwardsPoint.neg The old scanner required keyword and name on one physical line, so it matched nothing there and SILENTLY DROPPED the declaration — no MODEL row, no PROVEN row, no failure. Nine to ten externals per fork had no row at all, and the tier-A/B `neg` was missing from every committed table while the trust documents claimed that class was machine-checked. Four pinned tables were wrong, in four repositories, with the buttons green over them. No attacker was required; Aeneas's own formatting did it. The lesson is not "write a better regex". The scanner was FAIL-OPEN: input it could not parse produced silence instead of a stop. A gate that drops what it cannot read is worse than no gate, because the green covers a gap that is invisible in the diff. THE REPAIR, in three layers that each do only what they honestly can. · DISCOVERY stays textual, because the template cannot be imported — it declares the same names as the model and they would clash. It now strips NESTED `/- -/` blocks, reads a name that wraps to a later line, tracks namespace/section/end, and FAILS CLOSED: an unparseable keyword exits 2 naming file and line. · Phase 0d keeps the cheap pre-compile comparison against the table. · Phase 2d is new and authoritative. After compilation it generates a temp Lean driver (the Phase 2b idiom, so no permanent module and no manifest churn), imports every non-template gen module, and asks env.find? what each external IS: kind and originating module. Model module -> MODEL; non-axiom in a generated module -> PROVEN; an axiom outside the model, or a name that does not resolve, is a failure. Lean's verdict must equal the committed table's, and the answer count must equal the question count so a truncated resolver cannot pass. THE TABLES were regenerated and verified three ways before being committed: an independently written second scanner agreed on 228/228 rows across the four forks; all 47 changed rows carry file:line evidence; and the PROVEN row was put to Lean directly — kind = def, module = CurveField.Funs. dalek 54 -> 64 rows anza 42 -> 51 risc0 48 -> 57 betrusted 47 -> 56 Also corrected: `from_residual` was recorded EXTRA in every fork; the template does demand it, so it is MODEL. anza carried a spurious `hash|EXTRA` row from a mis-parse. NEGATIVE-TESTED. With Phase 0d neutered so Phase 2d was the gate under test, a PROVEN row edited to MODEL produces: SEMANTIC DRIFT: ...neg — table says MODEL, Lean says PROVEN selftest-correspondence.sh grew 4 cases -> 7: GPT's comment counterexample, the wrapped declaration that was actually live, and a fail-closed check. WHAT THIS DOES NOT CLOSE. Phase 2d resolves names in the environment built from gen/. It establishes what a name IS — assumption or proof, and where — not that the extraction faithfully represents the Rust. That remains the Charon/Aeneas trust assumption and no gate here can close it. Certified: both buttons, all four forks, purged trees. 8/8 green, with 62/48/53/52 externals resolved by Lean and every verdict matching.
2026-08-02 00:24:15 +00:00
prefix = [p for p in stack if p]
correspondence: a named section is not a namespace; an extra axiom is a failure Round-8 review (GPT-5.6, register key `section-prefix-bug`, CRITICAL). Reproduced here exactly before fixing. model-correspondence.py treated `namespace`, `section` and `end` as one event class and pushed a named section onto the fully-qualified-name prefix. Lean does not: `section Foo` opens a scope for `variable`/`open` and gives `end Foo` a label; it does not turn `bar` into `Foo.bar`. Given a template reading section Foo axiom bar : Nat end Foo the scanner reported `Foo.bar`, `--names` handed Phase 2d only `Foo.bar`, Lean resolved an unrelated `Foo.bar` definition elsewhere in the corpus, and the verdict came back PROVEN. The axiom the extraction ACTUALLY depends on was never queried. This survived both the fail-closed rewrite and the new Lean-semantic phase, in a scanner rewritten that same week specifically to stop dropping things. AND THE REASON IT STAYED SILENT, which is the half worth keeping. The real external did not vanish — it landed in the table as EXTRA, the one verdict that could not fail. A silent bucket beside a fail-closed parser is a slower way of dropping things. An extra AXIOM is now EXTRA-AXIOM and stops the button: the model exists to answer the template, so an assumption nothing asks for is either a parse we got wrong or an assumption nobody governs. Extra definitions stay tolerated; helpers in a model file are ordinary. That gate fired on the real corpora on its first run. Each fork's hand-maintained gen/CurveField/FunsExternal.lean carried AVX2/AVX512 backend axioms present in no template, no proof, no cone and no allowlist — dead assumptions in a pinned trusted-base file, reported as EXTRA and therefore invisible. extract.sh:16 confirms these files are never overwritten by extraction, so they were hand-written and are removed here: dalek 2, anza 3, risc0 4, betrusted 4 Nothing referenced them, so no certificate's cone changes; the trusted base simply gets smaller. Table rows 64->62, 51->48, 57->53, 56->52, and Phase 2d independently resolved 62/48/53/52 externals against the regenerated tables. GEN-MODEL.sha256 and HARNESS.sha256 both move: the model bytes changed, and the harness pins the table and the gen manifest themselves. Certified: round-10 sweep, 2h53m, ten instruments in each of four forks, 40/40 GREEN, 0 failing, 0 resource-limited. A full run was required — the --audit-only staleness gate correctly refused after a source change. Registered in formal-verification-control/review-findings.tsv as `section-prefix-bug` and `dead-model-axioms`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 19:29:54 +00:00
full = '.'.join(prefix + [im.group(1)]) if prefix else im.group(1)
names.setdefault(full, m.group(1))
P2-c: classify and pin the extraction boundary Aeneas emits a *_Template.lean naming everything the extracted code needs from outside itself — the extraction's own statement of its boundary. extract.sh has always said, in prose, "after regenerating, diff the template against the hand-written file". Prose is not a gate, and the diff cannot be one: the two files legitimately differ in almost every line, holes and Aeneas comments against real definitions and modeling policy. MEASURING FIRST CHANGED WHAT THIS ITEM SHOULD BE. The TODO offered two options — enforce the diff, or pin both files — and the answer turned out to be neither. Both files were ALREADY byte-pinned by Phase 0b. And two further things stand here: the generated Funs.lean imports the model and CALLS these externals, so the Lean compiler enforces their TYPES wherever the extracted code uses them; and the per-certificate exact cones catch any external that becomes, or stops being, an assumption anything depends on. What none of those three sees is the CLASSIFICATION: for each name the extraction asks for, whether this repository answers with an ASSUMPTION or with a PROOF. That is the tier-A/B claim the documents make in prose — the curve calls and the three curve types resolve to proven definitions rather than axioms, because gen/CurveField/Funs.lean opens `namespace curve25519_dalek` and so defines the very names Aeneas asks for. Nothing checked it. A regeneration that renamed one, or a model that quietly answered one with an axiom instead, would have left the documents claiming a proof where the repository had an assumption. Phase 0d recomputes the classification with model-correspondence.py (namespace-aware, so a definition inside a namespace counts under its full name) and requires equality with the committed MODEL-CORRESPONDENCE.txt. UNRESOLVED — the extraction asking for something nothing here provides — is a hard failure. dalek 43 MODEL 8 PROVEN 3 EXTRA anza 38 MODEL 0 PROVEN 4 EXTRA (no CurveSig crate) risc0 36 MODEL 8 PROVEN 4 EXTRA betrusted 35 MODEL 8 PROVEN 4 EXTRA selftest-correspondence.sh, five cases, negative-tested by disabling the comparison. The case that matters is 2: a PROVEN external answered by an axiom instead. No name changes anywhere, every byte pin still matches, and it compiles, because the signature is unchanged — before Phase 0d nothing in the button could tell. Trap recorded for whoever extends it: case 3 first deleted the PROVEN rows, which was VACUOUS on anza, since anza has none — it removed nothing, the table still matched, and the case passed while testing nothing. It now deletes the first row whatever its verdict AND asserts the file changed. extract.sh now points at the gate instead of asking a human to look. Certified by a full sweep: both buttons, all four forks, purged trees, machine otherwise idle. 8/8 green.
2026-07-31 15:53:31 +00:00
return names
Round-7 F1: make model/template correspondence SEMANTIC, and fail closed THE DEFECT, as found. GPT-5.6 showed the textual classifier could report PROVEN for a name Lean resolves to an axiom: a definition appearing only inside a `/- -/` comment was read as real. Repairing that exposed something larger and already live. Aeneas wraps long declarations: axiom curve25519_dalek.edwards.EdwardsPoint.Insts.CoreOpsArithNegEdwardsPoint.neg The old scanner required keyword and name on one physical line, so it matched nothing there and SILENTLY DROPPED the declaration — no MODEL row, no PROVEN row, no failure. Nine to ten externals per fork had no row at all, and the tier-A/B `neg` was missing from every committed table while the trust documents claimed that class was machine-checked. Four pinned tables were wrong, in four repositories, with the buttons green over them. No attacker was required; Aeneas's own formatting did it. The lesson is not "write a better regex". The scanner was FAIL-OPEN: input it could not parse produced silence instead of a stop. A gate that drops what it cannot read is worse than no gate, because the green covers a gap that is invisible in the diff. THE REPAIR, in three layers that each do only what they honestly can. · DISCOVERY stays textual, because the template cannot be imported — it declares the same names as the model and they would clash. It now strips NESTED `/- -/` blocks, reads a name that wraps to a later line, tracks namespace/section/end, and FAILS CLOSED: an unparseable keyword exits 2 naming file and line. · Phase 0d keeps the cheap pre-compile comparison against the table. · Phase 2d is new and authoritative. After compilation it generates a temp Lean driver (the Phase 2b idiom, so no permanent module and no manifest churn), imports every non-template gen module, and asks env.find? what each external IS: kind and originating module. Model module -> MODEL; non-axiom in a generated module -> PROVEN; an axiom outside the model, or a name that does not resolve, is a failure. Lean's verdict must equal the committed table's, and the answer count must equal the question count so a truncated resolver cannot pass. THE TABLES were regenerated and verified three ways before being committed: an independently written second scanner agreed on 228/228 rows across the four forks; all 47 changed rows carry file:line evidence; and the PROVEN row was put to Lean directly — kind = def, module = CurveField.Funs. dalek 54 -> 64 rows anza 42 -> 51 risc0 48 -> 57 betrusted 47 -> 56 Also corrected: `from_residual` was recorded EXTRA in every fork; the template does demand it, so it is MODEL. anza carried a spurious `hash|EXTRA` row from a mis-parse. NEGATIVE-TESTED. With Phase 0d neutered so Phase 2d was the gate under test, a PROVEN row edited to MODEL produces: SEMANTIC DRIFT: ...neg — table says MODEL, Lean says PROVEN selftest-correspondence.sh grew 4 cases -> 7: GPT's comment counterexample, the wrapped declaration that was actually live, and a fail-closed check. WHAT THIS DOES NOT CLOSE. Phase 2d resolves names in the environment built from gen/. It establishes what a name IS — assumption or proof, and where — not that the extraction faithfully represents the Rust. That remains the Charon/Aeneas trust assumption and no gate here can close it. Certified: both buttons, all four forks, purged trees. 8/8 green, with 62/48/53/52 externals resolved by Lean and every verdict matching.
2026-08-02 00:24:15 +00:00
P2-c: classify and pin the extraction boundary Aeneas emits a *_Template.lean naming everything the extracted code needs from outside itself — the extraction's own statement of its boundary. extract.sh has always said, in prose, "after regenerating, diff the template against the hand-written file". Prose is not a gate, and the diff cannot be one: the two files legitimately differ in almost every line, holes and Aeneas comments against real definitions and modeling policy. MEASURING FIRST CHANGED WHAT THIS ITEM SHOULD BE. The TODO offered two options — enforce the diff, or pin both files — and the answer turned out to be neither. Both files were ALREADY byte-pinned by Phase 0b. And two further things stand here: the generated Funs.lean imports the model and CALLS these externals, so the Lean compiler enforces their TYPES wherever the extracted code uses them; and the per-certificate exact cones catch any external that becomes, or stops being, an assumption anything depends on. What none of those three sees is the CLASSIFICATION: for each name the extraction asks for, whether this repository answers with an ASSUMPTION or with a PROOF. That is the tier-A/B claim the documents make in prose — the curve calls and the three curve types resolve to proven definitions rather than axioms, because gen/CurveField/Funs.lean opens `namespace curve25519_dalek` and so defines the very names Aeneas asks for. Nothing checked it. A regeneration that renamed one, or a model that quietly answered one with an axiom instead, would have left the documents claiming a proof where the repository had an assumption. Phase 0d recomputes the classification with model-correspondence.py (namespace-aware, so a definition inside a namespace counts under its full name) and requires equality with the committed MODEL-CORRESPONDENCE.txt. UNRESOLVED — the extraction asking for something nothing here provides — is a hard failure. dalek 43 MODEL 8 PROVEN 3 EXTRA anza 38 MODEL 0 PROVEN 4 EXTRA (no CurveSig crate) risc0 36 MODEL 8 PROVEN 4 EXTRA betrusted 35 MODEL 8 PROVEN 4 EXTRA selftest-correspondence.sh, five cases, negative-tested by disabling the comparison. The case that matters is 2: a PROVEN external answered by an axiom instead. No name changes anywhere, every byte pin still matches, and it compiles, because the signature is unchanged — before Phase 0d nothing in the button could tell. Trap recorded for whoever extends it: case 3 first deleted the PROVEN rows, which was VACUOUS on anza, since anza has none — it removed nothing, the table still matched, and the case passed while testing nothing. It now deletes the first row whatever its verdict AND asserts the file changed. extract.sh now points at the gate instead of asking a human to look. Certified by a full sweep: both buttons, all four forks, purged trees, machine otherwise idle. 8/8 green.
2026-07-31 15:53:31 +00:00
def main(root):
gen = os.path.join(root, 'gen')
templates = sorted(glob.glob(os.path.join(gen, '*', '*_Template.lean')))
# The proven corpus: every generated module that is neither a template nor
# a hand-written model. These are the files Aeneas produced from Rust.
models = {t.replace('_Template', '') for t in templates}
corpus = set()
for f in sorted(glob.glob(os.path.join(gen, '*', '*.lean'))):
if f in models or f.endswith('_Template.lean'):
continue
correspondence: a named section is not a namespace; an extra axiom is a failure Round-8 review (GPT-5.6, register key `section-prefix-bug`, CRITICAL). Reproduced here exactly before fixing. model-correspondence.py treated `namespace`, `section` and `end` as one event class and pushed a named section onto the fully-qualified-name prefix. Lean does not: `section Foo` opens a scope for `variable`/`open` and gives `end Foo` a label; it does not turn `bar` into `Foo.bar`. Given a template reading section Foo axiom bar : Nat end Foo the scanner reported `Foo.bar`, `--names` handed Phase 2d only `Foo.bar`, Lean resolved an unrelated `Foo.bar` definition elsewhere in the corpus, and the verdict came back PROVEN. The axiom the extraction ACTUALLY depends on was never queried. This survived both the fail-closed rewrite and the new Lean-semantic phase, in a scanner rewritten that same week specifically to stop dropping things. AND THE REASON IT STAYED SILENT, which is the half worth keeping. The real external did not vanish — it landed in the table as EXTRA, the one verdict that could not fail. A silent bucket beside a fail-closed parser is a slower way of dropping things. An extra AXIOM is now EXTRA-AXIOM and stops the button: the model exists to answer the template, so an assumption nothing asks for is either a parse we got wrong or an assumption nobody governs. Extra definitions stay tolerated; helpers in a model file are ordinary. That gate fired on the real corpora on its first run. Each fork's hand-maintained gen/CurveField/FunsExternal.lean carried AVX2/AVX512 backend axioms present in no template, no proof, no cone and no allowlist — dead assumptions in a pinned trusted-base file, reported as EXTRA and therefore invisible. extract.sh:16 confirms these files are never overwritten by extraction, so they were hand-written and are removed here: dalek 2, anza 3, risc0 4, betrusted 4 Nothing referenced them, so no certificate's cone changes; the trusted base simply gets smaller. Table rows 64->62, 51->48, 57->53, 56->52, and Phase 2d independently resolved 62/48/53/52 externals against the regenerated tables. GEN-MODEL.sha256 and HARNESS.sha256 both move: the model bytes changed, and the harness pins the table and the gen manifest themselves. Certified: round-10 sweep, 2h53m, ten instruments in each of four forks, 40/40 GREEN, 0 failing, 0 resource-limited. A full run was required — the --audit-only staleness gate correctly refused after a source change. Registered in formal-verification-control/review-findings.tsv as `section-prefix-bug` and `dead-model-axioms`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 19:29:54 +00:00
corpus.update(declared(f))
P2-c: classify and pin the extraction boundary Aeneas emits a *_Template.lean naming everything the extracted code needs from outside itself — the extraction's own statement of its boundary. extract.sh has always said, in prose, "after regenerating, diff the template against the hand-written file". Prose is not a gate, and the diff cannot be one: the two files legitimately differ in almost every line, holes and Aeneas comments against real definitions and modeling policy. MEASURING FIRST CHANGED WHAT THIS ITEM SHOULD BE. The TODO offered two options — enforce the diff, or pin both files — and the answer turned out to be neither. Both files were ALREADY byte-pinned by Phase 0b. And two further things stand here: the generated Funs.lean imports the model and CALLS these externals, so the Lean compiler enforces their TYPES wherever the extracted code uses them; and the per-certificate exact cones catch any external that becomes, or stops being, an assumption anything depends on. What none of those three sees is the CLASSIFICATION: for each name the extraction asks for, whether this repository answers with an ASSUMPTION or with a PROOF. That is the tier-A/B claim the documents make in prose — the curve calls and the three curve types resolve to proven definitions rather than axioms, because gen/CurveField/Funs.lean opens `namespace curve25519_dalek` and so defines the very names Aeneas asks for. Nothing checked it. A regeneration that renamed one, or a model that quietly answered one with an axiom instead, would have left the documents claiming a proof where the repository had an assumption. Phase 0d recomputes the classification with model-correspondence.py (namespace-aware, so a definition inside a namespace counts under its full name) and requires equality with the committed MODEL-CORRESPONDENCE.txt. UNRESOLVED — the extraction asking for something nothing here provides — is a hard failure. dalek 43 MODEL 8 PROVEN 3 EXTRA anza 38 MODEL 0 PROVEN 4 EXTRA (no CurveSig crate) risc0 36 MODEL 8 PROVEN 4 EXTRA betrusted 35 MODEL 8 PROVEN 4 EXTRA selftest-correspondence.sh, five cases, negative-tested by disabling the comparison. The case that matters is 2: a PROVEN external answered by an axiom instead. No name changes anywhere, every byte pin still matches, and it compiles, because the signature is unchanged — before Phase 0d nothing in the button could tell. Trap recorded for whoever extends it: case 3 first deleted the PROVEN rows, which was VACUOUS on anza, since anza has none — it removed nothing, the table still matched, and the case passed while testing nothing. It now deletes the first row whatever its verdict AND asserts the file changed. extract.sh now points at the gate instead of asking a human to look. Certified by a full sweep: both buttons, all four forks, purged trees, machine otherwise idle. 8/8 green.
2026-07-31 15:53:31 +00:00
rows, unresolved = [], []
for t in templates:
model = t.replace('_Template', '')
rel = os.path.relpath(t, gen).replace('_Template.lean', '')
correspondence: a named section is not a namespace; an extra axiom is a failure Round-8 review (GPT-5.6, register key `section-prefix-bug`, CRITICAL). Reproduced here exactly before fixing. model-correspondence.py treated `namespace`, `section` and `end` as one event class and pushed a named section onto the fully-qualified-name prefix. Lean does not: `section Foo` opens a scope for `variable`/`open` and gives `end Foo` a label; it does not turn `bar` into `Foo.bar`. Given a template reading section Foo axiom bar : Nat end Foo the scanner reported `Foo.bar`, `--names` handed Phase 2d only `Foo.bar`, Lean resolved an unrelated `Foo.bar` definition elsewhere in the corpus, and the verdict came back PROVEN. The axiom the extraction ACTUALLY depends on was never queried. This survived both the fail-closed rewrite and the new Lean-semantic phase, in a scanner rewritten that same week specifically to stop dropping things. AND THE REASON IT STAYED SILENT, which is the half worth keeping. The real external did not vanish — it landed in the table as EXTRA, the one verdict that could not fail. A silent bucket beside a fail-closed parser is a slower way of dropping things. An extra AXIOM is now EXTRA-AXIOM and stops the button: the model exists to answer the template, so an assumption nothing asks for is either a parse we got wrong or an assumption nobody governs. Extra definitions stay tolerated; helpers in a model file are ordinary. That gate fired on the real corpora on its first run. Each fork's hand-maintained gen/CurveField/FunsExternal.lean carried AVX2/AVX512 backend axioms present in no template, no proof, no cone and no allowlist — dead assumptions in a pinned trusted-base file, reported as EXTRA and therefore invisible. extract.sh:16 confirms these files are never overwritten by extraction, so they were hand-written and are removed here: dalek 2, anza 3, risc0 4, betrusted 4 Nothing referenced them, so no certificate's cone changes; the trusted base simply gets smaller. Table rows 64->62, 51->48, 57->53, 56->52, and Phase 2d independently resolved 62/48/53/52 externals against the regenerated tables. GEN-MODEL.sha256 and HARNESS.sha256 both move: the model bytes changed, and the harness pins the table and the gen manifest themselves. Certified: round-10 sweep, 2h53m, ten instruments in each of four forks, 40/40 GREEN, 0 failing, 0 resource-limited. A full run was required — the --audit-only staleness gate correctly refused after a source change. Registered in formal-verification-control/review-findings.tsv as `section-prefix-bug` and `dead-model-axioms`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 19:29:54 +00:00
tnames = set(declared(t))
mkinds = declared(model) if os.path.exists(model) else {}
mnames = set(mkinds)
P2-c: classify and pin the extraction boundary Aeneas emits a *_Template.lean naming everything the extracted code needs from outside itself — the extraction's own statement of its boundary. extract.sh has always said, in prose, "after regenerating, diff the template against the hand-written file". Prose is not a gate, and the diff cannot be one: the two files legitimately differ in almost every line, holes and Aeneas comments against real definitions and modeling policy. MEASURING FIRST CHANGED WHAT THIS ITEM SHOULD BE. The TODO offered two options — enforce the diff, or pin both files — and the answer turned out to be neither. Both files were ALREADY byte-pinned by Phase 0b. And two further things stand here: the generated Funs.lean imports the model and CALLS these externals, so the Lean compiler enforces their TYPES wherever the extracted code uses them; and the per-certificate exact cones catch any external that becomes, or stops being, an assumption anything depends on. What none of those three sees is the CLASSIFICATION: for each name the extraction asks for, whether this repository answers with an ASSUMPTION or with a PROOF. That is the tier-A/B claim the documents make in prose — the curve calls and the three curve types resolve to proven definitions rather than axioms, because gen/CurveField/Funs.lean opens `namespace curve25519_dalek` and so defines the very names Aeneas asks for. Nothing checked it. A regeneration that renamed one, or a model that quietly answered one with an axiom instead, would have left the documents claiming a proof where the repository had an assumption. Phase 0d recomputes the classification with model-correspondence.py (namespace-aware, so a definition inside a namespace counts under its full name) and requires equality with the committed MODEL-CORRESPONDENCE.txt. UNRESOLVED — the extraction asking for something nothing here provides — is a hard failure. dalek 43 MODEL 8 PROVEN 3 EXTRA anza 38 MODEL 0 PROVEN 4 EXTRA (no CurveSig crate) risc0 36 MODEL 8 PROVEN 4 EXTRA betrusted 35 MODEL 8 PROVEN 4 EXTRA selftest-correspondence.sh, five cases, negative-tested by disabling the comparison. The case that matters is 2: a PROVEN external answered by an axiom instead. No name changes anywhere, every byte pin still matches, and it compiles, because the signature is unchanged — before Phase 0d nothing in the button could tell. Trap recorded for whoever extends it: case 3 first deleted the PROVEN rows, which was VACUOUS on anza, since anza has none — it removed nothing, the table still matched, and the case passed while testing nothing. It now deletes the first row whatever its verdict AND asserts the file changed. extract.sh now points at the gate instead of asking a human to look. Certified by a full sweep: both buttons, all four forks, purged trees, machine otherwise idle. 8/8 green.
2026-07-31 15:53:31 +00:00
for n in sorted(tnames):
if n in mnames:
rows.append(f'{rel}|{n}|MODEL')
elif n in corpus:
rows.append(f'{rel}|{n}|PROVEN')
else:
rows.append(f'{rel}|{n}|UNRESOLVED')
unresolved.append(f'{rel}|{n}')
correspondence: a named section is not a namespace; an extra axiom is a failure Round-8 review (GPT-5.6, register key `section-prefix-bug`, CRITICAL). Reproduced here exactly before fixing. model-correspondence.py treated `namespace`, `section` and `end` as one event class and pushed a named section onto the fully-qualified-name prefix. Lean does not: `section Foo` opens a scope for `variable`/`open` and gives `end Foo` a label; it does not turn `bar` into `Foo.bar`. Given a template reading section Foo axiom bar : Nat end Foo the scanner reported `Foo.bar`, `--names` handed Phase 2d only `Foo.bar`, Lean resolved an unrelated `Foo.bar` definition elsewhere in the corpus, and the verdict came back PROVEN. The axiom the extraction ACTUALLY depends on was never queried. This survived both the fail-closed rewrite and the new Lean-semantic phase, in a scanner rewritten that same week specifically to stop dropping things. AND THE REASON IT STAYED SILENT, which is the half worth keeping. The real external did not vanish — it landed in the table as EXTRA, the one verdict that could not fail. A silent bucket beside a fail-closed parser is a slower way of dropping things. An extra AXIOM is now EXTRA-AXIOM and stops the button: the model exists to answer the template, so an assumption nothing asks for is either a parse we got wrong or an assumption nobody governs. Extra definitions stay tolerated; helpers in a model file are ordinary. That gate fired on the real corpora on its first run. Each fork's hand-maintained gen/CurveField/FunsExternal.lean carried AVX2/AVX512 backend axioms present in no template, no proof, no cone and no allowlist — dead assumptions in a pinned trusted-base file, reported as EXTRA and therefore invisible. extract.sh:16 confirms these files are never overwritten by extraction, so they were hand-written and are removed here: dalek 2, anza 3, risc0 4, betrusted 4 Nothing referenced them, so no certificate's cone changes; the trusted base simply gets smaller. Table rows 64->62, 51->48, 57->53, 56->52, and Phase 2d independently resolved 62/48/53/52 externals against the regenerated tables. GEN-MODEL.sha256 and HARNESS.sha256 both move: the model bytes changed, and the harness pins the table and the gen manifest themselves. Certified: round-10 sweep, 2h53m, ten instruments in each of four forks, 40/40 GREEN, 0 failing, 0 resource-limited. A full run was required — the --audit-only staleness gate correctly refused after a source change. Registered in formal-verification-control/review-findings.tsv as `section-prefix-bug` and `dead-model-axioms`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 19:29:54 +00:00
# AN EXTRA AXIOM IS A FAILURE, and this is the second half of the
# round-8 section-prefix finding. EXTRA was the one verdict that could
# not fail: the model declares something the template did not ask for.
# When the scanner mis-derived the template's name (`Foo.bar` instead of
# `bar`), the axiom the extraction ACTUALLY depends on did not vanish —
# it landed here, as a harmless-looking EXTRA row, while the invented
# name was certified PROVEN. A silent bucket next to a fail-closed
# parser is just a slower way of dropping things.
#
# There is no benign reading of an extra AXIOM either way. The model
# exists to answer the template; an assumption nothing asks for is
# either a parse the scanner got wrong or an unaudited assumption
# nobody is governing. Both must stop the button. Extra non-axiom
# declarations stay reportable-but-tolerated: helper definitions in a
# model file are ordinary.
P2-c: classify and pin the extraction boundary Aeneas emits a *_Template.lean naming everything the extracted code needs from outside itself — the extraction's own statement of its boundary. extract.sh has always said, in prose, "after regenerating, diff the template against the hand-written file". Prose is not a gate, and the diff cannot be one: the two files legitimately differ in almost every line, holes and Aeneas comments against real definitions and modeling policy. MEASURING FIRST CHANGED WHAT THIS ITEM SHOULD BE. The TODO offered two options — enforce the diff, or pin both files — and the answer turned out to be neither. Both files were ALREADY byte-pinned by Phase 0b. And two further things stand here: the generated Funs.lean imports the model and CALLS these externals, so the Lean compiler enforces their TYPES wherever the extracted code uses them; and the per-certificate exact cones catch any external that becomes, or stops being, an assumption anything depends on. What none of those three sees is the CLASSIFICATION: for each name the extraction asks for, whether this repository answers with an ASSUMPTION or with a PROOF. That is the tier-A/B claim the documents make in prose — the curve calls and the three curve types resolve to proven definitions rather than axioms, because gen/CurveField/Funs.lean opens `namespace curve25519_dalek` and so defines the very names Aeneas asks for. Nothing checked it. A regeneration that renamed one, or a model that quietly answered one with an axiom instead, would have left the documents claiming a proof where the repository had an assumption. Phase 0d recomputes the classification with model-correspondence.py (namespace-aware, so a definition inside a namespace counts under its full name) and requires equality with the committed MODEL-CORRESPONDENCE.txt. UNRESOLVED — the extraction asking for something nothing here provides — is a hard failure. dalek 43 MODEL 8 PROVEN 3 EXTRA anza 38 MODEL 0 PROVEN 4 EXTRA (no CurveSig crate) risc0 36 MODEL 8 PROVEN 4 EXTRA betrusted 35 MODEL 8 PROVEN 4 EXTRA selftest-correspondence.sh, five cases, negative-tested by disabling the comparison. The case that matters is 2: a PROVEN external answered by an axiom instead. No name changes anywhere, every byte pin still matches, and it compiles, because the signature is unchanged — before Phase 0d nothing in the button could tell. Trap recorded for whoever extends it: case 3 first deleted the PROVEN rows, which was VACUOUS on anza, since anza has none — it removed nothing, the table still matched, and the case passed while testing nothing. It now deletes the first row whatever its verdict AND asserts the file changed. extract.sh now points at the gate instead of asking a human to look. Certified by a full sweep: both buttons, all four forks, purged trees, machine otherwise idle. 8/8 green.
2026-07-31 15:53:31 +00:00
for n in sorted(mnames - tnames):
correspondence: a named section is not a namespace; an extra axiom is a failure Round-8 review (GPT-5.6, register key `section-prefix-bug`, CRITICAL). Reproduced here exactly before fixing. model-correspondence.py treated `namespace`, `section` and `end` as one event class and pushed a named section onto the fully-qualified-name prefix. Lean does not: `section Foo` opens a scope for `variable`/`open` and gives `end Foo` a label; it does not turn `bar` into `Foo.bar`. Given a template reading section Foo axiom bar : Nat end Foo the scanner reported `Foo.bar`, `--names` handed Phase 2d only `Foo.bar`, Lean resolved an unrelated `Foo.bar` definition elsewhere in the corpus, and the verdict came back PROVEN. The axiom the extraction ACTUALLY depends on was never queried. This survived both the fail-closed rewrite and the new Lean-semantic phase, in a scanner rewritten that same week specifically to stop dropping things. AND THE REASON IT STAYED SILENT, which is the half worth keeping. The real external did not vanish — it landed in the table as EXTRA, the one verdict that could not fail. A silent bucket beside a fail-closed parser is a slower way of dropping things. An extra AXIOM is now EXTRA-AXIOM and stops the button: the model exists to answer the template, so an assumption nothing asks for is either a parse we got wrong or an assumption nobody governs. Extra definitions stay tolerated; helpers in a model file are ordinary. That gate fired on the real corpora on its first run. Each fork's hand-maintained gen/CurveField/FunsExternal.lean carried AVX2/AVX512 backend axioms present in no template, no proof, no cone and no allowlist — dead assumptions in a pinned trusted-base file, reported as EXTRA and therefore invisible. extract.sh:16 confirms these files are never overwritten by extraction, so they were hand-written and are removed here: dalek 2, anza 3, risc0 4, betrusted 4 Nothing referenced them, so no certificate's cone changes; the trusted base simply gets smaller. Table rows 64->62, 51->48, 57->53, 56->52, and Phase 2d independently resolved 62/48/53/52 externals against the regenerated tables. GEN-MODEL.sha256 and HARNESS.sha256 both move: the model bytes changed, and the harness pins the table and the gen manifest themselves. Certified: round-10 sweep, 2h53m, ten instruments in each of four forks, 40/40 GREEN, 0 failing, 0 resource-limited. A full run was required — the --audit-only staleness gate correctly refused after a source change. Registered in formal-verification-control/review-findings.tsv as `section-prefix-bug` and `dead-model-axioms`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 19:29:54 +00:00
kind = mkinds.get(n, '')
if kind == 'axiom':
rows.append(f'{rel}|{n}|EXTRA-AXIOM')
unresolved.append(f'{rel}|{n} (axiom in the model that no '
f'template external asks for)')
else:
rows.append(f'{rel}|{n}|EXTRA')
P2-c: classify and pin the extraction boundary Aeneas emits a *_Template.lean naming everything the extracted code needs from outside itself — the extraction's own statement of its boundary. extract.sh has always said, in prose, "after regenerating, diff the template against the hand-written file". Prose is not a gate, and the diff cannot be one: the two files legitimately differ in almost every line, holes and Aeneas comments against real definitions and modeling policy. MEASURING FIRST CHANGED WHAT THIS ITEM SHOULD BE. The TODO offered two options — enforce the diff, or pin both files — and the answer turned out to be neither. Both files were ALREADY byte-pinned by Phase 0b. And two further things stand here: the generated Funs.lean imports the model and CALLS these externals, so the Lean compiler enforces their TYPES wherever the extracted code uses them; and the per-certificate exact cones catch any external that becomes, or stops being, an assumption anything depends on. What none of those three sees is the CLASSIFICATION: for each name the extraction asks for, whether this repository answers with an ASSUMPTION or with a PROOF. That is the tier-A/B claim the documents make in prose — the curve calls and the three curve types resolve to proven definitions rather than axioms, because gen/CurveField/Funs.lean opens `namespace curve25519_dalek` and so defines the very names Aeneas asks for. Nothing checked it. A regeneration that renamed one, or a model that quietly answered one with an axiom instead, would have left the documents claiming a proof where the repository had an assumption. Phase 0d recomputes the classification with model-correspondence.py (namespace-aware, so a definition inside a namespace counts under its full name) and requires equality with the committed MODEL-CORRESPONDENCE.txt. UNRESOLVED — the extraction asking for something nothing here provides — is a hard failure. dalek 43 MODEL 8 PROVEN 3 EXTRA anza 38 MODEL 0 PROVEN 4 EXTRA (no CurveSig crate) risc0 36 MODEL 8 PROVEN 4 EXTRA betrusted 35 MODEL 8 PROVEN 4 EXTRA selftest-correspondence.sh, five cases, negative-tested by disabling the comparison. The case that matters is 2: a PROVEN external answered by an axiom instead. No name changes anywhere, every byte pin still matches, and it compiles, because the signature is unchanged — before Phase 0d nothing in the button could tell. Trap recorded for whoever extends it: case 3 first deleted the PROVEN rows, which was VACUOUS on anza, since anza has none — it removed nothing, the table still matched, and the case passed while testing nothing. It now deletes the first row whatever its verdict AND asserts the file changed. extract.sh now points at the gate instead of asking a human to look. Certified by a full sweep: both buttons, all four forks, purged trees, machine otherwise idle. 8/8 green.
2026-07-31 15:53:31 +00:00
print('\n'.join(rows))
print(f'CORRESPONDENCE-COUNT|{len(rows)}')
return 1 if unresolved else 0
Round-7 F1: make model/template correspondence SEMANTIC, and fail closed THE DEFECT, as found. GPT-5.6 showed the textual classifier could report PROVEN for a name Lean resolves to an axiom: a definition appearing only inside a `/- -/` comment was read as real. Repairing that exposed something larger and already live. Aeneas wraps long declarations: axiom curve25519_dalek.edwards.EdwardsPoint.Insts.CoreOpsArithNegEdwardsPoint.neg The old scanner required keyword and name on one physical line, so it matched nothing there and SILENTLY DROPPED the declaration — no MODEL row, no PROVEN row, no failure. Nine to ten externals per fork had no row at all, and the tier-A/B `neg` was missing from every committed table while the trust documents claimed that class was machine-checked. Four pinned tables were wrong, in four repositories, with the buttons green over them. No attacker was required; Aeneas's own formatting did it. The lesson is not "write a better regex". The scanner was FAIL-OPEN: input it could not parse produced silence instead of a stop. A gate that drops what it cannot read is worse than no gate, because the green covers a gap that is invisible in the diff. THE REPAIR, in three layers that each do only what they honestly can. · DISCOVERY stays textual, because the template cannot be imported — it declares the same names as the model and they would clash. It now strips NESTED `/- -/` blocks, reads a name that wraps to a later line, tracks namespace/section/end, and FAILS CLOSED: an unparseable keyword exits 2 naming file and line. · Phase 0d keeps the cheap pre-compile comparison against the table. · Phase 2d is new and authoritative. After compilation it generates a temp Lean driver (the Phase 2b idiom, so no permanent module and no manifest churn), imports every non-template gen module, and asks env.find? what each external IS: kind and originating module. Model module -> MODEL; non-axiom in a generated module -> PROVEN; an axiom outside the model, or a name that does not resolve, is a failure. Lean's verdict must equal the committed table's, and the answer count must equal the question count so a truncated resolver cannot pass. THE TABLES were regenerated and verified three ways before being committed: an independently written second scanner agreed on 228/228 rows across the four forks; all 47 changed rows carry file:line evidence; and the PROVEN row was put to Lean directly — kind = def, module = CurveField.Funs. dalek 54 -> 64 rows anza 42 -> 51 risc0 48 -> 57 betrusted 47 -> 56 Also corrected: `from_residual` was recorded EXTRA in every fork; the template does demand it, so it is MODEL. anza carried a spurious `hash|EXTRA` row from a mis-parse. NEGATIVE-TESTED. With Phase 0d neutered so Phase 2d was the gate under test, a PROVEN row edited to MODEL produces: SEMANTIC DRIFT: ...neg — table says MODEL, Lean says PROVEN selftest-correspondence.sh grew 4 cases -> 7: GPT's comment counterexample, the wrapped declaration that was actually live, and a fail-closed check. WHAT THIS DOES NOT CLOSE. Phase 2d resolves names in the environment built from gen/. It establishes what a name IS — assumption or proof, and where — not that the extraction faithfully represents the Rust. That remains the Charon/Aeneas trust assumption and no gate here can close it. Certified: both buttons, all four forks, purged trees. 8/8 green, with 62/48/53/52 externals resolved by Lean and every verdict matching.
2026-08-02 00:24:15 +00:00
def emit_names(root):
"""Every name the EXTRACTION asks for, as `<rel>|<name>`.
Template discovery is unavoidably textual: the template is not imported (it
would clash with the model, which declares the same names), so no Lean
environment contains it. That is why `declared()` fails closed this list
is the input to the semantic phase, and a name missing here is a name
nothing will ever check.
"""
gen = os.path.join(root, 'gen')
for t in sorted(glob.glob(os.path.join(gen, '*', '*_Template.lean'))):
rel = os.path.relpath(t, gen).replace('_Template.lean', '')
for n in sorted(declared(t)):
print(f'{rel}|{n}')
return 0
P2-c: classify and pin the extraction boundary Aeneas emits a *_Template.lean naming everything the extracted code needs from outside itself — the extraction's own statement of its boundary. extract.sh has always said, in prose, "after regenerating, diff the template against the hand-written file". Prose is not a gate, and the diff cannot be one: the two files legitimately differ in almost every line, holes and Aeneas comments against real definitions and modeling policy. MEASURING FIRST CHANGED WHAT THIS ITEM SHOULD BE. The TODO offered two options — enforce the diff, or pin both files — and the answer turned out to be neither. Both files were ALREADY byte-pinned by Phase 0b. And two further things stand here: the generated Funs.lean imports the model and CALLS these externals, so the Lean compiler enforces their TYPES wherever the extracted code uses them; and the per-certificate exact cones catch any external that becomes, or stops being, an assumption anything depends on. What none of those three sees is the CLASSIFICATION: for each name the extraction asks for, whether this repository answers with an ASSUMPTION or with a PROOF. That is the tier-A/B claim the documents make in prose — the curve calls and the three curve types resolve to proven definitions rather than axioms, because gen/CurveField/Funs.lean opens `namespace curve25519_dalek` and so defines the very names Aeneas asks for. Nothing checked it. A regeneration that renamed one, or a model that quietly answered one with an axiom instead, would have left the documents claiming a proof where the repository had an assumption. Phase 0d recomputes the classification with model-correspondence.py (namespace-aware, so a definition inside a namespace counts under its full name) and requires equality with the committed MODEL-CORRESPONDENCE.txt. UNRESOLVED — the extraction asking for something nothing here provides — is a hard failure. dalek 43 MODEL 8 PROVEN 3 EXTRA anza 38 MODEL 0 PROVEN 4 EXTRA (no CurveSig crate) risc0 36 MODEL 8 PROVEN 4 EXTRA betrusted 35 MODEL 8 PROVEN 4 EXTRA selftest-correspondence.sh, five cases, negative-tested by disabling the comparison. The case that matters is 2: a PROVEN external answered by an axiom instead. No name changes anywhere, every byte pin still matches, and it compiles, because the signature is unchanged — before Phase 0d nothing in the button could tell. Trap recorded for whoever extends it: case 3 first deleted the PROVEN rows, which was VACUOUS on anza, since anza has none — it removed nothing, the table still matched, and the case passed while testing nothing. It now deletes the first row whatever its verdict AND asserts the file changed. extract.sh now points at the gate instead of asking a human to look. Certified by a full sweep: both buttons, all four forks, purged trees, machine otherwise idle. 8/8 green.
2026-07-31 15:53:31 +00:00
if __name__ == '__main__':
Round-7 F1: make model/template correspondence SEMANTIC, and fail closed THE DEFECT, as found. GPT-5.6 showed the textual classifier could report PROVEN for a name Lean resolves to an axiom: a definition appearing only inside a `/- -/` comment was read as real. Repairing that exposed something larger and already live. Aeneas wraps long declarations: axiom curve25519_dalek.edwards.EdwardsPoint.Insts.CoreOpsArithNegEdwardsPoint.neg The old scanner required keyword and name on one physical line, so it matched nothing there and SILENTLY DROPPED the declaration — no MODEL row, no PROVEN row, no failure. Nine to ten externals per fork had no row at all, and the tier-A/B `neg` was missing from every committed table while the trust documents claimed that class was machine-checked. Four pinned tables were wrong, in four repositories, with the buttons green over them. No attacker was required; Aeneas's own formatting did it. The lesson is not "write a better regex". The scanner was FAIL-OPEN: input it could not parse produced silence instead of a stop. A gate that drops what it cannot read is worse than no gate, because the green covers a gap that is invisible in the diff. THE REPAIR, in three layers that each do only what they honestly can. · DISCOVERY stays textual, because the template cannot be imported — it declares the same names as the model and they would clash. It now strips NESTED `/- -/` blocks, reads a name that wraps to a later line, tracks namespace/section/end, and FAILS CLOSED: an unparseable keyword exits 2 naming file and line. · Phase 0d keeps the cheap pre-compile comparison against the table. · Phase 2d is new and authoritative. After compilation it generates a temp Lean driver (the Phase 2b idiom, so no permanent module and no manifest churn), imports every non-template gen module, and asks env.find? what each external IS: kind and originating module. Model module -> MODEL; non-axiom in a generated module -> PROVEN; an axiom outside the model, or a name that does not resolve, is a failure. Lean's verdict must equal the committed table's, and the answer count must equal the question count so a truncated resolver cannot pass. THE TABLES were regenerated and verified three ways before being committed: an independently written second scanner agreed on 228/228 rows across the four forks; all 47 changed rows carry file:line evidence; and the PROVEN row was put to Lean directly — kind = def, module = CurveField.Funs. dalek 54 -> 64 rows anza 42 -> 51 risc0 48 -> 57 betrusted 47 -> 56 Also corrected: `from_residual` was recorded EXTRA in every fork; the template does demand it, so it is MODEL. anza carried a spurious `hash|EXTRA` row from a mis-parse. NEGATIVE-TESTED. With Phase 0d neutered so Phase 2d was the gate under test, a PROVEN row edited to MODEL produces: SEMANTIC DRIFT: ...neg — table says MODEL, Lean says PROVEN selftest-correspondence.sh grew 4 cases -> 7: GPT's comment counterexample, the wrapped declaration that was actually live, and a fail-closed check. WHAT THIS DOES NOT CLOSE. Phase 2d resolves names in the environment built from gen/. It establishes what a name IS — assumption or proof, and where — not that the extraction faithfully represents the Rust. That remains the Charon/Aeneas trust assumption and no gate here can close it. Certified: both buttons, all four forks, purged trees. 8/8 green, with 62/48/53/52 externals resolved by Lean and every verdict matching.
2026-08-02 00:24:15 +00:00
try:
if len(sys.argv) > 2 and sys.argv[1] == '--names':
sys.exit(emit_names(sys.argv[2]))
sys.exit(main(sys.argv[1]))
except ScanError as e:
# Fail closed and loudly. Never degrade to a partial table.
print('MODEL CORRESPONDENCE SCAN FAILED: %s' % e, file=sys.stderr)
sys.exit(2)