feat(okr): sandbagging-vakt i alle tre rapportformene
This commit is contained in:
parent
052e4d4e86
commit
47e7ac9e2a
3 changed files with 180 additions and 31 deletions
|
|
@ -97,6 +97,62 @@ function aspirationalSnitt(okrer) {
|
||||||
return `**Snitt aspirational: ${snitt.toFixed(2)}** over ${scorer.length} KR${hale}.`;
|
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
|
// Felles inngangsvakt for alle rapportformene. En form som stille rapporterte
|
||||||
// en tom syklus ville produsert et styringsdokument uten innhold, som ser
|
// en tom syklus ville produsert et styringsdokument uten innhold, som ser
|
||||||
// komplett ut.
|
// komplett ut.
|
||||||
|
|
@ -167,13 +223,14 @@ export function tertialrapport(syklus, opts = {}) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ut.push(...avvikSeksjon(syklus.okrer, 'Avvik som skal forklares'));
|
||||||
|
|
||||||
if (aspirational.antallKr > 0) {
|
if (aspirational.antallKr > 0) {
|
||||||
ut.push(
|
ut.push(
|
||||||
'## Aspirational Key Results',
|
'## Aspirational Key Results',
|
||||||
'',
|
'',
|
||||||
// Governance-invariant 1: typen merkes, slik at 0.7 ikke leses som svikt.
|
// 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',
|
...ASPIRATIONAL_RAMME,
|
||||||
'paa snittet paa tvers av alle aspirational-OKR -- ikke paa ett enkelt KR.',
|
|
||||||
'',
|
'',
|
||||||
...aspirational.linjer,
|
...aspirational.linjer,
|
||||||
);
|
);
|
||||||
|
|
@ -276,6 +333,8 @@ export function arsrapportDelIII(syklus, opts = {}) {
|
||||||
ut.push('### Vurdering av maaloppnaaelse', '', ...VURDERINGSFELT, '');
|
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
|
// Snittet hoerer PAA TVERS av Objectives (okr-framework.md:557) og staar derfor
|
||||||
// ikke i noen enkelt Objective-seksjon. Committed har bevisst ingen motpart:
|
// ikke i noen enkelt Objective-seksjon. Committed har bevisst ingen motpart:
|
||||||
// et snitt av binaere krav er ikke en stoerrelse.
|
// et snitt av binaere krav er ikke en stoerrelse.
|
||||||
|
|
@ -284,8 +343,7 @@ export function arsrapportDelIII(syklus, opts = {}) {
|
||||||
ut.push(
|
ut.push(
|
||||||
'## Aspirational maaloppnaaelse paa tvers av Objectives',
|
'## Aspirational maaloppnaaelse paa tvers av Objectives',
|
||||||
'',
|
'',
|
||||||
'Aspirational KR forventes aa lande rundt 0.7 med hoey varians, og vurderes',
|
...ASPIRATIONAL_RAMME,
|
||||||
'paa snittet -- ikke paa ett enkelt KR.',
|
|
||||||
'',
|
'',
|
||||||
snitt,
|
snitt,
|
||||||
'',
|
'',
|
||||||
|
|
@ -309,23 +367,6 @@ export function arsrapportDelIII(syklus, opts = {}) {
|
||||||
// og form fastsettes i departementets hovedinstruks. Et underlag som foregrep
|
// og form fastsettes i departementets hovedinstruks. Et underlag som foregrep
|
||||||
// kadensen ville gjeninnfoert den samme uverifiserte paastanden i maskinform.
|
// 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().
|
* 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(...avvikSeksjon(syklus.okrer, 'Avvik som krever departementets oppmerksomhet'));
|
||||||
ut.push(
|
|
||||||
'## Avvik som krever departementets oppmerksomhet',
|
// Forventningen staar rett under avvikslisten, der spoersmaalet «hvorfor er
|
||||||
'',
|
// ikke DETTE et avvik?» faktisk oppstaar hos mottakeren.
|
||||||
...(avvik.length > 0
|
ut.push('## Aspirational Key Results - forventning', '', ...ASPIRATIONAL_RAMME, '');
|
||||||
? avvik
|
|
||||||
: ['Ingen committed KR ligger under kravet i denne syklusen.']),
|
|
||||||
'',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Hjemmelen for at tildelingsbrevet SKAL inneholde styringsparametere er
|
// Hjemmelen for at tildelingsbrevet SKAL inneholde styringsparametere er
|
||||||
// bestemmelser om oekonomistyring i staten («bestemmelsene») punkt 1.5,
|
// bestemmelser om oekonomistyring i staten («bestemmelsene») punkt 1.5,
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,25 @@ Tre regler:
|
||||||
skalaen.** En leser som tror 0.7 betyr «70 % av målet» trekker feil konklusjon om
|
skalaen.** En leser som tror 0.7 betyr «70 % av målet» trekker feil konklusjon om
|
||||||
både ambisjonsnivå og resultat.
|
både ambisjonsnivå og resultat.
|
||||||
|
|
||||||
|
#### Hvorfor regelen er strukturell, ikke bare god skikk
|
||||||
|
|
||||||
|
De tre reglene kan leses som hensynsfull formidling. Det er de ikke. Den kausale
|
||||||
|
begrunnelsen ligger i target/forecast-separasjonen — se `okr-framework.md`,
|
||||||
|
«Beyond Budgeting og target/forecast-separasjonen», som er kanonisk for mekanismen.
|
||||||
|
|
||||||
|
Kort: **får aspirational-scoren konsekvenser i rapporteringen, er målet og prognosen
|
||||||
|
re-bundlet til ett tall.** Da har den som setter målet neste syklus en egeninteresse i
|
||||||
|
å sette det lavt — og sandbagging følger av systemet, ikke av manglende disiplin hos
|
||||||
|
den enkelte. En avviksliste som tar med aspirational-KR er derfor ikke først og fremst
|
||||||
|
urimelig mot årets team; den svekker neste års målsetting.
|
||||||
|
|
||||||
|
Det er også svaret til en revisor som spør hvorfor et aspirational-KR med lav score
|
||||||
|
ikke står oppført som avvik: fordi det aldri var et krav, og fordi å behandle det som
|
||||||
|
ett ville gjort neste års mål mindre ambisiøse. Merk at dette *ikke* er et argument
|
||||||
|
for å slippe å forklare lav score — aspirational-resultater skal analyseres og læres
|
||||||
|
av (se del III over). Det er et argument for at analysen hører hjemme et annet sted
|
||||||
|
enn i avvikslisten.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Gevinstrealisering
|
## Gevinstrealisering
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,7 @@ function seksjonsKropp(md, overskrift) {
|
||||||
test('(8a) del III bygger paa aarsrapportens eget anker og en seksjon per Objective', () => {
|
test('(8a) del III bygger paa aarsrapportens eget anker og en seksjon per Objective', () => {
|
||||||
const md = delIII();
|
const md = delIII();
|
||||||
assert.match(md, /[Aa]arets aktiviteter og resultater/, 'del III-ankeret mangler');
|
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.equal(objectives.length, 2, `forventet 2 Objective-seksjoner, fant ${objectives.length}`);
|
||||||
assert.match(md, /Trafikksikkerhet og tunnelsikkerhet/);
|
assert.match(md, /Trafikksikkerhet og tunnelsikkerhet/);
|
||||||
assert.match(md, /Digitalisering av tjenester/);
|
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);
|
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 ---
|
// --- (2d) Avvik-kolonnen maa vaere retningsagnostisk ---
|
||||||
//
|
//
|
||||||
// `naa >= target` ser riktig ut for oppadgaaende maal og er FEIL for
|
// `naa >= target` ser riktig ut for oppadgaaende maal og er FEIL for
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue