linkedin-studio/scripts/contract-gate/tests/ratify.test.ts
Kjell Tore Guttormsen ff3467de79 feat(linkedin-studio): accumulation binding — ratify + §E foldIns (fix #1 slice 2)
Slice 2 av fix #1 («rettelser fester seg»): binder rules.ts til den menneskelige
sannhetskilden (skrivekontrakt §E), så en korreksjon gjort én gang håndheves for
alltid i stedet for å gjenoppdages hver utgave (diagnose-gap 2).

- ratify.ts: importerer RULES direkte (ingen skjør tekst-parsing), krever en
  bijeksjon mot §E gate-bindings-manifestet (id ↔ ref ↔ severity) + dangling-
  §-anker-sjekk. CLI: `--ratify [kontrakt]` / `npm run ratify`. Sti: arg > env
  MASKINROMMET_CONTRACT > default (relativt til modulen).
- edition-state.template: foldIns[] fangst-kø (per-artikkel proveniens,
  pending|promoted|rejected, mekanisk→rules.ts / dømmekraft→§C2).
- 12 nye tester (33 totalt), tsc rent. Live-kontrakt-integrasjonstest ratify-er
  det ekte §E-manifestet rent (10 regler bundet 1:1).

Companion: skrivekontrakt §E (JA-promoter-prosedyre + manifest) i maskinrommet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 17:48:20 +02:00

136 lines
5.1 KiB
TypeScript

import { describe, test } from "node:test";
import assert from "node:assert/strict";
import { existsSync } from "node:fs";
import { RULES } from "../src/rules.js";
import {
ratify,
ratifyFile,
parseManifest,
defaultContractPath,
} from "../src/ratify.js";
const kinds = (r: { issues: { kind: string }[] }) => r.issues.map((i) => i.kind);
/** A minimal contract carrying the required section headings + a manifest fence. */
function contractWith(manifestRows: string[], opts: { bRows?: number } = {}): string {
const bRows = opts.bRows ?? 10;
const bTable = Array.from({ length: bRows }, (_, i) => `| ${i + 1} | regel ${i + 1} | hvorfor | flagg |`).join("\n");
return [
"## B. De ti reglene",
"| # | Regel | Hvorfor | Flagg |",
"|---|---|---|---|",
bTable,
"",
"## C1. Regelsjekk-gate",
"## C2. Redaktør-pass",
"## E. Akkumulering",
"<!-- ratify:begin -->",
"| rule-id | ref | severity | proveniens |",
"|---------|-----|----------|------------|",
...manifestRows,
"<!-- ratify:end -->",
].join("\n");
}
/** One manifest row per rule, transcribing id/ref/severity exactly. */
const validRows = RULES.map((r) => `| ${r.id} | ${r.ref} | ${r.severity} | prov |`);
describe("ratify — rules.ts ↔ §E-manifest binding", () => {
test("a manifest that mirrors RULES exactly is in sync", () => {
const r = ratify(contractWith(validRows));
assert.equal(r.ok, true, `unexpected issues: ${JSON.stringify(r.issues)}`);
assert.equal(r.issues.length, 0);
assert.equal(r.ruleCount, RULES.length);
assert.equal(r.manifestCount, RULES.length);
});
test("a missing manifest fence is a hard failure", () => {
const r = ratify("## B. regler\nno manifest here at all\n");
assert.equal(r.ok, false);
assert.ok(kinds(r).includes("missing-from-manifest"));
assert.equal(r.manifestCount, 0);
});
test("a rule with no manifest row → missing-from-manifest", () => {
// drop the first rule's row
const rows = validRows.slice(1);
const r = ratify(contractWith(rows));
assert.equal(r.ok, false);
const missing = r.issues.filter((i) => i.kind === "missing-from-manifest");
assert.equal(missing.length, 1);
assert.equal(missing[0].id, RULES[0].id);
});
test("a manifest row with no rule → extra-in-manifest", () => {
const rows = [...validRows, "| ghost-rule | §B-2 | warn | spøkelse |"];
const r = ratify(contractWith(rows));
assert.equal(r.ok, false);
const extra = r.issues.filter((i) => i.kind === "extra-in-manifest");
assert.equal(extra.length, 1);
assert.equal(extra[0].id, "ghost-rule");
});
test("a ref that differs from rules.ts → ref-mismatch", () => {
const rows = validRows.map((row, i) =>
i === 0 ? `| ${RULES[0].id} | §B-99 | ${RULES[0].severity} | x |` : row,
);
const r = ratify(contractWith(rows));
assert.equal(r.ok, false);
assert.ok(r.issues.some((i) => i.kind === "ref-mismatch" && i.id === RULES[0].id));
});
test("a severity that differs from rules.ts → severity-mismatch", () => {
const flip = RULES[0].severity === "block" ? "warn" : "block";
const rows = validRows.map((row, i) =>
i === 0 ? `| ${RULES[0].id} | ${RULES[0].ref} | ${flip} | x |` : row,
);
const r = ratify(contractWith(rows));
assert.equal(r.ok, false);
assert.ok(r.issues.some((i) => i.kind === "severity-mismatch" && i.id === RULES[0].id));
});
test("a §B-N ref pointing past the table → dangling-ref", () => {
// §B only has 5 rows here, but RULES cites §B-10 (B10-serie-tese)
const r = ratify(contractWith(validRows, { bRows: 5 }));
assert.equal(r.ok, false);
assert.ok(r.issues.some((i) => i.kind === "dangling-ref"));
});
test("a duplicate manifest id is flagged", () => {
const rows = [...validRows, validRows[0]];
const r = ratify(contractWith(rows));
assert.equal(r.ok, false);
assert.ok(r.issues.some((i) => i.kind === "extra-in-manifest" && i.detail.includes("duplisert")));
});
});
describe("parseManifest", () => {
test("returns null when the fence is absent", () => {
assert.equal(parseManifest("no markers"), null);
});
test("skips header + separator, keeps data rows", () => {
const rows = parseManifest(contractWith(validRows));
assert.ok(rows);
assert.equal(rows!.length, RULES.length);
assert.equal(rows![0].id, RULES[0].id);
assert.equal(rows![0].severity, RULES[0].severity);
});
});
describe("ratifyFile — real contract integration", () => {
const real = defaultContractPath();
// Skips on a fresh clone / CI where maskinrommet is absent; runs on KTG's machine.
test("the live skrivekontrakt.md §E-manifest ratifies clean", { skip: !existsSync(real) }, () => {
const r = ratifyFile(real);
assert.equal(r.ok, true, `drift vs live contract: ${JSON.stringify(r.issues, null, 2)}`);
assert.equal(r.ruleCount, RULES.length);
});
test("a non-existent contract path fails cleanly (no throw)", () => {
const r = ratifyFile("/nonexistent/skrivekontrakt.md");
assert.equal(r.ok, false);
assert.equal(r.contractPath, "/nonexistent/skrivekontrakt.md");
});
});