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.
603 lines
24 KiB
JavaScript
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`;
|
|
}
|