diff --git a/.gitignore b/.gitignore index 2c948ed..945822b 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,8 @@ docs/**/.session-state.local.json # Node.js scripts/analytics/node_modules/ scripts/analytics/build/ +scripts/contract-gate/node_modules/ +scripts/contract-gate/build/ # --- session/local state --- # STATE.md is LOCAL-ONLY (gitignored): no private remote exists and STATE must diff --git a/scripts/contract-gate/README.md b/scripts/contract-gate/README.md new file mode 100644 index 0000000..c603815 --- /dev/null +++ b/scripts/contract-gate/README.md @@ -0,0 +1,73 @@ +# Contract-gate — §B/§C1 mekanisk binding-gate + +Deterministisk gate som håndhever den **mekaniske** halvdelen av Maskinrommet-skrivekontrakten +(`maskinrommet/docs/skrivekontrakt.md`: §B de ti reglene, §C1 regelsjekk-gaten, §C2 prosa-håndverk) +**før et utkast leveres til operatøren** — så de samme mekaniske korreksjonene aldri når KTG to ganger. + +Dette er **slice 1** av fix #1 (§C2 bindende + akkumulerende). Den er bevisst _bindende_: ren +string/regex-evaluering, ingen AI, ingen nettverk — den kan ikke hoppes over eller snakkes rundt, +og kjører på millisekunder. + +## Hva gaten gjør (og ikke gjør) + +Tre-nivå-modellen fra diagnosen — denne gaten er **nivå 1**: + +| Nivå | Hva | Hvem | Hvor | +|------|-----|------|------| +| **1 — rent mekanisk** | §B/§C1-grep + tellinger | **denne gaten** (BLOCK/WARN) | her | +| 2 — AI-mekanisk | §C1 pkt 1/8/9 | `voice-scrubber` (fikser før handoff) | Step 4 | +| 3 — ekte dømmekraft | §C2 narrativ-arkitektur | `editorial-reviewer` (flagg til KTG) | Step 5.5 | + +**Klassifiseringsprinsipp:** BLOCK bare det gaten kan _bevise_ med ~null false positives. +Alt som krever dom → WARN (blokkerer ikke). Ekte dømmekraft / ekstern data er ikke her i det +hele tatt (blir hos nivå 2/3). + +### Regler (seed) + +| Regel | Ref | Severity | +|-------|-----|----------| +| Modell-stråmann (4 eksakte fraser) | §B-3 | **BLOCK** | +| Serie-tese «er ikke modellen» | §B-10 | **BLOCK** | +| `_understrek_`-emfase | §C1 | **BLOCK** | +| Metafor-ord (tak/lodd/reise/landskap) | §B-2 | WARN | +| Bar «modell» (alle treff) | §B-3 | WARN | +| «KI-en» som subjekt (>3×) | hygiene | WARN | +| PS-blokk | §B-9 | WARN | +| Tankestrek-tetthet (>1/50 ord) | §C2 | WARN | +| Versal-tic (emfase-ord, ikke akronymer) | §C2 | WARN | +| AI-tells (navngitte fraser) | §B-8 | WARN | + +Reglene er **data** (`src/rules.ts`), ikke hardkodet logikk — det er det som gjør +akkumuleringen (slice 2) ren: en ny regel = én rad i `skrivekontrakt.md` + én post her. + +### Bevisst utsatt (dokumenterte gap, ikke stille utelatelser) + +- **§C2 «postulerte tall»** (tall uten hedge/kilde) — krever hedge-nærhet-heuristikk med + ikke-triviell false-positive-risiko. +- **§C2 «ordrette gjentakelser»** (n-gram gjentatt >2×) — krever n-gram-analyse; kontraktens + navngitte fraser er utgave-spesifikke. + +Begge hører hjemme i en senere slice når heuristikken kan gjøres tillitsverdig. + +## Kjøre + +```bash +cd scripts/contract-gate +npm install # engangs (tsx + typescript) +node --import tsx src/cli.ts # kvittering + exit 0/1 +node --import tsx src/cli.ts --json # maskinlesbart +npm test # 21 tester +``` + +Exit: `0` = passert (ingen BLOCK, handoff tillatt), `1` = blokkert, `2` = bruksfeil. +WARN feiler aldri gaten — de surfaces for KTGs dom. + +## Validering (6 ekte utgaver, juni 2026) + +- **0 BLOCK** på publiserte Del 2–5 → false-blokkerer ikke ekte arbeid. +- **Del 6 (WIP):** fanget et ekte Regel-10-brudd («Den knappe ressursen er ikke modellen» i + brødteksten) automatisk — slog-drepingen i praksis. +- **Del 1:** blokkerer på samme frase, men ble publisert **før** Regel 10 ble kodifisert (01.06) — + historisk artefakt, ikke gate-feil. +- Versal-tic-redesignet (denylist av emfase-ord, ikke allowlist av akronymer) fjernet + NAV/DFØ/NPM-false-positivene som ekte data avslørte. diff --git a/scripts/contract-gate/package-lock.json b/scripts/contract-gate/package-lock.json new file mode 100644 index 0000000..b02ea9d --- /dev/null +++ b/scripts/contract-gate/package-lock.json @@ -0,0 +1,566 @@ +{ + "name": "linkedin-contract-gate", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "linkedin-contract-gate", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/scripts/contract-gate/package.json b/scripts/contract-gate/package.json new file mode 100644 index 0000000..db0bd1a --- /dev/null +++ b/scripts/contract-gate/package.json @@ -0,0 +1,16 @@ +{ + "name": "linkedin-contract-gate", + "version": "0.1.0", + "type": "module", + "description": "Deterministic §B/§C1 mechanical binding-gate for the Maskinrommet writing contract — runs BEFORE operator handoff so mechanical corrections never reach KTG twice", + "scripts": { + "build": "tsc", + "test": "node --import tsx --test tests/*.test.ts", + "start": "node --import tsx src/cli.ts" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + } +} diff --git a/scripts/contract-gate/src/cli.ts b/scripts/contract-gate/src/cli.ts new file mode 100644 index 0000000..3d31360 --- /dev/null +++ b/scripts/contract-gate/src/cli.ts @@ -0,0 +1,66 @@ +#!/usr/bin/env node +/** + * CLI for the §B/§C1 mechanical binding-gate. + * + * node --import tsx src/cli.ts [--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 [--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(); diff --git a/scripts/contract-gate/src/gate.ts b/scripts/contract-gate/src/gate.ts new file mode 100644 index 0000000..1c7f241 --- /dev/null +++ b/scripts/contract-gate/src/gate.ts @@ -0,0 +1,207 @@ +/** + * Deterministic engine for the §B/§C1 mechanical binding-gate. + * + * Pure string/regex evaluation of a draft against a structured ruleset. No AI, + * no network, no external state — so it is genuinely BINDING: it cannot be + * skipped or talked around, and it runs in milliseconds. Mechanical violations + * are caught here BEFORE the draft reaches KTG, so the same correction never + * reaches him twice. + * + * Matching runs on a MASKED copy of the draft (frontmatter + code stripped to + * spaces, line positions preserved) so identifiers/code never false-positive. + */ + +import { readFileSync } from "node:fs"; +import type { GateResult, Match, Rule, Violation } from "./types.js"; +import { RULES } from "./rules.js"; + +export { RULES } from "./rules.js"; + +/** + * Replace YAML frontmatter, fenced code blocks, and inline code spans with + * spaces — preserving newlines, length, and therefore every offset/line number. + */ +export function mask(text: string): string { + const chars = text.split(""); + const blank = (start: number, end: number) => { + for (let i = start; i < end && i < chars.length; i++) { + if (chars[i] !== "\n") chars[i] = " "; + } + }; + + // YAML frontmatter at the very top: ---\n … \n--- + if (text.startsWith("---\n")) { + const close = text.indexOf("\n---", 3); + if (close !== -1) { + const lineEnd = text.indexOf("\n", close + 1); + blank(0, lineEnd === -1 ? text.length : lineEnd); + } + } + + let m: RegExpExecArray | null; + const fence = /```[\s\S]*?```/g; + while ((m = fence.exec(text)) !== null) blank(m.index, m.index + m[0].length); + + const inline = /`[^`\n]*`/g; + while ((m = inline.exec(text)) !== null) blank(m.index, m.index + m[0].length); + + return chars.join(""); +} + +function lineStarts(text: string): number[] { + const starts = [0]; + for (let i = 0; i < text.length; i++) { + if (text[i] === "\n") starts.push(i + 1); + } + return starts; +} + +/** 1-based line of a character index. */ +function lineOf(starts: number[], index: number): number { + let lo = 0; + let hi = starts.length - 1; + let ans = 0; + while (lo <= hi) { + const mid = (lo + hi) >> 1; + if (starts[mid] <= index) { + ans = mid; + lo = mid + 1; + } else { + hi = mid - 1; + } + } + return ans + 1; +} + +function excerptAt(original: string, index: number, len: number): string { + const start = Math.max(0, index - 20); + const end = Math.min(original.length, index + len + 20); + return original.slice(start, end).replace(/\s+/g, " ").trim(); +} + +function countWords(masked: string): number { + const m = masked.match(/[A-Za-zÆØÅæøå0-9]+/g); + return m ? m.length : 0; +} + +interface Ctx { + masked: string; + original: string; + starts: number[]; +} + +function literalMatches(rule: Rule, ctx: Ctx): Match[] { + const hay = ctx.masked.toLowerCase(); + const out: Match[] = []; + for (const pat of rule.patterns) { + const needle = pat.toLowerCase(); + if (!needle) continue; + let from = 0; + let idx: number; + while ((idx = hay.indexOf(needle, from)) !== -1) { + out.push({ + line: lineOf(ctx.starts, idx), + excerpt: excerptAt(ctx.original, idx, needle.length), + }); + from = idx + needle.length; + } + } + return out; +} + +/** Run a rule's regex patterns; returns ALL matches (after the allow-filter). */ +function regexMatches(rule: Rule, ctx: Ctx, cap = Infinity): { matches: Match[]; count: number } { + const baseFlags = rule.flags ?? "gi"; + const flags = baseFlags.includes("g") ? baseFlags : baseFlags + "g"; + const matches: Match[] = []; + let count = 0; + for (const pat of rule.patterns) { + const re = new RegExp(pat, flags); + let m: RegExpExecArray | null; + while ((m = re.exec(ctx.masked)) !== null) { + if (m[0].length === 0) { + re.lastIndex++; + continue; + } + const trimmed = m[0].trim(); + count++; + if (matches.length < cap) { + const lead = m[0].length - m[0].trimStart().length; + const at = m.index + lead; + matches.push({ + line: lineOf(ctx.starts, at), + excerpt: excerptAt(ctx.original, at, trimmed.length), + }); + } + } + } + return { matches, count }; +} + +function evalRule(rule: Rule, ctx: Ctx, wordCount: number): Violation | null { + let matches: Match[] = []; + let observed: number | undefined; + let threshold: number | undefined; + + switch (rule.kind) { + case "literal": + matches = literalMatches(rule, ctx); + break; + case "regex": + matches = regexMatches(rule, ctx).matches; + break; + case "count": { + const { matches: ms, count } = regexMatches(rule, ctx, 8); + observed = count; + threshold = rule.threshold ?? 0; + if (count > threshold) matches = ms; + break; + } + case "density": { + const { matches: ms, count } = regexMatches(rule, ctx, 8); + const maxAllowed = Math.floor((wordCount / (rule.per ?? 50)) * (rule.threshold ?? 1)); + observed = count; + threshold = maxAllowed; + if (count > maxAllowed) matches = ms; + break; + } + } + + if (matches.length === 0) return null; + return { + ruleId: rule.id, + ref: rule.ref, + severity: rule.severity, + message: rule.message, + matches, + observed, + threshold, + }; +} + +/** Evaluate raw draft text against a ruleset. */ +export function evaluate( + text: string, + rules: Rule[] = RULES, + draftPath: string | null = null, +): GateResult { + const masked = mask(text); + const ctx: Ctx = { masked, original: text, starts: lineStarts(text) }; + const wordCount = countWords(masked); + + const blocks: Violation[] = []; + const warns: Violation[] = []; + for (const rule of rules) { + const v = evalRule(rule, ctx, wordCount); + if (!v) continue; + (v.severity === "block" ? blocks : warns).push(v); + } + + return { draftPath, wordCount, blocks, warns, passed: blocks.length === 0 }; +} + +/** Read a draft file from disk and evaluate it. */ +export function runGateFile(path: string, rules: Rule[] = RULES): GateResult { + const text = readFileSync(path, "utf8"); + return evaluate(text, rules, path); +} diff --git a/scripts/contract-gate/src/rules.ts b/scripts/contract-gate/src/rules.ts new file mode 100644 index 0000000..7754df3 --- /dev/null +++ b/scripts/contract-gate/src/rules.ts @@ -0,0 +1,141 @@ +/** + * 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: [ + "(? threshold (default 0) + | "density"; // count occurrences; flag when rate exceeds threshold per `per` words + +export interface Rule { + /** Stable id, e.g. "B3-modell-referansepunkt". */ + id: string; + /** Contract reference for the report, e.g. "§B-3 / §C1". */ + ref: string; + severity: Severity; + kind: RuleKind; + /** Literal substrings (kind=literal) or regex sources (regex/count/density). */ + patterns: string[]; + /** Operator-facing direction: what the violation is + what to do. */ + message: string; + /** count: max allowed before flagging (default 0). density: max per `per` words. */ + threshold?: number; + /** density window size in words (default 50). */ + per?: number; + /** Regex flags for regex/count/density (default "gi"). */ + flags?: string; +} + +export interface Match { + /** 1-based line in the ORIGINAL draft. */ + line: number; + /** Short single-line excerpt around the match (from the original text). */ + excerpt: string; +} + +export interface Violation { + ruleId: string; + ref: string; + severity: Severity; + message: string; + matches: Match[]; + /** Observed count/rate for count|density rules. */ + observed?: number; + threshold?: number; +} + +export interface GateResult { + draftPath: string | null; + wordCount: number; + blocks: Violation[]; + warns: Violation[]; + /** true iff there are no BLOCK violations — i.e. handoff is permitted. */ + passed: boolean; +} diff --git a/scripts/contract-gate/tests/gate.test.ts b/scripts/contract-gate/tests/gate.test.ts new file mode 100644 index 0000000..286a208 --- /dev/null +++ b/scripts/contract-gate/tests/gate.test.ts @@ -0,0 +1,178 @@ +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 }); + } + }); + }); +}); diff --git a/scripts/contract-gate/tsconfig.json b/scripts/contract-gate/tsconfig.json new file mode 100644 index 0000000..eaa1e8e --- /dev/null +++ b/scripts/contract-gate/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "build", "tests"] +}