linkedin-studio/scripts/contract-gate/src/cli.ts
Kjell Tore Guttormsen 58aebf05a9 feat(linkedin-studio): deterministic §B/§C1 contract binding-gate (fix #1 slice 1)
Adds scripts/contract-gate/ — a deterministic, AI-free gate that enforces the
mechanical half of the Maskinrommet writing contract (§B red flags + §C1
regelsjekk) BEFORE operator handoff, so the same mechanical correction never
reaches KTG twice. First slice of fix #1 (binding + accumulating); target
re-scoped to close all 4 diagnosis gaps = sustainable newsletter workflow.

- Structured ruleset as DATA (src/rules.ts) so accumulation (slice 2) appends.
- 3 BLOCK (modell-strawman, series-thesis "er ikke modellen", _underscore_)
  + 7 WARN (judgment-needed). Block only on provable, ~zero-FP matches.
- Code/frontmatter masking; line-accurate reporting; CLI receipt + exit code.
- 21/21 tests (node:test), tsc clean.
- Validated on 6 real editions: 0 BLOCK on published Del 2-5; caught a real
  Rule-10 violation in Del 6 (WIP) automatically. Versal-tic uses an
  emphasis-word denylist after real-data FPs (NAV/DFØ/NPM).

Finding: the plugin already mirrors §A + §C2; §C1 was the unhomed gap this fills.
Plugin structure lint unchanged (81/0/0); no new agents/commands/refs/skills.
2026-06-20 17:17:51 +02:00

66 lines
2.2 KiB
JavaScript

#!/usr/bin/env node
/**
* CLI for the §B/§C1 mechanical binding-gate.
*
* node --import tsx src/cli.ts <utkast.md> [--json]
*
* Exit code: 0 if the gate passes (no BLOCK violations — handoff permitted),
* 1 if BLOCKED, 2 on usage error. WARN violations never fail the gate; they are
* surfaced for KTG's judgment. The text output is the "kvittering" the writing
* contract asks for when a draft is delivered.
*/
import { runGateFile } from "./gate.js";
import type { GateResult, Violation } from "./types.js";
function printViolation(v: Violation): void {
console.log(` [${v.ref}] ${v.ruleId}`);
if (v.observed !== undefined) {
const t = v.threshold !== undefined ? ` (terskel: ${v.threshold})` : "";
console.log(` observert: ${v.observed}${t}`);
}
console.log(`${v.message}`);
const shown = v.matches.slice(0, 5);
for (const m of shown) console.log(` · L${m.line}: ${m.excerpt}`);
if (v.matches.length > shown.length) {
console.log(` · … +${v.matches.length - shown.length} til`);
}
}
function report(r: GateResult): void {
console.log("§B/§C1 regelsjekk-gate");
console.log(`Utkast: ${r.draftPath ?? "(stdin)"} · ${r.wordCount} ord\n`);
if (r.blocks.length) {
console.log(`BLOCK (${r.blocks.length}) — må fikses før handoff:`);
r.blocks.forEach(printViolation);
console.log("");
}
if (r.warns.length) {
console.log(`WARN (${r.warns.length}) — krever din dom (blokkerer ikke):`);
r.warns.forEach(printViolation);
console.log("");
}
if (r.passed) {
console.log(`✓ Gate passert — 0 block, ${r.warns.length} warn til vurdering.`);
} else {
console.log(
`✗ BLOKKERT — ${r.blocks.length} block, ${r.warns.length} warn. Fiks block-ene og kjør på nytt.`,
);
}
}
function main(): void {
const args = process.argv.slice(2);
const jsonOut = args.includes("--json");
const path = args.find((a) => !a.startsWith("--"));
if (!path) {
console.error("Bruk: contract-gate <utkast.md> [--json]");
process.exit(2);
}
const result = runGateFile(path);
if (jsonOut) console.log(JSON.stringify(result, null, 2));
else report(result);
process.exit(result.passed ? 0 : 1);
}
main();