feat(okr): syklus-leser med kanonisk scoreberegning

This commit is contained in:
Kjell Tore Guttormsen 2026-08-01 22:35:27 +02:00
commit 1592a73267
2 changed files with 236 additions and 0 deletions

View file

@ -72,3 +72,97 @@ test('(A4) steg 7: krN_type forstyrrer ikke OKF-noekkelen type', async () => {
assert.equal(fm.get('type'), 'OKR', `${f}: OKF-type forskjovet av krN_type`);
}
});
// --- (B) lib/syklus-data.mjs: leser + scoreberegning (steg 8) ---
const { lesSyklus, beregnScore } = await import('../lib/syklus-data.mjs');
const SYKLUS_DIR = join(ROOT, SYKLUS);
test('(B1) lesSyklus leser begge OKR-filene med KR-ene sine', () => {
const syklus = lesSyklus(SYKLUS_DIR);
assert.equal(syklus.id, 'T1-2026');
assert.equal(syklus.okrer.length, 2, 'forventet begge okr-*.md');
// Deterministisk rekkefolge (sortert filnavn) -- rapportgeneratoren i steg 9
// arver denne rekkefolgen, saa den er del av kontrakten, ikke en tilfeldighet.
assert.deepEqual(
syklus.okrer.map((o) => o.fil),
['okr-digitalisering.md', 'okr-trafikksikkerhet.md'],
);
const digi = syklus.okrer[0];
assert.equal(digi.tittel, 'Digitalisering av tjenester');
assert.equal(digi.krer.length, 2);
assert.deepEqual(digi.krer[0], {
n: 1,
navn: 'Andel digitale soeknader (prosent)',
baseline: 60,
target: 85,
naa: 63,
type: 'aspirational',
});
});
test('(B2) lesSyklus tolker tallene som tall, ikke strenger', () => {
const syklus = lesSyklus(SYKLUS_DIR);
for (const okr of syklus.okrer) {
for (const kr of okr.krer) {
for (const felt of ['baseline', 'target', 'naa']) {
assert.equal(typeof kr[felt], 'number', `${okr.fil} kr${kr.n}.${felt} er ikke tall`);
}
}
}
});
test('(B3) beregnScore: kanonisk formel paa et kjent KR-par', () => {
// Tunneler oppgradert: (17 - 12) / (20 - 12) = 0.625
assert.equal(beregnScore({ baseline: 12, target: 20, naa: 17 }), 0.625);
});
test('(B4) beregnScore: kapper til [0, 1.0]', () => {
assert.equal(beregnScore({ baseline: 0, target: 10, naa: 25 }), 1.0, 'over target -> 1.0');
assert.equal(beregnScore({ baseline: 10, target: 20, naa: 4 }), 0, 'under baseline -> 0');
});
// B-2, den kanoniske kanten (okr-framework.md:319). En score paa 0 ville lest som
// "ingen fremgang" naar sannheten er "forholdet er ikke definert" -- forskjellen
// mellom et KR som feilet og et KR som ikke kan scores som ratio.
test('(B5) beregnScore: target == baseline gir undefined, ikke 0', () => {
const score = beregnScore({ baseline: 0, target: 0, naa: 0 });
assert.equal(score, undefined);
assert.notEqual(score, 0, 'udefinert score maa ikke kollapse til 0');
});
test('(B6) beregnScore: nedadgaaende maal gir positiv score uten saertilfelle', () => {
// Saksbehandlingstid 14 -> 5 dager, naa 11: (11 - 14) / (5 - 14) = 0.333...
const score = beregnScore({ baseline: 14, target: 5, naa: 11 });
assert.ok(score > 0.33 && score < 0.34, `forventet ~0.333, fikk ${score}`);
});
test('(B7) beregnScore: binaert KR gir 0 eller 1', () => {
assert.equal(beregnScore({ baseline: 0, target: 1, naa: 0 }), 0);
assert.equal(beregnScore({ baseline: 0, target: 1, naa: 1 }), 1);
});
// Kast heller enn stille default (moenster: lib/innboks-frontmatter.mjs:64-72).
// En manglende target som stille ble 0 ville produsert en score som SER gyldig ut.
test('(B8) beregnScore kaster ved manglende eller ikke-numerisk felt', () => {
assert.throws(() => beregnScore({ baseline: 1, naa: 2 }), /target/);
assert.throws(() => beregnScore({ baseline: 1, target: 'x', naa: 2 }), /target/);
assert.throws(() => beregnScore(undefined), /KR/);
});
test('(B9) lesSyklus kaster ved ufullstendig KR i frontmatter', async (t) => {
const { mkdtempSync, writeFileSync, rmSync } = await import('node:fs');
const { tmpdir } = await import('node:os');
const dir = mkdtempSync(join(tmpdir(), 'okr-syklus-'));
t.after(() => rmSync(dir, { recursive: true, force: true }));
writeFileSync(
join(dir, 'okr-ufullstendig.md'),
['---', 'type: OKR', 'title: Ufullstendig', 'kr1_navn: Mangler tall', 'kr1_baseline: 3', '---', '# Ufullstendig', ''].join('\n'),
);
assert.throws(() => lesSyklus(dir), /kr1/i);
});
test('(B10) lesSyklus kaster naar katalogen ikke finnes', () => {
assert.throws(() => lesSyklus(join(ROOT, 'tests/fixtures/finnes-ikke')), /finnes ikke/i);
});