fix(okr): avviksvurderingen maa gaa gjennom score, ikke naa >= target

This commit is contained in:
Kjell Tore Guttormsen 2026-08-01 22:50:25 +02:00
commit 8ef8f7a405
2 changed files with 62 additions and 1 deletions

View file

@ -35,9 +35,19 @@ const formatScore = (score) => (score === undefined ? UDEFINERT : score.toFixed(
// 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 '-';
return kr.naa >= kr.target ? 'Nei' : 'Ja';
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) {

View file

@ -147,6 +147,57 @@ test('(6) tertialrapport kaster ved tom eller ufullstendig syklus', () => {
assert.throws(() => tertialrapport(undefined), /syklus/i);
});
// --- (2d) Avvik-kolonnen maa vaere retningsagnostisk ---
//
// `naa >= target` ser riktig ut for oppadgaaende maal og er FEIL for
// nedadgaaende: et krav om 5 dager, naa 11, gir 11 >= 5 = "ingen avvik" -- et
// brudd rapportert som etterlevd i et Riksrevisjon-vendt dokument. Fixturen kan
// ikke fange det (dens committed-KR er begge oppadgaaende, og dens nedadgaaende
// KR er aspirational, som kortslutter til "-"), saa syklusen konstrueres her.
// Retningen hoerer hjemme i beregnScore, som allerede haandterer den.
const NEDADGAAENDE = {
id: 'T-avvik',
okrer: [{
fil: 'okr-avvik.md',
tittel: 'Nedadgaaende krav',
krer: [
{ n: 1, navn: 'Saksbehandlingstid (dager)', baseline: 14, target: 5, naa: 11, type: 'committed' },
{ n: 2, navn: 'Alvorlige hendelser (antall)', baseline: 0, target: 0, naa: 3, type: 'committed' },
{ n: 3, navn: 'Naadd krav (dager)', baseline: 14, target: 5, naa: 4, type: 'committed' },
],
}],
};
const avvikFor = (navn) => {
const rad = tertialrapport(NEDADGAAENDE, { naa: NAA }).split('\n').find((l) => l.includes(navn));
assert.ok(rad, `fant ikke raden for ${navn}`);
return rad.split('|').map((c) => c.trim())[6];
};
test('(2d) committed nedadgaaende KR under kravet merkes som avvik', () => {
assert.equal(avvikFor('Saksbehandlingstid'), 'Ja', 'naa 11 mot krav 5 er et avvik');
});
test('(2d2) committed KR med target == baseline avviker naar naa ikke er lik kravet', () => {
assert.equal(avvikFor('Alvorlige hendelser'), 'Ja', '3 hendelser mot krav 0 er et avvik');
});
test('(2d3) committed KR som naar kravet merkes ikke som avvik', () => {
assert.equal(avvikFor('Naadd krav'), 'Nei', 'naa 4 mot krav 5 (nedadgaaende) er oppfylt');
});
// Rapporten sier selv at "en score under 1.0 er et avvik som skal forklares".
// Avvik-kolonnen maa aldri motsi den setningen paa samme rad.
test('(2d4) Avvik-kolonnen motsier ikke rapportens egen score-setning', () => {
const md = tertialrapport(NEDADGAAENDE, { naa: NAA });
const motsigelser = md.split('\n').filter((l) => {
const c = l.split('|').map((x) => x.trim());
if (c.length < 8 || c[6] !== 'Nei') return false;
return c[5] !== 'udefinert' && Number(c[5]) < 1;
});
assert.deepEqual(motsigelser, [], `score < 1.0 merket som ikke-avvik:\n${motsigelser.join('\n')}`);
});
// --- (7) CLI-kontrakten (steg 10) ---
//
// Testet BEGGE VEIER i samme fil: direkte import for atferd, execFileSync for