feat(okr): etatsstyringsmoete-underlag fra syklusdata
This commit is contained in:
parent
77031b0c9a
commit
052e4d4e86
5 changed files with 203 additions and 26 deletions
|
|
@ -17,7 +17,7 @@ Expert OKR guidance for Norwegian public sector. Google/Doerr methodology adapte
|
|||
| `/okr:analyse` | Cross-cycle analytics with Mermaid trend visualizations |
|
||||
| `/okr:oppsett` | Configure plugin: onboarding interview (full/mvp), cycle archival, profile update. Args: `full\|mvp\|arkiver\|oppdater\|vis` |
|
||||
| `/okr:innboks` | Ingest documents dropped in `.claude/okr/innboks/` into the OKF tree — deterministic convert/split/frontmatter/index, per-document security gate |
|
||||
| `/okr:rapport` | Generate a report from cycle data. Args: `tertial` (`arsrapport`/`etatsstyring` not yet implemented). Owns arithmetic and formatting invariants — never confidence |
|
||||
| `/okr:rapport` | Generate a report from cycle data. Args: `tertial` \| `arsrapport` (annual report part III) \| `etatsstyring` (agency-governance meeting brief). Owns arithmetic and formatting invariants — never confidence, and never the assessment fields it reserves for the organization |
|
||||
| `/okr:export` | Export OKR deliverables (quality review, gap matrix, status report, retrospective) to print-ready PDF via weasyprint |
|
||||
| `/okr:freshen-references` | KB self-evaluator: score 18 of the 19 domain reference files against an anchored rubric — the quality rubric itself is excluded, since scoring the scoring instrument is circular — + currency-poll public sources |
|
||||
| `/okr:help` | Full overview of all commands, agents, and recommended cycle workflow |
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: okr:rapport
|
||||
description: Generer rapport fra en OKR-syklus - tertialrapport med committed og aspirational holdt fra hverandre
|
||||
description: Generer rapport fra en OKR-syklus - tertialrapport, aarsrapportens del III eller etatsstyringsunderlag, med committed og aspirational holdt fra hverandre
|
||||
allowed-tools: Read, Bash, Glob
|
||||
argument-hint: "[tertial] (arsrapport og etatsstyring kommer senere)"
|
||||
argument-hint: "[tertial|arsrapport|etatsstyring]"
|
||||
---
|
||||
|
||||
# OKR Rapport - Generer styringsrapport fra syklusdata
|
||||
|
|
@ -39,15 +39,16 @@ hver gang, slik at det ikke oppstaar en andre sannhetskilde som kan drifte.
|
|||
flere, spoer brukeren hvilken. Er det ingen, si det og stopp (`/okr:oppsett`
|
||||
setter opp treet).
|
||||
|
||||
2. **Kjoer generatoren** via Bash:
|
||||
2. **Kjoer generatoren** via Bash, med rapportformen som andre argument
|
||||
(`tertial`, `arsrapport` eller `etatsstyring` - se tabellen under):
|
||||
```bash
|
||||
node ${CLAUDE_PLUGIN_ROOT}/scripts/syklus-rapport.mjs .claude/okr/syklus/[syklus-id] tertial
|
||||
```
|
||||
|
||||
3. **Tolk exit-koden**:
|
||||
- `0` - rapporten er skrevet til `rapport-tertial.md` i syklus-katalogen.
|
||||
Oppsummer for brukeren: hvilken syklus, hvor mange OKR, og at
|
||||
Confidence-kolonnen skal fylles ut i `/okr:sporing`.
|
||||
- `0` - rapporten er skrevet til sin egen fil i syklus-katalogen (se tabellen).
|
||||
Oppsummer for brukeren: hvilken syklus, hvor mange OKR, og hvilke felter som
|
||||
staar igjen aa fylle ut - Confidence-kolonnen fylles i `/okr:sporing`.
|
||||
- `1` - domenefeil: syklusdataene er ufullstendige. Meldingen navngir KR-en og
|
||||
feltet som mangler. Be brukeren fylle inn tallene i OKR-fila (formatet staar
|
||||
i `/okr:skriv`), og kjoer om igjen.
|
||||
|
|
@ -60,11 +61,17 @@ hver gang, slik at det ikke oppstaar en andre sannhetskilde som kan drifte.
|
|||
|
||||
## Rapportformer
|
||||
|
||||
| Form | Status |
|
||||
|------|--------|
|
||||
| `tertial` | Tilgjengelig |
|
||||
| `arsrapport` | Ikke implementert ennaa - kommandoen svarer med exit 2 |
|
||||
| `etatsstyring` | Ikke implementert ennaa - kommandoen svarer med exit 2 |
|
||||
| Form | Fil | Til hvem, og hva den reserverer |
|
||||
|------|-----|----------------------------------|
|
||||
| `tertial` | `rapport-tertial.md` | Underveisrapportering. Confidence-kolonnen fylles i `/okr:sporing`. |
|
||||
| `arsrapport` | `rapport-arsrapport.md` | Underlag til aarsrapportens **del III** «Aarets aktiviteter og resultater». Vurderingen av maaloppnaaelse per Objective er reservert til virksomheten - generatoren skriver den aldri. Finnes `historikk/`, listes materialet derfra under et flerarig perspektiv. |
|
||||
| `etatsstyring` | `rapport-etatsstyring.md` | Underlag til etatsstyringsmoetet: status per Objective, avvik som krever departementets oppmerksomhet, og en tabell som kobler Objectives til tildelingsbrevets styringsparametere (kravene fylles inn fra tildelingsbrevet). |
|
||||
|
||||
**De reserverte feltene staar i klammer og skal fylles ut.** Generatoren lar dem
|
||||
staa tomme fordi de krever skjoenn eller kilder den ikke leser - ikke fordi de er
|
||||
valgfrie. Gaa gjennom dem med brukeren etter kjoeringen.
|
||||
|
||||
Ingen av formene overskriver hverandre; tre kjoeringer gir tre filer.
|
||||
|
||||
## Relatert
|
||||
|
||||
|
|
|
|||
|
|
@ -296,3 +296,108 @@ export function arsrapportDelIII(syklus, opts = {}) {
|
|||
|
||||
return `${ut.join('\n').replace(/\n+$/, '')}\n`;
|
||||
}
|
||||
|
||||
// --- Etatsstyringsmoete-underlag (steg 12) ---
|
||||
//
|
||||
// Bygget paa styringsdialog-kapittelet i okr-offentlig-governance.md:96-114.
|
||||
// Etatsstyringsmoetet er det sentrale moetepunktet mellom departement og
|
||||
// virksomhet, og OKR-settet gir det en fast struktur: hva flyttet seg, hva
|
||||
// stoppet opp, hva ber vi om.
|
||||
//
|
||||
// Generatoren tar IKKE stilling til kadens. F-j fjernet paastanden om «2-4
|
||||
// etatsstyringsmoeter per aar» nettopp fordi den ikke lot seg verifisere; antall
|
||||
// og form fastsettes i departementets hovedinstruks. Et underlag som foregrep
|
||||
// kadensen ville gjeninnfoert den samme uverifiserte paastanden i maskinform.
|
||||
|
||||
// Avvik er committed-only. Predikatet er det samme som Avvik-kolonnen bruker --
|
||||
// via beregnScore, aldri naa >= target -- saa listen og kolonnen ikke kan gi
|
||||
// ulikt svar om samme KR.
|
||||
function avvikListe(okrer) {
|
||||
const rader = [];
|
||||
for (const okr of okrer) {
|
||||
for (const kr of okr.krer) {
|
||||
if (kr.type !== 'committed' || formatAvvik(kr, true) !== 'Ja') continue;
|
||||
rader.push(
|
||||
`- ${okr.tittel}: ${kr.navn} -- naa ${kr.naa} mot krav ${kr.target} `
|
||||
+ `(score ${formatScore(beregnScore(kr))})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return rader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bygger underlaget til et etatsstyringsmoete for en syklus lest av lesSyklus().
|
||||
*
|
||||
* @param {object} syklus struktur fra lesSyklus()
|
||||
* @param {{naa?: string}} [opts]
|
||||
* @returns {string} markdown
|
||||
*/
|
||||
export function etatsstyringsunderlag(syklus, opts = {}) {
|
||||
krevSyklus(syklus, 'etatsstyringsunderlag');
|
||||
|
||||
const naa = klokke(opts);
|
||||
const antallKr = syklus.okrer.reduce((n, okr) => n + okr.krer.length, 0);
|
||||
if (antallKr === 0) {
|
||||
throw new Error(`etatsstyringsunderlag: syklusen ${syklus.id} inneholder ingen KR aa rapportere`);
|
||||
}
|
||||
|
||||
const ut = [
|
||||
`# Etatsstyringsmoete - underlag (${syklus.id})`,
|
||||
'',
|
||||
`Generert: ${naa}`,
|
||||
'',
|
||||
'Antall og form paa etatsstyringsmoetene fastsettes i departementets',
|
||||
'hovedinstruks. Dette underlaget tar ikke stilling til kadensen; det gir',
|
||||
'moetet en struktur bygget paa syklusens egne tall.',
|
||||
'',
|
||||
...SKALAFORKLARING,
|
||||
];
|
||||
|
||||
for (const okr of syklus.okrer) {
|
||||
ut.push(`## ${okr.tittel}`, '');
|
||||
for (const [type, erCommitted, overskrift] of [
|
||||
['committed', true, '### Committed Key Results'],
|
||||
['aspirational', false, '### Aspirational Key Results'],
|
||||
]) {
|
||||
const krer = okr.krer.filter((kr) => kr.type === type);
|
||||
if (krer.length === 0) continue;
|
||||
ut.push(overskrift, '', ...krTabell(krer, erCommitted), '');
|
||||
}
|
||||
}
|
||||
|
||||
const avvik = avvikListe(syklus.okrer);
|
||||
ut.push(
|
||||
'## Avvik som krever departementets oppmerksomhet',
|
||||
'',
|
||||
...(avvik.length > 0
|
||||
? avvik
|
||||
: ['Ingen committed KR ligger under kravet i denne syklusen.']),
|
||||
'',
|
||||
);
|
||||
|
||||
// Hjemmelen for at tildelingsbrevet SKAL inneholde styringsparametere er
|
||||
// bestemmelser om oekonomistyring i staten («bestemmelsene») punkt 1.5,
|
||||
// gjengitt i DFOe-notat 2026:2 s. 49 (fn. 228). Generatoren leser ikke
|
||||
// tildelingsbrevet -- den har bare syklusdataene -- saa koblingen er et felt
|
||||
// den RESERVERER. En generator som gjettet hvilket krav et Objective svarer
|
||||
// paa, ville laget styringsinformasjon ingen har vedtatt.
|
||||
ut.push(
|
||||
'## Styringsparametere mot tildelingsbrevets krav',
|
||||
'',
|
||||
'Tildelingsbrevet skal sette styringsparametere for aa kunne vurdere',
|
||||
'maaloppnaaelse og resultater (bestemmelsene punkt 1.5). Koblingen mellom',
|
||||
'syklusens Objectives og disse parameterne staar her:',
|
||||
'',
|
||||
'| Objective | Styringsparameter i tildelingsbrevet | Krav |',
|
||||
'|-----------|--------------------------------------|------|',
|
||||
...syklus.okrer.map((okr) => `| ${okr.tittel} | | |`),
|
||||
'',
|
||||
'[Fylles ut av virksomheten: styringsparameter og krav hentes fra',
|
||||
'tildelingsbrevet. Generatoren leser bare syklusdataene og gjetter ikke',
|
||||
'hvilket krav et Objective svarer paa.]',
|
||||
'',
|
||||
);
|
||||
|
||||
return `${ut.join('\n').replace(/\n+$/, '')}\n`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import { fileURLToPath } from 'node:url';
|
|||
|
||||
import { parseFrontmatter, writeFrontmatter } from '../lib/frontmatter.mjs';
|
||||
import { lesSyklus } from '../lib/syklus-data.mjs';
|
||||
import { arsrapportDelIII, tertialrapport } from '../lib/syklus-rapport.mjs';
|
||||
import { arsrapportDelIII, etatsstyringsunderlag, tertialrapport } from '../lib/syklus-rapport.mjs';
|
||||
|
||||
// historikk/ er SOESKEN av syklus/, altsaa <syklusDir>/../../historikk. Den
|
||||
// avledningen er en determinisme-risiko: regnet ut blindt ville en syklus-katalog
|
||||
|
|
@ -75,8 +75,12 @@ const FORMER = {
|
|||
// kunne testes uten et tre paa filsystemet.
|
||||
kontekst: (syklusDir) => ({ historikk: lesHistorikk(syklusDir) }),
|
||||
},
|
||||
// Steg 12. Oppfoert her, ikke skjult, saa flaten er synlig foer den virker.
|
||||
etatsstyring: null,
|
||||
etatsstyring: {
|
||||
filnavn: 'rapport-etatsstyring.md',
|
||||
tittel: (id) => `Etatsstyringsmoete - underlag ${id}`,
|
||||
beskrivelse: 'Maskingenerert underlag til etatsstyringsmoetet. Koblingen mot tildelingsbrevets styringsparametere er reservert til virksomheten.',
|
||||
bygg: etatsstyringsunderlag,
|
||||
},
|
||||
};
|
||||
|
||||
class Bruksfeil extends Error {}
|
||||
|
|
@ -122,9 +126,6 @@ export function genererRapport(syklusDir, formNavn, opts = {}) {
|
|||
throw new Bruksfeil(`Ukjent form: ${formNavn}. Former: ${Object.keys(FORMER).join(' | ')}`);
|
||||
}
|
||||
const form = FORMER[formNavn];
|
||||
if (form === null) {
|
||||
throw new Bruksfeil(`Rapportformen «${formNavn}» er ikke implementert ennaa.`);
|
||||
}
|
||||
if (!existsSync(syklusDir) || !statSync(syklusDir).isDirectory()) {
|
||||
throw new Bruksfeil(`Syklus-katalogen finnes ikke: ${syklusDir}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue