linkedin-studio/scripts/contract-gate/src/rules.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

141 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Seed ruleset for the §B/§C1 mechanical binding-gate.
*
* Each rule transcribes a mechanically-checkable item from the Maskinrommet
* writing contract (maskinrommet/docs/skrivekontrakt.md). The classification
* principle (slice 1): BLOCK only what the gate can PROVE with ~zero false
* positives (exact forbidden phrases, markdown syntax); WARN on grep-flags that
* still need human/AI judgment (a word that *might* be a metaphor, a bare
* "modell" that *might* be neutral). Judgment-only / external-data rules
* (research/ traceability, term definitions, narrative architecture, the full
* AI-tell list) are intentionally NOT here — they stay with voice-scrubber
* (tier 2) and editorial-reviewer (tier 3).
*
* Deferred to a later slice (documented gaps, not silent omissions):
* - §C2 "postulerte tall" (numbers without hedge/source) — needs a
* hedge-proximity heuristic with non-trivial false-positive risk.
* - §C2 "ordrette gjentakelser" (any n-gram repeated >2×) — needs n-gram
* analysis; the contract's named phrases are edition-specific.
*/
import type { Rule } from "./types.js";
export const RULES: Rule[] = [
// ── BLOCK: provable, ~zero false positives ─────────────────────────────
{
id: "B3-modell-referansepunkt",
ref: "§B-3 / §C1",
severity: "block",
kind: "literal",
patterns: [
"det er ikke modellen som",
"ikke en bedre modell",
"modellen er god nok",
"uten å bytte modell",
],
message:
"Modell-stråmann (regel 3, KTGs mest gjentatte korreksjon): ingen påstand om at modellen er problemet/for svak. Skriv om til vår evne, vilje og kompetanse til å hente ut potensialet.",
},
{
id: "B10-serie-tese",
ref: "§B-10 / §C1",
severity: "block",
kind: "literal",
patterns: ["er ikke modellen"],
message:
"Serie-tesen / meta-tese (regel 10, korollar til regel 3): «… er ikke modellen» er den tause linsen, aldri en setning vi leverer. Positiv konkret landing («menneskene … er den knappe ressursen») er OK — den abstrakte meta-tesen er ikke.",
},
{
id: "understrek-emfase",
ref: "§C1 / hygiene",
severity: "block",
kind: "regex",
// Markdown underscore-emphasis: opening _ at a boundary, closing _ before
// whitespace/punctuation. snake_case (underscores between word chars) is
// NOT matched, so identifiers in prose don't false-positive.
patterns: ["(^|[\\s(])_[^_\\n]+_(?=[\\s).,!?:;]|$)"],
message:
"Emfase med _understrek_ rendrer rått (hygiene). Bruk **fet** (eller *kursiv* med stjerne).",
},
// ── WARN: grep-flag, needs human/AI judgment ───────────────────────────
{
id: "B2-metafor",
ref: "§B-2",
severity: "warn",
kind: "regex",
patterns: ["\\b(tak|lodd|reise|landskap)\\b"],
message:
"Mulig metafor som bærer argumentet (regel 2). Bekreft at bruken er konkret/nøytral, ikke utvidet bildespråk («et usynlig tak» → «KI-en når ikke dataene»).",
},
{
id: "B3-modell-bar",
ref: "§B-3",
severity: "warn",
kind: "count",
threshold: 0,
patterns: ["\\bmodell"],
message:
"«modell» nevnt — sjekk at HVERT treff er nøytral faktabruk («en KI-modell er trent på …»), ikke modellen brukt som referansepunkt/stråmann (regel 3).",
},
{
id: "KI-en-subjekt",
ref: "hygiene ★",
severity: "warn",
kind: "count",
threshold: 3,
patterns: ["\\bKI-en\\b"],
message:
"«KI-en» gjentatt som handlende subjekt er dårlig norsk (språk-hygiene). Varier: «KI», «verktøyet», eller omskriv («data KI har tilgang til»).",
},
{
id: "B9-PS",
ref: "§B-9 / §C1",
severity: "warn",
kind: "regex",
patterns: ["(^|\\n)\\s*P\\.?\\s?S\\.?\\b"],
message:
"PS/etter-closing-blokk (regel 9). Sjekk at den ikke åpner nytt begrep/rammeverk etter handlingskallet — leseren skal lukke på ett konkret handlingsvalg.",
},
{
id: "C2-tankestrek-tetthet",
ref: "§C2",
severity: "warn",
kind: "density",
threshold: 1,
per: 50,
patterns: ["\\u2014"], // em dash —
message:
"Tankestrek-tetthet over ~1 per 50 ord (§C2). Bytt halvparten med komma eller punktum; innskudd-formelen «X — Y — Z» bør være sjelden.",
},
{
id: "C2-versal-tic",
ref: "§C2",
severity: "warn",
kind: "regex",
// Case-sensitive, Unicode-aware (handles MÅ/NÅ/SÅ). DENYLIST, not allowlist:
// flag only UPPERCASE forms of known Norwegian emphasis words — NOT every
// 2+-uppercase run. Acronyms (NPM, DFØ, NAV) are not emphasis words, so they
// never flag. (Real-data finding on Del 4/5: an acronym allowlist can't keep
// up; the emphasis-word set is small and stable.)
flags: "gu",
patterns: [
"(?<!\\p{L})(OG|IKKE|IKKJE|MEN|MÅ|SKAL|ALDRI|ALLTID|ALLE|INGEN|BARE|KUN|NÅ|SÅ|HELE|ENHVER)(?!\\p{L})",
],
message:
"Versal-tic (§C2): emfase-ord i VERSALER midt i prosa («OG», «IKKE», «MEN») bryter kronikk-stilen. Bruk *kursiv* eller omformuler.",
},
{
id: "B8-ai-tells",
ref: "§B-8",
severity: "warn",
kind: "regex",
patterns: [
"la meg være ærlig",
"i en verden der",
"det handler ikke om .{1,40}? det handler om",
],
message:
"AI-tell (regel 8). Fjern/omformuler. Full liste: auto-memory `no-ai-tell-phrases`.",
},
];