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

@ -97,6 +97,62 @@ function aspirationalSnitt(okrer) {
return `**Snitt aspirational: ${snitt.toFixed(2)}** over ${scorer.length} KR${hale}.`;
}
// --- Sandbagging-vakten (steg 13): felles for alle tre rapportformene ---
//
// Regelen: en aspirational-OKR med lav score presenteres ALDRI som avvik
// (okr-offentlig-governance.md:148-156). Den er kodet ETT sted og brukes av alle
// formene, fordi en regel som maa gjentas per form er en regel som glipper i den
// fjerde.
//
// Begrunnelsen er kausal, ikke normativ (okr-framework.md:158-161, Bogsnes):
// faar aspirational-scoren konsekvenser i rapporteringen, er target og forecast
// re-bundlet -- og da kommer sandbaggingen tilbake av strukturell noedvendighet,
// uansett hvor disiplinert den som setter maalet er. Vakten beskytter altsaa
// ikke aspirational-KR-et; den beskytter maalsettingen i neste syklus.
// Avvik er committed-only. Predikatet er det samme som Avvik-kolonnen bruker --
// via beregnScore, aldri naa >= target -- saa listen og kolonnen ikke kan gi
// ulikt svar om samme KR.
function avvikListe(okrer) {
const rader = [];
for (const okr of okrer) {
for (const kr of okr.krer) {
if (kr.type !== 'committed' || formatAvvik(kr, true) !== 'Ja') continue;
rader.push(
`- ${okr.tittel}: ${kr.navn} -- naa ${kr.naa} mot krav ${kr.target} `
+ `(score ${formatScore(beregnScore(kr))})`,
);
}
}
return rader;
}
// Overskriften varierer med mottakeren; filteret gjoer det ikke.
function avvikSeksjon(okrer, overskrift) {
const rader = avvikListe(okrer);
return [
`## ${overskrift}`,
'',
'Kun committed KR staar her: et committed krav er naadd eller ikke. Aspirational',
'KR er holdt utenfor med vilje -- se forventningen til aspirational under.',
'',
...(rader.length > 0 ? rader : ['Ingen committed KR ligger under kravet i denne syklusen.']),
'',
];
}
// Forventningsteksten hoerer i rapporten, ikke bare i koden: mottakeren i
// departementet leser ikke 0.7 som suksess med mindre det staar eksplisitt
// (governance-regel 1).
const ASPIRATIONAL_RAMME = [
'Aspirational KR forventes aa lande rundt 0.7 med hoey varians, og vurderes',
'paa snittet paa tvers av alle aspirational-OKR -- ikke paa ett enkelt KR.',
'En lav score er derfor maaloppnaaelse, ikke svikt, og et aspirational KR staar',
'aldri i avviks-seksjonen. Fikk lav aspirational-score konsekvenser i',
'rapporteringen, ville maal og prognose vaert bundlet sammen igjen -- og',
'sandbagging fulgt strukturelt, ikke som et disiplinproblem.',
];
// Felles inngangsvakt for alle rapportformene. En form som stille rapporterte
// en tom syklus ville produsert et styringsdokument uten innhold, som ser
// komplett ut.
@ -167,13 +223,14 @@ export function tertialrapport(syklus, opts = {}) {
);
}
ut.push(...avvikSeksjon(syklus.okrer, 'Avvik som skal forklares'));
if (aspirational.antallKr > 0) {
ut.push(
'## Aspirational Key Results',
'',
// Governance-invariant 1: typen merkes, slik at 0.7 ikke leses som svikt.
'Aspirational KR forventes aa lande rundt 0.7 med hoey varians, og vurderes',
'paa snittet paa tvers av alle aspirational-OKR -- ikke paa ett enkelt KR.',
...ASPIRATIONAL_RAMME,
'',
...aspirational.linjer,
);
@ -276,6 +333,8 @@ export function arsrapportDelIII(syklus, opts = {}) {
ut.push('### Vurdering av maaloppnaaelse', '', ...VURDERINGSFELT, '');
}
ut.push(...avvikSeksjon(syklus.okrer, 'Avvik som skal forklares'));
// Snittet hoerer PAA TVERS av Objectives (okr-framework.md:557) og staar derfor
// ikke i noen enkelt Objective-seksjon. Committed har bevisst ingen motpart:
// et snitt av binaere krav er ikke en stoerrelse.
@ -284,8 +343,7 @@ export function arsrapportDelIII(syklus, opts = {}) {
ut.push(
'## Aspirational maaloppnaaelse paa tvers av Objectives',
'',
'Aspirational KR forventes aa lande rundt 0.7 med hoey varians, og vurderes',
'paa snittet -- ikke paa ett enkelt KR.',
...ASPIRATIONAL_RAMME,
'',
snitt,
'',
@ -309,23 +367,6 @@ export function arsrapportDelIII(syklus, opts = {}) {
// og form fastsettes i departementets hovedinstruks. Et underlag som foregrep
// kadensen ville gjeninnfoert den samme uverifiserte paastanden i maskinform.
// Avvik er committed-only. Predikatet er det samme som Avvik-kolonnen bruker --
// via beregnScore, aldri naa >= target -- saa listen og kolonnen ikke kan gi
// ulikt svar om samme KR.
function avvikListe(okrer) {
const rader = [];
for (const okr of okrer) {
for (const kr of okr.krer) {
if (kr.type !== 'committed' || formatAvvik(kr, true) !== 'Ja') continue;
rader.push(
`- ${okr.tittel}: ${kr.navn} -- naa ${kr.naa} mot krav ${kr.target} `
+ `(score ${formatScore(beregnScore(kr))})`,
);
}
}
return rader;
}
/**
* Bygger underlaget til et etatsstyringsmoete for en syklus lest av lesSyklus().
*
@ -366,15 +407,11 @@ export function etatsstyringsunderlag(syklus, opts = {}) {
}
}
const avvik = avvikListe(syklus.okrer);
ut.push(
'## Avvik som krever departementets oppmerksomhet',
'',
...(avvik.length > 0
? avvik
: ['Ingen committed KR ligger under kravet i denne syklusen.']),
'',
);
ut.push(...avvikSeksjon(syklus.okrer, 'Avvik som krever departementets oppmerksomhet'));
// Forventningen staar rett under avvikslisten, der spoersmaalet «hvorfor er
// ikke DETTE et avvik?» faktisk oppstaar hos mottakeren.
ut.push('## Aspirational Key Results - forventning', '', ...ASPIRATIONAL_RAMME, '');
// Hjemmelen for at tildelingsbrevet SKAL inneholde styringsparametere er
// bestemmelser om oekonomistyring i staten («bestemmelsene») punkt 1.5,