oficina / sheets Public

Add excelize oracle cross-check for the hand-authored formula corpus

calc/testdata/corpus/formulas.json was hand-authored (no soffice on any
machine that has touched this repo), so TestAgainstReferenceCorpus only
proved self-consistency with its own author's reasoning, not agreement
with an independent implementation.

Adds internal/sheets/xlsx/oracle_test.go (+ oracle_corpus_test.go,
oracle_excelize_test.go, oracle_divergences_test.go), which drives every
non-volatile corpus entry through excelize's own CalcCellValue and
requires it to agree with calc.Eval's live answer, unless the exact
disagreement is a reasoned, re-checked entry in oracleDivergences. Lives
in xlsx (not calc) because excelize_confinement_test.go confines
excelize imports to that tree even for test files, and xlsx already
bridges excelize and calc for the same reason (import.go/export.go).

Result: 60 entries, 59 agree, 1 reviewed divergence (excelize v2.11.0's
TRIM is strings.TrimSpace only, contradicting its own doc comment and
real Excel's documented interior-whitespace-collapsing behavior — ours
is correct), 0 unavailable. No changes to internal/sheets/calc.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APtNStXDZ5H2fAc1ZuGqtS
Paul Campbell pushed by paul@keymail.dev bbd6ef5e09bbfc2e024f9a251732f99d34272f9e parent cf7f93e
4 files changed, +506 −0
  • internal/sheets/xlsx/oracle_corpus_test.go +91 −0
  • internal/sheets/xlsx/oracle_divergences_test.go +62 −0
  • internal/sheets/xlsx/oracle_excelize_test.go +123 −0
  • internal/sheets/xlsx/oracle_test.go +230 −0
diff --git a/internal/sheets/xlsx/oracle_corpus_test.go b/internal/sheets/xlsx/oracle_corpus_test.go
new file mode 100644
index 0000000..a3cfcc0
--- /dev/null
+++ b/internal/sheets/xlsx/oracle_corpus_test.go
@@ -0,0 +1,91 @@
+package xlsx_test
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+// oracleCorpusPath and oracleExclusionsPath point at the SAME committed
+// files internal/sheets/calc/corpus_test.go reads (corpusPath,
+// exclusionsPath there) — one corpus, cross-checked from two packages.
+// This package cannot import calc's _test.go types (Go test files are
+// not part of a package's importable surface), so the JSON shape is
+// duplicated here rather than shared; keep it in sync with corpus_test.go
+// if formulas.json's wire format ever changes.
+const (
+ oracleCorpusPath = "../calc/testdata/corpus/formulas.json"
+ oracleExclusionsPath = "../calc/testdata/corpus/volatile-excluded.txt"
+)
+
+// oracleCorpusValue mirrors calc's corpusValue (corpus_test.go): only
+// the field named by Kind is meaningful.
+type oracleCorpusValue struct {
+ Kind string `json:"kind"`
+ Num float64 `json:"num,omitempty"`
+ Str string `json:"str,omitempty"`
+ Bool bool `json:"bool,omitempty"`
+ Err string `json:"err,omitempty"`
+}
+
+// oracleCorpusEntry mirrors calc's corpusEntry. Want is carried through
+// for context in failure/log messages, but this harness's actual
+// comparison is our engine's live Eval output against excelize's live
+// CalcCellValue output — Want plays no part in that comparison, unlike
+// TestAgainstReferenceCorpus in the calc package, which checks Eval
+// against Want directly.
+type oracleCorpusEntry struct {
+ Function string `json:"function"`
+ Expr string `json:"expr"`
+ Want oracleCorpusValue `json:"want"`
+}
+
+type oracleCorpusFile struct {
+ Note string `json:"note"`
+ Entries []oracleCorpusEntry `json:"entries"`
+}
+
+// loadOracleCorpus reads and parses the same formulas.json
+// TestAgainstReferenceCorpus (calc package) uses.
+func loadOracleCorpus(t *testing.T) []oracleCorpusEntry {
+ t.Helper()
+ data, err := os.ReadFile(filepath.FromSlash(oracleCorpusPath))
+ if err != nil {
+ t.Fatalf("loadOracleCorpus: %v", err)
+ }
+ var cf oracleCorpusFile
+ if err := json.Unmarshal(data, &cf); err != nil {
+ t.Fatalf("loadOracleCorpus: %v", err)
+ }
+ if len(cf.Entries) == 0 {
+ t.Fatal("loadOracleCorpus: corpus has no entries")
+ }
+ return cf.Entries
+}
+
+// loadOracleExclusions parses the same volatile-excluded.txt
+// TestAgainstReferenceCorpus honors, for the same reason: NOW/TODAY
+// return a different answer on every evaluation (calc's env.Now() is
+// pinned per-pass to a fixed instant; excelize's NOW()/TODAY() read the
+// real wall clock), so no captured pair of answers from the two engines
+// can ever be expected to agree, independent of whether either engine
+// is "right." Skipping here mirrors calc's own reasoning, it does not
+// invent a new one.
+func loadOracleExclusions(t *testing.T) map[string]bool {
+ t.Helper()
+ data, err := os.ReadFile(filepath.FromSlash(oracleExclusionsPath))
+ if err != nil {
+ t.Fatalf("loadOracleExclusions: %v", err)
+ }
+ out := map[string]bool{}
+ for _, line := range strings.Split(string(data), "\n") {
+ line = strings.TrimSpace(line)
+ if line == "" || strings.HasPrefix(line, "#") {
+ continue
+ }
+ out[line] = true
+ }
+ return out
+}
diff --git a/internal/sheets/xlsx/oracle_divergences_test.go b/internal/sheets/xlsx/oracle_divergences_test.go
new file mode 100644
index 0000000..b789923
--- /dev/null
+++ b/internal/sheets/xlsx/oracle_divergences_test.go
@@ -0,0 +1,62 @@
+package xlsx_test
+
+// oracleDivergence records one reviewed disagreement between this
+// package's own formula engine (internal/sheets/calc) and excelize's
+// independent one, for one exact corpus expression. This is NOT a
+// filter — TestAgainstExcelizeOracle (oracle_test.go) does not consult
+// this list to decide whether to run a comparison, only to decide
+// whether an already-found disagreement is one a human has already
+// looked at. A new disagreement not listed here fails the test; nothing
+// here can silently widen to cover one nobody has actually reviewed.
+//
+// OurAnswer and OracleAnswer are the exact strings the two engines
+// produced at the time this entry was written (calc's Value rendered by
+// renderCalcValue, excelize's raw CalcCellValue string) — the test
+// re-checks both engines' CURRENT answers against these on every run
+// (see the "stale divergence record" branch in TestAgainstExcelizeOracle)
+// so a later change to either engine that resolves — or changes the
+// shape of — a listed divergence cannot pass silently: either the
+// disagreement is gone (fix the entry by deleting it) or it has
+// changed shape (fix the entry to match, and re-review Note).
+type oracleDivergence struct {
+ Expr string
+ OurAnswer string
+ OracleAnswer string
+ Note string // one line: which engine we believe, and why
+}
+
+// oracleDivergences is the full, currently-reviewed list. As of this
+// harness's first run against all 60 corpus entries (30-ish functions),
+// exactly one disagreement was found — see the entry below for the
+// investigation. That is a real finding, not a placeholder: 59 of 60
+// entries have excelize, an entirely independent formula engine,
+// agreeing with ours.
+var oracleDivergences = []oracleDivergence{
+ {
+ Expr: `TRIM(" a b ")`,
+ OurAnswer: "a b",
+ OracleAnswer: "a b",
+ // Investigated 2026-08-30: real Excel's TRIM "removes all spaces
+ // from text except for single spaces between words" (Microsoft's
+ // own documented behavior) -- i.e. it collapses interior runs of
+ // whitespace to one space, not just leading/trailing. Our fnTrim
+ // (fn_text.go) implements exactly that via strings.Fields, which
+ // is what produces "a b" here.
+ //
+ // excelize v2.11.0's TRIM (calc.go, the TRIM function) is
+ // `newStringFormulaArg(strings.TrimSpace(...))` -- leading/
+ // trailing only, no interior collapse -- even though its OWN doc
+ // comment two lines above claims the correct behavior ("removes
+ // extra spaces ... except for single spaces between words"). Read
+ // directly from
+ // $(go env GOMODCACHE)/github.com/xuri/excelize/v2@v2.11.0/calc.go.
+ // This is excelize's implementation contradicting its own
+ // documented contract, not a legitimate alternate reading of
+ // TRIM's semantics.
+ //
+ // We believe OUR engine here: it matches real Excel's documented
+ // and observable behavior; excelize's TRIM does not match its own
+ // doc comment, let alone Excel's.
+ Note: "excelize v2.11.0's TRIM is strings.TrimSpace only (does not collapse interior whitespace runs), contradicting both its own doc comment and real Excel's documented TRIM behavior. Ours is correct.",
+ },
+}
diff --git a/internal/sheets/xlsx/oracle_excelize_test.go b/internal/sheets/xlsx/oracle_excelize_test.go
new file mode 100644
index 0000000..fd2a789
--- /dev/null
+++ b/internal/sheets/xlsx/oracle_excelize_test.go
@@ -0,0 +1,123 @@
+package xlsx_test
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/xuri/excelize/v2"
+)
+
+// oracleCell is the same fixed lookup table calc's corpusEnv
+// (corpus_test.go) sets up — A1:A3 hold 10/20/30, B1:B3 their English
+// names, C1 deliberately left unset — because VLOOKUP/INDEX/MATCH corpus
+// entries reference it by address. Anything a corpus entry needs beyond
+// this small table is out of scope for this harness (matching the
+// current corpus's own scope: every entry is either self-contained or
+// reads only this table).
+func newOracleWorkbook() (f *excelize.File, sheet string) {
+ f = excelize.NewFile()
+ sheet = f.GetSheetList()[0]
+ must(f.SetCellFloat(sheet, "A1", 10, -1, 64))
+ must(f.SetCellStr(sheet, "B1", "ten"))
+ must(f.SetCellFloat(sheet, "A2", 20, -1, 64))
+ must(f.SetCellStr(sheet, "B2", "twenty"))
+ must(f.SetCellFloat(sheet, "A3", 30, -1, 64))
+ must(f.SetCellStr(sheet, "B3", "thirty"))
+ // C1 intentionally unset.
+ return f, sheet
+}
+
+// oracleFormulaCell is where each corpus expression's formula is
+// written — outside A1:C3 so it can never collide with the support
+// table above.
+const oracleFormulaCell = "Z1"
+
+// oracleKnownErrorTokens are the spreadsheet error strings excelize (and
+// our own ErrKind.String, errors.go) can produce as a genuine cell
+// value, as opposed to excelize's own diagnostic text for a formula it
+// could not evaluate at all. "#CIRCULAR!" is deliberately absent: it is
+// this app's own invented token (errors.go's ErrKind.String), not a
+// real Excel error string, and never appears in excelize output.
+var oracleKnownErrorTokens = map[string]bool{
+ "#DIV/0!": true, "#VALUE!": true, "#REF!": true, "#NAME?": true,
+ "#NUM!": true, "#N/A": true, "#NULL!": true, "#SPILL!": true,
+ "#CALC!": true, "#GETTING_DATA": true,
+}
+
+// oracleAnswer is one corpus expression's outcome from excelize:
+// Available is false when excelize could not produce a real spreadsheet
+// answer at all (an unimplemented function, a formula it refuses to
+// parse) — see classifyOracleResult's doc comment for the full
+// reasoning. Value is only meaningful when Available is true.
+type oracleAnswer struct {
+ Available bool
+ Value string
+ Reason string // why Available is false, or empty
+}
+
+// computeOracleAnswer writes expr into a fresh excelize workbook (with
+// the same support table calc's corpusEnv provides) and asks excelize to
+// evaluate it via CalcCellValue — the independent-implementation half of
+// this harness's cross-check. See classifyOracleResult for how its
+// (value, error) pair is turned into an oracleAnswer.
+func computeOracleAnswer(t *testing.T, expr string) oracleAnswer {
+ t.Helper()
+ f, sheet := newOracleWorkbook()
+ defer func() {
+ if err := f.Close(); err != nil {
+ t.Errorf("close oracle workbook for %q: %v", expr, err)
+ }
+ }()
+
+ if err := f.SetCellFormula(sheet, oracleFormulaCell, "="+expr); err != nil {
+ return oracleAnswer{Available: false, Reason: "excelize rejected the formula: " + err.Error()}
+ }
+ val, err := f.CalcCellValue(sheet, oracleFormulaCell)
+ return classifyOracleResult(val, err)
+}
+
+// classifyOracleResult turns CalcCellValue's (value, error) pair into an
+// oracleAnswer. excelize is NOT consistent about what it returns for a
+// formula error (verified against v2.11.0 directly, not assumed):
+//
+// - A real spreadsheet error the formula itself produces (division by
+// zero, VLOOKUP with no match, ...) comes back as BOTH a non-nil err
+// AND a val that IS one of the standard error tokens (e.g. val="#N/A",
+// err="VLOOKUP no result found" — err's own text is not the token).
+// That is a genuine answer, not an unavailable oracle: val is used.
+// - A function excelize has not implemented ALSO comes back with a val
+// that happens to look like a token (val="#VALUE!") but err's text
+// contains "not support" — checked first, specifically so this case
+// is never mistaken for the first one just because val looks legit.
+// - A formula excelize's parser rejects outright comes back with
+// val="" and err="formula not valid".
+//
+// Anything else (an err whose text is not a recognized shape) is treated
+// as unavailable too, conservatively: better to under-claim oracle
+// coverage than to silently accept a value from a state this harness
+// does not actually understand.
+func classifyOracleResult(val string, err error) oracleAnswer {
+ if err == nil {
+ return oracleAnswer{Available: true, Value: val}
+ }
+ msg := err.Error()
+ if strings.Contains(msg, "not support") {
+ return oracleAnswer{Available: false, Reason: "unimplemented in excelize: " + msg}
+ }
+ if msg == "formula not valid" {
+ return oracleAnswer{Available: false, Reason: "excelize could not parse the formula: " + msg}
+ }
+ if oracleKnownErrorTokens[msg] {
+ return oracleAnswer{Available: true, Value: msg}
+ }
+ if oracleKnownErrorTokens[val] {
+ return oracleAnswer{Available: true, Value: val}
+ }
+ return oracleAnswer{Available: false, Reason: "unrecognized excelize error: " + msg}
+}
+
+func must(err error) {
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/internal/sheets/xlsx/oracle_test.go b/internal/sheets/xlsx/oracle_test.go
new file mode 100644
index 0000000..e3cd739
--- /dev/null
+++ b/internal/sheets/xlsx/oracle_test.go
@@ -0,0 +1,230 @@
+// Cross-checking against excelize — why this lives here, not in calc.
+//
+// calc/testdata/corpus/formulas.json (internal/sheets/calc) was meant to
+// be generated by driving a real LibreOffice headless, so the formula
+// engine is checked against an independent implementation rather than
+// its own author's reasoning. No machine that has touched this repo has
+// had soffice on PATH, so the corpus was hand-authored instead — see
+// loadCorpus's doc comment in calc/corpus_test.go. That corpus proves
+// self-consistency, not conformance: it shipped, for a while, with
+// COUNT(1,2,"a",TRUE) expecting 2 (a wrong implementation's answer) until
+// a human reviewer noticed real spreadsheets return 3 — a mistake the
+// corpus, checking only itself, could not have caught.
+//
+// excelize (github.com/xuri/excelize/v2, already in go.mod for XLSX
+// import/export) ships its own formula engine, CalcCellValue — a
+// different codebase by different authors, independent of ours by
+// construction. TestAgainstExcelizeOracle below drives it against every
+// non-volatile corpus entry and cross-checks its answer against calc's
+// live Eval — not against the corpus's "want" field, which is what
+// TestAgainstReferenceCorpus (calc package) already checks and is
+// exactly the self-consistency this harness exists to get past.
+//
+// This lives in internal/sheets/xlsx, not internal/sheets/calc, for one
+// mechanical reason and one architectural one:
+// - internal/guards/excelize_confinement_test.go fails any file outside
+// internal/sheets/xlsx (any depth) that imports excelize, with no
+// exemption for _test.go files. A file in calc that imported excelize
+// — even test-only — would fail that guard.
+// - xlsx already imports both excelize and calc (import.go, export.go)
+// for exactly the same reason this harness needs both: it is the one
+// package whose whole job is bridging excelize's world and ours
+// (import.go's package doc: "excelize is confined to this package
+// deliberately ... letting any other package import it would couple
+// our storage to theirs"). A cross-check between the two engines is
+// that same bridging job, just for verification instead of import.
+//
+// A brand-new internal/oracle package was the other option considered:
+// rejected because it would need excelizeHome widened in the guard to
+// cover it, for no benefit over reusing the tree that already exists for
+// exactly this pairing of dependencies.
+package xlsx_test
+
+import (
+ "math"
+ "strconv"
+ "testing"
+ "time"
+
+ "amadan.net/oficina/sheets/internal/sheets/calc"
+)
+
+// oracleEnv is calc.Env backed by the identical fixed lookup table
+// computeOracleAnswer's excelize workbook sets up (oracle_excelize_test.go):
+// A1:A3 hold 10/20/30, B1:B3 their English names, C1 deliberately unset.
+// Now is pinned rather than wired to the real clock because NOW/TODAY are
+// excluded from this comparison anyway (loadOracleExclusions) — nothing
+// here ever reads it for a real answer, but calc.Env requires it.
+type oracleEnv map[calc.CellKey]calc.Value
+
+func (e oracleEnv) CellValue(k calc.CellKey) calc.Value { return e[k] }
+func (e oracleEnv) Now() time.Time { return time.Date(2026, 8, 30, 12, 0, 0, 0, time.UTC) }
+
+var oracleCellEnv = oracleEnv{
+ {RowID: 1, ColID: 1}: {Kind: calc.KindNum, Num: 10},
+ {RowID: 1, ColID: 2}: {Kind: calc.KindStr, Str: "ten"},
+ {RowID: 2, ColID: 1}: {Kind: calc.KindNum, Num: 20},
+ {RowID: 2, ColID: 2}: {Kind: calc.KindStr, Str: "twenty"},
+ {RowID: 3, ColID: 1}: {Kind: calc.KindNum, Num: 30},
+ {RowID: 3, ColID: 2}: {Kind: calc.KindStr, Str: "thirty"},
+}
+
+// renderCalcValue renders a calc.Value the same way an excelize
+// CalcCellValue answer would read as text, so the two sides of the
+// comparison — and oracleDivergences' recorded strings — are directly
+// comparable and directly printable.
+func renderCalcValue(v calc.Value) string {
+ switch v.Kind {
+ case calc.KindNum:
+ return strconv.FormatFloat(v.Num, 'g', -1, 64)
+ case calc.KindStr:
+ return v.Str
+ case calc.KindBool:
+ if v.Bool {
+ return "TRUE"
+ }
+ return "FALSE"
+ case calc.KindErr:
+ return v.Err.String()
+ default: // calc.KindEmpty
+ return ""
+ }
+}
+
+// agreesWithOracle compares our engine's Value against excelize's raw
+// answer string. Numeric comparison tolerates a small epsilon — matching
+// calc's own corpus_test.go valuesMatch — because excelize's
+// CalcCellValue renders floats to fewer significant digits than Go's
+// full round-trip precision (observed directly: SQRT(2) comes back as
+// "1.4142135623731", 13 significant digits, not
+// "1.4142135623730951"), which is a rendering difference, not a
+// disagreement about the value.
+func agreesWithOracle(our calc.Value, oracleVal string) bool {
+ switch our.Kind {
+ case calc.KindNum:
+ f, err := strconv.ParseFloat(oracleVal, 64)
+ if err != nil {
+ return false
+ }
+ return f == our.Num || math.Abs(f-our.Num) < 1e-9
+ case calc.KindBool:
+ want := "FALSE"
+ if our.Bool {
+ want = "TRUE"
+ }
+ return oracleVal == want
+ case calc.KindErr:
+ return oracleVal == our.Err.String()
+ case calc.KindStr:
+ return oracleVal == our.Str
+ default: // calc.KindEmpty
+ return oracleVal == ""
+ }
+}
+
+// mustParseOracle parses expr with calc.Parse, failing the test on a
+// parse error — every corpus expression is already known-parseable
+// (TestAgainstReferenceCorpus parses the same file), so a failure here
+// means the corpus and this harness have drifted apart.
+func mustParseOracle(t *testing.T, expr string) calc.Node {
+ t.Helper()
+ n, err := calc.Parse(expr)
+ if err != nil {
+ t.Fatalf("mustParseOracle(%q): %v", expr, err)
+ }
+ return n
+}
+
+// TestAgainstExcelizeOracle is the cross-check itself. For every
+// non-volatile corpus entry it evaluates the SAME expression with calc's
+// own engine and with excelize's, and requires them to agree UNLESS the
+// exact disagreement is already reviewed in oracleDivergences.
+//
+// Three outcomes per entry, all counted and none silently dropped:
+// - agree: the common case.
+// - known divergence: logged with its reasoned note, not a failure —
+// but the recorded strings are checked against what both engines
+// currently say, so the record cannot rot out from under a later
+// change to either engine.
+// - unavailable: excelize could not produce a real answer at all
+// (unimplemented function, parse rejection) — logged explicitly as
+// its own category, never merged into "agree" or silently skipped.
+//
+// Anything else — a disagreement with no matching entry in
+// oracleDivergences — fails the test outright.
+func TestAgainstExcelizeOracle(t *testing.T) {
+ entries := loadOracleCorpus(t)
+ excluded := loadOracleExclusions(t)
+ known := indexOracleDivergences(t)
+
+ var agree, unavailable, knownDivergent, compared int
+ for _, e := range entries {
+ if excluded[e.Function] {
+ continue
+ }
+ compared++
+
+ ourVal := calc.Eval(mustParseOracle(t, e.Expr), oracleCellEnv)
+ ourStr := renderCalcValue(ourVal)
+
+ oa := computeOracleAnswer(t, e.Expr)
+ if !oa.Available {
+ unavailable++
+ t.Logf("ORACLE UNAVAILABLE: %s %q: %s", e.Function, e.Expr, oa.Reason)
+ continue
+ }
+
+ if agreesWithOracle(ourVal, oa.Value) {
+ agree++
+ continue
+ }
+
+ d, isKnown := known[e.Expr]
+ if !isKnown {
+ t.Errorf("NEW DISAGREEMENT: %s %q: ours=%q excelize=%q — not in oracleDivergences (oracle_divergences_test.go); investigate before adding it there, do not assume we are wrong", e.Function, e.Expr, ourStr, oa.Value)
+ continue
+ }
+ if d.OurAnswer != ourStr || d.OracleAnswer != oa.Value {
+ t.Errorf("STALE DIVERGENCE RECORD: %s %q: recorded ours=%q/excelize=%q, now ours=%q/excelize=%q — update oracle_divergences_test.go (either the disagreement is gone, or it changed shape and needs re-review)", e.Function, e.Expr, d.OurAnswer, d.OracleAnswer, ourStr, oa.Value)
+ continue
+ }
+ knownDivergent++
+ t.Logf("KNOWN DIVERGENCE: %s %q: ours=%q excelize=%q — %s", e.Function, e.Expr, ourStr, oa.Value, d.Note)
+ }
+
+ t.Logf("oracle cross-check: %d entries compared (%d volatile excluded), agree=%d known-divergence=%d unavailable=%d",
+ compared, len(entries)-compared, agree, knownDivergent, unavailable)
+}
+
+// indexOracleDivergences keys oracleDivergences by Expr and fails fast
+// on a duplicate key — two records for the same expression is itself a
+// list-rot bug (which one is authoritative?), not a valid state.
+func indexOracleDivergences(t *testing.T) map[string]oracleDivergence {
+ t.Helper()
+ out := make(map[string]oracleDivergence, len(oracleDivergences))
+ for _, d := range oracleDivergences {
+ if _, dup := out[d.Expr]; dup {
+ t.Fatalf("oracleDivergences: duplicate entry for expr %q", d.Expr)
+ }
+ out[d.Expr] = d
+ }
+ return out
+}
+
+// TestOracleDivergencesMatchCorpus fails on an oracleDivergences entry
+// whose Expr does not match any current corpus entry — an orphaned
+// divergence record (the corpus changed under it, or it was mistyped) is
+// the same kind of silent rot TestExclusionsAreRealFunctions
+// (calc/corpus_test.go) guards against for the exclusion list.
+func TestOracleDivergencesMatchCorpus(t *testing.T) {
+ entries := loadOracleCorpus(t)
+ exprs := make(map[string]bool, len(entries))
+ for _, e := range entries {
+ exprs[e.Expr] = true
+ }
+ for _, d := range oracleDivergences {
+ if !exprs[d.Expr] {
+ t.Errorf("oracleDivergences: %q does not match any corpus entry — stale record", d.Expr)
+ }
+ }
+}