import { describe, test } from "node:test"; import assert from "node:assert/strict"; import { mkdtempSync, writeFileSync, rmSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { evaluate, runGateFile, mask } from "../src/gate.js"; import { RULES } from "../src/rules.js"; const ids = (vs: { ruleId: string }[]) => vs.map((v) => v.ruleId); describe("contract-gate engine", () => { describe("clean draft", () => { test("a contract-compliant snippet passes with no blocks and no warns", () => { const text = "Jeg bruker KI hver dag og ser hvordan saksbehandlere henter ut potensialet.\n" + "En kontraktsjurist sparte to dager. Verktøyet leverer det vi ber om, og " + "ledelsen tar valget."; const r = evaluate(text, RULES); assert.equal(r.passed, true); assert.equal(r.blocks.length, 0, `unexpected blocks: ${ids(r.blocks)}`); assert.equal(r.warns.length, 0, `unexpected warns: ${ids(r.warns)}`); }); }); describe("BLOCK: modell-stråmann (§B-3)", () => { test("an exact forbidden phrase blocks handoff", () => { const text = "Poenget er at det er ikke modellen som mangler, men kompetansen."; const r = evaluate(text, RULES); assert.equal(r.passed, false); assert.ok(ids(r.blocks).includes("B3-modell-referansepunkt")); }); test("'modellen er god nok' blocks", () => { const r = evaluate("Mange tror modellen er god nok allerede.", RULES); assert.equal(r.passed, false); assert.ok(ids(r.blocks).includes("B3-modell-referansepunkt")); }); }); describe("BLOCK: serie-tese (§B-10)", () => { test("the meta-thesis 'er ikke modellen' blocks", () => { const text = "Tesen er enkel: den knappe ressursen er ikke modellen."; const r = evaluate(text, RULES); assert.equal(r.passed, false); assert.ok(ids(r.blocks).includes("B10-serie-tese")); }); test("REGRESSION: positive concrete landing 'er den knappe ressursen' does NOT block", () => { const text = "Menneskene i organisasjonen er den knappe ressursen, ikke verktøyet."; const r = evaluate(text, RULES); assert.equal(r.passed, true, `unexpected blocks: ${ids(r.blocks)}`); assert.ok(!ids(r.blocks).includes("B10-serie-tese")); }); }); describe("BLOCK: _understrek_ emphasis (§C1)", () => { test("underscore emphasis blocks", () => { const r = evaluate("Dette er _viktig_ å forstå.", RULES); assert.equal(r.passed, false); assert.ok(ids(r.blocks).includes("understrek-emfase")); }); test("snake_case identifiers in prose do NOT block", () => { const r = evaluate("Variabelen LINKEDIN_STUDIO_DATA styrer roten.", RULES); assert.ok(!ids(r.blocks).includes("understrek-emfase")); }); }); describe("code masking", () => { test("a forbidden phrase inside a fenced code block is NOT flagged", () => { const text = "Her er konfigen:\n```\nmodellen er god nok\n```\nEllers ren prosa."; const r = evaluate(text, RULES); assert.equal(r.passed, true, `unexpected blocks: ${ids(r.blocks)}`); }); test("a forbidden phrase inside an inline code span is NOT flagged", () => { const r = evaluate("Flagget `er ikke modellen` i koden.", RULES); assert.ok(!ids(r.blocks).includes("B10-serie-tese")); }); test("mask preserves line count and positions", () => { const text = "linje1\n```\nkode\n```\nlinje5"; const masked = mask(text); assert.equal(masked.split("\n").length, text.split("\n").length); assert.equal(masked.length, text.length); }); }); describe("WARN: non-blocking flags", () => { test("a metaphor word warns but does not block", () => { const text = "Det er et usynlig tak over hele organisasjonen."; const r = evaluate(text, RULES); assert.equal(r.passed, true); assert.ok(ids(r.warns).includes("B2-metafor")); assert.ok(!ids(r.blocks).includes("B2-metafor")); }); test("high em-dash density warns", () => { const text = "Ord — ord — ord — ord — ord."; const r = evaluate(text, RULES); assert.ok(ids(r.warns).includes("C2-tankestrek-tetthet")); }); test("uppercase emphasis word warns", () => { const dirty = evaluate("Dette er IKKE bra, selv om KI og IT er OK.", RULES); assert.ok(ids(dirty.warns).includes("C2-versal-tic")); }); test("REGRESSION: acronyms (NPM/DFØ/NAV) are NOT versal-tics", () => { // Denylist semantics: only UPPERCASE emphasis words flag, never acronyms. const r = evaluate("Verktøyet NPM og CLI, og DFØ og NAV, er nyttige.", RULES); assert.ok(!ids(r.warns).includes("C2-versal-tic"), `flagged: ${ids(r.warns)}`); }); test("uppercase 'MÅ' (with Å) is caught by the Unicode-aware pattern", () => { const r = evaluate("Du MÅ gjøre dette nå.", RULES); assert.ok(ids(r.warns).includes("C2-versal-tic")); }); test("a plain acronym sentence raises no versal warn", () => { const clean = evaluate("KI og IT samarbeider tett med NSM og EU.", RULES); assert.ok(!ids(clean.warns).includes("C2-versal-tic")); }); test("'KI-en' as subject warns only past the repetition threshold", () => { const few = evaluate("KI-en gjør jobben.", RULES); assert.ok(!ids(few.warns).includes("KI-en-subjekt")); const many = evaluate( "KI-en gjør X. KI-en gjør Y. KI-en gjør Z. KI-en gjør W.", RULES, ); assert.ok(ids(many.warns).includes("KI-en-subjekt")); }); test("bare 'modell' is surfaced as a warn for per-occurrence review", () => { const r = evaluate("En KI-modell er trent på data.", RULES); assert.ok(ids(r.warns).includes("B3-modell-bar")); assert.equal(r.passed, true); }); }); describe("reporting", () => { test("line numbers point at the original draft line", () => { const text = "linje1\nlinje2\nDette er _viktig_ her.\nlinje4"; const r = evaluate(text, RULES); const v = r.blocks.find((b) => b.ruleId === "understrek-emfase"); assert.ok(v); assert.equal(v!.matches[0].line, 3); }); test("count rules report observed and threshold", () => { const r = evaluate("En modell, to modeller, tre modell-typer.", RULES); const v = r.warns.find((w) => w.ruleId === "B3-modell-bar"); assert.ok(v); assert.ok((v!.observed ?? 0) >= 3); }); }); describe("runGateFile", () => { test("reads a draft file and evaluates it", () => { const dir = mkdtempSync(join(tmpdir(), "contract-gate-")); try { const p = join(dir, "draft.md"); writeFileSync(p, "Tesen: den knappe ressursen er ikke modellen."); const r = runGateFile(p); assert.equal(r.draftPath, p); assert.equal(r.passed, false); assert.ok(ids(r.blocks).includes("B10-serie-tese")); } finally { rmSync(dir, { recursive: true, force: true }); } }); }); });