import { describe, expect, onTestFinished, test } from "bun:test" import { createHash } from "node:crypto" import { copyFileSync, existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os" import { delimiter, join } from "node:path" import { PROMOTION } from "./fact-commands-meaning.mjs" import { writeFidelityRecord } from "./fidelity-records.mjs" import { PAIR_KINDS } from "./fact-pairs.mjs" import { VERDICT_SCHEMA } from "./meaning-check.mjs" import { POLISH_MARKUP_REASON } from "./polish-apply.mjs" import { COMMANDS, run } from "./report-tools.mjs" import { SCRIPT, FIXTURES, sessionDir, nodeWithoutBun, defectsFile, FIDELITY, sha256, CliRow, CliJson, nodeJson, fidelitySession, verify, codesOf, Pair, pairedSession, verdicts, calibrationRows, runLines, PolishJson, polishSession, polishApply, setField, fillTemplate, textSource, ledgerRow } from "./tests/report-tools-support" describe("fact, source, and claims under node", () => { test("#given a fresh session #when snapshot, add, quote, derive, fill, and verify run #then the filled report verifies clean", () => { // given const { dir, s, snap, add, quote, derive, mm } = fidelitySession() // when const filled = nodeJson(["fact", "fill", join(dir, "report.src.html"), "--out", join(dir, "report.html"), ...s]) const checked = verify(dir, "report.html") // then expect(snap.code).toBe(0) expect(add.code).toBe(0) expect(add.json.data.row.value).toBe("320") expect(add.json.data.context.match).toBe("320 m") expect(quote.code).toBe(0) expect(quote.json.data.row.fact_id).toBe("Q1") expect(derive.code).toBe(0) expect(mm).toBe("F2") expect(filled.code).toBe(0) expect(readFileSync(join(dir, "report.html"), "utf8")).toContain("320,000 mm (320 m)") expect(checked.code).toBe(0) expect(checked.json.defects).toEqual([]) const record = JSON.parse(readFileSync(join(dir, "fidelity-defects.json"), "utf8")) expect(record.artifact_sha256).toBe(sha256(join(dir, "report.html"))) }) test("#given a filled report #when a value is hand-edited or a number typed #then verify exits 1 with the code", () => { const { dir, s } = fidelitySession() nodeJson(["fact", "fill", join(dir, "report.src.html"), "--out", join(dir, "report.html"), ...s]) const html = readFileSync(join(dir, "report.html"), "utf8") writeFileSync(join(dir, "unit.html"), html.replace("320 m", "320 mm")) writeFileSync(join(dir, "typed.html"), html.replace("

How", "

Revenue grew 12% [S4].

\n

How")) const unit = verify(dir, "unit.html") const typed = verify(dir, "typed.html") expect(unit.code).toBe(1) expect(codesOf(unit.json)).toContain("unit_mismatch") expect(typed.code).toBe(1) expect(codesOf(typed.json)).toContain("untraceable_number") }) test("#given an anchor that is not in the source #when fact add runs #then it exits 1 with fact_not_located and writes no row", () => { const { dir, s } = fidelitySession() const proc = nodeJson(["fact", "add", "--source", "S4", "--find", "nope", "--label", "x", ...s]) expect(proc.code).toBe(1) expect(proc.stdout).toContain("fact_not_located") expect(JSON.parse(readFileSync(join(dir, "fact-ledger.json"), "utf8")).rows.map((r: { fact_id: string }) => r.fact_id)).toEqual(["F1", "Q1", "F2"]) }) test("#given a snapshotted source #when snapshotted again with the same or different content #then same is a no-op and different exits 1", () => { const { dir, s } = fidelitySession() writeFileSync(join(dir, "other.txt"), "Span length: 999 m\n") expect(nodeJson(["source", "snapshot", "--id", "S4", "--html", join(FIDELITY, "sources", "S4.html"), ...s]).code).toBe(0) expect(nodeJson(["source", "snapshot", "--id", "S4", "--text", join(dir, "other.txt"), ...s]).code).toBe(1) }) test("#given an assumed value with and without a rationale #when added #then the row is kind assumed, and no rationale exits 2", () => { const { s } = fidelitySession() const added = nodeJson(["fact", "add", "--assumed", "12", "--unit", "%", "--rationale", "planning share chosen by the requester", "--label", "share_assumed", "--json", ...s]) expect(added.code).toBe(0) expect(added.json.data.row.kind).toBe("assumed") expect(nodeJson(["fact", "add", "--assumed", "12", "--label", "x", ...s]).code).toBe(2) }) test("#given the fixture claim graph #when claims index runs #then claims.json lists its claims", () => { const dir = sessionDir() const proc = nodeJson(["claims", "index", join(FIDELITY, "claim-graph.md"), "--out", join(dir, "claims.json")]) expect(proc.code).toBe(0) expect(JSON.parse(readFileSync(join(dir, "claims.json"), "utf8")).claims.length).toBeGreaterThan(0) }) test("#given a ledger #when exported as csv #then rows carry fact_id and plot_value and stderr has the coverage line", () => { const { s } = fidelitySession() const proc = nodeJson(["fact", "export", "--csv", ...s]) expect(proc.code).toBe(0) expect(proc.stdout.split("\n")[0]).toContain("fact_id") expect(proc.stdout).toContain("F2") expect(proc.stderr).toContain("1 of 1 sources snapshotted") }) }) describe("meaning verifier under node", () => { test("#given the filled fixture #when fact pairs runs #then pairs.json holds one quote pair per cited ledger quote", () => { const { dir, pairs } = pairedSession() const written = JSON.parse(readFileSync(join(dir, "pairs.json"), "utf8")) expect(pairs.code).toBe(0) expect(written.skipped).toBe(false) expect(written.pairs.map((p: Pair) => [p.pair_id, p.kind, p.fact_ids])).toEqual([ ["P1", "quote", ["F1"]], ["P2", "quote", ["Q1"]], ]) expect(written.pairs_sha256).toMatch(/^[0-9a-f]{64}$/) expect(Object.keys(written.counts).filter((key) => key !== "total" && key !== "force_strong")).toEqual([...PAIR_KINDS]) expect(written.verdict_schema).toEqual(JSON.parse(JSON.stringify(VERDICT_SCHEMA))) }) test("#given cheap and strong verdicts agreeing on hedge_dropped #when meaning-check runs #then it exits 1 with one confirmed defect and one calibration line", () => { const { dir, s } = pairedSession() const drift = { pair_id: "P2", verdict: "drift", drift_kind: "hedge_dropped", fragment: "no earlier than 2027", source_fragment: "we estimate", explanation: "The estimate reads as a fact." } const cheap = verdicts(dir, "cheap.json", "cheap", [drift, { pair_id: "P1", verdict: "ok" }]) const strong = verdicts(dir, "strong.json", "strong", [drift]) const checked = nodeJson(["fact", "meaning-check", "--pairs", join(dir, "pairs.json"), "--verdicts", cheap, "--verdicts", strong, ...s]) const record = JSON.parse(readFileSync(join(dir, "fidelity-meaning-defects.json"), "utf8")) expect(checked.code).toBe(1) expect(codesOf(checked.json)).toEqual(["meaning_drift_confirmed"]) expect(codesOf(record)).toEqual(["meaning_drift_confirmed"]) expect(calibrationRows(dir)).toHaveLength(1) expect(calibrationRows(dir)[0]).toMatchObject({ pair_id: "P2", drift_kind: "hedge_dropped", confirmed: true, disputed: false }) const header = JSON.parse(readFileSync(join(dir, "fidelity-calibration.jsonl"), "utf8").split("\n")[0]) expect(header.promotion).toEqual({ ...PROMOTION }) const again = nodeJson(["fact", "meaning-check", "--pairs", join(dir, "pairs.json"), "--verdicts", cheap, "--verdicts", strong, ...s]) expect(again.code).toBe(1) expect(calibrationRows(dir)).toHaveLength(1) }) test("#given a dispute on the confirmed pair #when meaning-check runs #then the defect stays and the calibration line is disputed", () => { const { dir, s } = pairedSession() const drift = { pair_id: "P2", verdict: "drift", drift_kind: "hedge_dropped", fragment: "no earlier than 2027", source_fragment: "we estimate" } const files = ["--verdicts", verdicts(dir, "cheap.json", "cheap", [drift, { pair_id: "P1", verdict: "ok" }]), "--verdicts", verdicts(dir, "strong.json", "strong", [drift])] const checked = nodeJson(["fact", "meaning-check", "--pairs", join(dir, "pairs.json"), ...files, "--dispute", "P2", "--reason", "the operator states it", ...s]) expect(checked.code).toBe(1) expect(codesOf(checked.json)).toEqual(["meaning_drift_confirmed"]) expect(calibrationRows(dir)[0]).toMatchObject({ pair_id: "P2", disputed: true, reason: "the operator states it" }) }) test("#given --edition md #when meaning-check runs #then it writes that edition's record, and an unknown edition refuses", () => { const { dir, s } = pairedSession() const clean = [{ pair_id: "P1", verdict: "ok" }, { pair_id: "P2", verdict: "ok" }] const files = ["--verdicts", verdicts(dir, "cheap.json", "cheap", clean), "--verdicts", verdicts(dir, "strong.json", "strong", clean)] const checked = nodeJson(["fact", "meaning-check", "--pairs", join(dir, "pairs.json"), ...files, "--edition", "md", ...s]) const unknown = nodeJson(["fact", "meaning-check", "--pairs", join(dir, "pairs.json"), ...files, "--edition", "epub", ...s]) expect(checked.code).toBe(0) expect(JSON.parse(readFileSync(join(dir, "fidelity-meaning-defects.md.json"), "utf8"))).toMatchObject({ record_kind: "meaning", artifact_sha256: sha256(join(dir, "report.html")) }) expect(existsSync(join(dir, "fidelity-meaning-defects.json"))).toBe(false) expect(unknown.code).toBe(2) }) test("#given a dropped verdict and an unread pair #when meaning-check runs #then both pairs are meaning_unverified and it exits 1", () => { const { dir, s } = pairedSession() const cheap = verdicts(dir, "cheap.json", "cheap", [{ pair_id: "P2", verdict: "drift", drift_kind: "hedge_dropped", fragment: "certainly by 2027", source_fragment: "we estimate" }]) const checked = nodeJson(["fact", "meaning-check", "--pairs", join(dir, "pairs.json"), "--verdicts", cheap, ...s]) const summary = JSON.parse(checked.stderr.trim().split("\n").at(-1) ?? "{}") expect(checked.code).toBe(1) expect(checked.json.status).toBe("fail") expect(codesOf(checked.json)).toEqual(["meaning_unverified", "meaning_unverified"]) expect(checked.json.unverified.map((gap: { pair_id: string }) => gap.pair_id)).toEqual(["P1", "P2"]) expect(summary).toMatchObject({ command: "fact meaning-check", defects: 2, dropped: 1, unverified: 2 }) }) test("#given an unreadable verdict file or a malformed pairs file #when meaning-check runs #then it exits 2", () => { const { dir, s } = pairedSession() const broken = join(dir, "broken.json") writeFileSync(broken, "{ not json") expect(nodeJson(["fact", "meaning-check", "--pairs", join(dir, "pairs.json"), "--verdicts", broken, ...s]).code).toBe(2) const ok = verdicts(dir, "cheap.json", "cheap", []) for (const body of ["{}", "null", '{ "pairs": "P1" }']) { writeFileSync(join(dir, "bad-pairs.json"), body) expect(nodeJson(["fact", "meaning-check", "--pairs", join(dir, "bad-pairs.json"), "--verdicts", ok, ...s]).code, body).toBe(2) } }) test("#given a confirmed run then a dispute on the same inputs #when meaning-check reruns #then the log still holds one run with the dispute", () => { const { dir, s } = pairedSession() const drift = { pair_id: "P2", verdict: "drift", drift_kind: "hedge_dropped", fragment: "no earlier than 2027", source_fragment: "we estimate" } const files = ["--verdicts", verdicts(dir, "cheap.json", "cheap", [drift, { pair_id: "P1", verdict: "ok" }]), "--verdicts", verdicts(dir, "strong.json", "strong", [drift])] const first = nodeJson(["fact", "meaning-check", "--pairs", join(dir, "pairs.json"), ...files, ...s]) const disputed = nodeJson(["fact", "meaning-check", "--pairs", join(dir, "pairs.json"), ...files, "--dispute", "P2", "--reason", "the operator states it", ...s]) expect(disputed.code).toBe(1) expect(disputed.json.run_id).toBe(first.json.run_id) expect(disputed.json.promotion_eligibility).toMatchObject({ runs: 1, confirmed: 1, disputed_confirmed: 1, disputed_to_confirmed: 1 }) expect(calibrationRows(dir)).toEqual([expect.objectContaining({ pair_id: "P2", disputed: true, reason: "the operator states it" })]) const plain = nodeJson(["fact", "meaning-check", "--pairs", join(dir, "pairs.json"), ...files, ...s]) expect(plain.json.promotion_eligibility).toMatchObject({ runs: 1, disputed_confirmed: 1 }) }) test("#given a run with no flagged pair #when meaning-check runs #then the run is still logged and counted", () => { const { dir, s } = pairedSession() const ok = (pair_id: string) => ({ pair_id, verdict: "ok" }) const files = ["--verdicts", verdicts(dir, "cheap.json", "cheap", [ok("P1"), ok("P2")]), "--verdicts", verdicts(dir, "strong.json", "strong", [ok("P1"), ok("P2")])] const checked = nodeJson(["fact", "meaning-check", "--pairs", join(dir, "pairs.json"), ...files, ...s]) expect(checked.code).toBe(0) expect(calibrationRows(dir)).toEqual([]) expect(runLines(dir)).toEqual([expect.objectContaining({ run_id: checked.json.run_id, flagged: 0 })]) expect(checked.json.promotion_eligibility.runs).toBe(1) }) test("#given a corrupt calibration log #when meaning-check runs #then it exits 2 naming the file and line", () => { const { dir, s } = pairedSession() writeFileSync(join(dir, "fidelity-calibration.jsonl"), "{ not json\n") const checked = nodeJson(["fact", "meaning-check", "--pairs", join(dir, "pairs.json"), "--verdicts", verdicts(dir, "cheap.json", "cheap", []), ...s]) expect(checked.code).toBe(2) expect(checked.stderr).toContain(`${join(dir, "fidelity-calibration.jsonl")}:1`) }) test("#given a skipped pairs file #when meaning-check runs #then it is accepted only when the locked brief says off", () => { const { dir, s } = fidelitySession() const skip = join(dir, "skip.json") writeFileSync(skip, JSON.stringify({ schemaVersion: 1, skipped: true, pairs: [] })) const check = () => nodeJson(["fact", "meaning-check", "--pairs", skip, "--verdicts", verdicts(dir, "cheap.json", "cheap", []), ...s]) const on = check() expect(on.code).toBe(1) expect(on.json.error).toBe("meaning_check_required") writeFileSync(join(dir, "brief.md"), "# Brief\n") expect(check().json.error).toBe("fidelity_interview_incomplete") expect(check().code).toBe(1) writeFileSync(join(dir, "brief.md"), readFileSync(join(FIXTURES, "fidelity-brief-locked.md"), "utf8").replace("meaning check: on", "meaning check: off")) const off = check() expect(off.code).toBe(0) expect(off.json.status).toBe("skipped") }) test("#given a brief with meaning check off #when fact pairs runs #then it exits 0 and writes skipped", () => { const { dir, s } = fidelitySession() const brief = readFileSync(join(dir, "brief.md"), "utf8").replace("meaning check: on", "meaning check: off") writeFileSync(join(dir, "brief.md"), brief) nodeJson(["fact", "fill", join(dir, "report.src.html"), "--out", join(dir, "report.html"), ...s]) const pairs = nodeJson(["fact", "pairs", join(dir, "report.html"), "--out", join(dir, "pairs.json"), ...s]) expect(pairs.code).toBe(0) expect(JSON.parse(readFileSync(join(dir, "pairs.json"), "utf8"))).toMatchObject({ skipped: true }) }) })