feat(okr): sandbagging-vakt i alle tre rapportformene

This commit is contained in:
Kjell Tore Guttormsen 2026-08-02 06:57:53 +02:00
commit 47e7ac9e2a
3 changed files with 180 additions and 31 deletions

View file

@ -203,7 +203,7 @@ function seksjonsKropp(md, overskrift) {
test('(8a) del III bygger paa aarsrapportens eget anker og en seksjon per Objective', () => {
const md = delIII();
assert.match(md, /[Aa]arets aktiviteter og resultater/, 'del III-ankeret mangler');
const objectives = md.split('\n').filter((l) => /^##\s+(?!Flerarig|Aspirational)/.test(l));
const objectives = md.split('\n').filter((l) => /^##\s+(?!Flerarig|Aspirational|Avvik)/.test(l));
assert.equal(objectives.length, 2, `forventet 2 Objective-seksjoner, fant ${objectives.length}`);
assert.match(md, /Trafikksikkerhet og tunnelsikkerhet/);
assert.match(md, /Digitalisering av tjenester/);
@ -302,6 +302,99 @@ test('(9d) etatsstyringsunderlag kaster ved tom eller ufullstendig syklus', () =
assert.throws(() => etatsstyringsunderlag(undefined), /syklus/i);
});
// --- (10) Sandbagging-vakten (steg 13) ---
//
// Briefens navngitte suksesskriterium: en aspirational-OKR med lav score
// presenteres ALDRI som avvik. Regelen staar i okr-offentlig-governance.md:148-156;
// her far den mekanisk handhevelse i alle tre rapportformene.
//
// Vakten er bygget i tre ledd, og rekkefolgen er ikke tilfeldig:
// (10a) POSITIV -- et committed KR under kravet STAAR i avviks-seksjonen.
// Uten dette leddet ville (10b) bestaatt trivielt paa en
// seksjon som ikke finnes eller er tom, og vakten vaert prosa.
// (10b) NEGATIV -- fixturens aspirational-KR med lav score staar der ikke.
// (10c) MUTASJON -- samme KR, flyttet til committed, DUKKER OPP. Det beviser at
// filteret henger paa TYPEN og ikke paa navnet, og at
// seksjonslokatoren i (10b) faktisk fant noe aa lete i.
//
// De tre formene har strukturelt ulike avviks-regioner, saa lokatoren ankrer paa
// overskriftsformen (`## Avvik...`), som er felles.
function avvikKropp(md) {
const overskrift = md.split('\n').find((l) => /^##\s+Avvik/.test(l));
assert.ok(overskrift, 'rapportformen har ingen avviks-seksjon aa filtrere');
const kropp = seksjonsKropp(md, overskrift.trim());
assert.ok(kropp && kropp.length > 0, 'avviks-seksjonen er tom');
return kropp;
}
// Fixturens KR-er, med de rollene steg 7 ga dem.
const COMMITTED_UNDER_KRAV = 'Tunneler med oppgradert sikkerhetsutrustning';
const ASPIRATIONAL_LAV_SCORE = ['Andel digitale soeknader', 'Gjennomsnittlig saksbehandlingstid'];
for (const [navn, bygger] of FORMER) {
test(`(10a/${navn}) committed KR under kravet STAAR i avviks-seksjonen`, () => {
const kropp = avvikKropp(byggFor(bygger));
assert.match(
kropp,
new RegExp(COMMITTED_UNDER_KRAV),
`committed KR under kravet mangler i avviks-seksjonen:\n${kropp}`,
);
});
test(`(10b/${navn}) aspirational KR med lav score staar IKKE i avviks-seksjonen`, () => {
const kropp = avvikKropp(byggFor(bygger));
for (const kr of ASPIRATIONAL_LAV_SCORE) {
assert.doesNotMatch(
kropp,
new RegExp(kr),
`aspirational KR presentert som avvik -- sandbagging-vakten er brutt:\n${kropp}`,
);
}
});
test(`(10c/${navn}) mutasjon: samme KR som committed ER et avvik`, () => {
const syklus = lesSyklus(SYKLUS_DIR);
const mutert = {
...syklus,
okrer: syklus.okrer.map((okr) => ({
...okr,
krer: okr.krer.map((kr) => ({ ...kr, type: 'committed' })),
})),
};
const kropp = avvikKropp(bygger(mutert, { naa: NAA }));
for (const kr of ASPIRATIONAL_LAV_SCORE) {
assert.match(
kropp,
new RegExp(kr),
`filteret henger ikke paa typen -- KR-et uteble ogsaa som committed:\n${kropp}`,
);
}
});
// Uten den kausale begrunnelsen er vakten en norm mottakeren kan overproeve.
// Bogsnes' poeng (okr-framework.md:158-161) er at re-bundling av
// aspirational-scoren i rapporteringen henter sandbaggingen tilbake
// strukturelt -- det argumentet holder for en Riksrevisjon-leser.
test(`(10d/${navn}) rapporten baerer forventningen til aspirational, ikke bare tallet`, () => {
const md = byggFor(bygger);
assert.match(md, /0\.7/, 'forventningen om ~0.7 maa staa eksplisitt');
assert.match(md, /sandbagging/i, 'begrunnelsen for at lav aspirational-score ikke er avvik mangler');
});
}
// Generatoren handhever regelen; governance-fila baerer begrunnelsen. Staar
// begrunnelsen bare i koden, kan en leser overproeve vakten uten aa moete
// argumentet -- og det er nettopp en Riksrevisjon-vendt leser som vil sporre.
test('(10e) governance-fila begrunner regelen kausalt, ikke bare normativt', () => {
const doc = readFileSync(
join(ROOT, 'skills/okr-offentlig-sektor/references/okr-offentlig-governance.md'),
'utf8',
);
assert.match(doc, /re-bundl/i, 'den kausale mekanismen (re-bundling) mangler');
assert.match(doc, /okr-framework\.md/, 'begrunnelsen maa peke til kanon, ikke duplisere den');
});
// --- (2d) Avvik-kolonnen maa vaere retningsagnostisk ---
//
// `naa >= target` ser riktig ut for oppadgaaende maal og er FEIL for