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.
This commit is contained in:
mrwulf 2026-08-02 02:24:15 +02:00
parent 08811d9118
commit 4fafaab608
6 changed files with 343 additions and 28 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
.lake/ .lake/
lake-manifest.json lake-manifest.json
.audit-basis .audit-basis
__pycache__/

View file

@ -1,21 +1,21 @@
6c821b8e465d3b394cb3cbb4bb3757791ace064b6d1b273ba9a41402dac74e24 AUDIT-MANIFEST.txt 6c821b8e465d3b394cb3cbb4bb3757791ace064b6d1b273ba9a41402dac74e24 AUDIT-MANIFEST.txt
d505e4dd9283673e78fb34e25780c94334e03de4ad20400f8289d594ab004daf check-scalar.sh d505e4dd9283673e78fb34e25780c94334e03de4ad20400f8289d594ab004daf check-scalar.sh
0c0ee439fd5c907719478b08c73f8fc62fc5147b90c87baadf53f1301da2037e check.sh afe5511860d91f857f1179755facc83542f6bc75620263b264fdac69f05fa6b8 check.sh
9b24f501b1e92b8ae41db0729ae9fbdbf10438ae89764ed29aa07efc2c3d6dd6 extract.sh 9b24f501b1e92b8ae41db0729ae9fbdbf10438ae89764ed29aa07efc2c3d6dd6 extract.sh
0ea20d74cd359da404ee3be116058374cbb9fd992ed170e5f6c64f8d7a6b2733 GEN-MODEL.sha256 0ea20d74cd359da404ee3be116058374cbb9fd992ed170e5f6c64f8d7a6b2733 GEN-MODEL.sha256
e95982c15c7d754f0c9bcffef95d4c9d4c63589ac51ecdd40870133f377a005c inventory-allowlist-scalar.txt e95982c15c7d754f0c9bcffef95d4c9d4c63589ac51ecdd40870133f377a005c inventory-allowlist-scalar.txt
86ee83b703d17c1f04af654657219b344b0076bc994c0b791ca6b6c5a0090d4f inventory-allowlist.txt 86ee83b703d17c1f04af654657219b344b0076bc994c0b791ca6b6c5a0090d4f inventory-allowlist.txt
0bb01bc4abaafa8537d460682004d1f336980b28bc4fe1968bcc9c3bc3bc71ba inventory_gate.sh 0bb01bc4abaafa8537d460682004d1f336980b28bc4fe1968bcc9c3bc3bc71ba inventory_gate.sh
736ea4be712e1b5bcda10ecb466f0dec7008a2a36eabdfd77563976299c43cce lean-guard 736ea4be712e1b5bcda10ecb466f0dec7008a2a36eabdfd77563976299c43cce lean-guard
fde2e987a9f69cb9f39b18ab8b405d73db4d9abee9f714f8be993a40ef617c03 model-correspondence.py 7a9c89cf2b96313bd4bcddb534e8d06724bd7250234e670baadf6a9b0d1fbf81 model-correspondence.py
b14a4a6f349971f56f7528b93fe5c8dcbfc3b7516c8593361262f5a29aefec08 MODEL-CORRESPONDENCE.txt 174f6c5a3f0f48feabcf80ddfb031e6459aca5edd2b476d4015581df764165ba MODEL-CORRESPONDENCE.txt
772ca6dd22443c83dc35d5428598c8d17a01c69db5be008474d06476fa66f7f8 Proofs/Audit.lean 772ca6dd22443c83dc35d5428598c8d17a01c69db5be008474d06476fa66f7f8 Proofs/Audit.lean
859dcb7fcef13e8b49a8b36a496f46e9fc56448410d3bde16c78361215f4bc19 Proofs/InventoryCore.lean 859dcb7fcef13e8b49a8b36a496f46e9fc56448410d3bde16c78361215f4bc19 Proofs/InventoryCore.lean
660d35343f5673d4bc854787b6a34063c956567bd7e2eb62955a92e3c55bfa6b Proofs/Inventory.lean 660d35343f5673d4bc854787b6a34063c956567bd7e2eb62955a92e3c55bfa6b Proofs/Inventory.lean
6fbeb50d3951c7c5ac593f6dec91096fc798123ed0c500fdfa39fb20b118ea78 Proofs/InventoryScalar.lean 6fbeb50d3951c7c5ac593f6dec91096fc798123ed0c500fdfa39fb20b118ea78 Proofs/InventoryScalar.lean
bf71e8d4eb312ebc687bf7e218d90b910543cd92e782078174868d012aca7250 selftest-auditonly.sh bf71e8d4eb312ebc687bf7e218d90b910543cd92e782078174868d012aca7250 selftest-auditonly.sh
eb81df6d154b413b243ad282e3b1bfec92fde158a215f89993c08586a121f171 selftest-axgate.sh eb81df6d154b413b243ad282e3b1bfec92fde158a215f89993c08586a121f171 selftest-axgate.sh
73d629325b667e715c7e6a633332950ace019e18e6d143ec5cb25846b8ddeb80 selftest-correspondence.sh 9bf6559fb364bbfdec94649c459215061df80f1f813b4b0ec869c7f8e18a8cfc selftest-correspondence.sh
3d5898161d663eccad162269a5a6c102319077e22e1f2d89a8bfcab6926d29f6 selftest-harness.sh 3d5898161d663eccad162269a5a6c102319077e22e1f2d89a8bfcab6926d29f6 selftest-harness.sh
1df031a075fc438c5229d01cbc44ee6ac489a272cd736624f7ca45ef1a4ddb7f selftest-inventory.sh 1df031a075fc438c5229d01cbc44ee6ac489a272cd736624f7ca45ef1a4ddb7f selftest-inventory.sh
26f10a749e03cecd7ad173d0d621498386444d8e347f606a99a2fadb06738d86 selftest-shapes.sh 26f10a749e03cecd7ad173d0d621498386444d8e347f606a99a2fadb06738d86 selftest-shapes.sh

View file

@ -11,14 +11,21 @@ CurveField/FunsExternal|U64.Insts.SubtleConditionallySelectable.conditional_swap
CurveField/FunsExternal|U8.Insts.CoreHashHash.hash|MODEL CurveField/FunsExternal|U8.Insts.CoreHashHash.hash|MODEL
CurveField/FunsExternal|U8.Insts.SubtleConstantTimeEq.ct_eq|MODEL CurveField/FunsExternal|U8.Insts.SubtleConstantTimeEq.ct_eq|MODEL
CurveField/FunsExternal|backend.get_selected_backend|MODEL CurveField/FunsExternal|backend.get_selected_backend|MODEL
CurveField/FunsExternal|backend.serial.curve_models.AffineNielsPoint.Insts.SubtleConditionallySelectable.conditional_swap|MODEL
CurveField/FunsExternal|backend.serial.curve_models.ProjectiveNielsPoint.Insts.SubtleConditionallySelectable.conditional_swap|MODEL
CurveField/FunsExternal|backend.serial.scalar_mul.variable_base.mul|MODEL CurveField/FunsExternal|backend.serial.scalar_mul.variable_base.mul|MODEL
CurveField/FunsExternal|backend.serial.u64.field.FieldElement51.Insts.CoreCmpEq.assert_fields_are_eq|MODEL
CurveField/FunsExternal|backend.serial.u64.scalar.Scalar52.sub.black_box|MODEL CurveField/FunsExternal|backend.serial.u64.scalar.Scalar52.sub.black_box|MODEL
CurveField/FunsExternal|core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.get|MODEL CurveField/FunsExternal|core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.get|MODEL
CurveField/FunsExternal|core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.get_mut|MODEL CurveField/FunsExternal|core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.get_mut|MODEL
CurveField/FunsExternal|core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.get_unchecked|MODEL
CurveField/FunsExternal|core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.get_unchecked_mut|MODEL
CurveField/FunsExternal|core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.index|MODEL CurveField/FunsExternal|core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.index|MODEL
CurveField/FunsExternal|core.ops.range.RangeFull.Insts.CoreSliceIndexSliceIndexSliceSlice.index_mut|MODEL
CurveField/FunsExternal|edwards.CompressedEdwardsY.from_slice|MODEL CurveField/FunsExternal|edwards.CompressedEdwardsY.from_slice|MODEL
CurveField/FunsExternal|edwards.EdwardsPoint.Insts.CoreCmpEq.assert_fields_are_eq|MODEL CurveField/FunsExternal|edwards.EdwardsPoint.Insts.CoreCmpEq.assert_fields_are_eq|MODEL
CurveField/FunsExternal|edwards.EdwardsPoint.Insts.CoreIterTraitsAccumSum.sum|MODEL CurveField/FunsExternal|edwards.EdwardsPoint.Insts.CoreIterTraitsAccumSum.sum|MODEL
CurveField/FunsExternal|edwards.EdwardsPoint.Insts.SubtleConditionallySelectable.conditional_assign|MODEL
CurveField/FunsExternal|edwards.EdwardsPoint.Insts.SubtleConditionallySelectable.conditional_swap|MODEL CurveField/FunsExternal|edwards.EdwardsPoint.Insts.SubtleConditionallySelectable.conditional_swap|MODEL
CurveField/FunsExternal|subtle.Choice.Insts.CoreConvertFromU8.from|MODEL CurveField/FunsExternal|subtle.Choice.Insts.CoreConvertFromU8.from|MODEL
CurveField/FunsExternal|subtle.Choice.Insts.CoreOpsBitBitAndChoiceChoice.bitand|MODEL CurveField/FunsExternal|subtle.Choice.Insts.CoreOpsBitBitAndChoiceChoice.bitand|MODEL
@ -29,9 +36,12 @@ CurveField/FunsExternal|subtle.ConditionallySelectable.conditional_swap.default|
CurveField/FunsExternal|backend.vector.scalar_mul.variable_base.spec_avx2.mul|EXTRA CurveField/FunsExternal|backend.vector.scalar_mul.variable_base.spec_avx2.mul|EXTRA
CurveField/FunsExternal|backend.vector.scalar_mul.variable_base.spec_avx512ifma_avx512vl.mul|EXTRA CurveField/FunsExternal|backend.vector.scalar_mul.variable_base.spec_avx512ifma_avx512vl.mul|EXTRA
CurveField/FunsExternal|backend.vector.scalar_mul.vartime_double_base.spec_avx2.mul|EXTRA CurveField/FunsExternal|backend.vector.scalar_mul.vartime_double_base.spec_avx2.mul|EXTRA
CurveField/FunsExternal|backend.vector.scalar_mul.vartime_double_base.spec_avx512ifma_avx512vl.mul|EXTRA
CurveField/TypesExternal|subtle.Choice|MODEL CurveField/TypesExternal|subtle.Choice|MODEL
CurveSig/FunsExternal|core.result.Result.Insts.CoreOpsTry_traitFromResidualResultInfallibleE.from_residual|MODEL
CurveSig/FunsExternal|core.result.Result.Insts.CoreOpsTry_traitTry.branch|MODEL CurveSig/FunsExternal|core.result.Result.Insts.CoreOpsTry_traitTry.branch|MODEL
CurveSig/FunsExternal|curve25519_dalek.edwards.CompressedEdwardsY.as_bytes|PROVEN CurveSig/FunsExternal|curve25519_dalek.edwards.CompressedEdwardsY.as_bytes|PROVEN
CurveSig/FunsExternal|curve25519_dalek.edwards.EdwardsPoint.Insts.CoreOpsArithNegEdwardsPoint.neg|PROVEN
CurveSig/FunsExternal|curve25519_dalek.edwards.EdwardsPoint.compress|PROVEN CurveSig/FunsExternal|curve25519_dalek.edwards.EdwardsPoint.compress|PROVEN
CurveSig/FunsExternal|curve25519_dalek.edwards.EdwardsPoint.vartime_double_scalar_mul_basepoint|PROVEN CurveSig/FunsExternal|curve25519_dalek.edwards.EdwardsPoint.vartime_double_scalar_mul_basepoint|PROVEN
CurveSig/FunsExternal|curve25519_dalek.scalar.Scalar.from_bytes_mod_order|PROVEN CurveSig/FunsExternal|curve25519_dalek.scalar.Scalar.from_bytes_mod_order|PROVEN
@ -40,10 +50,9 @@ CurveSig/FunsExternal|ed25519.Signature.to_bytes|MODEL
CurveSig/FunsExternal|signature.compressed_from_bytes|MODEL CurveSig/FunsExternal|signature.compressed_from_bytes|MODEL
CurveSig/FunsExternal|signature.error.Error.new|MODEL CurveSig/FunsExternal|signature.error.Error.new|MODEL
CurveSig/FunsExternal|verifying.sha512_hash3|MODEL CurveSig/FunsExternal|verifying.sha512_hash3|MODEL
CurveSig/FunsExternal|core.result.Result.Insts.CoreOpsTry_traitFromResidualResultInfallibleE.from_residual|EXTRA
CurveSig/TypesExternal|curve25519_dalek.edwards.CompressedEdwardsY|PROVEN CurveSig/TypesExternal|curve25519_dalek.edwards.CompressedEdwardsY|PROVEN
CurveSig/TypesExternal|curve25519_dalek.edwards.EdwardsPoint|PROVEN CurveSig/TypesExternal|curve25519_dalek.edwards.EdwardsPoint|PROVEN
CurveSig/TypesExternal|curve25519_dalek.scalar.Scalar|PROVEN CurveSig/TypesExternal|curve25519_dalek.scalar.Scalar|PROVEN
CurveSig/TypesExternal|ed25519.Signature|MODEL CurveSig/TypesExternal|ed25519.Signature|MODEL
CurveSig/TypesExternal|signature.error.Error|MODEL CurveSig/TypesExternal|signature.error.Error|MODEL
CORRESPONDENCE-COUNT|48 CORRESPONDENCE-COUNT|57

View file

@ -769,6 +769,128 @@ for f in "$HERE"/Proofs/*.lean; do
grep -qx "$b" <<<"$COVERED" || echo " NOT INVENTORIED HERE (separate button): Proofs/$b.lean" grep -qx "$b" <<<"$COVERED" || echo " NOT INVENTORIED HERE (separate button): Proofs/$b.lean"
done done
[ "$INVFAIL" = 0 ] || { echo "INVENTORY COVERAGE FAILED"; exit 1; } [ "$INVFAIL" = 0 ] || { echo "INVENTORY COVERAGE FAILED"; exit 1; }
# ── Phase 2d: SEMANTIC model/template correspondence ────────────────────────
# Phase 0d asks a text scanner what the extraction's boundary looks like. This
# phase asks LEAN what it actually is, and requires the two to agree.
#
# WHY BOTH. Round-7 review (GPT-5.6, finding F1) showed the textual classifier
# could be made to report PROVEN for a name Lean resolves to an axiom — a
# definition inside a `/- -/` comment was read as real. Worse, and found while
# repairing that: Aeneas wraps long declarations, and the old scanner required
# keyword and name on one physical line, so it SILENTLY DROPPED them. Nine to
# ten externals per fork had no row at all, and one — the tier-A/B `neg` — was
# missing from every committed table while the docs claimed that class was
# machine-checked.
#
# A source scanner cannot decide this question. Whether a name resolves to an
# assumption or to a proof is a property of the elaborated ENVIRONMENT: it turns
# on imports, namespaces, `export`, aliases and shadowing, none of which are
# visible to a regex. So the scanner's job is now only DISCOVERY — what does the
# template ask for — and even that fails closed. The verdict comes from Lean.
#
# The template itself is deliberately not imported: it declares the same names
# as the hand-written model and the two would clash. Discovery is therefore
# unavoidably textual, which is exactly why `model-correspondence.py` must stop
# rather than skip on anything it cannot parse.
echo "=== Phase 2d: semantic model/template correspondence ==="
SEMNAMES=$(mktemp /tmp/check-semnames-XXXX.txt)
SEMOUT=$(mktemp /tmp/check-semout-XXXX.txt)
python3 "$HERE/model-correspondence.py" --names "$HERE" > "$SEMNAMES" || {
echo "MODEL CORRESPONDENCE FAILED: could not enumerate the extraction's externals."
rm -f "$SEMNAMES" "$SEMOUT"; exit 1; }
SEM=$(mktemp "$HERE/.semcheck-XXXX.lean")
{
# Import every generated module that is not a template. There are no name
# clashes between crates (verified), and the crate roots transitively pull
# their own models, so this is the same environment the proofs are built on.
for m in $(cd "$HERE/gen" && find . -name '*.lean' -not -name '*_Template.lean' \
| sed 's|^\./||; s|\.lean$||; s|/|.|g' | sort); do
echo "import $m"
done
cat <<'LEANSEM'
open Lean in
#eval show CoreM Unit from do
let env ← getEnv
let path := System.FilePath.mk (← IO.getEnv "SEMNAMES").get!
for line in (← IO.FS.lines path) do
let parts := line.splitOn "|"
if h : parts.length = 2 then
let rel := parts[0]!
let nm := parts[1]!.toName
match env.find? nm with
| none => IO.println s!"SEM|{rel}|{parts[1]!}|ABSENT|-"
| some ci =>
let kind := match ci with
| .axiomInfo _ => "axiom"
| .defnInfo _ => "def"
| .thmInfo _ => "theorem"
| .opaqueInfo _ => "opaque"
| .inductInfo _ => "inductive"
| .ctorInfo _ => "ctor"
| .recInfo _ => "recursor"
| .quotInfo _ => "quot"
let mdl := match env.getModuleIdxFor? nm with
| some i => toString env.header.moduleNames[i]!
| none => "<current>"
IO.println s!"SEM|{rel}|{parts[1]!}|{kind}|{mdl}"
LEANSEM
} > "$SEM"
cd "$AENEAS_LEAN"
SEM_RC=0
SEMNAMES="$SEMNAMES" lake env bash -c "
set -uo pipefail
cd '$HERE/gen' && export LEAN_PATH=\"\$LEAN_PATH:\$PWD:$HERE\"
cd '$HERE'
LEAN_TIMEOUT=$TIMEOUT LEAN_MAX_CORES=$CORES '$HERE/lean-guard' '$SEM'
" > "$SEMOUT" 2>&1 || SEM_RC=$?
cd "$HERE"
rm -f "$SEM" "${SEM%.lean}.olean"
if [ "$SEM_RC" -ne 0 ]; then
echo "SEMANTIC CORRESPONDENCE FAILED: the resolver did not run."
tail -12 "$SEMOUT" | sed 's/^/ /'
rm -f "$SEMNAMES" "$SEMOUT"; exit 1
fi
# Every name the extraction asks for must have been resolved, and its Lean
# verdict must equal the committed table's. The mapping is deliberately strict:
# resolves into the hand-written model module -> MODEL
# resolves to a NON-AXIOM in a generated module -> PROVEN
# anything else -> failure
SEMFAIL=0
NSEM=$(grep -c '^SEM|' "$SEMOUT" || true)
NWANT=$(grep -c '|' "$SEMNAMES" || true)
if [ "$NSEM" -ne "$NWANT" ]; then
echo " SEMANTIC CORRESPONDENCE TRUNCATED: asked about $NWANT externals, Lean answered for $NSEM"
SEMFAIL=1
fi
while IFS='|' read -r _tag rel name kind mdl; do
[ "$_tag" = SEM ] || continue
want=$(awk -F'|' -v r="$rel" -v n="$name" '$1==r && $2==n {print $3}' "$HERE/MODEL-CORRESPONDENCE.txt")
case "$kind:$mdl" in
axiom:"${rel//\//.}") got=MODEL ;;
*:"${rel//\//.}") got=MODEL ;;
axiom:*) got=AXIOM-OUTSIDE-MODEL ;;
ABSENT:*) got=UNRESOLVED ;;
*) got=PROVEN ;;
esac
if [ -z "$want" ]; then
echo " SEMANTIC DRIFT: $rel|$name resolves ($kind in $mdl) but has NO ROW in MODEL-CORRESPONDENCE.txt"
SEMFAIL=1
elif [ "$want" != "$got" ]; then
echo " SEMANTIC DRIFT: $rel|$name — table says $want, Lean says $got ($kind in $mdl)"
SEMFAIL=1
fi
done < "$SEMOUT"
rm -f "$SEMNAMES" "$SEMOUT"
if [ "$SEMFAIL" != 0 ]; then
echo "SEMANTIC CORRESPONDENCE FAILED: the committed table does not match what Lean resolves."
exit 1
fi
echo " $NWANT externals resolved by Lean; every verdict matches the committed table"
echo ""
# ── Phase 3: axiom audit of every certificate ─────────────────────────────── # ── Phase 3: axiom audit of every certificate ───────────────────────────────
echo "=== Phase 3: axiom audit ===" echo "=== Phase 3: axiom audit ==="
EXPECTED="[propext, Classical.choice, Quot.sound]" EXPECTED="[propext, Classical.choice, Quot.sound]"

View file

@ -8,36 +8,151 @@ needs from outside. Each such name must be provided by exactly one of:
assumption, which the axiom gate and the per-certificate cones assumption, which the axiom gate and the per-certificate cones
then govern; then govern;
PROVEN resolved to a real definition in the proven corpus, because a PROVEN resolved to a real definition in the proven corpus, because a
module of this repository declares it (namespace-aware). This is module of this repository declares it (namespace-aware).
the valuable case and the one the docs claim for the tier-A/B
curve calls; nothing has ever checked it.
Anything else is drift: the extraction asks for something this repository does Anything else is drift: the extraction asks for something this repository does
not provide. not provide.
"""
import re, sys, os, glob
DECL = re.compile(
r'^[ \t]*(?:@\[[^\]]*\]\s*)?(?:private |protected |noncomputable |unsafe )*' WHY THIS FILE WAS REWRITTEN 2026-08-01, round-7 external review
r'(axiom|def|abbrev|opaque|structure|inductive)[ \t]+([A-Za-z_][A-Za-z0-9_.\'!?]*)')
NS = re.compile(r'^[ \t]*(namespace|end)[ \t]+([A-Za-z_][A-Za-z0-9_.\']*)') 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.
"""
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)
def declared(path): def declared(path):
"""Fully-qualified names declared in one file, honouring namespaces.""" """Fully-qualified names declared in one file.
names, stack = set(), []
for line in open(path, encoding='utf-8', errors='replace'): Raises ScanError on any declaration keyword whose name cannot be read.
m = NS.match(line) """
if m: raw = open(path, encoding='utf-8', errors='replace').read()
if m.group(1) == 'namespace': text = strip_comments(raw)
stack.append(m.group(2))
elif stack and stack[-1] == m.group(2): # 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)]
names = set()
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
if kind in ('namespace', 'section'):
stack.append(arg)
elif stack:
stack.pop() stack.pop()
continue prefix = [p for p in stack if p]
d = DECL.match(line) names.add('.'.join(prefix + [im.group(1)]) if prefix else im.group(1))
if d:
names.add('.'.join(stack + [d.group(2)]) if stack else d.group(2))
return names return names
def main(root): def main(root):
gen = os.path.join(root, 'gen') gen = os.path.join(root, 'gen')
templates = sorted(glob.glob(os.path.join(gen, '*', '*_Template.lean'))) templates = sorted(glob.glob(os.path.join(gen, '*', '*_Template.lean')))
@ -70,5 +185,30 @@ def main(root):
print(f'CORRESPONDENCE-COUNT|{len(rows)}') print(f'CORRESPONDENCE-COUNT|{len(rows)}')
return 1 if unresolved else 0 return 1 if unresolved else 0
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
if __name__ == '__main__': if __name__ == '__main__':
try:
if len(sys.argv) > 2 and sys.argv[1] == '--names':
sys.exit(emit_names(sys.argv[2]))
sys.exit(main(sys.argv[1])) 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)

View file

@ -114,6 +114,49 @@ sed -i '0,/|MODEL$/s/|MODEL$/|PROVEN/' "$HERE/MODEL-CORRESPONDENCE.txt"
expect "case 4: a verdict edited in the committed table" 1 "MODEL CORRESPONDENCE DRIFT" expect "case 4: a verdict edited in the committed table" 1 "MODEL CORRESPONDENCE DRIFT"
cp "$STASH/corr" "$HERE/MODEL-CORRESPONDENCE.txt" cp "$STASH/corr" "$HERE/MODEL-CORRESPONDENCE.txt"
# ── 5/6. THE ROUND-7 FINDINGS, so they cannot regress ──────────────────────
# Both were real. Case 5 is GPT-5.6's constructive counterexample: a definition
# that exists ONLY inside a block comment was read as a real declaration, so the
# scanner reported PROVEN for a name Lean resolves to an axiom. Case 6 is the
# one that was live in four committed tables: Aeneas wraps long declarations,
# the old scanner required keyword and name on one physical line, and so it
# SILENTLY DROPPED them — nine to ten externals per fork had no row at all.
#
# Case 6 is the more important of the two. A gate that drops what it cannot
# read is worse than no gate: it prints green across a gap that is invisible in
# the diff. The scanner must now FAIL rather than skip.
CX=$(mktemp -d)
mkdir -p "$CX/gen/Forged"
printf 'axiom Forged.value : Nat\n' > "$CX/gen/Forged/FunsExternal_Template.lean"
printf 'axiom\n Forged.value : Nat\n' > "$CX/gen/Forged/FunsExternal.lean"
printf '/-\nnamespace Forged\ndef value : Nat := 0\nend Forged\n-/\n' > "$CX/gen/Forged/Funs.lean"
OUT=$(python3 "$HERE/model-correspondence.py" "$CX" 2>&1)
if grep -q 'Forged.value|MODEL' <<<"$OUT"; then
echo " ✓ case 5: a definition inside a block comment is not read as a declaration"
else
echo " ✗ case 5: comment-only definition mis-read — scanner says:"; sed 's/^/ /' <<<"$OUT"
FAILURES=$((FAILURES+1))
fi
printf 'axiom\n Forged.wrapped\n :\n Nat\n' >> "$CX/gen/Forged/FunsExternal_Template.lean"
OUT=$(python3 "$HERE/model-correspondence.py" "$CX" 2>&1); RC=$?
if [ "$RC" -ne 0 ] && grep -q 'UNRESOLVED\|Forged.wrapped' <<<"$OUT"; then
echo " ✓ case 6: a declaration whose name wraps to the next line is SEEN, not dropped"
else
echo " ✗ case 6: wrapped declaration dropped or mis-handled (rc=$RC):"; sed 's/^/ /' <<<"$OUT"
FAILURES=$((FAILURES+1))
fi
printf 'axiom\n' > "$CX/gen/Forged/FunsExternal_Template.lean"
OUT=$(python3 "$HERE/model-correspondence.py" "$CX" 2>&1); RC=$?
if [ "$RC" -eq 2 ] && grep -q 'fails closed' <<<"$OUT"; then
echo " ✓ case 7: an unparseable declaration stops the scanner (exit 2), never silence"
else
echo " ✗ case 7: unparseable declaration did not fail closed (rc=$RC)"
FAILURES=$((FAILURES+1))
fi
rm -rf "$CX"
expect "restored: the table matches again" 0 "answered by PROVEN definitions" expect "restored: the table matches again" 0 "answered by PROVEN definitions"
echo "" echo ""