okr/lib/syklus-rapport.mjs
Kjell Tore Guttormsen 3a0cbcfbf9 feat(okr): deterministisk skriveledd for status.md (1.10.0)
Kjeden /okr:rapport -> /okr:sporing -> status.md var beskrevet i prosa, men
manglet et mekanisk ledd: ingenting foerte en generert rad inn i status.md --
fila coaching-hook teller at-risk-KR i, /okr:moeter bygger agenda fra, og
/okr:oppsett arkiver leser sluttscorer fra. Rot-aarsaken var at HELE
sporings-stien var lesende (ingen Write/Edit/Bash i allowed-tools).

scripts/syklus-status.mjs tar confidence som JSON paa stdin og bygger radene
gjennom statusrapport() i lib/syklus-rapport.mjs -- samme kode som
tertialrapporten. Aritmetikken kan derfor ikke drifte fra /okr:rapport, fordi
det ikke finnes en andre implementasjon av den. En LLM skriver ikke av et tall
paa veien, og en maskin utleder ikke et trafikklys: beslutning 9 staar, presist
formulert som at generatoren PLASSERER en etikett den faar inn som data og
UTLEDER aldri en.

Haandhevet i kode, ikke i prosa: lukket vokabular (On Track/At Risk/Off Track),
full dekning (en tom confidence-celle ville lest som groenn hos hver konsument),
typo-fangst paa ukjente KR-navn, og kuratert-fil-vern -- en status.md uten
provenans-noekkelen `kilde: sporing` er haandholdt og overskrives aldri (samme
mekanikk som innboks-write bruker med `kilde: innboks`).

Suite 329 -> 343. Fem vakter mutasjons-verifisert: rad-drift mot
tertialrapporten, vokab, dekning (baade alene og sammen med vokab-vakten som
overlapper den), og kuratert-vernet.
2026-08-10 20:56:26 +02:00

603 lines
24 KiB
JavaScript

// syklus-rapport.mjs
// D5 steg 9: tertialrapport fra en lest syklus. REN modul (ingen shebang, ingen
// isMain-CLI) -- orkestratoren bor i scripts/syklus-rapport.mjs.
// Zero npm dependencies.
//
// SEEMEN, og hvorfor den ligger der den ligger (beslutning B-3):
// generatoren eier ARITMETIKK, TABELLSTRUKTUR og FORMATERINGSINVARIANTER.
// Den eier ALDRI confidence. okr-framework.md:563 gjoer confidence-tabellen til
// eneste sannhetskilde og forbyr andre filer aa definere egne terskler;
// okr-calculator.md:249 avviser mekanisk utledning ordrett ("bruk gapet ... som
// ETT innspill til confidence-vurderingen, ikke som en mekanisk regel"). En
// generator som satte et trafikklys fra score ville derfor oppfunnet en terskel
// doktrinen forbyr. Confidence-kolonnen staar tom by design og fylles av
// /okr:sporing, der skjoennet hoerer hjemme.
//
// PLASSERE er ikke det samme som AA EIE (S60). `statusrapport` nederst i fila
// emitterer etiketter i Confidence-kolonnen -- men kun etiketter den faar inn
// som data fra /okr:sporing, og den kaster heller enn aa fylle en celle den
// ikke har faatt en verdi for. Skillet er hele poenget med beslutning 9:
// aritmetikken skal ikke re-typas av en LLM, og vurderingen skal ikke utledes
// av en maskin. Rapportformene (tertial/aarsrapport/etatsstyring) utleder
// aldri confidence, og vakt (5c) i tests/syklus-rapport.test.mjs holder dem
// til det. `statusrapport` staar bevisst UTENFOR den listen, fordi den ikke
// utleder noe -- den plasserer et skjoenn som allerede er utoevd.
//
// Committed-kolonnen "Avvik" er IKKE en terskel: den er sammenligningen
// naa >= target, som okr-offentlig-governance.md:150-152 krever ("et lovkrav er
// naadd eller ikke; score 0.9 paa et lovpaalagt krav er et avvik").
//
// Rapporten holdes ASCII-ren, som resten av den maskingenererte flaten.
import { beregnScore } from './syklus-data.mjs';
// Literal for score som ikke er definert som ratio (target == baseline, B-2).
// Et tomt felt ville lest som manglende data; "0" ville loeyet om at KR-en
// ikke har beveget seg.
const UDEFINERT = 'udefinert';
const TABELLHODE = [
'| KR | Baseline | Target | Naa | Score | Avvik | Confidence |',
'|----|----------|--------|-----|-------|-------|------------|',
];
const formatScore = (score) => (score === undefined ? UDEFINERT : score.toFixed(2));
// Governance-invariant 2: committed maales binaert mot kravet, ikke paa score.
// Aspirational har ingen Avvik-kolonneverdi -- 0.7 er forventet, ikke svikt.
//
// Predikatet gaar gjennom beregnScore, ALDRI gjennom `naa >= target` direkte:
// den sammenligningen er retningsavhengig og gir feil svar begge veier for
// nedadgaaende krav (krav 5 dager, naa 11 -> "ingen avvik"; krav 5, naa 4 ->
// "avvik"). beregnScore haandterer retningen allerede, fordi teller og nevner
// begge blir negative. Bonus: kolonnen kan da ikke motsi rapportens egen
// setning om at en score under 1.0 er et avvik.
const formatAvvik = (kr, erCommitted) => {
if (!erCommitted) return '-';
const score = beregnScore(kr);
// target == baseline: ingen ratio aa maale mot, men kravet er fortsatt et tall.
if (score === undefined) return kr.naa === kr.target ? 'Nei' : 'Ja';
return score >= 1 ? 'Nei' : 'Ja';
};
function krRad(kr, erCommitted, confidence = '') {
const score = formatScore(beregnScore(kr));
// Siste celle (Confidence) staar tom for rapportformene -- se seem-notatet
// over. Er den fylt, kommer verdien fra /okr:sporing via `statusrapport`, og
// resten av raden er byte for byte den samme.
const siste = confidence === '' ? ' ' : ` ${confidence} `;
return `| ${kr.navn} | ${kr.baseline} | ${kr.target} | ${kr.naa} | ${score} | ${formatAvvik(kr, erCommitted)} |${siste}|`;
}
const krTabell = (krer, erCommitted, hentConfidence = () => '') => [
...TABELLHODE,
...krer.map((kr) => krRad(kr, erCommitted, hentConfidence(kr))),
];
// 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, hentConfidence = () => '') {
const linjer = [];
let antallKr = 0;
for (const okr of okrer) {
const krer = okr.krer.filter((kr) => kr.type === type);
if (krer.length === 0) continue;
antallKr += krer.length;
linjer.push(`### ${okr.tittel}`, '', ...krTabell(krer, erCommitted, hentConfidence), '');
}
return { linjer, antallKr };
}
// Aspirational vurderes paa SNITTET paa tvers av alle aspirational-OKR, ikke
// paa ett enkelt KR (okr-framework.md:557). Snittet er derfor kanonisk for
// denne typen -- og finnes bevisst ikke som et felles tall paa tvers av typene.
// KR uten definert score kan ikke inngaa; antallet oppgis i stedet for aa
// forsvinne stille.
function aspirationalSnitt(okrer) {
const scorer = [];
let udefinerte = 0;
for (const okr of okrer) {
for (const kr of okr.krer) {
if (kr.type !== 'aspirational') continue;
const s = beregnScore(kr);
if (s === undefined) udefinerte += 1;
else scorer.push(s);
}
}
if (scorer.length === 0) return null;
const snitt = scorer.reduce((a, b) => a + b, 0) / scorer.length;
const hale = udefinerte > 0 ? ` (${udefinerte} KR uten definert score er holdt utenfor)` : '';
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;
}
const harAspirational = (okrer) => okrer.some((okr) => okr.krer.some((kr) => kr.type === 'aspirational'));
// Overskriften varierer med mottakeren; filteret gjoer det ikke.
//
// Kryssreferansen til aspirational-seksjonen settes bare naar den seksjonen
// faktisk kommer. En syklus med bare committed -- fullt lovlig, og typisk for en
// ren etterlevelses-syklus -- ville ellers faatt et styringsdokument som peker
// paa en seksjon som ikke er der.
function avvikSeksjon(okrer, overskrift) {
const rader = avvikListe(okrer);
return [
`## ${overskrift}`,
'',
'Kun committed KR staar her: et committed krav er naadd eller ikke.',
...(harAspirational(okrer)
? ['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.
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 SKALA_TALL = [
// 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}.`,
];
const SKALAFORKLARING = [
...SKALA_TALL,
'',
'Confidence fylles ut av /okr:sporing. Den utledes ikke av tallene her --',
'confidence er en sannsynlighetsvurdering, ikke en funksjon av score.',
'',
];
// Samme tall-avsnitt, men i statusrapporten ER confidence fylt. Setningen maa
// da si hvor den kom fra, ellers leser mottakeren et trafikklys uten aa vite om
// det er vurdert eller regnet ut.
const SKALAFORKLARING_STATUS = [
...SKALA_TALL,
'',
'Confidence er satt av /okr:sporing og gjengitt her uendret. Den er en',
'sannsynlighetsvurdering mot den kanoniske skalaen i okr-framework.md, ikke',
'en funksjon av score, og er aldri utledet av tallene i tabellene.',
'',
];
const klokke = (opts) => opts.naa || process.env.OKR_NOW || new Date().toISOString();
/**
* Bygger tertialrapporten for en syklus lest av lesSyklus().
*
* @param {object} syklus struktur fra lesSyklus()
* @param {{naa?: string}} [opts] naa overstyrer klokka; ellers OKR_NOW, ellers veggklokke
* (klokke-soem etter moenster fra scripts/compose-org-profile.mjs:61)
* @returns {string} markdown
*/
export function tertialrapport(syklus, opts = {}) {
krevSyklus(syklus, 'tertialrapport');
const naa = klokke(opts);
const committed = seksjon(syklus.okrer, 'committed', true);
const aspirational = seksjon(syklus.okrer, 'aspirational', false);
if (committed.antallKr + aspirational.antallKr === 0) {
throw new Error(`tertialrapport: syklusen ${syklus.id} inneholder ingen KR aa rapportere`);
}
const ut = [
`# Tertialrapport ${syklus.id}`,
'',
`Generert: ${naa}`,
'',
...SKALAFORKLARING,
];
if (committed.antallKr > 0) {
ut.push(
'## Committed Key Results',
'',
// Governance-invariant 1 + 2, uttalt der mottakeren leser tallene.
'Committed KR maales binaert mot kravet: kravet er naadd eller ikke. En score',
'under 1.0 er et avvik som skal forklares, ikke et godt resultat.',
'',
...committed.linjer,
);
}
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_RAMME,
'',
...aspirational.linjer,
);
const snitt = aspirationalSnitt(syklus.okrer);
if (snitt) ut.push(snitt, '');
}
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, '');
}
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.
const snitt = aspirationalSnitt(syklus.okrer);
if (snitt) {
ut.push(
'## Aspirational maaloppnaaelse paa tvers av Objectives',
'',
...ASPIRATIONAL_RAMME,
'',
snitt,
'',
);
}
ut.push(...flerarigSeksjon(historikk));
return `${ut.join('\n').replace(/\n+$/, '')}\n`;
}
// --- Etatsstyringsmoete-underlag (steg 12) ---
//
// Bygget paa styringsdialog-kapittelet i okr-offentlig-governance.md:96-114.
// Etatsstyringsmoetet er det sentrale moetepunktet mellom departement og
// virksomhet, og OKR-settet gir det en fast struktur: hva flyttet seg, hva
// stoppet opp, hva ber vi om.
//
// Generatoren tar IKKE stilling til kadens. F-j fjernet paastanden om «2-4
// etatsstyringsmoeter per aar» nettopp fordi den ikke lot seg verifisere; antall
// og form fastsettes i departementets hovedinstruks. Et underlag som foregrep
// kadensen ville gjeninnfoert den samme uverifiserte paastanden i maskinform.
/**
* Bygger underlaget til et etatsstyringsmoete for en syklus lest av lesSyklus().
*
* @param {object} syklus struktur fra lesSyklus()
* @param {{naa?: string}} [opts]
* @returns {string} markdown
*/
export function etatsstyringsunderlag(syklus, opts = {}) {
krevSyklus(syklus, 'etatsstyringsunderlag');
const naa = klokke(opts);
const antallKr = syklus.okrer.reduce((n, okr) => n + okr.krer.length, 0);
if (antallKr === 0) {
throw new Error(`etatsstyringsunderlag: syklusen ${syklus.id} inneholder ingen KR aa rapportere`);
}
const ut = [
`# Etatsstyringsmoete - underlag (${syklus.id})`,
'',
`Generert: ${naa}`,
'',
'Antall og form paa etatsstyringsmoetene fastsettes i departementets',
'hovedinstruks. Dette underlaget tar ikke stilling til kadensen; det gir',
'moetet en struktur bygget paa syklusens egne tall.',
'',
...SKALAFORKLARING,
];
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(...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 -- og bare naar
// syklusen har aspirational-KR aa forklare.
if (harAspirational(syklus.okrer)) {
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,
// gjengitt i DFOe-notat 2026:2 s. 49 (fn. 228). Generatoren leser ikke
// tildelingsbrevet -- den har bare syklusdataene -- saa koblingen er et felt
// den RESERVERER. En generator som gjettet hvilket krav et Objective svarer
// paa, ville laget styringsinformasjon ingen har vedtatt.
ut.push(
'## Styringsparametere mot tildelingsbrevets krav',
'',
'Tildelingsbrevet skal sette styringsparametere for aa kunne vurdere',
'maaloppnaaelse og resultater (bestemmelsene punkt 1.5). Koblingen mellom',
'syklusens Objectives og disse parameterne staar her:',
'',
'| Objective | Styringsparameter i tildelingsbrevet | Krav |',
'|-----------|--------------------------------------|------|',
...syklus.okrer.map((okr) => `| ${okr.tittel} | | |`),
'',
'[Fylles ut av virksomheten: styringsparameter og krav hentes fra',
'tildelingsbrevet. Generatoren leser bare syklusdataene og gjetter ikke',
'hvilket krav et Objective svarer paa.]',
'',
);
return `${ut.join('\n').replace(/\n+$/, '')}\n`;
}
// --- Statusrapport (S60): det uwirede leddet ---
//
// Kjeden er generator -> /okr:sporing -> status.md, og dette er siste ledd.
// `status.md` er fila coaching-hook.mjs teller at-risk-rader i, /okr:moeter
// bygger check-in-agenda fra, og /okr:oppsett arkiver leser sluttscorer fra.
// Foer S60 fantes det ingen mekanisk vei dit: malen sto i commands/sporing.md,
// og en LLM maatte skrive av tallene generatoren nettopp hadde beregnet.
//
// Derfor tar denne funksjonen confidence som DATA og bygger radene med samme
// krRad som rapportformene. Aritmetikken kan ikke drifte fra tertialrapporten,
// fordi det ikke finnes en andre implementasjon av den.
// Lukket vokabular. okr-framework.md:564 er eneste sannhetskilde for confidence
// og forbyr andre filer aa definere egne nivaaer; her betyr det at et
// nabo-uttrykk som "Groenn" eller "on-track" avvises i stedet for aa bli tolket.
// Etikettene holdes ASCII-rene som resten av den maskingenererte flaten --
// hooken matcher paa ordene, aldri paa emoji.
const CONFIDENCE_VOKAB = ['On Track', 'At Risk', 'Off Track'];
// Inputen er noekklet paa KR-navn. To KR med samme navn ville da delt etikett
// uten at noen kunne se det, saa kollisjonen avvises foer den blir en stille
// feilrapportering.
function confidenceOppslag(syklus, confidence, hvor) {
if (confidence === null || typeof confidence !== 'object' || Array.isArray(confidence)) {
throw new Error(`${hvor}: confidence mangler -- forventet et oppslag fra KR-navn til etikett`);
}
const krNavn = [];
for (const okr of syklus.okrer) {
for (const kr of okr.krer) krNavn.push(kr.navn);
}
const duplikat = krNavn.find((navn, i) => krNavn.indexOf(navn) !== i);
if (duplikat) {
throw new Error(`${hvor}: to KR deler navnet "${duplikat}" -- confidence kan ikke tilordnes entydig`);
}
const kjent = new Set(krNavn);
for (const navn of Object.keys(confidence)) {
if (!kjent.has(navn)) {
throw new Error(`${hvor}: ukjent KR-navn i confidence: "${navn}" finnes ikke i syklusen`);
}
}
const mangler = krNavn.filter((navn) => confidence[navn] === undefined);
if (mangler.length > 0) {
throw new Error(`${hvor}: mangler confidence for ${mangler.length} KR: ${mangler.map((n) => `"${n}"`).join(', ')}`);
}
for (const navn of krNavn) {
const etikett = confidence[navn];
if (!CONFIDENCE_VOKAB.includes(etikett)) {
throw new Error(
`${hvor}: ugyldig confidence-etikett ${JSON.stringify(etikett)} for "${navn}". `
+ `Lovlige verdier: ${CONFIDENCE_VOKAB.join(' | ')}`,
);
}
}
return (kr) => confidence[kr.navn];
}
/**
* Bygger statusrapporten for en syklus lest av lesSyklus(), med confidence satt
* av /okr:sporing.
*
* @param {object} syklus struktur fra lesSyklus()
* @param {{naa?: string, confidence?: Record<string, string>}} [opts]
* confidence er et oppslag fra KR-navn til en etikett i det lukkede
* vokabularet. Hvert KR i syklusen MAA ha en verdi -- en tom celle ville gitt
* en fil som ser vurdert ut uten aa vaere det, og som konsumentene teller som
* groenn.
* @returns {string} markdown
*/
export function statusrapport(syklus, opts = {}) {
krevSyklus(syklus, 'statusrapport');
const naa = klokke(opts);
const hentConfidence = confidenceOppslag(syklus, opts.confidence, 'statusrapport');
const committed = seksjon(syklus.okrer, 'committed', true, hentConfidence);
const aspirational = seksjon(syklus.okrer, 'aspirational', false, hentConfidence);
if (committed.antallKr + aspirational.antallKr === 0) {
throw new Error(`statusrapport: syklusen ${syklus.id} inneholder ingen KR aa rapportere`);
}
const ut = [
`# Statusrapport ${syklus.id}`,
'',
`Generert: ${naa}`,
'',
...SKALAFORKLARING_STATUS,
];
if (committed.antallKr > 0) {
ut.push(
'## Committed Key Results',
'',
'Committed KR maales binaert mot kravet: kravet er naadd eller ikke. En score',
'under 1.0 er et avvik som skal forklares, ikke et godt resultat.',
'',
...committed.linjer,
);
}
ut.push(...avvikSeksjon(syklus.okrer, 'Avvik som skal forklares'));
if (aspirational.antallKr > 0) {
ut.push(
'## Aspirational Key Results',
'',
...ASPIRATIONAL_RAMME,
'',
...aspirational.linjer,
);
const snitt = aspirationalSnitt(syklus.okrer);
if (snitt) ut.push(snitt, '');
}
return `${ut.join('\n').replace(/\n+$/, '')}\n`;
}