feat(okr): etatsstyringsmoete-underlag fra syklusdata

This commit is contained in:
Kjell Tore Guttormsen 2026-08-02 06:53:46 +02:00
commit 052e4d4e86
5 changed files with 203 additions and 26 deletions

View file

@ -13,7 +13,7 @@ import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { lesSyklus } from '../lib/syklus-data.mjs';
import { arsrapportDelIII, tertialrapport } from '../lib/syklus-rapport.mjs';
import { arsrapportDelIII, etatsstyringsunderlag, tertialrapport } from '../lib/syklus-rapport.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const SYKLUS_DIR = join(ROOT, 'tests/fixtures/okf-realistic/syklus/T1-2026');
@ -33,6 +33,7 @@ const rapport = () => tertialrapport(lesSyklus(SYKLUS_DIR), { naa: NAA });
const FORMER = [
['tertial', (syklus, opts) => tertialrapport(syklus, opts)],
['arsrapport', (syklus, opts) => arsrapportDelIII(syklus, opts)],
['etatsstyring', (syklus, opts) => etatsstyringsunderlag(syklus, opts)],
];
const byggFor = (bygger, opts = {}) => bygger(lesSyklus(SYKLUS_DIR), { naa: NAA, ...opts });
@ -253,6 +254,54 @@ test('(8d) arsrapportDelIII kaster ved tom eller ufullstendig syklus', () => {
assert.throws(() => arsrapportDelIII(undefined), /syklus/i);
});
// --- (9) Etatsstyringsmoete-underlag (steg 12) ---
//
// Bygget paa styringsdialog-kapittelet i okr-offentlig-governance.md:96-114.
// Hjemmelen for styringsparametere er bestemmelser om oekonomistyring i staten
// («bestemmelsene») punkt 1.5, gjengitt i DFOe-notat 2026:2 s. 49 (fn. 228).
const underlag = (opts = {}) => etatsstyringsunderlag(lesSyklus(SYKLUS_DIR), { naa: NAA, ...opts });
test('(9a) underlaget skiller committed og aspirational', () => {
const md = underlag();
assert.match(md, /^###?\s+Committed Key Results/m, 'mangler committed-seksjon');
assert.match(md, /^###?\s+Aspirational Key Results/m, 'mangler aspirational-seksjon');
});
// F-j fjernet «2-4 etatsstyringsmoeter per aar» fordi paastanden ikke lot seg
// verifisere. Den disiplinen gjelder generatoren ogsaa: antall og form fastsettes
// i departementets hovedinstruks, og et underlag som paastaar en kadens
// foregriper noe det ikke kan vite.
test('(9b) underlaget hardkoder ingen moetefrekvens', () => {
const md = underlag();
const paastander = md.split('\n').filter(
(l) => /(\d+|to|tre|fire)\s*(-\s*\d+\s*)?(etatsstyrings)?m[o]+ter\b/i.test(l)
|| /m[o]+ter?\s+(per|i)\s+(aar|halvaar)/i.test(l)
|| /(halvaarlig|kvartalsvis|maanedlig|ukentlig)[^\n]{0,20}m[o]+te/i.test(l),
);
assert.deepEqual(paastander, [], `hardkodet moetefrekvens:\n${paastander.join('\n')}`);
// Den positive motparten: underlaget sier hvor kadensen faktisk fastsettes.
assert.match(md, /hovedinstruks/i, 'underlaget maa peke paa hovedinstruksen for antall og form');
});
test('(9c) styringsparametere kobles mot tildelingsbrevet, og oppfinnes ikke', () => {
const md = underlag();
const kropp = seksjonsKropp(md, '## Styringsparametere mot tildelingsbrevets krav');
assert.ok(kropp, 'mangler seksjonen som kobler Objectives til tildelingsbrevets krav');
assert.match(kropp, /tildelingsbrev/i);
// Generatoren leser ikke tildelingsbrevet -- den har bare syklusdataene. Da er
// koblingen et felt den RESERVERER, aldri et den fyller. Hvert Objective staar
// i tabellen; kravet ved siden av er tomt.
assert.match(kropp, /Trafikksikkerhet og tunnelsikkerhet/);
assert.match(kropp, /Digitalisering av tjenester/);
assert.match(kropp, /[Ff]ylles ut/, 'koblingen maa vaere merket som utfylles av virksomheten');
});
test('(9d) etatsstyringsunderlag kaster ved tom eller ufullstendig syklus', () => {
assert.throws(() => etatsstyringsunderlag({ id: 'T1-2026', okrer: [] }), /ingen OKR/i);
assert.throws(() => etatsstyringsunderlag(undefined), /syklus/i);
});
// --- (2d) Avvik-kolonnen maa vaere retningsagnostisk ---
//
// `naa >= target` ser riktig ut for oppadgaaende maal og er FEIL for
@ -348,13 +397,14 @@ test('(7a) CLI: exit 2 ved feil aritet', async () => {
assert.equal((await kjoerCli([SYKLUS_DIR])).code, 2);
});
test('(7b) CLI: exit 2 ved ukjent og ved ennaa uimplementert form', async () => {
assert.equal((await kjoerCli([SYKLUS_DIR, 'tullball'])).code, 2);
// etatsstyring kommer i steg 12 og skal si det, ikke feile stumt.
for (const form of ['etatsstyring']) {
const r = await kjoerCli([SYKLUS_DIR, form]);
assert.equal(r.code, 2, `${form} burde gi exit 2`);
assert.match(r.stderr, /ikke implementert/i, `${form} mangler forklarende melding`);
// Alle tre formene er implementert etter steg 12; den gjenvaerende bruksfeilen er
// en form som ikke finnes. Meldingen navngir de gyldige formene, saa exit 2 kan
// handteres uten aa lese kildekoden.
test('(7b) CLI: exit 2 ved ukjent form, med formene navngitt', async () => {
const r = await kjoerCli([SYKLUS_DIR, 'tullball']);
assert.equal(r.code, 2);
for (const form of ['tertial', 'arsrapport', 'etatsstyring']) {
assert.match(r.stderr, new RegExp(form), `feilmeldingen mangler formen ${form}`);
}
});
@ -429,6 +479,20 @@ test('(7h) CLI: i et ekte OKR-tre leses historikk-katalogen, ikke index.md', asy
assert.doesNotMatch(innhold, /index\.md/, 'index.md er navigasjon, ikke kildemateriale');
});
test('(7i) CLI: etatsstyring skrives til sin egen fil', async (t) => {
const { rmSync, readFileSync: les } = await import('node:fs');
const { rot, dir } = await midlertidigSyklus();
t.after(() => rmSync(rot, { recursive: true, force: true }));
const r = await kjoerCli([dir, 'etatsstyring']);
assert.equal(r.code, 0, `forventet exit 0, fikk ${r.code}: ${r.stderr ?? ''}`);
const innhold = les(join(dir, 'rapport-etatsstyring.md'), 'utf8');
assert.match(innhold, /^---\ntype: Status\n/, 'skrevet fil mangler OKF-frontmatter');
assert.match(innhold, /Styringsparametere mot tildelingsbrevets krav/);
// Tre former, tre filnavn: en form som overskrev en annens fil ville gjort den
// siste kjoeringen til den eneste som fantes.
assert.equal(lesSyklus(dir).okrer.length, 2, 'rapporten ble lest som en OKR');
});
// Idempotens: rapporten skal kunne kjoeres om igjen uten aa endre bundlen, og
// uten aa bli lest som en OKR neste gang (filnavnet starter ikke med okr-).
test('(7f) CLI: to kjoeringer gir byte-identisk fil', async (t) => {