fix(okr): drift-laas kommando-antall og haandhev rapport utenfor bundlen

This commit is contained in:
Kjell Tore Guttormsen 2026-08-02 10:36:26 +02:00
commit d63438581d
4 changed files with 92 additions and 8 deletions

View file

@ -169,7 +169,7 @@ It does **not** own confidence, nor the assessment fields it reserves — the pe
> /okr:freshen-references
```
`/okr:help` lists all 14 commands, 7 agents, and a recommended workflow keyed to where you are in the tertial cycle. `/okr:freshen-references` is a knowledge-base self-evaluator: it scores 18 of the 19 domain reference files against an anchored quality rubric — the quality rubric itself is excluded, since scoring the scoring instrument is circular — and polls named public sources (UN EGDI, EU eGov Benchmark, OECD DGI, Digdir, WCAG, forvaltningsloven) to flag outdated `Sist oppdatert` markers.
`/okr:help` lists all 16 commands, 7 agents, and a recommended workflow keyed to where you are in the tertial cycle. `/okr:freshen-references` is a knowledge-base self-evaluator: it scores 18 of the 19 domain reference files against an anchored quality rubric — the quality rubric itself is excluded, since scoring the scoring instrument is circular — and polls named public sources (UN EGDI, EU eGov Benchmark, OECD DGI, Digdir, WCAG, forvaltningsloven) to flag outdated `Sist oppdatert` markers.
---

View file

@ -20,7 +20,7 @@
// Exit: 0 = rapport produsert · 1 = uventet feil (inkl. confinement-brudd)
// 2 = bruksfeil (manglende/ukjent sti)
import { writeFileSync, renameSync, existsSync, statSync } from 'node:fs';
import { writeFileSync, renameSync, existsSync, statSync, realpathSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@ -55,7 +55,25 @@ if (isMain) {
try {
const ut = byggRapport(bundleRoot);
if (rapportFil) {
writeAtomic(path.resolve(rapportFil), ut);
// Haandhever loeftet i headeren: rapporten skal ALDRI havne i bundlen den
// vurderer. En rapport i treet ville blitt klassifisert av neste kjoering
// (selv-forurensning), og et loefte som bare staar i prosa er ikke et
// loefte -- samme standard som tool-lista i commands/arkivklar.md.
//
// BEGGE sider maa realpath-es, ellers er sammenligningen verdiloes: paa
// macOS er /var en symlenke til /private/var, saa en realpath-et rot mot
// en bare resolvet maal-sti sammenligner to skrivemaater av samme sted og
// slipper alt gjennom. Maalet finnes ikke ennaa, saa det er FORELDRE-
// katalogen som realpath-es -- samme grep som writeConfined i
// lib/innboks-write.mjs.
const maalDir = realpathSync(path.dirname(path.resolve(rapportFil)));
const maal = path.join(maalDir, path.basename(rapportFil));
const rot = realpathSync(path.resolve(bundleRoot));
if (maal === rot || maal.startsWith(rot + path.sep)) {
process.stderr.write(`arkivklar: rapporten kan ikke skrives inne i bundlen som vurderes: ${rapportFil}\n`);
process.exit(1);
}
writeAtomic(maal, ut);
process.stdout.write(`Arkivklar: vurderingsgrunnlag skrevet til ${rapportFil}\n`);
} else {
process.stdout.write(ut);

View file

@ -18,7 +18,7 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync, symlinkSync } from 'node:fs';
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, rmSync, symlinkSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
@ -363,3 +363,30 @@ test('(19d) GDPR-posisjonen staar i governance og ender paa «behold, men skjerm
'Datatilsynets vedtakskompetanse skal ikke siteres med paragrafnummer (kun delvis verifisert)',
);
});
test('(18j) CLI nekter aa skrive rapporten inn i bundlen den vurderer', () => {
// Skriptets header lover at rapporten ALDRI havner i bundlen. Prosa som lover
// noe koden ikke haandhever er nettopp det (19a) avviser for allowed-tools --
// samme standard maa gjelde her. Rapporten i treet ville dessuten blitt
// klassifisert av neste kjoering, altsaa selv-forurensning.
//
// Kjoerer mot en TMP-bundle, ikke den delte fixturen: i RED-fasen (foer vakten
// fantes) skrev nettopp denne casen `rapport.md` inn i tests/fixtures/ og
// roednet okf-check-suiten. Det er STATE-gotchaen «CLI-VERIFY SKRIVER I
// FIXTUREN» -- en test som kan skitne til delt state, gjoer det til slutt.
withTmp((tmp) => {
const rot = join(tmp, 'bundle');
mkdirSync(join(rot, 'dokumenter'), { recursive: true });
writeFileSync(join(rot, 'dokumenter', 'notat.md'), '---\ntype: Notat\n---\n\n# Notat\n');
const r = kjor([rot, join(rot, 'rapport.md')]);
assert.equal(r.code, 1, `forventet exit 1, fikk ${r.code}`);
assert.match(r.stderr ?? '', /inne i bundlen/i, 'feilmeldingen skal si hvorfor');
assert.ok(!existsSync(join(rot, 'rapport.md')), 'ingenting skal vaere skrevet i bundlen');
// Utenfor bundlen skal det fortsatt gaa fint.
const ok = kjor([rot, join(tmp, 'rapport.md')]);
assert.equal(ok.code, 0, `rapport utenfor bundlen skal gaa fint, fikk ${ok.code}`);
assert.ok(existsSync(join(tmp, 'rapport.md')), 'rapporten skal vaere skrevet utenfor bundlen');
});
});

View file

@ -451,15 +451,27 @@ test('(g3) dimensjonstall i kvalitet + kvalitetssjekker == faktisk antall i rubr
assert.deepEqual(feilAnkertall, [], `hver dimensjon skal ha fem ankere:\n${feilAnkertall.join('\n')}`);
// Drift-laasen: hvert tall prosaen oppgir maa matche den utledede tellingen.
//
// Aksene daekker BEGGE spraak. SKILL.md er engelsk etter policy (CLAUDE.md
// «SKILL.md: English»), saa en rent norsk akse ville vaert blind for
// «all 10 dimensions» selv med SKILL.md i fil-lista. Mutasjons-verifisert:
// med kun de norske aksene slapp nettopp den formen gjennom.
const akser = [
[/(\d+)\s+dimensjonene?\b/gi, total, 'totalt antall dimensjoner'],
[/(\d+)\s+(?:dimensjonene?|dimensions?)\b/gi, total, 'totalt antall dimensjoner'],
[/\bObjective\s*\((\d+)\)/g, obj, 'Objective-gruppa'],
[/\bKey Result\s*\((\d+)\)/g, kr, 'Key Result-gruppa'],
[/\bde (\d+) Objective-dimensjonene\b/g, obj, 'Objective-gruppa'],
[/\bde (\d+) Key Result-dimensjonene\b/g, kr, 'Key Result-gruppa'],
[/\b(?:de|the)\s+(\d+)\s+Objective[-\s]dimensjonene?\b/gi, obj, 'Objective-gruppa'],
[/\b(?:de|the)\s+(\d+)\s+Key Result[-\s]dimensjonene?\b/gi, kr, 'Key Result-gruppa'],
[/\b(?:de|the)\s+(\d+)\s+Objective\s+dimensions?\b/gi, obj, 'Objective-gruppa (en)'],
[/\b(?:de|the)\s+(\d+)\s+Key Result\s+dimensions?\b/gi, kr, 'Key Result-gruppa (en)'],
];
// SKILL.md er med selv om den er ENGELSK og i dag ikke oppgir noe
// dimensjonstall. Den staar her av samme grunn som R4 la den inn i
// canonScan() (se :36-39): SKILL.md er nettopp flata drift har overlevd paa
// foer, og et engelsk «all 11 dimensions» ville vaert usynlig for enhver
// norsk grep. Aksene under matcher begge spraak.
const drift = [];
for (const f of ['commands/kvalitet.md', 'agents/kvalitetssjekker-agent.md']) {
for (const f of ['commands/kvalitet.md', 'agents/kvalitetssjekker-agent.md', SKILL]) {
const body = readDoc(f);
for (const [re, forventet, hva] of akser) {
for (const m of body.matchAll(re)) {
@ -470,6 +482,33 @@ test('(g3) dimensjonstall i kvalitet + kvalitetssjekker == faktisk antall i rubr
assert.deepEqual(drift, [], `dimensjonstall-drift (rubrikkfila = ${obj}+${kr}=${total}):\n${drift.join('\n')}`);
});
// --- (g5) kommando-antall drift-laas (D7 steg 19 / D6-beslutning 7) ---
//
// README.md:172 sa «all 14 commands» mens treet hadde 15 -- utdatert FOER denne
// oekten, og steg 19 gjorde den ett verre ved aa legge til /okr:arkivklar.
// Samme defektklasse som help.md/README.md-glippen i steg 12 (fikset i d2ac153):
// en levert kommandoform skal synes paa ALLE flater samtidig.
//
// Tallet utledes av commands/*.md -- aldri hardkodet, jf. (d) F-g.
test('(g5) prosa-omtaler av kommando-antall == faktisk antall commands/*.md', () => {
const antall = mdFiles('commands').length;
assert.ok(antall >= 10, `parser-sanity: fant ${antall} kommandofiler`);
// Tabellene som ER kommandolista skal ha en rad per fil.
for (const f of ['commands/help.md', 'CLAUDE.md']) {
const rader = readDoc(f).split('\n').filter((l) => /^\|\s*`\/okr:/.test(l)).length;
assert.equal(rader, antall, `${f}: ${rader} tabellrader != ${antall} kommandofiler`);
}
const drift = [];
for (const f of ['README.md', 'commands/help.md', 'CLAUDE.md', SKILL]) {
for (const m of readDoc(f).matchAll(/\b(\d+)\s+(?:commands|kommandoer)\b/gi)) {
if (Number(m[1]) !== antall) drift.push(`${f}: "${m[0].trim()}" != ${antall}`);
}
}
assert.deepEqual(drift, [], `kommando-antall drift (telt = ${antall}):\n${drift.join('\n')}`);
});
// --- (g4) guardrail som KR-designmoenster (D7 steg 16) ---
//
// Baseline foer steget: 0 treff paa "guardrail" i commands/, 2 i okr-antipatterns.md