mirror of
https://github.com/saymrwulf/betrusted-ed25519-verified.git
synced 2026-09-04 20:24:08 +00:00
Phase 2b asks the kernel whether any AXIOM is declared under Proofs/. Phase 3
pins the cones of the named certificates. Between them sat every other
declaration in the corpus — around three thousand of them — and a helper lemma
quietly acquiring a hash oracle in its cone moved nothing either phase looked
at.
Phase 2c closes that. Ported from ltl-accumulator-verified, where a nine-attack
self-test proved a source-regex enumerator evadable by attributed, private,
indented and `instance` declarations and by a nested-namespace basename
collision. Reading the compiled environment sees what the kernel saw; no name
shape hides. Every constant contributes module, name, kind and full axiom cone,
and the observed set must equal inventory-allowlist.txt exactly in BOTH
directions, with a count trailer so a truncated run cannot pass as an empty
diff.
FOUR THINGS THIS BUILD GOT WRONG, each caught by a check rather than by review:
- The number of inventory drivers is a per-repo FACT, not an assumption.
dalek and anza cannot import their corpus as one environment (Proofs.Basic
and Proofs.ConstSpecs both declare CurveFieldProofs.zero_spec); risc0 and
betrusted have no Proofs.Basic at all. Determined by compiling a probe.
check.sh now DISCOVERS its drivers from the filesystem instead of naming
two, and the generator refuses to split out a module the repo lacks.
- The split let one real declaration hide behind another's entry. Keyed on
name alone, the two zero_specs produced byte-identical records, so 3022
declarations were covered by 3021 allowlist entries. Caught by the count
trailer. Every record now carries its originating module.
- The gate's success line said "single sanctioned axiom", inherited from the
accumulator's policy. This corpus permits NONE. A success message
describing a different rule is how an assertion stops meaning anything.
- selftest-axgate.sh lifted Phase 2b with a range ending at "Phase 3", so
inserting Phase 2c between them made it swallow the new phase and die on
variables only check.sh defines — surfacing as the BASELINE case failing,
a self-test blaming a gate for its own extraction bug. Both self-tests now
stop at the next phase marker whatever it is called, and refuse to run if
they capture more than one phase. The guard is the fix; the range was the
symptom.
WHAT THIS IS NOT, recorded in TRUSTED-BASE.md at the same length as the claim:
- No independent cone walker. The accumulator cross-checks collectAxioms
against a hand-written walker. Ported here it was wrong in BOTH directions
on mathlib's inductive shapes: EdPoint gave [] against the kernel's three
axioms, and once extended, ProjPoint gave three against the kernel's none.
Two implementations disagreeing both ways are a second wrong answer, not a
check. These cones rest on collectAxioms alone.
- Thirteen Proofs/Scalar* modules are inventoried by nothing — the
second-button seam, still open. Phase 2c names every uncovered module on
every run so the omission is visible rather than inferred.
selftest-inventory.sh exercises the shipping gate with six cases, each
asserting a specific diagnostic, including the one that matters: a cone
widened by one oracle while name, module and kind stay put. Negative-tested by
disabling the gate's diff, which turns two cases red including one for the
wrong reason, correctly reported as such.
Verified green: 20 runs across the four repositories (four buttons, four
harness, four inventory, four axgate, four binding self-tests), zero red. The
four check-scalar.sh greens from the preceding sweep stand: that script neither
reads the pin file nor changed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
108 lines
6 KiB
Bash
Executable file
108 lines
6 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# selftest-inventory.sh — adversarial self-test for check.sh Phase 2c.
|
|
#
|
|
# WHAT PHASE 2c IS FOR. Phase 2b asks the kernel whether any AXIOM is declared
|
|
# under Proofs/. It says nothing about the ~3000 other declarations. Phase 3
|
|
# pins the cones of the 31 named certificates. Between them sits everything
|
|
# else: a helper lemma that quietly acquired an oracle in its cone, a
|
|
# declaration added, removed or renamed, a compiler-generated auxiliary that
|
|
# changed shape. Phase 2c pins that whole surface and diffs it both ways.
|
|
#
|
|
# Cases, each asserting a SPECIFIC diagnostic:
|
|
# 0 positive control: the untouched tree passes
|
|
# 1 an allowlist row deleted -> UNCLASSIFIED (in env, not allowlisted)
|
|
# 2 an allowlist row invented -> STALE (allowlisted, not in env)
|
|
# 3 a cone silently widened -> BOTH, because the record changed
|
|
# 4 an axiom row appears -> AXIOM SURFACE DRIFT
|
|
# 5 the count trailer disagrees -> INVENTORY TRUNCATED (no vacuous pass)
|
|
#
|
|
# It runs the SHIPPING inventory_gate.sh against a recorded observation, so no
|
|
# Lean is needed and the whole thing takes a second. The observation itself is
|
|
# produced by check.sh Phase 2c; this test attacks the gate that judges it.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
set -uo pipefail
|
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
FAILURES=0
|
|
STASH="$(mktemp -d)"
|
|
trap 'rm -rf "$STASH"' EXIT INT TERM
|
|
|
|
ALLOW="$HERE/inventory-allowlist.txt"
|
|
[ -s "$ALLOW" ] || { echo "FATAL: inventory-allowlist.txt missing or empty"; exit 1; }
|
|
|
|
# The observation a green run would produce: the allowlist itself plus a
|
|
# trailer. Deriving it from the allowlist is exactly right for this test — the
|
|
# question is whether the GATE reacts correctly to differences, and each case
|
|
# below introduces one.
|
|
mkobs() { # mkobs <file> [extra-line...]
|
|
local out="$1"; shift
|
|
grep '^INV|' "$ALLOW" > "$out"
|
|
for l in "$@"; do printf '%s\n' "$l" >> "$out"; done
|
|
LC_ALL=C sort -o "$out" "$out"
|
|
echo "INV-COUNT|$(grep -c '^INV|' "$out")" >> "$out"
|
|
}
|
|
|
|
expect() { # expect <label> <obs> <allow> <want-rc> <want-substring>
|
|
local label="$1" obs="$2" allow="$3" want_rc="$4" want_txt="$5" out rc
|
|
out=$("$HERE/inventory_gate.sh" "$obs" "$allow" 2>&1); rc=$?
|
|
if [ "$rc" -ne "$want_rc" ]; then
|
|
echo " ✗ $label: exit $rc, expected $want_rc"; echo "$out" | sed 's/^/ /'
|
|
FAILURES=$((FAILURES+1)); return
|
|
fi
|
|
if ! grep -qF "$want_txt" <<<"$out"; then
|
|
echo " ✗ $label: exit code right, diagnostic wrong (rejected for the wrong reason)"
|
|
echo " wanted: $want_txt"; echo "$out" | sed 's/^/ /'
|
|
FAILURES=$((FAILURES+1)); return
|
|
fi
|
|
echo " ✓ $label"
|
|
}
|
|
|
|
echo "=== selftest-inventory: attacking check.sh Phase 2c's gate ==="
|
|
|
|
# ── 0. positive control ────────────────────────────────────────────────────
|
|
mkobs "$STASH/obs.txt"
|
|
expect "case 0 control: a faithful observation passes" "$STASH/obs.txt" "$ALLOW" 0 "environment == allowlist"
|
|
|
|
# ── 1. a row deleted from the allowlist: the declaration is still there, so
|
|
# the gate must report it as unclassified rather than shrug.
|
|
VICTIM=$(grep '^INV|' "$ALLOW" | grep '|theorem|' | head -1)
|
|
grep -vxF "$VICTIM" "$ALLOW" > "$STASH/allow-short.txt"
|
|
expect "case 1: allowlist row deleted -> UNCLASSIFIED" "$STASH/obs.txt" "$STASH/allow-short.txt" 1 "UNCLASSIFIED"
|
|
|
|
# ── 2. a row invented in the allowlist: nothing in the environment matches it.
|
|
cp "$ALLOW" "$STASH/allow-extra.txt"
|
|
echo "INV|Proofs.Ghost|CurveFieldProofs.ghost_lemma|theorem|Classical.choice" >> "$STASH/allow-extra.txt"
|
|
expect "case 2: allowlist row with no declaration -> STALE" "$STASH/obs.txt" "$STASH/allow-extra.txt" 1 "STALE"
|
|
|
|
# ── 3. THE ONE THAT MATTERS: a cone silently widened. Same module, same name,
|
|
# same kind — only the axiom cone grew. Phases 2b and 3 both pass this:
|
|
# 2b only looks for axiom DECLARATIONS, and 3 only pins the 31 named
|
|
# certificates. If the victim is not one of those, nothing else sees it.
|
|
WIDENED=$(sed 's/$/,sha2.Sha512/' <<<"$VICTIM")
|
|
mkobs "$STASH/obs-wide.txt"
|
|
grep -vxF "$VICTIM" "$STASH/obs-wide.txt" > "$STASH/t" && mv "$STASH/t" "$STASH/obs-wide.txt"
|
|
printf '%s\n' "$WIDENED" >> "$STASH/obs-wide.txt"
|
|
LC_ALL=C sort -o "$STASH/obs-wide.txt" "$STASH/obs-wide.txt"
|
|
echo "INV-COUNT|$(grep -c '^INV|' "$STASH/obs-wide.txt")" >> "$STASH/obs-wide.txt"
|
|
expect "case 3: a cone widened by one oracle -> UNCLASSIFIED" "$STASH/obs-wide.txt" "$ALLOW" 1 "UNCLASSIFIED"
|
|
|
|
# ── 4. an axiom appears in the audited corpus. The sanctioned external models
|
|
# live in gen/, outside every module the drivers cover, so any axiom here
|
|
# is a declaration smuggled into the proof corpus.
|
|
mkobs "$STASH/obs-ax.txt" "INV|Proofs.FeQ|CurveFieldProofs.rogue|axiom|"
|
|
expect "case 4: an axiom in the corpus -> AXIOM SURFACE DRIFT" "$STASH/obs-ax.txt" "$ALLOW" 1 "AXIOM SURFACE DRIFT"
|
|
|
|
# ── 5. truncation. "Nothing found" and "nothing wrong" must not share a path:
|
|
# a crashed or cut-short run has to fail, not pass as an empty diff.
|
|
grep '^INV|' "$ALLOW" | head -100 > "$STASH/obs-trunc.txt"
|
|
echo "INV-COUNT|$(grep -c '^INV|' "$ALLOW")" >> "$STASH/obs-trunc.txt"
|
|
expect "case 5: trailer disagrees with the lines -> INVENTORY TRUNCATED" "$STASH/obs-trunc.txt" "$ALLOW" 1 "INVENTORY TRUNCATED"
|
|
|
|
echo ""
|
|
if [ "$FAILURES" -eq 0 ]; then
|
|
echo "SELFTEST PASSED — Phase 2c's gate rejects surface drift in both directions,"
|
|
echo "for the stated reason in each case."
|
|
exit 0
|
|
fi
|
|
echo "SELFTEST FAILED: $FAILURES case(s) did not behave as claimed."
|
|
exit 1
|