feat(okr): aarsrapport del III-generator
This commit is contained in:
parent
8ef8f7a405
commit
77031b0c9a
3 changed files with 384 additions and 65 deletions
|
|
@ -56,6 +56,11 @@ function krRad(kr, erCommitted) {
|
|||
return `| ${kr.navn} | ${kr.baseline} | ${kr.target} | ${kr.naa} | ${score} | ${formatAvvik(kr, erCommitted)} | |`;
|
||||
}
|
||||
|
||||
const krTabell = (krer, erCommitted) => [
|
||||
...TABELLHODE,
|
||||
...krer.map((kr) => krRad(kr, erCommitted)),
|
||||
];
|
||||
|
||||
// Ett avsnitt per OKR, med KR-ene som tabellrader. Rekkefolgen arves fra
|
||||
// lesSyklus (sortert filnavn) og er del av determinisme-kontrakten.
|
||||
function seksjon(okrer, type, erCommitted) {
|
||||
|
|
@ -65,9 +70,7 @@ function seksjon(okrer, type, erCommitted) {
|
|||
const krer = okr.krer.filter((kr) => kr.type === type);
|
||||
if (krer.length === 0) continue;
|
||||
antallKr += krer.length;
|
||||
linjer.push(`### ${okr.tittel}`, '', ...TABELLHODE);
|
||||
for (const kr of krer) linjer.push(krRad(kr, erCommitted));
|
||||
linjer.push('');
|
||||
linjer.push(`### ${okr.tittel}`, '', ...krTabell(krer, erCommitted), '');
|
||||
}
|
||||
return { linjer, antallKr };
|
||||
}
|
||||
|
|
@ -94,6 +97,36 @@ function aspirationalSnitt(okrer) {
|
|||
return `**Snitt aspirational: ${snitt.toFixed(2)}** over ${scorer.length} KR${hale}.`;
|
||||
}
|
||||
|
||||
// Felles inngangsvakt for alle rapportformene. En form som stille rapporterte
|
||||
// en tom syklus ville produsert et styringsdokument uten innhold, som ser
|
||||
// komplett ut.
|
||||
function krevSyklus(syklus, hvor) {
|
||||
if (!syklus || typeof syklus !== 'object' || !Array.isArray(syklus.okrer)) {
|
||||
throw new Error(`${hvor}: forventet en syklus fra lesSyklus()`);
|
||||
}
|
||||
if (syklus.okrer.length === 0) {
|
||||
throw new Error(`${hvor}: syklusen ${syklus.id ?? ''} inneholder ingen OKR`.trim());
|
||||
}
|
||||
}
|
||||
|
||||
// Skala- og confidence-avsnittet er felles for alle formene, ikke fordi det
|
||||
// sparer linjer, men fordi de tre governance-invariantene ikke kan gjelde bare
|
||||
// den ene rapporten mottakeren tilfeldigvis leser.
|
||||
const SKALAFORKLARING = [
|
||||
// Governance-invariant 3: skalaen forklares, og score presenteres ALDRI som
|
||||
// prosent maaloppnaaelse -- en leser som tror 0.7 betyr "70 % av maalet"
|
||||
// trekker feil konklusjon om baade ambisjonsniva og resultat.
|
||||
'Score er en andel paa skalaen 0 til 1.0, beregnet som',
|
||||
'(naa - baseline) / (target - baseline). Den er ikke prosent maaloppnaaelse.',
|
||||
`Et KR der target er lik baseline har ingen definert andel og staar som ${UDEFINERT}.`,
|
||||
'',
|
||||
'Confidence fylles ut av /okr:sporing. Den utledes ikke av tallene her --',
|
||||
'confidence er en sannsynlighetsvurdering, ikke en funksjon av score.',
|
||||
'',
|
||||
];
|
||||
|
||||
const klokke = (opts) => opts.naa || process.env.OKR_NOW || new Date().toISOString();
|
||||
|
||||
/**
|
||||
* Bygger tertialrapporten for en syklus lest av lesSyklus().
|
||||
*
|
||||
|
|
@ -103,14 +136,9 @@ function aspirationalSnitt(okrer) {
|
|||
* @returns {string} markdown
|
||||
*/
|
||||
export function tertialrapport(syklus, opts = {}) {
|
||||
if (!syklus || typeof syklus !== 'object' || !Array.isArray(syklus.okrer)) {
|
||||
throw new Error('tertialrapport: forventet en syklus fra lesSyklus()');
|
||||
}
|
||||
if (syklus.okrer.length === 0) {
|
||||
throw new Error(`tertialrapport: syklusen ${syklus.id ?? ''} inneholder ingen OKR`.trim());
|
||||
}
|
||||
krevSyklus(syklus, 'tertialrapport');
|
||||
|
||||
const naa = opts.naa || process.env.OKR_NOW || new Date().toISOString();
|
||||
const naa = klokke(opts);
|
||||
|
||||
const committed = seksjon(syklus.okrer, 'committed', true);
|
||||
const aspirational = seksjon(syklus.okrer, 'aspirational', false);
|
||||
|
|
@ -124,16 +152,7 @@ export function tertialrapport(syklus, opts = {}) {
|
|||
'',
|
||||
`Generert: ${naa}`,
|
||||
'',
|
||||
// Governance-invariant 3: skalaen forklares, og score presenteres ALDRI som
|
||||
// prosent maaloppnaaelse -- en leser som tror 0.7 betyr "70 % av maalet"
|
||||
// trekker feil konklusjon om baade ambisjonsniva og resultat.
|
||||
'Score er en andel paa skalaen 0 til 1.0, beregnet som',
|
||||
'(naa - baseline) / (target - baseline). Den er ikke prosent maaloppnaaelse.',
|
||||
`Et KR der target er lik baseline har ingen definert andel og staar som ${UDEFINERT}.`,
|
||||
'',
|
||||
'Confidence fylles ut av /okr:sporing. Den utledes ikke av tallene her --',
|
||||
'confidence er en sannsynlighetsvurdering, ikke en funksjon av score.',
|
||||
'',
|
||||
...SKALAFORKLARING,
|
||||
];
|
||||
|
||||
if (committed.antallKr > 0) {
|
||||
|
|
@ -164,3 +183,116 @@ export function tertialrapport(syklus, opts = {}) {
|
|||
|
||||
return `${ut.join('\n').replace(/\n+$/, '')}\n`;
|
||||
}
|
||||
|
||||
// --- Aarsrapport del III (steg 11) ---
|
||||
//
|
||||
// Del III «Aarets aktiviteter og resultater» er hovedplassen for OKR i den
|
||||
// statlige aarsrapporten (okr-offentlig-governance.md:125-133).
|
||||
//
|
||||
// ANTIPATTERNET generatoren maa unngaa, ordrett fra primaerkilden --
|
||||
// Riksrevisjonen (2020), «Undersoekelse av etats- og virksomhetsstyringen av
|
||||
// Norsk institutt for biooekonomi (NIBIO)», del av Dokument 1 (2020-2021) s. 107:
|
||||
// framstillingen «synliggjoer i mindre grad NIBIOs analyser av maaloppnaaelsen og
|
||||
// framstaar som et oeyeblikksbilde av NIBIOs aktiviteter og resultater». Saken ble
|
||||
// senere avsluttet etter forbedringer i maal- og resultatstyringen (gjengitt i
|
||||
// DFOe-notat 2026:2 s. 48) -- funnet er en historisk dokumentert svakhet
|
||||
// forvaltningen har rettet, ikke gjeldende kritikk av NIBIO.
|
||||
//
|
||||
// Konsekvensen for koden er konkret: en generator som bare dumper KR-tabeller
|
||||
// PRODUSERER nettopp det oeyeblikksbildet. Den reserverer derfor plass til
|
||||
// vurderingen av maaloppnaaelse per Objective -- og fyller den aldri selv, av
|
||||
// samme grunn som den ikke setter confidence: vurderingen krever skjoenn.
|
||||
const VURDERINGSFELT = [
|
||||
'[Fylles ut av virksomheten: analysen av maaloppnaaelsen for dette Objectivet --',
|
||||
'hva tallene over betyr, hva som forklarer avvikene, og hva som er laert.',
|
||||
'Generatoren fyller ikke feltet; en vurdering av maaloppnaaelse krever skjoenn.',
|
||||
'Uten analysen staar del III igjen som et oeyeblikksbilde av aktiviteter og',
|
||||
'resultater, som er nettopp det revisjonen har paapekt. Slett klammene naar',
|
||||
'feltet er fylt ut.]',
|
||||
];
|
||||
|
||||
// DFOe-notat 2026:2 kap. 5.3.3 (s. 61): virksomhetene «skal planlegge med baade
|
||||
// ettaarig og flerarig perspektiv», men «i tildelingsbrevene og aarsrapportene vi
|
||||
// har sett paa omtales imidlertid i liten grad det ettaarige i et flerarig
|
||||
// perspektiv». Generatoren PEKER derfor paa materialet fra tidligere sykluser --
|
||||
// den regner ikke trend paa tvers av dem. Historikk-filene baerer retrospektiv-
|
||||
// prosa, ikke KR-tall; en beregnet utvikling ville vaert oppdiktet.
|
||||
function flerarigSeksjon(historikk) {
|
||||
if (historikk.length === 0) return [];
|
||||
return [
|
||||
'## Flerarig perspektiv',
|
||||
'',
|
||||
'Virksomheten skal planlegge med baade ettaarig og flerarig perspektiv.',
|
||||
'Materialet fra tidligere sykluser ligger i historikk-katalogen:',
|
||||
'',
|
||||
...historikk.map((h) => `- ${h.tittel} (${h.fil})`),
|
||||
'',
|
||||
'[Fylles ut av virksomheten: utviklingen sett over tid. Generatoren viser',
|
||||
'hvilket materiale som finnes, og sammenligner ikke sykluser den ikke har',
|
||||
'tall fra.]',
|
||||
'',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bygger aarsrapportens del III for en syklus lest av lesSyklus().
|
||||
*
|
||||
* @param {object} syklus struktur fra lesSyklus()
|
||||
* @param {{naa?: string, historikk?: Array<{tittel: string, fil: string}>}} [opts]
|
||||
* historikk leses av kalleren (scripts/syklus-rapport.mjs), ikke her -- modulen
|
||||
* holdes fri for disk, saa formen er testbar uten et tre paa filsystemet.
|
||||
* @returns {string} markdown
|
||||
*/
|
||||
export function arsrapportDelIII(syklus, opts = {}) {
|
||||
krevSyklus(syklus, 'arsrapportDelIII');
|
||||
|
||||
const naa = klokke(opts);
|
||||
const historikk = Array.isArray(opts.historikk) ? opts.historikk : [];
|
||||
const antallKr = syklus.okrer.reduce((n, okr) => n + okr.krer.length, 0);
|
||||
if (antallKr === 0) {
|
||||
throw new Error(`arsrapportDelIII: syklusen ${syklus.id} inneholder ingen KR aa rapportere`);
|
||||
}
|
||||
|
||||
const ut = [
|
||||
`# Aarsrapport del III - Aarets aktiviteter og resultater (${syklus.id})`,
|
||||
'',
|
||||
`Generert: ${naa}`,
|
||||
'',
|
||||
...SKALAFORKLARING,
|
||||
];
|
||||
|
||||
// Per Objective, fordi del III dokumenterer maaloppnaaelse mot tildelingsbrevets
|
||||
// krav -- og kravene henger paa Objectives, ikke paa rapportformen.
|
||||
for (const okr of syklus.okrer) {
|
||||
ut.push(`## ${okr.tittel}`, '');
|
||||
for (const [type, erCommitted, overskrift] of [
|
||||
['committed', true, '### Committed Key Results'],
|
||||
['aspirational', false, '### Aspirational Key Results'],
|
||||
]) {
|
||||
const krer = okr.krer.filter((kr) => kr.type === type);
|
||||
if (krer.length === 0) continue;
|
||||
ut.push(overskrift, '', ...krTabell(krer, erCommitted), '');
|
||||
}
|
||||
ut.push('### Vurdering av maaloppnaaelse', '', ...VURDERINGSFELT, '');
|
||||
}
|
||||
|
||||
// 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.
|
||||
const snitt = aspirationalSnitt(syklus.okrer);
|
||||
if (snitt) {
|
||||
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.',
|
||||
'',
|
||||
snitt,
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
ut.push(...flerarigSeksjon(historikk));
|
||||
|
||||
return `${ut.join('\n').replace(/\n+$/, '')}\n`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue