Skip to content

Commit bd12ae2

Browse files
committed
fix(ci): gloss golden suite resolves via committed L&S fixture (M-023i)
test:gloss evals build_glosses.cjs live defs, which load the 30MB utils/ext_tmp/ L&S dump — gitignored, so CI has none. Every L&S-dependent golden row resolved null in CI; the step was red from its 2026-08-08 introduction (69 fails, then 67) while staying green locally. Add utils/build_ls_fixture.cjs + committed utils/ls_golden_fixture.json (~95 entries / 253 KB: fixpoint closure of the L&S entries the golden rows can reach, incl. failure-path keys). The test always resolves through the fixture — identical CI/local semantics — and a local drift guard re-runs against the full dump and errors ('run npm run build:gloss-fixture') if the fixture stops reproducing it. Verified: 2067/2067 with and without utils/ext_tmp/ (CI simulation).
1 parent c567975 commit bd12ae2

6 files changed

Lines changed: 255 additions & 36 deletions

File tree

wiktionary_pron/CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ Each of these cost real debugging time in past sessions. Check this list before
144144
- **Browser Latin ≠ Node Latin**: the browser flow macronizes input first (provinciarum → prōvinciārum), so e2e IPA expectations carry length marks that the Node suite's don't.
145145
- **RU/UK stress-transfer skips multi-form dictionary entries by design** — test it with a single-form word (голова → голова́), not вода (record "во́да, вода́" has a comma → skipped).
146146
- **`scripts/tests/init.js` was silently cwd-dependent** — its `fs.readFile` resolved `../../lua_modules/...` against `process.cwd()`, so the IPA suite only ran from `scripts/tests/`. Fixed 2026-08-06 by anchoring `LUA_ROOT` to the module path; keep it cwd-independent (coverage runs it from the repo root).
147+
- **The gloss golden suite evals `build_glosses.cjs` live defs, so it inherits every file dependency the build has** — including `utils/ext_tmp/ls_*.json` (30MB L&S, gitignored). CI has no `ext_tmp`, so `test:gloss` was red in CI from the day it was added (2026-08-08) while staying green locally. Fixed 2026-08-12: the test resolves through the **committed fixture** `utils/ls_golden_fixture.json` (~95 entries, `npm run build:gloss-fixture` regenerates from ext_tmp); a local drift guard errors if the fixture stops reproducing the full dump. **After editing `utils/gloss_golden.json` or the extractor's L&S keys, regenerate the fixture in the same commit** or CI stays red.
147148

148149
**Dictionary-gloss traps** (2026-08-07, M-005 research)
149150
- **Perseus tag gender is at index 6, not index 3.** `n-s---fn-` → gender is `tag[6]` (`f`), `tag[3]` (`s`) is number/subcategorization. Reading `tag[3]` makes gender always empty and silently collapses (lemma|POS|gender) to (lemma|POS).

wiktionary_pron/docs/LESSONS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,3 +718,11 @@ Catilinam I, Vergil Aeneid I, Ovid Metamorphoses I.
718718
details open — popup stays inside the viewport.
719719
- **Lesson.** Any non-bubbling structural event on dynamically-rebuilt DOM needs
720720
a "wire after every rebuild" helper, not a one-time delegated listener.
721+
722+
## The golden gloss suite never ran in CI — it needed the gitignored 30MB L&S dump (2026-08-12)
723+
724+
- **Symptom.** CI `node-tests` failing only on `test:gloss`: `2000 passed, 67 failed`, every `got: null`. Locally green. `e2e`, `unit`, `ipa`, `census` all fine.
725+
- **Root cause.** `test_gloss_regression.cjs` evals the head of `build_glosses.cjs` and resolves golden rows through the extractor's **live defs**. Those defs populate `lsByKey` from `utils/ext_tmp/ls_*.json` — the 30MB Lewis & Short dump, **gitignored** ("build input, not shipped"). A fresh CI checkout has no `ext_tmp`, so every L&S-dependent lemma (`terra`, `qui`, `venio`, the numbered homographs…) resolved to `null`. The suite was added 2026-08-08 and **both** CI runs since failed — it had simply never been green. Nothing caught it because `npm test` is green locally, where the 30MB sits on disk.
726+
- **Lesson.** A test that evals production build code inherits every file dependency that code has. If any of those files are gitignored, the test can only ever be green on the author's machine. CI is the truth-teller: check what a fresh checkout actually has.
727+
- **Fix.** `utils/build_ls_fixture.cjs` extracts the ~95 Lewis & Short entries the 2067 golden rows can *reach* (fixpoint over the eval'd defs, growing by missed-key base-families → 253KB) into **committed** `utils/ls_golden_fixture.json`. The test always resolves through the fixture (`makeDefs(true)`), giving identical semantics in CI and locally; when `utils/ext_tmp/` IS present, a **drift guard** re-runs against the full dump and errors with "run `npm run build:gloss-fixture`" if the fixture stops reproducing it. Regenerate after golden edits: `npm run build:gloss-fixture`, commit the fixture in the same commit.
728+
- **Lesson.** Two-source divergence is the failure mode to kill: making the committed fixture the *authoritative* source (not "full data locally, fixture in CI") is what prevents "passes locally, red in CI". The drift guard only exists to catch the fixture going stale locally, before it ships.

wiktionary_pron/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"test:gloss": "node utils/test_gloss_regression.cjs",
1616
"test:census": "node utils/gloss_census.cjs",
1717
"build:gloss": "node utils/build_glosses.cjs",
18+
"build:gloss-fixture": "node utils/build_ls_fixture.cjs",
1819
"serve": "http-server .. -p 8993 -s",
1920
"build:ext": "node utils/sync_extension.cjs"
2021
},
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#!/usr/bin/env node
2+
// Build utils/ls_golden_fixture.json — the minimal Lewis & Short entry set the
3+
// golden suite touches, so `npm run test:gloss` runs in CI, where the 30MB
4+
// utils/ext_tmp/ L&S dump is deliberately NOT committed (gitignore: "build input
5+
// ... not shipped"). The test resolves every golden row through the LIVE defs of
6+
// build_glosses.cjs, so it needs real L&S entries; the fixture supplies exactly
7+
// the ones the 2067+ rows can reach — including the failure-path entries a
8+
// resolve() only touches when an earlier lookup misses (the "vestitus2" class).
9+
//
10+
// node utils/build_ls_fixture.cjs
11+
//
12+
// Regenerate after adding/editing rows in utils/gloss_golden.json that touch new
13+
// L&S lemmas, or after the extractor starts reading different keys. Commit the
14+
// regenerated utils/ls_golden_fixture.json in the same commit as the golden edit.
15+
// Needs utils/ext_tmp/ on disk — run locally, never in CI.
16+
//
17+
// Method: fixpoint over the eval'd defs. Start with an empty selected set and
18+
// repeatedly (1) eval the defs with only the selected entries loaded, (2) run
19+
// every golden row through resolve(), recording every lsByKey key it looked for
20+
// but missed, (3) add the whole base-family of each missed key. Grows until no
21+
// new entries appear — by construction the committed fixture reproduces the full
22+
// 30MB dump for every golden row (enforced as a drift guard by
23+
// test_gloss_regression.cjs when utils/ext_tmp/ is present locally).
24+
25+
const fs = require("fs");
26+
const path = require("path");
27+
const zlib = require("zlib");
28+
29+
const GOLDEN = "utils/gloss_golden.json";
30+
const OUT = "utils/ls_golden_fixture.json";
31+
const EXTRACT_ROOT = path.join("utils", "ext_tmp");
32+
33+
// ---- full L&S dump (local only) ----
34+
const lsAll = new Map();
35+
for (const c of "ABCDEFGHIJKLMNOPQRSTUVXYZ") {
36+
const p = path.join(EXTRACT_ROOT, `ls_${c}.json`);
37+
if (!fs.existsSync(p)) continue;
38+
for (const e of JSON.parse(fs.readFileSync(p, "utf8"))) {
39+
if (e && e.key) lsAll.set(String(e.key).toLowerCase(), e);
40+
}
41+
}
42+
if (!lsAll.size) {
43+
console.error(`${EXTRACT_ROOT}/ has no L&S data — run this locally, not in CI.`);
44+
process.exit(1);
45+
}
46+
// base ("mora", digits stripped) → all keys in that family, for O(1) closure.
47+
const famByBase = new Map();
48+
for (const ek of lsAll.keys()) {
49+
const b = ek.replace(/\d+$/, "");
50+
if (!famByBase.has(b)) famByBase.set(b, []);
51+
famByBase.get(b).push(ek);
52+
}
53+
54+
// ---- eval the extractor defs with the L&S load redirected to a global array ----
55+
const src = fs.readFileSync("utils/build_glosses.cjs", "utf8");
56+
const head = src.slice(0, src.indexOf("// ---- build ----"));
57+
const LS_BLOCK_RE = /\/\/ ---- L&S index ----\nconst lsByKey = new Map\(\), lsByBase = new Map\(\);\n[\s\S]*?\n\}\n/;
58+
const WL_BLOCK = `const rows = new Map();
59+
const formSets = new Map();
60+
const lemmaPosCount = new Map();
61+
for (const line of fs.readFileSync("macronizer/macrons.txt","utf8").split("\\n")) {
62+
const p = line.split("\\t"); if (p.length<4) continue;
63+
const lem = p[2].toLowerCase();
64+
rows.set(p[2]+"|"+p[1], {lemma:p[2], tag:p[1]});
65+
if (!formSets.has(lem)) formSets.set(lem, new Set());
66+
formSets.get(lem).add(p[0]+"|"+p[3]);
67+
const pos = POS_MAP[p[1][0]] || p[1][0];
68+
if (!lemmaPosCount.has(lem)) lemmaPosCount.set(lem, {});
69+
lemmaPosCount.get(lem)[pos] = (lemmaPosCount.get(lem)[pos]||0)+1;
70+
}
71+
`;
72+
function makeDefs() {
73+
if (!LS_BLOCK_RE.test(head)) {
74+
throw new Error("build_glosses.cjs L&S load changed — update LS_BLOCK_RE in " + __filename);
75+
}
76+
let d = head
77+
.replace(WL_BLOCK, "var rows = new Map(), formSets = new Map(), formSetsTag = new Map(), lemmaPosCount = new Map();\n")
78+
.replace(LS_BLOCK_RE, `// ---- L&S index (fixture source) ----
79+
var lsByKey = new Map(), lsByBase = new Map();
80+
for (const e of globalThis.__LS_SOURCE__) {
81+
if (!e || !e.key) continue;
82+
const key = String(e.key).toLowerCase();
83+
lsByKey.set(key, e);
84+
const base = key.replace(/\\d+$/, "");
85+
if (!lsByBase.has(base)) lsByBase.set(base, []);
86+
lsByBase.get(base).push(e);
87+
}
88+
`);
89+
d = d.replace(/^const \{ createEngine \} = .*$/gm, "").replace(/^const (fs|path|zlib|engine) = .*$/gm, "").replace(/^const engine = .*$/gm, "");
90+
d = d.replace(/\bconst\s+/g, "var ").replace(/\blet\s+/g, "var ");
91+
return d;
92+
}
93+
const defs = makeDefs();
94+
const golden = JSON.parse(fs.readFileSync(GOLDEN, "utf8"));
95+
96+
function loadCache() {
97+
const cache = JSON.parse(zlib.gunzipSync(fs.readFileSync("utils/gloss_lemma_cache.json.gz")));
98+
for (const [lem, e] of Object.entries(cache)) {
99+
if (e.forms) formSets.set(lem, new Set(e.forms));
100+
if (e.formsTag) formSetsTag.set(lem, new Set(e.formsTag));
101+
}
102+
for (const [lem, e] of Object.entries(cache)) lemmaPosCount.set(lem, e.pos);
103+
const origDominantPos = dominantPos;
104+
dominantPos = function (l) {
105+
const cnt = lemmaPosCount.get(l);
106+
if (!cnt) return origDominantPos(l);
107+
let best = null, bestN = 0;
108+
for (const [p, n] of Object.entries(cnt)) if (n > bestN) { best = p; bestN = n; }
109+
return best || "N";
110+
};
111+
}
112+
113+
// ---- fixpoint ----
114+
let selected = new Set();
115+
for (let iter = 0; ; iter++) {
116+
if (iter > 60) { console.error("fixture fixpoint did not converge"); process.exit(1); }
117+
globalThis.__LS_SOURCE__ = [...selected].map((k) => lsAll.get(k));
118+
eval(defs);
119+
loadCache();
120+
// record every lsByKey lookup that came up empty
121+
const miss = new Set();
122+
const origGet = lsByKey.get.bind(lsByKey);
123+
lsByKey.get = function (k) {
124+
if (k != null && !lsByKey.has(String(k))) miss.add(String(k));
125+
return origGet(k);
126+
};
127+
for (const row of golden) {
128+
try { resolve(row.lemma, dominantPos(row.lemma.toLowerCase())); } catch (e) {}
129+
}
130+
let added = 0;
131+
for (const k of miss) {
132+
for (const ek of famByBase.get(k.replace(/\d+$/, "")) || []) {
133+
if (!selected.has(ek)) { selected.add(ek); added++; }
134+
}
135+
}
136+
if (added === 0) break;
137+
console.log(`iter ${iter}: ${selected.size} entries (+${added})`);
138+
}
139+
140+
const out = [...selected].sort().map((k) => lsAll.get(k));
141+
fs.writeFileSync(OUT, JSON.stringify(out));
142+
const kb = Math.round(fs.statSync(OUT).size / 1024);
143+
console.log(`wrote ${OUT}: ${out.length} entries, ${kb} KB (of ${lsAll.size} total)`);

wiktionary_pron/utils/ls_golden_fixture.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

wiktionary_pron/utils/test_gloss_regression.cjs

Lines changed: 101 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
// expectations in utils/gloss_golden.json are resolved through the LIVE defs of
44
// utils/build_glosses.cjs (same eval trick as _probe_one.cjs) — no 6-minute build.
55
//
6+
// The L&S source is the COMMITTED fixture utils/ls_golden_fixture.json (the 30MB
7+
// utils/ext_tmp/ dump is gitignored, so CI has no L&S data), giving the suite
8+
// IDENTICAL semantics in CI and locally. Regenerate the fixture after golden
9+
// edits with `npm run build:gloss-fixture` and commit it alongside. When
10+
// utils/ext_tmp/ IS present locally, a drift guard re-runs against the full dump
11+
// and fails if the fixture no longer reproduces it (see utils/build_ls_fixture.cjs).
12+
//
613
// The wordlist parse (macrons.txt, 812k lines) is cached in
714
// utils/gloss_lemma_cache.json.gz so a run is ~1-2s, not ~4s+3.3s.
815
//
@@ -41,9 +48,37 @@ for (const line of fs.readFileSync("macronizer/macrons.txt","utf8").split("\\n")
4148
}
4249
`;
4350
let defs = head.replace(WL_BLOCK, "var rows = new Map(), formSets = new Map(), formSetsTag = new Map(), lemmaPosCount = new Map();\n");
44-
defs = defs.replace(/^const \{ createEngine \} = .*$/gm, "").replace(/^const (fs|path|zlib|engine) = .*$/gm, "").replace(/^const engine = .*$/gm, "");
45-
defs = defs.replace(/\bconst\s+/g, "var ").replace(/\blet\s+/g, "var ");
46-
eval(defs);
51+
const FIXTURE = "utils/ls_golden_fixture.json";
52+
const hasFullLs = fs.existsSync(path.join("utils", "ext_tmp"));
53+
// ---- L&S source = the committed fixture (see build_ls_fixture.cjs) ----
54+
const LS_BLOCK_RE = /\/\/ ---- L&S index ----\nconst lsByKey = new Map\(\), lsByBase = new Map\(\);\n[\s\S]*?\n\}\n/;
55+
const LS_FIXTURE_BLOCK = `// ---- L&S index (CI fixture: utils/ls_golden_fixture.json) ----
56+
var lsByKey = new Map(), lsByBase = new Map();
57+
for (const e of JSON.parse(fs.readFileSync("utils/ls_golden_fixture.json", "utf8"))) {
58+
if (!e || !e.key) continue;
59+
const key = String(e.key).toLowerCase();
60+
lsByKey.set(key, e);
61+
const base = key.replace(/\\d+$/, "");
62+
if (!lsByBase.has(base)) lsByBase.set(base, []);
63+
lsByBase.get(base).push(e);
64+
}
65+
`;
66+
function makeDefs(useFixture) {
67+
let d = head.replace(WL_BLOCK, "var rows = new Map(), formSets = new Map(), formSetsTag = new Map(), lemmaPosCount = new Map();\n");
68+
if (useFixture) {
69+
if (!LS_BLOCK_RE.test(d)) throw new Error("build_glosses.cjs L&S load changed — update LS_BLOCK_RE in " + __filename);
70+
d = d.replace(LS_BLOCK_RE, LS_FIXTURE_BLOCK);
71+
}
72+
d = d.replace(/^const \{ createEngine \} = .*$/gm, "").replace(/^const (fs|path|zlib|engine) = .*$/gm, "").replace(/^const engine = .*$/gm, "");
73+
d = d.replace(/\bconst\s+/g, "var ").replace(/\blet\s+/g, "var ");
74+
return d;
75+
}
76+
if (!fs.existsSync(FIXTURE)) {
77+
console.error(`Missing ${FIXTURE} — run \`npm run build:gloss-fixture\` locally (needs utils/ext_tmp/) and commit it.`);
78+
process.exit(1);
79+
}
80+
// Authoritative pass always uses the fixture → identical semantics in CI and locally.
81+
eval(makeDefs(true));
4782

4883
// ---- wordlist cache ----
4984
if (process.argv.includes("--rebuild-cache") || !fs.existsSync(CACHE)) {
@@ -72,60 +107,90 @@ if (process.argv.includes("--rebuild-cache") || !fs.existsSync(CACHE)) {
72107
console.log(`cached ${Object.keys(out).length} lemmas`);
73108
}
74109

75-
const cache = JSON.parse(zlib.gunzipSync(fs.readFileSync(CACHE)));
76-
for (const [lem, e] of Object.entries(cache)) {
77-
if (e.forms) formSets.set(lem, new Set(e.forms));
78-
if (e.formsTag) formSetsTag.set(lem, new Set(e.formsTag));
110+
function loadWordlistCache() {
111+
const cache = JSON.parse(zlib.gunzipSync(fs.readFileSync(CACHE)));
112+
for (const [lem, e] of Object.entries(cache)) {
113+
if (e.forms) formSets.set(lem, new Set(e.forms));
114+
if (e.formsTag) formSetsTag.set(lem, new Set(e.formsTag));
115+
}
116+
// dominantPos reads lemmaPosCount — rebuild it faithfully, then override dominantPos
117+
for (const [lem, e] of Object.entries(cache)) lemmaPosCount.set(lem, e.pos);
118+
const origDominantPos = dominantPos;
119+
dominantPos = function (l) {
120+
const cnt = lemmaPosCount.get(l);
121+
if (!cnt) return origDominantPos(l);
122+
let best = null, bestN = 0;
123+
for (const [p, n] of Object.entries(cnt)) if (n > bestN) { best = p; bestN = n; }
124+
return best || "N";
125+
};
126+
return cache;
79127
}
80-
// dominantPos reads lemmaPosCount — rebuild it faithfully, then override dominantPos
81-
for (const [lem, e] of Object.entries(cache)) lemmaPosCount.set(lem, e.pos);
82-
const origDominantPos = dominantPos;
83-
dominantPos = function (l) {
84-
const cnt = lemmaPosCount.get(l);
85-
if (!cnt) return origDominantPos(l);
86-
let best = null, bestN = 0;
87-
for (const [p, n] of Object.entries(cnt)) if (n > bestN) { best = p; bestN = n; }
88-
return best || "N";
89-
};
128+
const cache = loadWordlistCache();
90129

91130
// ---- golden suite ----
92131
const golden = JSON.parse(fs.readFileSync(GOLDEN, "utf8"));
93-
let pass = 0, fail = 0;
94-
const fails = [];
132+
const norm = (s) => (s || "").toLowerCase().replace(/[.,;]$/, "").replace(/\s+/g, " ").trim();
95133
let engine = null;
96134
// CORE_GLOSS keys must each have a golden row — a curated override with no
97135
// regression gate would silently rot (monotone rule: every override gets a golden
98136
// row in the same commit). No-op when utils/core_gloss.json doesn't exist yet.
99137
const coreGlossPath = "utils/core_gloss.json";
138+
const coreFails = [];
100139
if (fs.existsSync(coreGlossPath)) {
101140
const core = JSON.parse(fs.readFileSync(coreGlossPath, "utf8"));
102141
const covered = new Set(golden.map(r => r.lemma.toLowerCase()));
103142
for (const k of Object.keys(core)) {
104-
if (!covered.has(k)) { fail++; fails.push({ lemma: k, expect: { contains: core[k] }, got: "(no golden row)", note: "core_gloss override missing from golden suite" }); }
143+
if (!covered.has(k)) coreFails.push({ lemma: k, expect: { contains: core[k] }, got: "(no golden row)", note: "core_gloss override missing from golden suite" });
105144
}
106145
for (const row of golden) {
107146
if (core[row.lemma] && row.expect !== null && !evalExpect(row.expect, core[row.lemma], (s)=>(s||"").toLowerCase())) {
108-
fail++; fails.push({ lemma: row.lemma, expect: row.expect, got: core[row.lemma], note: "core_gloss override contradicts golden expectation" });
147+
coreFails.push({ lemma: row.lemma, expect: row.expect, got: core[row.lemma], note: "core_gloss override contradicts golden expectation" });
109148
}
110149
}
111150
}
112-
for (const row of golden) {
113-
const { lemma, expect, note, wordsOnly } = row;
114-
let got = null;
115-
try {
116-
const pos = dominantPos(lemma.toLowerCase());
117-
got = resolve(lemma, pos);
118-
if (!got && wordsOnly) {
119-
// WORDS-only fallback path (gender-tolerant): lazily init the engine.
120-
if (!engine) engine = require("whitakers-words/node").createEngine();
121-
got = wGloss(lemma, pos, "");
151+
function runGoldenSuite() {
152+
const fails = [];
153+
let pass = 0;
154+
for (const row of golden) {
155+
const { lemma, expect, note, wordsOnly } = row;
156+
let got = null;
157+
try {
158+
const pos = dominantPos(lemma.toLowerCase());
159+
got = resolve(lemma, pos);
160+
if (!got && wordsOnly) {
161+
// WORDS-only fallback path (gender-tolerant): lazily init the engine.
162+
if (!engine) engine = require("whitakers-words/node").createEngine();
163+
got = wGloss(lemma, pos, "");
164+
}
165+
} catch (e) { got = "ERROR: " + e.message; }
166+
if (evalExpect(expect, got, norm)) pass++;
167+
else fails.push({ lemma, expect, got, note });
168+
}
169+
return { pass, fails };
170+
}
171+
// The authoritative run resolves through the fixture (identical in CI and locally).
172+
const main = runGoldenSuite();
173+
174+
// ---- drift guard (local only): the full 30MB dump must reproduce the fixture ----
175+
// Re-eval with the full dump whenever it exists (so --tier=full below still
176+
// resolves against real L&S); compare against the fixture only in normal mode.
177+
if (hasFullLs) {
178+
eval(makeDefs(false));
179+
loadWordlistCache();
180+
if (!process.argv.includes("--tier=full")) {
181+
const full = runGoldenSuite();
182+
const stale = main.fails.filter(f => !full.fails.some(g => g.lemma === f.lemma));
183+
if (stale.length) {
184+
console.error(`\nL&S fixture STALE: ${stale.length} rows pass with utils/ext_tmp/ but not with ${FIXTURE}.`);
185+
console.error(`Run \`npm run build:gloss-fixture\` and commit the regenerated fixture.`);
186+
for (const s of stale) console.error(` ${s.lemma}: expect ${JSON.stringify(s.expect)} got ${JSON.stringify(s.got)}`);
122187
}
123-
} catch (e) { got = "ERROR: " + e.message; }
124-
const norm = (s) => (s || "").toLowerCase().replace(/[.,;]$/, "").replace(/\s+/g, " ").trim();
125-
const ok = evalExpect(expect, got, norm);
126-
if (ok) { pass++; }
127-
else { fail++; fails.push({ lemma, expect, got, note }); }
188+
}
128189
}
190+
191+
const fails = [...coreFails, ...main.fails];
192+
const fail = fails.length;
193+
const pass = golden.length - main.fails.length;
129194
console.log(`\n${pass} passed, ${fail} failed (${golden.length} total)`);
130195
for (const f of fails) {
131196
console.log(`\nFAIL ${f.lemma} (${f.note || ""})`);

0 commit comments

Comments
 (0)