/* Golden tests pinning lib/lexicon.js — the one resolver shared by word lookup and the gate. It ships unchanged. */ import { describe, it, expect } from "vitest"; import { readFileSync } from "node:fs"; import { buildLexicon, Lexicon, PARTICLES } from "@lib/lexicon.js"; import { haeche, past } from "@lib/conjugation.js"; import { flatten } from "@lib/gate.js"; import type { Curriculum } from "@lib/gate.js"; const conj = { haeche, past }; describe("ORDER MATTERS — roadmap words are entered first", () => { /* 마셔 belongs to unit 2.3. Expanded from the deck first, it becomes a form of 마시다 and inherits that stem's permission, so a unit-2.3 word turns legal in Phase 1. In the fixture corpus that is four messages of violations that stop being reported. */ it("keeps a scheduled form its own head", () => { const lex = buildLexicon( { roadmapWords: ["마셔"], deck: [{ ko: "마시다", en: "to drink", pos: "verb" }] }, conj, ); expect(lex.heads("마셔")).toEqual(["마셔"]); expect(lex.get("마셔")).toMatchObject({ src: "roadmap", base: "" }); }); it("links it to the stem when the deck got there first", () => { const lex = buildLexicon({ deck: [{ ko: "마시다", en: "to drink", pos: "verb" }] }, conj); expect(lex.heads("마셔")).toEqual(["마셔", "마시다"]); }); }); describe("heads — every route, best first", () => { const lex = buildLexicon( { roadmapWords: ["닭"], deck: [ { ko: "마시다", en: "to drink", pos: "verb" }, { ko: "친구", en: "friend", pos: "noun" }, ], glossExtra: [["이야", "it is"]], sfx: [["쿵", "thud"]], }, conj, ); it("strips a particle", () => { expect(lex.heads("닭이")).toEqual(["닭"]); expect(lex.heads("친구가")).toEqual(["친구"]); }); it("follows a generated form back to its stem", () => { expect(lex.heads("마셨어")).toEqual(["마셨어", "마시다"]); }); it("deconjugates a form nobody generated, but only onto a known verb", () => { expect(lex.heads("마시면")).toEqual(["마시다"]); }); it("returns nothing for a word it cannot place", () => { expect(lex.heads("없는말")).toEqual([]); }); }); describe("lookup — what the student sees on a tap", () => { const lex = buildLexicon( { deck: [{ ko: "마시다", en: "to drink", pos: "verb" }, { ko: "친구", en: "friend", pos: "noun" }] }, conj, ); it("notes the particle", () => { expect(lex.lookup("친구가")).toMatchObject({ ko: "친구가", gloss: "friend", note: "with 조사 가" }); }); it("notes the dictionary form", () => { expect(lex.lookup("마시면")).toMatchObject({ gloss: "to drink", note: "a form of 마시다" }); }); it("is null for an unknown word", () => { expect(lex.lookup("없는말")).toBeNull(); }); }); describe("Lexicon", () => { it("never replaces an existing entry — the first writer wins", () => { const lex = new Lexicon(); lex.add("밥", "rice"); lex.add("밥", "meal"); expect(lex.get("밥")!.gloss).toBe("rice"); }); it("strips the longer particle forms", () => { expect(PARTICLES).toEqual(expect.arrayContaining(["에서", "한테", "까지", "처럼"])); }); }); describe("the shipped data", () => { const read = (f: string) => JSON.parse(readFileSync(new URL(`../../data/${f}`, import.meta.url), "utf8")); const curriculum = read("curriculum.json") as Curriculum; const deck = read("deck.json") as { topics: Record }; const glossExtra = read("gloss-extra.json") as { entries: { ko: string; en: string; note?: string }[] }; const sentences = read("sentences.json") as { sentences: { parts: [string, string][] }[] }; const sfx = read("sfx.json") as { items: { ko: string; en: string }[] }; const units = flatten(curriculum); const lex = buildLexicon( { roadmapWords: units.flatMap((u) => u.words ?? []), deck: Object.values(deck.topics) .flat() .map(([ko, , en, pos]) => ({ ko, en, pos })), glossExtra: glossExtra.entries.map((g) => [g.ko, g.en, g.note] as const), sentences: sentences.sentences, sfx: sfx.items.map((i) => [i.ko, i.en] as const), }, conj, ); it("resolves every roadmap word", () => { const missing = units.flatMap((u) => u.words ?? []).filter((w) => !lex.lookup(w)); expect(missing).toEqual([]); }); });