import { describe, test } from "node:test"; import assert from "node:assert/strict"; import { validateBinding } from "../src/binding.js"; import type { LivedSpecifics, Slot } from "../src/binding.js"; import type { Bank, Specific } from "../src/types.js"; // ---- fixtures --------------------------------------------------------------- function mkSpecific(over: Partial & { id: string }): Specific { return { id: over.id, type: over.type ?? "other", content: over.content ?? "x", topicTags: over.topicTags ?? ["t"], provenance: over.provenance ?? { capturedAt: "2026-06-20", source: "test" }, verification: over.verification ?? "n/a", status: over.status ?? "active", }; } const mkBank = (specifics: Specific[]): Bank => ({ schemaVersion: 1, specifics }); const slot = (slotId: string, binding: Slot["binding"], over: Partial = {}): Slot => ({ slotId, kind: over.kind ?? "key-point", label: over.label ?? `label ${slotId}`, binding, }); const ls = (slots: Slot[]): LivedSpecifics => ({ slots, status: "pending" }); // ---- tests ------------------------------------------------------------------ describe("validateBinding — the «vaghet avvises» gate (G4)", () => { test("every slot resolved (specific + abstrakt + ekstern) → PASS, coverage counted", () => { const bank = mkBank([mkSpecific({ id: "aaa", type: "named-case" })]); const res = validateBinding( ls([ slot("kp1", { type: "specific", specificId: "aaa" }), slot("kp2", { type: "abstrakt", rationale: "bevisst abstrakt her" }), slot("kp3", { type: "ekstern", source: "research note 2" }), ]), bank, ); assert.equal(res.verdict, "PASS"); assert.equal(res.problems.length, 0); assert.deepEqual(res.coverage, { total: 3, specific: 1, abstrakt: 1, ekstern: 1, unresolved: 0, }); }); test("an unresolved slot → BLOCK with reason 'unresolved'", () => { const res = validateBinding( ls([ slot("kp1", { type: "specific", specificId: "aaa" }), slot("kp2", { type: "unresolved" }), ]), mkBank([mkSpecific({ id: "aaa" })]), ); assert.equal(res.verdict, "BLOCK"); assert.ok( res.problems.some((p) => p.slotId === "kp2" && p.reason === "unresolved"), "kp2 should be flagged unresolved", ); assert.equal(res.coverage.unresolved, 1); }); test("a specific slot whose id is not in the bank → BLOCK 'dangling-specific'", () => { const res = validateBinding( ls([slot("kp1", { type: "specific", specificId: "missing" })]), mkBank([]), ); assert.equal(res.verdict, "BLOCK"); assert.ok( res.problems.some((p) => p.slotId === "kp1" && p.reason === "dangling-specific"), "dangling id must be caught", ); // dangling still counts under `specific` in coverage (binding TYPE is specific) assert.equal(res.coverage.specific, 1); }); test("an abstrakt escape with a blank rationale is vaghet → BLOCK 'unjustified-abstrakt'", () => { const res = validateBinding(ls([slot("kp1", { type: "abstrakt", rationale: " " })]), mkBank([])); assert.equal(res.verdict, "BLOCK"); assert.ok( res.problems.some((p) => p.slotId === "kp1" && p.reason === "unjustified-abstrakt"), "an unjustified abstraction must be rejected", ); }); test("ekstern needs no source (research fills it later) → PASS", () => { const res = validateBinding( ls([ slot("a", { type: "ekstern" }), slot("b", { type: "abstrakt", rationale: "x" }), slot("c", { type: "abstrakt", rationale: "y" }), ]), mkBank([]), ); assert.equal(res.verdict, "PASS"); assert.deepEqual(res.coverage, { total: 3, specific: 0, abstrakt: 2, ekstern: 1, unresolved: 0, }); }); test("a slot backed by an UNVERIFIED number → PASS + warning (regel 6/7)", () => { const bank = mkBank([mkSpecific({ id: "num1", type: "number", verification: "unverified" })]); const res = validateBinding(ls([slot("kp1", { type: "specific", specificId: "num1" })]), bank); assert.equal(res.verdict, "PASS", "an unverified number warns, it does not block"); assert.ok( res.warnings.some((w) => w.slotId === "kp1" && w.reason === "unverified-number"), "unverified number must surface as a warning", ); }); test("a slot backed by a VERIFIED number → PASS, no warning", () => { const bank = mkBank([mkSpecific({ id: "num1", type: "number", verification: "verified" })]); const res = validateBinding(ls([slot("kp1", { type: "specific", specificId: "num1" })]), bank); assert.equal(res.verdict, "PASS"); assert.equal(res.warnings.length, 0); }); test("empty slot-map → PASS, total 0 (the command decides whether ≥1 slot is required)", () => { const res = validateBinding(ls([]), mkBank([])); assert.equal(res.verdict, "PASS"); assert.equal(res.coverage.total, 0); assert.equal(res.problems.length, 0); }); test("multiple problems are all reported (gate is not short-circuited)", () => { const res = validateBinding( ls([ slot("a", { type: "unresolved" }), slot("b", { type: "specific", specificId: "nope" }), slot("c", { type: "abstrakt", rationale: "" }), ]), mkBank([]), ); assert.equal(res.verdict, "BLOCK"); assert.equal(res.problems.length, 3); }); });