156 lines
6.2 KiB
JavaScript
156 lines
6.2 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.
|
|
//
|
|
// 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.
|
|
const formatAvvik = (kr, erCommitted) => {
|
|
if (!erCommitted) return '-';
|
|
return kr.naa >= kr.target ? 'Nei' : 'Ja';
|
|
};
|
|
|
|
function krRad(kr, erCommitted) {
|
|
const score = formatScore(beregnScore(kr));
|
|
// Siste celle (Confidence) staar bevisst tom -- se seem-notatet over.
|
|
return `| ${kr.navn} | ${kr.baseline} | ${kr.target} | ${kr.naa} | ${score} | ${formatAvvik(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) {
|
|
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}`, '', ...TABELLHODE);
|
|
for (const kr of krer) linjer.push(krRad(kr, erCommitted));
|
|
linjer.push('');
|
|
}
|
|
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}.`;
|
|
}
|
|
|
|
/**
|
|
* 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 = {}) {
|
|
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());
|
|
}
|
|
|
|
const naa = opts.naa || process.env.OKR_NOW || new Date().toISOString();
|
|
|
|
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}`,
|
|
'',
|
|
// 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.',
|
|
'',
|
|
];
|
|
|
|
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,
|
|
);
|
|
}
|
|
|
|
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.linjer,
|
|
);
|
|
const snitt = aspirationalSnitt(syklus.okrer);
|
|
if (snitt) ut.push(snitt, '');
|
|
}
|
|
|
|
return `${ut.join('\n').replace(/\n+$/, '')}\n`;
|
|
}
|