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
\nCompletion 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("")
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
\nRevenue was {{F21:hbm_rev}}.
`
const result = fill(source, LEDGER, { format: "html", replaceFailed: failedTwice(sentenceKey("market", ["F21"])) })
expect(result.out).toContain("")
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`)
})
})