import { describe, expect, test } from "bun:test" import { readFileSync } from "node:fs" import { join } from "node:path" import { fill } from "./fact-fill.mjs" import { retractRow, supersedeRow, validateLedger } from "./fact-ledger.mjs" import { failureCount, sentenceKey } from "./fidelity-state.mjs" const LEDGER = JSON.parse(readFileSync(join(import.meta.dir, "tests/fixtures/fidelity/ledger-basic.json"), "utf8")) const F12_DATA = '320 m' const QUOTE = "we estimate completion no earlier than 2027" // Korean particles and words, as escapes: eul, girok, i, ollatda, ga, isang. const EUL = "\uC744" const GIROK = "\uAE30\uB85D" const I = "\uC774" const OLLATDA = "\uC62C\uB790\uB2E4" const GA = "\uAC00" const ISANG = "\uC774\uC0C1" type State = { schemaVersion: number; failures: Record; replaced: string[] } function failedTwice(key: string): State { return { schemaVersion: 1, failures: { [key]: 2 }, replaced: [] } } const codes = (list: { code: string }[]) => list.map((item) => item.code) describe("fill: fixture", () => { test("the ledger fixture is a valid ledger", () => { expect(validateLedger(LEDGER)).toEqual([]) }) }) describe("fill: value placeholders (html)", () => { test("{{F12:span_len}} renders the element with the raw text", () => { const result = fill("

The span is {{F12:span_len}}.

", LEDGER, { format: "html" }) expect(result.errors).toEqual([]) expect(result.out).toBe(`

The span is ${F12_DATA}.

`) expect(result.fillMap).toHaveLength(1) expect(result.fillMap[0]).toMatchObject({ token: "{{F12:span_len}}", fact_id: "F12", rendered: F12_DATA }) }) test(":q copies the source qualifier with the quantity", () => { const result = fill("

{{F12:span_len:q}}

", LEDGER, { format: "html" }) expect(result.out).toContain('data-lineage="MEASURED">up to 320 m
') }) test(":value and :unit render one part each", () => { const result = fill("

{{F12:span_len:value}} metres, unit {{F12:span_len:unit}}

", LEDGER, { format: "md" }) expect(result.out).toBe("

320 metres, unit m

") }) test("a converted row renders DERIVED lineage", () => { const result = fill("

{{F40:span_mm}}

", LEDGER, { format: "html" }) expect(result.out).toContain('data-fact="F40" data-unit="mm" data-lineage="DERIVED">320000 mm') }) test("an all-English template fills with no warnings", () => { const result = fill("The span is {{F12:span_len}} long.", LEDGER, { format: "html" }) expect(result.out).not.toBeNull() expect(result.warnings).toEqual([]) }) }) describe("fill: fatal errors write nothing", () => { test("a label mismatch is pointer_label_mismatch with out null", () => { const result = fill("

Line one.

\n

{{F12:wrong}}

", LEDGER, { format: "html" }) expect(result.out).toBeNull() expect(result.errors[0]).toMatchObject({ code: "pointer_label_mismatch", token: "{{F12:wrong}}", line: 2 }) }) test("an unknown id is unresolved_placeholder; no row is guessed", () => { const result = fill("

{{F99:x}}

", LEDGER, { format: "html" }) expect(result.out).toBeNull() expect(codes(result.errors)).toEqual(["unresolved_placeholder"]) }) test("a malformed pointer is unresolved_placeholder", () => { expect(codes(fill("

{{F12}}

", LEDGER, { format: "html" }).errors)).toEqual(["unresolved_placeholder"]) }) test("a unit typed after a value placeholder is unit_duplicated, never absorbed", () => { const result = fill("

{{F13:rate}}%

", LEDGER, { format: "html" }) expect(result.out).toBeNull() expect(codes(result.errors)).toEqual(["unit_duplicated"]) expect(fill("

{{F13:rate:value}}%

", LEDGER, { format: "html" }).errors).toEqual([]) }) test("a retracted row is unresolved_placeholder; fill never renders it", () => { const ledger = structuredClone(LEDGER) retractRow(ledger, "F12", "source withdrawn") expect(validateLedger(ledger)).toEqual([]) const result = fill("

Line one.

\n

{{F12:span_len}}

", ledger, { format: "html" }) expect(result.out).toBeNull() expect(result.fillMap).toEqual([]) expect(result.errors).toEqual([{ code: "unresolved_placeholder", token: "{{F12:span_len}}", line: 2, message: "F12 is retracted; cite its replacement" }]) }) test("a superseded row is unresolved_placeholder; fill never renders it", () => { const ledger = structuredClone(LEDGER) supersedeRow(ledger, "F12", "F40", "restated in mm") expect(validateLedger(ledger)).toEqual([]) const result = fill("{{F12:span_len:value}}", ledger, { format: "md" }) expect(result.out).toBeNull() expect(result.fillMap).toEqual([]) expect(result.errors).toEqual([{ code: "unresolved_placeholder", token: "{{F12:span_len:value}}", line: 1, message: "F12 is superseded; cite its replacement" }]) }) test("an anchor occurring twice is quote_anchor_unresolved", () => { const result = fill('

{{Q7:completion:from="ti":to="2027"}}

', LEDGER, { format: "html" }) expect(result.out).toBeNull() expect(codes(result.errors)).toEqual(["quote_anchor_unresolved"]) }) }) describe("fill: quote placeholders", () => { test("from/to renders the sub-span in quotes plus the source marker", () => { const result = fill('

{{Q7:completion:from="no earlier":to="2027"}}

', LEDGER, { format: "html" }) expect(result.out).toBe('

\u2026no earlier than 2027 [S4]

') }) test("full and lead selectors", () => { expect(fill("{{Q7:completion}}", LEDGER, { format: "html" }).out).toBe(`${QUOTE} [S4]`) expect(fill("{{Q7:completion:lead=2}}", LEDGER, { format: "html" }).out).toBe('we estimate\u2026 [S4]') }) test("md format puts the selector into fillMap", () => { const result = fill("{{Q7:completion:lead=2}}", LEDGER, { format: "md" }) expect(result.out).toBe('"we estimate\u2026" [S4]') expect(result.fillMap[0]).toMatchObject({ fact_id: "Q7", selector: "lead=2", quote_span: [0, 11] }) }) test("a translation span after a quote placeholder is preserved and mapped", () => { const source = '{{Q7:completion}} translation rest' const result = fill(source, LEDGER, { format: "html" }) expect(result.out).toContain('translation rest') const entry = result.fillMap.find((item: { translates?: string }) => item.translates === "Q7") expect(entry).toBeDefined() expect(result.out.slice(entry.start, entry.end)).toBe(entry.rendered) const md = fill("{{Q7:completion}} translation", LEDGER, { format: "md" }) expect(md.fillMap.some((item: { translates?: string }) => item.translates === "Q7")).toBe(true) }) }) describe("fill: Korean particles", () => { test("a closed final keeps eul", () => { const result = fill(`

{{F21:hbm_rev}}${EUL} ${GIROK}

`, LEDGER, { format: "html" }) expect(result.out).toContain(`${EUL} ${GIROK}`) expect(result.warnings).toEqual([]) }) test("percent is open: i becomes ga", () => { const result = fill(`

{{F13:rate}}${I} ${OLLATDA}

`, LEDGER, { format: "html" }) expect(result.out).toContain(`15.2%${GA} ${OLLATDA}`) }) test("a candidate particle with no boundary is untouched and warned", () => { const result = fill(`

{{F13:rate}}${ISANG}

`, LEDGER, { format: "html" }) expect(result.out).toContain(`${ISANG}

`) expect(codes(result.warnings)).toEqual(["particle_unverified"]) }) }) describe("fill: md maps", () => { test("fillMap offsets slice to the rendered text", () => { const result = fill(`# Findings\n\nSpan {{F12:span_len}}, rate {{F13:rate}}${I} ${OLLATDA}, {{Q7:completion:lead=3}}.\n`, LEDGER, { format: "md" }) expect(result.errors).toEqual([]) expect(result.fillMap).toHaveLength(3) for (const entry of result.fillMap) expect(result.out.slice(entry.start, entry.end)).toBe(entry.rendered) expect(result.fillMap[0].rendered).toBe("320 m") expect(result.out).toContain(`15.2%${GA}`) }) }) describe("fill: replace-failed", () => { const SOURCE = '

Findings

\n

Completion slips: {{Q7:completion}}. Other text stays.

' const KEY = sentenceKey("findings", ["Q7"]) test("a key that failed twice becomes the attributed blockquote; no entity warns", () => { const state = failedTwice(KEY) const result = fill(SOURCE, LEDGER, { format: "html", replaceFailed: state }) expect(result.out).not.toBeNull() expect(result.out).toContain(`
`) expect(result.out).toContain(`"${QUOTE}"`) expect(result.out).toContain("
[S4]
") expect(result.out).toContain("Other text stays.

") expect(result.out).not.toContain("Completion slips") expect(codes(result.warnings)).toEqual(["fallback_without_entity"]) expect(state.replaced).toEqual([KEY]) }) test("a row with entity and valid_at carries them in the footer", () => { const source = `

Market

\n

Revenue was {{F21:hbm_rev}}.

` const result = fill(source, LEDGER, { format: "html", replaceFailed: failedTwice(sentenceKey("market", ["F21"])) }) expect(result.out).toContain("
[S5], on HBM3E (2026 Q3)
") expect(result.warnings).toEqual([]) }) test("a key below two failures is filled normally", () => { const state: State = { schemaVersion: 1, failures: { [KEY]: 1 }, replaced: [] } const result = fill(SOURCE, LEDGER, { format: "html", replaceFailed: state }) expect(result.out).toContain("Completion slips:") expect(state.replaced).toEqual([]) }) test("fallback omit removes the sentence", () => { const result = fill(SOURCE, LEDGER, { format: "html", replaceFailed: failedTwice(KEY), fallback: "omit" }) expect(result.out).toBe('

Findings

\n

Other text stays.

') }) test("view leaves the state unchanged", () => { const state = failedTwice(KEY) const result = fill(SOURCE, LEDGER, { format: "html", replaceFailed: state, view: true }) expect(result.out).toContain(" block with the footer line", () => { const result = fill("## Findings\n\nCompletion slips: {{Q7:completion}}.\n", LEDGER, { format: "md", replaceFailed: failedTwice(KEY) }) expect(result.out).toBe(`## Findings\n\n> "${QUOTE}"\n> [S4]\n`) }) })