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.
This commit is contained in:
parent
285d18ebfa
commit
3a0cbcfbf9
13 changed files with 618 additions and 50 deletions
|
|
@ -13,6 +13,16 @@
|
|||
// 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").
|
||||
|
|
@ -50,27 +60,30 @@ const formatAvvik = (kr, erCommitted) => {
|
|||
return score >= 1 ? 'Nei' : 'Ja';
|
||||
};
|
||||
|
||||
function krRad(kr, erCommitted) {
|
||||
function krRad(kr, erCommitted, confidence = '') {
|
||||
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)} | |`;
|
||||
// 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) => [
|
||||
const krTabell = (krer, erCommitted, hentConfidence = () => '') => [
|
||||
...TABELLHODE,
|
||||
...krer.map((kr) => krRad(kr, erCommitted)),
|
||||
...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) {
|
||||
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), '');
|
||||
linjer.push(`### ${okr.tittel}`, '', ...krTabell(krer, erCommitted, hentConfidence), '');
|
||||
}
|
||||
return { linjer, antallKr };
|
||||
}
|
||||
|
|
@ -178,19 +191,35 @@ function krevSyklus(syklus, hvor) {
|
|||
// 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 = [
|
||||
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();
|
||||
|
||||
/**
|
||||
|
|
@ -451,3 +480,124 @@ export function etatsstyringsunderlag(syklus, opts = {}) {
|
|||
|
||||
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`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue