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>
136 lines
5.1 KiB
TypeScript
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");
|
|
});
|
|
});
|