Compare commits

..

No commits in common. "main" and "v1.8.2" have entirely different histories.

50 changed files with 304 additions and 6001 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "okr", "name": "okr",
"version": "1.10.0", "version": "1.8.2",
"description": "Expert OKR guidance for Norwegian public sector. Write, review, cascade, track and govern OKR based on Google/Doerr methodology adapted for 4-month tertial cycles.", "description": "Expert OKR guidance for Norwegian public sector. Write, review, cascade, track and govern OKR based on Google/Doerr methodology adapted for 4-month tertial cycles.",
"author": { "author": {
"name": "Kjell Tore Guttormsen" "name": "Kjell Tore Guttormsen"

View file

@ -5,56 +5,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.10.0] - 2026-08-10
Minor-release: **det uwirede leddet**. Kjeden `/okr:rapport``/okr:sporing``status.md` var beskrevet i prosa, men manglet et mekanisk ledd — ingenting førte en generert rad inn i `status.md`. Det er fila `coaching-hook` teller at-risk-KR i, `/okr:møter` bygger check-in-agenda fra, og `/okr:oppsett arkiver` leser sluttscorer fra. Ingen nye npm-avhengigheter. Suite 329 → 343 cases.
### Added
- **`scripts/syklus-status.mjs`** — deterministisk skriveledd for `.claude/okr/syklus/[id]/status.md`. Tar confidence som JSON på stdin (oppslag fra KR-navn til etikett) og bygger radene med samme kode som tertialrapporten, gjennom `statusrapport()` i `lib/syklus-rapport.mjs`. **Aritmetikken kan ikke drifte fra `/okr:rapport`, fordi det ikke finnes en andre implementasjon av den** — en egen vakt sammenligner radene tegn for tegn mot tertialrapportens, bortsett fra confidence-cellen. Atomisk skriv (temp + rename), klokke-søm via `OKR_NOW` gir byte-identiske kjøringer.
- **Kuratert-fil-vern på `status.md`** — en eksisterende fil uten provenans-nøkkelen `kilde: sporing` regnes som håndholdt, og skrivingen avvises med exit 1 i stedet for å klobbe brukerens egne data. Samme mekanikk som `lib/innboks-write.mjs` bruker med `kilde: innboks`. Egne tidligere kjøringer overskrives fritt.
- **Lukket confidence-vokabular håndhevet i kode**`On Track` / `At Risk` / `Off Track` fra `okr-framework.md`, som er eneste sannhetskilde. Et nabo-uttrykk («Grønn», «on-track») avvises i stedet for å tolkes. Hvert KR i syklusen må ha en etikett: en fil med tomme celler ser vurdert ut uten å være det, og konsumentene ville lest den som grønn.
### Changed
- **`/okr:sporing` har fått `Bash`** og skriver `status.md` via scriptet i stedet for å formulere fila selv. Hele sporings-stien var lesende før dette, hvilket var den mekaniske grunnen til at leddet manglet. Kommandoen eier fortsatt **vurderingen** og bare den; malen i kommandofila er erstattet av et utdrag av det scriptet faktisk produserer, med ASCII-etiketter uten emoji som resten av den maskingenererte flaten.
- **Beslutning 9 presisert, ikke svekket:** generatoren *plasserer* en etikett den får inn som data, og *utleder* aldri en. Rapportformene (tertial/årsrapport/etatsstyring) emitterer fortsatt aldri confidence selv, og vakt (5c) holder dem til det. `statusrapport` står bevisst utenfor den listen, fordi den ikke utleder noe — den gjengir et skjønn som allerede er utøvd.
### Kjent avvik
- **`coaching-hook` teller committed og aspirational KR under ett når den varsler om confidence.** Hooken teller alle tabellrader merket `At Risk`/`Off Track` og sier «OBS: N KR er At Risk/Off Track — vurder tiltak eller juster forventninger». `okr-framework.md` sier samtidig at *de fleste stretch-KR bør stå At Risk*, og at permanent grønt er selve sandbagging-signalet. For en velfungerende aspirational-portefølje peker varselet derfor feil vei: eneste måte å dempe det på er å merke stretch-KR grønne. Kollisjonen er **ikke ny i art** — en håndskrevet `status.md` etter den gamle malen ble talt likt — men fra og med 1.10.0 fylles kolonnen deterministisk, så den inntreffer alltid i stedet for av og til. Fikset hører hjemme i hooken, ikke i skriveleddet, og er ført som egen post for en senere patch. Selve rapporteringen er ikke berørt: aspirational-KR står fortsatt aldri i avviks-seksjonen (låst beslutning 15).
**Lukket etter release** (patch-lane, egen økt): hooken teller nå seksjons-bevisst. Committed KR teller på både `At Risk` og `Off Track`; aspirational KR teller **kun**`Off Track`, siden «blokkere oppstår, progresjon stopper opp» er et reelt signal også for et stretch-KR, mens `At Risk` er den forventede tilstanden kanon beskriver. En rad hooken ikke kan plassere i en seksjon telles som committed — håndskrevne `status.md` fra før 1.10.0 har ingen seksjonsstruktur, og en fil som varsler for lite er farligere enn en som varsler for mye. Avviket over gjaldt altså 1.10.0 slik den ble utgitt, og gjelder ikke lenger.
## [1.9.0] - 2026-08-09
Minor-release: **«Styringssløyfa lukkes»** — kjeden fra tildelingsbrev til årsrapport dekkes nå i én sammenhengende form, i stedet for å stoppe der OKR-ene var skrevet. Åtte arbeidsstrømmer over fem execute-sesjoner (fase D, D4D8). Ingen nye npm-avhengigheter; de fire eksisterende er fortsatt exact-pinnet. Suite 197 → 315 cases.
### Added
- **Tre deterministiske rapportgeneratorer**`/okr:rapport tertial | arsrapport | etatsstyring` bygger tertialrapport, årsrapport del III-utkast og etatsstyringsmøte-underlag direkte fra syklusdata. Én kommandoflate med argument (mønster: `/okr:oppsett`), ikke tre kommandoer. Arkitekturen er `scripts/syklus-rapport.mjs``lib/syklus-data.mjs` (lesing + score) + `lib/syklus-rapport.mjs` (rendring), uten LLM i stien. Determinismen er målt, ikke påstått: to påfølgende kjøringer mot samme fixture gir byte-identisk output, verifisert på både modul- og CLI-nivå.
- **KR-datakontrakt i `/okr:skriv`-malen**`krN_navn`, `krN_baseline`, `krN_target`, `krN_naa`, `krN_type` (`committed` | `aspirational`) som frontmatter ved siden av punktprosaen. Maskinlaget legges til; menneskelaget beholdes. **Score lagres aldri** — den beregnes som `(naa - baseline) / (target - baseline)` klampet til [0, 1]. En lagret score ville vært en andre sannhetskilde som kan drifte fra tallene den utledes av; en egen vakt feller enhver `krN_score`-nøkkel som måtte dukke opp i malen.
- **`/okr:arkivklar`** — vurderingsgrunnlag mot bevaringsforskrifta (FOR-2025-12-19-2729 §§ 7 og 30, med § 3 som safety-net): hva i OKR-treet som trolig faller under bevaringspåbudet, og hva som bør *vurderes* overført til virksomhetens sak-arkiv. Kommandoen **sletter ingenting og tar ikke stilling på virksomhetens vegne** — vurderingen ligger til dokumentasjonsplanen (arkivlova § 8 / arkivforskrifta § 12 b), ikke til et verktøy. En egen vakt skanner den *genererte rapporten* for avgjørelsesord, slik at prosaen ikke kan gli fra vurderingsgrunnlag til dom. Ukjent OKF-type ⇒ § 3 ⇒ BEVAR.
- **Ny referansefil `gevinstrealisering-okr.md`** — DFØs nyttestyring mappet mot OKR (gevinst → outcome-KR, gevinstansvarlig → KR-eier), med linjeeierskapet som hovedpoeng og Prosjektveiviseren-koblingen.
- **Ny referansefil `okr-kommunal-styring.md`** — den kommunale styringslinjen på egne premisser: kommunalt selvstyre uten tildelingsbrev (kommuneloven §§ 2-1/2-2), økonomiplan og kommuneplanens handlingsdel (§§ 14-1 til 14-4, jf. plan- og bygningsloven § 11-1), kommunedirektørens utrednings- og iverksettingsansvar (§ 13-1), og koblingen mellom en fireårig økonomiplan og en firemåneders OKR-syklus. Fylkeskommunen dekkes eksplisitt. **KOSTRA** er lagt inn som indikatorkilde i samme fil — det eneste målte hullet da fila kom (0 treff før, 15 nå).
- **Styringssløyfas ut-side i `okr-offentlig-governance.md`** — der guiden tidligere hadde én 4-raders tabell om årsrapport, dekker den nå styringsdialogen som helhet: etatsstyringsmøtet som sentralt møtepunkt, årsrapportens seks faste deler med del III («Årets aktiviteter og resultater») som ankeret for måloppnåelse og del IV som stedet OKR-prosessen selv dokumenteres, samt tre regler for å rapportere aspirational OKR oppover uten at 0.7 leses som svikt.
- **Strategi→OKR og Beyond Budgeting i `okr-framework.md`** — to nye kildebelagte seksjoner (Bogsnes). Begge var uten kildegrunnlag i repoet før denne fasen.
- **Leading/lagging-balanse som rubrikk-dimensjon** med fem ankere, og **guardrail som KR-designmønster** i både `/okr:skriv` og `/okr:kvalitet`. Dimensjonstallet utledes, aldri hardkodet.
### Changed
- **Committed og aspirational separeres i alle tre rapportformene.** En aspirational OKR med lav score presenteres ikke som avvik — det er formålet med kategorien. En **sandbagging-vakt** låser dette som test, med begrunnelsen skrevet inn i rapporten selv: strukturell innramming, ikke et disiplinproblem hos den som satte målet.
- **Generatoren eier aritmetikk og formatering — aldri confidence, og aldri vurderingsfeltene den reserverer for organisasjonen.** `okr-framework.md` gjør confidence til en sannsynlighetsvurdering, ikke en funksjon av score; en generator som utledet den mekanisk ville gjort et skjønnsfelt til en kodesti.
- **`okf_version` emitteres nå i rot-indeksens frontmatter**, `okf_layout` blir i brødteksten (OKF-spec §12, beslutning 6). De to markørene bærer ulike konsepter og får derfor ulike flater: `okf_version` er den maskinlesbare kontrakten konsumenter utenfor pluginen leser. **Lesetoleransen er permanent, ikke en overgangsordning**`okf-check` leser begge plasseringer, fordi bundles på disk ikke migrerer samtidig og ingen mekanisme tvinger dem. PRESENCE håndheves fortsatt ikke; nøkkelen er MAY i specen.
- **Path-confinement er trukket ut til én delt modul** (`lib/path-confinement.mjs`), brukt av både ingestion (skrivesiden) og arkivklar-traverseringen (lesesiden). To lag som fanger ulike angrepsformer: leksikalsk `..`-escape/absolutt-override, og symlenke-escape via `realpathSync`. To kopier av en sikkerhetsregel må endres i takt for alltid, og gjør det ikke.
- **Referansebiblioteket 17 → 19 filer.** Flatene bærer med vilje ulike tall: totalflatene sier 19, domeneflatene 18 (kvalitetsrubrikken ekskluderes, siden å score scoringsinstrumentet er sirkulært), og `SKILL.md`-registeret er uummerert.
- **F-j lukket.** Dette lukker det siste av de ti F-funnene fra `review-2026-07-16.md` §3. F-j (styringssløyfas ut-side, kommunal styringslinje, gevinstrealisering) ble eksplisitt utsatt fra 1.8.0 til denne fasen via Non-Goal i C-fasens brief, mens SC-85 samtidig påsto at alle ti var lukket. Diskrepansen var reell og er nå løst ved å faktisk lukke funnet — SC-85 leses fra og med her som dekkende for F-a…F-j.
- **Kildeattribusjonene skjerpet etter primærkildesjekk.** Riksrevisjon-knaggene i `okr-offentlig-governance.md` er daterte og gjelder avsluttede saker — de siteres aldri i presens. DFØs nyttestyringsveileder erstatter 2014-gevinstrealiseringen.
### Fixed
- **Siterings-aksen nådde produsenten i 1.8.2, men ikke checkeren.** `okf-check`s `rootMarkers`/`pick` returnerte rå streng, så samme fil ga to lesninger: produsenten tolket `okf_version: "0.2"` som `0.2`, mens checkeren ekkoet `"0.2"`. Catalog målte divergensen direkte mot sin egen checker på en bundle som følger upstreams eget kanoniske eksempel (`SPEC.md:773`) — begge implementasjonene *aksepterte*, avviket lå på verdien. `unquote()` er nå eksportert fra `okf-index.mjs` og importert av `okf-check.mjs`, så begge sider deler én regel i stedet for hver sin kopi. Ingen gate rødnet av dette, fordi paritetskorpuset ikke har en sitert fikstur — hvilket også betyr at paritetsgatens grønne farge ikke dekket aksen; den var «ikke kjørt», ikke «som forventet».
- **Avviksvurderingen gikk utenom score.** Den sammenlignet `naa` mot `target` direkte, som gir feil svar for synkende mål (der lavere er bedre). Den går nå gjennom den kanoniske scoren.
- **`export-pdf.py` rendret OKF-frontmatter som brødtekst.** `python-markdown` uten `meta`-extensionen parser ikke YAML-frontmatter, så `type`/`resource`/`timestamp` havnet øverst i eksporten som `<hr/><p>type: …</p><hr/>`. Hver fil i treet bærer frontmatter, så maskinlaget lakk inn i dokumenter som skal til virksomhetens sak-arkiv.
- **Stale referansetelling i README.** «scores 16 of the 17 domain reference files» hang igjen fra før F-j, som bumpet én telleflate men ikke denne. Alle fem flatene er nå konsistente.
### Kjent avvik
- **`/okr:arkivklar` er rent lesende ved *skript*, ikke ved tool-liste alene.** Fasens brief krevde en tool-liste uten `Write`, `Edit` **eller** `Bash`; levert er `Read, Bash, Glob`, fordi kommandoen kjører `scripts/arkivklar.mjs`. Lesegrensen håndheves derfor i `lib/path-confinement.mjs` (to lag, mutasjons-verifisert) og av at kommandoen ikke har noe skriveverktøy — ikke av tool-lista alene. Avviket ble innført i planen uten at briefen ble oppdatert, og er registrert i `/trekreview` som MAJOR `PLAN_EXECUTE_DRIFT`. Det står her fordi det er en svakere garanti enn den som ble spesifisert, ikke fordi det er en kjent feil i dag.
## [1.8.2] - 2026-07-31 ## [1.8.2] - 2026-07-31
Patch-release: **siterte markørverdier korrumperte begge rot-markørene**. Bugen lå i released 1.8.1-kode og traff enhver bundle som hadde skrevet `okf_version` slik OKF-specen selv viser den. Ingen nye kommandoer, agenter eller referansefiler. Patch-release: **siterte markørverdier korrumperte begge rot-markørene**. Bugen lå i released 1.8.1-kode og traff enhver bundle som hadde skrevet `okf_version` slik OKF-specen selv viser den. Ingen nye kommandoer, agenter eller referansefiler.

View file

@ -1,4 +1,4 @@
# OKR Offentlig Sektor v1.10.0 # OKR Offentlig Sektor v1.8.2
Expert OKR guidance for Norwegian public sector. Google/Doerr methodology adapted for 4-month tertial cycles. Expert OKR guidance for Norwegian public sector. Google/Doerr methodology adapted for 4-month tertial cycles.
@ -9,7 +9,7 @@ Expert OKR guidance for Norwegian public sector. Google/Doerr methodology adapte
| `/okr:skriv` | Write new OKR with guided Objective and KR creation | | `/okr:skriv` | Write new OKR with guided Objective and KR creation |
| `/okr:kvalitet` | Quality check OKR against rubric and antipatterns | | `/okr:kvalitet` | Quality check OKR against rubric and antipatterns |
| `/okr:kaskade` | Cascade OKR from org to team, visualize alignment | | `/okr:kaskade` | Cascade OKR from org to team, visualize alignment |
| `/okr:sporing` | Track progress, calculate scores, generate check-ins. Owns the *confidence assessment* and writes `status.md` through `scripts/syklus-status.mjs` — never by hand, so the arithmetic cannot drift from `/okr:rapport` | | `/okr:sporing` | Track progress, calculate scores, generate check-ins |
| `/okr:møter` | Plan OKR workshops, check-ins, reviews, 1:1s | | `/okr:møter` | Plan OKR workshops, check-ins, reviews, 1:1s |
| `/okr:innføring` | OKR adoption planning, resistance handling, maturity | | `/okr:innføring` | OKR adoption planning, resistance handling, maturity |
| `/okr:governance` | Tildelingsbrev translation, Riksrevisjon compliance | | `/okr:governance` | Tildelingsbrev translation, Riksrevisjon compliance |
@ -17,10 +17,8 @@ Expert OKR guidance for Norwegian public sector. Google/Doerr methodology adapte
| `/okr:analyse` | Cross-cycle analytics with Mermaid trend visualizations | | `/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: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: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` (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:export` | Export OKR deliverables (quality review, gap matrix, status report, retrospective) to print-ready PDF via weasyprint |
| `/okr:arkivklar` | Assessment basis against bevaringsforskrifta (§§ 7/30, safety-net § 3) — what in the tree likely falls under the preservation duty and should be *considered* for transfer to the case/archive system. Read-only by tool list (no `Write`/`Edit`); never decides, never deletes | | `/okr:freshen-references` | KB self-evaluator: score 16 of the 17 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: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 | | `/okr:help` | Full overview of all commands, agents, and recommended cycle workflow |
## Agents ## Agents
@ -48,7 +46,7 @@ Expert OKR guidance for Norwegian public sector. Google/Doerr methodology adapte
| Component | Location | | Component | Location |
|-----------|----------| |-----------|----------|
| SKILL.md (okr-offentlig-sektor) | `skills/okr-offentlig-sektor/SKILL.md` | | SKILL.md (okr-offentlig-sektor) | `skills/okr-offentlig-sektor/SKILL.md` |
| References (19) | `skills/okr-offentlig-sektor/references/` | | References (17) | `skills/okr-offentlig-sektor/references/` |
| SKILL.md (okr-second-brain-search) | `skills/okr-second-brain-search/SKILL.md` | | SKILL.md (okr-second-brain-search) | `skills/okr-second-brain-search/SKILL.md` |
The second skill (`okr-second-brain-search`) does on-demand retrieval over the user's OKF-compatible wiki (native Glob/Read/Grep, no MCP) — see OKF Knowledge Layout below. The second skill (`okr-second-brain-search`) does on-demand retrieval over the user's OKF-compatible wiki (native Glob/Read/Grep, no MCP) — see OKF Knowledge Layout below.
@ -66,13 +64,12 @@ Cycle archival: `/okr:oppsett arkiver` — moves `syklus/` to `historikk/`, gene
Context files carry OKF v0.1 Documents-style frontmatter (conforms to the OKF v0.1 minimal contract, canonized in `catalog/docs/okf-second-brain/spec.md`): a Title-Case `type` (`Organisasjonsprofil`, `Tildelingsbrev`, `Virksomhetsplan`, `Overordnede OKR`, `OKR`, `Retrospektiv`, `Status`) plus recommended `resource`/`title`/`description`/`tags`/`timestamp`. Only `type` is de-facto required. The flat frontmatter parser reads only single-line keys (`type`, profile/config fields); tree files may carry multi-line `tags` lists without breaking it. Files written by inbox ingestion additionally carry the provenance extension key `kilde: innboks`. Context files carry OKF v0.1 Documents-style frontmatter (conforms to the OKF v0.1 minimal contract, canonized in `catalog/docs/okf-second-brain/spec.md`): a Title-Case `type` (`Organisasjonsprofil`, `Tildelingsbrev`, `Virksomhetsplan`, `Overordnede OKR`, `OKR`, `Retrospektiv`, `Status`) plus recommended `resource`/`title`/`description`/`tags`/`timestamp`. Only `type` is de-facto required. The flat frontmatter parser reads only single-line keys (`type`, profile/config fields); tree files may carry multi-line `tags` lists without breaking it. Files written by inbox ingestion additionally carry the provenance extension key `kilde: innboks`.
**Two bundle roots** with distinct lifecycles: project `.claude/okr/` (cwd-bound cycle/work) and home `~/.claude/okr/org/` (reinstall-surviving org identity). Each root carries its own `index.md` (`# H1` + `* [title](link) - description`) and, on the root index, two distinct markers (OKF spec §12): `okf_version` (the upstream OKF version targeted, currently `0.1`) and `okf_layout` (this plugin's own layout revision, currently `kb-layout-2026-06`). The two markers sit on **different surfaces** by decision 6: `okf_version` in the root index's frontmatter (the machine-readable contract consumers outside the plugin read), `okf_layout` in its body. Sub-level indexes carry no frontmatter and no markers. `okf-check` reads `okf_version` from **both** placements — frontmatter first, body as fallback — since bundles on disk do not migrate simultaneously. `okf-index`/`okf-check` run per root; retrieval Globs both roots as a union — never a short-circuit — with project content winning on conflict (`skills/okr-second-brain-search/SKILL.md:35,46,95` is the authoritative procedure). Only the org *profile* resolves by precedence (`hooks/scripts/inject-okr-context.mjs:54-56` short-circuits on the project hit). **Two bundle roots** with distinct lifecycles: project `.claude/okr/` (cwd-bound cycle/work) and home `~/.claude/okr/org/` (reinstall-surviving org identity). Each root carries its own `index.md` (no frontmatter; `# H1` + `* [title](link) - description`) and, on the root index, two distinct markers (OKF spec §12): `okf_version` (the upstream OKF version targeted, currently `0.1`) and `okf_layout` (this plugin's own layout revision, currently `kb-layout-2026-06`). `okf-index`/`okf-check` run per root; retrieval Globs both roots as a union — never a short-circuit — with project content winning on conflict (`skills/okr-second-brain-search/SKILL.md:35,46,95` is the authoritative procedure). Only the org *profile* resolves by precedence (`hooks/scripts/inject-okr-context.mjs:54-56` short-circuits on the project hit).
- `lib/frontmatter.mjs` — shared frontmatter parser/writer; both hooks route through it (strips trailing ` #…` only from unquoted values). - `lib/frontmatter.mjs` — shared frontmatter parser/writer; both hooks route through it (strips trailing ` #…` only from unquoted values).
- `scripts/okf-index.mjs` — regenerate a root's per-level `index.md` (verbatim OKF index format). - `scripts/okf-index.mjs` — regenerate a root's per-level `index.md` (verbatim OKF index format).
- `scripts/okf-check.mjs` — validate each concept file carries `type:` (exit 1 + count otherwise); echo `okf_version` + `okf_layout` per root (pure echo, no value validation). - `scripts/okf-check.mjs` — validate each concept file carries `type:` (exit 1 + count otherwise); echo `okf_version` + `okf_layout` per root (pure echo, no value validation).
- `scripts/compose-org-profile.mjs` — build the nested org-profile YAML with top-level OKF keys (`type`/`resource`/`timestamp`) before piping to `write-org-profile.mjs`. - `scripts/compose-org-profile.mjs` — build the nested org-profile YAML with top-level OKF keys (`type`/`resource`/`timestamp`) before piping to `write-org-profile.mjs`.
- `scripts/syklus-status.mjs` — deterministic writer for `syklus/[id]/status.md` (`/okr:sporing`): reads confidence as JSON on stdin (KR name → `On Track` | `At Risk` | `Off Track`), builds the rows through `statusrapport()` in `lib/syklus-rapport.mjs` — the same code the tertial report uses, so the arithmetic has no second implementation to drift from. Full coverage is required (an empty confidence cell would read as green to every consumer), the vocabulary is closed, and an existing `status.md` without the provenance key `kilde: sporing` is treated as hand-curated and never overwritten. Atomic write; `OKR_NOW` makes runs byte-identical.
- `scripts/innboks-ingest.mjs` — inbox-ingestion orchestrator (`/okr:innboks`): walks `.claude/okr/innboks/`, converts (txt/md/docx/eml/pdf via `lib/convert/index.mjs`), heading-splits, stamps OKF frontmatter + `kilde: innboks`, routes by type, then gates each document with `okf-check --strict-ingest` — a failing document is discarded alone (staging + per-document rollback), survivors get relations and index entries. Non-destructive (originals stay in the inbox), idempotent by construction (timestamp = source mtime), path-confined (realpath). Binary formats need `npm install --ignore-scripts` (4 exact-pinned pure-JS deps); txt/md run dependency-free. - `scripts/innboks-ingest.mjs` — inbox-ingestion orchestrator (`/okr:innboks`): walks `.claude/okr/innboks/`, converts (txt/md/docx/eml/pdf via `lib/convert/index.mjs`), heading-splits, stamps OKF frontmatter + `kilde: innboks`, routes by type, then gates each document with `okf-check --strict-ingest` — a failing document is discarded alone (staging + per-document rollback), survivors get relations and index entries. Non-destructive (originals stay in the inbox), idempotent by construction (timestamp = source mtime), path-confined (realpath). Binary formats need `npm install --ignore-scripts` (4 exact-pinned pure-JS deps); txt/md run dependency-free.
Retrieval is on-demand via the `okr-second-brain-search` skill. The UserPromptSubmit hook no longer pre-injects the full context-file enumeration — it emits the core profile plus one resolved pointer to `index.md` (< 512 B, independent of file count). Retrieval is on-demand via the `okr-second-brain-search` skill. The UserPromptSubmit hook no longer pre-injects the full context-file enumeration — it emits the core profile plus one resolved pointer to `index.md` (< 512 B, independent of file count).
@ -89,7 +86,7 @@ Retrieval is on-demand via the `okr-second-brain-search` skill. The UserPromptSu
/okr:skriv ──→ kvalitetssjekker (auto) /okr:skriv ──→ kvalitetssjekker (auto)
/okr:kvalitet ──→ kvalitetssjekker /okr:kvalitet ──→ kvalitetssjekker
/okr:kaskade ──→ kaskadebygger /okr:kaskade ──→ kaskadebygger
/okr:sporing ──→ fremdriftssporer ──→ scripts/syklus-status.mjs (skriver status.md) ──→ Linear MCP (optional) /okr:sporing ──→ fremdriftssporer ──→ Linear MCP (optional)
/okr:møter ──→ møtefasilitator /okr:møter ──→ møtefasilitator
/okr:governance ──→ styringsrådgiver /okr:governance ──→ styringsrådgiver
/okr:innføring ──→ (inline, no agent) /okr:innføring ──→ (inline, no agent)
@ -98,9 +95,7 @@ Retrieval is on-demand via the `okr-second-brain-search` skill. The UserPromptSu
/okr:oppsett ──→ (inline wizard: full/mvp/arkiver/oppdater/vis) /okr:oppsett ──→ (inline wizard: full/mvp/arkiver/oppdater/vis)
/okr:oppsett arkiver ──→ cycle archival + retrospektiv-generering /okr:oppsett arkiver ──→ cycle archival + retrospektiv-generering
/okr:innboks ──→ scripts/innboks-ingest.mjs (convert → split → frontmatter → per-doc gate → relations → index) /okr:innboks ──→ scripts/innboks-ingest.mjs (convert → split → frontmatter → per-doc gate → relations → index)
/okr:rapport ──→ scripts/syklus-rapport.mjs ──→ lib/syklus-data.mjs (read + score) + lib/syklus-rapport.mjs (render)
/okr:export ──→ scripts/export-pdf.py (weasyprint, documented prerequisite) /okr:export ──→ scripts/export-pdf.py (weasyprint, documented prerequisite)
/okr:arkivklar ──→ scripts/arkivklar.mjs ──→ lib/arkivklar.mjs (traverse + classify + render) + lib/path-confinement.mjs (read boundary)
/okr:freshen-references ──→ (inline KB-evaluator + currency-polling via WebSearch/Task) /okr:freshen-references ──→ (inline KB-evaluator + currency-polling via WebSearch/Task)
/okr:help ──→ (inline command/agent/workflow overview) /okr:help ──→ (inline command/agent/workflow overview)
SessionStart ──→ coaching-hook.mjs (proactive coaching) SessionStart ──→ coaching-hook.mjs (proactive coaching)

391
README.md
View file

@ -1,291 +1,221 @@
# okr # OKR for Public Sector
Expert OKR guidance for Norwegian public sector. Write, review, cascade, track and govern OKR based on Google/Doerr methodology adapted for 4-month tertial cycles.
Å omsette et strategidokument til mål teamene faktisk arbeider mot er det vanskelige > Turn strategy into measurable goals. An AI coach that learns your organization, tracks progress across cycles, and guides you from first OKR to organizational mastery.
leddet, og der de fleste innføringer stopper opp. Denne pluginen er en KI-coach for
nettopp den oversettelsen, bygget for vokabularet arbeidet allerede skjer i:
tertialsykluser, tildelingsbrev, mål- og resultatstyring, og styringslinjen fra
Stortingsmelding ned til team-OKR.
> **Solo-maintained, fork-and-own.** This plugin is a starting point, not a vendor product. Issues are welcome as signals; pull requests are not accepted. See [GOVERNANCE.md](GOVERNANCE.md) for the full model and what upstream provides. > **Solo-maintained, fork-and-own.** This plugin is a starting point, not a vendor product. Issues are welcome as signals; pull requests are not accepted. See [GOVERNANCE.md](GOVERNANCE.md) for the full model and what upstream provides.
*KI-generert: all kode er produsert av Claude Code gjennom dialogdrevet utvikling. Hver endring er menneskestyrt, gjennomgått og validert før commit. Eierskap til output tilfaller brukeren etter Anthropic Consumer Terms §4; denne pluginen er lisensiert MIT.* *AI-generated: all code produced by Claude Code through dialog-driven development. [Full disclosure →](../../README.md#ai-generated-code-disclosure)*
![Version](https://img.shields.io/badge/version-1.10.0-blue) ![Version](https://img.shields.io/badge/version-1.8.2-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple) ![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Commands](https://img.shields.io/badge/commands-16-blue) ![Agents](https://img.shields.io/badge/agents-7-orange)
![Commands](https://img.shields.io/badge/commands-14-blue)
![Hooks](https://img.shields.io/badge/hooks-3-green)
![References](https://img.shields.io/badge/references-17-yellow)
![License](https://img.shields.io/badge/license-MIT-lightgrey) ![License](https://img.shields.io/badge/license-MIT-lightgrey)
## Table of Contents ---
- [Install](#install) ## Why This Exists
- [Hvorfor denne finnes](#hvorfor-denne-finnes)
- [Hva den gjør](#hva-den-gjør)
- [Non-goals](#non-goals)
- [Kom i gang](#kom-i-gang)
- [Bygget for norsk offentlig sektor](#bygget-for-norsk-offentlig-sektor)
- [Under panseret](#under-panseret)
- [Begrensninger](#begrensninger)
- [Changelog](#changelog)
- [Lisens](#lisens)
## Install Every organization has a strategy. Few manage to turn it into goals that teams actually work toward.
Bruk `https://`-formen. Klone-knappen i forgeens UI gir en `ssh://`-URL, og OKR (Objectives and Key Results) is a proven framework for that translation — used by Google and Intel, and documented in Norwegian practice: Digdir runs OKR in its product delivery model, NAV product teams use it at team level, Oslo Origo built and open-sourced its own OKR tracker, and FINN.no has more than six years of it. Every adopter named here has a public source in `skills/okr-offentlig-sektor/references/okr-sources.md` § 4; organizations we could not source are not named. But adopting OKR is hard. The methodology sounds simple ("write inspiring goals with measurable results") until you try it. Then you hit real questions:
`marketplace add` svarer med `Invalid git URL` — en melding som aldri nevner protokollen.
```bash - *How do we connect our OKR to the goals in our tildelingsbrev?*
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git - *What's a good Key Result vs. just an activity disguised as one?*
claude plugin install okr@ktg-plugin-marketplace - *How do we handle it when political priorities shift mid-cycle?*
``` - *Are we actually getting better at this, or just going through the motions?*
Eller aktiver den direkte i `~/.claude/settings.json` — en fungerende alternativ vei, This plugin is an AI coach that helps with all of these. It's built specifically for Norwegian public sector — 4-month tertial cycles, DFØ's "mål- og resultatstyring" framework, tillitsvalgt involvement, and the governance chain from Stortingsmelding to team OKR.
ikke en erstatning for de to kommandoene over:
```json ### What makes it different from a methodology guide?
{ "enabledPlugins": { "okr@ktg-plugin-marketplace": true } }
```
Binære innboks-formater (`docx`/`pdf`/`eml`) krever i tillegg A methodology guide explains OKR theory. This plugin *knows your organization*.
`npm install --ignore-scripts` i plugin-mappen; `txt`/`md`-ingestion og alle andre
kommandoer kjører uten avhengigheter. After a one-time onboarding conversation, it remembers your maturity level, your strategic documents, your current OKR, and your cultural challenges. Every interaction builds on that knowledge. When you ask it to write new OKR, it already knows what your organization is trying to achieve. When you check quality, it checks alignment against your actual strategy — not a generic rubric.
Over time, it tracks how your OKR practice evolves. It spots recurring mistakes, shows score trends, and coaches you based on where you are in the cycle. Early in the cycle? Focus on alignment. Midway? Time for a progress check. Near the end? Let's prepare the retrospective.
--- ---
## Hvorfor denne finnes ## What It Does
Hver organisasjon har en strategi. Få lykkes med å omsette den til mål teamene faktisk arbeider mot. ### From Strategy to OKR
OKR (Objectives and Key Results) er et velprøvd rammeverk for nettopp den oversettelsen — brukt av Google og Intel, og dokumentert i norsk praksis: Digdir kjører OKR i sin produktleveransemodell, NAV-produktteam bruker det på teamnivå, Oslo Origo bygde og har åpen kildekode-utgitt sin egen OKR-tracker, og FINN.no har over seks år med det. Hver adopter nevnt her har en offentlig kilde i `skills/okr-offentlig-sektor/references/okr-sources.md` § 4; organisasjoner vi ikke kunne kildebelegge er ikke navngitt. Men å innføre OKR er vanskelig. Metodikken virker enkel («skriv inspirerende mål med målbare resultater») helt til man prøver. Da møter man de virkelige spørsmålene: The core workflow: take your strategic goals (from virksomhetsplan, tildelingsbrev, etatsstrategi, or any strategic document) and transform them into well-structured OKR.
- *Hvordan kobler vi OKR-ene våre til målene i tildelingsbrevet?*
- *Hva er en god Key Result versus bare en aktivitet forkledd som en?*
- *Hvordan håndterer vi det når politiske prioriteringer skifter midt i syklusen?*
- *Blir vi faktisk bedre på dette, eller går vi bare gjennom motionene?*
Denne pluginen er en KI-coach som hjelper med alt dette. Den er bygget spesifikt for norsk offentlig sektor — 4-måneders tertialsykluser, DFØs rammeverk for mål- og resultatstyring, involvering av tillitsvalgte, og styringslinjen fra Stortingsmelding til team-OKR.
### Hva gjør den annerledes enn en metodikk-guide?
En metodikk-guide forklarer OKR-teori. Denne pluginen *kjenner organisasjonen din*.
Etter én engangs onboardingsamtale husker den modenhetsnivået ditt, strategiske dokumentene dine, dine nåværende OKR og dine kulturelle utfordringer. Hver interaksjon bygger på den kunnskapen. Når du ber den skrive nye OKR, vet den allerede hva organisasjonen din prøver å oppnå. Når du kvalitetssjekker, sjekker den alignment mot din faktiske strategi — ikke en generisk rubrikk.
Over tid følger den med på hvordan OKR-praksisen din utvikler seg. Den fanger opp gjentatte feil, viser scoretrender, og coacher deg basert på hvor du er i syklusen. Tidlig i syklusen? Fokuser på alignment. Halvveis? Tid for en fremdriftssjekk. Nær slutten? La oss forberede retrospektivet.
---
## Hva den gjør
### Fra strategi til OKR
Kjerneflyten: ta dine strategiske mål (fra virksomhetsplan, tildelingsbrev, etatsstrategi, eller et annet strategisk dokument) og omsett dem til godt strukturerte OKR.
``` ```
> /okr:skriv Vi vil gjøre søknadsprosessen raskere for innbyggerne > /okr:skriv We want to make the application process faster for citizens
``` ```
Pluginen leder deg gjennom å skrive et inspirerende Objective og målbare Key Results, kalibrere ambisjonsnivå, og sjekke alignment mot organisasjonens strategiske retning. The plugin guides you through writing an inspiring Objective and measurable Key Results, calibrating ambition, and checking alignment against your organization's strategic direction.
### Kvalitet utover stavekontroll ### Quality That Goes Beyond Spell-Check
``` ```
> /okr:kvalitet > /okr:kvalitet
``` ```
Sjekker OKR-ene dine mot 19 navngitte antipatterns (som «aktivitet forkledd som Key Result», «sandbagging», eller «sett-og-glem») og scorer kvalitet langs flere dimensjoner. Har OKR-ene dine problemer, får du konkrete forbedringsforslag — ikke bare «prøv hardere». Checks your OKR against 19 named antipatterns (like "activity disguised as a Key Result", "sandbagging", or "set-and-forget") and scores quality across multiple dimensions. If your OKR has issues, you get concrete improvement suggestions — not just "try harder."
### Strategisk alignment ### Strategy Alignment
``` ```
> /okr:kaskade > /okr:kaskade
``` ```
Kaskaderer OKR fra organisasjonsnivå ned til team. Viser hvilke strategiske mål som har teamstøtte og hvilke som er foreldreløse. Har du lagret OKR-ene på org-nivå, leser den dem automatisk. Cascades OKR from organizational level down to teams. Shows which strategic goals have team support and which are orphaned. If you've stored your org-level OKR, it reads them automatically.
### Gap-analyse ### Gap Analysis
``` ```
> /okr:gap > /okr:gap
``` ```
Sammenligner strategidokumentene dine (tildelingsbrev, virksomhetsplan) mot din nåværende OKR og produserer en dekningsmatrise: hvilke krav er fullt dekket, hvilke er delvis dekket, og hvilke er helt fraværende. For hvert gap foreslår den OKR-formuleringer. Compares your strategic documents (tildelingsbrev, virksomhetsplan) against your current OKR and produces a coverage matrix: which requirements are fully covered, which are partially covered, and which are completely missing. For each gap, it suggests OKR formulations.
### Fremdriftssporing ### Progress Tracking
``` ```
> /okr:sporing > /okr:sporing
``` ```
Beregner score (0.01.0), vurderer confidence-nivåer, genererer prognoser, og flagger Key Results i risiko. Bruker du Linear, kan den synkronisere status der også. Calculates scores (0.01.0), assesses confidence levels, generates forecasts, and flags Key Results that are at risk. If you use Linear, it can sync status there too.
### Tverrsyklus-læring ### Cross-Cycle Learning
``` ```
> /okr:analyse > /okr:analyse
``` ```
Etter din første fullførte syklus er dette der den egentlige verdien begynner. Leser dine arkiverte sykluser og viser: After your first completed cycle, this is where the real value begins. Reads your archived cycles and shows:
- **Scoretrender** — blir du bedre? Visualisert med diagrammer. - **Score trends** — are you improving? Visualized with charts.
- **Gjentatte antipatterns** — de samme feilene over sykluser blir flagget eksplisitt. - **Recurring antipatterns** — the same mistakes across cycles get flagged explicitly.
- **Alignment-progresjon** — blir teamenes dekning av org-målene bedre? - **Alignment progression** — is your team coverage of org goals getting better?
### Proaktiv coaching ### Proactive Coaching
Du trenger ikke huske å sjekke inn selv. Når du starter en sesjon, forteller pluginen deg automatisk hvor du er i syklusen og hva du bør fokusere på: You don't have to remember to check in. When you start a session, the plugin automatically tells you where you are in the cycle and what to focus on:
> *«OKR-coaching: Uke 12 av 16 i T1-2026. Nærmer seg syklusslutt — fokuser på endelig scoring og forberedelse av retrospektiv. 2 KR flagget som i risiko i siste statusoppdatering.»* > *"OKR coaching: Week 12 of 16 in T1-2026. Approaching cycle end — focus on final scoring and retrospective preparation. 2 KR flagged as at-risk in last status update."*
### Møter og fasilitering ### Meetings and Facilitation
``` ```
> /okr:møter > /okr:møter
``` ```
Genererer skreddersydde agendaer for OKR-workshops, check-ins, reviews og 1:1-samtaler etter CFR-rammeverket (Conversations, Feedback, Recognition) — tilpasset offentlig sektor der bonus ikke er en mulighet. Generates tailored agendas for OKR workshops, check-ins, reviews, and 1:1 conversations using the CFR (Conversations, Feedback, Recognition) framework — adapted for public sector where bonuses aren't an option.
### Innføring og endringsledelse ### Adoption and Change Management
``` ```
> /okr:innføring > /okr:innføring
``` ```
Planlegger du å innføre OKR i organisasjonen? Få en faset innføringsplan med strategier for motstandshåndtering, modenhetsvurdering, og veiledning i å involvere tillitsvalgte — fordi i norsk offentlig sektor fungerer ikke endringsledelse uten involvering av tillitsvalgte. Planning to introduce OKR in your organization? Get a phased rollout plan with resistance-handling strategies, maturity assessment, and guidance on involving tillitsvalgte — because in Norwegian public sector, change management without union involvement doesn't work.
### Styring og etterlevelse ### Governance and Compliance
``` ```
> /okr:governance > /okr:governance
``` ```
Oversetter tildelingsbrevkrav til OKR. Kartlegger styringslinjen (Stortingsmelding → tildelingsbrev → etatsstrategi → OKR). Verifiserer at OKR-dokumentasjonen din møter Riksrevisjonens standarder. Translate tildelingsbrev requirements into OKR. Map the governance chain (Stortingsmelding → tildelingsbrev → etatsstrategi → OKR). Verify that your OKR documentation meets Riksrevisjon standards.
### Eksport av leveranser ### Export Deliverables
``` ```
> /okr:export > /okr:export
``` ```
Rendrer alle OKR-leveranser — kvalitetsreview, gap-matrise, statusrapport, eller retrospektiv — til en utskriftsklar A4-PDF for ledelsen eller Riksrevisjonen. Tabeller får striping og scoreceller er fargekodet (grønn/gul/rød). PDF-generering er en dokumentert forutsetning (`pip install markdown weasyprint`, `brew install pango`), ikke medfulgt — en manglende avhengighet avsluttes med en tydelig installasjonshint, aldri en traceback. Renders any OKR deliverable — quality review, gap matrix, status report, or retrospective — to a print-ready A4 PDF for management or Riksrevisjon. Tables get striping and score cells are color-coded (green/yellow/red). PDF generation is a documented prerequisite (`pip install markdown weasyprint`, `brew install pango`), not bundled — a missing dependency exits with a clear install hint, never a traceback.
### Arkivklarhet ### Inbox Ingestion
```
> /okr:arkivklar
```
Rapporterer hvilke kategorier av bevaringsforskrifta (FOR-2025-12-19-2729) treets innhold sannsynligvis faller under — § 7 for den statlige styringslinjen, § 30 for den kommunale, og § 3-sikkerhetsnettet for alt som ikke er navngitt — og hva som bør *vurderes* for overføring til organisasjonens sak-/arkivsystem. De to styringslinjene er parallelle, ikke alternative: samme fil kan matche en kategori i begge, og begge rapporteres.
Kommandoen avgjør ingenting. Kassasjon krever myndighet fra Nasjonalarkivet under arkivlova § 13, og vurderingen selv hører til organisasjonens dokumentasjonsplan (arkivlova § 8, arkivforskrifta § 12 b). Det finnes **ingen slettemodell i denne pluginen, og ingen kommer** — kommandoen deklarerer verken `Write` eller `Edit`, så den er read-only i verktøylaget, ikke bare i prosa. Å falle under § 3 betyr at materialet *må bevares*, ikke det motsatte. Rapporten navngir også hva den ikke kan se: arkivforskrifta § 1 bokstav c gjør vurderingen betinget av om dokumentasjonen allerede forvaltes som arkiv i et annet system, og den tilstanden rapporteres som ukjent snarere enn antatt bort.
### Innboks-ingestion
``` ```
> /okr:innboks > /okr:innboks
``` ```
Slipp dokumenter — tildelingsbrev-PDF-er, virksomhetsplan-docx, møtenotater — i `.claude/okr/innboks/` og ta dem inn i kunnskapstreet i ett steg. Pipelinen er deterministisk (ingen KI i kjernen): konverter (txt/md/docx/eml/pdf) → del på headings → stemple OKF-frontmatter med en `kilde: innboks`-proveniensmarkør → rute etter dokumenttype → validere hvert dokument mot en streng sikkerhetsgate. Et dokument som feiler gaten forkastes alene med en per-dokument-rapport; originalene forblir alltid urørt i innboksen, og å kjøre på nytt på uendret input er en byte-identisk no-op. Kjente v1-begrensninger (flat PDF-ekstraksjon, tap av tabellstruktur) er listet i CHANGELOG. Drop documents — tildelingsbrev PDFs, virksomhetsplan docx, meeting notes — into `.claude/okr/innboks/` and ingest them into the knowledge tree in one sweep. The pipeline is deterministic (no LLM in the core): convert (txt/md/docx/eml/pdf) → split on headings → stamp OKF frontmatter with a `kilde: innboks` provenance marker → route by document type → validate each document against a strict security gate. A document that fails the gate is discarded alone with a per-document report; originals always stay untouched in the inbox, and re-running on unchanged input is a byte-identical no-op. Known v1 limitations (flat PDF extraction, table structure loss) are listed in the CHANGELOG.
### Syklusrapportering ### Help and Maintenance
```
> /okr:rapport
```
Genererer en rapport direkte fra KR-tallene i syklusens `okr-*.md`-frontmatter — deterministisk, så samme data gir byte-identisk output. Tre former, ett kommandoargument: `tertial` for løpende rapportering, `arsrapport` for del III av den lovpålagte årsrapporten («Årets aktiviteter og resultater»), og `etatsstyring` for underlaget til etatsstyringsmøtet. Forpliktende og ambisiøse Key Results rapporteres **separat** i alle tre, aldri aggregert til én figur: de to måles mot forskjellige standarder, og et kombinert gjennomsnitt er tvetydig av konstruksjon. Generatoren eier aritmetikken, tabellstrukturen og formateringsinvariantene (score angis alltid på 01.0-skalaen, aldri som prosent måloppnåelse; en forpliktende KR under mål flagges som et avvik, ikke som en middels score).
En ambisiøs KR med lav score presenteres **aldri** som et avvik — å lande nær 0.7 er forventet oppnåelse for den typen, og å la den ambisiøse scoren ha konsekvenser i rapporteringen ville bunte sammen mål og prognose igjen, noe som er det som gjør sandbagging strukturelt rasjonelt i neste syklus.
Den eier **ikke** confidence, heller ikke vurderingsfeltene den reserverer — den per-Objective-vurderingen av måloppnåelse i årsrapporten, og kartleggingen fra Objectives til tildelingsbrevets styringsparametere. Disse er latt som markerte plassholdere fordi de krever vurdering eller kilder generatoren ikke leser. Confidence-kolonnen er på samme måte latt tom med vilje og fylles inn under `/okr:sporing`, fordi confidence er en sannsynlighetsvurdering — kanonen utpeker én kilde til sannhet for On Track/At Risk/Off Track-skalaen og avviser eksplisitt å avlede den mekanisk fra score. Score selv skrives aldri til disk; de beregnes på nytt fra `baseline`/`target`/`naa` hver kjøring så en andre, driftende sannhetskilde ikke kan oppstå.
### Hjelp og vedlikehold
``` ```
> /okr:help > /okr:help
> /okr:freshen-references > /okr:freshen-references
``` ```
`/okr:help` lister alle 16 kommandoer, 7 agenter, og en anbefalt arbeidsflyt nøkket til hvor du er i tertialsyklusen. `/okr:freshen-references` er en kunnskapsbase-selvevaluator: den scorer 18 av de 19 domenereferansefilene mot en ankret kvalitetsrubrikk — kvalitetsrubrikken selv er utelatt, siden å score scoringsinstrumentet er sirkulært — og poller navngitte offentlige kilder (UN EGDI, EU eGov Benchmark, OECD DGI, Digdir, WCAG, forvaltningsloven) for å flagge utdaterte «Sist oppdatert»-markører. `/okr:help` lists all 14 commands, 7 agents, and a recommended workflow keyed to where you are in the tertial cycle. `/okr:freshen-references` is a knowledge-base self-evaluator: it scores 16 of the 17 domain reference files against an anchored quality rubric — the quality rubric itself is excluded, since scoring the scoring instrument is circular — and polls named public sources (UN EGDI, EU eGov Benchmark, OECD DGI, Digdir, WCAG, forvaltningsloven) to flag outdated `Sist oppdatert` markers.
--- ---
## Non-goals ## Getting Started
Ting denne pluginen bevisst ikke gjør, så du kan avgjøre tidlig om den passer: ### Install
- **Den er ikke et OKR-oppfølgingssystem.** Det finnes ingen database, ingen server Add to your Claude Code plugin configuration:
og ingen web-UI. All tilstand er Markdown-filer under `.claude/okr/` i ditt eget
prosjekt, pluss en maskin-global org-profil i `~/.claude/okr/org/`. Linear er den
eneste oppfølgingsintegrasjonen, og den er valgfri.
- **Den gjør ikke organisasjonens vurderinger.** `/okr:rapport` eier aritmetikken og
formateringsinvariantene; den per-Objective-vurderingen av måloppnåelse og
kartleggingen til tildelingsbrevets styringsparametere er latt som markerte
plassholdere. Confidence er en vurdering, aldri avledet fra score.
- **Den avgjør ikke arkivering, og sletter aldri.** `/okr:arkivklar` produserer et
vurderingsgrunnlag mot bevaringsforskrifta — hva som bør *vurderes* for overføring
til sak-/arkivsystemet. Den er read-only etter verktøylisten.
- **Den er ikke et flerbrukersamarbeidsverktøy.** Én operatør, lokale filer, ingen
kontoer, ingen delt tilstand.
- **Den erstatter ikke metodikklitteraturen.** Kunnskapsbasen er en kildebelagt
arbeidsreferanse, ikke en erstatning for Doerr, Wodtke eller DFØs egen veiledning.
--- ```json
{
"enabledPlugins": {
"okr@ktg-plugin-marketplace": true
}
}
```
## Kom i gang ### First Conversation
Installer først — se [Install](#install) øverst.
### Første samtale
``` ```
> /okr:oppsett mvp > /okr:oppsett mvp
``` ```
Seks spørsmål, fem minutter. Pluginen blir kjent med organisasjonen din, modenhetsnivå, verktøy og syklusfase. Fra det punktet tilpasser hver kommando seg til konteksten din. Six questions, five minutes. The plugin learns your organization, maturity level, tools, and cycle phase. From that point on, every command adapts to your context.
Vil du ha den fulle opplevelsen? `/okr:oppsett full` kjører et dypere intervju (15-20 minutter) som også fanger opp strategisk kontekst, kulturelle utfordringer og organisasjonsstruktur. Want the full experience? `/okr:oppsett full` runs a deeper interview (15-20 minutes) that also captures your strategic context, cultural challenges, and organizational structure.
### Typisk arbeidsflyt ### Typical Workflow
**Starter fra scratch:** **Starting fresh:**
``` ```
/okr:oppsett full # Engangs oppsett /okr:oppsett full # One-time setup
/okr:skriv # Skriv din første OKR /okr:skriv # Write your first OKR
/okr:kvalitet # Kvalitetssjekk /okr:kvalitet # Quality check
/okr:kaskade # Alignér med org-mål /okr:kaskade # Align with org goals
``` ```
**Under en syklus:** **During a cycle:**
``` ```
/okr:sporing # Oppdater fremdrift /okr:sporing # Update progress
/okr:møter # Forbered check-in-møte /okr:møter # Prepare check-in meeting
``` ```
**Ved syklusslutt:** **End of cycle:**
``` ```
/okr:oppsett arkiver # Arkiver med retrospektiv /okr:oppsett arkiver # Archive with retrospective
/okr:analyse # Se hvordan trenden går /okr:analyse # See how you're trending
/okr:skriv # Start neste syklus /okr:skriv # Start the next cycle
``` ```
--- ---
## Bygget for norsk offentlig sektor ## Built for Norwegian Public Sector
### Hva er annerledes med OKR i offentlig sektor? ### What's different about OKR in offentlig sektor?
| Utfordring | Hvordan pluginen hjelper | | Challenge | How the plugin helps |
|-----------|---------------------| |-----------|---------------------|
| 4-måneders tertialsykluser (ikke kvartal) | Innebygget tertialstøtte med korrekt ukeberegning og fasedeteksjon | | 4-month tertial cycles (not quarterly) | Built-in tertial support with correct week counting and phase detection |
| Tildelingsbrevet driver prioriteringer | `/okr:governance` og `/okr:gap` oversetter og sjekker dekning | | Tildelingsbrev drives priorities | `/okr:governance` and `/okr:gap` translate and check coverage |
| DFØ bruker annen terminologi | Innebygget kobling mellom «mål- og resultatstyring» og OKR-begreper | | DFØ uses different terminology | Built-in mapping between "mål- og resultatstyring" and OKR concepts |
| Riksrevisjonen krever dokumentasjon | Revisjonsklart format med baseline, mål og datakilde per KR | | Riksrevisjonen requires documentation | Audit-ready format with baseline, target, data source per KR |
| Ingen bonusordninger | CFR-rammeverk designet for anerkjennelse uten økonomiske incentiver | | No performance bonuses | CFR framework designed for recognition without financial incentives |
| Tillitsvalgte må involveres | Veiledning om drøftingsplikt, transparente KR, og involvering av tillitsvalgte | | Tillitsvalgte must be involved | Guidance on drøftingsplikt, transparent KR, and union involvement |
| Konsensuskultur | Råd kalibrert for organisasjoner der stretch-mål kjennes risikabelt | | Consensus culture | Advice calibrated for organizations where stretch goals feel risky |
| Politiske prioriteringer kan skifte midt i syklusen | Strategier for å håndtere mandatendringer midt i syklusen | | Political priorities can shift mid-cycle | Strategies for handling mid-cycle mandate changes |
### Styringslinjen ### The governance chain
``` ```
Stortingsmelding / NTP Stortingsmelding / NTP
@ -294,122 +224,83 @@ Tildelingsbrev fra departement
| |
Etatsstrategi / Virksomhetsplan Etatsstrategi / Virksomhetsplan
| |
Org-OKR (årlig, strategisk) Org-level OKR (annual, strategic)
| |
Syklus-OKR (tertial, taktisk) Cycle OKR (tertial, tactical)
| |
Team-OKR Team OKR
``` ```
Pluginen forstår denne hierarkien og hjelper deg holde alignment på hvert nivå. The plugin understands this hierarchy and helps you maintain alignment at every level.
### Den dokumenterte problemklassen
Designet retter seg mot en problemklasse som er beskrevet av revisjonsmyndigheten
selv, ikke en generell påstand om styring i offentlig sektor.
**Les datoene før funnene.** Alle de fire revisjonene under ble gjennomført i
**20192020**. Riksrevisjonen har siden **avsluttet disse sakene etter forbedringer
i mål- og resultatstyringen** (DFØ-notat 2026:2, fn. 152 og fn. 217). De siteres her
som historisk dokumenterte svakheter forvaltningen har adressert — ikke som aktuell
kritikk av noen etat.
Alle fire er kapitler i **Dokument 1** (den årlige regnskaps- og
etterlevelsesrevisjonen), ikke i Dokument 3-serien for forvaltningsrevisjon. Stabile
per-kapittel-URL-er finnes ikke, så hver siteres gjennom DFØs sammenstilling med side
og fotnote.
| Dokumentert funn (20192020) | Kilde | Problemklasse pluginen arbeider på |
|---|---|---|
| Årsrapportene gir informasjon om aktiviteter og tjenesteleveranser, og i begrenset grad om effekter — tilskrevet målingsproblemer, inkludert manglende statistikk | Riksrevisjonen (2019), del av Dokument 1 (20192020), som gjengitt i DFØ-notat 2026:2 s. 53 (fn. 253) | Leading/lagging-balanse som rubrikkdimensjon; årsrapport-generering (`/okr:rapport arsrapport`) |
| Årsrapportfremstillingen viste etatens egen analyse av måloppnåelse i mindre grad, og fremstod i stedet som et snapshot av aktiviteter og resultater (NIBIO, 2020) | Riksrevisjonen (2020), del av Dokument 1, som gjengitt i DFØ-notat 2026:2 s. 4748 (fn. 216) | Antipatternet rapport-generatoren er bygget for å unngå |
| Departementet hadde ikke etablert et system for mål- og resultatstyring som ga tilstrekkelig informasjon om hvor effektivt etaten brukte ressursene sine (Havforskningsinstituttet, 2019) | Riksrevisjonen (2019), del av Dokument 1 (20192020), som gjengitt i DFØ-notat 2026:2 s. 48 (fn. 222) | Gevinstrealisering koblet til OKR i stedet for behandlet som et eget spor |
| Styringsinformasjonen ga ikke et tilstrekkelig grunnlag for prioritering (Luftfartstilsynet og Havforskningsinstituttet, 2019) | Riksrevisjonen (2019), del av Dokument 1 (20192020), som gjengitt i DFØ-notat 2026:2 s. 47 (fn. 210211) | Gap-analyse mot tildelingsbrev (`/okr:gap`) |
Det positive mandatet for hva `/okr:governance` og `/okr:gap` leser ut av et
tildelingsbrev er separat: departementet **skal** sette styringsparametere for å
vurdere måloppnåelse og resultater, og sette krav til årsrapporten — *Bestemmelser
om økonomistyring i staten* punkt 1.5 (DFØ-notat 2026:2 s. 49, fn. 228).
**Hva som ikke påstås her.** Disse sitatene er DFØs gjengivelse av Riksrevisjonen,
ikke Riksrevisjonens egen ordlyd; NIBIO-funnet spesielt er ikke verifisert mot
primærkilden. Ingenting over sier at pluginen løser disse funnene, at funnene er
åpne, eller at noen etat er mangelfull i dag. Et separat konsulenttall fra 2021 som
noen ganger siteres i denne konteksten er utelatt med vilje: det er ikke
Riksrevisjonens, og utvalget er ukjent.
--- ---
## Under panseret ## Under the Hood
### Agenter ### Agents
7 spesialiserte agenter aktiveres automatisk: 7 specialized agents activate automatically:
| Agent | Rolle | | Agent | Role |
|-------|-------| |-------|------|
| kvalitetssjekker | Scorer kvalitet, oppdager antipatterns, sjekker alignment | | kvalitetssjekker | Score quality, detect antipatterns, check alignment |
| kaskadebygger | Bygger kaskader mellom organisasjonsnivåer | | kaskadebygger | Build cascades between organizational levels |
| fremdriftssporer | Beregner score, prognoserer fullføring, flagger risiko | | fremdriftssporer | Calculate scores, forecast completion, flag risks |
| møtefasilitator | Genererer møteagendaer og fasiliteringsmateriale | | møtefasilitator | Generate meeting agendas and facilitation materials |
| styringsrådgiver | Styringsanalyse, tildelingsbrev-oversettelse, revisjonsetterlevelse | | styringsrådgiver | Governance analysis, tildelingsbrev translation, audit compliance |
| gapanalytiker | Dekningsmatrise mellom strategidokumenter og OKR | | gapanalytiker | Coverage matrix between strategic documents and OKR |
| trendanalytiker | Tverrsyklus-mønsteranalyse med trendvisualisering | | trendanalytiker | Cross-cycle pattern analysis with trend visualization |
### Hooks ### Hooks
| Event | Hva den gjør | | Event | What it does |
|-------|-------------| |-------|-------------|
| SessionStart | Proaktiv coaching — forteller deg hvor du er i syklusen og hva du bør fokusere på | | SessionStart | Proactive coaching — tells you where you are in the cycle and what to focus on |
| UserPromptSubmit | Injiserer organisasjonsprofilen din og tilgjengelige kontekstfiler i hver interaksjon | | UserPromptSubmit | Injects your organization profile and available context files into every interaction |
| PreCompact | Bevarer OKR-utkasttilstand hvis samtalen blir lang | | PreCompact | Preserves OKR draft state if the conversation gets long |
### Skills ### Skills
| Skill | Rolle | | Skill | Role |
|-------|-------| |-------|------|
| okr-offentlig-sektor | Kjerne-OKR-metodikk og domenekunnskap om norsk offentlig sektor | | okr-offentlig-sektor | Core OKR methodology and Norwegian public-sector domain knowledge |
| okr-second-brain-search | On-demand-henting fra din personlige OKF-wiki (`.claude/okr/` + `~/.claude/okr/org/`) — i fri chat og under `/okr:*`-kommandoer, uten å forhåndsinjisere alt | | okr-second-brain-search | On-demand retrieval from your personal OKF wiki (`.claude/okr/` + `~/.claude/okr/org/`) — in free chat and during `/okr:*` commands, without pre-injecting everything |
### Avhengigheter ### Dependencies
Pluginens kjerne er avhengighetsfri (kun `node:`-builtins). Innboks-ingestion-pipelinen (`/okr:innboks`, v1.7.0) er det ene bevisste unntaket: den trenger fire rene JS-konverteringsbiblioteker, eksakt pinnet i `package.json` (ingen `^`/`~`-ranges) — `mammoth` (docx), `turndown` (HTML→markdown), `postal-mime` (eml), `unpdf` (PDF). Installer med `npm install --ignore-scripts` (repoets `.npmrc` håndhever `ignore-scripts=true` som supply-chain-beskyttelse); krever Node >= 22. Alt annet i pluginen kjører uten `node_modules/`. The plugin core is zero-dependency (`node:` builtins only). The inbox-ingestion pipeline (`/okr:innboks`, v1.7.0) is the one deliberate exception: it needs four pure-JS conversion libraries, exact-pinned in `package.json` (no `^`/`~` ranges) — `mammoth` (docx), `turndown` (HTML→markdown), `postal-mime` (eml), `unpdf` (PDF). Install with `npm install --ignore-scripts` (the repo `.npmrc` enforces `ignore-scripts=true` as supply-chain protection); requires Node >= 22. Everything else in the plugin runs without `node_modules/`.
### Kunnskapsbase ### Knowledge Base
19 referansefiler som dekker OKR-metodikk, styring i norsk offentlig sektor (statlig og kommunal styringslinje), antipatterns, møteguider, metrikkbibliotek, ankrede kvalitetsrubrikker, integrasjonsmønstre, og mer. Pluginen leser bare det som er relevant for hver interaksjon — aldri hele biblioteket samtidig. 17 reference files covering OKR methodology, Norwegian public sector governance, antipatterns, meeting guides, metrics library, anchored quality rubrics, integration patterns, and more. The plugin reads only what's relevant to each interaction — never the whole library at once.
### Persistent kontekst ### Persistent Context
``` ```
.claude/ .claude/
okr.local.md # Din organisasjonsprofil okr.local.md # Your organizational profile
okr/ okr/
strategisk-kontekst/ # Strategiske dokumenter du oppgir strategisk-kontekst/ # Strategic documents you provide
syklus/T1-2026/ # Aktiv syklus' OKR og status syklus/T1-2026/ # Active cycle OKR and status
historikk/ # Arkiverte sykluser med retrospektiver historikk/ # Archived cycles with retrospectives
dokumenter/ # Andre relevante dokumenter dokumenter/ # Any other relevant documents
``` ```
--- ---
## Begrensninger ## Limitations
- Designet for norsk offentlig sektor — kan kreve tilpasning for andre kontekster - Designed for Norwegian public sector — may not fit other contexts without adaptation
- Ikke testet i et reelt OKR-program over flere sykluser - Not yet tested in a real multi-cycle OKR program
- Linear er den eneste støttede oppfølgingsintegrasjonen - Linear is the only supported tracking integration
- Norsk språk for alle brukervendte interaksjoner - Norwegian language for all user-facing interactions
--- ---
## Changelog ## Version History
Full detalj i [CHANGELOG.md](CHANGELOG.md). Høydepunkter: | Version | Date | Highlights |
| Versjon | Dato | Høydepunkter |
|---------|------|------------| |---------|------|------------|
| **1.10.0** | 2026-08-10 | Det uwirede leddet: `/okr:sporing` skriver nå `status.md` deterministisk via `scripts/syklus-status.mjs` i stedet for å formulere fila selv. Radene bygges med samme kode som tertialrapporten, så score og avvik er de samme bytene i begge — confidence kommer inn som data fra kommandoen, aldri utledet av tallene. Lukket vokabular (`On Track`/`At Risk`/`Off Track`) og full dekning håndheves i kode; en håndholdt `status.md` uten `kilde: sporing` overskrives aldri. Suite 329 → 343 |
| **1.9.0** | 2026-08-09 | «Styringssløyfa lukkes» — tildelingsbrev til årsrapport i én kjede. Tre deterministiske rapportgeneratorer (tertialrapport, årsrapport del III, etatsstyringsmøte-underlag) bygget på en KR-datakontrakt der score alltid beregnes og aldri lagres; `/okr:arkivklar` som rent lesende vurderingsgrunnlag mot bevaringsforskrifta (§§ 7/30, safety-net § 3) med path-confinement i delt modul; committed/aspirational separert i alle tre rapportformene med sandbagging-vakt; leading/lagging og guardrail som KR-designmønster i rubrikk, `skriv` og `kvalitet`; ny referansefil `gevinstrealisering-okr.md`, KOSTRA-seksjon og Beyond Budgeting/strategi→OKR i framework (17 → 19 filer); `okf_version` flyttet til rot-indeksens frontmatter med permanent lesetoleranse for begge plasseringer. Suite 197 → 315 |
| **1.8.2** | 2026-07-31 | Patch: siterte markørverdier korrumperte begge rot-markørene — `okf_version: "0.2"` (formen OKF-specen selv viser) ble avvist som ikke-upstream, flyttet til `okf_layout`, og `okf_version` nedgradert til vår konstant. Anførselstegn strippes nå ved parse (før tolkning og før emisjon) og verdien emitteres unquoted; unquote er konservativ (kun matchende par). Suite 187 → 192 | | **1.8.2** | 2026-07-31 | Patch: siterte markørverdier korrumperte begge rot-markørene — `okf_version: "0.2"` (formen OKF-specen selv viser) ble avvist som ikke-upstream, flyttet til `okf_layout`, og `okf_version` nedgradert til vår konstant. Anførselstegn strippes nå ved parse (før tolkning og før emisjon) og verdien emitteres unquoted; unquote er konservativ (kun matchende par). Suite 187 → 192 |
| **1.8.1** | 2026-07-25 | Patch: `okf_version`/`okf_layout`-splitt (OKF-spec §12) — rot-`index.md` bærer nå to distinkte markører i stedet for én som bar to urelaterte konsepter: `okf_version` = upstream OKF-versjonen (`0.1`, verdisett eid av Google), `okf_layout` = pluginens egen layout-revisjon (`kb-layout-2026-06`). Automatisk migrasjonssti for eksisterende bundles (layout-verdi i `okf_version` flyttes verbatim); `okf-check` ekkoer begge; `--okf-layout` erstatter `--okf-version` (beholdt som deprecated alias m/ stderr-varsel) | | **1.8.1** | 2026-07-25 | Patch: `okf_version`/`okf_layout`-splitt (OKF-spec §12) — rot-`index.md` bærer nå to distinkte markører i stedet for én som bar to urelaterte konsepter: `okf_version` = upstream OKF-versjonen (`0.1`, verdisett eid av Google), `okf_layout` = pluginens egen layout-revisjon (`kb-layout-2026-06`). Automatisk migrasjonssti for eksisterende bundles (layout-verdi i `okf_version` flyttes verbatim); `okf-check` ekkoer begge; `--okf-layout` erstatter `--okf-version` (beholdt som deprecated alias m/ stderr-varsel) |
| **1.8.0** | 2026-07-25 | «Én kanon» — metodekonsolidering etter dyp review: én kanonisk confidence-tabell og én kadens-doktrine i `okr-framework.md` med konsument-dedup på tvers av referanser, kommandoer og agenter; score→modenhet-avledningen fjernet; `committed` = forpliktelse OG påvirkbarhet; kvalitetsvurdering ankret i rubrikken (alle 10 dimensjoner, ingen parallelle scorebånd); ekte antipattern-kategorier; milepæl-/binær-unntak; «align, don't cascade» i kaskadeflatene; markedsclaims kildebelagt + nye kilder (NCT, EBM 2024); ny konsistensvakt `tests/canon-consistency.test.mjs` (suite 149 → 167 cases) | | **1.8.0** | 2026-07-25 | «Én kanon» — metodekonsolidering etter dyp review: én kanonisk confidence-tabell og én kadens-doktrine i `okr-framework.md` med konsument-dedup på tvers av referanser, kommandoer og agenter; score→modenhet-avledningen fjernet; `committed` = forpliktelse OG påvirkbarhet; kvalitetsvurdering ankret i rubrikken (alle 10 dimensjoner, ingen parallelle scorebånd); ekte antipattern-kategorier; milepæl-/binær-unntak; «align, don't cascade» i kaskadeflatene; markedsclaims kildebelagt + nye kilder (NCT, EBM 2024); ny konsistensvakt `tests/canon-consistency.test.mjs` (suite 149 → 167 cases) |
@ -421,15 +312,15 @@ Full detalj i [CHANGELOG.md](CHANGELOG.md). Høydepunkter:
| **1.4.0** | 2026-06-24 | Domenedybde + kildeintegritet (Fase 1), UX-/kommando-/agent-modning (Fase 2), hybrid org-kontekst-lesing | | **1.4.0** | 2026-06-24 | Domenedybde + kildeintegritet (Fase 1), UX-/kommando-/agent-modning (Fase 2), hybrid org-kontekst-lesing |
| 1.3.2 | 2026-06-24 | Fix: loopende Stop-hook fjernet | | 1.3.2 | 2026-06-24 | Fix: loopende Stop-hook fjernet |
| 1.3.1 | 2026-06-23 | Fakta- og troverdighetssanering | | 1.3.1 | 2026-06-23 | Fakta- og troverdighetssanering |
| **1.3.0** | 2026-04-08 | Gap-analyse, tverrsyklus-analytics med Mermaid-visualiseringer, proaktiv SessionStart-coaching | | **1.3.0** | 2026-04-08 | Gap analysis, cross-cycle analytics with Mermaid visualizations, proactive SessionStart coaching |
| **1.1.0** | 2026-04-08 | Persistent kontekst, dypere onboarding, kontekstbevisste kommandoer, syklusarkivering | | **1.1.0** | 2026-04-08 | Persistent context, deep onboarding, context-aware commands, cycle archival |
| **1.0.0** | 2026-04-08 | Arkitekturoverhaling, selvstendige kommandoer, hooks, markedsplass-klar | | **1.0.0** | 2026-04-08 | Architecture overhaul, self-contained commands, hooks, marketplace-ready |
| 0.5.0 | 2026-02-07 | Første formelle versjon | | 0.5.0 | 2026-02-07 | First formal version |
Se [CHANGELOG.md](CHANGELOG.md) for alle detaljer. See [CHANGELOG.md](CHANGELOG.md) for full details.
--- ---
## Lisens ## License
[MIT](LICENSE) [MIT](LICENSE)

View file

@ -11,7 +11,7 @@
If you discover a security vulnerability, please: If you discover a security vulnerability, please:
1. **Do not** open a public issue 1. **Do not** open a public issue
2. Email <security@fromaitochitta.com>, or report privately via the Forgejo repository: https://git.fromaitochitta.com/open/okr 2. Email the maintainer directly, or report privately via the Forgejo repository (git.fromaitochitta.com/open/okr)
3. Include: 3. Include:
- Description of the vulnerability - Description of the vulnerability
- Steps to reproduce - Steps to reproduce

View file

@ -61,10 +61,7 @@ Score = (Nåværende - Baseline) / (Target - Baseline)
2. **Beregn score**: 2. **Beregn score**:
- Per KR - Per KR
- Oppsummert **per type**, aldri som ett felles tall: committed rapporteres som - Samlet (vektet gjennomsnitt)
hvor mange KR som har nådd kravet, aspirational som snittet på tvers av
aspirational-KR. De to typene måles mot hver sin målestokk, og et aggregat på
tvers av dem er tvetydig (`okr-framework.md`, committed/aspirational-skillet).
3. **Vurder confidence**: 3. **Vurder confidence**:
- Basert på trend og gjenstående tid - Basert på trend og gjenstående tid
@ -93,29 +90,14 @@ Score = (Nåværende - Baseline) / (Target - Baseline)
### Objective: [tekst] ### Objective: [tekst]
#### Committed Key Results | KR | Baseline | Target | Nå | Score | Trend | Status |
|----|----------|--------|-----|-------|-------|--------|
| KR1: [kort] | X | Y | Z | 0.XX | ↗️/→/↘️ | ✅/⚠️/❌ |
| KR2: [kort] | X | Y | Z | 0.XX | ↗️/→/↘️ | ✅/⚠️/❌ |
| KR3: [kort] | X | Y | Z | 0.XX | ↗️/→/↘️ | ✅/⚠️/❌ |
| KR | Baseline | Target | Nå | Score | Trend | Avvik | Confidence | **Samlet score:** 0.XX
|----|----------|--------|-----|-------|-------|-------|------------| **Confidence:** [On Track 🟢 | At Risk 🟡 | Off Track 🔴]
| KR1: [kort] | X | Y | Z | 0.XX | ↗️/→/↘️ | Ja/Nei | [nivå] |
| KR2: [kort] | X | Y | Z | 0.XX | ↗️/→/↘️ | Ja/Nei | [nivå] |
**Committed: N av M KR har nådd kravet.** Kravet er nådd eller ikke — hvert avvik
forklares for seg, og de summeres ikke til ett tall.
**Confidence, committed:** [On Track 🟢 | At Risk 🟡 | Off Track 🔴]
#### Aspirational Key Results
| KR | Baseline | Target | Nå | Score | Trend | Avvik | Confidence |
|----|----------|--------|-----|-------|-------|-------|------------|
| KR3: [kort] | X | Y | Z | 0.XX | ↗️/→/↘️ | - | [nivå] |
**Snitt aspirational: 0.XX** over N KR. Rundt 0.7 er forventet måloppnåelse —
en lav score her er ikke et avvik.
**Confidence, aspirational:** [On Track 🟢 | At Risk 🟡 | Off Track 🔴]
Nivået i `[nivå]`-cellene settes fra den kanoniske confidence-tabellen i
`okr-framework.md` — ikke fra score.
--- ---

View file

@ -32,7 +32,7 @@ Org-kontekst kan være sendt med i Task-prompten fra den kallende kommandoen —
## Din oppgave ## Din oppgave
Scor mot ALLE 11 dimensjonene i den kanoniske rubrikken Scor mot ALLE 10 dimensjonene i den kanoniske rubrikken
(`${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-quality-rubrics.md`, (`${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-quality-rubrics.md`,
fem ankere per dimensjon). Bær ikke et eget scorebånd — rubrikkfila eier anker → skala. fem ankere per dimensjon). Bær ikke et eget scorebånd — rubrikkfila eier anker → skala.
@ -43,13 +43,12 @@ fem ankere per dimensjon). Bær ikke et eget scorebånd — rubrikkfila eier ank
- **Scope** — riktig dimensjonert for én tertial - **Scope** — riktig dimensjonert for én tertial
- **Alignment** — koblet oppover til org-OKR/tildelingsbrev - **Alignment** — koblet oppover til org-OKR/tildelingsbrev
2. **Scor hvert Key Result** mot de 6 Key Result-dimensjonene: 2. **Scor hvert Key Result** mot de 5 Key Result-dimensjonene:
- **Målbarhet** — konkrete tall med baseline → target - **Målbarhet** — konkrete tall med baseline → target
- **Outcome** — reell effekt heller enn output/aktivitet - **Outcome** — reell effekt heller enn output/aktivitet
- **Ambisjon** — riktig stretch (~70 %), ikke sandbagget - **Ambisjon** — riktig stretch (~70 %), ikke sandbagget
- **Datakilde** — spesifisert og faktisk tilgjengelig - **Datakilde** — spesifisert og faktisk tilgjengelig
- **Uavhengighet** — teamet kan påvirke utfallet vesentlig (påvirkning, ikke nødvendigvis full kontroll) - **Uavhengighet** — teamet kan påvirke utfallet vesentlig (påvirkning, ikke nødvendigvis full kontroll)
- **Leading/lagging-balanse** — kobler en leading-indikator som måles underveis til et lagging-utfall som bekrefter effekten, med erkjent tidsforsinkelse
3. **Sjekk for antipatterns** fra `references/okr-antipatterns.md`: 3. **Sjekk for antipatterns** fra `references/okr-antipatterns.md`:
- Aktivitetsorientert - Aktivitetsorientert

View file

@ -1,89 +0,0 @@
---
name: okr:arkivklar
description: Vurderingsgrunnlag mot bevaringsforskrifta - hva i OKR-treet faller trolig under bevaringspaabudet, og hva boer vurderes overfoert til sak-arkivet
allowed-tools: Read, Bash, Glob
argument-hint: "[bundle-rot - default .claude/okr]"
---
# OKR Arkivklar - vurderingsgrunnlag mot bevaringsforskrifta
Gaa gjennom OKR-treet og rapporter hvilke kategorier i bevaringsforskrifta
(FOR-2025-12-19-2729) innholdet trolig faller under, og hva som boer **vurderes
overfoert** til virksomhetens sak-/arkivsystem.
## Hva denne kommandoen IKKE gjoer
**Den sletter ingenting, og den kan ikke slette noe.** Tool-lista over
inneholder verken `Write` eller `Edit` — kommandoen er lesende i verktoeylaget,
ikke bare i prosaen. Presedens: `commands/sporing.md`.
**Den tar heller ingen avgjoerelse.** Kassasjon — «å gjere til inkjes
dokumentasjon slik at han ikkje lenger finst» (arkivlova § 2 g) — krever enten
forskriftshjemmel fra Nasjonalarkivet eller loeyve derfra (arkivlova § 13).
Selve vurderingen hoerer til virksomhetens dokumentasjonsplan, jf. arkivlova § 8
og arkivforskrifta § 12 b. Ingen av delene kan avledes fra en filtype, og
verktoeyet forsoeker det ikke.
Det finnes **ingen slettemodell i denne pluginen**, og ingen skal innfoeres.
Trenger du aa arkivere en syklus eller ta ut innholdet, er det
`/okr:oppsett arkiver` og `/okr:export` som daekker det behovet.
## Kjoering
```bash
# Rapport til skjerm (default)
node "${CLAUDE_PLUGIN_ROOT}/scripts/arkivklar.mjs" .claude/okr
# Rapport til fil (skrives atomisk, ALDRI inn i bundlen selv)
node "${CLAUDE_PLUGIN_ROOT}/scripts/arkivklar.mjs" .claude/okr arkivklar-rapport.md
```
Exit-koder: `0` rapport produsert · `1` uventet feil (inkludert brudd paa
path-confinement) · `2` bruksfeil (manglende eller ukjent sti).
Kjoerer brukeren kommandoen uten argument, bruk `.claude/okr` som bundle-rot.
## Hva rapporten inneholder
| Seksjon | Paragraf | Innhold |
|---------|----------|---------|
| Statleg styringslinje | bevaringsforskrifta § 7 | tildelingsbrev, rapportar, etatsstyringsmoeter, evalueringa |
| Kommunal styringslinje | bevaringsforskrifta § 30 | handlingsprogram, tertialrapportering, aarsmelding |
| Ikke navngitt | bevaringsforskrifta § 3 | alt annet — **skal bevares** inntil Nasjonalarkivet har fastsett reglar |
De to styringslinjene er **parallelle, ikke alternative**. Samme fil kan treffe en
kategori i begge, og da rapporteres begge — verktoeyet velger ikke styringslinje
paa virksomhetens vegne.
At noe havner under § 3 betyr at det skal **bevares**, ikke det motsatte.
Sikringsparagrafen fastsetter at dokumentasjon som ikke er nevnt i forskrifta
skal takast vare paa inntil Nasjonalarkivet har fastsett reglar.
## Den ukjente forutsetningen
Arkivforskrifta § 1 bokstav c gjoer vurderingen betinget av om dokumentasjonen
**allerede blir sikra og forvalta som arkiv i eit anna informasjonssystem**.
Verktoeyet leser kun OKR-treet og ser ikke sak-/arkivsystemet. Rapporten navngir
derfor tilstanden som **ukjent** i stedet for aa anta den bort — det er en
avklaring virksomheten maa gjoere selv.
## Naar bruke
- Foer en syklus arkiveres, for aa se hva som boer over i sak-arkivet.
- Ved etablering av dokumentasjonsplan, som innspill om hva OKR-arbeidet faktisk
produserer av arkivpliktig materiale.
- Ved sporsmaal om personopplysninger i treet — se GDPR-posisjonen i
`references/okr-offentlig-governance.md`. Kortversjonen: pluginen sletter
ikke, den sperrer.
## Etter kjoering
Presenter rapporten, og vaer eksplisitt paa at den er et **grunnlag for
vurdering**, ikke en konklusjon. Foreslaa at virksomhetens arkivfaglige
ressurs tar stilling til overfoering. Ikke tilby aa slette noe.
## Referanser
- `references/okr-offentlig-governance.md` — GDPR-posisjonen og arkivregelverket
- Arkivlova LOV-2025-06-20-96 (i kraft 01.01.2026)
- Bevaringsforskrifta FOR-2025-12-19-2729 · arkivforskrifta FOR-2025-12-17-2647

View file

@ -1,6 +1,6 @@
--- ---
name: okr:freshen-references name: okr:freshen-references
description: KB-selvevaluator og currency-polling — scorer de 18 domene-referansefilene mot en ankret kvalitetsrubrikk og oppdager utdaterte kilder description: KB-selvevaluator og currency-polling — scorer de 16 domene-referansefilene mot en ankret kvalitetsrubrikk og oppdager utdaterte kilder
allowed-tools: Read, Glob, WebSearch, Task allowed-tools: Read, Glob, WebSearch, Task
argument-hint: "[referansefil å fokusere på, eller tom for full gjennomgang]" argument-hint: "[referansefil å fokusere på, eller tom for full gjennomgang]"
--- ---
@ -9,7 +9,7 @@ argument-hint: "[referansefil å fokusere på, eller tom for full gjennomgang]"
Vedlikehold kunnskapsbasen (KB) bak OKR-pluginet. Kommandoen gjør to ting: Vedlikehold kunnskapsbasen (KB) bak OKR-pluginet. Kommandoen gjør to ting:
1. **KB-selvevaluator** — scorer hver av de 18 domene-referansefilene mot en 1. **KB-selvevaluator** — scorer hver av de 16 domene-referansefilene mot en
ankret kvalitetsrubrikk (dekning + kvalitet), slik at svake filer kan ankret kvalitetsrubrikk (dekning + kvalitet), slik at svake filer kan
prioriteres for forbedring. prioriteres for forbedring.
2. **Currency-polling** — sjekker navngitte offentlige kilder for å oppdage om 2. **Currency-polling** — sjekker navngitte offentlige kilder for å oppdage om
@ -26,32 +26,30 @@ Oppdag referansefilene direkte fra disk (hooken for-injiserer ikke lenger en fil
- Bruk `Glob``${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/*.md` - Bruk `Glob``${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/*.md`
for å bekrefte at fil-listen under fortsatt stemmer før scoring. for å bekrefte at fil-listen under fortsatt stemmer før scoring.
- Hvis brukeren oppgir en spesifikk referansefil som argument: scor kun den. - Hvis brukeren oppgir en spesifikk referansefil som argument: scor kun den.
- Ellers: kjør full gjennomgang av alle 18 filer. - Ellers: kjør full gjennomgang av alle 16 filer.
## Arbeidsflyt ## Arbeidsflyt
### Del A — KB-selvevaluator ### Del A — KB-selvevaluator
**Filer som scores (18 domene-referansefiler):** **Filer som scores (16 domene-referansefiler):**
1. `cfr-framework.md` 1. `cfr-framework.md`
2. `dfo-okr-mapping.md` 2. `dfo-okr-mapping.md`
3. `gevinstrealisering-okr.md` 3. `individual-vs-team-okr.md`
4. `individual-vs-team-okr.md` 4. `meeting-guides.md`
5. `meeting-guides.md` 5. `metrics-library.md`
6. `metrics-library.md` 6. `okr-antipatterns.md`
7. `okr-antipatterns.md` 7. `okr-arshjul.md`
8. `okr-arshjul.md` 8. `okr-calculator.md`
9. `okr-calculator.md` 9. `okr-cheatsheet.md`
10. `okr-cheatsheet.md` 10. `okr-examples.md`
11. `okr-examples.md` 11. `okr-framework.md`
12. `okr-framework.md` 12. `okr-implementation.md`
13. `okr-implementation.md` 13. `okr-integrations.md`
14. `okr-integrations.md` 14. `okr-oboard-guide.md`
15. `okr-kommunal-styring.md` 15. `okr-offentlig-governance.md`
16. `okr-oboard-guide.md` 16. `okr-sources.md`
17. `okr-offentlig-governance.md`
18. `okr-sources.md`
**Eksplisitt ekskludert:** `okr-quality-rubrics.md` scores IKKE av denne **Eksplisitt ekskludert:** `okr-quality-rubrics.md` scores IKKE av denne
evaluatoren. Den er selv scoringsinstrumentet for `/okr:kvalitet`; en evaluatoren. Den er selv scoringsinstrumentet for `/okr:kvalitet`; en
@ -164,7 +162,7 @@ bumpes, og hvilke faktapåstander som må re-verifiseres.
## Referanser ## Referanser
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/` — de 18 domenefilene som scores - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/` — de 16 domenefilene som scores
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-quality-rubrics.md` — OKR-scoringsrubrikk (ekskludert fra KB-scoring; ankerstilen gjenbrukt her) - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-quality-rubrics.md` — OKR-scoringsrubrikk (ekskludert fra KB-scoring; ankerstilen gjenbrukt her)
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-sources.md` — primærkilde-register (provenans-stil) - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-sources.md` — primærkilde-register (provenans-stil)
- `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/metrics-library.md` — bærer volatile offentlig-metrikker (hovedmål for currency-polling) - `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/metrics-library.md` — bærer volatile offentlig-metrikker (hovedmål for currency-polling)

View file

@ -26,9 +26,7 @@ kommandoene for det temaet. Ellers vis full oversikt.
| `/okr:analyse` | Kryss-syklus-analyse med Mermaid-trendvisualisering | | `/okr:analyse` | Kryss-syklus-analyse med Mermaid-trendvisualisering |
| `/okr:oppsett` | Konfigurer plugin: onboarding (`full`/`mvp`), `arkiver`, `oppdater`, `vis` | | `/okr:oppsett` | Konfigurer plugin: onboarding (`full`/`mvp`), `arkiver`, `oppdater`, `vis` |
| `/okr:innboks` | Ingest dokumenter fra innboksen (`.claude/okr/innboks/`) til kunnskapstreet | | `/okr:innboks` | Ingest dokumenter fra innboksen (`.claude/okr/innboks/`) til kunnskapstreet |
| `/okr:rapport` | Generer rapport fra syklusdata: `tertial`, `arsrapport` (del III) eller `etatsstyring` — committed og aspirational hver for seg |
| `/okr:export` | Eksporter OKR-dokumenter til print-klar PDF (ledelse/Riksrevisjon) | | `/okr:export` | Eksporter OKR-dokumenter til print-klar PDF (ledelse/Riksrevisjon) |
| `/okr:arkivklar` | Vurderingsgrunnlag mot bevaringsforskrifta (§§ 7/30, sikring § 3) — hva boer vurderes overfoert til sak-arkivet. Lesende: sletter aldri, avgjoer aldri |
| `/okr:freshen-references` | KB-selvevaluator + currency-polling av offentlige kilder | | `/okr:freshen-references` | KB-selvevaluator + currency-polling av offentlige kilder |
| `/okr:help` | Denne oversikten — kommandoer, agenter, anbefalt arbeidsflyt | | `/okr:help` | Denne oversikten — kommandoer, agenter, anbefalt arbeidsflyt |
@ -65,10 +63,9 @@ du er — coaching-hooken minner deg på dette ved sesjonsstart.
### Sent i syklus (uke 1216) — lukk og lær ### Sent i syklus (uke 1216) — lukk og lær
8. `/okr:sporing` — endelig scoring 8. `/okr:sporing` — endelig scoring
9. `/okr:export` — eksporter status/retrospektiv til PDF for ledelse/Riksrevisjon 9. `/okr:export` — eksporter status/retrospektiv til PDF for ledelse/Riksrevisjon
10. `/okr:arkivklar` — se hva som boer vurderes overfoert til sak-arkivet foer syklusen lukkes 10. `/okr:oppsett arkiver` — arkiver syklus, generer retrospektiv
11. `/okr:oppsett arkiver` — arkiver syklus, generer retrospektiv 11. `/okr:analyse` — se trender på tvers av sykluser
12. `/okr:analyse` — se trender på tvers av sykluser 12. `/okr:skriv` — start neste syklus med lærdommene
13. `/okr:skriv` — start neste syklus med lærdommene
### Løpende vedlikehold ### Løpende vedlikehold
- `/okr:innboks` — dropp dokumenter i `.claude/okr/innboks/` og ingest dem til kunnskapstreet - `/okr:innboks` — dropp dokumenter i `.claude/okr/innboks/` og ingest dem til kunnskapstreet

View file

@ -47,34 +47,15 @@ Scoringen bruker ÉN kanonisk sannhetskilde — bær ikke et eget scorebånd ell
inline-rubrikk her: inline-rubrikk her:
`${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-quality-rubrics.md` `${CLAUDE_PLUGIN_ROOT}/skills/okr-offentlig-sektor/references/okr-quality-rubrics.md`
Fila har fem ankere per dimensjon (svakest → sterkest) over alle 11 dimensjonene: Fila har fem ankere per dimensjon (svakest → sterkest) over alle 10 dimensjonene:
- **Objective (5):** Inspirerende, Klarhet, Outcome-fokus, Scope, Alignment - **Objective (5):** Inspirerende, Klarhet, Outcome-fokus, Scope, Alignment
- **Key Result (6):** Målbarhet, Outcome, Ambisjon, Datakilde, Uavhengighet, Leading/lagging-balanse - **Key Result (5):** Målbarhet, Outcome, Ambisjon, Datakilde, Uavhengighet
Les ankerbeskrivelsen per dimensjon, finn nivået OKR-en faktisk treffer, og scor 1-5. Les ankerbeskrivelsen per dimensjon, finn nivået OKR-en faktisk treffer, og scor 1-5.
Anker → 0-10-skala er definert i rubrikkfila («Bruk»-avsnittet): anker 1 → 1-2, Anker → 0-10-skala er definert i rubrikkfila («Bruk»-avsnittet): anker 1 → 1-2,
anker 2 → 3-4, anker 3 → 5-6, anker 4 → 7-8, anker 5 → 9-10. Det er rubrikkfila som anker 2 → 3-4, anker 3 → 5-6, anker 4 → 7-8, anker 5 → 9-10. Det er rubrikkfila som
eier skalaen; ikke dupliser den her. eier skalaen; ikke dupliser den her.
## Guardrail-sjekkpunkt
Sjekk om KR-settet har **minst én guardrail** — en metrikk som ikke skal forbedres,
men som avslører om et annet KR forbedres på bekostning av noe. Mangler den, er
settet blindt for skadevirkninger: det kan nå 1.0 mens noe utenfor målebildet
forvitrer.
Sjekkpunktet henger sammen med dimensjonen **Leading/lagging-balanse** i rubrikken:
guardrailen er ofte stedet lagging-halvdelen av paret hører hjemme, fordi den måler
tilstanden som skal *bevares* mens leading-indikatoren beveger seg. Scor
dimensjonen i rubrikken; bruk dette punktet til å si om guardrailen finnes i det
hele tatt.
Flagg særskilt **rangerings-KR-er** mot andre virksomheter (typisk KOSTRA-plassering).
SSB advarer selv om at forskjeller i utgifter per person i målgruppen «ikke
utelukkende kan tolkes som et resultat av prioriteringer på lokalt nivå» — en
rangering er derfor ikke et holdbart KR. Foreslå en guardrail på egen tjenestekvalitet
i stedet.
## Vanlige antipatterns å sjekke ## Vanlige antipatterns å sjekke
1. **Aktivitets-OKR** — måler hva vi gjør, ikke hva vi oppnår 1. **Aktivitets-OKR** — måler hva vi gjør, ikke hva vi oppnår

View file

@ -1,80 +0,0 @@
---
name: okr:rapport
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|etatsstyring]"
---
# OKR Rapport - Generer styringsrapport fra syklusdata
Bygg en rapport direkte fra KR-tallene i syklusens `okr-*.md`-filer. Generatoren
er deterministisk: samme data gir samme dokument, hver gang.
## Hva generatoren eier - og hva den ikke eier
Generatoren eier **aritmetikken, tabellstrukturen og formateringsinvariantene**:
den beregner score, holder committed og aspirational fra hverandre, og skriver
skalaforklaringen som hindrer at 0.7 leses som «70 % av maalet».
Den eier **aldri confidence**. Confidence-kolonnen staar tom med vilje.
`okr-framework.md` er eneste sannhetskilde for On Track / At Risk / Off Track, og
`okr-calculator.md` avviser mekanisk utledning eksplisitt - gapet mellom faktisk
og forventet score er *ett innspill* til vurderingen, ikke en regel. En generator
som satte trafikklyset selv ville oppfunnet en terskel doktrinen forbyr.
Confidence fylles derfor ut i `/okr:sporing`, der skjoennet hoerer hjemme.
Score lagres heller aldri til fil - den beregnes fra `baseline`/`target`/`naa`
hver gang, slik at det ikke oppstaar en andre sannhetskilde som kan drifte.
## Forutsetninger
- Node.js 22 eller nyere. Ingen npm-avhengigheter.
- Syklusens OKR-filer maa baere KR-datakontrakten i frontmatter
(`krN_navn`, `krN_baseline`, `krN_target`, `krN_naa`, `krN_type`) - se
`/okr:skriv`. Mangler tallene, sier generatoren fra i stedet for aa gjette.
## Arbeidsflyt
1. **Finn syklusen** - list katalogene med Glob (`.claude/okr/syklus/*/`). Er det
flere, spoer brukeren hvilken. Er det ingen, si det og stopp (`/okr:oppsett`
setter opp treet).
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 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.
- `2` - bruksfeil: feil antall argumenter, ukjent rapportform, eller
syklus-katalogen finnes ikke. Sjekk stien mot Glob-resultatet fra steg 1.
4. **Les rapporten** og gjennomgaa den med brukeren. Vaer spesielt tydelig paa
committed-avvik: et committed KR under target er et avvik som skal forklares,
ikke et middels resultat.
## Rapportformer
| 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
- `/okr:skriv` - KR-datakontrakten rapporten leser
- `/okr:sporing` - check-in og confidence-vurdering
- `/okr:analyse` - trender paa tvers av sykluser

View file

@ -86,33 +86,6 @@ Generer målbare Key Results for et gitt Objective.
- **2-5 stk per Objective** — typisk 3 - **2-5 stk per Objective** — typisk 3
- **Balanserte** — minst én per dimensjon (kvantitet, kvalitet, hastighet) - **Balanserte** — minst én per dimensjon (kvantitet, kvalitet, hastighet)
- **Har datakilde** — vet hvor tallene kommer fra - **Har datakilde** — vet hvor tallene kommer fra
- **Minst én guardrail** — en metrikk som ikke skal forbedres (se under)
### Guardrail — metrikken som ikke skal forbedres
Et KR-sett trenger **minst én guardrail**: en metrikk som ikke er et forbedringsmål,
men som avslører om et annet KR forbedres på bekostning av noe. Guardrailen har et
gulv eller tak («skal ikke falle under», «skal ikke overstige»), ikke en
baseline → target-bane. Uten den kan settet nå 1.0 mens noe utenfor målebildet
forvitrer.
Formuler den som en grense:
```
KR3 (guardrail): Brukertilfredshet skal ikke falle under 4,1 av 5 mens
saksbehandlingstiden reduseres
```
**Hvorfor dette ikke er byråkrati:** en rangerings-KR mot andre kommuner («komme
blant de 10 beste i KOSTRA») er metodisk uholdbar — SSB advarer selv om at
forskjeller i utgifter per person i målgruppen «ikke utelukkende kan tolkes som et
resultat av prioriteringer på lokalt nivå». Å erstatte rangeringen med en guardrail
på egen tjenestekvalitet er guardrail-tenkning i praksis: du måler det du faktisk
rår over, og verner det du ikke vil miste.
Guardrailen er ofte det naturlige stedet å plassere lagging-halvdelen av
leading/lagging-paret — se dimensjonen **Leading/lagging-balanse** i
`references/okr-quality-rubrics.md`.
### Typer Key Results ### Typer Key Results
@ -131,54 +104,6 @@ KR[n]: [Formulering med baseline → target]
- Type: Committed / Aspirational - Type: Committed / Aspirational
``` ```
### Maskinlaget: KR-datakontrakten i frontmatter
Punktprosaen over er for mennesker og beholdes uendret. Ved siden av den legges
**samme tall som maskinlesbar frontmatter** i `okr-*.md`, slik at rapport-
generatorene (`/okr:rapport`) kan lese syklusen uten å tolke prosa.
Emitter fem nøkler per KR `n`, nummerert fra 1:
```yaml
---
type: OKR
kr1_navn: Saksbehandlingstid førerkort (dager)
kr1_baseline: 14
kr1_target: 5
kr1_naa: 11
kr1_type: committed
kr2_navn: Andel digitale søknader (%)
kr2_baseline: 60
kr2_target: 85
kr2_naa: 72
kr2_type: aspirational
---
```
| Nøkkel | Innhold |
|--------|---------|
| `krN_navn` | Tekst. Kortform av KR-en. **Enheten hører hjemme her** (og i prosaen) |
| `krN_baseline` | Utgangspunktet ved syklusstart |
| `krN_target` | Målverdien ved syklusslutt |
| `krN_naa` | Nåverdien. **Oppdateres ved hver check-in** |
| `krN_type` | `committed` eller `aspirational` — små bokstaver |
**Tre regler som gjør kontrakten lesbar for generatoren:**
1. **Rene tall uten enhet.** `kr1_baseline: 14`, aldri `14 dager` eller `60 %`.
Enheten bæres av `krN_navn` og av prosaen — et tall med enhet er ikke et tall.
2. **Score skrives aldri til fil.** Den beregnes av generatoren som
`(nåværende baseline) / (target baseline)`. En lagret score er en andre
sannhetskilde som drifter fra tallene den ble utledet av. Ingen `krN_score`.
3. **Nedadgående mål trenger ingen særbehandling**`baseline: 14, target: 5`
fungerer som-er, fordi teller og nevner begge blir negative.
For binære KR (milepæl): sett `krN_baseline: 0` og `krN_target: 1`, og la
`krN_naa` være `0` eller `1`.
`/okr:skriv` skriver ikke til disk. Emitter frontmatteren sammen med OKR-teksten
og be brukeren lagre den i `.claude/okr/syklus/[syklus-id]/okr-[navn].md`.
## Strategi-til-OKR ## Strategi-til-OKR
Når brukeren har strategidokument eller tildelingsbrev som input: Når brukeren har strategidokument eller tildelingsbrev som input:

View file

@ -1,7 +1,7 @@
--- ---
name: okr:sporing name: okr:sporing
description: Spor OKR-fremgang, beregn score og generer check-in rapporter description: Spor OKR-fremgang, beregn score og generer check-in rapporter
allowed-tools: Read, Bash, AskUserQuestion, Task, ToolSearch, Glob allowed-tools: Read, AskUserQuestion, Task, ToolSearch, Glob
argument-hint: "[OKR eller tall for oppdatering]" argument-hint: "[OKR eller tall for oppdatering]"
--- ---
@ -47,50 +47,14 @@ Score = (Nåværende - Baseline) / (Target - Baseline)
- Baseline og target (hvis ikke kjent) - Baseline og target (hvis ikke kjent)
- Dato for måling - Dato for måling
Er `/okr:rapport tertial` kjørt for syklusen, finnes tallene allerede i 2. **Beregn score** per KR og samlet (vektet gjennomsnitt)
`rapport-tertial.md` — les den i stedet for å spørre om dem på nytt.
Confidence-kolonnen der står tom med vilje: generatoren eier aritmetikken,
denne kommandoen eier vurderingen. Det er den kolonnen du fyller i steg 3.
2. **Beregn score** per KR, og oppsummer **per type** — aldri som ett felles tall.
Committed og aspirational måles mot hver sin målestokk, og et aggregat på tvers
av dem er tvetydig (`okr-framework.md`, committed/aspirational-skillet):
- **Committed:** hvor mange KR som har nådd kravet. Et committed KR er nådd
eller ikke; et snitt av binære krav er ikke en størrelse.
- **Aspirational:** snittet på tvers av aspirational-KR. Rundt 0.7 er forventet
måloppnåelse, ikke svikt.
3. **Vurder confidence** — sett nivået fra den **kanoniske confidence-tabellen i 3. **Vurder confidence** — sett nivået fra den **kanoniske confidence-tabellen i
`okr-framework.md`** (On Track 🟢 / At Risk 🟡 / Off Track 🔴, sannsynlighet for å `okr-framework.md`** (On Track 🟢 / At Risk 🟡 / Off Track 🔴, sannsynlighet for å
nå target). Innfør ingen egne nivåer eller terskler her. Trend er ett *innspill* nå target). Innfør ingen egne nivåer eller terskler her. Trend er ett *innspill*
til vurderingen: peker den mot target, flater den ut, eller har fremgangen stoppet? til vurderingen: peker den mot target, flater den ut, eller har fremgangen stoppet?
4. **Skriv `status.md` med scriptet — aldri for hånd.** Du eier vurderingen; 4. **Generer rapport** med anbefalte tiltak
generatoren eier tallene. Lever confidence som JSON på stdin, ett oppslag fra
KR-navn (nøyaktig som i `kr*_navn`) til etikett:
```bash
node ${CLAUDE_PLUGIN_ROOT}/scripts/syklus-status.mjs .claude/okr/syklus/[id] <<'JSON'
{
"Andel digitale søknader (prosent)": "At Risk",
"Gjennomsnittlig saksbehandlingstid (dager)": "On Track"
}
JSON
```
Etikettene er det lukkede vokabularet fra `okr-framework.md`: `On Track`,
`At Risk`, `Off Track`. Alle KR i syklusen må ha en etikett — scriptet nekter
å skrive en fil med tomme celler, fordi en slik fil ser vurdert ut uten å være
det, og konsumentene ville lest den som grønn.
**Skriv aldri av tallene selv.** Scriptet bygger radene med samme kode som
`/okr:rapport`, så score og avvik i `status.md` er de samme bytene der.
Exit-koder: `0` skrevet · `1` domenefeil (ugyldig eller manglende etikett,
eller en håndholdt `status.md` i veien — den overskrives aldri) · `2` bruksfeil
(feil argument, katalogen finnes ikke, ugyldig JSON).
5. **Presenter resultatet** med anbefalte tiltak, og pek på fila som ble skrevet.
### Ved ukentlig check-in ### Ved ukentlig check-in
@ -100,57 +64,42 @@ Generer en strukturert check-in:
3. Beslutninger som trengs 3. Beslutninger som trengs
4. Fokus neste periode 4. Fokus neste periode
## Fila scriptet skriver ## Eksempel på output
`.claude/okr/syklus/[id]/status.md` er et OKF-konsept i second-brain-treet, ikke løs Når statusrapporten lagres som `.claude/okr/syklus/[id]/status.md` i second-brain-treet,
markdown. Scriptet setter frontmatteren selv — `type: Status` (uten den feller bær filen **OKF-frontmatter** (påkrevd `type: Status` + anbefalt `title`/`description`/
`okf-check` hele roten) og `kilde: sporing`, provenans-nøkkelen som gjør at en senere `timestamp`); rapportinnholdet under følger frontmatteren:
kjøring vet at fila er generert og trygt kan erstatte den. Etikettene er ASCII, uten
emoji, som resten av den maskingenererte flaten; `coaching-hook` matcher på ordene.
Forkortet utdrag av resultatet:
```markdown ```markdown
--- ---
type: Status type: Status
title: Statusrapport T1-2026 syklus: T1-2026
timestamp: "2026-05-01T10:00:00.000Z" title: "Status T1-2026"
kilde: sporing description: "Fremdrift og scoring uke 8 av 16"
timestamp: "[ISO-8601]"
--- ---
# Statusrapport T1-2026 ## OKR Status - Uke 8 av 16
## Committed Key Results ### Objective: Forbedre trafikksikkerhet i skolesoner
Committed KR maales binaert mot kravet: kravet er naadd eller ikke. En score | KR | Baseline | Target | Nå | Score | Status |
under 1.0 er et avvik som skal forklares, ikke et godt resultat. |----|----------|--------|-----|-------|--------|
| KR1: Redusere ulykker | 40 | 30 | 35 | 0.50 | At Risk 🟡 |
| KR2: Fartshumper installert | 0% | 100% | 60% | 0.60 | On Track 🟢 |
| KR3: Foreldre-tilfredshet | 60% | 90% | 75% | 0.50 | At Risk 🟡 |
### Trafikksikkerhet og tunnelsikkerhet **Samlet score: 0.53** (vektet gjennomsnitt)
| KR | Baseline | Target | Naa | Score | Avvik | Confidence | **Samlet confidence: At Risk 🟡**
|----|----------|--------|-----|-------|-------|------------| - KR1 og KR3 trenger fokus
| Tunneler med oppgradert sikkerhetsutrustning (antall) | 12 | 20 | 17 | 0.63 | Ja | At Risk | - KR2 ligger foran plan
## Avvik som skal forklares **Anbefalte tiltak:**
1. Prioriter tiltak for KR1 (sikkerhet er kritisk)
## Aspirational Key Results 2. Kjør brukerundersøkelse for KR3 før neste måned
| KR | Baseline | Target | Naa | Score | Avvik | Confidence |
|----|----------|--------|-----|-------|-------|------------|
| Andel digitale søknader (prosent) | 60 | 85 | 63 | 0.12 | - | At Risk |
**Snitt aspirational: 0.23** over 2 KR.
``` ```
Rammeteksten om committed, avvik og aspirational kommer fra generatoren — du skal ikke
skrive den om. Det du eier, og som ikke finnes i fila, er **tiltakene**: presenter dem i
svaret ditt.
**Eksempel på anbefalte tiltak:**
1. Prioriter oppgraderingen av de tre gjenstående tunnelene — kravet er et avvik,
ikke et delresultat.
2. Kjør brukerundersøkelse på selvbetjeningsløsningen før neste måling.
## Linear-integrasjon ## Linear-integrasjon
Hvis Linear er konfigurert (sjekk med ToolSearch): Hvis Linear er konfigurert (sjekk med ToolSearch):

View file

@ -1,220 +0,0 @@
# Pilot T3-2026 — protokoll
> Protokollen for den reelle tertialsyklusen (T3 2026, septemberdesember) som skal
> avgjøre om pluginen holder i bruk, ikke bare i test. Skrevet **før** første
> live-kjøring, med vilje: en pilot som definerer suksesskriteriene sine underveis
> måler ikke noe.
>
> **Denne fila er offentlig** (`origin` = `open/okr.git`). Den inneholder derfor
> ingen organisasjonsdata, ingen dokumentnavn og ingen OKR-innhold — bare regler,
> format og kriterier. Se §4 for hvor pilotens faktiske innhold bor.
>
> Status: datagrensen (§2) og katalogprinsippet (§3) er **låst** 2026-08-11.
> Org-profil og tildelingsbrev er fortsatt **åpen post** (§8).
## 1. Hva piloten skal bevise
Én påstand står uverifisert i hele planverket: at pluginen fungerer i en ekte
styringssyklus. Alt annet er dekket av tester og fagkanon. Piloten finnes for å
erstatte den påstanden med dokumentasjon — i begge retninger. **En pilot som ikke
kan konkludere negativt, beviser ingenting.**
Konkret skal piloten svare på tre ting:
1. **Kommer man gjennom?** Lar hele styringssløyfa — tildelingsbrev inn,
tertial-/årsrapportunderlag ut — seg faktisk kjøre på ekte dokumenter, av én
person, uten å lese kildekoden for å komme videre?
2. **Er svarene faglig riktige når inputen er ekte?** Fikserte testdata er
snille. Ekte tildelingsbrev er lange, vage og delvis selvmotsigende.
3. **Hvor gjør det vondt?** Friksjonene er den egentlige leveransen. Funn-loggen
(§5) er pilotens primære produkt — ikke OKR-ene som lages underveis.
## 2. Datahåndteringsregel (LÅST 2026-08-11, operatørbeslutning G4)
**Kun offentlige dokumenter inn i pilot-treet.** Et dokument er offentlig i denne
protokollens forstand når det kan pekes til en offentlig tilgjengelig kilde —
typisk tildelingsbrev, årsrapporter, virksomhetsstrategier og tildelingsbrevets
vedlegg slik de er publisert.
Utenfor grensen, uten unntak i denne piloten:
- **E-post (`.eml`)** i enhver form, uansett innhold.
- **Interne dokumenter som ikke er publisert**, selv når de er ugraderte — internt
arbeidsmateriale, utkast, referater, personalrelatert innhold.
- **Personopplysninger** i enhver form, inkludert navn i eksempel-OKR. Roller
skrives som rolle («KR-eier», «avdelingsdirektør»), aldri som person.
Regelen gjelder alt som **legges i treet** (`.claude/okr/`), ikke hva operatøren
leser ved siden av. Sensitivt innhold er gatet bak 2.0.0-veiledningen
(sensitivitets-/GDPR-posisjon + `okr:rydd`) og åpnes ikke tidlig i piloten.
**Etterprøvbarhet:** ved hvert checkpoint skal hver fil i pilot-treets
`dokumenter/` og `strategisk-kontekst/` kunne pekes til sin offentlige kilde. En
fil som ikke kan det, fjernes — den er et brudd på grensen, ikke en gråsone.
## 3. Hvor piloten kjører (LÅST 2026-08-11, operatørbeslutning G4)
**Piloten kjører i en egen arbeidskatalog utenfor dette repoet.** Krav til
katalogen:
- **Ikke plugin-repoet.** Pluginens `.claude/` er riktignok gitignored, så det
ville ikke lekket — men piloten skal teste pluginen slik en installert bruker
møter den, ikke slik utvikleren gjør. Å blande styringsdata med plugin-utvikling
ødelegger begge deler.
- **Ingen offentlig remote.** Enten uten remote, eller mot en privat remote.
- **Egen `STATE.md`** per kontinuitetskonvensjonen — pilot-øktene er egne økter i
egen tab, ikke en gren av plugin-utviklingen.
- Eksakt sti fastsettes ved G4-resten (§8).
**Maskin-global tilstand — den ene ikke-reverterbare skrivingen.**
`/okr:oppsett` skriver `~/.claude/okr/org/profil.md`
(`scripts/write-org-profile.mjs:29`). Den fila ligger utenfor ethvert git-repo og
kan ikke tilbakestilles med en commit. Per 2026-08-11 **finnes den ikke**, så
første kjøring er trygg. Regelen gjelder derfor alt etterpå:
> Etter hver vellykket `/okr:oppsett`, kopier `~/.claude/okr/org/profil.md` til
> pilot-katalogen som `profil.md.pilot-backup`. Før enhver senere `/okr:oppsett`
> som kan overskrive den, verifiser at backupen er nyere enn siste bevisste
> endring.
## 4. Publiseringsgrense — hva som kan nå det offentlige speilet
Dette repoets `origin` er `open/okr.git`. Alt i `docs/` blir offentlig ved push.
| Innhold | Hvor det bor | Offentlig? |
|---|---|---|
| Denne protokollen | `docs/pilot-t3-2026.md` | Ja — org-nøytral med vilje |
| Funn-loggen | Pilot-katalogen (§3) | **Nei** |
| Pilot-treet (dokumenter, OKR, status) | Pilot-katalogen `.claude/okr/` | **Nei** |
| Konsolidert pilot-rapport (E8/E9) | `docs/` | Ja — **kun sanitert**, se under |
| CHANGELOG-referanse til piloten (2.0.0) | `CHANGELOG.md` | Ja |
**Saniteringsregel for pilot-rapporten:** rapporten som når `docs/` beskriver
*funnene* — hva som gikk galt i pluginen, hvor, og hva som ble gjort — aldri
organisasjonens innhold. Ingen KR-tekst, ingen måltall, ingen dokumentnavn, ingen
organisasjonsnavn utover det operatøren eksplisitt godkjenner ved E9. Et funn som
ikke lar seg beskrive uten organisasjonens innhold, blir stående i den private
funn-loggen og refereres i rapporten kun som antall.
## 5. Funn-logg — format
Funn-loggen bor i pilot-katalogen som `funn-logg.md` og er pilotens primære
produkt. Én oppføring per friksjon, i tabellform:
| Felt | Innhold |
|---|---|
| `ID` | `F-01`, `F-02`, … fortløpende, gjenbrukes aldri |
| `Dato` | ISO-dato for observasjonen |
| `Versjon` | Plugin-versjon som ble kjørt (`1.10.0`, …) |
| `Hvor` | Kommando, skript eller referansefil — så presist som mulig |
| `Observert` | Hva som faktisk skjedde. Skrives **før** diagnosen |
| `Forventet` | Hva operatøren ventet seg, og hvorfor |
| `Alvorlighet` | `blokkerende` \| `friksjon` \| `kosmetisk` |
| `Rute` | `patch-lane` \| `2.0.0` \| `dokumentasjon` \| `avvist` (med begrunnelse) |
| `Status` | `åpen` \| `lukket` + commit-hash |
**Alvorlighetsgradene er definert, ikke skjønn:**
- **`blokkerende`** — oppgaven lar seg ikke fullføre uten omvei operatøren måtte
finne på selv, **eller** outputen er faglig gal (feil aritmetikk, feil doktrine,
feil kildebruk). Et faglig galt svar som kommer pent formatert er blokkerende,
ikke kosmetisk.
- **`friksjon`** — oppgaven lar seg fullføre, men koster manuelle steg, eller
krever kunnskap som ikke står noe sted brukeren ville lett.
- **`kosmetisk`** — språk, formatering, ordvalg. Ingenting blir galt av det.
**Ruting:** `blokkerende` går til patch-lanen i samme uke den oppdages —
piloten kjører videre på fikset versjon, og `Versjon`-feltet gjør det mulig å se
hvilke funn som gjelder hvilken kode. `friksjon` og `kosmetisk` samles og rutes
ved neste checkpoint.
**To disiplinregler som avgjør om loggen er verdt noe:**
1. **En friksjon som ble omgått uten å bli logget, finnes ikke.** Det er den
vanligste feilen i egen-pilotering: operatøren kjenner koden, fikser i hodet,
går videre — og loggen kommer til å vise en plugin som fungerer bedre enn den
gjør. Logg først, fiks etterpå.
2. **Observasjon før diagnose.** `Observert` skrives uten forklaring på hvorfor.
En diagnose skrevet samtidig som observasjonen former observasjonen.
**Operatørens kodekjennskap er en systematisk feilkilde, og håndteres her.**
Operatøren har skrevet pluginen og vil aldri *måtte* lese kildekoden for å komme
videre — enhver suksessport formulert som «kom i mål uten å lese koden» består
derfor trivielt. Regelen som faktisk måler det: **hver gang operatøren brukte
kunnskap om pluginen som ikke står i dokumentasjonen, er det et `friksjon`-funn**,
også når det ikke kostet et sekund. Det er den knappen som skiller «fungerer» fra
«fungerer for den som bygde den».
## 6. Suksesskriterier
Kriteriene er pilotens egne, og skal ikke forveksles med organisasjonens T3-OKR
— de er *objektet* piloten tester, ikke målestokken. Skillet committed/aspirational
følger pluginens egen doktrine.
**Committed — forventning: alle møtt. Avvik krever skriftlig forklaring i rapporten.**
De er binære prosessporter, ikke metrikker, og scores derfor møtt/ikke møtt.
| # | Kriterium | Bevis |
|---|---|---|
| K1 | Hele inn-siden kjørt på ekte tildelingsbrev: `/okr:oppsett`, `/okr:governance`, `/okr:gap`, `/okr:skriv` + kvalitetssjekk — hver fullført | Pilot-tre + funn-logg |
| K2 | Hele ut-siden kjørt på ekte syklusdata ved pilot-slutt: `/okr:sporing` sluttscoring, `/okr:rapport tertial`, `/okr:oppsett arkiver`, `/okr:analyse` | Genererte filer i pilot-treet |
| K3 | Funn-loggen har én oppføring per friksjon, med `Observert` skrevet før diagnose | Loggen selv |
| K4 | Ingen `blokkerende` funn står `åpen` ved pilot-slutt | Loggen selv |
| K5 | Datagrensen (§2) holdt gjennom hele piloten — hver fil i pilot-treet kan pekes til offentlig kilde | Checkpoint-gjennomgang |
**Aspirational — stretch. `At Risk` er den forventede tilstanden, ikke et varsel.**
Scores 01.0 ved pilot-slutt.
| # | Kriterium |
|---|---|
| A1 | T3-OKR-ene ble brukt i minst én reell styringsdialog (ledelsesreview eller etatsstyringsmøte), og materiellet kom fra pluginen |
| A2 | Institusjonelt minne demonstrert: minst én gang svarte retrieval på et spørsmål operatøren ellers måtte lett manuelt for |
| A3 | Én annen person enn operatøren kjørte minst én kommando og kom i mål uten hjelp |
A3 avhenger av noen utenfor prosjektet og kan falle på tilgjengelighet alene. Det
er grunnen til at den er aspirational — ikke fordi den er mindre interessant.
**Piloten kan konkludere negativt.** Faller K1 eller K2, er konklusjonen at
pluginen ikke er bevist i bruk, og 2.0.0-CHANGELOGens «Validated in a real T3
cycle» skrives ikke. Det er et gyldig utfall.
## 7. Kadens
| Punkt | Uke | Hva |
|---|---|---|
| E1b — oppstart | 36 (~1. sept) | Live inn-side: `/okr:oppsett`, `/okr:governance`, `/okr:gap`, `/okr:skriv` + kvalitetssjekk. Funn-loggen opprettes |
| E6 — checkpoint 1 | 40 (okt) | Månedsrytme på ekte data: `/okr:sporing` check-in, `/okr:møter`-materiell i reelt møte. Datagrense-gjennomgang (K5). Blokkerende funn rutes |
| E7 — checkpoint 2 | 45 (nov) | Samme. I tillegg: innboks-ingestion live på offentlige dokumenter, retrieval-eval mot voksende tre |
| E8 — avslutning | 4950 (des) | Ut-siden: sluttscoring, arkivering + retrospektiv, `/okr:analyse`. Funn-loggen konsolideres |
| E9 — rapport | des | Sanitert pilot-rapport til `docs/`, 360-re-evaluering, 2.0.0-release med go-gate |
Mellom checkpointene kjøres piloten som normal drift — funn logges når de
oppstår, ikke samlet i etterkant.
## 8. Åpne poster (resten av G4)
Må avklares før E1b starter, uke 36:
1. **Org-profilen** — hvilken organisasjon og hvilket program piloten kjører på
(`organisasjon:`/`program:` i profilen).
2. **Tildelingsbrevet** — hvilket konkret dokument, og den offentlige kilden det
kan pekes til per §2.
3. **Eksakt sti** til pilot-katalogen per §3.
Ingen av disse er nødvendige for protokollen, og ingen av dem skrives inn i denne
fila når de avklares — de hører hjemme i pilot-katalogens egen `STATE.md` (§4).
## 9. Verifiseringslogg
| Påstand i dette dokumentet | Verifisert mot |
|---|---|
| `/okr:oppsett` skriver `~/.claude/okr/org/profil.md`, utenfor git | `scripts/write-org-profile.mjs:29` |
| Fila finnes ikke per 2026-08-11 | `ls ~/.claude/okr/org/` → No such file or directory |
| `origin` er det offentlige speilet `open/okr.git` | `git remote -v` |
| `docs/` er tracked; `.claude/` er gitignored | `.gitignore:40` + `git check-ignore -v` |
| Kommandoene i K1/K2 finnes | `ls commands/` (16 kommandoer, alle nevnte til stede) |
| Kadensen følger fasekartet | Sesjonsplanen, fase E (E1, E6, E7, E8, E9) |
| Datagrense og katalogvalg er operatørbeslutninger | G4 i sesjonsplanens beslutningsport-tabell; besvart 2026-08-11 |
*Skrevet 2026-08-11 (E1a). Datagrensen og katalogprinsippet er låst; org-profil og
tildelingsbrev står åpne til uke 36.*

View file

@ -9,27 +9,6 @@ import { readFileSync, existsSync, readdirSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { parseFrontmatter } from '../../lib/frontmatter.mjs'; import { parseFrontmatter } from '../../lib/frontmatter.mjs';
// Rad-predikatene er NAVNGITTE konstanter fordi to testfiler leser dem ut av
// denne kilden i stedet for aa kopiere dem (tests/syklus-rapport.test.mjs,
// tests/syklus-status.test.mjs). En kopiert regex ville bare bevist at testen er
// enig med seg selv. Navne-forankring, ikke posisjon: predikatene er ikke lenger
// to, saa «det foerste og det andre literalet» ville vaert en vakt som stille
// plukket feil regex ved neste endring.
// En KR-rad i statusrapporten er en markdown-tabellrad.
const ER_KR_RAD = /^\s*\|.*\|\s*$/;
// R1 (1.8.0): den kanoniske skalaen fra okr-framework.md -- On Track / At Risk /
// Off Track. De to norske etikettene beholdes som bakover-kompatibilitet for
// status-filer skrevet foer 1.8.0. On Track matcher ingen av dem.
const FARE_ORD = /at risk|off track|i fare|blokkert/i;
// Delmengden som betyr «progresjonen har stoppet opp» (okr-framework.md:610).
// Den eneste som ogsaa gjelder aspirational KR -- se seksjons-regelen under.
const STOPPET_ORD = /off track|blokkert/i;
// Toppnivaa-overskrift. `### Undertittel` matcher IKKE: etter `##` staar `#`,
// som ikke er [ \t]. Verdi-grenen er [ \t], aldri \s (laast beslutning 14).
const ER_TOPPSEKSJON = /^[ \t]*##[ \t]+(.*)$/;
const ER_ASPIRATIONAL = /^aspirational\b/i;
const cwd = process.cwd(); const cwd = process.cwd();
const configPath = join(cwd, '.claude', 'okr.local.md'); const configPath = join(cwd, '.claude', 'okr.local.md');
@ -97,34 +76,20 @@ try {
try { try {
// M1/m1 (B2): tell status-MARKERTE tabellrader, ikke raaforekomster -- // M1/m1 (B2): tell status-MARKERTE tabellrader, ikke raaforekomster --
// markoer-ord i forklaringstekst/prosa skal ikke inflatere telleren. // markoer-ord i forklaringstekst/prosa skal ikke inflatere telleren.
// En KR-rad i statusrapporten er en markdown-tabellrad (`| ... |`).
// //
// Patch-lane #1 (S61): tellingen er SEKSJONS-BEVISST. okr-framework.md:605 // R1 (1.8.0): status-malen bruker den kanoniske confidence-skalaen fra
// sier at de fleste stretch-KR BOER staa At Risk, og at permanent groent er // okr-framework.md -- On Track / At Risk / Off Track. De to norske
// sandbagging-signalet. Talte vi alle ikke-groenne rader under ett, ba vi // etikettene beholdes som bakover-kompatibilitet for status-filer skrevet
// operatoeren om tiltak for at aspirational-portefoeljen oppfoerer seg // foer 1.8.0. On Track matcher ingen av alternativene og telles ikke.
// slik doktrinen krever -- og eneste maate aa dempe nagget paa ville vaert
// aa merke stretch-KR groenne, noeyaktig det kanon advarer mot.
//
// Regelen, som foelger av det:
// committed -> At Risk OG Off Track teller (kravet er naadd eller ikke)
// aspirational -> KUN Off Track teller; At Risk er forventet tilstand
//
// Fallback (operatoerbeslutning S61): en rad hooken ikke kan plassere
// telles som COMMITTED. Haandskrevne status.md fra foer 1.10.0 har ingen
// seksjonsstruktur, og en stille fil er den farlige feilen -- den ser
// vurdert ut uten aa vaere det. Samme verdi som dekningsvakten i
// statusrapport() haandhever paa skrivesiden.
const statusContent = readFileSync(statusPath, 'utf8'); const statusContent = readFileSync(statusPath, 'utf8');
let iAspirational = false; atRiskCount = statusContent
for (const line of statusContent.split('\n')) { .split('\n')
const overskrift = ER_TOPPSEKSJON.exec(line); .filter(
if (overskrift) { (line) => /^\s*\|.*\|\s*$/.test(line)
iAspirational = ER_ASPIRATIONAL.test(overskrift[1].trim()); && /at risk|off track|i fare|blokkert/i.test(line),
continue; )
} .length;
if (!ER_KR_RAD.test(line)) continue;
if ((iAspirational ? STOPPET_ORD : FARE_ORD).test(line)) atRiskCount += 1;
}
} catch { /* skip */ } } catch { /* skip */ }
} }

View file

@ -1,200 +0,0 @@
// arkivklar.mjs
// D7 steg 17: traverser en OKF-bundle og klassifiser innholdet mot
// bevaringsforskrifta (FOR-2025-12-19-2729). REN modul -- den leser filer den
// faar rota til, men tar ingen beslutning og skriver ingenting.
//
// Disklesing hoerer til scripts/, ikke lib/ (D6-beslutning 1). Denne modulen er
// grensetilfellet: den MAA lese for aa klassifisere, men den bestemmer ikke hvor
// rota ligger og formaterer ingen rapport. Den tar rota som argument, akkurat som
// arsrapportDelIII tar {historikk} som data.
//
// PATH-CONFINEMENT SOM LESEGRENSE (NFR med navngitt suksesskriterium):
// hvert lesemaal realpath-bekreftes under bundle-rota, via den DELTE regelen i
// lib/path-confinement.mjs -- samme kode som ingestion bruker paa skrivesiden.
// Tolags fordi de to lagene fanger ulike former; se path-confinement.mjs.
//
// MODULEN RETURNERER VURDERINGSGRUNNLAG, ALDRI EN AVGJOERELSE. Den sier hvilken
// kategori noe TROLIG faller under, aldri hva som skal skje med det. Kassasjon
// krever Nasjonalarkivets hjemmel (arkivlova § 13) etter en vurdering loven
// legger til virksomhetens dokumentasjonsplan (arkivlova § 8, arkivforskrifta
// § 12 b) -- ingen av delene kan avledes fra en filtype.
import { readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
import path from 'node:path';
import { parseFrontmatter } from './frontmatter.mjs';
import { resolveUnderBundle, assertRealUnderBundle } from './path-confinement.mjs';
const HVEM = 'arkivklar';
// Bevaringsforskrifta § 7 (statleg sektor) navngir «tildelingsbrev, rapportar,
// etatsstyringsmoeter og evalueringa»; § 30 (kommunal sektor) navngir
// «handlingsprogram, tertialrapportering, aarsmelding».
// https://lovdata.no/dokument/SF/forskrift/2025-12-19-2729
//
// Laast arkitektur-beslutning 8: linjene er PARALLELLE, ikke alternative. En type
// kan treffe en kategori i begge, og da rapporteres begge -- modulen velger ikke
// styringslinje paa virksomhetens vegne.
const KATEGORI_BY_TYPE = {
Tildelingsbrev: { stat: 'tildelingsbrev', kommune: null },
Virksomhetsplan: { stat: null, kommune: 'handlingsprogram' },
Status: { stat: 'rapportar', kommune: 'tertialrapportering' },
Retrospektiv: { stat: 'evalueringa', kommune: 'aarsmelding' },
};
export const PARAGRAF = {
stat: '§ 7',
kommune: '§ 30',
sikring: '§ 3',
};
// Klassifiser en OKF-type mot begge styringslinjer.
//
// Sikringsparagrafen (§ 3) lukker resten: «All dokumentasjon som gjeld nye
// oppgaaver [...] skal takast vare paa inntil Nasjonalarkivet har fastsett
// reglar», og det samme for oppgaver som «av andre aarsaker ikkje er nemnde i
// forskrifta». Arkivfaglig oppsummert: det som ikke er nevnt, skal bevares.
// Ukjent type betyr derfor ALDRI «kan slettes» -- den er den mest forsiktige
// kategorien, ikke den minst.
export function klassifiser(type) {
const treff = KATEGORI_BY_TYPE[type];
if (!treff) return { stat: null, kommune: null, sikring: true };
return { stat: treff.stat, kommune: treff.kommune, sikring: false };
}
// Resolver et bundle-relativt lesemaal og bekreft at det faktisk ligger under
// rota. Kaster ved confinement-brudd; lar ENOENT boble uendret (de to feilene
// krever ulik handling hos kalleren -- se (17d)).
export function lesemaal(bundleRoot, rel) {
const resolvedBundle = path.resolve(bundleRoot);
const realBundle = realpathSync(resolvedBundle);
const maal = resolveUnderBundle(resolvedBundle, rel, HVEM);
return assertRealUnderBundle(realBundle, maal, `lesemaal ${rel}`, HVEM);
}
// index.md er navigasjon (OKF-indeksformatet), ikke et konsept. Den skal ikke
// klassifiseres -- ellers ville hver katalog produsert en falsk «Dokument»-rad.
const ER_INDEKS = (navn) => navn === 'index.md';
function lesType(absolutt) {
const { get } = parseFrontmatter(readFileSync(absolutt, 'utf8'));
return get('type') ?? null;
}
// Traverser bundlen og returner vurderingsgrunnlag per konseptfil.
//
// readdirSync sorteres eksplisitt: traverseringsrekkefoelgen er en del av
// kontrakten, siden rapporten nedstroems skal vaere byte-identisk mellom to
// kjoeringer (samme determinisme-krav som historikk-lesingen i D6).
export function traverserBundle(bundleRoot) {
const resolvedBundle = path.resolve(bundleRoot);
const realBundle = realpathSync(resolvedBundle);
const filer = [];
function gaa(relDir) {
const absDir = relDir === '' ? resolvedBundle : resolveUnderBundle(resolvedBundle, relDir, HVEM);
assertRealUnderBundle(realBundle, absDir, `katalog ${relDir || '.'}`, HVEM);
for (const entry of readdirSync(absDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
const rel = relDir === '' ? entry.name : `${relDir}/${entry.name}`;
// Symlenker foelges ikke blindt: lesemaal realpath-bekrefter hver node,
// saa en symlenket katalog som peker ut stopper traverseringen.
const abs = lesemaal(resolvedBundle, rel);
if (entry.isDirectory() || (entry.isSymbolicLink() && statSync(abs).isDirectory())) {
gaa(rel);
continue;
}
if (!entry.name.endsWith('.md') || ER_INDEKS(entry.name)) continue;
const type = lesType(abs);
filer.push({ rel, type, ...klassifiser(type) });
}
}
gaa('');
return { rot: realBundle, filer };
}
// Klokke-soem: OKR_NOW (ISO-8601) overstyrer veggklokka, saa to kjoeringer over
// samme tre gir byte-identisk rapport. Speiler lib/syklus-rapport.mjs:194.
const klokke = (opts) => opts?.naa || process.env.OKR_NOW || new Date().toISOString();
// Rendrer vurderingsgrunnlaget. Formuleringen er en INVARIANT, ikke stil:
//
// - Hver kategori presenteres som «faller trolig under ... § N», med
// paragrafhenvisning, og vurderingen legges eksplisitt til virksomhetens
// dokumentasjonsplan (arkivlova § 8, arkivforskrifta § 12 b).
// - Rapporten sier hva som boer VURDERES OVERFOERT til sak-arkivet. Den sier
// aldri hva som skal slettes eller kasseres -- kassasjon krever
// Nasjonalarkivets hjemmel (arkivlova § 13). Det finnes INGEN slettemodell i
// denne pluginen, og ingen skal innfoeres: retensjonsbehovet er dekket av
// /okr:oppsett arkiver og /okr:export.
// - Arkivforskrifta § 1 bokstav c gjoer vurderingen betinget av om
// dokumentasjonen allerede forvaltes som arkiv i et ANNET system. Det kan
// verktoeyet ikke se, og rapporten navngir derfor tilstanden som ukjent i
// stedet for aa anta den bort.
export function rapport(grunnlag, opts) {
const { rot, filer } = grunnlag;
const l = [];
l.push('# Arkivklar — vurderingsgrunnlag');
l.push('');
l.push(`Bundle: \`${rot}\``);
l.push(`Generert: ${klokke(opts)}`);
l.push(`Konseptfiler gjennomgaatt: ${filer.length}`);
l.push('');
l.push('> **Dette er et vurderingsgrunnlag, ikke en avgjoerelse.** Rapporten viser hvilke');
l.push('> kategorier i bevaringsforskrifta (FOR-2025-12-19-2729) innholdet trolig faller');
l.push('> under, og hva som boer vurderes overfoert til virksomhetens sak-/arkivsystem.');
l.push('> Selve vurderingen hoerer til virksomhetens dokumentasjonsplan, jf. arkivlova § 8');
l.push('> og arkivforskrifta § 12 b. Kassasjon krever hjemmel fra Nasjonalarkivet etter');
l.push('> arkivlova § 13, og avgjoeres ikke her.');
l.push('');
l.push('## Ukjent forutsetning');
l.push('');
l.push('Arkivforskrifta § 1 bokstav c gjoer vurderingen betinget av om denne');
l.push('dokumentasjonen **allerede forvaltes som arkiv i et annet system**. Verktoeyet');
l.push('leser kun dette treet og kan ikke se sak-/arkivsystemet. Tilstanden er derfor');
l.push('**ukjent**, og maa avklares av virksomheten foer grunnlaget brukes.');
l.push('');
const seksjoner = [
['stat', `Statleg styringslinje — bevaringsforskrifta ${PARAGRAF.stat}`, (f) => f.stat !== null],
['kommune', `Kommunal styringslinje — bevaringsforskrifta ${PARAGRAF.kommune}`, (f) => f.kommune !== null],
];
for (const [noekkel, tittel, filter] of seksjoner) {
const treff = filer.filter(filter);
l.push(`## ${tittel}`);
l.push('');
if (treff.length === 0) {
l.push('Ingen filer i treet traff en navngitt kategori i denne paragrafen.');
} else {
l.push('| Fil | OKF-type | Navngitt kategori | Vurdering |');
l.push('|-----|----------|-------------------|-----------|');
for (const f of treff) {
l.push(`| \`${f.rel}\` | ${f.type ?? '(ingen type)'} | ${f[noekkel]} | Boer vurderes overfoert til sak-arkivet |`);
}
}
l.push('');
}
const sikring = filer.filter((f) => f.sikring);
l.push(`## Ikke navngitt — sikringsparagrafen ${PARAGRAF.sikring}`);
l.push('');
l.push('Bevaringsforskrifta § 3 fastsetter at dokumentasjon som ikke er nevnt i');
l.push('forskrifta skal takast vare paa inntil Nasjonalarkivet har fastsett reglar.');
l.push('At en fil staar her betyr derfor at den skal **bevares**, ikke det motsatte.');
l.push('');
if (sikring.length === 0) {
l.push('Ingen filer falt utenfor de navngitte kategoriene.');
} else {
l.push('| Fil | OKF-type | Vurdering |');
l.push('|-----|----------|-----------|');
for (const f of sikring) {
l.push(`| \`${f.rel}\` | ${f.type ?? '(ingen type)'} | Bevares inntil Nasjonalarkivet har fastsett reglar |`);
}
}
l.push('');
return `${l.join('\n')}\n`;
}

View file

@ -11,17 +11,6 @@
// beholder en intern '#' ("A #B" -> A #B). Retter comment-leak-bugen der // beholder en intern '#' ("A #B" -> A #B). Retter comment-leak-bugen der
// `okr_frikoblet_fra_loenn: true # ...` lakk kommentaren inn i verdien. // `okr_frikoblet_fra_loenn: true # ...` lakk kommentaren inn i verdien.
// //
// Verdi-delen matches med `[ \t]*`, ALDRI `\s*`: `\s` inkluderer linjeskift, saa
// en verdi-`\s*` lot enhver key med tom rest-av-linje sluke den neste ikke-tomme
// linja. Utslaget var ikke begrenset til list-keys -- en tom `title:` returnerte
// neste keys hele linje ("type: OKR"), og et tomt `description:` dempet
// okf-checks anbefalt-felt-advarsel med data som tilhorte en annen key.
// De to `\s`-ene har ULIK jobb: innrykk-anker (bevart) vs. verdi-avgrensning
// (strammet). Tester: (L1)-(L4) + (L6). Bytt aldri verdi-grenen tilbake til `\s*`.
// `\t` i klassen er intensjons-dokumentasjon, ikke atferd: .trim() under gjor
// `[ \t]*` og `[ ]*` ekvivalente (mutasjons-verifisert). Det som BAERER fiksen er
// at linjeskift er utenfor klassen.
//
// Tolererer fler-linje OKF-list-verdier (f.eks. `tags:`) uten krasj: get() paa // Tolererer fler-linje OKF-list-verdier (f.eks. `tags:`) uten krasj: get() paa
// en list-key returnerer null (rest-av-linja er tom); list-elementer paa // en list-key returnerer null (rest-av-linja er tom); list-elementer paa
// foelgende linjer konsumeres aldri (ingen konsument leser tre-filenes `tags`). // foelgende linjer konsumeres aldri (ingen konsument leser tre-filenes `tags`).
@ -37,7 +26,7 @@ export function parseFrontmatter(content) {
const get = (key) => { const get = (key) => {
if (raw === null) return null; if (raw === null) return null;
const m = raw.match(new RegExp(`^\\s*${key}:[ \\t]*(.*)$`, 'm')); const m = raw.match(new RegExp(`^\\s*${key}:\\s*(.*)$`, 'm'));
if (!m) return null; if (!m) return null;
let v = m[1].trim(); let v = m[1].trim();
if (v === '') return null; if (v === '') return null;

View file

@ -38,10 +38,6 @@ import path from 'node:path';
import { homedir } from 'node:os'; import { homedir } from 'node:os';
import { writeFrontmatter, parseFrontmatter } from './frontmatter.mjs'; import { writeFrontmatter, parseFrontmatter } from './frontmatter.mjs';
import {
resolveUnderBundle as resolveUnderBundleShared,
assertRealUnderBundle as assertRealUnderBundleShared,
} from './path-confinement.mjs';
// Den home-kanoniske org-profil-rota. Ingestion skal ALDRI skrive hit (den eies // Den home-kanoniske org-profil-rota. Ingestion skal ALDRI skrive hit (den eies
// av write-org-profile.mjs); prosjekt-bundlen er .claude/okr under cwd. // av write-org-profile.mjs); prosjekt-bundlen er .claude/okr under cwd.
@ -57,14 +53,28 @@ function writeAtomic(target, data) {
renameSync(tmp, target); renameSync(tmp, target);
} }
// M2 (A1): tolags path-confinement (leksikalsk + symlink) bor i // Resolver en bundle-relativ sti og asserter at den blir UNDER bundle-rota.
// lib/path-confinement.mjs -- delt med lib/arkivklar.mjs, som trenger NOEYAKTIG // Avviser '..'-escape og absolutt-override (path.resolve lar en absolutt rel
// samme regel paa lesesiden. Wrapperne her binder bare modulnavnet inn i // vinne -- containment-sjekken fanger det).
// feilmeldingen, saa den kallende modulen fortsatt er identifiserbar. function resolveUnderBundle(resolvedBundle, rel) {
const resolveUnderBundle = (resolvedBundle, rel) => const resolved = path.resolve(resolvedBundle, rel);
resolveUnderBundleShared(resolvedBundle, rel, 'innboks-write'); if (resolved !== resolvedBundle && !resolved.startsWith(resolvedBundle + path.sep)) {
const assertRealUnderBundle = (realBundle, p, what) => throw new Error(`innboks-write: maal-sti utenfor bundle-rot avvist: ${rel}`);
assertRealUnderBundleShared(realBundle, p, what, 'innboks-write'); }
return resolved;
}
// M2 (A1): den leksikalske sjekken over slipper symlinks -- en symlinket
// katalog/original INNE i bundlen kan peke UT av den. realpathSync paa den
// faktiske noden (destinasjons-parent etter mkdir / original foer peker-skriv)
// maa ogsaa lande under bundle-rotas realpath, ellers avvises skrivet.
function assertRealUnderBundle(realBundle, p, what) {
const real = realpathSync(p);
if (real !== realBundle && !real.startsWith(realBundle + path.sep)) {
throw new Error(`innboks-write: ${what} resolverer utenfor bundle-rot (symlink-escape avvist): ${p}`);
}
return real;
}
// Destinasjons-parent opprettes, realpath-sjekkes, DERETTER skrives det atomisk. // Destinasjons-parent opprettes, realpath-sjekkes, DERETTER skrives det atomisk.
function writeConfined(realBundle, target, data, what) { function writeConfined(realBundle, target, data, what) {

View file

@ -1,45 +0,0 @@
// path-confinement.mjs
// Delt lesegrense/skrivegrense mot en bundle-rot. Trukket ut av
// lib/innboks-write.mjs (D7 steg 17) fordi arkivklar-traverseringen trenger
// NOEYAKTIG samme regel paa lesesiden som ingestion har paa skrivesiden.
//
// EN delt regel, ett sted -- samme disiplin som unquote() i lib/frontmatter.mjs.
// To kopier av en sikkerhetsregel maa endres i takt for alltid, og gjoer det ikke.
//
// Tolags, fordi de to lagene fanger ULIKE angrepsformer:
// (1) LEKSIKALSK (resolveUnderBundle) -- '..'-escape og absolutt-override.
// path.resolve() lar en absolutt `rel` vinne over rota; containment-sjekken
// fanger det. path.sep i prefikssjekken er load-bearing: uten den tillater
// basen /uploads ogsaa soesken-katalogen /uploads-other.
// (2) SYMLINK (assertRealUnderBundle) -- path.resolve() loeser IKKE symlenker,
// saa en symlinket node INNE i bundlen kan peke UT av den. realpathSync paa
// den faktiske noden maa ogsaa lande under rotas realpath.
// Lag (1) alene er utilstrekkelig, og lag (2) alene kaster paa stier som ikke
// finnes ennaa. Begge trengs.
import path from 'node:path';
import { realpathSync } from 'node:fs';
// Resolver en bundle-relativ sti og asserter at den blir UNDER bundle-rota.
// `hvem` gaar inn i feilmeldingen slik at kalleren er identifiserbar -- den
// eneste grunnen funksjonen tar den i det hele tatt.
export function resolveUnderBundle(resolvedBundle, rel, hvem = 'path-confinement') {
const resolved = path.resolve(resolvedBundle, rel);
if (resolved !== resolvedBundle && !resolved.startsWith(resolvedBundle + path.sep)) {
throw new Error(`${hvem}: maal-sti utenfor bundle-rot avvist: ${rel}`);
}
return resolved;
}
// realpathSync kaster ENOENT paa en sti som ikke finnes. Det er kallerens ansvar
// aa kalle denne paa en node som ER opprettet (destinasjons-parent etter mkdir,
// eller en fil som faktisk ligger der) -- vi pakker den derfor IKKE inn i en
// try/catch her: en ENOENT skal boble som ENOENT, ikke maskeres som et
// confinement-brudd. De to feilene krever ulik handling hos kalleren.
export function assertRealUnderBundle(realBundle, p, what, hvem = 'path-confinement') {
const real = realpathSync(p);
if (real !== realBundle && !real.startsWith(realBundle + path.sep)) {
throw new Error(`${hvem}: ${what} resolverer utenfor bundle-rot (symlink-escape avvist): ${p}`);
}
return real;
}

View file

@ -1,142 +0,0 @@
// syklus-data.mjs
// D5 steg 8: leser en OKR-syklus fra disk og beregner KR-score kanonisk.
// REN modul -- ingen shebang, ingen isMain-CLI (lib/-siden av splitten;
// orkestratoren bor i scripts/syklus-rapport.mjs). Zero npm dependencies.
//
// Datamodellen er beslutning B-1: flate `krN_`-noekler i okr-*.md sin frontmatter.
// Noekkelen `krN_type` kolliderer ikke med OKF-noekkelen `type`, fordi
// lib/frontmatter.mjs:29 ankrer paa `^\s*type:` -- verifisert kjoert mot data,
// ikke bare lest (tests/syklus-data.test.mjs A4).
//
// Beslutning B-2: score BEREGNES, lagres aldri. En lagret score er en andre
// sannhetskilde som kan drifte fra tallene den ble utledet av.
//
// Disiplin: modulen KASTER heller enn aa falle tilbake paa en stille default
// (moenster: lib/innboks-frontmatter.mjs:64-72). En manglende target som stille
// ble 0 ville produsert en score som ser gyldig ut, og et styringsdokument som
// lyver med to desimalers presisjon er verre enn et som feiler.
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { basename, join } from 'node:path';
import { parseFrontmatter } from './frontmatter.mjs';
// De fem feltene KR-datakontrakten krever. Alle maa vaere til stede per KR n --
// et delvis utfylt KR er en feil, ikke et KR med hull.
const KR_FELT = ['navn', 'baseline', 'target', 'naa', 'type'];
const KR_TALLFELT = ['baseline', 'target', 'naa'];
const KR_TYPER = ['committed', 'aspirational'];
function somTall(verdi, felt, hvor) {
const n = Number(verdi);
if (verdi === null || verdi === undefined || String(verdi).trim() === '' || !Number.isFinite(n)) {
throw new Error(`${hvor}: ${felt} maa vaere et rent tall uten enhet (fikk: ${verdi})`);
}
return n;
}
/**
* Kanonisk KR-score: (naa - baseline) / (target - baseline), kappet til [0, 1.0].
*
* Tre kanter, alle fra okr-framework.md:319 / okr-calculator.md:7-24:
* - target === baseline -> undefined (IKKE 0). Forholdet er udefinert, ikke null
* fremgang; forskjellen er den mellom et KR som feilet og et som ikke kan
* scores som ratio.
* - nedadgaaende maal trenger ingen saertilfelle: teller og nevner blir begge
* negative, saa fortegnet gaar opp av seg selv.
* - binaert KR (baseline 0, target 1) faller ut av samme formel som 0 eller 1.
*
* @returns {number|undefined} score i [0, 1.0], eller undefined naar udefinert.
*/
export function beregnScore(kr) {
if (!kr || typeof kr !== 'object') {
throw new Error(`beregnScore: forventet et KR-objekt (fikk: ${kr})`);
}
const baseline = somTall(kr.baseline, 'baseline', 'beregnScore');
const target = somTall(kr.target, 'target', 'beregnScore');
const naa = somTall(kr.naa, 'naa', 'beregnScore');
if (target === baseline) return undefined;
const raa = (naa - baseline) / (target - baseline);
return Math.min(1, Math.max(0, raa));
}
// Samler `krN_*`-noeklene i frontmatteren til en sortert KR-liste. Nummereringen
// leses fra dataene (ikke antatt 1..n), saa et hull i nummerserien blir synlig
// som et manglende KR i stedet for aa forskyve alle KR-ene etter det.
function lesKrer(fm, hvor) {
const numre = new Set();
for (const linje of (fm.raw ?? '').split('\n')) {
const m = /^\s*kr(\d+)_[a-z]+\s*:/.exec(linje);
if (m) numre.add(Number(m[1]));
}
const krer = [];
for (const n of [...numre].sort((a, b) => a - b)) {
const raa = {};
for (const felt of KR_FELT) {
const verdi = fm.get(`kr${n}_${felt}`);
if (verdi === null) {
throw new Error(
`${hvor}: kr${n} mangler ${felt}. KR-datakontrakten krever alle fem feltene ` +
`(${KR_FELT.join(', ')}) -- se /okr:skriv.`,
);
}
raa[felt] = verdi;
}
const type = String(raa.type).toLowerCase();
if (!KR_TYPER.includes(type)) {
throw new Error(
`${hvor}: kr${n}_type maa vaere ${KR_TYPER.join(' eller ')} (fikk: ${raa.type})`,
);
}
const kr = { n, navn: raa.navn, type };
for (const felt of KR_TALLFELT) {
kr[felt] = somTall(raa[felt], `kr${n}_${felt}`, hvor);
}
krer.push({ n, navn: kr.navn, baseline: kr.baseline, target: kr.target, naa: kr.naa, type });
}
return krer;
}
/**
* Leser alle `okr-*.md` i en syklus-katalog til en normalisert struktur.
*
* Filrekkefolgen er sortert filnavn og er DEL AV KONTRAKTEN: rapportgeneratoren
* arver den, saa to kjoeringer over samme katalog gir samme dokument.
*
* @param {string} syklusDir katalog som inneholder okr-*.md
* @returns {{id: string, okrer: Array<{fil: string, tittel: string, krer: Array}>}}
*/
export function lesSyklus(syklusDir) {
let stat;
try {
stat = statSync(syklusDir);
} catch {
throw new Error(`lesSyklus: syklus-katalogen finnes ikke: ${syklusDir}`);
}
if (!stat.isDirectory()) {
throw new Error(`lesSyklus: syklus-katalogen finnes ikke som katalog: ${syklusDir}`);
}
const filer = readdirSync(syklusDir)
.filter((n) => n.startsWith('okr-') && n.endsWith('.md'))
.sort();
if (filer.length === 0) {
throw new Error(`lesSyklus: fant ingen okr-*.md i ${syklusDir}`);
}
const okrer = filer.map((fil) => {
const fm = parseFrontmatter(readFileSync(join(syklusDir, fil), 'utf8'));
return {
fil,
tittel: fm.get('title') ?? fil.replace(/\.md$/, ''),
krer: lesKrer(fm, fil),
};
});
return { id: basename(syklusDir), okrer };
}

View file

@ -1,603 +0,0 @@
// syklus-rapport.mjs
// D5 steg 9: tertialrapport fra en lest syklus. REN modul (ingen shebang, ingen
// isMain-CLI) -- orkestratoren bor i scripts/syklus-rapport.mjs.
// Zero npm dependencies.
//
// SEEMEN, og hvorfor den ligger der den ligger (beslutning B-3):
// generatoren eier ARITMETIKK, TABELLSTRUKTUR og FORMATERINGSINVARIANTER.
// Den eier ALDRI confidence. okr-framework.md:563 gjoer confidence-tabellen til
// eneste sannhetskilde og forbyr andre filer aa definere egne terskler;
// okr-calculator.md:249 avviser mekanisk utledning ordrett ("bruk gapet ... som
// ETT innspill til confidence-vurderingen, ikke som en mekanisk regel"). En
// generator som satte et trafikklys fra score ville derfor oppfunnet en terskel
// 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").
//
// Rapporten holdes ASCII-ren, som resten av den maskingenererte flaten.
import { beregnScore } from './syklus-data.mjs';
// Literal for score som ikke er definert som ratio (target == baseline, B-2).
// Et tomt felt ville lest som manglende data; "0" ville loeyet om at KR-en
// ikke har beveget seg.
const UDEFINERT = 'udefinert';
const TABELLHODE = [
'| KR | Baseline | Target | Naa | Score | Avvik | Confidence |',
'|----|----------|--------|-----|-------|-------|------------|',
];
const formatScore = (score) => (score === undefined ? UDEFINERT : score.toFixed(2));
// 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 '-';
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, confidence = '') {
const score = formatScore(beregnScore(kr));
// 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, hentConfidence = () => '') => [
...TABELLHODE,
...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, 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, hentConfidence), '');
}
return { linjer, antallKr };
}
// Aspirational vurderes paa SNITTET paa tvers av alle aspirational-OKR, ikke
// paa ett enkelt KR (okr-framework.md:557). Snittet er derfor kanonisk for
// denne typen -- og finnes bevisst ikke som et felles tall paa tvers av typene.
// KR uten definert score kan ikke inngaa; antallet oppgis i stedet for aa
// forsvinne stille.
function aspirationalSnitt(okrer) {
const scorer = [];
let udefinerte = 0;
for (const okr of okrer) {
for (const kr of okr.krer) {
if (kr.type !== 'aspirational') continue;
const s = beregnScore(kr);
if (s === undefined) udefinerte += 1;
else scorer.push(s);
}
}
if (scorer.length === 0) return null;
const snitt = scorer.reduce((a, b) => a + b, 0) / scorer.length;
const hale = udefinerte > 0 ? ` (${udefinerte} KR uten definert score er holdt utenfor)` : '';
return `**Snitt aspirational: ${snitt.toFixed(2)}** over ${scorer.length} KR${hale}.`;
}
// --- Sandbagging-vakten (steg 13): felles for alle tre rapportformene ---
//
// Regelen: en aspirational-OKR med lav score presenteres ALDRI som avvik
// (okr-offentlig-governance.md:148-156). Den er kodet ETT sted og brukes av alle
// formene, fordi en regel som maa gjentas per form er en regel som glipper i den
// fjerde.
//
// Begrunnelsen er kausal, ikke normativ (okr-framework.md:158-161, Bogsnes):
// faar aspirational-scoren konsekvenser i rapporteringen, er target og forecast
// re-bundlet -- og da kommer sandbaggingen tilbake av strukturell noedvendighet,
// uansett hvor disiplinert den som setter maalet er. Vakten beskytter altsaa
// ikke aspirational-KR-et; den beskytter maalsettingen i neste syklus.
// 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;
}
const harAspirational = (okrer) => okrer.some((okr) => okr.krer.some((kr) => kr.type === 'aspirational'));
// Overskriften varierer med mottakeren; filteret gjoer det ikke.
//
// Kryssreferansen til aspirational-seksjonen settes bare naar den seksjonen
// faktisk kommer. En syklus med bare committed -- fullt lovlig, og typisk for en
// ren etterlevelses-syklus -- ville ellers faatt et styringsdokument som peker
// paa en seksjon som ikke er der.
function avvikSeksjon(okrer, overskrift) {
const rader = avvikListe(okrer);
return [
`## ${overskrift}`,
'',
'Kun committed KR staar her: et committed krav er naadd eller ikke.',
...(harAspirational(okrer)
? ['Aspirational KR er holdt utenfor med vilje -- se forventningen til',
'aspirational under.']
: []),
'',
...(rader.length > 0 ? rader : ['Ingen committed KR ligger under kravet i denne syklusen.']),
'',
];
}
// Forventningsteksten hoerer i rapporten, ikke bare i koden: mottakeren i
// departementet leser ikke 0.7 som suksess med mindre det staar eksplisitt
// (governance-regel 1).
const ASPIRATIONAL_RAMME = [
'Aspirational KR forventes aa lande rundt 0.7 med hoey varians, og vurderes',
'paa snittet paa tvers av alle aspirational-OKR -- ikke paa ett enkelt KR.',
'En lav score er derfor maaloppnaaelse, ikke svikt, og et aspirational KR staar',
'aldri i avviks-seksjonen. Fikk lav aspirational-score konsekvenser i',
'rapporteringen, ville maal og prognose vaert bundlet sammen igjen -- og',
'sandbagging fulgt strukturelt, ikke som et disiplinproblem.',
];
// Felles inngangsvakt for alle rapportformene. En form som stille rapporterte
// en tom syklus ville produsert et styringsdokument uten innhold, som ser
// komplett ut.
function krevSyklus(syklus, hvor) {
if (!syklus || typeof syklus !== 'object' || !Array.isArray(syklus.okrer)) {
throw new Error(`${hvor}: forventet en syklus fra lesSyklus()`);
}
if (syklus.okrer.length === 0) {
throw new Error(`${hvor}: syklusen ${syklus.id ?? ''} inneholder ingen OKR`.trim());
}
}
// 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 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();
/**
* Bygger tertialrapporten for en syklus lest av lesSyklus().
*
* @param {object} syklus struktur fra lesSyklus()
* @param {{naa?: string}} [opts] naa overstyrer klokka; ellers OKR_NOW, ellers veggklokke
* (klokke-soem etter moenster fra scripts/compose-org-profile.mjs:61)
* @returns {string} markdown
*/
export function tertialrapport(syklus, opts = {}) {
krevSyklus(syklus, 'tertialrapport');
const naa = klokke(opts);
const committed = seksjon(syklus.okrer, 'committed', true);
const aspirational = seksjon(syklus.okrer, 'aspirational', false);
if (committed.antallKr + aspirational.antallKr === 0) {
throw new Error(`tertialrapport: syklusen ${syklus.id} inneholder ingen KR aa rapportere`);
}
const ut = [
`# Tertialrapport ${syklus.id}`,
'',
`Generert: ${naa}`,
'',
...SKALAFORKLARING,
];
if (committed.antallKr > 0) {
ut.push(
'## Committed Key Results',
'',
// Governance-invariant 1 + 2, uttalt der mottakeren leser tallene.
'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',
'',
// Governance-invariant 1: typen merkes, slik at 0.7 ikke leses som svikt.
...ASPIRATIONAL_RAMME,
'',
...aspirational.linjer,
);
const snitt = aspirationalSnitt(syklus.okrer);
if (snitt) ut.push(snitt, '');
}
return `${ut.join('\n').replace(/\n+$/, '')}\n`;
}
// --- Aarsrapport del III (steg 11) ---
//
// Del III «Aarets aktiviteter og resultater» er hovedplassen for OKR i den
// statlige aarsrapporten (okr-offentlig-governance.md:125-133).
//
// ANTIPATTERNET generatoren maa unngaa, ordrett fra primaerkilden --
// Riksrevisjonen (2020), «Undersoekelse av etats- og virksomhetsstyringen av
// Norsk institutt for biooekonomi (NIBIO)», del av Dokument 1 (2020-2021) s. 107:
// framstillingen «synliggjoer i mindre grad NIBIOs analyser av maaloppnaaelsen og
// framstaar som et oeyeblikksbilde av NIBIOs aktiviteter og resultater». Saken ble
// senere avsluttet etter forbedringer i maal- og resultatstyringen (gjengitt i
// DFOe-notat 2026:2 s. 48) -- funnet er en historisk dokumentert svakhet
// forvaltningen har rettet, ikke gjeldende kritikk av NIBIO.
//
// Konsekvensen for koden er konkret: en generator som bare dumper KR-tabeller
// PRODUSERER nettopp det oeyeblikksbildet. Den reserverer derfor plass til
// vurderingen av maaloppnaaelse per Objective -- og fyller den aldri selv, av
// samme grunn som den ikke setter confidence: vurderingen krever skjoenn.
const VURDERINGSFELT = [
'[Fylles ut av virksomheten: analysen av maaloppnaaelsen for dette Objectivet --',
'hva tallene over betyr, hva som forklarer avvikene, og hva som er laert.',
'Generatoren fyller ikke feltet; en vurdering av maaloppnaaelse krever skjoenn.',
'Uten analysen staar del III igjen som et oeyeblikksbilde av aktiviteter og',
'resultater, som er nettopp det revisjonen har paapekt. Slett klammene naar',
'feltet er fylt ut.]',
];
// DFOe-notat 2026:2 kap. 5.3.3 (s. 61): virksomhetene «skal planlegge med baade
// ettaarig og flerarig perspektiv», men «i tildelingsbrevene og aarsrapportene vi
// har sett paa omtales imidlertid i liten grad det ettaarige i et flerarig
// perspektiv». Generatoren PEKER derfor paa materialet fra tidligere sykluser --
// den regner ikke trend paa tvers av dem. Historikk-filene baerer retrospektiv-
// prosa, ikke KR-tall; en beregnet utvikling ville vaert oppdiktet.
function flerarigSeksjon(historikk) {
if (historikk.length === 0) return [];
return [
'## Flerarig perspektiv',
'',
'Virksomheten skal planlegge med baade ettaarig og flerarig perspektiv.',
'Materialet fra tidligere sykluser ligger i historikk-katalogen:',
'',
...historikk.map((h) => `- ${h.tittel} (${h.fil})`),
'',
'[Fylles ut av virksomheten: utviklingen sett over tid. Generatoren viser',
'hvilket materiale som finnes, og sammenligner ikke sykluser den ikke har',
'tall fra.]',
'',
];
}
/**
* Bygger aarsrapportens del III for en syklus lest av lesSyklus().
*
* @param {object} syklus struktur fra lesSyklus()
* @param {{naa?: string, historikk?: Array<{tittel: string, fil: string}>}} [opts]
* historikk leses av kalleren (scripts/syklus-rapport.mjs), ikke her -- modulen
* holdes fri for disk, saa formen er testbar uten et tre paa filsystemet.
* @returns {string} markdown
*/
export function arsrapportDelIII(syklus, opts = {}) {
krevSyklus(syklus, 'arsrapportDelIII');
const naa = klokke(opts);
const historikk = Array.isArray(opts.historikk) ? opts.historikk : [];
const antallKr = syklus.okrer.reduce((n, okr) => n + okr.krer.length, 0);
if (antallKr === 0) {
throw new Error(`arsrapportDelIII: syklusen ${syklus.id} inneholder ingen KR aa rapportere`);
}
const ut = [
`# Aarsrapport del III - Aarets aktiviteter og resultater (${syklus.id})`,
'',
`Generert: ${naa}`,
'',
...SKALAFORKLARING,
];
// Per Objective, fordi del III dokumenterer maaloppnaaelse mot tildelingsbrevets
// krav -- og kravene henger paa Objectives, ikke paa rapportformen.
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), '');
}
ut.push('### Vurdering av maaloppnaaelse', '', ...VURDERINGSFELT, '');
}
ut.push(...avvikSeksjon(syklus.okrer, 'Avvik som skal forklares'));
// Snittet hoerer PAA TVERS av Objectives (okr-framework.md:557) og staar derfor
// ikke i noen enkelt Objective-seksjon. Committed har bevisst ingen motpart:
// et snitt av binaere krav er ikke en stoerrelse.
const snitt = aspirationalSnitt(syklus.okrer);
if (snitt) {
ut.push(
'## Aspirational maaloppnaaelse paa tvers av Objectives',
'',
...ASPIRATIONAL_RAMME,
'',
snitt,
'',
);
}
ut.push(...flerarigSeksjon(historikk));
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.
/**
* 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), '');
}
}
ut.push(...avvikSeksjon(syklus.okrer, 'Avvik som krever departementets oppmerksomhet'));
// Forventningen staar rett under avvikslisten, der spoersmaalet «hvorfor er
// ikke DETTE et avvik?» faktisk oppstaar hos mottakeren -- og bare naar
// syklusen har aspirational-KR aa forklare.
if (harAspirational(syklus.okrer)) {
ut.push('## Aspirational Key Results - forventning', '', ...ASPIRATIONAL_RAMME, '');
}
// 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`;
}
// --- 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`;
}

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "okr-offentlig-sektor", "name": "okr-offentlig-sektor",
"version": "1.10.0", "version": "1.8.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "okr-offentlig-sektor", "name": "okr-offentlig-sektor",
"version": "1.10.0", "version": "1.8.2",
"dependencies": { "dependencies": {
"mammoth": "1.12.0", "mammoth": "1.12.0",
"postal-mime": "2.7.5", "postal-mime": "2.7.5",

View file

@ -1,6 +1,6 @@
{ {
"name": "okr-offentlig-sektor", "name": "okr-offentlig-sektor",
"version": "1.10.0", "version": "1.8.2",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "OKR-plugin for norsk offentlig sektor. Pure-JS konverterings-deps for innboks-ingestion (bevisst zero-dep-brudd, se README).", "description": "OKR-plugin for norsk offentlig sektor. Pure-JS konverterings-deps for innboks-ingestion (bevisst zero-dep-brudd, se README).",

View file

@ -1,86 +0,0 @@
#!/usr/bin/env node
// arkivklar.mjs
// D7 steg 18: orkestrator for /okr:arkivklar. Leser en OKF-bundle og skriver et
// VURDERINGSGRUNNLAG mot bevaringsforskrifta -- aldri en avgjoerelse.
//
// Arbeidsdelingen foelger D6-beslutning 1: lib/arkivklar.mjs eier traversering,
// klassifisering og rendring; dette skriptet eier disk-siden (hvor rota ligger,
// hvor rapporten havner) og CLI-kontrakten.
//
// STDOUT ER DEFAULT, fil er opt-in. Kommandoen (/okr:arkivklar) deklarerer
// verken Write eller Edit, og en verify-kjoering mot tests/fixtures/ skal ikke
// kunne skitne til fixturen. Oppgis en rapport-fil, skrives den ATOMISK
// (temp i samme katalog + renameSync) og ALDRI inn i bundlen selv.
//
// INGEN SLETTEMODELL FINNES HER, og ingen skal innfoeres. Retensjonsbehovet er
// dekket av /okr:oppsett arkiver (arkivering av syklus) og /okr:export (uttrekk).
// Kassasjon krever Nasjonalarkivets hjemmel etter arkivlova § 13 og er ikke en
// operasjon dette verktoeyet kan ha.
//
// Exit: 0 = rapport produsert · 1 = uventet feil (inkl. confinement-brudd)
// 2 = bruksfeil (manglende/ukjent sti)
import { writeFileSync, renameSync, existsSync, statSync, realpathSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { traverserBundle, rapport } from '../lib/arkivklar.mjs';
// Atomisk: temp-fil i SAMME katalog, deretter renameSync over maalet.
// Speiler write-org-profile.mjs:34-40 / innboks-write.mjs.
function writeAtomic(target, data) {
const dir = path.dirname(target);
const tmp = path.join(dir, `${path.basename(target)}.${process.pid}.tmp`);
writeFileSync(tmp, data);
renameSync(tmp, target);
}
export function byggRapport(bundleRoot, opts) {
return rapport(traverserBundle(bundleRoot), opts);
}
// --- CLI ---
const isMain = process.argv[1]
&& fileURLToPath(import.meta.url) === process.argv[1];
if (isMain) {
const [bundleRoot, rapportFil] = process.argv.slice(2);
if (!bundleRoot) {
process.stderr.write('Bruk: node arkivklar.mjs <bundle-rot> [rapport-fil]\n');
process.exit(2);
}
if (!existsSync(bundleRoot) || !statSync(bundleRoot).isDirectory()) {
process.stderr.write(`Finnes ikke (eller er ikke en katalog): ${bundleRoot}\n`);
process.exit(2);
}
try {
const ut = byggRapport(bundleRoot);
if (rapportFil) {
// Haandhever loeftet i headeren: rapporten skal ALDRI havne i bundlen den
// vurderer. En rapport i treet ville blitt klassifisert av neste kjoering
// (selv-forurensning), og et loefte som bare staar i prosa er ikke et
// loefte -- samme standard som tool-lista i commands/arkivklar.md.
//
// BEGGE sider maa realpath-es, ellers er sammenligningen verdiloes: paa
// macOS er /var en symlenke til /private/var, saa en realpath-et rot mot
// en bare resolvet maal-sti sammenligner to skrivemaater av samme sted og
// slipper alt gjennom. Maalet finnes ikke ennaa, saa det er FORELDRE-
// katalogen som realpath-es -- samme grep som writeConfined i
// lib/innboks-write.mjs.
const maalDir = realpathSync(path.dirname(path.resolve(rapportFil)));
const maal = path.join(maalDir, path.basename(rapportFil));
const rot = realpathSync(path.resolve(bundleRoot));
if (maal === rot || maal.startsWith(rot + path.sep)) {
process.stderr.write(`arkivklar: rapporten kan ikke skrives inne i bundlen som vurderes: ${rapportFil}\n`);
process.exit(1);
}
writeAtomic(maal, ut);
process.stdout.write(`Arkivklar: vurderingsgrunnlag skrevet til ${rapportFil}\n`);
} else {
process.stdout.write(ut);
}
process.exit(0);
} catch (e) {
process.stderr.write(`arkivklar: ${e.message}\n`);
process.exit(1);
}
}

View file

@ -70,12 +70,7 @@ def build_html(markdown_text):
"""Convert Markdown to a full standalone HTML document with embedded CSS.""" """Convert Markdown to a full standalone HTML document with embedded CSS."""
body = markdown.markdown( body = markdown.markdown(
markdown_text, markdown_text,
# "meta" parser YAML-frontmatter og fjerner den fra dokumentet. Uten den extensions=["tables", "fenced_code", "codehilite", "toc", "attr_list"],
# rendres OKF-frontmatteren (type/resource/timestamp/...) som broedtekst
# oeverst i PDF-en -- maskinlaget lekker inn i et dokument som skal til
# virksomhetens sak-arkiv. Verifisert mot python-markdown: uten "meta"
# blir frontmatteren <hr/><p>type: ...</p><hr/>.
extensions=["tables", "fenced_code", "codehilite", "toc", "attr_list", "meta"],
) )
return ( return (
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">" "<!DOCTYPE html><html><head><meta charset=\"utf-8\">"

View file

@ -19,30 +19,9 @@ import { fileURLToPath } from 'node:url';
import { parseFrontmatter } from '../lib/frontmatter.mjs'; import { parseFrontmatter } from '../lib/frontmatter.mjs';
import { TYPE_VOCAB } from '../lib/okf-vocab.mjs'; import { TYPE_VOCAB } from '../lib/okf-vocab.mjs';
import { resolveBundleLink } from '../lib/okf-links.mjs'; import { resolveBundleLink } from '../lib/okf-links.mjs';
import { unquote } from './okf-index.mjs';
const RECOMMENDED = ['resource', 'title', 'description', 'timestamp']; const RECOMMENDED = ['resource', 'title', 'description', 'timestamp'];
// OKFs konsept-felt er ROT-noekler. parseFrontmatter.get() er derimot bevisst
// innrykk-tolerant (`^\s*key:`, lib/frontmatter.mjs:29) fordi nestet oppslag er
// load-bearing for inject-okr-context.mjs's organisasjon:/program:-lesing. Den
// toleransen hoerer ikke hjemme i denne gaten, saa rot-nivaa-kravet ligger her
// i KALLEREN -- lib-en skal fortsatt tolerere innrykk for sine egne konsumenter.
//
// Uten dette leste gaten en `type:` under `metadata:` som filas type (falsk
// negativ: utypet fil passerte), lot en nestet `type:` FOER rot-noekkelen skygge
// rot-verdien (first-match), felte dokumenter paa en lenke i en NESTET `title:`
// (falsk positiv i strict-ingest) og lot et nestet `description:` dempe en
// advarsel som burde staatt.
//
// Visningen re-parses av SAMME lib, aldri av en kopi av regelen: sitering og
// trailing-kommentar-stripping har fortsatt kun EN implementasjon.
function rootLevelGet(fmRaw) {
if (fmRaw === null) return () => null;
const flat = fmRaw.split('\n').filter((l) => !/^\s/.test(l)).join('\n');
return parseFrontmatter(`---\n${flat}\n---`).get;
}
// Lenke-deteksjon for --strict-ingest: hvert lenke-maal maa vaere en trygg, // Lenke-deteksjon for --strict-ingest: hvert lenke-maal maa vaere en trygg,
// on-disk bundle-root-relativ .md (anti-RAG-poison). B3 (A1): ALLE fire // on-disk bundle-root-relativ .md (anti-RAG-poison). B3 (A1): ALLE fire
// standard lenkeformer fanges -- inline, referanse-definisjon, autolink og // standard lenkeformer fanges -- inline, referanse-definisjon, autolink og
@ -78,54 +57,17 @@ function walkConcepts(root) {
return out; return out;
} }
// Les rotens to markoerer fra index.md. `okf_version` = upstream OKF-versjon // Les rotens to markoerer (markdown-tekst i index.md, ikke frontmatter).
// (spec §3), `okf_layout` = emitterens egen layout-revisjon (spec §12, valgfri). // `okf_version` = upstream OKF-versjon (spec §3), `okf_layout` = emitterens egen
// Fravaerende markoer -> null. Ren ekko: verdiene valideres ikke (spec §3 er // layout-revisjon (spec §12, valgfri). Fravaerende markoer -> null. Ren ekko:
// ikke haandhevende paa form ennaa). // verdiene valideres ikke (spec §3 er ikke haandhevende paa form ennaa).
//
// TO PLASSERINGER, bevisst (D8 steg 20). Beslutning 6 flytter `okf_version` til
// frontmatter mens `okf_layout` blir i broedteksten, men bundles paa disk
// migrerer ikke samtidig -- en bundle skrevet av en eldre okf-index baerer
// fortsatt begge i broedteksten. Lesingen proever derfor frontmatter FOERST og
// faller tilbake til broedteksten. Presedensen er en REGEL her, ikke en
// bivirkning av at frontmatter tilfeldigvis staar oeverst i fila.
//
// De to lagene har ULIKE tolkningsregler, og det er hele grunnen til at de er
// skilt:
// - frontmatter ER YAML -> parseFrontmatter haandterer sitering OG trailing
// ` # kommentar` (lib/frontmatter.mjs:39). En raa `^key:`-scan lot
// kommentaren lekke inn i verdien.
// - broedteksten er IKKE YAML -> der er unquote() alene riktig regel, delt
// import fra okf-index (produsent-siden) og ikke en kopi. Uten den ga samme
// fil to lesninger: produsenten tolket `okf_version: "0.2"` som 0.2,
// checkeren ekkoet «"0.2"». Upstreams eneste kanoniske eksempel med verdi
// (SPEC.md:773) er sitert, saa det er formen en spec-tro bundle har paa disk.
function rootMarkers(root) { function rootMarkers(root) {
const idx = join(root, 'index.md'); const idx = join(root, 'index.md');
if (!existsSync(idx)) return { okfVersion: null, okfLayout: null }; if (!existsSync(idx)) return { okfVersion: null, okfLayout: null };
const raw = readFileSync(idx, 'utf8'); const raw = readFileSync(idx, 'utf8');
const { raw: fmRaw } = parseFrontmatter(raw);
// Rot-nivaa ogsaa her: broedtekst-fallbacken under har ALLTID vaert rot-ankret
// (`^key:` uten \s*), saa en innrykk-tolerant frontmatter-gren ga samme fil to
// svar avhengig av hvilket lag som traff foerst.
const fmGet = rootLevelGet(fmRaw);
// Broedteksten = alt UNDER frontmatter-blokken, slik at de to lagene kan feile
// hver for seg (mutasjon M1 roedner (20a)/(20d) NETTOPP fordi fallbacken ikke
// ser frontmatter-linjene).
//
// DEKNING: uttrekket ER produksjonsobserverbart siden lib/frontmatter.mjs's
// list-key-kontrakt ble sann (verdi-grenen strammet fra `\s*` til `[ \t]*`).
// get() paa en list-key gir naa null, saa fm-laget taper og fallbacken kjoerer
// for list-keys -- foerste gang den grenen naas i praksis. Uten uttrekket ville
// `^okf_version:` matchet INNE i frontmatteren og plukket «- 9.9». `body = raw`
// roedner naa (L7) (mutasjons-verifisert). S57 beholdt dette mot nettopp den
// latente defekten; den er ikke lenger latent, og vakten er ekte.
const body = fmRaw === null ? raw : raw.slice(raw.indexOf('\n---', 3) + 4);
const pick = (key) => { const pick = (key) => {
const fromFm = fmGet(key); const m = raw.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
if (fromFm !== null) return unquote(fromFm); return m ? m[1].trim() : null;
const m = body.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
return m ? unquote(m[1].trim()) : null;
}; };
return { okfVersion: pick('okf_version'), okfLayout: pick('okf_layout') }; return { okfVersion: pick('okf_version'), okfLayout: pick('okf_layout') };
} }
@ -146,7 +88,7 @@ export function checkBundle(root, { strictIngest = false, files } = {}) {
const strictErrors = []; const strictErrors = [];
for (const f of concepts) { for (const f of concepts) {
const raw = readFileSync(f, 'utf8'); const raw = readFileSync(f, 'utf8');
const get = rootLevelGet(parseFrontmatter(raw).raw); const { get } = parseFrontmatter(raw);
const rel = relative(root, f); const rel = relative(root, f);
const type = get('type'); const type = get('type');
if (!type) { if (!type) {

View file

@ -2,25 +2,16 @@
// okf-index.mjs // okf-index.mjs
// Genererer OKF-kompatible `index.md` per nivaa i en bundle-rot (prosjekt // Genererer OKF-kompatible `index.md` per nivaa i en bundle-rot (prosjekt
// `.claude/okr/` eller home `~/.claude/okr/org/`). Verbatim OKF-«Documents/kb // `.claude/okr/` eller home `~/.claude/okr/org/`). Verbatim OKF-«Documents/kb
// Layout»-index-form. ROT-index: // Layout»-index-form:
// ---
// okf_version: <upstream> (frontmatter -- OKF-versjonen bundelen sikter mot)
// ---
//
// # Overskrift // # Overskrift
// //
// okf_layout: <revisjon> (broedtekst -- vaar egen layout-revisjon, valgfri) // okf_version: <upstream> (KUN rot-index -- OKF-versjonen bundelen sikter mot)
// okf_layout: <revisjon> (KUN rot-index -- vaar egen layout-revisjon, valgfri)
// //
// * [title](relativ.md) - description // * [title](relativ.md) - description
// UNDERKATALOG-index: samme, men uten frontmatter og uten markoerer. // Ingen frontmatter paa index.md (OKF-reservert). Konsept-filers `title`/
// // `description` leses via lib/frontmatter.mjs. Underkataloger faar en peker til
// De to markoerene ligger paa ULIKE flater med vilje (beslutning 6, D8 steg 21): // sin egen index.md. Kjoeres PER ROT (de to bundlene har ulik livssyklus).
// `okf_version` er upstream-eid og den maskinlesbare kontrakten konsumenter
// utenfor pluginen leser -> frontmatter; `okf_layout` er vaar egen revisjon ->
// broedtekst. Frontmatter paa index.md gjelder DERFOR kun roten -- underkatalog-
// indekser er fortsatt frontmatter-frie. Konsept-filers `title`/`description`
// leses via lib/frontmatter.mjs. Underkataloger faar en peker til sin egen
// index.md. Kjoeres PER ROT (de to bundlene har ulik livssyklus).
// //
// Idempotens / vedlikehold (NFR): en eksisterende index.md sin `# overskrift`, // Idempotens / vedlikehold (NFR): en eksisterende index.md sin `# overskrift`,
// rotens markoer-verdier, og menneske-skrevne beskrivelser for underkatalog- // rotens markoer-verdier, og menneske-skrevne beskrivelser for underkatalog-
@ -32,7 +23,7 @@
import { readdirSync, readFileSync, writeFileSync, existsSync, renameSync } from 'node:fs'; import { readdirSync, readFileSync, writeFileSync, existsSync, renameSync } from 'node:fs';
import { join, basename, resolve } from 'node:path'; import { join, basename, resolve } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { parseFrontmatter, writeFrontmatter } from '../lib/frontmatter.mjs'; import { parseFrontmatter } from '../lib/frontmatter.mjs';
// To distinkte markoerer (OKF-spec §12) -- ett felt skal ikke baere to urelaterte // To distinkte markoerer (OKF-spec §12) -- ett felt skal ikke baere to urelaterte
// konsepter. `okf_version` = upstream Google OKF-versjonen bundelen sikter mot // konsepter. `okf_version` = upstream Google OKF-versjonen bundelen sikter mot
@ -55,7 +46,7 @@ const UPSTREAM_VERSION_RE = /^\d+(?:\.\d+)+$/;
// upstream-versjon som en layout-verdi og migreres til feil markoer. // upstream-versjon som en layout-verdi og migreres til feil markoer.
// KUN et matchende par strippes: en halv anfoerselstegn-sekvens er en ugyldig // KUN et matchende par strippes: en halv anfoerselstegn-sekvens er en ugyldig
// verdi som skal bevares uroert, ikke gjettes paa. // verdi som skal bevares uroert, ikke gjettes paa.
export function unquote(s) { function unquote(s) {
const q = s[0]; const q = s[0];
if ((q === '"' || q === "'") && s.length >= 2 && s.endsWith(q)) return s.slice(1, -1); if ((q === '"' || q === "'") && s.length >= 2 && s.endsWith(q)) return s.slice(1, -1);
return s; return s;
@ -207,22 +198,11 @@ function writeIndexFor(dir, isRoot, explicitLayout, callerRank) {
const heading = existing.heading const heading = existing.heading
|| (isRoot ? 'OKF second brain' : titleFromName(basename(dir))); || (isRoot ? 'OKF second brain' : titleFromName(basename(dir)));
// Beslutning 6: de to markoerene har ulike eiere og ulik livssyklus, saa de const lines = [`# ${heading}`, ''];
// faar ulike flater. `okf_version` (upstream, Google-eid) er den maskinlesbare
// kontrakten konsumenter utenfor pluginen leser -> frontmatter. `okf_layout`
// (vaar egen revisjon) blir staaende i broedteksten. KUN rot-indeksen; en
// underkatalog-index faar aldri frontmatter.
const lines = [];
let rootLayout = null;
if (isRoot) { if (isRoot) {
const { version, layout } = resolveMarkers(existing, explicitLayout); const { version, layout } = resolveMarkers(existing, explicitLayout);
rootLayout = layout; lines.push(`okf_version: ${version}`, `okf_layout: ${layout}`, '');
// Siste element av split er '' (writeFrontmatter avslutter med \n) -- droppes
// her og erstattes av den bevisste blanklinja mellom blokk og overskrift.
lines.push(...writeFrontmatter({ okf_version: version }).split('\n').slice(0, -1), '');
} }
lines.push(`# ${heading}`, '');
if (isRoot) lines.push(`okf_layout: ${rootLayout}`, '');
for (const sd of subdirs) { for (const sd of subdirs) {
const link = `${sd}/index.md`; const link = `${sd}/index.md`;

View file

@ -1,156 +0,0 @@
#!/usr/bin/env node
// syklus-rapport.mjs
// D5 steg 10: orkestrator for syklus-generatorene. Leser en syklus-katalog,
// bygger den forespurte rapportformen og persisterer den atomisk.
//
// Bruk:
// node syklus-rapport.mjs <syklus-dir> <form>
//
// Former: tertial | arsrapport | etatsstyring
// EN kommandoflate med argument, ikke tre kommandoer -- samme moenster som
// /okr:oppsett full|mvp|arkiver|oppdater|vis. `etatsstyring` kommer i steg 12
// og svarer inntil da med exit 2 og en forklarende melding, aldri stum feil.
//
// Exit-koder (kontrakten commands/rapport.md mapper til norsk brukertekst):
// 0 rapporten er skrevet
// 1 domenefeil -- syklusdataene er ufullstendige eller ikke rapporterbare
// 2 bruksfeil -- feil aritet, ukjent/uimplementert form, katalog finnes ikke
//
// Klokke-soem: OKR_NOW (ISO-8601) overstyrer veggklokka, saa to kjoeringer over
// samme data gir byte-identisk fil (moenster: scripts/compose-org-profile.mjs:61).
//
// Zero npm dependencies.
import { existsSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
import { basename, dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseFrontmatter, writeFrontmatter } from '../lib/frontmatter.mjs';
import { lesSyklus } from '../lib/syklus-data.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
// utenfor et OKR-tre pekt paa en vilkaarlig katalog paa maskinen, og rapporten
// variert med hva som tilfeldigvis laa der. Derfor avledes stien KUN naar
// foreldrekatalogen faktisk heter `syklus`; ellers finnes det ingen historikk.
//
// Katalogen leses direkte -- ALDRI via index.md. Indeksen er navigasjon og kan
// peke paa filer som ikke finnes (fixturen har en slik dangling-lenke med vilje);
// et styringsdokument som lister materiale som ikke eksisterer er verre enn et
// som utelater seksjonen. readdir sorteres, fordi byte-identiske kjoeringer er
// kontrakt og lesrekkefolgen fra filsystemet ikke er garantert.
function lesHistorikk(syklusDir) {
const syklusRot = dirname(resolve(syklusDir));
if (basename(syklusRot) !== 'syklus') return [];
const dir = join(dirname(syklusRot), 'historikk');
let filer;
try {
filer = readdirSync(dir);
} catch {
return [];
}
return filer
.filter((n) => n.endsWith('.md') && n !== 'index.md')
.sort()
.map((fil) => {
const fm = parseFrontmatter(readFileSync(join(dir, fil), 'utf8'));
return { tittel: fm.get('title') ?? fil.replace(/\.md$/, ''), fil };
});
}
const FORMER = {
tertial: {
filnavn: 'rapport-tertial.md',
tittel: (id) => `Tertialrapport ${id}`,
beskrivelse: 'Maskingenerert tertialrapport med committed og aspirational holdt fra hverandre.',
bygg: tertialrapport,
},
arsrapport: {
filnavn: 'rapport-arsrapport.md',
tittel: (id) => `Aarsrapport del III ${id}`,
beskrivelse: 'Maskingenerert underlag til aarsrapportens del III. Vurderingen av maaloppnaaelse er reservert til virksomheten.',
bygg: arsrapportDelIII,
// Disklesing hoerer til orkestratoren, ikke til lib/: generatorformen skal
// kunne testes uten et tre paa filsystemet.
kontekst: (syklusDir) => ({ historikk: lesHistorikk(syklusDir) }),
},
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 {}
// Skrevet fil er OKF-konsept, ikke loes markdown: den lander INNE i bundlen, og
// en fil uten `type:` ville felt okf-check for hele roten. `type: Status` er
// dessuten riktig -- rapporten ER en statusflate, og blir retrievbar for
// okr-second-brain-search paa kjoepet.
// Filnavnet starter bevisst IKKE med `okr-`: ellers ville neste lesSyklus()
// plukket rapporten opp som en OKR-definisjon.
function komponer(form, syklus, naa, kontekst) {
const frontmatter = writeFrontmatter({
type: 'Status',
resource: 'local',
title: form.tittel(syklus.id),
description: form.beskrivelse,
timestamp: naa,
});
return `${frontmatter}\n${form.bygg(syklus, { naa, ...kontekst })}`;
}
// Atomisk skriv: temp i SAMME katalog (rename er kun atomisk innen filsystem),
// process.pid i navnet mot samtidige kjoeringer. Moenster: lib/innboks-write.mjs.
function skrivAtomisk(maal, innhold) {
const temp = `${maal}.${process.pid}.tmp`;
try {
writeFileSync(temp, innhold, 'utf8');
renameSync(temp, maal);
} catch (e) {
if (existsSync(temp)) {
try { unlinkSync(temp); } catch { /* opprydding er best-effort */ }
}
throw e;
}
}
export function genererRapport(syklusDir, formNavn, opts = {}) {
if (!syklusDir || !formNavn) {
throw new Bruksfeil('Bruk: node syklus-rapport.mjs <syklus-dir> <form>\n'
+ `Former: ${Object.keys(FORMER).join(' | ')}`);
}
if (!(formNavn in FORMER)) {
throw new Bruksfeil(`Ukjent form: ${formNavn}. Former: ${Object.keys(FORMER).join(' | ')}`);
}
const form = FORMER[formNavn];
if (!existsSync(syklusDir) || !statSync(syklusDir).isDirectory()) {
throw new Bruksfeil(`Syklus-katalogen finnes ikke: ${syklusDir}`);
}
const naa = opts.naa || process.env.OKR_NOW || new Date().toISOString();
const syklus = lesSyklus(syklusDir);
const kontekst = form.kontekst ? form.kontekst(syklusDir) : {};
const maal = join(syklusDir, form.filnavn);
skrivAtomisk(maal, komponer(form, syklus, naa, kontekst));
return { fil: maal, syklus: syklus.id, okrer: syklus.okrer.length };
}
// --- CLI ---
const isMain = process.argv[1]
&& fileURLToPath(import.meta.url) === process.argv[1];
if (isMain) {
const [syklusDir, form] = process.argv.slice(2);
try {
const r = genererRapport(syklusDir, form);
process.stdout.write(
`Rapport skrevet: ${r.fil}\n Syklus: ${r.syklus}\n OKR rapportert: ${r.okrer}\n`,
);
process.exit(0);
} catch (e) {
process.stderr.write(`${basename(process.argv[1])}: ${e.message}\n`);
process.exit(e instanceof Bruksfeil ? 2 : 1);
}
}

View file

@ -1,160 +0,0 @@
#!/usr/bin/env node
// syklus-status.mjs
// S60: siste ledd i kjeden generator -> /okr:sporing -> status.md.
//
// Bruk:
// node syklus-status.mjs <syklus-dir> < confidence.json
//
// Confidence leses fra STDIN som JSON: et oppslag fra KR-navn til etikett i det
// lukkede vokabularet (On Track | At Risk | Off Track).
//
// node scripts/syklus-status.mjs .claude/okr/syklus/T1-2026 <<'JSON'
// { "Andel digitale soeknader (prosent)": "At Risk" }
// JSON
//
// HVORFOR STDIN, og ikke flagg: KR-navn er norsk fritekst med mellomrom,
// parenteser og prosenttegn. Som `--confidence "<navn>=<etikett>"` ville hvert
// navn maattet siteres riktig gjennom et shell-lag, og en feilsitering ville
// gitt feil etikett paa riktig KR -- en stille feilrapportering. JSON har
// entydig avgrensning og feiler hoeylytt.
//
// ARBEIDSDELINGEN (laast beslutning 9): /okr:sporing eier VURDERINGEN og leverer
// den som data hit. Dette scriptet eier ARITMETIKKEN og FORMATERINGEN, og henter
// begge fra lib/syklus-rapport.mjs -- de samme radene tertialrapporten bygger.
// Ingen LLM skriver av et tall paa veien, og ingen maskin utleder et trafikklys.
//
// Exit-koder (kontrakten commands/sporing.md mapper til norsk brukertekst):
// 0 status.md er skrevet
// 1 domenefeil -- ugyldig/manglende confidence, eller kuratert fil i veien
// 2 bruksfeil -- feil aritet, katalog finnes ikke, ugyldig JSON paa stdin
//
// Klokke-soem: OKR_NOW (ISO-8601) overstyrer veggklokka, saa to kjoeringer over
// samme data gir byte-identisk fil (moenster: scripts/syklus-rapport.mjs:19).
//
// Zero npm dependencies.
import { existsSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
import { basename, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseFrontmatter, writeFrontmatter } from '../lib/frontmatter.mjs';
import { lesSyklus } from '../lib/syklus-data.mjs';
import { statusrapport } from '../lib/syklus-rapport.mjs';
const FILNAVN = 'status.md';
// Provenans-noekkelen kuratert-fil-vernet leser. Samme mekanikk som
// lib/innboks-write.mjs:88-91 bruker med `kilde: innboks`: en eksisterende
// maal-fil UTEN noekkelen er haandkuratert, og skrivingen avvises.
const PROVENANS = { noekkel: 'kilde', verdi: 'sporing' };
class Bruksfeil extends Error {}
// Vernet er det eneste stedet dette scriptet kan oedelegge noe for brukeren:
// status.md kan vaere skrevet for haand, og ligger i BRUKERENS syklusdata --
// ikke i repoet, altsaa ikke en commit unna. Et stille overskriv ville vaert
// datatap uten spor. Egne tidligere kjoeringer overskrives fritt: de baerer
// noekkelen, og idempotens er hele poenget med klokke-soemmen.
function krevSkrivbar(maal) {
if (!existsSync(maal)) return;
let fm;
try {
fm = parseFrontmatter(readFileSync(maal, 'utf8'));
} catch {
fm = null;
}
if (fm?.get(PROVENANS.noekkel) === PROVENANS.verdi) return;
throw new Error(
`nekter aa overskrive kuratert (ikke-generert) fil: ${maal}\n`
+ ` Fila mangler "${PROVENANS.noekkel}: ${PROVENANS.verdi}" i frontmatteren og kan vaere skrevet for haand.\n`
+ ' Flytt eller slett den hvis den skal erstattes av en generert statusrapport.',
);
}
// Atomisk skriv: temp i SAMME katalog (rename er kun atomisk innen filsystem),
// process.pid i navnet mot samtidige kjoeringer. Moenster: scripts/syklus-rapport.mjs:107.
function skrivAtomisk(maal, innhold) {
const temp = `${maal}.${process.pid}.tmp`;
try {
writeFileSync(temp, innhold, 'utf8');
renameSync(temp, maal);
} catch (e) {
if (existsSync(temp)) {
try { unlinkSync(temp); } catch { /* opprydding er best-effort */ }
}
throw e;
}
}
// Skrevet fil er OKF-konsept, ikke loes markdown: den lander INNE i bundlen, og
// en fil uten `type:` ville felt okf-check for hele roten. `type: Status` er
// riktig -- dette ER statusflaten, og den blir retrievbar for
// okr-second-brain-search paa kjoepet.
function komponer(syklus, naa, confidence) {
const frontmatter = writeFrontmatter({
type: 'Status',
resource: 'local',
title: `Statusrapport ${syklus.id}`,
description: 'Maskingenerert statusrapport. Confidence er satt av /okr:sporing.',
timestamp: naa,
[PROVENANS.noekkel]: PROVENANS.verdi,
});
return `${frontmatter}\n${statusrapport(syklus, { naa, confidence })}`;
}
export function skrivStatus(syklusDir, confidence, opts = {}) {
if (!syklusDir) {
throw new Bruksfeil('Bruk: node syklus-status.mjs <syklus-dir> < confidence.json');
}
if (!existsSync(syklusDir) || !statSync(syklusDir).isDirectory()) {
throw new Bruksfeil(`Syklus-katalogen finnes ikke: ${syklusDir}`);
}
const naa = opts.naa || process.env.OKR_NOW || new Date().toISOString();
const syklus = lesSyklus(syklusDir);
const maal = join(syklusDir, FILNAVN);
// Vernet foer komponeringen: en avvist skriv skal ikke koste noe, og
// rekkefolgen gjoer det umulig aa skrive foerst og oppdage vernet etterpaa.
krevSkrivbar(maal);
skrivAtomisk(maal, komponer(syklus, naa, confidence));
return { fil: maal, syklus: syklus.id, krer: syklus.okrer.reduce((n, o) => n + o.krer.length, 0) };
}
// --- CLI ---
function lesStdin() {
let raa;
try {
raa = readFileSync(0, 'utf8');
} catch {
throw new Bruksfeil('Klarte ikke aa lese confidence fra stdin. Pipe inn JSON: ... | node syklus-status.mjs <dir>');
}
if (raa.trim() === '') {
throw new Bruksfeil('Tom stdin. Forventet JSON: { "<KR-navn>": "On Track | At Risk | Off Track" }');
}
try {
return JSON.parse(raa);
} catch (e) {
throw new Bruksfeil(`Ugyldig JSON paa stdin: ${e.message}`);
}
}
const isMain = process.argv[1]
&& fileURLToPath(import.meta.url) === process.argv[1];
if (isMain) {
const [syklusDir] = process.argv.slice(2);
try {
// Arity foer stdin: uten katalog er det ingenting aa lese confidence FOR.
if (!syklusDir) {
throw new Bruksfeil('Bruk: node syklus-status.mjs <syklus-dir> < confidence.json');
}
const r = skrivStatus(syklusDir, lesStdin());
process.stdout.write(
`Status skrevet: ${r.fil}\n Syklus: ${r.syklus}\n KR med confidence: ${r.krer}\n`,
);
process.exit(0);
} catch (e) {
process.stderr.write(`${basename(process.argv[1])}: ${e.message}\n`);
process.exit(e instanceof Bruksfeil ? 2 : 1);
}
}

View file

@ -2,7 +2,7 @@
name: okr-offentlig-sektor name: okr-offentlig-sektor
description: >- description: >-
OKR (Objectives and Key Results) for Norwegian public sector: writing OKR, reviewing OKR quality, cascading OKR from strategy to team, tracking progress, running OKR meetings, translating tildelingsbrev to OKR. Also CFR, OKR antipatterns, scoring, Oboard. Triggers on: "OKR", "skriv OKR", "vurder OKR", "OKR-scoring", "kaskadere OKR", "tildelingsbrev til OKR", "OKR for offentlig sektor". OKR (Objectives and Key Results) for Norwegian public sector: writing OKR, reviewing OKR quality, cascading OKR from strategy to team, tracking progress, running OKR meetings, translating tildelingsbrev to OKR. Also CFR, OKR antipatterns, scoring, Oboard. Triggers on: "OKR", "skriv OKR", "vurder OKR", "OKR-scoring", "kaskadere OKR", "tildelingsbrev til OKR", "OKR for offentlig sektor".
version: "1.10.0" version: "1.8.2"
--- ---
# OKR Skill for Offentlig Sektor (Norge) # OKR Skill for Offentlig Sektor (Norge)
@ -143,10 +143,8 @@ All reference material is in `references/`:
- `metrics-library.md` — Common KPIs for transport/roads/digital services - `metrics-library.md` — Common KPIs for transport/roads/digital services
### Governance ### Governance
- `okr-offentlig-governance.md` — Tildelingsbrev, political steering, audit readiness (state sector) - `okr-offentlig-governance.md` — Tildelingsbrev, political steering, audit readiness
- `okr-kommunal-styring.md` — Municipal steering line: self-government, økonomiplan, kommunedirektør (no tildelingsbrev)
- `dfo-okr-mapping.md` — DFØ terminology to OKR terminology bridge - `dfo-okr-mapping.md` — DFØ terminology to OKR terminology bridge
- `gevinstrealisering-okr.md` — Nyttestyring (DFØ 2026, replaces gevinstrealisering): result chain, effect-level KR, benefit owner as KR owner
- `okr-implementation.md` — Rollout methodology and change management - `okr-implementation.md` — Rollout methodology and change management
- `okr-integrations.md` — OKR + Scrum/Kanban/SAFe and tool integration - `okr-integrations.md` — OKR + Scrum/Kanban/SAFe and tool integration
- `individual-vs-team-okr.md` — Team vs individual OKR rationale and alternatives - `individual-vs-team-okr.md` — Team vs individual OKR rationale and alternatives

View file

@ -1,191 +0,0 @@
# Gevinstrealisering, nyttestyring og OKR
Et OKR-sett som stopper ved leveransen måler output, ikke effekt. Denne fila dekker
metodikken staten bruker for å hente ut effekten *etter* leveransen — og hvordan den
kobles til Key Results uten at koblingen blir en oversettelsesøvelse.
**Merk begrepsskiftet før du leser videre:** DFØ byttet i april 2026 ut
«gevinstrealisering» med **«nyttestyring»** og «gevinster» med **«nyttevirkninger»**.
Filnavnet beholder det gamle ordet fordi det er det folk søker på og sier i praksis.
Teksten bruker DFØs gjeldende begreper som primære.
## Innholdsfortegnelse
1. [Begrepsskiftet: fra gevinstrealisering til nyttestyring](#begrepsskiftet-fra-gevinstrealisering-til-nyttestyring)
2. [Resultatkjeden: hvor i kjeden et KR hører hjemme](#resultatkjeden-hvor-i-kjeden-et-kr-hører-hjemme)
3. [Effektiv ressursbruk dekomponert](#effektiv-ressursbruk-dekomponert)
4. [Hvem eier nytten](#hvem-eier-nytten)
5. [Hvorfor effekt-KR er vanskelig: revisjonsfunn](#hvorfor-effekt-kr-er-vanskelig-revisjonsfunn)
6. [Fra nytteoversikt til Key Result](#fra-nytteoversikt-til-key-result)
7. [Kilder](#kilder)
## Begrepsskiftet: fra gevinstrealisering til nyttestyring
DFØ publiserte 24. april 2026 **«Veileder om nyttestyring av statlige tiltak»**. Den er
ikke et tillegg til den tidligere veiledningen — den **erstatter** den. DFØs egen ordlyd:
> «Denne veilederen erstatter veilederen «Gevinstrealisering planlegging for å hente ut
> gevinster av offentlige prosjekter» (2014). En sentral endring er at vi har byttet ut
> begrepet gevinstrealisering med nyttestyring fordi det gir en riktigere beskrivelse av
> metoden. Gevinster er erstattet med nyttevirkninger.»
| Tidligere begrep (2014) | Gjeldende begrep (2026) |
|---|---|
| Gevinstrealisering | Nyttestyring |
| Gevinst | Nyttevirkning |
| Gevinstrealiseringsplan | Nytterealiseringsplan |
| Gevinstoversikt | Nytteoversikt |
Nyttestyring er ifølge DFØ «en metode for å utrede, planlegge, gjennomføre og følge opp
tiltak slik at statlige midler brukes til det beste for samfunnet». Metoden bygger på
prinsippene fra gevinstrealisering, men strekker seg over hele tiltakets livsløp og gjør
samfunnsøkonomisk lønnsomhet til et tydeligere styringsprinsipp.
### Hva dette betyr for språkbruken i en OKR-prosess
Bruk DFØs gjeldende begreper i dokumenter som skal ut av virksomheten — tildelingsbrev-svar,
årsrapport, etatsstyringsmøter. Innad tåler de fleste organisasjoner en overgangsperiode
der begge ordene lever side om side. **Ikke bruk energi på å korrigere folk som sier
«gevinst»** — poenget er ikke ordet, men at målingen ligger på effekten og ikke på
leveransen.
### Virkeområde — og hva det ikke er
> «Nyttestyring skal brukes i prosjekter som er omfattet av statens prosjektmodell
> (investeringsprosjekter med en anslått kostnadsramme på over 1 milliard kroner, eller
> over 300 millioner kroner for digitaliseringsprosjekter). Vi vil presisere at
> nyttestyring også er relevant for prosjekter og investeringstiltak med en langt lavere
> kostnadsramme enn disse terskelverdiene.»
Under terskelverdiene er veilederen et **støtteverktøy, ikke et krav**. DFØ sier det selv:
«Veilederen er ikke ment som et nytt krav eller en fast metode som skal brukes i alle
sammenhenger.» En OKR-prosess skal derfor ikke fremstille nyttestyring som en plikt for
enhver virksomhet — det ville vært å skjerpe et krav kilden ikke stiller.
## Resultatkjeden: hvor i kjeden et KR hører hjemme
DFØs begrepsapparat bygger på en resultatkjede som gjør det presist hvor et Key Result
skal ligge:
```
Innsatsfaktorer → Aktiviteter og prosesser → Produkter og tjenester
→ Brukereffekter → Samfunnseffekter
```
| Ledd i kjeden | OKR-plassering |
|---|---|
| Innsatsfaktorer (ressurser, årsverk, budsjett) | Forutsetning — aldri et KR |
| Aktiviteter og prosesser | Aktivitet — bevisst **ikke** et KR |
| Produkter og tjenester | Leveranse — svakt KR, aksepteres bare som milepæl |
| **Brukereffekter** | **Her hører de fleste KR-er hjemme** |
| **Samfunnseffekter** | Objective-nivå, eller lagging-KR over flere sykluser |
Nytteoversikten fra en nyttestyringsprosess er allerede formulert på effektnivå. Det er
derfor den er verdt så mye i OKR-arbeid: **den har gjort outcome-formuleringen ferdig.**
Har virksomheten en nytteoversikt, er den råmaterialet for KR-ene — ikke noe som skal
oversettes på nytt.
## Effektiv ressursbruk dekomponert
DFØ deler «effektiv ressursbruk» i tre, og skillet er nyttig når et Objective skal gjøres
målbart uten å kollapse til kroner spart:
| Type | DFØs formulering | Som KR ser det slik ut |
|---|---|---|
| **Kostnadseffektivitet** | «gjøre tingene riktig» | Enhetskostnad, saksbehandlingstid, ressursbruk per leveranse |
| **Formålseffektivitet** | «gjøre de riktige tingene» | Brukereffekt, måloppnåelse mot samfunnsoppdraget |
| **Prioriteringseffektivitet** | «prioritere mellom ulike mål … som kan være i konflikt med hverandre» | Fordeling mellom områder; sjelden ett tall — ofte et Objective |
Den vanligste feilen er å måle bare kostnadseffektivitet fordi den er lettest å telle, og
så kalle resultatet «effektivisering». En virksomhet kan bli svært kostnadseffektiv på noe
den ikke burde gjort i det hele tatt. **Har OKR-settet bare kostnadseffektivitets-KR-er,
mangler det formålseffektiviteten** — og det er den departementet spør etter i
styringsdialogen.
## Hvem eier nytten
Nytteansvarlig (tidligere: gevinstansvarlig) skal være en **linjeleder** fra den delen av
organisasjonen som faktisk skal realisere nytten — ikke prosjektlederen. Det tilsvarer
KR-eier i OKR.
Dette er allerede utdypet med begrunnelse, ansvarslinjer og konsekvensen for
committed-merking i **`okr-offentlig-governance.md`, seksjonen «Gevinstrealisering»** —
inkludert mappingtabellen mellom de to begrepsapparatene. Se dit; det gjentas ikke her.
## Hvorfor effekt-KR er vanskelig: revisjonsfunn
To dokumenterte funn forklarer hvorfor effektmåling er vanskelig i praksis — og hvorfor et
verktøy som gjør den enklere har en reell adressat. **Begge er historiske funn som
forvaltningen har arbeidet med siden**, og de skal siteres slik, aldri som stående kritikk.
### Årsrapporter beskriver aktiviteter, i liten grad effekter (2019)
Riksrevisjonens undersøkelse om departementenes styringsinformasjon og rapportering til
Stortinget på et tverrsektorielt område, med barnefattigdom som eksempel (2019, del av
Dokument 1 (20192020)), fant at årsrapportene «gir informasjon om aktiviteter og
tjenesteleveranser og i liten grad effekter». Begrunnelsen som oppgis er måleproblemer —
blant annet manglende statistikk og rapportering.
Det er verdt å merke seg *hvorfor*: dette er ikke uvilje, det er at effekt er dyrere å måle
enn aktivitet. En OKR-prosess som krever effekt-KR uten å skaffe datagrunnlaget flytter bare
problemet.
### Årsrapporten som øyeblikksbilde (NIBIO, 2020)
Riksrevisjonen gjennomgikk NIBIOs årsrapporter for 20152019 og skrev:
> «Framstillingen synliggjør i mindre grad NIBIOs analyser av måloppnåelsen og framstår som
> et øyeblikksbilde av NIBIOs aktiviteter og resultater. NIBIO rapporterer kvantitativt på
> blant annet publikasjonspoeng, gjennomsnittlig faktureringsgrad per årsverk og fakturerte
> timer per forskerårsverk, men vurderer eller kommenterer i liten grad måloppnåelse og
> effektiv ressursbruk med utgangspunkt i disse tallene.»
>
> — Riksrevisjonen (2020), del av Dokument 1 (20202021), s. 107
**Riksrevisjonen har senere avsluttet saken etter forbedringer i mål- og resultatstyringen**
(gjengitt i DFØ-notat 2026:2 s. 48). Funnet står altså som et dokumentert antimønster, ikke
som en gjeldende merknad mot NIBIO.
Antimønsteret er presist: det er ikke mangel på tall. NIBIO rapporterte tall. Det som manglet
var **analysen som knytter tallene til måloppnåelsen**. Et OKR-sett kan feile på nøyaktig
samme måte — mange KR-er med pene verdier, og ingen vurdering av hva verdiene betyr for
Objectivet.
## Fra nytteoversikt til Key Result
1. **La nyttevirkningene bli KR, ikke leveransene.** Har tiltaket en nytteoversikt, er
outcome-formuleringen allerede gjort — bruk den framfor å oppfinne en ny.
2. **Sett KR-eier = nytteansvarlig.** Er de forskjellige personer, har du to parallelle
ansvarslinjer for samme effekt, og ingen av dem eier den helt.
3. **Regn med at nytten kommer etter syklusen leveransen skjer i.** Leveranse og effekt
hører ofte hjemme i to ulike OKR-sykluser. Det er normalt — ikke press effekten inn i
leveranse-syklusen for å få et pent tall ved tertialslutt.
4. **Skriv ned hvordan nyttevirkningen skal måles før syklusen starter.** Er målemetoden
uavklart ved syklusstart, blir KR-en i praksis en aktivitet uansett hvordan den er
formulert.
5. **Ta med minst én formålseffektivitets-KR.** Et sett som bare måler kostnad besvarer
ikke spørsmålet styringsdialogen faktisk stiller.
## Kilder
- [DFØ — Veileder om nyttestyring av statlige tiltak (24.04.2026)](https://www.dfo.no/node/94330/print) (offisiell) — erstatter gevinstrealiseringsveilederen fra 2014; begrepsskiftet, virkeområde, terskelverdier
- [DFØ — lanseringssak for nyttestyringsveilederen](https://www.dfo.no/nyhetsarkiv/dfo-lanserer-ny-veileder-om-nyttestyring-av-statlige-tiltak) (offisiell) — «ikke et krav et støtteverktøy»
- [DFØ-notat 2026:2 «Styring i staten»](https://www.dfo.no/sites/default/files/2026-05/dfo-notat-2026-2-styring-i-staten-dokumentgjennomgang-tilstand-utfordringer_0.pdf) (offisiell) — resultatkjeden og dekomponeringen av effektiv ressursbruk (kap. 1.5, s. 89); avslutningsstatus for NIBIO-saken (s. 48)
- [Riksrevisjonen — Undersøkelse av etats- og virksomhetsstyringen av NIBIO (03.11.2020)](https://www.riksrevisjonen.no/rapporter-mappe/no-2020-2021/undersokelse-av-etats--og-virksomhetsstyringen-av-norsk-institutt-for-biookonomi) (offisiell) — sammendrag og kritikknivå
- Riksrevisjonen (2019), «Riksrevisjonens undersøkelse om departementenes styringsinformasjon og rapportering til Stortinget på et tverrsektorielt område med barnefattigdom som eksempel», del av Dokument 1 (20192020) — gjengitt i DFØ-notat 2026:2 s. 53; ikke lest i Riksrevisjonens egen fulltekst
- [Riksrevisjonen — Dokument 1 (20202021), fulltekst](https://www.stortinget.no/globalassets/pdf/dokumentserien/2020-2021/dok1-202021.pdf) (offisiell) — NIBIO-sitatet, s. 107
- [DFØ-notat 2026:1 «Spredt nytte, konkret kostnad»](https://www.dfo.no/sites/default/files/2026-03/dfo-notat-2026-1-spredt-nytte-konkret-kostnad_1.pdf) (offisiell) — dokumentasjon av nytte i digitaliseringstiltak
**Siteringsdisiplin for denne fila:** funnet om at årsrapporter beskriver aktiviteter og i
liten grad effekter (2019) er gjengitt via DFØ-notat 2026:2 s. 53, ikke lest i Riksrevisjonens
egen fulltekst; det dateres derfor til 2019 uten avslutningspåstand. NIBIO-funnet er derimot
verifisert mot primærkilden, og avslutningen er belagt via DFØ. Skillet er bevisst — ikke
utjevn det ved å gi begge samme sikkerhetsnivå.
### Beslektede referanser
- `okr-offentlig-governance.md` — nytteansvarlig/KR-eier, mappingtabellen, tildelingsbrev og årsrapport
- `okr-framework.md` — kanonisk OKR-metodikk (scoring, kadens, confidence, committed/aspirational)
- `metrics-library.md` — metrikker og målenivå, inkludert datapunktet om gevinstrealisering som feiler
- `okr-antipatterns.md` — aktivitet-som-KR og beslektede antimønstre
- `okr-kommunal-styring.md` — kommunal styringslinje; KOSTRA som indikatorkilde

View file

@ -235,13 +235,12 @@ Neste steg: [liste]
``` ```
Objective: [navn] Objective: [navn]
Committed: [X] av [Y] KR nådd Samlet score: [0.0-1.0]
Aspirational: [0.0-1.0] (snitt over [N] KR)
Key Results: Key Results:
1. [KR1 navn] (committed): Target [X], Actual [Y], Score [Z], Avvik [ja/nei] 1. [KR1 navn]: Target [X], Actual [Y], Score [Z]
2. [KR2 navn] (committed): Target [X], Actual [Y], Score [Z], Avvik [ja/nei] 2. [KR2 navn]: Target [X], Actual [Y], Score [Z]
3. [KR3 navn] (aspirational): Target [X], Actual [Y], Score [Z] 3. [KR3 navn]: Target [X], Actual [Y], Score [Z]
Hva gikk bra: Hva gikk bra:
- [bullet] - [bullet]

View file

@ -232,8 +232,7 @@ Oppsummering av alle OKR for et team:
├────────────────────────────────────────────────────────────────┤ ├────────────────────────────────────────────────────────────────┤
│ TEAM TOTALT │ │ TEAM TOTALT │
│ │ │ │
│ Committed: ____ av ____ KR nådd │ │ Samlet score: ______ (______%) │
│ Aspirational: ______ (snitt over ____ KR) │
│ │ │ │
│ On Track: ____ OKR At Risk: ____ OKR Off Track: ____ OKR │ │ On Track: ____ OKR At Risk: ____ OKR Off Track: ____ OKR │
│ │ │ │

View file

@ -123,185 +123,6 @@ Etatens KR → Teamets Objective
I offentlig sektor anbefales en balanse: 60% committed, 40% aspirational. I offentlig sektor anbefales en balanse: 60% committed, 40% aspirational.
## Beyond Budgeting og target/forecast-separasjonen
Skillet mellom committed og aspirational over er ikke en OKR-oppfinnelse. Det er den samme
separasjonen Bjarte Bogsnes og Beyond Budgeting-miljøet har argumentert for siden
1990-tallet — og deres versjon gir den **kausale** forklaringen på hvorfor sandbagging
oppstår, der OKR-litteraturen ofte bare formaner mot det.
### Separasjonsmekanismen
Bogsnes' kjernepoeng er mer presist enn «avskaff budsjettet». Det er at ett tall ikke kan
tjene tre formål samtidig:
> «A target is an aspiration, **what we want to happen**, while a forecast is an
> expectation, **what we think will happen** … And last but not least resource allocation
> is about optimization of scarce resources.»
Det tradisjonelle budsjettet **bundler** disse tre i ett tall. Bundlingen er selve
problemet: hvis det samme tallet både er målet mitt, prognosen min og bevilgningen min, har
jeg sterk egeninteresse i å holde det lavt. Bogsnes er eksplisitt på at dette er
systemdrevet, ikke moralsk — «people respond to the system that we have set up for them …
this is not about fixing people, it's about fixing the system».
Etter separasjon *kan* målet være mer ambisiøst enn prognosen — og det bør det ofte være.
### Hvorfor dette er den samme mekanismen som committed/aspirational
| OKR-språk | Bogsnes' språk | Hva det er |
|---|---|---|
| **Aspirational** (0.7 forventet) | **Target** — «what we want to happen» | Aspirasjon. Skal *ikke* nås 100 %. |
| **Committed** (1.0 forventet) | Nærmere **forecast** — «what we think will happen» | Forventning og forpliktelse. |
| Sandbagging | Gaming av det bundlede budsjett-tallet | Samme patologi, samme årsak |
Konsekvensen er strukturell og bør leses som en advarsel: **får aspirational-scoren
konsekvenser i rapporteringen, har du re-bundlet det OKR nettopp skilte — og da kommer
sandbaggingen tilbake med matematisk nødvendighet.** Det er ikke et spørsmål om
disiplin hos den som setter målet.
### De tolv prinsippene
Prinsippene siteres på **ordlyd**, ikke på kortnavn. Grunnen er konkret: BBIs eget
materiale og utbredte sekundærkilder navngir flere av prinsippene ulikt, og et kortnavn
alene bærer derfor ikke entydig mening.
**Ledelsesprinsipper:**
| # | Ordlyd (BBI) |
|---|---|
| 1 | «Engage and inspire people around bold and noble causes; not around short-term financial targets» |
| 2 | «Govern through shared values and sound judgement; not through detailed rules and regulations» |
| 3 | «Make information open for self-regulation, innovation, learning and control; don't restrict it» |
| 4 | «Cultivate a strong sense of belonging and organise around accountable teams; avoid hierarchical control and bureaucracy» |
| 5 | «Trust people with freedom to act; don't punish everyone if someone should abuse it» |
| 6 | «Connect everyone's work with customer needs; avoid conflicts of interest» |
**Prosessprinsipper:**
| # | Ordlyd (BBI) |
|---|---|
| 1 | «Set directional, ambitious and relative goals; avoid fixed and cascaded targets» |
| 2 | «Make forecasting a lean and unbiased process; not a rigid and political exercise» |
| 3 | «Foster a cost conscious mind-set. Plan and make resources available as needed; not through detailed annual budget allocations» |
| 4 | «Evaluate performance holistically to guide interventions; not based on measurement only and not for rewards only» |
| 5 | «Reward shared success against competition; not against fixed performance contracts» |
| 6 | «Organise management processes dynamically around business rhythms and events; not around the calendar year only» |
### Hva som lar seg overføre til norsk offentlig sektor
Anvendbarheten deler seg i tre, og skillet går ikke mellom «bra» og «dårlig», men mellom
hva virksomheten selv rår over og hva Stortinget bestemmer.
**Gruppe A — fritt anvendbart.** Ledelsesprinsippene (formål framfor kortsiktige
finansielle mål, styring på verdier og skjønn, åpen informasjon, ansvarlige team, tillit,
kobling til brukerbehov). Ingenting i statlig eller kommunalt regelverk står i veien for
disse; de handler om hvordan virksomheten leder seg selv.
**Gruppe B — anvendbart med oversettelse.** Retningsgivende og relative mål framfor faste
kaskaderte mål; prognoser som en nøktern og upolitisk øvelse; helhetlig
prestasjonsvurdering. Disse krever tilpasning fordi tildelingsbrev og styringsdialog
inneholder faste, gitte krav — men kravet gjelder *hva* som skal oppnås, ikke at
virksomhetens interne målbilde må være like fastlåst.
**Gruppe C — sperret av den årlige bevilgningen.** Prinsippet om at ressurser gjøres
tilgjengelige etter behov framfor gjennom detaljerte årlige budsjettbevilgninger lar seg
ikke gjennomføre i statlig sektor. Bevilgningsreglementet
([FOR-2005-05-26-876](https://lovdata.no/dokument/STV/forskrift/2005-05-26-876)) § 3 første
ledd slår fast: *«Budsjettet vedtas for kalenderåret.»* Dette er ettårsprinsippet, forankret
i Grunnloven § 75.
To presiseringer hører med, og begge er viktige:
1. **Sperren rammer bevilgningsvedtaket, ikke virksomhetens interne styring.** At Stortinget
bevilger for ett år av gangen sier ingenting om hvordan virksomheten disponerer, planlegger
eller måler internt. Å bruke ettårsprinsippet som argument mot flerårig OKR-horisont er en
utvidelse regelverket ikke gir dekning for.
2. **Ettårsprinsippet er en hovedregel med lovfestede unntak.** § 5 «Bevilgningsvedtak» slår
fast at ubrukte utgiftsbevilgninger som hovedregel ikke kan overføres til etterfølgende
budsjettår — men gjør to unntak: ubrukt driftsbevilgning «kan overføres til neste
budsjettår med inntil fem prosent av bevilgningen», og bevilgningsvedtak med stikkordet
«kan overføres» gir «hjemmel til å overføre ubrukt bevilgning til de to følgende
budsjettårene». I tillegg krever forpliktelser ut over budsjettåret Stortingets særlige
samtykke (§ 6), og unntak i enkeltsaker krever uttrykkelig stortingsvedtak (§ 2). Sperren
er reell, men den er ikke et absolutt forbud mot flerårig horisont.
### Et ærlig forbehold om overførbarhet
De dokumenterte Beyond Budgeting-casene er private virksomheter — Equinor og Handelsbanken
er de mest omtalte. **Det er ikke funnet dokumenterte case fra norsk statlig eller kommunal
sektor.** Equinor-caset skal derfor ikke leses som bevis for at modellen lar seg overføre til
en virksomhet med tildelingsbrev og årlig bevilgning. Verdien av Beyond Budgeting her er
først og fremst diagnostisk: den forklarer *hvorfor* sandbagging oppstår, og den forklaringen
holder uavhengig av sektor.
### Kilder
- [Beyond Budgeting Institute — de tolv prinsippene (PDF)](https://bbrt.org/wp-content/uploads/bb_principles.pdf) (offisiell) — ordrett prinsippordlyd
- [Bogsnes: Beyond Budgeting — business agility in practice](https://www.valueglide.com/bjarte-bogsnes-beyond-budgeting-business-agility-in-practice) (sekundær) — target/forecast-separasjonen i Bogsnes' egne ord
- [Business Agility Institute — introduksjon til Beyond Budgeting](https://businessagility.institute/learn/an-introduction-to-beyond-budgeting-full-version/593) (sekundær) — Ambition to Action, Bogsnes' rolle
- [Bevilgningsreglementet (FOR-2005-05-26-876)](https://lovdata.no/dokument/STV/forskrift/2005-05-26-876) (offisiell/juridisk) — § 3 første ledd ettårsprinsippet; §§ 2, 5 og 6 unntakene
- [Finansdepartementet — Veileder til statlig budsjettarbeid, kap. 5.2.2.2](https://www.regjeringen.no/no/dokumenter/veileder-i-statlig-budsjettarbeid/id439275?ch=3) (offisiell) — ettårsprinsippet navngitt
## Fra strategi til OKR
Strategien sier hvor virksomheten skal over flere år. OKR-syklusen er fire måneder lang.
Oversettelsen mellom de to er der de fleste OKR-sett faller fra hverandre — enten ved at
syklusen mister strategisk retning, eller ved at strategien blir stående som festtale
uten målbart nedslag.
### Det ettårige i et flerårig perspektiv
Kravet er at virksomheter skal planlegge med både ettårig og flerårig perspektiv. DFØs
gjennomgang av tildelingsbrev og årsrapporter observerer at dette i praksis er svakt:
i dokumentene de så på omtales «i liten grad det ettårige i et flerårig perspektiv», med
kapittelet «Vurdering av fremtidsutsikter» i årsrapporten som unntaket.
Dette er en **observasjon fra et utvalg dokumenter, ikke en andel**. DFØ er selv eksplisitt
på at utvalget ikke er representativt og at funnene ikke kan generaliseres. Bruk den som en
kjent fallgruve å styre unna — aldri som et tall om hvor mange virksomheter som gjør dette.
**Praktisk konsekvens for OKR-arbeidet:** et strategisk Objective bør ha en eksplisitt
flerårig bane som tertialsyklusene henger på. Uten den blir hver syklus et selvstendig løp,
og strategien får aldri kumulativ effekt.
### Aktivitetsoppdrag beskriver ikke forventede effekter
Den samme gjennomgangen observerer at mange av oppdragene i tildelingsbrevene «i hovedsak
kan karakteriseres som aktivitetsoppdrag» — «tiltaksorienterte» og uten beskrivelse av
forventede effekter. Videre at det i noen tildelingsbrev «fremstår … uklart hva virksomheten
måles på», fordi forventningene er formulert både som mål og som løpende tekst.
Det er nøyaktig det antimønsteret OKR-metodikken advarer mot — aktivitet forkledd som mål —
observert i selve styringsdokumentet virksomheten skal svare på. Det gir en praktisk regel:
**Når tildelingsbrevet gir deg en aktivitet, er oversettelsesjobben din å finne effekten
aktiviteten skal produsere, og la den bli Key Result.** Aktiviteten blir da et tiltak under
KR-en, ikke KR-en selv. Er effekten ikke mulig å identifisere, er det i seg selv et funn å
ta med inn i styringsdialogen — ikke en grunn til å gjøre aktiviteten til KR.
Merk også at begrepsbruken varierer mellom tildelingsbrev: noen bruker «styringsparameter»,
andre «vurderingskriterier», «styringsinformasjon» eller «indikatorer». En OKR-prosess kan
derfor ikke anta ett fast begrep når den leser et tildelingsbrev.
### Steg for oversettelsen
1. **Finn den flerårige banen** strategien beskriver, og plasser inneværende år på den.
2. **Skill mål fra aktivitet** i tildelingsbrevet — uansett hvilket begrep det bruker.
3. **Formuler effekten** hvert aktivitetsoppdrag skal produsere; det er KR-kandidaten.
4. **Fordel over syklusene** slik at hver tertial har et meningsfullt delmål, ikke en
vilkårlig tredjedel av årsmålet.
5. **Noter det du ikke fikk oversatt.** Uoversettelige oppdrag hører hjemme i
styringsdialogen, ikke i et OKR-sett som later som det dekker dem.
### Kilder
- [DFØ-notat 2026:2 «Styring i staten»](https://www.dfo.no/sites/default/files/2026-05/dfo-notat-2026-2-styring-i-staten-dokumentgjennomgang-tilstand-utfordringer_0.pdf) (offisiell) — kap. 5.3.1 aktivitetsoppdrag og uklar målstruktur; kap. 5.3.3 ettårig i flerårig perspektiv; kap. 1.31.4 utvalgets begrensninger
> **Siteringsdisiplin:** DFØ-notat 2026:2 er en foranalyse basert på et ikke-representativt
> utvalg. Notatet sier selv at det «vil trolig gi et skjevt bilde av tilstanden». Funnene
> siteres som observasjoner, aldri som andeler eller tall om forvaltningen som helhet.
## Komplett årshjul for OKR ## Komplett årshjul for OKR
### Visuell oversikt ### Visuell oversikt
@ -548,11 +369,10 @@ Vektet score = (0.7×2 + 0.5×1 + 0.8×1) / (2+1+1) = 2.7 / 4 = 0.675
**Når bruke vekting:** **Når bruke vekting:**
- Når noen KR-er er tydelig viktigere enn andre - Når noen KR-er er tydelig viktigere enn andre
- Når ressursinnsatsen varierer vesentlig mellom KR-er - Når ressursinnsatsen varierer vesentlig mellom KR-er
- Når noen KR-er er "committed" og andre "aspirational"
**Anbefaling:** Start med uvektet gjennomsnitt. Introduser vekting kun hvis det gir tydelig verdi. **Anbefaling:** Start med uvektet gjennomsnitt. Introduser vekting kun hvis det gir tydelig verdi.
**Vekting løser ikke blandet type.** Vekting hører hjemme *innenfor* én type — committed mot committed, aspirational mot aspirational — der den håndterer at noen KR veier tyngre enn andre. Blandet type er et annet problem: de to måles mot ulike standarder, og ingen vekt gjør dem sammenlignbare i ett tall. Svaret er å rapportere dem hver for seg, ikke å vekte dem sammen (se under).
#### Aggreger ikke committed og aspirational sammen #### Aggreger ikke committed og aspirational sammen
Committed og aspirational OKR-scorer måles mot **ulike standarder** og bør ikke blandes til ett aggregat: committed forventes å nå **1.0** (enten grønn eller rød — et avvik krever forklaring), mens aspirational forventes å lande rundt **0.7** med høy varians og vurderes på **snittet på tvers av alle aspirational-OKR**, ikke på én enkelt. Å aggregere de to typene til ett tall blander to forskjellige målestokker og gjør resultatet tvetydig — et committed-snitt på 0.85 er en bekymring, mens et aspirational-snitt på 0.85 er svært bra. Rapportér derfor de to typene hver for seg. (Dette følger som logisk implikasjon av Doerr/Googles to-måls-struktur — ikke et ordrett Doerr-sitat; se committed-vs-aspirational-tabellen i `okr-cheatsheet.md`.) Committed og aspirational OKR-scorer måles mot **ulike standarder** og bør ikke blandes til ett aggregat: committed forventes å nå **1.0** (enten grønn eller rød — et avvik krever forklaring), mens aspirational forventes å lande rundt **0.7** med høy varians og vurderes på **snittet på tvers av alle aspirational-OKR**, ikke på én enkelt. Å aggregere de to typene til ett tall blander to forskjellige målestokker og gjør resultatet tvetydig — et committed-snitt på 0.85 er en bekymring, mens et aspirational-snitt på 0.85 er svært bra. Rapportér derfor de to typene hver for seg. (Dette følger som logisk implikasjon av Doerr/Googles to-måls-struktur — ikke et ordrett Doerr-sitat; se committed-vs-aspirational-tabellen i `okr-cheatsheet.md`.)
@ -688,8 +508,7 @@ For team-nivå rapportering:
┌─────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────┐
│ TEAM: [Teamnavn] Syklus: [X] 20XX │ │ TEAM: [Teamnavn] Syklus: [X] 20XX │
├─────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────┤
│ Committed: X av Y KR nådd │ │ Samlet score: 0.XX (XX%) │
│ Aspirational: 0.XX (snitt over N KR) │
│ On Track: X av Y OKR-er │ │ On Track: X av Y OKR-er │
│ At Risk: X av Y OKR-er │ │ At Risk: X av Y OKR-er │
│ Off Track: X av Y OKR-er │ │ Off Track: X av Y OKR-er │

View file

@ -1,316 +0,0 @@
# OKR og den kommunale styringslinjen
Kommuner og fylkeskommuner styres **ikke** som statlige etater. Den statlige
styringslinjen — tildelingsbrev fra departement, etatsstyringsmøter, årsrapport
til eier — er beskrevet i `okr-offentlig-governance.md`. Den linjen gjelder ikke
i en kommune, og å bruke den som mal der er en kategorifeil, ikke en forenkling.
Denne filen beskriver den kommunale linjen på egne premisser: hvor målene kommer
fra, hvem som vedtar dem, hvilken kadens loven pålegger, og hvordan et
4-måneders OKR-løp kobles til en 4-årig økonomiplan.
> **Omfang:** Filen dekker styringslinjen — hjemmel, organer, plandokumenter og
> kadens. OKR-metodikken selv (scoring, confidence, check-in-kadens, rubrikk)
> er kanonisert i `okr-framework.md` og gjentas bevisst ikke her.
## Innholdsfortegnelse
1. [Selvstyre: ingen tildelingsbrev](#selvstyre-ingen-tildelingsbrev)
2. [Økonomiplanen og kommuneplanens handlingsdel](#økonomiplanen-og-kommuneplanens-handlingsdel)
3. [Kommunedirektørens rolle](#kommunedirektørens-rolle)
4. [4-årig plan møter 4-måneders OKR](#4-årig-plan-møter-4-måneders-okr)
5. [Fylkeskommunen](#fylkeskommunen)
6. [KOSTRA som indikatorkilde](#kostra-som-indikatorkilde)
7. [Kilder](#kilder)
---
## Selvstyre: ingen tildelingsbrev
Kommunalt selvstyre er lovfestet i kommuneloven (LOV-2018-06-22-83) §§ 2-1 og
2-2. Hver kommune er et **eget rettssubjekt** og treffer sine egne vedtak innenfor
rammene av lov. Det finnes **ingen tildelingsbrev** til en kommune — ingen
departementsbrev som setter årets mål, og ingen eier å rapportere måloppnåelse
tilbake til på den måten en statlig etat gjør.
Konsekvensen for OKR-arbeid er direkte:
| Statlig etat | Kommune |
|---|---|
| Mål kommer **inn** via tildelingsbrev | Mål **vedtas lokalt** av kommunestyret |
| Departementet er eier og mottaker | Innbyggerne er oppdragsgiver; kommunestyret er øverste organ |
| `/okr:governance` og `/okr:gap` oversetter tildelingsbrev | Ingen tildelingsbrev å oversette — inngangen er økonomiplan og kommuneplan |
| Årsrapport til departementet | Årsberetning og årsregnskap til kommunestyret |
Statlige organer kan **anbefale, benchmarke, finansiere og sette lovpålagte
minstekrav** overfor kommuner — men de kan ikke styre kommunal måloppnåelse via
brev. Nasjonale metrikker kan derfor ikke pålegges top-down; de er lokalt eid.
Dette er også premisset bak metrikk-eierskapet omtalt i `metrics-library.md`.
**Praktisk følge for pluginen:** kjører du `/okr:governance` eller `/okr:gap` i
en kommune, finnes det ikke noe tildelingsbrev å analysere. Bruk økonomiplanen
med handlingsdel som kildedokument i stedet — det er den som bærer de vedtatte
målene.
---
## Økonomiplanen og kommuneplanens handlingsdel
Kommuneloven kapittel 14 regulerer økonomiforvaltningen. Fire bestemmelser
bærer styringslinjen:
| Hjemmel | Innhold |
|---|---|
| **§ 14-1** | Kommuner og fylkeskommuner skal ha en forsvarlig økonomiforvaltning |
| **§ 14-2** | Kommunestyret skal **selv** vedta økonomiplan, årsbudsjett, årsregnskap og årsberetning, og etablere rutiner for internkontroll |
| **§ 14-3** | Saksbehandlingen: formannskapet innstiller til kommunestyrets vedtak om økonomiplan og årsbudsjett (jf. § 5-6) |
| **§ 14-4** | Økonomiplanen skal omfatte **minst fire år** og revideres årlig |
Det avgjørende for OKR-arbeid er § 14-4s kobling til plansystemet:
**økonomiplanen kan inngå i eller utgjøre kommuneplanens handlingsdel** etter
plan- og bygningsloven § 11-1 fjerde ledd. Mange kommuner slår dem sammen til ett
dokument — ofte kalt *handlings- og økonomiplan* eller *handlingsprogram* — nettopp
for å slippe å vedlikeholde to parallelle målhierarkier.
Det betyr at kommunen normalt har **ett vedtatt dokument** som samtidig bærer
målene og pengene, rullerer årlig, og strekker seg minst fire år fram. Det er
dette dokumentet OKR skal koble seg til — ikke et tildelingsbrev.
```
Kommuneplanens samfunnsdel (langsiktig retning, 12+ år)
Handlings- og økonomiplan (minst 4 år, rulleres årlig, vedtas av
= handlingsdel + økonomiplan kommunestyret innen utgangen av året)
Årsbudsjett (år 1 i økonomiplanen, bindende)
OKR-syklus (4 måneder — operasjonaliserer
årets prioriteringer)
```
**Merk kadens-forskjellen mot staten:** statens årshjul er bygget rundt
statsbudsjettet og tildelingsbrevet. Kommunens årshjul er bygget rundt
økonomiplanrulleringen og kommunestyrets budsjettvedtak. Se `okr-arshjul.md` for
selve syklusarbeidet; datoene der er statlige og må erstattes med kommunens egne
vedtaksfrister.
---
## Kommunedirektørens rolle
Kommunedirektøren (tidligere rådmannen) er øverste leder for kommunens
administrasjon, jf. kommuneloven § 13-1. To sider av rollen er relevante for OKR:
- **Utredningsansvaret.** Kommunedirektøren skal påse at saker som legges fram for
folkevalgte organer er forsvarlig utredet. Et OKR-sett som skal informere et
politisk vedtak, må tåle den standarden — det er en reell kvalitetsterskel, ikke
en formalitet.
- **Iverksettingsansvaret.** Kommunedirektøren leder administrasjonen innenfor de
rammer, retningslinjer og pålegg kommunestyret gir. OKR lever på
*administrasjonssiden* av dette skillet: målene er politisk vedtatt, mens
hvordan de nås er administrasjonens ansvar.
Dette gir en klarere arbeidsdeling enn i staten, og den bør respekteres i
OKR-formuleringen:
| Nivå | Eier | OKR-rolle |
|---|---|---|
| Kommunestyret | Folkevalgt | Vedtar mål og rammer — leverer *retningen* |
| Kommunedirektøren | Administrativ toppleder | Eier oversettelsen til OKR og svarer for gjennomføringen |
| Kommunalsjef / virksomhetsleder | Administrativ linje | Eier virksomhetens OKR-sett |
| Team | Utførende | Eier team-OKR og gjennomfører |
Et vanlig feilgrep er å formulere OKR som binder det politiske nivået — mål
kommunestyret ikke har vedtatt. Da har administrasjonen tatt en beslutning som
ikke er dens å ta. Kanon for hva som kan være *committed*, står i
`okr-framework.md`; poenget her er at den kommunale grensen går ved
kommunestyrevedtaket.
---
## 4-årig plan møter 4-måneders OKR
Spennet mellom en fireårig økonomiplan og en firemåneders OKR-syklus er ikke et
problem som skal løses bort — det er to ulike tidshorisonter som gjør hver sin
jobb. Koblingen går gjennom årsbudsjettet:
| Horisont | Dokument | Spørsmål det svarer på |
|---|---|---|
| 12+ år | Kommuneplanens samfunnsdel | Hva slags kommune vil vi være? |
| Minst 4 år | Handlings- og økonomiplan | Hva prioriterer vi, og hva koster det? |
| 1 år | Årsbudsjett | Hva er bindende bevilget i år? |
| 4 måneder | OKR-syklus | Hva flytter vi faktisk nå? |
Tre praktiske regler:
1. **Én OKR-syklus skal spore til én prioritering i handlings- og økonomiplanen.**
Kan du ikke peke på hvilken vedtatt prioritering et Objective tjener, mangler
det politisk forankring.
2. **Økonomiplanen rulleres årlig — OKR-settet rulleres hver syklus.** Ved
rullering av økonomiplanen bør inneværende OKR-resultater være input, ikke en
ettertanke. Dette er kommunens motstykke til den statlige koblingen mellom
årsrapport og neste tildelingsbrev.
3. **Ikke lag OKR med fireårig horisont.** Fireårige ambisjoner hører hjemme i
økonomiplanen. OKR-settet skal være det som kan flyttes innenfor syklusen.
---
## Fylkeskommunen
Kommuneloven gjelder likt for kommuner og fylkeskommuner. Fylkestinget svarer til
kommunestyret, fylkesutvalget til formannskapet, og fylkeskommunedirektøren til
kommunedirektøren. Alt over gjelder tilsvarende.
Én praktisk forskjell er verdt å merke: fylkeskommunen har flere oppgaver der den
er utøvende for nasjonal politikk (samferdsel, videregående opplæring,
tannhelse), og møter derfor oftere statlige forventninger, tilskuddsordninger og
rapporteringskrav i praksis — uten at det endrer at målene fortsatt vedtas
lokalt av fylkestinget.
---
## KOSTRA som indikatorkilde
Handlingsdelen kobler mål til tiltak til **indikator**. Det er på indikatornivået
kommunale OKR-sett henter tallene sine — og i praksis kommer de fra KOSTRA. Denne
seksjonen handler derfor ikke om KOSTRA som statistikk, men om KOSTRA som
*målekilde for Key Results*: hvilke typer tall som finnes, når de kommer, og hva
de ikke tåler å bli brukt til.
### De fire nøkkeltallstypene som KR-taksonomi
SSB setter regnskaps- og tjenestedata sammen til nøkkeltall i fire kategorier:
| Type | SSBs egen formulering | Som KR |
|---|---|---|
| **Prioritering** | «viser hvordan kommunens frie inntekter er fordelt til ulike formål» | Ressursfordeling mellom tjenesteområder |
| **Dekningsgrad** | «viser tjenestetilbudet i forhold til ulike målgrupper for tilbudet» | Andel av målgruppen som faktisk får tjenesten |
| **Produktivitet / enhetskostnader** | «viser kostnader/bruk av ressurser i forhold til tjenesteproduksjonen» | Hva en enhet av tjenesten koster |
| **Utdypende tjenesteindikatorer** | «viser nøkkeltall som supplerer indikatorer presentert under prioritering, dekningsgrader og produktivitet/enhetskostnader, men som ikke kan plasseres under disse overskriftene» | Restkategorien — vurder hver for seg |
SSBs utdypende definisjoner:
- **Prioriteringsindikatorene** «skal si noe om hvor mye av egne penger kommunen ”velger”
å bruke til de enkelte tjenesteområdene. En tjeneste kan sies å være høyt prioritert når
en kommune bruker en relativt stor andel av sine ressurser på en bestemt tjeneste.»
- **Dekningsgrad:** «En dekningsgrad måler andelen av målgruppen som er mottakere av en
tjeneste. Dersom målgruppen er heterogen, kan den med fordel splittes opp slike at vi får
separate dekningsgrader for ulike deler av målgruppen.»
- **Produktivitetsindikatorene** «eller enhetskostnadsindikatorene skal si noe om hva det
koster å produsere en enhet av tjenesten. Produktiviteten kan sies å være høy dersom
ressursbruken er lav i forhold til produksjonen.»
> **Kvalitet er ikke en egen KOSTRA-indikatortype.** Dette er en utbredt misforståelse, og
> den har praktisk betydning. KOSTRA måler prioritering, dekning, produktivitet og
> utdypende tjenesteindikatorer — ikke kvalitet som egen kategori. Konsekvensen for
> KR-design er direkte: **et produktivitets-KR uten et kvalitetsmål ved siden av kan
> forbedres ved å svekke tjenesten.** SSB sier det selv, se guardrailen under.
**Ikke navngi enkeltindikatorer i et OKR-sett som skal leve over flere år.** SSB endrer
skjema og kontoplan årlig; en KR som peker på en konkret indikator-ID råtner. Pek på
indikator*typen* og hent den gjeldende indikatoren ved syklusstart.
### Publiseringskadensen — og regelen som følger av den
| Publisering | Dato | Status |
|---|---|---|
| Foreløpige tall | 15. mars | Ureviderte |
| Reviderte tall | 15. juni | Etter kommunenes retting og SSBs kvalitetskontroll |
SSBs egen formulering: «SSB publiserer foreløpige tall for kommunene 15. mars, og reviderte
tall 15. juni.» Og om hva som skjer imellom: «Før publiseringen 15. juni har kommunene hatt
mulighet til å rette feil og mangler i sine data, samt at SSB har gjennomført
kvalitetskontroller og editering av datamaterialet. **De foreløpige nøkkeltallene per 15.
mars kan være beheftet med feil.**»
**Den harde regelen: KOSTRA er årlig lagging og kan aldri være en tertialvis
KR-måling.** Tallene finnes rett og slett ikke i tertialoppløsning. En KR formulert som
«forbedre KOSTRA-indikator X i løpet av T2» er umulig å måle når T2 avsluttes.
Det er verdt å merke seg en ekstra felle for firemånederssykluser: **avslutning av T1 faller
i april — mellom foreløpige og reviderte tall.** Det er det verst tenkelige tidspunktet å låse
et resultat på: tallet du rapporterer da er per SSBs egen advarsel ureviderte tall som «kan
være beheftet med feil», og det kan endre seg i juni.
**Bruk KOSTRA slik i stedet:**
- Som **årlig lagging-KR** på et strategisk Objective som løper over flere sykluser.
- Som **baseline** ved syklusstart — hva var utgangspunktet.
- Aldri som den løpende framdriftsmålingen innenfor en syklus. Til det trenger du en
leading-indikator kommunen selv måler.
### Sammenlignbarhet: SSBs eget forbehold som guardrail
SSB advarer selv, gjennomgående og i alle tre hovedkategoriene, mot å lese forskjeller
mellom kommuner som forskjeller i innsats.
Om produktivitet:
> «Når produksjonen blir målt ved antall mottakere blir det imidlertid ikke tatt hensyn til
> variasjoner i kvaliteten på tjenestene som brukerne mottar. Det blir heller ikke tatt
> hensyn til variasjoner i brukernes behov eller pleietyngde. Det kan derfor være flere
> tolkninger av hvorfor en kommune har høye utgifter per mottaker.»
SSB lister selv tre konkurrerende tolkninger av det samme tallet: lav produktivitet, **høy
kvalitet**, eller høye enhetskostnader som skyldes smådriftsulemper, lange reiseavstander
eller lønnsnivå.
Om prioritering:
> «Slike elementer bidrar til at forskjeller i utgifter per person i målgruppen ikke
> utelukkende kan tolkes som et resultat av prioriteringer på lokalt nivå.»
Om dekningsgrad:
> «Forskjeller i samlet dekningsgrad mellom kommuner kan derfor skyldes at målgruppen er
> forskjellig sammensatt, i tillegg til at kommunene har forskjellige inntekter og
> utgiftsbehov og gjør forskjellige prioriteringer.»
> **Guardrail — rangerings-KR er metodisk uholdbare.** En Key Result av formen «bli topp 10
> i KOSTRA på dekningsgrad X» eller «ligge over gjennomsnittet i KOSTRA-gruppen» bryter med
> kildens egen advarsel. Forskjellen mellom kommunene kan være demografi, geografi eller
> inntektsnivå — altså noe kommunen ikke rår over. Å forplikte seg til en plassering er
> derfor å forplikte seg til noe andres tall like mye som til sin egen innsats.
>
> **Formuler i stedet KR-en mot egen utvikling:** «øke dekningsgrad X fra 62 % til 68 %».
> Da måler du det du faktisk påvirker.
### Praktisk bruk i et kommunalt OKR-sett
1. **Hent baseline ved syklusstart** fra siste *reviderte* KOSTRA-årgang, og noter årgangen.
2. **Velg indikatortype bevisst** — prioritering, dekning eller produktivitet svarer på tre
helt ulike spørsmål. Vær eksplisitt på hvilket du stiller.
3. **Par produktivitets-KR med et kvalitetsmål** kommunen måler selv. KOSTRA gir deg ikke
kvalitetsmålet.
4. **Legg lagging-KR-en på årsnivå**, med leading-KR-er kommunen måler selv innenfor
syklusene.
5. **Bruk egen utvikling, ikke rangering**, som målform.
Rammen dette henger i — økonomiplanens fireårige horisont og kommunestyrets vedtakskompetanse
— er dekket over i «Økonomiplanen og kommuneplanens handlingsdel»; den gjentas ikke her.
### Kilder
- [SSB — Dokumentasjon av KOSTRA](https://www.ssb.no/offentlig-sektor/kostra/statistikk/kostra-kommune-stat-rapportering/om-kostra/dokumentasjon-av-kostra) (offisiell) — de fire nøkkeltallstypene med definisjoner; sammenlignbarhetsforbeholdene
- [SSB — KOSTRA årlig, foreløpige tall (om statistikken)](https://www.ssb.no/offentlig-sektor/statistikker/kostrahoved/aar-forelopige) (offisiell) — publiseringskadens og mars-forbeholdet
- [SSBs brev til kommunene om KOSTRA-rapportering (PDF)](https://www.statsforvalteren.no/contentassets/0648d572aba04018a789312f19846efd/bev-til-kommunane-fra-ssb-om-rapportering-for-2022.pdf) (offisiell) — årshjulet med frister
---
## Kilder
- [Kommuneloven (LOV-2018-06-22-83), kapittel 14 — Økonomiforvaltning](https://lovdata.no/lov/2018-06-22-83/kap14) — §§ 14-1, 14-2, 14-3, 14-4 (offisiell/juridisk)
- [Kommuneloven §§ 2-1 og 2-2 — kommunalt selvstyre](https://lovdata.no/lov/2018-06-22-83) (offisiell/juridisk)
- [Plan- og bygningsloven § 11-1 — kommuneplan og handlingsdel](https://lovdata.no/dokument/NL/lov/2008-06-27-71/KAPITTEL_2-4-2) (offisiell/juridisk)
- [Regjeringen — veileder om statlig styring av kommuner og fylkeskommuner](https://www.regjeringen.no/no/dokumenter/veileder-om-statlig-styring-av-kommuner-og-fylkeskommuner/id2791598) (offisiell)
- [Veileder til budsjett- og regnskapsforskriften (KDD, januar 2024)](https://www.regjeringen.no/contentassets/7bf9b58579724f19a28acb81fb51d8af/januar-2024-veileder-til-budsjett-og-regnskapsforskriften.pdf) (offisiell)
### Beslektede referanser
- `okr-offentlig-governance.md` — den statlige styringslinjen (tildelingsbrev, etatsstyring, årsrapport)
- `okr-arshjul.md` — syklusarbeid og budsjettsynkronisering (statlige datoer)
- `metrics-library.md` — metrikker og eierskap, inkludert kommunalt metrikk-eierskap
- `okr-framework.md` — kanonisk OKR-metodikk (scoring, kadens, confidence)

View file

@ -7,19 +7,13 @@ Norsk offentlig sektor har unike styringsmekanismer som OKR må forholde seg til
## Innholdsfortegnelse ## Innholdsfortegnelse
1. [Tildelingsbrev og OKR](#tildelingsbrev-og-okr) 1. [Tildelingsbrev og OKR](#tildelingsbrev-og-okr)
2. [Gevinstrealisering](#gevinstrealisering) 2. [Politisk styring](#politisk-styring)
3. [Politisk styring](#politisk-styring) 3. [Hierarkisk alignment](#hierarkisk-alignment)
4. [Hierarkisk alignment](#hierarkisk-alignment) 4. [Revisjons- og kontrollperspektiv](#revisjons--og-kontrollperspektiv)
5. [Revisjons- og kontrollperspektiv](#revisjons--og-kontrollperspektiv) 5. [Spesielle hensyn](#spesielle-hensyn)
6. [Arkiv, GDPR og sletting](#arkiv-gdpr-og-sletting) 6. [Konkrete eksempler](#konkrete-eksempler)
7. [Spesielle hensyn](#spesielle-hensyn) 7. [Tillitsreformen og styringsspenningen](#tillitsreformen-og-styringsspenningen)
8. [Konkrete eksempler](#konkrete-eksempler) 8. [Ressurser](#ressurser)
9. [Tillitsreformen og styringsspenningen](#tillitsreformen-og-styringsspenningen)
10. [Ressurser](#ressurser)
> **Gjelder statlig sektor.** Denne guiden beskriver den statlige styringslinjen:
> tildelingsbrev, etatsstyring og årsrapport til departementet. Kommuner og
> fylkeskommuner styres etter en annen linje — se `okr-kommunal-styring.md`.
--- ---
@ -87,156 +81,20 @@ I offentlig sektor må du være tydelig på hva som er "must-have" vs "stretch":
**Anbefalt balanse i offentlig sektor**: 60% committed, 40% aspirational **Anbefalt balanse i offentlig sektor**: 60% committed, 40% aspirational
### Styringssløyfas ut-side: fra OKR tilbake til eier ### Rapportering: OKR som supplement til årsrapport
Tildelingsbrevet er **inn-siden** av styringssløyfa — den er dekket over. Ut-siden OKR erstatter ikke årsrapportering, men kan styrke den:
er det som går tilbake til departementet: rapportering underveis, årsrapporten, og
dialogen i etatsstyringsmøtene. En OKR-praksis som bare er bygget for inn-siden
produserer mål ingen svarer for.
#### Styringsdialogen ```
Årsrapport OKR-bidrag
─────────────────────────────────────────────────────
Resultatregnskap → Score på Committed OKR
Måloppnåelse → Progress på alle OKR
Risikoer/utfordringer → Læring fra OKR med lav score
Fremtidsplaner → Input til neste års OKR
```
*Styringsdialog* er samlebetegnelsen på styringsdokumenter, rapportering og møter **Tips**: Bruk OKR-score og lærdommer aktivt i årsrapporten som dokumentasjon på systematisk målstyring.
av styringskarakter mellom departement og underliggende virksomhet. Det sentrale
møtepunktet er **etatsstyringsmøtet** — møtene har ulike navn i ulike
departementer (halvårsmøte, årsrapportmøte, etatsstyringsmøte), men funksjonen er
den samme. Styringsdialogen skal dokumenteres.
| Ut-siden | Når | OKR-bidrag |
|---|---|---|
| Underveisrapportering (tertial/halvår der tildelingsbrevet krever det) | Etter tildelingsbrevets krav | Score og confidence per KR; avvik forklart med læring, ikke bortforklaring |
| **Etatsstyringsmøte** | Antall og form fastsettes i departementets hovedinstruks | OKR-settet som agenda-struktur: hva flyttet seg, hva stoppet opp, hva ber vi om |
| **Årsrapport** | Til departementet innen **15. mars** året etter | Del III bærer måloppnåelsen — se under |
| Innspill til neste tildelingsbrev | Høst | Lave scores og lærdommer som dokumentert grunnlag for neste års prioriteringer |
**Etatsstyringsmøtet er der OKR gir mest igjen.** Et møte som ellers lett blir en
gjennomgang av aktiviteter, får med OKR en fast struktur: hvert Objective har en
score, en confidence og en eier. Det flytter samtalen fra *hva har dere gjort* til
*hva har dere oppnådd, og hva trenger dere fra oss*.
#### Årsrapportens struktur — hvor OKR hører hjemme
Statlige virksomheters årsrapport har **seks faste deler** med gitte navn og
rekkefølge:
| Del | Navn | OKR-bidrag |
|---|---|---|
| I | Leders beretning | Helhetsvurdering: nådde vi det vi satte oss fore? |
| II | Introduksjon til virksomheten og hovedtall | — (strukturell) |
| **III** | **Årets aktiviteter og resultater** | **Hovedplassen for OKR.** Score per Objective, måloppnåelse mot tildelingsbrevets krav, og læring fra KR som ikke ble nådd |
| IV | Styring og kontroll i virksomheten | OKR-prosessen selv som dokumentasjon på systematisk målstyring og internkontroll |
| V | Vurdering av framtidsutsikter | Input til neste syklus; hva lave scores sier om kapasitet og risiko |
| VI | Årsregnskap | — (finansiell) |
**Del III er ankeret.** Det er her måloppnåelse mot tildelingsbrevet skal
dokumenteres, og der et OKR-sett med scoringshistorikk er direkte anvendbart. Merk
at forhold som påvirker virksomheten uten å slå ut i regnskapsoppstillingene også
hører hjemme i del III.
**Del IV er den undervurderte.** Riksrevisjonen og departementet ser her etter om
virksomheten har systematisk styring — ikke bare om målene ble nådd. En dokumentert
OKR-prosess med faste check-ins, scoring og retrospektiver er nettopp slik
dokumentasjon. Se `okr-arshjul.md` for syklusarbeidet og
[Revisjons- og kontrollperspektiv](#revisjons--og-kontrollperspektiv) under.
#### Å rapportere aspirational OKR oppover
Den vanligste feilen på ut-siden er å rapportere en aspirational OKR på 0.7 som en
*svikt*. Det er den ikke — 0.7 er forventet måloppnåelse for aspirational (se
committed/aspirational-skillet over). Men mottakeren i departementet leser ikke
nødvendigvis 0.7 som suksess med mindre det står eksplisitt.
Tre regler:
1. **Merk alltid typen i rapporteringen.** «Aspirational, score 0.7 (= forventet)»
er én linje som forhindrer en hel misforståelse.
2. **Committed OKR rapporteres binært mot kravet.** Et lovkrav er nådd eller ikke;
score 0.9 på et lovpålagt krav er et avvik som skal forklares, ikke en god score.
3. **Ikke konverter scores til prosent måloppnåelse i årsrapporten uten å forklare
skalaen.** En leser som tror 0.7 betyr «70 % av målet» trekker feil konklusjon om
både ambisjonsnivå og resultat.
#### Hvorfor regelen er strukturell, ikke bare god skikk
De tre reglene kan leses som hensynsfull formidling. Det er de ikke. Den kausale
begrunnelsen ligger i target/forecast-separasjonen — se `okr-framework.md`,
«Beyond Budgeting og target/forecast-separasjonen», som er kanonisk for mekanismen.
Kort: **får aspirational-scoren konsekvenser i rapporteringen, er målet og prognosen
re-bundlet til ett tall.** Da har den som setter målet neste syklus en egeninteresse i
å sette det lavt — og sandbagging følger av systemet, ikke av manglende disiplin hos
den enkelte. En avviksliste som tar med aspirational-KR er derfor ikke først og fremst
urimelig mot årets team; den svekker neste års målsetting.
Det er også svaret til en revisor som spør hvorfor et aspirational-KR med lav score
ikke står oppført som avvik: fordi det aldri var et krav, og fordi å behandle det som
ett ville gjort neste års mål mindre ambisiøse. Merk at dette *ikke* er et argument
for å slippe å forklare lav score — aspirational-resultater skal analyseres og læres
av (se del III over). Det er et argument for at analysen hører hjemme et annet sted
enn i avvikslisten.
---
## Gevinstrealisering
Et OKR-sett som stopper ved leveransen måler output, ikke effekt. Gevinstrealisering
er den etablerte offentlige metodikken for å hente ut effekten *etter* at et
prosjekt er levert — og den kobler tett til hvorfor Key Results skal formuleres som
outcome.
### Hvorfor dette hører til OKR
De fleste offentlige prosjekter har forventninger om gevinster som først kan tas ut
etter at prosjektet er avsluttet. Gevinstrealisering er metoden for å planlegge og
organisere både linjen og prosjektgruppen slik at gevinstene faktisk hentes ut.
Det er nøyaktig samme skille som OKR gjør mellom aktivitet og effekt:
| Gevinstrealisering | OKR |
|---|---|
| Gevinst (effekt etter leveranse) | Key Result (outcome) |
| Leveranse / tiltak | Aktivitet — bevisst **ikke** et Key Result |
| Gevinstoversikt og gevinstkart | Sammenhengen Objective → KR |
| Gevinstrealiseringsplan | OKR-sett over flere sykluser |
| **Gevinstansvarlig** | **KR-eier** |
### Gevinstansvarlig er en linjerolle
Det viktigste enkeltpunktet: **gevinstansvarlig skal være en linjeleder fra den
delen av organisasjonen som faktisk skal realisere gevinsten** — ikke
prosjektlederen. Prosjektet leverer; linjen henter ut effekten.
Dette er samme prinsipp som at et Key Result skal eies av noen som kan påvirke
det. En KR-eier som ikke rår over utfallet, er den vanligste årsaken til at
committed-merkingen går galt (se committed/aspirational over).
### Praktisk kobling
**Begrepsskiftet 2026:** DFØs veileder «Gevinstrealisering» (2014) er **erstattet**
av «Veileder om nyttestyring av statlige tiltak» (24. april 2026). DFØ byttet
samtidig ut «gevinstrealisering» med **nyttestyring** og «gevinster» med
**nyttevirkninger**. Metodikken, resultatkjeden og koblingen til Key Results er
dekket i `gevinstrealisering-okr.md` — bruk den fila, ikke 2014-terminologien.
Nyttestyring skal brukes i prosjekter omfattet av statens prosjektmodell, men DFØ
presiserer selv at den også er relevant under terskelverdiene — og at veilederen
«ikke er ment som et nytt krav eller en fast metode som skal brukes i alle
sammenhenger». Fremstill den derfor ikke som en plikt for alle virksomheter.
Arbeidet er samordnet med **Prosjektveiviseren** (Digdir), den anbefalte modellen
for digitaliseringsprosjekter i offentlig sektor.
Når et OKR-sett skal dekke et prosjekt med gevinstrealiseringsplan:
1. **La gevinstene bli KR, ikke leveransene.** Har prosjektet et gevinstkart, er
det allerede en outcome-formulering — bruk den.
2. **Sett KR-eier = gevinstansvarlig.** Er de forskjellige personer, har du to
parallelle ansvarslinjer for samme effekt.
3. **Regn med at gevinsten kommer etter syklusen prosjektet leveres i.** Leveransen
og gevinsten hører ofte i to ulike OKR-sykluser. Det er normalt — ikke press
gevinsten inn i leveranse-syklusen for å få et pent tall.
> **Målenivå:** Sektoren rapporterer selv at rundt 15 % av gevinstrealiseringen
> feiler (andelen er synkende, og bekreftet av Riksrevisjonen). Se
> `metrics-library.md` for datapunktet og kilden.
--- ---
@ -380,48 +238,6 @@ Riksrevisjonen vurderer om virksomheten:
- Regelmessig tracking (dokumenterer progresjon) - Regelmessig tracking (dokumenterer progresjon)
- Eksplisitt kobling til strategi (viser alignment) - Eksplisitt kobling til strategi (viser alignment)
### Daterte knagger: hva Riksrevisjonen faktisk har påpekt
Disse fire er **kildebelagte knagger**, ikke retorikk. De hører til
**Dokument 1**-serien (den årlige regnskaps- og etterlevelsesrevisjonen), ikke
Dokument 3 (forvaltningsrevisjon) — et strukturelt skille, ikke en detalj: et
søk i Dokument 3 finner dem aldri.
| # | Funn | Revidert virksomhet, år | Kilde |
|---|------|------------------------|-------|
| A | Årsrapportene gir informasjon om aktiviteter og tjenesteleveranser, og i liten grad effekter — begrunnet med måleproblemer, blant annet manglende statistikk | Tverrsektorielt (barnefattigdom), 2019 | DFØ-notat 2026:2 s. 53 (fn. 253) |
| B | Framstillingen i årsrapporten synliggjorde i mindre grad virksomhetens analyser av måloppnåelsen, og framstod heller som et øyeblikksbilde av aktiviteter og resultater | NIBIO, 2020 | DFØ-notat 2026:2 s. 4748 (fn. 216) |
| C | Departementet hadde ikke utarbeidet et mål- og resultatstyringssystem som ga tilstrekkelig informasjon om hvor effektivt virksomheten utnyttet ressursene sine | Havforskningsinstituttet, 2019 | DFØ-notat 2026:2 s. 48 (fn. 222) |
| D | Styringsinformasjonen ga ikke tilstrekkelig grunnlag for prioriteringer; risikovurderingene ga ikke informasjonsgrunnlag for å prioritere mellom oppgaver | Luftfartstilsynet + Havforskningsinstituttet, 2019 | DFØ-notat 2026:2 s. 47 (fn. 210211) |
#### ⚠️ Dateringsdisiplinen er bindende — den er ikke en anbefaling
**Sakene er AVSLUTTET.** Riksrevisjonen avsluttet dem etter forbedringer i mål-
og resultatstyringen (DFØ-notat 2026:2 fn. 152 og fn. 217).
Derfor: siter **aldri** disse funnene i presens. «Riksrevisjonen finner at
statlige årsrapporter mangler effektrapportering» er presens om et perfektum —
det er feil, og det er lett å begå fordi funnene er skarpe. Riktig form er
«Riksrevisjonen påpekte i 2019 … og avsluttet saken etter forbedringer».
To ting til, som gjelder hver gang funnene brukes:
1. **Sitatene er DFØs gjengivelse av Riksrevisjonen**, ikke Riksrevisjonens egen
ordlyd. Funn B er ikke verifisert mot primærkilde. Stabile URL-er til de
enkelte Dokument 1-kapitlene finnes ikke, så siteringsformen er
«Riksrevisjonen (år), del av Dokument 1 (20192020), gjengitt i DFØ-notat
2026:2 s. NN» — aldri en direktelenke.
2. **Bruk dem som problembeskrivelse, aldri som anklage** mot en konkret
virksomhet brukeren jobber i eller med.
#### Den positive hjemmelen
Knaggene over beskriver svakheter. Hjemmelen for at tildelingsbrevet *skal*
inneholde det `/okr:governance` og `/okr:gap` leser ut av det, er en annen kilde:
departementet skal sette styringsparametere for å kunne vurdere måloppnåelse og
resultater, og sette krav til innholdet i årsrapporten — *Bestemmelser om
økonomistyring i staten* punkt 1.5 (DFØ-notat 2026:2 s. 49, fn. 228).
### Dokumentasjonskrav ### Dokumentasjonskrav
For å tilfredsstille revisjonsperspektivet, dokumenter: For å tilfredsstille revisjonsperspektivet, dokumenter:
@ -467,87 +283,6 @@ Riksrevisjonen har påpekt risiko for utilsiktede konsekvenser ved strenge resul
--- ---
## Arkiv, GDPR og sletting
**Posisjonen i én setning: pluginen sletter ikke, den sperrer.**
Dette er ikke en avveining mellom to regelsett som trekker hver sin vei. Både
arkivlova og GDPR ender samme sted — behold, men skjerm.
### Regimeskiftet 01.01.2026
Hele arkivregelverket ble erstattet 1. januar 2026:
| Regelverk | Referanse |
|---|---|
| Lov om dokumentasjon og arkiv (arkivlova) | LOV-2025-06-20-96 |
| Arkivforskrifta | FOR-2025-12-17-2647 |
| Bevaringsforskrifta | FOR-2025-12-19-2729 |
Arkivverket heter fra samme dato **Nasjonalarkivet**. Eldre veiledning som viser
til arkivlova av 1992 er ikke lenger dekkende.
### Arkivlova § 13 — sletteplikt fra annen lov er ikke kassasjonshjemmel
Kassasjon er legaldefinert i arkivlova § 2 g som å «gjere til inkjes
dokumentasjon slik at han ikkje lenger finst». Et organ kan bare kassere
dokumentasjon som enten ikke skal tas vare på etter forskrift fra
Nasjonalarkivet, eller der Nasjonalarkivet har gitt løyve.
Det avgjørende for GDPR-spørsmålet står i **§ 13 tredje punktum**, ordrett:
> «Føresegner i andre lover om plikt til å slette opplysningar gir berre grunnlag
> for kassasjon når det er **klart fastsett** eller føresett at opplysningane
> ikkje skal finnast i arkiva for ettertida.»
Er vilkåret ikke oppfylt, skal sletteplikten i stedet oppfylles
**«på andre måtar enn ved kassasjon»** — altså ved sperring, skjerming eller
tilgangsbegrensning.
Konsekvensen er skarp: **GDPR art. 17 er i seg selv ikke kassasjonshjemmel.**
### GDPR art. 17 nr. 3 — unntakene peker samme vei
Sletteretten i art. 17 gjelder uansett ikke fullt ut her:
- **Art. 17 nr. 3 bokstav b** unntar behandling som er nødvendig for å oppfylle
en rettslig forpliktelse, eller «for å utføre en oppgave i allmennhetens
interesse eller utøve offentlig myndighet».
- **Art. 17 nr. 3 bokstav d** unntar behandling «for arkivformål i allmennhetens
interesse […] i samsvar med artikkel 89 nr. 1».
Merk asymmetrien: GDPR *tillater* å beholde, mens arkivlova *påbyr* det. Det er
ikke to regler i konflikt — det er én regel med et unntak som peker samme vei.
### Hva dette betyr for OKR-treet
Pluginens driftsområde ligger **innenfor** bevaringspåbudet, ikke utenfor det:
bevaringsforskrifta § 7 navngir tildelingsbrev, rapportar, etatsstyringsmøter og
evalueringa; § 30 navngir handlingsprogram, tertialrapportering og årsmelding.
Sikringsparagrafen § 3 lukker resten — det som ikke er nevnt, skal bevares inntil
Nasjonalarkivet har fastsett reglar.
Praktisk:
1. **Ingen automatisk kassasjon.** Verdien av et dokument avgjøres ofte av
hendelser *etter* at en ryddekommando ville kjørt — presedens er det klassiske
eksempelet. `/okr:arkivklar` rapporterer derfor grunnlag, aldri handling.
2. **Sletteforespørsel fra en registrert** håndteres av virksomheten etter
§ 13-vurderingen over, ikke ved å fjerne filer fra `.claude/okr/`.
3. **Vurderingen er betinget av noe verktøyet ikke ser.** Arkivforskrifta § 1
bokstav c gjør den avhengig av om dokumentasjonen «allereie blir sikra og
forvalta som arkiv i eit anna informasjonssystem». Den tilstanden er ukjent
for pluginen og må avklares av virksomheten.
4. **Sanksjonsapparatet er skjerpet** — arkivlova kap. 4 gir tilsyn, pålegg om
retting med tvangsmulkt og straff. Feil her er ikke en formalitet.
> **Organinterne dokumenter:** offentleglova § 14 lar et organ unnta dokument
> utarbeidet for egen saksforberedelse. Det er et **«kan»-unntak fra innsyn**, ikke
> et vern og ikke et unntak fra bevaringsplikt. At noe i `.claude/okr/` kan falle
> inn under § 14, sier derfor ingenting om hvorvidt det skal bevares.
---
## Spesielle hensyn ## Spesielle hensyn
### Sektorovergripende mål ### Sektorovergripende mål
@ -742,16 +477,10 @@ En nøktern realitetssjekk: tillitsbasert styring er «mye hørt, men lite sett
- `okr-framework.md` - Vår metodikk og årshjul - `okr-framework.md` - Vår metodikk og årshjul
- `okr-arshjul.md` - Visuelt årshjul med budsjettprosess - `okr-arshjul.md` - Visuelt årshjul med budsjettprosess
- `meeting-guides.md` - Agendaer for alignment-workshops - `meeting-guides.md` - Agendaer for alignment-workshops
- `okr-kommunal-styring.md` - Den kommunale styringslinjen (selvstyre, økonomiplan, kommunedirektør)
### Eksterne kilder ### Eksterne kilder
- [DFØ: Mål- og resultatstyring i staten](https://dfo.no/fagomrader/styring-i-staten/mal-og-resultatstyring) - [DFØ: Mål- og resultatstyring i staten](https://dfo.no/fagomrader/styring-i-staten/mal-og-resultatstyring)
- [DFØ: Veileder i etatsstyring](https://dfo.no/fagomrader/etats-og-virksomhetsstyring/etatsstyring/veileder-i-etatsstyring) - [DFØ: Veileder i etatsstyring](https://dfo.no/fagomrader/etats-og-virksomhetsstyring/etatsstyring/veileder-i-etatsstyring)
- [DFØ: Hovedinstruks del D — styringsdialog](https://www.dfo.no/fagomrader/styring-i-staten/etatsstyring/hovedinstruks/hovedinstruks-del-d-styringsdialog) (etatsstyringsmøtets plass i styringsdialogen)
- [DFØ: Veiledningsnotat — årsrapport for statlige virksomheter](https://www.dfo.no/sites/default/files/2023-01/Veiledningsnotat%20til%20%C3%A5rsrapport%20for%20statlige%20virksomheter_oppdatert260123.pdf) (årsrapportens seks deler)
- [Bestemmelser om økonomistyring i staten](https://lovdata.no/dokument/INS/forskrift/2003-12-12-1939) (årsrapport til departementet innen 15. mars)
- [DFØ: Veileder om nyttestyring av statlige tiltak](https://www.dfo.no/node/94330/print) (24.04.2026 — **erstatter** gevinstrealiseringsveilederen fra 2014; se `gevinstrealisering-okr.md`)
- [Digdir: Prosjektveiviseren — gevinstrealisering i de ulike fasene](https://prosjektveiviseren.digdir.no/god-praksis/gevinstrealisering-i-de-ulike-fasene/117)
- [Regjeringen: Tildelingsbrev](https://www.regjeringen.no/no/dokument/tildelingsbrev-og-arsrapportar/id2357472/) - [Regjeringen: Tildelingsbrev](https://www.regjeringen.no/no/dokument/tildelingsbrev-og-arsrapportar/id2357472/)
- [Riksrevisjonen: Undersøkelse av mål- og resultatstyring](https://www.stortinget.no/no/Saker-og-publikasjoner/Publikasjoner/Dokumenter/) - [Riksrevisjonen: Undersøkelse av mål- og resultatstyring](https://www.stortinget.no/no/Saker-og-publikasjoner/Publikasjoner/Dokumenter/)
- [NAV: Mål- og resultatstyring - kan det bidra til å få flere i arbeid?](https://arbeidogvelferd.nav.no/) - [NAV: Mål- og resultatstyring - kan det bidra til å få flere i arbeid?](https://arbeidogvelferd.nav.no/)

View file

@ -98,17 +98,6 @@ Måler om teamet **kan påvirke vesentlig** — om det rår over de viktigste dr
4. **Anker 4** — Teamet rår over de fleste driverne, med en mindre ekstern avhengighet. 4. **Anker 4** — Teamet rår over de fleste driverne, med en mindre ekstern avhengighet.
5. **Anker 5 (sterkest)** — Teamet rår over de viktigste driverne og kan påvirke utfallet vesentlig. 5. **Anker 5 (sterkest)** — Teamet rår over de viktigste driverne og kan påvirke utfallet vesentlig.
### Leading/lagging-balanse
Måler om Key Result-settet kobler en **leading**-indikator (måles underveis, teamet påvirker den i perioden) til et **lagging**-utfall (bekrefter effekten, men foreligger først i etterkant). Riksrevisjonen har som kjernefunn at statlige årsrapporter gir informasjon om aktiviteter og tjenesteleveranser «og i liten grad effekter» — rene lagging-mål er vanskelige å rapportere tertialvis, og svaret er å måle begge deler bevisst, ikke å droppe effekten. KOSTRA er et typisk lagging-mål: nøkkeltallene gjelder foregående år og publiseres foreløpig 15. mars og endelig 15. juni, så de kan aldri følges opp tertialvis.
1. **Anker 1 (svakest)** — Kun aktivitetsmål; ingenting i settet sier om aktiviteten virket.
2. **Anker 2** — Kun leading-indikatorer; effekten er påstått, men ingen måler den.
3. **Anker 3** — Både leading og lagging finnes, men koblingen mellom dem er ikke uttalt.
4. **Anker 4** — Leading og lagging er koblet, men tidsforsinkelsen er ikke erkjent (lagging-utfallet behandles som om det forelå i perioden).
5. **Anker 5 (sterkest)** — Eksplisitt par av leading-indikator og lagging-utfall, med uttalt tidsforsinkelse og navngitt tidspunkt for når lagging-tallet faktisk foreligger.
> **Merk:** et lagging-mål med årlig publisering hører hjemme som *lagging*-halvdelen av paret, aldri som tertialvis KR. Se `okr-kommunal-styring.md` for KOSTRA-kadensen.
> **Avveining Outcome ↔ Uavhengighet (samfunnseffekt-KR):** Et sterkt Outcome-KR (Anker 5: reell samfunnseffekt for bruker/samfunn) er ofte et *delt* utfall teamet ikke rår fullt over — det scorer da lavt på Uavhengighet. Dette er en reell spenning, ikke en formuleringsfeil, og den skal ikke «løses» ved å svekke outcome-ambisjonen. **Done-condition:** par samfunnseffekt-KR-et (aspirational/delt) med ett eller flere **committed leading-tiltak** teamet faktisk kontrollerer. Scor da Uavhengighet på *tiltakene*, og aksepter lav Uavhengighet på selve outcome-KR-et som prisen for å måle noe som faktisk betyr noe. Et KR skal aldri nedgraderes fra outcome til ren aktivitet kun for å heve Uavhengighet-scoren. > **Avveining Outcome ↔ Uavhengighet (samfunnseffekt-KR):** Et sterkt Outcome-KR (Anker 5: reell samfunnseffekt for bruker/samfunn) er ofte et *delt* utfall teamet ikke rår fullt over — det scorer da lavt på Uavhengighet. Dette er en reell spenning, ikke en formuleringsfeil, og den skal ikke «løses» ved å svekke outcome-ambisjonen. **Done-condition:** par samfunnseffekt-KR-et (aspirational/delt) med ett eller flere **committed leading-tiltak** teamet faktisk kontrollerer. Scor da Uavhengighet på *tiltakene*, og aksepter lav Uavhengighet på selve outcome-KR-et som prisen for å måle noe som faktisk betyr noe. Et KR skal aldri nedgraderes fra outcome til ren aktivitet kun for å heve Uavhengighet-scoren.
--- ---

View file

@ -10,7 +10,7 @@ description: >-
prompt. Triggers on: "våre mål", "overordnede mål", "mål dette tertialet", prompt. Triggers on: "våre mål", "overordnede mål", "mål dette tertialet",
"tertialmål", "tildelingsbrev", "hva sier OKR-ene våre", "forrige syklus", "tertialmål", "tildelingsbrev", "hva sier OKR-ene våre", "forrige syklus",
"strategi", "styringssignaler". "strategi", "styringssignaler".
version: "1.10.0" version: "1.8.2"
--- ---
# OKR Second-Brain Search # OKR Second-Brain Search
@ -75,33 +75,14 @@ in their frontmatter. These files passed a deterministic conformance gate but
**no human review** — they are the least-trusted tier of the bundle (see **no human review** — they are the least-trusted tier of the bundle (see
ranking and the security envelope below). ranking and the security envelope below).
Each level has an **`index.md`** formatted as a heading plus one bullet per Each level has an **`index.md`** with **no frontmatter**, formatted as a heading
concept file. Sub-level indexes carry **no frontmatter**: plus one bullet per concept file:
``` ```
# Heading # Heading
* [title](relative.md) - description * [title](relative.md) - description
``` ```
A **root** index additionally carries the two bundle markers, deliberately on
different surfaces: `okf_version` (the upstream OKF version) in frontmatter, and
`okf_layout` (this plugin's own layout revision) in the body:
```
---
okf_version: 0.1
---
# Heading
okf_layout: kb-layout-2026-06
* [title](relative.md) - description
```
These markers are metadata about the bundle, not retrievable content — skip them
when ranking.
The `index.md` is the curated table of contents for its level — use it to The `index.md` is the curated table of contents for its level — use it to
navigate and to break ranking ties (below). navigate and to break ranking ties (below).

View file

@ -1,392 +0,0 @@
// arkivklar.test.mjs
// D7 steg 17: path-confinement som LESEGRENSE + klassifisering mot
// bevaringsforskrifta (FOR-2025-12-19-2729) §§ 7 og 30.
//
// Path-confinement er kravets kjerne, og de tre angrepsformene fanges av ULIKE
// lag. Derfor er de tre SEPARATE cases, ikke en felles «avvis ugyldig sti»:
// (17a) ../-traversering -> leksikalsk lag
// (17b) symlenke ut -> realpath-lag (path.resolve loeser IKKE symlenker)
// (17c) soesken-katalog -> path.sep i prefikssjekken (/uploads vs /uploads-other)
// En felles case ville bestaatt paa ett bein og skjult at de to andre var borte.
// Mutasjons-verifisert per bein -- se sesjonsloggen (D7/S49).
//
// (17d) skiller ENOENT fra confinement-brudd: realpathSync kaster paa en sti som
// ikke finnes, og de to feilene krever ulik handling hos kalleren. Maskeres den
// ene som den andre, blir «fila mangler» rapportert som «angrepsforsoek».
//
// Zero npm deps. Moenster: tests/innboks-write.test.mjs (mkdtemp + symlinkSync).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, rmSync, symlinkSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { lesemaal, klassifiser, traverserBundle, rapport } from '../lib/arkivklar.mjs';
const FIXTURE = fileURLToPath(new URL('./fixtures/okf-realistic', import.meta.url));
const SCRIPT = fileURLToPath(new URL('../scripts/arkivklar.mjs', import.meta.url));
const NAA = '2026-08-02T10:00:00.000Z';
function withTmp(fn) {
const tmp = mkdtempSync(join(tmpdir(), 'arkivklar-'));
try {
fn(tmp);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
}
// ==================== (17a-c) path-confinement som lesegrense ====================
test('(17a) lesemaal avviser ../-traversering ut av bundle-rota', () => {
withTmp((tmp) => {
const rot = join(tmp, 'bundle');
mkdirSync(rot, { recursive: true });
writeFileSync(join(tmp, 'hemmelig.md'), '# utenfor\n');
// Assertionen pinner det LEKSIKALSKE laget («maal-sti ... avvist»), ikke
// bare «avvist». Med en loesere regex bestod denne casen paa
// realpath-lagets melding -- som ogsaa inneholder «utenfor bundle-rot» --
// og det leksikalske laget kunne fjernes uten at noen case roednet
// (mutasjons-verifisert: den gjorde nettopp det).
assert.throws(
() => lesemaal(rot, '../hemmelig.md'),
/maal-sti utenfor bundle-rot avvist/,
'../-escape skal avvises av det leksikalske laget',
);
});
});
test('(17b) lesemaal avviser symlenke som peker UT av bundle-rota', () => {
withTmp((tmp) => {
const rot = join(tmp, 'bundle');
const utenfor = join(tmp, 'utenfor');
mkdirSync(rot, { recursive: true });
mkdirSync(utenfor, { recursive: true });
writeFileSync(join(utenfor, 'hemmelig.md'), '# utenfor\n');
// Symlenken ligger INNE i bundlen og er leksikalsk uskyldig: den
// resolverer under rota. Kun realpath avsloerer at den peker ut.
symlinkSync(utenfor, join(rot, 'lenke'));
assert.throws(
() => lesemaal(rot, 'lenke/hemmelig.md'),
/symlink-escape/,
'symlenke ut av bundlen skal avvises av realpath-laget',
);
});
});
test('(17c) lesemaal avviser soesken-katalog med samme prefiks', () => {
withTmp((tmp) => {
// Klassisk prefiks-felle: basen /uploads slipper /uploads-other gjennom
// dersom prefikssjekken mangler path.sep.
const rot = join(tmp, 'uploads');
const soesken = join(tmp, 'uploads-other');
mkdirSync(rot, { recursive: true });
mkdirSync(soesken, { recursive: true });
writeFileSync(join(soesken, 'secret.txt'), 'hemmelig\n');
// Som (17a): pinner det leksikalske laget, ellers er det path.sep-sjekken
// denne casen finnes for som blir utestet.
assert.throws(
() => lesemaal(rot, '../uploads-other/secret.txt'),
/maal-sti utenfor bundle-rot avvist/,
'soesken-katalog med delt prefiks skal avvises av path.sep-sjekken',
);
});
});
test('(17d) lesemaal skiller ENOENT fra confinement-brudd', () => {
withTmp((tmp) => {
const rot = join(tmp, 'bundle');
mkdirSync(rot, { recursive: true });
// En sti som er LOVLIG innenfor rota, men ikke finnes, skal gi ENOENT --
// ikke en confinement-feil. Kalleren maa kunne skille de to.
assert.throws(
() => lesemaal(rot, 'finnes-ikke.md'),
(err) => err.code === 'ENOENT',
'manglende fil innenfor rota skal gi ENOENT, ikke confinement-brudd',
);
});
});
// ==================== klassifisering mot §§ 7 og 30 ====================
test('klassifiser: navngitte kategorier i § 7 (stat) og § 30 (kommune)', () => {
// Laast arkitektur-beslutning 8: styringslinjene er PARALLELLE. En type kan
// treffe en kategori i begge, og da skal BEGGE rapporteres -- modulen velger
// ikke linje for virksomheten.
const tildeling = klassifiser('Tildelingsbrev');
assert.equal(tildeling.stat, 'tildelingsbrev');
assert.equal(tildeling.kommune, null);
assert.equal(tildeling.sikring, false);
const status = klassifiser('Status');
assert.equal(status.stat, 'rapportar');
assert.equal(status.kommune, 'tertialrapportering');
const virksomhetsplan = klassifiser('Virksomhetsplan');
assert.equal(virksomhetsplan.stat, null);
assert.equal(virksomhetsplan.kommune, 'handlingsprogram');
const retro = klassifiser('Retrospektiv');
assert.equal(retro.stat, 'evalueringa');
assert.equal(retro.kommune, 'aarsmelding');
});
test('klassifiser: ikke-navngitt type faller til sikringsparagrafen § 3', () => {
// Bevaringsforskrifta § 3: det som ikke er nevnt, skal bevares inntil
// Nasjonalarkivet har fastsett reglar. Ukjent er derfor ALDRI «kan slettes».
for (const type of ['OKR', 'Overordnede OKR', 'Notat', 'Dokument', 'Heltukjent']) {
const k = klassifiser(type);
assert.equal(k.sikring, true, `${type} skal falle til § 3`);
assert.equal(k.stat, null, `${type} skal ikke paastaas navngitt i § 7`);
assert.equal(k.kommune, null, `${type} skal ikke paastaas navngitt i § 30`);
}
});
// ==================== traversering av en ekte bundle ====================
test('traverserBundle: klassifiserer fixturens filer og er deterministisk', () => {
const a = traverserBundle(FIXTURE);
const b = traverserBundle(FIXTURE);
assert.deepEqual(a, b, 'to kjoeringer skal gi identisk resultat (sortert traversering)');
assert.ok(a.filer.length >= 8, `forventet >= 8 konseptfiler, fant ${a.filer.length}`);
// index.md er navigasjon, ikke konsept -- den skal ikke klassifiseres.
assert.equal(
a.filer.filter((f) => f.rel.endsWith('index.md')).length,
0,
'index.md skal holdes utenfor klassifiseringen',
);
const tildeling = a.filer.find((f) => f.rel === 'strategisk-kontekst/tildelingsbrev-2026.md');
assert.ok(tildeling, 'fixturens tildelingsbrev skal finnes');
assert.equal(tildeling.type, 'Tildelingsbrev');
assert.equal(tildeling.stat, 'tildelingsbrev');
const retro = a.filer.find((f) => f.rel === 'historikk/retrospektiv-T3-2025.md');
assert.ok(retro, 'fixturens retrospektiv skal finnes');
assert.equal(retro.kommune, 'aarsmelding');
});
test('traverserBundle: symlenket underkatalog som peker ut avvises', () => {
withTmp((tmp) => {
const rot = join(tmp, 'bundle');
const utenfor = join(tmp, 'utenfor');
mkdirSync(join(rot, 'dokumenter'), { recursive: true });
mkdirSync(utenfor, { recursive: true });
writeFileSync(join(utenfor, 'hemmelig.md'), '---\ntype: Notat\n---\n\n# ute\n');
writeFileSync(join(rot, 'dokumenter', 'ok.md'), '---\ntype: Notat\n---\n\n# inne\n');
symlinkSync(utenfor, join(rot, 'lekkasje'));
assert.throws(
() => traverserBundle(rot),
/symlink-escape/,
'traverseringen skal stoppe paa en symlenket katalog som peker ut',
);
});
});
// ==================== (18) rapporten tar ikke stilling ====================
// FORBUDTE ORD skannes i den GENERERTE RAPPORTEN, aldri i kildefila.
// scripts/arkivklar.mjs og lib/arkivklar.mjs baerer selv ordene «slett»/«kassasjon»
// i forklarende prosa («ingen slettemodell finnes») -- en vakt som skannet kilden
// ville felt sin egen forbydende tekst og maattet mykes opp til den ble verdiloes.
// Det er S48-defektklassen; skann utfallet, ikke intensjonen.
const IMPERATIV = /\b(slett|slette|slettes|kasser|kassere|kasseres|fjern|fjernes)\b/i;
test('(18a) rapporten inneholder ingen kassasjons- eller slette-imperativ', () => {
const linjer = rapport(traverserBundle(FIXTURE), { naa: NAA }).split('\n');
const treff = linjer.filter((l) => IMPERATIV.test(l)).map((l) => l.trim());
assert.deepEqual(
treff,
[],
`rapporten skal aldri instruere kassasjon/sletting:\n${treff.join('\n')}`,
);
});
test('(18b) parser-sanity: IMPERATIV-vakten fanger faktisk et slikt imperativ', () => {
// Uten denne ville (18a) bestaatt trivielt paa en regex som ikke matcher noe.
assert.ok(IMPERATIV.test('Disse filene kan slettes etter 2027.'), 'vakten skal treffe et ekte imperativ');
assert.ok(!IMPERATIV.test('Vurderingen hoerer til virksomhetens dokumentasjonsplan.'), 'legitim prosa skal gaa klar');
});
test('(18c) rapporten baerer paragrafhenvisning og vurderingsformulering', () => {
const ut = rapport(traverserBundle(FIXTURE), { naa: NAA });
for (const forventet of ['§ 7', '§ 30', '§ 3', 'arkivlova § 8', 'arkivforskrifta § 12 b', 'vurder']) {
assert.ok(ut.toLowerCase().includes(forventet.toLowerCase()), `rapporten mangler «${forventet}»`);
}
});
test('(18d) rapporten navngir den ukjente tilstanden etter arkivforskrifta § 1 bokstav c', () => {
// Vurderingen er betinget av om dokumentasjonen ALLEREDE forvaltes som arkiv i
// et annet system -- en tilstand verktoeyet ikke kan se. Rapporten maa si det,
// ikke stilltiende anta det ene eller det andre.
const ut = rapport(traverserBundle(FIXTURE), { naa: NAA });
assert.ok(/§ 1 bokstav c/.test(ut), 'rapporten skal navngi arkivforskrifta § 1 bokstav c');
assert.ok(/ukjent/i.test(ut), 'rapporten skal navngi tilstanden som ukjent');
});
test('(18e) rapporten er deterministisk (to kjoeringer byte-identisk)', () => {
const a = rapport(traverserBundle(FIXTURE), { naa: NAA });
const b = rapport(traverserBundle(FIXTURE), { naa: NAA });
assert.equal(a, b, 'samme input + samme klokke skal gi byte-identisk rapport');
});
// ==================== (18f-h) CLI-form og exit-koder ====================
function kjor(args, env = {}) {
try {
const stdout = execFileSync(process.execPath, [SCRIPT, ...args], {
encoding: 'utf8',
env: { ...process.env, OKR_NOW: NAA, ...env },
});
return { code: 0, stdout };
} catch (e) {
return { code: e.status, stdout: e.stdout ?? '', stderr: e.stderr ?? '' };
}
}
test('(18f) CLI paa fixturen gir exit 0 og skriver rapporten til stdout', () => {
const r = kjor([FIXTURE]);
assert.equal(r.code, 0, `forventet exit 0, fikk ${r.code}`);
assert.ok(/§ 7/.test(r.stdout), 'stdout skal baere rapporten');
assert.deepEqual(
r.stdout.split('\n').filter((l) => IMPERATIV.test(l)),
[],
'CLI-utskriften skal heller ikke baere slette-imperativ',
);
});
test('(18g) CLI uten argument gir exit 2 (bruksfeil)', () => {
assert.equal(kjor([]).code, 2, 'manglende bundle-rot skal gi exit 2');
});
test('(18h) CLI paa ikke-eksisterende rot gir exit 2', () => {
withTmp((tmp) => {
assert.equal(kjor([join(tmp, 'finnes-ikke')]).code, 2, 'ukjent rot skal gi exit 2');
});
});
test('(18i) CLI skriver rapport atomisk til oppgitt fil, uten aa roere bundlen', () => {
withTmp((tmp) => {
const ut = join(tmp, 'arkivklar-rapport.md');
const r = kjor([FIXTURE, ut]);
assert.equal(r.code, 0, `forventet exit 0, fikk ${r.code}`);
const skrevet = readFileSync(ut, 'utf8');
assert.ok(/§ 7/.test(skrevet), 'fila skal baere rapporten');
assert.deepEqual(
readdirSync(tmp).filter((n) => n.endsWith('.tmp')),
[],
'ingen .tmp skal lekke (atomisk temp + rename)',
);
});
});
// ==================== (19) kommandoflaten og registrering ====================
const ROOT = fileURLToPath(new URL('..', import.meta.url));
const les = (rel) => readFileSync(join(ROOT, rel), 'utf8');
// Presedens: commands/sporing.md:4 -- en kommando uten skriveverktoey KAN ikke
// skrive, uansett hva prosaen lover. Tool-lista er den haandhevbare grensen;
// prosaen er det ikke. /okr:arkivklar er raadgivende og skal aldri kunne endre
// treet den vurderer.
const SKRIVEVERKTOEY = ['Write', 'Edit', 'NotebookEdit', 'MultiEdit'];
function allowedTools(rel) {
const m = les(rel).match(/^allowed-tools:\s*(.+)$/m);
assert.ok(m, `${rel}: mangler allowed-tools i frontmatter`);
return m[1].split(',').map((t) => t.trim());
}
test('(19a) arkivklar-kommandoen deklarerer ingen skriveverktoey', () => {
const tools = allowedTools('commands/arkivklar.md');
const forbudte = tools.filter((t) => SKRIVEVERKTOEY.includes(t));
assert.deepEqual(
forbudte,
[],
`/okr:arkivklar er lesende og skal ikke ha skriveverktoey; fant: ${forbudte.join(', ')}`,
);
assert.ok(tools.includes('Read'), 'kommandoen trenger Read');
assert.ok(tools.includes('Bash'), 'kommandoen trenger Bash for aa kjoere skriptet');
});
test('(19b) parser-sanity: vakten feiler faktisk om et skriveverktoey legges til', () => {
// Uten denne beviser (19a) bare at lista ikke inneholder noe -- den kunne
// bestaatt paa en tom/uparset liste. Her mates vakten en liste som SKAL felle.
const mutant = ['Read', 'Bash', 'Glob', 'Write'];
assert.deepEqual(
mutant.filter((t) => SKRIVEVERKTOEY.includes(t)),
['Write'],
'vakten skal fange Write om det legges til',
);
});
test('(19c) arkivklar er registrert paa alle kommandoflater', () => {
// D6-beslutning 7: en levert form skal synes paa ALLE flater samtidig.
// Sjekklista er commands/<navn>.md, commands/help.md, CLAUDE.md, README.md --
// help.md og README.md var nettopp de to som ble oversett i steg 12.
const mangler = [];
for (const f of ['CLAUDE.md', 'README.md', 'commands/help.md']) {
if (!/okr:arkivklar/.test(les(f))) mangler.push(f);
}
assert.deepEqual(mangler, [], `/okr:arkivklar mangler paa flate(r):\n${mangler.join('\n')}`);
});
test('(19d) GDPR-posisjonen staar i governance og ender paa «behold, men skjerm»', () => {
const gov = les('skills/okr-offentlig-sektor/references/okr-offentlig-governance.md');
for (const forventet of [
'klart fastsett', // arkivlova § 13 tredje punktum, ordrett
'andre måtar enn ved kassasjon',
'17 nr. 3', // GDPR art. 17 nr. 3 b og d
'LOV-2025-06-20-96',
'FOR-2025-12-19-2729',
'Nasjonalarkivet',
]) {
assert.ok(gov.includes(forventet), `governance mangler «${forventet}»`);
}
assert.ok(/sperrer|skjerm/i.test(gov), 'posisjonen skal formuleres som «sletter ikke, sperrer»');
// Datatilsynets vedtakskompetanse er KUN DELVIS verifisert (403 paa
// Nasjonalarkivets side). Den skal derfor ikke siteres med paragrafnummer.
assert.ok(
!/Datatilsynet[^.]{0,80}§\s*\d/.test(gov),
'Datatilsynets vedtakskompetanse skal ikke siteres med paragrafnummer (kun delvis verifisert)',
);
});
test('(18j) CLI nekter aa skrive rapporten inn i bundlen den vurderer', () => {
// Skriptets header lover at rapporten ALDRI havner i bundlen. Prosa som lover
// noe koden ikke haandhever er nettopp det (19a) avviser for allowed-tools --
// samme standard maa gjelde her. Rapporten i treet ville dessuten blitt
// klassifisert av neste kjoering, altsaa selv-forurensning.
//
// Kjoerer mot en TMP-bundle, ikke den delte fixturen: i RED-fasen (foer vakten
// fantes) skrev nettopp denne casen `rapport.md` inn i tests/fixtures/ og
// roednet okf-check-suiten. Det er STATE-gotchaen «CLI-VERIFY SKRIVER I
// FIXTUREN» -- en test som kan skitne til delt state, gjoer det til slutt.
withTmp((tmp) => {
const rot = join(tmp, 'bundle');
mkdirSync(join(rot, 'dokumenter'), { recursive: true });
writeFileSync(join(rot, 'dokumenter', 'notat.md'), '---\ntype: Notat\n---\n\n# Notat\n');
const r = kjor([rot, join(rot, 'rapport.md')]);
assert.equal(r.code, 1, `forventet exit 1, fikk ${r.code}`);
assert.match(r.stderr ?? '', /inne i bundlen/i, 'feilmeldingen skal si hvorfor');
assert.ok(!existsSync(join(rot, 'rapport.md')), 'ingenting skal vaere skrevet i bundlen');
// Utenfor bundlen skal det fortsatt gaa fint.
const ok = kjor([rot, join(tmp, 'rapport.md')]);
assert.equal(ok.code, 0, `rapport utenfor bundlen skal gaa fint, fikk ${ok.code}`);
assert.ok(existsSync(join(tmp, 'rapport.md')), 'rapporten skal vaere skrevet utenfor bundlen');
});
});

View file

@ -108,9 +108,9 @@ function divergentEnumeration(line) {
// ==================== parser-sanity (jf. B1) ==================== // ==================== parser-sanity (jf. B1) ====================
test('parser-sanity: headingsOf finner 11 rubrikk-dimensjoner', () => { test('parser-sanity: headingsOf finner 10 rubrikk-dimensjoner', () => {
const dims = headingsOf(readDoc(`${REF}/okr-quality-rubrics.md`), /^### /); const dims = headingsOf(readDoc(`${REF}/okr-quality-rubrics.md`), /^### /);
assert.equal(dims.length, 11, `forventet 11 dims, fant ${dims.length}`); assert.equal(dims.length, 10, `forventet 10 dims, fant ${dims.length}`);
}); });
test('parser-sanity: mdTables finner confidence-tabellen i okr-framework.md', () => { test('parser-sanity: mdTables finner confidence-tabellen i okr-framework.md', () => {
@ -163,113 +163,6 @@ test('(a2) F-c: ingen parallelle confidence-etikettsett utenfor kanon', () => {
); );
}); });
// --- (k) Separasjonsregelen i malene (D6 steg 14) ---
//
// okr-framework.md:557: committed og aspirational boer ikke blandes til ett
// aggregat -- de to typene rapporteres hver for seg. Generatoren
// (lib/syklus-rapport.mjs) haandhever det; malene gjorde det ikke. En regel med to
// lesninger er nettopp defektklassen S38-S40 kostet tre oekter.
//
// Skanne-settet er references/ + commands/ + agents/ -- ALLE flater som emitterer
// en rapportform. references/ sto foerst utenfor for aa skaane en ANNEN diskusjon
// (per-Objective-aggregering i kalkulatoren), men eksklusjonen var KATALOG-BRED og
// tok team-aggregatene med seg: tre maler i references/ lovet et samlet tall
// (meeting-guides.md, okr-calculator.md, okr-framework.md). Samme lekse som R4 --
// en vakt smalere enn sitt eget case-navn gjoer drift usynlig.
//
// Per-Objective-aggregeringen eksklusjonen skulle skaane gaar fortsatt klar, og
// gjoer det paa MOENSTERET i stedet for paa katalogen: "Beregn samlet score for et
// Objective med flere KR" (okr-calculator.md:122) har ingen verdi paa linjen, og
// "Objective total"-radene sier ikke "samlet score".
//
// To former, fordi bruddet har to skrivemaater:
// ETIKETT -- "Samlet score" sammen med en VERDI paa linjen (tall, X-plassholder
// eller utfyllingsfelt). Verdien og etiketten bindes bevisst IKKE
// til hverandre posisjonelt: "Samlet score (committed +
// aspirational): 0.53" har noeyaktig det aggregatet vakten finnes
// for, og et moenster som krevde verdien rett etter etiketten slapp
// den gjennom (mutasjons-verifisert -- den gjorde det).
// INSTRUKS -- "samlet (vektet gjennomsnitt)", som ber leseren lage aggregatet.
//
// De andre aksene gaar klar: rubrikkens 0-10-skala har ingen verdi paa linjen,
// og det arkiverte datafeltet `samlet_score` er snake_case, ikke "samlet score".
// Mermaids akselabel rendrer det samme datafeltet i Title Case og unntas
// eksplisitt -- arkiv-kontrakten er en annen sak enn maleneS rapportform.
const AGGREGAT_ORD = /samlet\s+score/i;
const VERDISLOT = /\d[.,]\d|\d\.XX|_{3,}|\bX{1,2}\b/;
const MERMAID_AKSE = /^\s*[xy]-axis\b/;
const AGGREGAT_INSTRUKS = /\bsamlet\b[^\n]{0,25}vektet\s+gjennomsnitt/i;
test('(k) separasjonsregelen: ingen mal emitterer et udifferensiert samlet-aggregat', () => {
const maler = [...mdFiles(REF), ...mdFiles('commands'), ...mdFiles('agents')];
assert.ok(maler.length >= 30, `parser-sanity: forventet >= 30 maler, fant ${maler.length}`);
const violations = [];
for (const f of maler) {
readDoc(f).split('\n').forEach((line, i) => {
// Differensiert = linjen navngir NOEYAKTIG EN type. En linje som nevner
// begge ("Samlet score (committed + aspirational)") er nettopp det
// udifferensierte aggregatet vakten finnes for -- den skal ikke slippe
// gjennom paa aa ha uttalt ordene.
if (/committed/i.test(line) !== /aspirational/i.test(line)) return;
if (MERMAID_AKSE.test(line)) return;
const etikett = AGGREGAT_ORD.test(line) && VERDISLOT.test(line);
if (etikett || AGGREGAT_INSTRUKS.test(line)) {
violations.push(`${f}:${i + 1}: ${line.trim()}`);
}
});
}
assert.deepEqual(
violations,
[],
`udifferensiert aggregat i mal (okr-framework.md:557 -- rapporter typene hver for seg):\n${violations.join('\n')}`,
);
});
// --- (k2) kanon sanksjonerer ikke det (k) forbyr (patch-lane #4) ---
//
// okr-framework.md listet "Naar noen KR-er er committed og andre aspirational" som
// en GRUNN TIL AA VEKTE -- seks linjer over seksjonen som forbyr aa aggregere de to
// typene i det hele tatt. Malene lovet et samlet tall fordi kanon sa at vekting var
// svaret paa blandet type. (k) alene ville rettet symptomet og latt kilden staa.
//
// Vakten er STRUKTURELL, ikke semantisk: den leser kulepunktlisten under
// "Naar bruke vekting:" -- en liste over TILFELLER DER MAN SKAL VEKTE -- og feller
// et punkt som navngir begge typene. Et punkt DER er en sanksjon i kraft av sin
// posisjon, uansett ordlyd. Den korrigerende prosaen ("vekting gjelder innenfor EN
// type; committed og aspirational rapporteres hver for seg") navngir ogsaa begge,
// men staar UTENFOR listen og gaar derfor klar. Et negasjons-moenster som skulle
// skille sanksjon fra forbud semantisk ville vaert nettopp den falske vakten S58
// ble felt av: den ville maalt ordlyd, ikke paastand.
const VEKTING_LISTE = /^\*\*N\u00e5r bruke vekting/;
test('(k2) vektings-listen oppgir ikke blandet committed/aspirational som vektings-grunn', () => {
const linjer = readDoc(join(REF, 'okr-framework.md')).split('\n');
const start = linjer.findIndex((l) => VEKTING_LISTE.test(l.trim()));
assert.ok(start >= 0, 'parser-sanity: fant ikke "Naar bruke vekting:"-listen');
// Listen er sammenhengende; foerste ikke-kulepunkt (her: blanklinjen foran
// **Anbefaling:**) avslutter den. Skiller listen seg fra overskriften senere,
// rydder sanity-asserten under opp i det hoeylytt i stedet for aa skanne tomt.
const punkter = [];
for (let i = start + 1; i < linjer.length; i += 1) {
const tekst = linjer[i].trim();
if (!tekst.startsWith('-')) break;
punkter.push({ nr: i + 1, tekst });
}
assert.ok(punkter.length >= 2, `parser-sanity: forventet >= 2 kulepunkter, fant ${punkter.length}`);
const violations = punkter
.filter((p) => /committed/i.test(p.tekst) && /aspirational/i.test(p.tekst))
.map((p) => `okr-framework.md:${p.nr}: ${p.tekst}`);
assert.deepEqual(
violations,
[],
`vekting oppgitt som svar paa blandet type -- motsier separasjonsregelen i samme fil:\n${violations.join('\n')}`,
);
});
// --- F-d kadens x publikum (D1 / review.md 607313e3) --- // --- F-d kadens x publikum (D1 / review.md 607313e3) ---
// Den forrige vakten bandt kadens-adjektivet DIREKTE til check-in (\s+), og kunne derfor // Den forrige vakten bandt kadens-adjektivet DIREKTE til check-in (\s+), og kunne derfor
// ikke feile paa den KANONISKE tabellen: etter "Maanedlig" kommer "**" og en cellevegg, // ikke feile paa den KANONISKE tabellen: etter "Maanedlig" kommer "**" og en cellevegg,
@ -438,10 +331,10 @@ function dimTokens(body) {
return tokens; return tokens;
} }
// (g) F-e agent 11 dims. RED til Step 10. Utled dimensjonsnavnene, ikke hardkod. // (g) F-e agent 10 dims. RED til Step 10. Utled dimensjonsnavnene, ikke hardkod.
test('(g) F-e: kvalitetssjekker-agent daekker alle 11 rubrikk-dimensjoner', () => { test('(g) F-e: kvalitetssjekker-agent daekker alle 10 rubrikk-dimensjoner', () => {
const dims = headingsOf(readDoc(`${REF}/okr-quality-rubrics.md`), /^### /); const dims = headingsOf(readDoc(`${REF}/okr-quality-rubrics.md`), /^### /);
assert.equal(dims.length, 11, `parser-sanity: forventet 11 dims, fant ${dims.length}`); assert.equal(dims.length, 10, `parser-sanity: forventet 10 dims, fant ${dims.length}`);
const tokens = dimTokens(readDoc('agents/kvalitetssjekker-agent.md')); const tokens = dimTokens(readDoc('agents/kvalitetssjekker-agent.md'));
const missing = dims.filter((d) => !tokens.has(d)); const missing = dims.filter((d) => !tokens.has(d));
assert.deepEqual(missing, [], `kvalitetssjekker-agent mangler rubrikk-dims: ${missing.join(', ')}`); assert.deepEqual(missing, [], `kvalitetssjekker-agent mangler rubrikk-dims: ${missing.join(', ')}`);
@ -456,131 +349,6 @@ test('(g2) F-e: dim-vakten skiller Outcome fra Outcome-fokus', () => {
assert.ok(!tokens.has('Outcome'), '"Outcome" skal IKKE dekkes av "Outcome-fokus"'); assert.ok(!tokens.has('Outcome'), '"Outcome" skal IKKE dekkes av "Outcome-fokus"');
}); });
// --- (g3) dimensjonstall drift-laas paa tvers av de TRE flatene (D7 steg 15) ---
//
// Rubrikkfila er sannhetskilden; kvalitet.md og kvalitetssjekker-agent.md OMTALER
// antallet i prosa. Da steg 15 la til en 11. dimensjon var det nettopp de omtalene
// som kunne bli staaende paa "10" -- samme defektklasse som (d) F-g loeser for
// antipattern-antallet: ALDRI hardkod tallet i vakten, utled det.
//
// Tre teller-akser, fordi prosaen baerer alle tre: totalen ("alle 11 dimensjonene"),
// Objective-gruppa og Key Result-gruppa. En vakt paa bare totalen ville sluppet
// gjennom "de 5 Key Result-dimensjonene" etter at gruppa ble seks.
function rubricDims() {
const body = readDoc(`${REF}/okr-quality-rubrics.md`);
const groups = { Objective: [], 'Key Result': [] };
let cur = null;
for (const line of body.split('\n')) {
const g = line.match(/^## (Objective|Key Result)-dimensjoner\s*$/);
if (g) { cur = g[1]; continue; }
if (/^## /.test(line)) { cur = null; continue; }
const d = line.match(/^### (.+?)\s*$/);
if (d && cur) groups[cur].push(d[1]);
}
return groups;
}
test('(g3) dimensjonstall i kvalitet + kvalitetssjekker == faktisk antall i rubrikkfila', () => {
const groups = rubricDims();
const obj = groups.Objective.length;
const kr = groups['Key Result'].length;
const total = obj + kr;
// parser-sanity: gruppene skal finnes og summere til overskrifts-tellingen.
assert.ok(obj > 0 && kr > 0, `parser-sanity: fant ${obj} Objective- og ${kr} KR-dims`);
assert.equal(
total,
headingsOf(readDoc(`${REF}/okr-quality-rubrics.md`), /^### /).length,
'gruppe-summen skal daekke alle ###-dimensjoner (ingen dim utenfor de to gruppene)',
);
// Hver dimensjon skal ha NOEYAKTIG fem ankere -- ankerformen er det rubrikkfila lover.
const blocks = readDoc(`${REF}/okr-quality-rubrics.md`).split(/^### /m).slice(1);
const feilAnkertall = blocks
.map((b) => [b.split('\n')[0].trim(), (b.match(/^\d+\. \*\*Anker \d/gm) ?? []).length])
.filter(([, n]) => n !== 5)
.map(([navn, n]) => `${navn}: ${n} ankere`);
assert.deepEqual(feilAnkertall, [], `hver dimensjon skal ha fem ankere:\n${feilAnkertall.join('\n')}`);
// Drift-laasen: hvert tall prosaen oppgir maa matche den utledede tellingen.
//
// Aksene daekker BEGGE spraak. SKILL.md er engelsk etter policy (CLAUDE.md
// «SKILL.md: English»), saa en rent norsk akse ville vaert blind for
// «all 10 dimensions» selv med SKILL.md i fil-lista. Mutasjons-verifisert:
// med kun de norske aksene slapp nettopp den formen gjennom.
const akser = [
[/(\d+)\s+(?:dimensjonene?|dimensions?)\b/gi, total, 'totalt antall dimensjoner'],
[/\bObjective\s*\((\d+)\)/g, obj, 'Objective-gruppa'],
[/\bKey Result\s*\((\d+)\)/g, kr, 'Key Result-gruppa'],
[/\b(?:de|the)\s+(\d+)\s+Objective[-\s]dimensjonene?\b/gi, obj, 'Objective-gruppa'],
[/\b(?:de|the)\s+(\d+)\s+Key Result[-\s]dimensjonene?\b/gi, kr, 'Key Result-gruppa'],
[/\b(?:de|the)\s+(\d+)\s+Objective\s+dimensions?\b/gi, obj, 'Objective-gruppa (en)'],
[/\b(?:de|the)\s+(\d+)\s+Key Result\s+dimensions?\b/gi, kr, 'Key Result-gruppa (en)'],
];
// SKILL.md er med selv om den er ENGELSK og i dag ikke oppgir noe
// dimensjonstall. Den staar her av samme grunn som R4 la den inn i
// canonScan() (se :36-39): SKILL.md er nettopp flata drift har overlevd paa
// foer, og et engelsk «all 11 dimensions» ville vaert usynlig for enhver
// norsk grep. Aksene under matcher begge spraak.
const drift = [];
for (const f of ['commands/kvalitet.md', 'agents/kvalitetssjekker-agent.md', SKILL]) {
const body = readDoc(f);
for (const [re, forventet, hva] of akser) {
for (const m of body.matchAll(re)) {
if (Number(m[1]) !== forventet) drift.push(`${f}: "${m[0].trim()}" != ${forventet} (${hva})`);
}
}
}
assert.deepEqual(drift, [], `dimensjonstall-drift (rubrikkfila = ${obj}+${kr}=${total}):\n${drift.join('\n')}`);
});
// --- (g5) kommando-antall drift-laas (D7 steg 19 / D6-beslutning 7) ---
//
// README.md:172 sa «all 14 commands» mens treet hadde 15 -- utdatert FOER denne
// oekten, og steg 19 gjorde den ett verre ved aa legge til /okr:arkivklar.
// Samme defektklasse som help.md/README.md-glippen i steg 12 (fikset i d2ac153):
// en levert kommandoform skal synes paa ALLE flater samtidig.
//
// Tallet utledes av commands/*.md -- aldri hardkodet, jf. (d) F-g.
test('(g5) prosa-omtaler av kommando-antall == faktisk antall commands/*.md', () => {
const antall = mdFiles('commands').length;
assert.ok(antall >= 10, `parser-sanity: fant ${antall} kommandofiler`);
// Tabellene som ER kommandolista skal ha en rad per fil.
for (const f of ['commands/help.md', 'CLAUDE.md']) {
const rader = readDoc(f).split('\n').filter((l) => /^\|\s*`\/okr:/.test(l)).length;
assert.equal(rader, antall, `${f}: ${rader} tabellrader != ${antall} kommandofiler`);
}
const drift = [];
for (const f of ['README.md', 'commands/help.md', 'CLAUDE.md', SKILL]) {
for (const m of readDoc(f).matchAll(/\b(\d+)\s+(?:commands|kommandoer)\b/gi)) {
if (Number(m[1]) !== antall) drift.push(`${f}: "${m[0].trim()}" != ${antall}`);
}
}
assert.deepEqual(drift, [], `kommando-antall drift (telt = ${antall}):\n${drift.join('\n')}`);
});
// --- (g4) guardrail som KR-designmoenster (D7 steg 16) ---
//
// Baseline foer steget: 0 treff paa "guardrail" i commands/, 2 i okr-antipatterns.md
// (:197 og :220). Begrepet fantes altsaa i kunnskapslaget, men naadde aldri malene
// pluginen faktisk emitterer -- samme uwirede-ledd-form som D6 steg 14 lukket.
// kvalitet.md skal dessuten KOBLE guardrail til leading/lagging-dimensjonen fra
// steg 15; uten koblingen blir sjekkpunktet et loest ord.
test('(g4) guardrail er wiret som KR-designmoenster i skriv + kvalitet', () => {
const mangler = [];
for (const f of ['commands/skriv.md', 'commands/kvalitet.md']) {
if (!/guardrail/i.test(readDoc(f))) mangler.push(`${f}: mangler guardrail-omtale`);
}
const kval = readDoc('commands/kvalitet.md');
const koblet = kval
.split('\n')
.some((l) => /guardrail/i.test(l) && /leading\/lagging|lagging/i.test(l));
if (!koblet) mangler.push('commands/kvalitet.md: guardrail ikke koblet til leading/lagging-dimensjonen');
assert.deepEqual(mangler, [], `guardrail-designmoenster (anker: okr-antipatterns.md:197,220):\n${mangler.join('\n')}`);
});
// (h) F-a fjerning. RED til Step 7. Score->modenhet-avledning borte; sandbagging-trendsignal til stede. // (h) F-a fjerning. RED til Step 7. Score->modenhet-avledning borte; sandbagging-trendsignal til stede.
test('(h) F-a: ingen score->modenhet-avledning; sandbagging-trendsignal til stede', () => { test('(h) F-a: ingen score->modenhet-avledning; sandbagging-trendsignal til stede', () => {
const problems = []; const problems = [];
@ -656,143 +424,3 @@ test('(k) R2: kommandoer som instruerer Glob deklarerer Glob i allowed-tools', (
`Glob instruert i body uten dekning i allowed-tools:\n${missing.join('\n')}`, `Glob instruert i body uten dekning i allowed-tools:\n${missing.join('\n')}`,
); );
}); });
// --- (l) D4 steg 3: framework-seksjonene baerer hver sin egen kildeseksjon ---
// Fase D tilfoerer to navngitte seksjoner i okr-framework.md. Begge er domeneprosa
// bygget paa eksterne kilder, og kildedisiplinen krever at HVER av dem baerer sin
// egen kildeseksjon -- ikke en felles liste nederst, som gjoer det umulig aa se
// hvilken paastand som stoetter seg paa hva. Vakten er strukturell (seksjon finnes +
// seksjonen inneholder "### Kilder"), aldri paa ordlyd i selve prosaen.
const D4_FRAMEWORK_SECTIONS = [
'Fra strategi til OKR',
'Beyond Budgeting og target/forecast-separasjonen',
];
// Kropp mellom "## <heading>" og neste "## " -- brukes til aa binde kildeseksjonen
// til RIKTIG seksjon i stedet for aa lete i hele fila.
function sectionBody(body, heading) {
const lines = body.split('\n');
const start = lines.findIndex((l) => l.trim() === `## ${heading}`);
if (start === -1) return null;
const rest = lines.slice(start + 1);
const end = rest.findIndex((l) => /^## /.test(l));
return (end === -1 ? rest : rest.slice(0, end)).join('\n');
}
test('(l) D4: framework-seksjonene finnes og baerer hver sin kildeseksjon', () => {
const body = readDoc(`${REF}/okr-framework.md`);
const missing = [];
for (const heading of D4_FRAMEWORK_SECTIONS) {
const sec = sectionBody(body, heading);
if (sec === null) {
missing.push(`seksjon mangler: ## ${heading}`);
continue;
}
if (!/^### Kilder\s*$/m.test(sec)) {
missing.push(`seksjon uten egen kildeseksjon: ## ${heading}`);
}
}
assert.deepEqual(
missing,
[],
`fase D-seksjoner i okr-framework.md:\n${missing.join('\n')}`,
);
});
// (l2) BBIs eget materiale navngir det siste prosessprinsippet "Coordination";
// utbredte sekundaerkilder kaller det "Rhythm". Diskrepansen er reell, og loesningen
// er aa sitere ORDLYD framfor kortnavn. Vakten laaser at sekundaerkilde-navnet aldri
// snieker seg inn i kanon.
test('(l2) D4: BBI-prinsipper siteres paa ordlyd, ikke paa det omstridte kortnavnet', () => {
const offenders = linesMatching(readDoc(`${REF}/okr-framework.md`), /Rhythm/);
assert.deepEqual(
offenders,
[],
'BBIs egen ordlyd navngir prinsippet "Coordination"; "Rhythm" er sekundaerkilde-navngiving',
);
});
// --- (m) D4 steg 4: KOSTRA som indikatorkilde i den kommunale styringslinjen ---
// Den kommunale fila hadde 0 treff paa KOSTRA foer fase D, samtidig som KOSTRA er
// den faktiske indikatorkilden kommunale KR-er henter fra. Vakten laaser at
// seksjonen finnes og baerer sin egen kildeseksjon, samme form som (l).
test('(m) D4: kommunal-styring baerer KOSTRA-seksjon med egen kildeseksjon', () => {
const body = readDoc(`${REF}/okr-kommunal-styring.md`);
const missing = [];
const sec = sectionBody(body, 'KOSTRA som indikatorkilde');
if (sec === null) {
missing.push('seksjon mangler: ## KOSTRA som indikatorkilde');
} else if (!/^### Kilder\s*$/m.test(sec)) {
missing.push('KOSTRA-seksjonen mangler egen kildeseksjon');
}
assert.deepEqual(missing, [], `KOSTRA-seksjon i okr-kommunal-styring.md:\n${missing.join('\n')}`);
});
// (m2) SEKTORAVGRENSNING. DFO-notat 2026:2 "Styring i staten" er eksplisitt avgrenset
// mot kommunal sektor. Notatet er en rik kilde som brukes flere steder i kanon, og
// nettopp derfor er det lett aa dra det med inn i den kommunale fila ved et uhell.
// Vakten er en ren fraavaers-invariant paa notatets identifikator.
test('(m2) D4: kommunal-styring siterer ikke DFO-notat 2026:2 (sektoravgrensning)', () => {
const offenders = linesMatching(
readDoc(`${REF}/okr-kommunal-styring.md`),
/2026:2|dfo-notat-2026-2/i,
);
assert.deepEqual(
offenders,
[],
'DFO-notat 2026:2 er avgrenset mot kommunal sektor og kan ikke siteres her',
);
});
// --- (n) D5 steg 6: KR-datakontrakten (beslutning B-1) er dokumentert i skriv-malen ---
// Generatorene i lib/syklus-*.mjs leser KR-tall fra `krN_`-noekler i okr-*.md sin
// frontmatter. Kontrakten har ingen skjema-fil aa haandheve seg mot -- den haandheves
// av at DEFINISJONSFLATEN (skriv-malen) dokumenterer den, siden /okr:skriv er stedet
// tallene oppstaar. Vakten binder de to sidene sammen: endres kontrakten i lib/ uten
// at malen foelger etter, produserer brukeren filer generatoren ikke kan lese.
const KR_KONTRAKT_NOEKLER = ['navn', 'baseline', 'target', 'naa', 'type'];
test('(n) D5: skriv.md dokumenterer alle fem krN_-noeklene i KR-datakontrakten', () => {
const doc = readDoc('commands/skriv.md');
const missing = KR_KONTRAKT_NOEKLER.filter(
(k) => !new RegExp(`kr(?:N|\\d+)_${k}\\b`).test(doc),
);
assert.deepEqual(
missing,
[],
`KR-datakontrakten (B-1) mangler noekler i commands/skriv.md: ${missing.join(', ')}`,
);
});
// B-2: score BEREGNES, lagres aldri. En `krN_score`-noekkel i malen ville skapt en
// andre sannhetskilde som kan drifte fra (naa - baseline) / (target - baseline).
// Fravaers-invariant paa NOEKKELFORMEN slik lib/frontmatter.mjs:29 leser den
// (`^\s*key:`) -- ikke paa ordet "score", som malen legitimt bruker om
// moonshot/roofshot-kalibrering (:124), og ikke paa prosa som FORBYR noekkelen.
// Det er noekkelen i frontmatter som skaper den andre sannhetskilden; en omtale
// av den gjoer det ikke.
test('(n2) D5: skriv.md introduserer ingen krN_score-noekkel (score lagres aldri)', () => {
const offenders = linesMatching(readDoc('commands/skriv.md'), /^\s*kr(?:N|\d+)_score\s*:/m);
assert.deepEqual(
offenders,
[],
'score skal beregnes fra baseline/target/naa, aldri lagres som egen noekkel',
);
});
// Patch-lane #1: export-pdf.py bygger HTML med python-markdown, men uten
// `meta`-extensionen parses ikke YAML-frontmatter -- den renderes som broedtekst
// oeverst i PDF-en. Hver OKF-fil i treet BAERER frontmatter (type/resource/
// timestamp/...), saa enhver eksport av en bundle-fil lekker maskinlaget inn i
// et dokument som skal til sak-arkivet. Vakten laaser extensionen som naervaers-
// invariant paa den faktiske kallsiden, ikke paa prosa om den.
test('(p1) export-pdf.py aktiverer meta-extensionen (frontmatter skal ikke rendres)', () => {
const py = readDoc('scripts/export-pdf.py');
const m = py.match(/extensions=\[([^\]]*)\]/);
assert.ok(m, 'scripts/export-pdf.py: fant ingen extensions=[...] i markdown-kallet');
const exts = m[1].split(',').map((s) => s.trim().replace(/^["']|["']$/g, ''));
assert.ok(
exts.includes('meta'),
`python-markdown uten "meta" rendrer YAML-frontmatter som broedtekst; fant: ${exts.join(', ')}`,
);
});

View file

@ -148,133 +148,6 @@ test('at-risk telles paa kanonisk 1.8.0-vokabular (At Risk + Off Track, ikke On
}); });
}); });
// --- Seksjons-bevisst telling (patch-lane #1) ---
//
// okr-framework.md:605: «De fleste stretch-KR boer derfor starte At Risk -- ikke
// groent», og permanent groent ER sandbagging-signalet. En hook som teller ALLE
// ikke-groenne rader under ett ber derfor operatoeren om «tiltak» for at
// aspirational-portefoeljen oppfoerer seg slik doktrinen krever, og eneste maate
// aa dempe nagget paa er aa merke stretch-KR groenne -- noeyaktig det kanon
// advarer mot. Fra 1.10.0 fylles Confidence-kolonnen deterministisk av
// /okr:sporing, saa kollisjonen inntreffer alltid, ikke bare av og til.
//
// Operatoerbeslutning (S61), to ledd:
// (a) aspirational At Risk er FORVENTET og nagger ikke; aspirational Off Track
// teller, fordi «blokkere oppstaar, progresjon stopper opp» (framework:610)
// er et reelt signal ogsaa for et stretch-KR.
// (b) en rad hooken ikke kan plassere i en seksjon telles som COMMITTED.
// Haandskrevne status.md fra foer 1.10.0 har ingen seksjonsstruktur, og en
// stille fil er den farlige feilen: den ser vurdert ut uten aa vaere det.
// Samme verdi som dekningsvakten i statusrapport() haandhever.
const GENERERT_HODE = [
'# Statusrapport T2-2026',
'',
'| KR | Baseline | Target | Naa | Score | Avvik | Confidence |',
'|----|----------|--------|-----|-------|-------|------------|',
];
// Bygger en fil med generatorens seksjonsstruktur. `## Avvik som skal forklares`
// ligger MELLOM de to seksjonene i ekte output og er med her med vilje: den
// inneholder kun punktlister, og skal verken telles eller oedelegge
// seksjons-tilhoerigheten for radene under.
function generertStatus({ committed = [], aspirational = [] }) {
const ut = ['# Statusrapport T2-2026', ''];
if (committed.length > 0) {
ut.push('## Committed Key Results', '', '### Trafikksikkerhet', '', ...GENERERT_HODE.slice(2), ...committed, '');
}
ut.push(
'## Avvik som skal forklares',
'',
'- Trafikksikkerhet: KR1 -- naa 17 mot krav 20 (score 0.63)',
'',
);
if (aspirational.length > 0) {
ut.push('## Aspirational Key Results', '', '### Digitalisering', '', ...GENERERT_HODE.slice(2), ...aspirational, '');
}
return ut.join('\n');
}
function writeStatus(work, innhold) {
const statusDir = join(work, '.claude', 'okr', 'syklus', 'T2-2026');
mkdirSync(statusDir, { recursive: true });
writeFileSync(join(statusDir, 'status.md'), innhold);
}
test('aspirational At Risk nagger IKKE -- kanon sier stretch-KR boer staa der', () => {
withWork((work) => {
writeConfig(work, '---\nid: "T2-2026"\n---\n');
writeStatus(work, generertStatus({
committed: ['| KR1: Alvorlige hendelser | 0 | 0 | 0 | udefinert | Nei | On Track |'],
aspirational: [
'| KR2: Andel digitale soeknader | 60 | 85 | 63 | 0.12 | - | At Risk |',
'| KR3: Saksbehandlingstid | 14 | 5 | 11 | 0.33 | - | At Risk |',
],
}));
const out = runHook(work, '2026-06-20');
assert.doesNotMatch(
out,
/OBS:/,
'en sunn aspirational-portefoelje (At Risk) skal ikke utloese tiltaks-nagget',
);
});
});
test('aspirational Off Track teller -- progresjonen har stoppet opp', () => {
withWork((work) => {
writeConfig(work, '---\nid: "T2-2026"\n---\n');
writeStatus(work, generertStatus({
aspirational: [
'| KR2: Andel digitale soeknader | 60 | 85 | 63 | 0.12 | - | At Risk |',
'| KR3: Saksbehandlingstid | 14 | 5 | 11 | 0.33 | - | Off Track |',
],
}));
const out = runHook(work, '2026-06-20');
assert.match(out, /OBS: 1 KR er merket/, 'kun Off Track-raden skal telle (1), ikke At Risk-raden');
});
});
test('committed teller baade At Risk og Off Track, aspirational At Risk holdes utenfor', () => {
withWork((work) => {
writeConfig(work, '---\nid: "T2-2026"\n---\n');
writeStatus(work, generertStatus({
committed: [
'| KR1: Alvorlige hendelser | 0 | 0 | 0 | udefinert | Nei | Off Track |',
'| KR2: Oppgradert utrustning | 12 | 20 | 17 | 0.63 | Ja | At Risk |',
],
aspirational: ['| KR3: Andel digitale soeknader | 60 | 85 | 63 | 0.12 | - | At Risk |'],
}));
const out = runHook(work, '2026-06-20');
assert.match(out, /OBS: 2 KR er merket/, 'de to committed-radene teller; aspirational At Risk gjoer ikke');
});
});
// Fallback-beslutningen (b), som EGEN vakt. De to eldre at-risk-testene over
// treffer den ogsaa, men de handler om noe annet (rad-form og vokabular), saa en
// fremtidig endring kunne snudd fallback-semantikken uten at noen test NAVNGA
// den. Moenster: S60s M3 -- en overlappet vakt er en udekket vakt.
test('fallback: rader uten seksjonsoverskrift telles som committed, ikke bortfiltrert', () => {
withWork((work) => {
writeConfig(work, '---\nid: "T2-2026"\n---\n');
// Haandskrevet form fra foer 1.10.0: ingen ## Committed / ## Aspirational.
writeStatus(work, [
'# Status T2-2026',
'',
...GENERERT_HODE.slice(2),
'| KR1: Redusere ulykker | 40 | 30 | 35 | 0.50 | Ja | At Risk |',
'| KR2: Oppetid | 10 | 25 | 22 | 0.80 | Nei | On Track |',
'| KR3: Tilfredshet | 60 | 90 | 65 | 0.17 | Ja | At Risk |',
'',
].join('\n'));
const out = runHook(work, '2026-06-20');
assert.match(
out,
/OBS: 2 KR er merket/,
'uplasserbare At Risk-rader maa fortsatt telle -- en stille fil er den farlige feilen',
);
});
});
test('OKR_NOW sen fase: late-coaching', () => { test('OKR_NOW sen fase: late-coaching', () => {
withWork((work) => { withWork((work) => {
writeConfig(work, '---\nid: "T2-2026"\n---\n'); writeConfig(work, '---\nid: "T2-2026"\n---\n');

View file

@ -6,25 +6,7 @@ description: Tertialmaal for digital selvbetjening.
tags: tags:
- digital - digital
timestamp: '2026-02-01T09:05:00+00:00' timestamp: '2026-02-01T09:05:00+00:00'
kr1_navn: Andel digitale soeknader (prosent)
kr1_baseline: 60
kr1_target: 85
kr1_naa: 63
kr1_type: aspirational
kr2_navn: Gjennomsnittlig saksbehandlingstid (dager)
kr2_baseline: 14
kr2_target: 5
kr2_naa: 11
kr2_type: aspirational
--- ---
# Digitalisering av tjenester # Digitalisering av tjenester
Objective: Oeke selvbetjeningsgrad i publikumstjenester. Objective: Oeke selvbetjeningsgrad i publikumstjenester.
KR1: Oeke andel digitale soeknader fra 60 til 85 prosent.
- Maalemetode: Andel soeknader levert via selvbetjeningsloesningen.
- Datakilde: Soeknadsloggen.
- Type: Aspirational
KR2: Redusere gjennomsnittlig saksbehandlingstid fra 14 til 5 dager.
- Maalemetode: Median tid fra mottak til vedtak.
- Datakilde: Saksbehandlingssystemet.
- Type: Aspirational

View file

@ -7,25 +7,8 @@ tags:
- sikkerhet - sikkerhet
- tunnel - tunnel
timestamp: '2026-02-01T09:00:00+00:00' timestamp: '2026-02-01T09:00:00+00:00'
kr1_navn: Alvorlige tunnelhendelser (antall)
kr1_baseline: 0
kr1_target: 0
kr1_naa: 0
kr1_type: committed
kr2_navn: Tunneler med oppgradert sikkerhetsutrustning (antall)
kr2_baseline: 12
kr2_target: 20
kr2_naa: 17
kr2_type: committed
--- ---
# Trafikksikkerhet og tunnelsikkerhet # Trafikksikkerhet og tunnelsikkerhet
Objective: Styrke tunnelsikkerhet i tertialet. Objective: Styrke tunnelsikkerhet i tertialet.
KR1: Naa nullvisjon for alvorlige tunnelhendelser. KR1: Naa nullvisjon for alvorlige tunnelhendelser.
- Maalemetode: Registrerte alvorlige hendelser i hendelsesdatabasen.
- Datakilde: Vegtrafikksentralen.
- Type: Committed
KR2: Oeke antall tunneler med oppgradert sikkerhetsutrustning fra 12 til 20.
- Maalemetode: Telling av ferdigattesterte tunneler.
- Datakilde: Prosjektportefoeljen.
- Type: Committed

View file

@ -123,54 +123,11 @@ test('writeFrontmatter: array-verdi (tags) -> OKF multi-linje liste (Step 3)', (
const { get } = parseFrontmatter(block); const { get } = parseFrontmatter(block);
assert.equal(get('type'), 'OKR', 'skalar FOR list-blokk resolver'); assert.equal(get('type'), 'OKR', 'skalar FOR list-blokk resolver');
assert.equal(get('kilde'), 'innboks', 'skalar ETTER innrykket list-blokk resolver fortsatt'); assert.equal(get('kilde'), 'innboks', 'skalar ETTER innrykket list-blokk resolver fortsatt');
// get() paa en list-key returnerer null -- tolerance-kontrakten i // Lese-siden uendret (Step 3 scope = kun skrive-siden): get() paa en list-key
// frontmatter.mjs:14-16 er sann etter at verdi-grenen ble strammet fra \s* // krasjer ikke (eksisterende tolerance-kontrakt, frontmatter.mjs:14-16). Den
// til [ \t]* (se (L1)). Fram til da returnerte den foerste list-ELEMENT. // returnerer foerste list-element fordi parser-\s* spiser newline -- IKKE
assert.equal(get('tags'), null, 'get paa list-key gir null'); // null; konsumentene leser aldri tag-VERDIER, kun at nabo-skalarer resolver.
}); assert.doesNotThrow(() => get('tags'), 'get paa list-key krasjer ikke');
// --- Linjeskift-aksen (L1-L5) ------------------------------------------------
// `:\s*(.*)$` lot \s* krysse linjeskift, saa ENHVER key med tom rest-av-linje
// slukte den neste ikke-tomme linja. Utslaget er ikke begrenset til list-keys:
// en tom `title:` returnerte neste keys HELE linje ("type: OKR"). Fikset ved aa
// stramme KUN den andre \s* til [ \t]*; den foerste (`^\s*`, innrykk-toleranse)
// er load-bearing for inject-okr-context.mjs:69 og staar urort -- (L4) vokter det.
test('(L1) list-key: get() returnerer null (kontrakten :14-16 blir sann)', () => {
const { get } = parseFrontmatter(
'---\ntype: OKR\ntags:\n - alpha\n - beta\ntitle: Ekte tittel\n---\n',
);
assert.equal(get('tags'), null, 'list-key skal gi null, ikke foerste list-element');
assert.equal(get('title'), 'Ekte tittel', 'skalar etter list-blokk resolver fortsatt');
});
test('(L2) tom skalar-key sluker ikke neste rot-key', () => {
const { get } = parseFrontmatter('---\ntitle:\ntype: OKR\n---\n');
assert.equal(get('title'), null, 'tom title skal gi null, ikke "type: OKR"');
assert.equal(get('type'), 'OKR', 'den slukte keyen resolver selv');
});
test('(L3) tom key rett foran innrykket list-blokk gir null', () => {
const { get } = parseFrontmatter('---\ntype: OKR\ndescription:\ntags:\n - x\n---\n');
assert.equal(get('description'), null, 'tom description skal gi null, ikke "tags:"');
});
test('(L4) innrykk-toleransen BEVART: nestet oppslag resolver, forelder gir null', () => {
const { get } = parseFrontmatter(
'---\norganisasjon:\n navn: "NestetOrg"\n type: "offentlig"\n---\n',
);
assert.equal(get('navn'), 'NestetOrg', 'load-bearing inject:69 -- foerste \\s* er urort');
assert.equal(get('organisasjon'), null, 'tom forelder-key skal ikke gi "navn: ..."');
});
test('(L5) tab etter kolon: verdien resolver rent', () => {
// MERK: denne er IKKE en vakt for `\t` i tegnklassen. Mutasjonstest viste at
// `[ ]*` passerer like godt -- .trim() paa :39 normaliserer tabene uansett,
// saa `[ \t]*` og `[ ]*` er atferdsmessig ekvivalente for all input. `\t` staar
// igjen som intensjons-dokumentasjon (horisontal whitespace), ikke som atferd.
// Testen vokter kontrakten "tab-separert verdi resolver", som holder uansett.
const { get } = parseFrontmatter('---\nnavn:\t\tOrg\n---\n');
assert.equal(get('navn'), 'Org');
}); });
test('writeFrontmatter: skalar uendret av array-gren (additiv)', () => { test('writeFrontmatter: skalar uendret av array-gren (additiv)', () => {

View file

@ -17,7 +17,6 @@ import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { generateIndexes } from '../scripts/okf-index.mjs'; import { generateIndexes } from '../scripts/okf-index.mjs';
import { checkBundle } from '../scripts/okf-check.mjs'; import { checkBundle } from '../scripts/okf-check.mjs';
import { parseFrontmatter } from '../lib/frontmatter.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const CHECK = join(ROOT, 'scripts', 'okf-check.mjs'); const CHECK = join(ROOT, 'scripts', 'okf-check.mjs');
@ -747,71 +746,6 @@ test('okf-index: UBALANSERT anfoerselstegn strippes ikke (konservativ unquote)',
} }
}); });
// --- S40: siterings-aksen naar ogsaa CHECKEREN (lese-siden) ---
// 1.8.2 unquotet PRODUSENTEN (okf-index/parseExistingIndex). Checkerens
// rootMarkers/pick returnerte fortsatt raa streng, saa samme fil ga to lesninger:
// produsenten tolket `okf_version: "0.2"` som 0.2, checkeren ekkoet «"0.2"».
// Catalog maalte divergensen mot sin egen checker (begge AKSEPTERER -- avviket er
// paa VERDIEN) paa en bundle som foelger upstreams eget eksempel (SPEC.md:773).
// Unquote er samme konservative regel som paa produsent-siden: KUN et matchende
// par er YAML-syntaks; en halv sekvens er en ugyldig verdi som bevares uroert.
test('okf-check: SITERT okf_version rapporteres unquoted (siterings-akse, lese-siden)', () => {
const dir = tmpRoot();
try {
writeRootIndex(dir, ['okf_version: "0.2"']);
const r = checkBundle(dir);
assert.equal(r.okfVersion, '0.2', 'anfoerselstegn er YAML-syntaks, ikke del av verdien');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-check: ENKELTSITERT okf_version rapporteres unquoted', () => {
const dir = tmpRoot();
try {
writeRootIndex(dir, ["okf_version: '0.2'"]);
const r = checkBundle(dir);
assert.equal(r.okfVersion, '0.2');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-check: SITERT okf_layout rapporteres unquoted', () => {
const dir = tmpRoot();
try {
writeRootIndex(dir, ['okf_version: 0.2', 'okf_layout: "kb-layout-2099-12"']);
const r = checkBundle(dir);
assert.equal(r.okfLayout, 'kb-layout-2099-12', 'begge markoerene leses likt');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-check: UBALANSERT anfoerselstegn bevares uroert (konservativ unquote)', () => {
const dir = tmpRoot();
try {
writeRootIndex(dir, ['okf_version: "0.2']);
const r = checkBundle(dir);
assert.equal(r.okfVersion, '"0.2', 'halv sekvens er ugyldig verdi, ikke noe aa gjette paa');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-check CLI: sitert markoerverdi ekkoes unquoted i utskriften', () => {
const dir = tmpRoot();
try {
writeRootIndex(dir, ['okf_version: "0.2"', 'okf_layout: "kb-layout-2099-12"']);
const { stdout } = runCheck(dir);
assert.match(stdout, /okf_version: 0\.2$/m, 'menneskelig sammenligning skal se verdien, ikke syntaksen');
assert.match(stdout, /okf_layout: kb-layout-2099-12$/m);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-index CLI: --okf-layout bumper layout og lar okf_version staa', () => { test('okf-index CLI: --okf-layout bumper layout og lar okf_version staa', () => {
const dir = tmpRoot(); const dir = tmpRoot();
try { try {
@ -900,357 +834,3 @@ test('okf-check: rot-index uten okf_layout ekkoer MANGLER for den, ikke for okf_
rmSync(dir, { recursive: true, force: true }); rmSync(dir, { recursive: true, force: true });
} }
}); });
// --- D8 steg 20: okf-check leser markoerene fra BEGGE plasseringene ---
// Beslutning 6 flytter `okf_version` til rot-indeksens frontmatter (steg 21),
// mens `okf_layout` blir staaende i broedteksten. Bundles migrerer IKKE
// samtidig -- en bundle skrevet av en eldre okf-index baerer fortsatt begge i
// broedteksten -- saa LESINGEN maa tolerere begge former gjennom migrasjonen.
// Presedens: frontmatter foerst, deretter broedteksten UNDER frontmatter-blokken.
// Fortsatt ren ekko (ingen verdivalidering) og fortsatt ingen PRESENCE-krav.
//
// De tre casene er valgt for aa DISKRIMINERE de to lagene. S49-laerdommen:
// en assertion som ikke kan skille HVILKET lag som fanget gjoer en tolagsvakt
// til en ettlagsvakt uten at noe blir roedt. Her roedner hvert lag for seg:
// (20a) kun frontmatter -> roedner naar frontmatter-laget fjernes
// (20b) frontmatter UTEN noekkelen, verdi i -> roedner naar broedtekst-laget fjernes
// broedteksten (den naive "har fila frontmatter?
// bruk kun den"-regresjonen)
// (20c) begge, ULIKE verdier -> pinner presedensen; roedner naar
// rekkefoelgen snus
// Uten (20b) ville steg 20 bestaatt UTEN kodeendring: dagens rootMarkers()
// regex-scanner raa filtekst med `m`-flagg og treffer allerede frontmatter-
// linjer -- foerste treff vinner, og frontmatter staar oeverst.
// Rot-index med VILKAARLIG innhold (helleren writeRootIndex laaser formen).
function writeRawRootIndex(dir, content) {
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'index.md'), content);
}
test('(20a) okf-check: okf_version i rot-indeksens FRONTMATTER leses', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
writeRawRootIndex(dir, '---\nokf_version: 0.1\n---\n\n# Bundle\n\nokf_layout: kb-layout-2026-06\n');
const r = checkBundle(dir);
assert.equal(r.okfVersion, '0.1', 'frontmatter-plassert okf_version skal leses');
assert.equal(r.okfLayout, 'kb-layout-2026-06', 'broedtekst-plassert okf_layout skal fortsatt leses');
assert.equal(runCheck(dir).status, 0, 'migrert form skal gi exit 0');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(20b) okf-check: frontmatter UTEN okf_version faller tilbake til broedteksten', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
// Umigrert bundle som likevel har en frontmatter-blokk: verdien staar i
// broedteksten. En lesing som slaar seg til ro med "fila HAR frontmatter"
// mister markoeren her -- det er regresjonen dette steget er likest til.
writeRawRootIndex(dir, '---\ntitle: Bundle\n---\n\n# Bundle\n\nokf_version: 0.1\nokf_layout: kb-layout-2026-06\n');
const r = checkBundle(dir);
assert.equal(r.okfVersion, '0.1', 'broedtekst-verdien skal finnes selv naar frontmatter finnes uten noekkelen');
assert.equal(r.okfLayout, 'kb-layout-2026-06');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(20c) okf-check: staar markoeren BEGGE steder, vinner frontmatter', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
// Overgangstilstand: en delvis migrert index kan baere begge. Frontmatter er
// maalformen, saa den er den autoritative -- broedteksten er restene.
writeRawRootIndex(dir, '---\nokf_version: 0.9\n---\n\n# Bundle\n\nokf_version: 0.1\nokf_layout: kb-layout-2026-06\n');
const r = checkBundle(dir);
assert.equal(r.okfVersion, '0.9', 'frontmatter skal ha presedens over broedteksten');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(20d) okf-check: sitert okf_version i frontmatter unquotes som paa produsent-siden', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
// Upstreams eget kanoniske eksempel (SPEC.md:773) er sitert. Samme fil skal
// ikke gi to lesninger avhengig av hvor markoeren staar.
writeRawRootIndex(dir, '---\nokf_version: "0.2"\n---\n\n# Bundle\n');
assert.equal(checkBundle(dir).okfVersion, '0.2', 'anfoerselstegn er YAML-syntaks, ikke del av verdien');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(20e) okf-check: markoer helt fravaerende gir fortsatt exit 0 (ingen PRESENCE-krav)', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
writeRawRootIndex(dir, '---\ntitle: Bundle\n---\n\n# Bundle\n');
const r = checkBundle(dir);
assert.equal(r.okfVersion, null, 'fravaerende markoer skal vaere null');
assert.equal(r.okfLayout, null);
assert.equal(runCheck(dir).status, 0, 'okf_version er MAY i specen -- fravaer er ikke en feil');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(20f) okf-check: frontmatter-markoer tolkes som YAML -- trailing kommentar lekker ikke inn i verdien', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
// Dette er caset som SKILLER de to lagene, og det eneste som roedner uten
// kodeendring: frontmatter ER YAML, saa ` # ...` er en kommentar og ikke del
// av verdien (lib/frontmatter.mjs:39, comment-leak-fiksen). Broedtekst-
// markoeren er IKKE YAML -- der er unquote() alene fortsatt riktig regel.
// Uten denne casen kunne steg 20 «bestaa» med dagens ettlags-regex, som
// scanner raa filtekst og ikke vet hva en frontmatter-blokk er.
writeRawRootIndex(dir, '---\nokf_version: 0.1 # upstream OKF-versjon\n---\n\n# Bundle\n');
assert.equal(checkBundle(dir).okfVersion, '0.1', 'YAML-kommentaren skal strippes i frontmatter');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
// --- D8 steg 21: okf_version emitteres i rot-indeksens frontmatter ---
// Laast arkitektur-beslutning 6: `okf_version` -> frontmatter, `okf_layout` ->
// broedtekst. De to markoerene har ulike eiere (Google vs. oss) og ulik
// livssyklus; frontmatter er den maskinlesbare flaten konsumenter utenfor
// pluginen leser, broedteksten er vaar egen. Gjelder KUN rot-indeksen --
// underkatalog-indekser forblir uten frontmatter.
//
// PRESENCE haandheves fortsatt IKKE: `okf_version` er MAY i specen, og dette
// steget flytter plassering, det innfoerer ikke et krav.
//
// Emisjon og lesing testes HVER FOR SEG (S38-S40-laerdommen: samme fil, samme
// markoer, to lesninger). (21e) er den ene bevisste integrasjonen: den beviser
// at steg 20s toleranse faktisk moeter det steg 21 skriver.
test('(21a) okf-index: rot-index baerer okf_version i FRONTMATTER, okf_layout i broedteksten', () => {
const dir = tmpRoot();
try {
cpSync(REALISTIC, dir, { recursive: true });
generateIndexes(dir);
const root = readFileSync(join(dir, 'index.md'), 'utf8');
assert.ok(root.startsWith('---\n'), 'rot-index skal aapne med en frontmatter-blokk');
const { get } = parseFrontmatter(root);
assert.equal(get('okf_version'), '0.1', 'okf_version skal ligge i frontmatter');
assert.equal(get('okf_layout'), null, 'okf_layout skal IKKE ligge i frontmatter');
const body = root.slice(root.indexOf('\n---', 3) + 4);
assert.match(body, /^okf_layout: kb-layout-2026-06$/m, 'okf_layout skal staa i broedteksten');
assert.ok(!/^okf_version:/m.test(body), 'okf_version skal ikke ogsaa staa i broedteksten');
assert.match(body, /^# /m, 'overskriften skal fortsatt finnes, under frontmatteren');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(21b) okf-index: underkatalog-index faar ALDRI frontmatter', () => {
const dir = tmpRoot();
try {
cpSync(REALISTIC, dir, { recursive: true });
generateIndexes(dir);
const sub = readFileSync(join(dir, 'strategisk-kontekst', 'index.md'), 'utf8');
assert.ok(!sub.startsWith('---'), 'undernivaa skal IKKE ha frontmatter');
assert.equal(sub.split('\n')[0].slice(0, 2), '# ', 'undernivaa starter fortsatt paa overskriften');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(21c) okf-index: to kjoeringer gir BYTE-IDENTISK rot-index (idempotens over ny plassering)', () => {
const dir = tmpRoot();
try {
cpSync(REALISTIC, dir, { recursive: true });
generateIndexes(dir);
const first = readFileSync(join(dir, 'index.md'), 'utf8');
generateIndexes(dir);
const second = readFileSync(join(dir, 'index.md'), 'utf8');
// Fanger at parseExistingIndex ikke forstaar den nye plasseringen:
// duplisert markoer, forskjoevet overskrift, eller markoer re-utledet til default.
assert.equal(second, first, 'andre kjoering skal ikke endre en byte');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(21d) okf-index: umigrert broedtekst-form loeftes til frontmatter, verdiene bevart', () => {
const dir = tmpRoot();
try {
// Bundle skrevet av en eldre okf-index: begge markoerene i broedteksten.
writeRootIndex(dir, ['okf_version: 0.3', 'okf_layout: kb-layout-2099-12']);
generateIndexes(dir);
const root = readFileSync(join(dir, 'index.md'), 'utf8');
assert.equal(parseFrontmatter(root).get('okf_version'), '0.3', 'den FUNNE versjonen skal loeftes, ikke erstattes av konstanten');
const body = root.slice(root.indexOf('\n---', 3) + 4);
assert.match(body, /^okf_layout: kb-layout-2099-12$/m, 'layout skal bli staaende i broedteksten, verbatim');
assert.ok(!/^okf_version:/m.test(body), 'den gamle broedtekst-linja skal ikke bli liggende igjen');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(21e) okf-index -> okf-check: det emitteren skriver, leser checkeren', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
generateIndexes(dir);
const r = checkBundle(dir);
assert.equal(r.okfVersion, '0.1', 'checkeren skal finne den frontmatter-plasserte versjonen');
assert.equal(r.okfLayout, 'kb-layout-2026-06', 'og den broedtekst-plasserte layouten');
assert.equal(runCheck(dir).status, 0);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
// --- S57 (patch-lane #2): OKF-felt leses paa ROT-NIVAA, ikke innrykket ---
// lib/frontmatter.mjs's `^\s*key:` er BEVISST innrykk-tolerant (nestet oppslag er
// load-bearing for inject-okr-context.mjs's organisasjon:/program:-lesing) -- saa
// rot-nivaa-kravet hoerer hjemme i KALLEREN. OKFs konsept-felt er rot-noekler:
// en `type:` under `metadata:` er ikke filas type.
test('(N1) okf-check: type: KUN nestet -> fila teller som utypet (exit != 0)', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
writeFileSync(
join(dir, 'dokumenter', 'nestet-type.md'),
'---\ntitle: Uten rot-type\nmetadata:\n type: OKR\n---\n# Uten rot-type\n',
);
const { status, stdout } = runCheck(dir);
assert.notEqual(status, 0, `nestet type: skal ikke telle som typet:\n${stdout}`);
assert.match(stdout, /nestet-type\.md/, 'skal navngi den utypede filen');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(N2) okf-check: en nestet type: FOER rot-type: skygger ikke rot-verdien', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
// Rot-verdien er i vokabularet; den nestede (som staar FOERST, og som
// first-match-regelen ellers ville plukket) er det ikke.
writeFileSync(
join(dir, 'dokumenter', 'skygge.md'),
'---\nmetadata:\n type: Tilfeldig\ntype: Notat\ntitle: Skygge\ndescription: x\n---\n# Skygge\n',
);
const r = checkBundle(dir, { strictIngest: true, files: [join(dir, 'dokumenter', 'skygge.md')] });
assert.deepEqual(r.missingType, [], 'rot-type: finnes -> fila er typet');
assert.deepEqual(r.strictErrors, [], 'vokab-sjekken skal se rot-verdien «Notat», ikke den nestede');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(N3) okf-check --strict-ingest: lenke i en NESTET title er ikke filas title', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
// Ingen rot-title. Den nestede lenka leses av ingen konsument -> ingen
// injeksjons-vektor, og skal derfor ikke felle dokumentet.
writeFileSync(
join(dir, 'dokumenter', 'nestet-title.md'),
'---\ntype: Notat\nmetadata:\n title: "[Klikk her](http://evil.example)"\n---\n# Nestet title\n',
);
const r = checkBundle(dir, { strictIngest: true, files: [join(dir, 'dokumenter', 'nestet-title.md')] });
assert.ok(
!r.strictErrors.some((e) => /lenke-baerende title/.test(e)),
`nestet title skal ikke gi lenke-baerende-title-feil, fikk:\n${r.strictErrors.join('\n')}`,
);
// Lenke-PORTEN er en annen akse og uendret: den skanner hele filen, ogsaa
// frontmatteren, saa den eksterne URL-en felles fortsatt -- av lenke-regelen,
// ikke av title-regelen.
assert.ok(
r.strictErrors.some((e) => /utrygg lenke/.test(e)),
'den filbrede lenke-porten skal fortsatt felle den eksterne URL-en',
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(N4) okf-check: et NESTET anbefalt felt demper ikke advarselen', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
writeFileSync(
join(dir, 'dokumenter', 'nestet-anbefalt.md'),
'---\ntype: Notat\nmetadata:\n description: Skjult beskrivelse.\n---\n# Nestet anbefalt\n',
);
const r = checkBundle(dir, { files: [join(dir, 'dokumenter', 'nestet-anbefalt.md')] });
assert.ok(
r.warnings.some((w) => /nestet-anbefalt\.md: mangler anbefalt felt .description./.test(w)),
`rot-nivaa description mangler -> advarsel forventet, fikk:\n${r.warnings.join('\n')}`,
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(L6) okf-check: et TOMT anbefalt felt demper ikke advarselen', () => {
// Linjeskift-aksen naar rootLevelGet: innrykk-filteret fjerner list-elementer,
// men et tomt `description:` fulgt av neste ROT-key slukte den linja og gjorde
// feltet ikke-tomt -- advarselen ble dempet av data som tilhorte `title`.
const dir = tmpRoot();
try {
buildCleanIngest(dir);
writeFileSync(
join(dir, 'dokumenter', 'tom-anbefalt.md'),
'---\ntype: Notat\ndescription:\ntitle: Ekte tittel\n---\n# Tom anbefalt\n',
);
const r = checkBundle(dir, { files: [join(dir, 'dokumenter', 'tom-anbefalt.md')] });
assert.ok(
r.warnings.some((w) => /tom-anbefalt\.md: mangler anbefalt felt .description./.test(w)),
`tomt description -> advarsel forventet, fikk:\n${r.warnings.join('\n')}`,
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(L7) okf-check: broedtekst-fallbacken plukker ikke list-verdi FRA frontmatter', () => {
// S57 beholdt broedtekst-uttrekket (okf-check.mjs:125) mot en LATENT defekt den
// ikke kunne maale: saa lenge get() paa en list-key ga foerste list-element,
// vant fm-laget alltid og fallbacken kjorte aldri for list-keys. Etter (L1) gir
// fm-laget null -> fallbacken kjorer -> uttrekket er foerste gang observerbart.
// Uten det ville `^okf_version:` matchet inne i frontmatteren og plukket «- 9.9».
const dir = tmpRoot();
try {
buildCleanIngest(dir);
writeFileSync(
join(dir, 'index.md'),
['---', 'type: Index', 'okf_version:', ' - 9.9', '---', '# Rot', '', 'okf_layout: kb-layout-2026-06', ''].join('\n'),
);
const r = checkBundle(dir, { files: [] });
assert.equal(r.okfVersion, null, 'list-verdi i frontmatter skal ikke lekke via fallbacken');
assert.equal(r.okfLayout, 'kb-layout-2026-06', 'ekte broedtekst-markoer resolver fortsatt');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(N5) okf-check: en NESTET okf_version i rot-index leses ikke som rot-markoer', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
// Frontmatter-grenen i rootMarkers var innrykk-tolerant mens broedtekst-
// fallbacken alltid har vaert rot-ankret (`^key:`). Samme fil ga da to svar
// avhengig av hvilket lag som traff foerst -- her skal begge si MANGLER.
writeFileSync(
join(dir, 'index.md'),
'---\nmeta:\n okf_version: "9.9"\n---\n# Bundle\n\nokf_layout: kb-layout-2026-06\n',
);
const r = checkBundle(dir);
assert.equal(r.okfVersion, null, 'nestet okf_version er ikke rotens markoer');
assert.equal(r.okfLayout, 'kb-layout-2026-06', 'den ekte broedtekst-markoeren skal fortsatt leses');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

View file

@ -2,7 +2,7 @@
// Step 10 (A1): package.json-kontrakten for det bevisste zero-dep-bruddet. // Step 10 (A1): package.json-kontrakten for det bevisste zero-dep-bruddet.
// Verifiserer at dep-laget er EXACT-pinnet (ingen ^/~/*), at engines-gulvet er // Verifiserer at dep-laget er EXACT-pinnet (ingen ^/~/*), at engines-gulvet er
// satt (unpdf krever node >= 22), at pakken er ESM (type: module), at versjonen // satt (unpdf krever node >= 22), at pakken er ESM (type: module), at versjonen
// er 1.10.0 (S60: det uwirede leddet) paa ALLE shippede flater, og at .npmrc slaar av // er 1.8.2 (patch-lane: unquote av siterte markoerverdier) paa ALLE shippede flater, og at .npmrc slaar av
// install-scripts (Shai-Hulud / supply-chain). Zero npm deps i selve testen. // install-scripts (Shai-Hulud / supply-chain). Zero npm deps i selve testen.
// Moenster: tests/frontmatter.test.mjs (les fil, assert struktur). // Moenster: tests/frontmatter.test.mjs (les fil, assert struktur).
@ -24,10 +24,10 @@ function readPkg() {
return JSON.parse(readFileSync(PKG, 'utf8')); return JSON.parse(readFileSync(PKG, 'utf8'));
} }
test('package.json: type module + version 1.10.0 (minor)', () => { test('package.json: type module + version 1.8.2 (patch-lane)', () => {
const pkg = readPkg(); const pkg = readPkg();
assert.equal(pkg.type, 'module'); assert.equal(pkg.type, 'module');
assert.equal(pkg.version, '1.10.0'); assert.equal(pkg.version, '1.8.2');
}); });
test('package.json: engines.node-gulv satt (unpdf krever >= 22)', () => { test('package.json: engines.node-gulv satt (unpdf krever >= 22)', () => {

View file

@ -1,175 +0,0 @@
// syklus-data.test.mjs
// D5 (fase D, steg 7-8): datamodellen for styringssloeyfa. Testene deles i to lag
// som landet i hver sin commit:
// (A) FIXTURE-KONTRAKTEN (steg 7) -- asserterer paa fixture-INNHOLD, ikke paa
// lib-kode. Disse er roede foer fixturen utvides og groenne etterpaa, slik at
// steg 8 har data aa bygge mot (RED-baseline-disiplin, jf. innboks-ingest).
// (B) LESER + SCOREBEREGNING (steg 8) -- lib/syklus-data.mjs.
//
// Beslutning B-1: flate `krN_`-noekler i okr-*.md sin frontmatter. Verifisert kjoert,
// ikke bare lest: `^\s*type:` i lib/frontmatter.mjs:29 matcher ikke `kr1_type:`, saa
// OKF-noekkelen `type` og KR-noekkelen `krN_type` kolliderer ikke.
// Beslutning B-2: score BEREGNES, lagres aldri. target == baseline gir undefined.
//
// Offline, zero-dep, node:test. Test-kilden holdes ASCII-ren (bash 3.2 set -u).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const SYKLUS = 'tests/fixtures/okf-realistic/syklus/T1-2026';
const OKR_FILER = ['okr-trafikksikkerhet.md', 'okr-digitalisering.md'];
const lesFixture = (navn) => readFileSync(join(ROOT, SYKLUS, navn), 'utf8');
// --- (A) Fixture-kontrakten (steg 7) ---
test('(A1) steg 7: begge OKR-fixturene baerer kr1_baseline', () => {
const missing = OKR_FILER.filter((f) => !/^\s*kr1_baseline\s*:/m.test(lesFixture(f)));
assert.deepEqual(
missing,
[],
`fixture-OKR uten kr1_baseline: ${missing.join(', ')}`,
);
});
test('(A2) steg 7: begge KR-typene forekommer i fixturen', () => {
const alle = OKR_FILER.map(lesFixture).join('\n');
assert.match(alle, /^\s*kr\d+_type\s*:\s*committed\s*$/m, 'ingen committed KR i fixturen');
assert.match(
alle,
/^\s*kr\d+_type\s*:\s*aspirational\s*$/m,
'ingen aspirational KR i fixturen',
);
});
// B-2-kanten skal vaere dekket av DATA, ikke bare av enhetstest -- ellers moeter
// generatoren i steg 9-10 den foerste gang i produksjon.
test('(A3) steg 7: fixturen inneholder ett KR-par der target == baseline', () => {
const funn = [];
for (const f of OKR_FILER) {
const doc = lesFixture(f);
for (const m of doc.matchAll(/^\s*kr(\d+)_baseline\s*:\s*(\S+)\s*$/gm)) {
const t = new RegExp(`^\\s*kr${m[1]}_target\\s*:\\s*(\\S+)\\s*$`, 'm').exec(doc);
if (t && t[1] === m[2]) funn.push(`${f}:kr${m[1]}`);
}
}
assert.ok(
funn.length >= 1,
'ingen KR med target == baseline -- udefinert-score-kanten (B-2) er udekket av data',
);
});
// OKF-laget skal vaere uroert av KR-utvidelsen: `type:` er fortsatt OKR-typen, ikke
// en KR-type. Dette er antakelse 2 i planen, her bundet til faktisk fixture-innhold.
test('(A4) steg 7: krN_type forstyrrer ikke OKF-noekkelen type', async () => {
const { parseFrontmatter } = await import('../lib/frontmatter.mjs');
for (const f of OKR_FILER) {
const fm = parseFrontmatter(lesFixture(f));
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);
});
// somTall brukte Number.isNaN, som IKKE fanger Infinity/-Infinity -- kun NaN.
// target: 1e400 -> Number(1e400) === Infinity, en stille score 0 fremfor kast.
test('(B11) beregnScore kaster ved Infinity-felt', () => {
assert.throws(() => beregnScore({ baseline: 1, target: 1e400, naa: 2 }), /target/);
assert.throws(() => beregnScore({ baseline: -1e400, target: 10, naa: 2 }), /baseline/);
});
test('(B10) lesSyklus kaster naar katalogen ikke finnes', () => {
assert.throws(() => lesSyklus(join(ROOT, 'tests/fixtures/finnes-ikke')), /finnes ikke/i);
});

View file

@ -1,654 +0,0 @@
// syklus-rapport.test.mjs
// D5 steg 9: tertialrapport-generatoren. Tester tre bindende krav:
// (1) committed og aspirational rapporteres HVER FOR SEG (okr-framework.md:557)
// (2) de tre formateringsinvariantene (okr-offentlig-governance.md:148-156)
// (3) hook-kontrakten mot hooks/scripts/coaching-hook.mjs:86-92
//
// Offline, zero-dep, node:test. Test-kilden holdes ASCII-ren (bash 3.2 set -u).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { lesSyklus } from '../lib/syklus-data.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');
const NAA = '2026-05-01T10:00:00.000Z';
const rapport = () => tertialrapport(lesSyklus(SYKLUS_DIR), { naa: NAA });
// --- Invariantene gjelder HVER rapportform, ikke bare tertialrapporten ---
//
// Steg 9 hengte alle forbudene paa tertialrapporten. Da ville steg 11-12 levert
// to nye former med NULL dekning paa nettopp de vaktene som baerer doktrinen --
// og (5c), som forbyr generatoren aa utlede confidence, ville dekket 1 av 3
// former. Formene registreres her; en ny form arver hele invariant-settet ved aa
// staa i listen. Radform-vaktene (5a/5b) staar bevisst UTENFOR: de handler om
// tabellen coaching-hook.mjs teller, og aarsrapport/etatsstyring er ikke
// forpliktet til den tabellformen.
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 });
// --- Hook-kontrakten: predikatene LESES fra hooken, aldri kopieres ---
//
// En kopiert regex beviser at testen er enig med seg selv. Ved aa hente
// literalene ut av coaching-hook.mjs' egen kilde blir en fremtidig endring i
// hooken en ROED suite her, i stedet for en stille inkompatibilitet.
// Hooken er i forbidden_paths for dette steget -- forbudt aa ENDRE, ikke aa LESE.
// Hentes ved NAVN, ikke ved posisjon. Hooken ble seksjons-bevisst i S61 og har
// flere predikater enn de to rad-predikatene; «det foerste og det andre
// literalet i fila» ville da vaert en vakt som stille plukket feil regex.
function hookPredikater() {
const src = readFileSync(join(ROOT, 'hooks/scripts/coaching-hook.mjs'), 'utf8');
const hent = (navn) => {
const m = src.match(new RegExp(`^const ${navn} = (/(?:[^/\\\\\\n]|\\\\.)+/[gimsuy]*);`, 'm'));
assert.ok(m, `parser-sanity: fant ikke konstanten ${navn} i coaching-hook.mjs`);
const lit = m[1];
const siste = lit.lastIndexOf('/');
return new RegExp(lit.slice(1, siste), lit.slice(siste + 1));
};
return { radForm: hent('ER_KR_RAD'), fareOrd: hent('FARE_ORD') };
}
const erKrRad = (linje) => /^\s*\|/.test(linje) && !/^\s*\|[\s|:-]+\|\s*$/.test(linje)
&& !/\|\s*KR\s*\|/.test(linje);
test('(1) committed og aspirational rapporteres hver for seg', () => {
const md = rapport();
assert.match(md, /^##\s+Committed/m, 'mangler committed-seksjon');
assert.match(md, /^##\s+Aspirational/m, 'mangler aspirational-seksjon');
});
// okr-framework.md:557 -- aa aggregere de to typene til ett tall blander to
// maalestokker og gjoer resultatet tvetydig.
for (const [navn, bygger] of FORMER) {
test(`(1b/${navn}) rapportformen emitterer ingen felles samlet score`, () => {
const offenders = byggFor(bygger).split('\n').filter((l) => /samlet\s+score/i.test(l));
assert.deepEqual(offenders, [], `felles aggregat funnet:\n${offenders.join('\n')}`);
});
}
test('(2a) invariant 1: typen er merket i rapporteringen', () => {
const md = rapport();
assert.match(md, /committed/i);
assert.match(md, /aspirational/i);
});
test('(2b) invariant 2: committed rapporteres binaert mot kravet', () => {
const md = rapport();
// Avvik-kolonnen finnes, og tunnel-KR2 (17 av 20) er et avvik selv om
// scoren er 0.63 -- et krav er naadd eller ikke.
assert.match(md, /\|\s*Avvik\s*\|/, 'mangler Avvik-kolonne for committed');
const rad = md.split('\n').find((l) => l.includes('oppgradert sikkerhetsutrustning'));
assert.ok(rad, 'fant ikke committed-KR-raden');
assert.match(rad, /\|\s*Ja\s*\|/, 'committed under target maa merkes som avvik');
});
for (const [navn, bygger] of FORMER) {
test(`(2c/${navn}) invariant 3: skalaen forklares, og score konverteres ikke til prosent`, () => {
const md = byggFor(bygger);
assert.match(md, /skala/i, 'score-skalaen maa forklares eksplisitt');
const prosentPaaScore = md.split('\n').filter((l) => /score[^|\n]*%|%[^|\n]*score/i.test(l));
assert.deepEqual(prosentPaaScore, [], 'score skal ikke presenteres som prosent maaloppnaaelse');
});
// B-2-kanten moetes her av DATA (fixturens kr1 har target == baseline).
// Uten en eksplisitt literal ville cellen blitt "undefined" eller kastet.
test(`(3/${navn}) udefinert score rendres som en literal, ikke som undefined eller NaN`, () => {
const md = byggFor(bygger);
assert.match(md, /udefinert/, 'target == baseline maa rendres som "udefinert"');
assert.doesNotMatch(md, /undefined|NaN/, 'raa JS-verdier lekket til rapporten');
});
test(`(4/${navn}) determinisme: to kjoeringer med samme naa gir byte-identisk streng`, () => {
assert.equal(byggFor(bygger), byggFor(bygger));
});
}
test('(4b) klokke-soemmen: OKR_NOW styrer tidsstempelet', (t) => {
const forrige = process.env.OKR_NOW;
t.after(() => {
if (forrige === undefined) delete process.env.OKR_NOW;
else process.env.OKR_NOW = forrige;
});
process.env.OKR_NOW = '2026-06-15T08:00:00.000Z';
const md = tertialrapport(lesSyklus(SYKLUS_DIR));
assert.match(md, /2026-06-15T08:00:00\.000Z/);
});
// --- (5) Hook-kontrakten, tredelt ---
test('(5a) hook-kontrakt: hver KR-rad matcher hookens radform', () => {
const { radForm } = hookPredikater();
const rader = rapport().split('\n').filter(erKrRad);
assert.ok(rader.length >= 4, `parser-sanity: fant ${rader.length} KR-rader`);
const avvikende = rader.filter((l) => !radForm.test(l));
assert.deepEqual(avvikende, [], `rader hooken ikke kan telle:\n${avvikende.join('\n')}`);
});
test('(5b) hook-kontrakt: en kanonisk etikett i Confidence-cellen goer raden tellbar', () => {
const { radForm, fareOrd } = hookPredikater();
const rad = rapport().split('\n').filter(erKrRad)[0];
// Confidence er siste kolonne og staar tom by design -- fyll den slik
// /okr:sporing ville gjort det.
const fylt = rad.replace(/\|\s*\|\s*$/, '| At Risk |');
assert.notEqual(fylt, rad, 'fant ingen tom Confidence-celle aa fylle');
assert.ok(radForm.test(fylt) && fareOrd.test(fylt), 'hooken ville ikke telt den fylte raden');
});
// LOAD-BEARING. Generatoren eier aritmetikk, aldri confidence:
// okr-framework.md:563 gjoer confidence-tabellen til eneste sannhetskilde, og
// okr-calculator.md:249 avviser mekanisk utledning eksplisitt ("ETT innspill
// ... ikke som en mekanisk regel"). En fremtidig sesjon som "hjelpsomt" utleder
// et trafikklys fra score ville oppfunnet en terskel doktrinen forbyr.
for (const [navn, bygger] of FORMER) {
test(`(5c/${navn}) generatoren emitterer aldri en confidence-etikett selv`, () => {
const { fareOrd } = hookPredikater();
const offenders = byggFor(bygger).split('\n').filter((l) => fareOrd.test(l));
assert.deepEqual(
offenders,
[],
`generatoren utledet confidence mekanisk -- forbudt av okr-calculator.md:249:\n${offenders.join('\n')}`,
);
});
}
test('(6) tertialrapport kaster ved tom eller ufullstendig syklus', () => {
assert.throws(() => tertialrapport({ id: 'T1-2026', okrer: [] }), /ingen OKR/i);
assert.throws(() => tertialrapport(undefined), /syklus/i);
});
// --- (8) Aarsrapport del III (steg 11) ---
//
// Ankeret er okr-offentlig-governance.md:125-133: del III «Aarets aktiviteter og
// resultater» er hovedplassen for OKR i den statlige aarsrapporten.
//
// Antipatternet generatoren maa unngaa er kildebelagt i primaerkilden:
// Riksrevisjonen (2020), «Undersoekelse av etats- og virksomhetsstyringen av NIBIO»,
// del av Dokument 1 (2020-2021) s. 107 -- framstillingen «synliggjoer i mindre grad
// NIBIOs analyser av maaloppnaaelsen og framstaar som et oeyeblikksbilde av NIBIOs
// aktiviteter og resultater». En generator som bare dumper tabeller PRODUSERER det
// oeyeblikksbildet. Derfor RESERVERER den plassen til vurderingen -- og fyller den
// aldri selv, siden en vurdering av maaloppnaaelse krever skjoenn.
const delIII = (opts = {}) => arsrapportDelIII(lesSyklus(SYKLUS_DIR), { naa: NAA, ...opts });
// Seksjonskropp: linjene mellom en overskrift og neste overskrift paa samme
// eller hoeyere nivaa.
function seksjonsKropp(md, overskrift) {
const linjer = md.split('\n');
const start = linjer.findIndex((l) => l.trim() === overskrift);
if (start === -1) return null;
const niva = overskrift.match(/^#+/)[0].length;
const kropp = [];
for (const linje of linjer.slice(start + 1)) {
const m = /^(#+)\s/.exec(linje);
if (m && m[1].length <= niva) break;
kropp.push(linje);
}
return kropp.join('\n').trim();
}
test('(8a) del III bygger paa aarsrapportens eget anker og en seksjon per Objective', () => {
const md = delIII();
assert.match(md, /[Aa]arets aktiviteter og resultater/, 'del III-ankeret mangler');
const objectives = md.split('\n').filter((l) => /^##\s+(?!Flerarig|Aspirational|Avvik)/.test(l));
assert.equal(objectives.length, 2, `forventet 2 Objective-seksjoner, fant ${objectives.length}`);
assert.match(md, /Trafikksikkerhet og tunnelsikkerhet/);
assert.match(md, /Digitalisering av tjenester/);
});
test('(8b) hvert Objective reserverer et vurderingsfelt generatoren ikke fyller', () => {
const md = delIII();
const felt = md.split('\n').filter((l) => /^###\s+Vurdering av maaloppnaaelse\s*$/.test(l));
assert.equal(felt.length, 2, `forventet ett vurderingsfelt per Objective, fant ${felt.length}`);
const kropp = seksjonsKropp(md, '### Vurdering av maaloppnaaelse');
assert.ok(kropp, 'fant ingen kropp under vurderingsfeltet');
// Plassholderen er merket som plassholder, ikke skrevet som en vurdering.
assert.match(kropp, /^\[/, 'vurderingsfeltet maa staa som en merket plassholder');
assert.match(kropp, /\]$/, 'plassholderen maa vaere avgrenset');
assert.match(kropp, /[Ff]ylles ut av virksomheten/, 'plassholderen maa si hvem som fyller den');
// En generator som «hjelpsomt» skrev inn tall eller en konklusjon ville
// gjort seg til vurderingsinstans. Ingen tall i feltet = ingen vurdering.
assert.doesNotMatch(kropp, /\d/, `generatoren fylte vurderingsfeltet med data:\n${kropp}`);
});
test('(8c) flerarig perspektiv kommer fra historikk, og oppfinnes ikke uten data', () => {
const utenHistorikk = delIII();
assert.doesNotMatch(
utenHistorikk,
/^##\s+Flerarig perspektiv/m,
'uten historikk skal seksjonen utelates, ikke staa tom',
);
const md = delIII({
historikk: [
{ tittel: 'Retrospektiv T3-2025', fil: 'retrospektiv-T3-2025.md' },
{ tittel: 'Retrospektiv T2-2025', fil: 'retrospektiv-T2-2025.md' },
],
});
assert.match(md, /^##\s+Flerarig perspektiv/m, 'historikk skal gi et flerarig perspektiv');
const kropp = seksjonsKropp(md, '## Flerarig perspektiv');
assert.match(kropp, /Retrospektiv T3-2025/);
assert.match(kropp, /Retrospektiv T2-2025/);
// DFOe-notat 2026:2 kap. 5.3.3 (s. 61): det ettaarige omtales i liten grad i
// flerarig perspektiv. Generatoren peker paa materialet -- den sammenligner ikke
// sykluser den ikke har tall fra.
assert.doesNotMatch(kropp, /\d\.\d\d/, 'generatoren skal ikke regne trend paa tvers av sykluser');
});
test('(8d) arsrapportDelIII kaster ved tom eller ufullstendig syklus', () => {
assert.throws(() => arsrapportDelIII({ id: 'T1-2026', okrer: [] }), /ingen OKR/i);
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);
});
// --- (10) Sandbagging-vakten (steg 13) ---
//
// Briefens navngitte suksesskriterium: en aspirational-OKR med lav score
// presenteres ALDRI som avvik. Regelen staar i okr-offentlig-governance.md:148-156;
// her far den mekanisk handhevelse i alle tre rapportformene.
//
// Vakten er bygget i tre ledd, og rekkefolgen er ikke tilfeldig:
// (10a) POSITIV -- et committed KR under kravet STAAR i avviks-seksjonen.
// Uten dette leddet ville (10b) bestaatt trivielt paa en
// seksjon som ikke finnes eller er tom, og vakten vaert prosa.
// (10b) NEGATIV -- fixturens aspirational-KR med lav score staar der ikke.
// (10c) MUTASJON -- samme KR, flyttet til committed, DUKKER OPP. Det beviser at
// filteret henger paa TYPEN og ikke paa navnet, og at
// seksjonslokatoren i (10b) faktisk fant noe aa lete i.
//
// De tre formene har strukturelt ulike avviks-regioner, saa lokatoren ankrer paa
// overskriftsformen (`## Avvik...`), som er felles.
function avvikKropp(md) {
const overskrift = md.split('\n').find((l) => /^##\s+Avvik/.test(l));
assert.ok(overskrift, 'rapportformen har ingen avviks-seksjon aa filtrere');
const kropp = seksjonsKropp(md, overskrift.trim());
assert.ok(kropp && kropp.length > 0, 'avviks-seksjonen er tom');
return kropp;
}
// Fixturens KR-er, med de rollene steg 7 ga dem.
const COMMITTED_UNDER_KRAV = 'Tunneler med oppgradert sikkerhetsutrustning';
const ASPIRATIONAL_LAV_SCORE = ['Andel digitale soeknader', 'Gjennomsnittlig saksbehandlingstid'];
for (const [navn, bygger] of FORMER) {
test(`(10a/${navn}) committed KR under kravet STAAR i avviks-seksjonen`, () => {
const kropp = avvikKropp(byggFor(bygger));
assert.match(
kropp,
new RegExp(COMMITTED_UNDER_KRAV),
`committed KR under kravet mangler i avviks-seksjonen:\n${kropp}`,
);
});
test(`(10b/${navn}) aspirational KR med lav score staar IKKE i avviks-seksjonen`, () => {
const kropp = avvikKropp(byggFor(bygger));
for (const kr of ASPIRATIONAL_LAV_SCORE) {
assert.doesNotMatch(
kropp,
new RegExp(kr),
`aspirational KR presentert som avvik -- sandbagging-vakten er brutt:\n${kropp}`,
);
}
});
test(`(10c/${navn}) mutasjon: samme KR som committed ER et avvik`, () => {
const syklus = lesSyklus(SYKLUS_DIR);
const mutert = {
...syklus,
okrer: syklus.okrer.map((okr) => ({
...okr,
krer: okr.krer.map((kr) => ({ ...kr, type: 'committed' })),
})),
};
const kropp = avvikKropp(bygger(mutert, { naa: NAA }));
for (const kr of ASPIRATIONAL_LAV_SCORE) {
assert.match(
kropp,
new RegExp(kr),
`filteret henger ikke paa typen -- KR-et uteble ogsaa som committed:\n${kropp}`,
);
}
});
// Uten den kausale begrunnelsen er vakten en norm mottakeren kan overproeve.
// Bogsnes' poeng (okr-framework.md:158-161) er at re-bundling av
// aspirational-scoren i rapporteringen henter sandbaggingen tilbake
// strukturelt -- det argumentet holder for en Riksrevisjon-leser.
test(`(10d/${navn}) rapporten baerer forventningen til aspirational, ikke bare tallet`, () => {
const md = byggFor(bygger);
assert.match(md, /0\.7/, 'forventningen om ~0.7 maa staa eksplisitt');
assert.match(md, /sandbagging/i, 'begrunnelsen for at lav aspirational-score ikke er avvik mangler');
});
}
// Avviks-seksjonen viser til «forventningen til aspirational under». Den
// seksjonen finnes bare naar syklusen HAR aspirational-KR. En syklus med bare
// committed -- fullt lovlig, og typisk for en ren etterlevelses-syklus -- ville
// faatt et styringsdokument som peker paa en seksjon som ikke er der. Fixturen
// kan ikke fange det; den har begge typene.
const KUN_COMMITTED = {
id: 'T-kun-committed',
okrer: [{
fil: 'okr-etterlevelse.md',
tittel: 'Etterlevelse',
krer: [{ n: 1, navn: 'Palagte tilsyn gjennomfoert (antall)', baseline: 0, target: 12, naa: 9, type: 'committed' }],
}],
};
for (const [navn, bygger] of FORMER) {
test(`(10f/${navn}) ingen henvisning til en aspirational-seksjon som ikke finnes`, () => {
const md = bygger(KUN_COMMITTED, { naa: NAA });
const viser = /forventningen til aspirational under/i.test(md);
const finnes = /^##+\s+Aspirational/m.test(md);
assert.equal(
viser && !finnes,
false,
'avviks-seksjonen viser til en aspirational-seksjon rapporten ikke har',
);
});
}
// Generatoren handhever regelen; governance-fila baerer begrunnelsen. Staar
// begrunnelsen bare i koden, kan en leser overproeve vakten uten aa moete
// argumentet -- og det er nettopp en Riksrevisjon-vendt leser som vil sporre.
test('(10e) governance-fila begrunner regelen kausalt, ikke bare normativt', () => {
const doc = readFileSync(
join(ROOT, 'skills/okr-offentlig-sektor/references/okr-offentlig-governance.md'),
'utf8',
);
assert.match(doc, /re-bundl/i, 'den kausale mekanismen (re-bundling) mangler');
assert.match(doc, /okr-framework\.md/, 'begrunnelsen maa peke til kanon, ikke duplisere den');
});
// --- (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
// exit-koden -- fordi exit-koden ER kontrakten kommandofila mapper til norsk
// brukertekst. En atferdstest alene ville ikke fanget at 1 og 2 byttet plass.
// Moenster: tests/innboks-ingest.test.mjs (execFileSync for exit-kode).
const CLI = join(ROOT, 'scripts/syklus-rapport.mjs');
async function kjoerCli(args, opts = {}) {
const { execFileSync } = await import('node:child_process');
try {
const stdout = execFileSync(process.execPath, [CLI, ...args], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, OKR_NOW: NAA, ...(opts.env ?? {}) },
});
return { code: 0, stdout };
} catch (e) {
return { code: e.status, stdout: e.stdout ?? '', stderr: e.stderr ?? '' };
}
}
// Egen kopi av syklusen per test som skriver -- CLI-en persisterer, og en test
// skal aldri skitne til fixturen den delte med de andre testene.
async function midlertidigSyklus(filer) {
const { mkdtempSync, writeFileSync, mkdirSync, copyFileSync, readdirSync } = await import('node:fs');
const { tmpdir } = await import('node:os');
const rot = mkdtempSync(join(tmpdir(), 'okr-rapport-'));
const dir = join(rot, 'T1-2026');
mkdirSync(dir);
if (filer) {
for (const [navn, innhold] of Object.entries(filer)) writeFileSync(join(dir, navn), innhold);
} else {
for (const f of readdirSync(SYKLUS_DIR)) copyFileSync(join(SYKLUS_DIR, f), join(dir, f));
}
return { rot, dir };
}
test('(7a) CLI: exit 2 ved feil aritet', async () => {
assert.equal((await kjoerCli([])).code, 2);
assert.equal((await kjoerCli([SYKLUS_DIR])).code, 2);
});
// 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}`);
}
});
test('(7c) CLI: exit 2 naar syklus-katalogen ikke finnes', async () => {
assert.equal((await kjoerCli([join(ROOT, 'finnes-ikke'), 'tertial'])).code, 2);
});
test('(7d) CLI: exit 1 ved ufullstendig syklusdata', async (t) => {
const { rmSync } = await import('node:fs');
const { rot, dir } = await midlertidigSyklus({
'okr-ufullstendig.md': ['---', 'type: OKR', 'title: Ufullstendig', 'kr1_navn: Mangler tall', 'kr1_baseline: 3', '---', '# Ufullstendig', ''].join('\n'),
});
t.after(() => rmSync(rot, { recursive: true, force: true }));
const r = await kjoerCli([dir, 'tertial']);
assert.equal(r.code, 1, 'domenefeil skal gi exit 1, ikke 2');
assert.match(r.stderr, /kr1/i);
});
test('(7e) CLI: exit 0 og fil paa disk ved gyldig input', async (t) => {
const { rmSync, existsSync, readFileSync: les } = await import('node:fs');
const { rot, dir } = await midlertidigSyklus();
t.after(() => rmSync(rot, { recursive: true, force: true }));
const r = await kjoerCli([dir, 'tertial']);
assert.equal(r.code, 0, `forventet exit 0, fikk ${r.code}: ${r.stderr ?? ''}`);
const ut = join(dir, 'rapport-tertial.md');
assert.ok(existsSync(ut), 'rapporten ble ikke skrevet');
const innhold = les(ut, 'utf8');
assert.match(innhold, /^---\ntype: Status\n/, 'skrevet fil mangler OKF-frontmatter');
assert.match(innhold, /## Committed Key Results/);
});
// --- (7g/7h) historikk-stien er avledet, og avledningen er en determinisme-risiko ---
//
// historikk/ er SOESKEN av syklus/, altsaa <syklusDir>/../../historikk. Regnet ut
// blindt ville en syklus-katalog utenfor et OKR-tre (som testenes tmp-kopi) pekt paa
// en vilkaarlig katalog paa maskinen -- og rapporten ville variert med hva som
// tilfeldigvis laa der. Derfor avledes historikk KUN naar foreldrekatalogen faktisk
// heter `syklus`. Begge grenene testes: uten tre (7g) og med tre (7h).
test('(7g) CLI: arsrapport skrives, og uten OKR-tre utledes ingen historikk', 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, 'arsrapport']);
assert.equal(r.code, 0, `forventet exit 0, fikk ${r.code}: ${r.stderr ?? ''}`);
const innhold = les(join(dir, 'rapport-arsrapport.md'), 'utf8');
assert.match(innhold, /^---\ntype: Status\n/, 'skrevet fil mangler OKF-frontmatter');
assert.match(innhold, /[Aa]arets aktiviteter og resultater/);
assert.doesNotMatch(innhold, /^##\s+Flerarig perspektiv/m, 'historikk ble utledet utenfor et OKR-tre');
});
test('(7h) CLI: i et ekte OKR-tre leses historikk-katalogen, ikke index.md', async (t) => {
const { rmSync, mkdirSync, mkdtempSync, writeFileSync, copyFileSync, readdirSync, readFileSync: les } = await import('node:fs');
const { tmpdir } = await import('node:os');
const rot = mkdtempSync(join(tmpdir(), 'okr-tre-'));
const dir = join(rot, 'syklus', 'T1-2026');
const hist = join(rot, 'historikk');
mkdirSync(dir, { recursive: true });
mkdirSync(hist);
for (const f of readdirSync(SYKLUS_DIR)) copyFileSync(join(SYKLUS_DIR, f), join(dir, f));
writeFileSync(join(hist, 'retrospektiv-T3-2025.md'), ['---', 'type: Retrospektiv', 'title: Retrospektiv T3-2025', '---', '# Retrospektiv T3-2025', ''].join('\n'));
// index.md peker paa en fil som ikke finnes (samme dangling-kant som fixturen).
// Leser generatoren index.md i stedet for katalogen, dukker den opp i rapporten.
writeFileSync(join(hist, 'index.md'), ['# Historikk', '', '* [Retrospektiv T2-2025](retrospektiv-T2-2025.md) - finnes ikke', ''].join('\n'));
t.after(() => rmSync(rot, { recursive: true, force: true }));
assert.equal((await kjoerCli([dir, 'arsrapport'])).code, 0);
const innhold = les(join(dir, 'rapport-arsrapport.md'), 'utf8');
assert.match(innhold, /^##\s+Flerarig perspektiv/m, 'historikk-katalogen ble ikke lest');
assert.match(innhold, /Retrospektiv T3-2025/);
assert.doesNotMatch(innhold, /T2-2025/, 'en dangling index.md-lenke ble rapportert som materiale');
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');
});
// Fixturens historikk er FLAT prosa-filer. Den EKTE formen kommer fra
// /okr:oppsett arkiver (commands/oppsett.md:514-518), som lager BEGGE deler:
// katalogen `historikk/[id]/` med syklusens filer, OG den flate
// `historikk/[id]-resultater.md` med sammendraget. Sammendraget er riktig
// granularitet for et flerarig perspektiv; katalogen er raamaterialet under.
// Uten denne testen ville fixtur, kode og (7h) vaert tre artefakter som er enige
// med hverandre -- og ingen av dem den kommandoen som faktisk skriver treet.
test('(7j) historikk leses fra arkiveringens EKTE form: sammendragsfil, ikke katalog', async (t) => {
const { rmSync, mkdirSync, mkdtempSync, writeFileSync, copyFileSync, readdirSync, readFileSync: les } = await import('node:fs');
const { tmpdir } = await import('node:os');
const rot = mkdtempSync(join(tmpdir(), 'okr-arkiv-'));
const dir = join(rot, 'syklus', 'T1-2026');
const hist = join(rot, 'historikk');
mkdirSync(dir, { recursive: true });
mkdirSync(join(hist, 'T3-2025'), { recursive: true });
for (const f of readdirSync(SYKLUS_DIR)) copyFileSync(join(SYKLUS_DIR, f), join(dir, f));
// Kopien av syklusen ligger i katalogen; sammendraget flatt ved siden av.
writeFileSync(join(hist, 'T3-2025', 'okr-gammel.md'), ['---', 'type: OKR', 'title: Gammel OKR', '---', ''].join('\n'));
writeFileSync(join(hist, 'T3-2025-resultater.md'), ['---', 'type: Retrospektiv', 'title: Resultater T3-2025', '---', '# Resultater T3-2025', ''].join('\n'));
t.after(() => rmSync(rot, { recursive: true, force: true }));
assert.equal((await kjoerCli([dir, 'arsrapport'])).code, 0);
const kropp = les(join(dir, 'rapport-arsrapport.md'), 'utf8');
assert.match(kropp, /Resultater T3-2025/, 'sammendragsfila fra arkiveringen ble ikke funnet');
// Den arkiverte syklus-katalogen er raamateriale, ikke en post i det flerarige
// perspektivet -- og `[id]/` ender ikke paa .md, saa den filtreres bort som seg selv.
assert.doesNotMatch(kropp, /Gammel OKR/, 'generatoren gikk inn i den arkiverte syklus-katalogen');
});
// 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) => {
const { rmSync, readFileSync: les } = await import('node:fs');
const { rot, dir } = await midlertidigSyklus();
t.after(() => rmSync(rot, { recursive: true, force: true }));
await kjoerCli([dir, 'tertial']);
const foerste = les(join(dir, 'rapport-tertial.md'), 'utf8');
await kjoerCli([dir, 'tertial']);
assert.equal(les(join(dir, 'rapport-tertial.md'), 'utf8'), foerste);
assert.equal(lesSyklus(dir).okrer.length, 2, 'rapporten ble lest som en OKR');
});

View file

@ -1,253 +0,0 @@
// syklus-status.test.mjs
// S60: det uwirede leddet -- generator -> /okr:sporing -> status.md.
//
// Kjeden var beskrevet i prosa og manglet et mekanisk ledd: ingenting foerte en
// generert rad inn i `status.md`, fila coaching-hook.mjs, /okr:moeter og
// /okr:oppsett arkiver alle leser. Operatoerbeslutning (S60): et deterministisk
// merge-steg eier skrivingen -- ALDRI en LLM som skriver av tallene.
//
// Testene binder fire ting:
// (1) aritmetikken re-typas aldri: status-radene er tertialrapportens rader
// med confidence-cellen fylt, tegn for tegn ellers.
// (2) beslutning 9 staar: generatoren PLASSERER en etikett den faar utenfra,
// og OPPFINNER aldri en. Uten input kaster den.
// (3) det lukkede vokabularet (okr-framework.md:564, eneste sannhetskilde).
// (4) kuratert-fil-vern: en haandholdt status.md klobbes aldri (moenster fra
// lib/innboks-write.mjs:88-91).
//
// Offline, zero-dep, node:test. Test-kilden holdes ASCII-ren (bash 3.2 set -u).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { lesSyklus } from '../lib/syklus-data.mjs';
import { statusrapport, tertialrapport } from '../lib/syklus-rapport.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const FIXTURE = join(ROOT, 'tests/fixtures/okf-realistic/syklus/T1-2026');
const CLI = join(ROOT, 'scripts/syklus-status.mjs');
const NAA = '2026-05-01T10:00:00.000Z';
// Fixturen har sin egen status.md (okf-retrieval-testene leser den). Kopier KUN
// okr-*.md til en temp-katalog, saa skrivetestene aldri roerer fixturen.
function lagSyklus(t) {
const dir = mkdtempSync(join(tmpdir(), 'okr-status-'));
t.after(() => rmSync(dir, { recursive: true, force: true }));
for (const fil of readdirSync(FIXTURE)) {
if (fil.startsWith('okr-') && fil.endsWith('.md')) {
copyFileSync(join(FIXTURE, fil), join(dir, fil));
}
}
return dir;
}
const FULL = {
'Alvorlige tunnelhendelser (antall)': 'Off Track',
'Tunneler med oppgradert sikkerhetsutrustning (antall)': 'At Risk',
'Andel digitale soeknader (prosent)': 'At Risk',
'Gjennomsnittlig saksbehandlingstid (dager)': 'On Track',
};
const syklus = () => lesSyklus(FIXTURE);
const bygg = (confidence = FULL, opts = {}) => statusrapport(syklus(), { naa: NAA, confidence, ...opts });
const kjor = (dir, stdin, env = {}) => spawnSync(
process.execPath,
[CLI, dir],
{ input: typeof stdin === 'string' ? stdin : JSON.stringify(stdin), encoding: 'utf8', env: { ...process.env, OKR_NOW: NAA, ...env } },
);
// En KR-rad er en tabellrad som verken er hode eller skillelinje.
const erKrRad = (linje) => /^\s*\|/.test(linje) && !/^\s*\|[\s|:-]+\|\s*$/.test(linje)
&& !/\|\s*KR\s*\|/.test(linje);
const krRader = (md) => md.split('\n').filter(erKrRad);
// Raden uten siste celle -- alt generatoren eier.
const utenConfidence = (rad) => rad.replace(/\|[^|]*\|\s*$/, '|');
// Hookens EGNE predikater, lest fra kilden. En kopiert regex ville bevist at
// testen er enig med seg selv (moenster: syklus-rapport.test.mjs).
// Hentes ved NAVN, ikke ved posisjon (se samme helper i syklus-rapport.test.mjs).
function hookPredikater() {
const src = readFileSync(join(ROOT, 'hooks/scripts/coaching-hook.mjs'), 'utf8');
const hent = (navn) => {
const m = src.match(new RegExp(`^const ${navn} = (/(?:[^/\\\\\\n]|\\\\.)+/[gimsuy]*);`, 'm'));
assert.ok(m, `parser-sanity: fant ikke konstanten ${navn} i coaching-hook.mjs`);
const lit = m[1];
const siste = lit.lastIndexOf('/');
return new RegExp(lit.slice(1, siste), lit.slice(siste + 1));
};
return { radForm: hent('ER_KR_RAD'), fareOrd: hent('FARE_ORD') };
}
// --- (1) LOAD-BEARING: aritmetikken kommer fra generatoren, aldri fra input ---
//
// Dette er hele grunnen til at leddet er et script og ikke en instruksjon til en
// LLM: tallene i status.md skal vaere de SAMME bytene generatoren beregnet.
test('(1) status-radene er tertialrapportens rader med bare confidence-cellen endret', () => {
const tertial = krRader(tertialrapport(syklus(), { naa: NAA })).map(utenConfidence);
const status = krRader(bygg()).map(utenConfidence);
assert.ok(tertial.length >= 4, `parser-sanity: fant ${tertial.length} KR-rader`);
assert.deepEqual(status, tertial, 'status.md re-typer aritmetikk i stedet for aa arve den');
});
test('(2) hver KR-rad baerer etiketten som ble gitt for den KR-en', () => {
const md = bygg();
for (const [navn, etikett] of Object.entries(FULL)) {
const rad = krRader(md).find((l) => l.includes(navn));
assert.ok(rad, `fant ingen rad for ${navn}`);
assert.match(rad, new RegExp(`\\|\\s*${etikett}\\s*\\|\\s*$`), `feil etikett i siste celle for ${navn}`);
}
});
// --- (3) Beslutning 9: plasserer, oppfinner aldri ---
test('(3) statusrapport uten confidence-input kaster, den gjetter aldri', () => {
assert.throws(() => statusrapport(syklus(), { naa: NAA }), /confidence/i);
assert.throws(() => statusrapport(syklus(), { naa: NAA, confidence: {} }), /confidence|mangler/i);
});
// Mutasjon M3 (S60) viste at vokab-sjekken OGSAA feller manglende dekning:
// `undefined` er ikke en lovlig etikett. Behovet for en egen dekningssjekk er
// likevel reelt -- den lister ALLE manglende KR i én melding, mens vokab-vakten
// bare rapporterer den foerste som en type-feil. Assertionen paa meldingen er
// det som gjoer den dedikerte vakten dekket for seg; uten den ville M3 gaatt
// groenn og vakten vaert uverifisert.
test('(4) manglende etikett for ett KR kaster, og feilen navngir KR-en', () => {
const delvis = { ...FULL };
delete delvis['Andel digitale soeknader (prosent)'];
assert.throws(() => bygg(delvis), /mangler confidence for 1 KR/);
assert.throws(() => bygg(delvis), /Andel digitale soeknader/);
});
test('(4b) manglende etiketter rapporteres samlet, ikke én om gangen', () => {
assert.throws(() => bygg({ 'Andel digitale soeknader (prosent)': 'At Risk' }), /mangler confidence for 3 KR/);
});
// Et ukjent navn er nesten alltid en skrivefeil. Uten denne vakten ville KR-en
// den var MENT for staatt uten etikett, og feilen blitt oppdaget av en leser.
test('(5) ukjent KR-navn i inputen kaster, og feilen navngir det ukjente navnet', () => {
assert.throws(() => bygg({ ...FULL, 'KR som ikke finnes': 'On Track' }), /KR som ikke finnes/);
});
// --- (6) Lukket vokabular: okr-framework.md:564 er eneste sannhetskilde ---
test('(6) etikett utenfor det kanoniske vokabularet kaster', () => {
for (const ugyldig of ['Groenn', 'on-track', 'OK', 'At risk 70%', '']) {
assert.throws(
() => bygg({ ...FULL, 'Andel digitale soeknader (prosent)': ugyldig }),
/confidence|etikett/i,
`godtok ugyldig etikett: ${JSON.stringify(ugyldig)}`,
);
}
});
test('(6b) de tre kanoniske etikettene godtas', () => {
for (const gyldig of ['On Track', 'At Risk', 'Off Track']) {
assert.doesNotThrow(() => bygg({ ...FULL, 'Andel digitale soeknader (prosent)': gyldig }));
}
});
// --- (7) Hook-kontrakten: fila MAA vaere tellbar for coaching-hook.mjs ---
// Rad-FORMEN er det generatoren skylder hooken: hver KR-rad maa matche hookens
// eget rad-predikat. Hva hooken saa velger aa telle er hookens sak, og maales i
// (7b) ved aa kjoere den.
test('(7) hver KR-rad matcher hookens rad-predikat', () => {
const { radForm, fareOrd } = hookPredikater();
const rader = krRader(bygg());
const utellbare = rader.filter((l) => !radForm.test(l));
assert.deepEqual(utellbare, [], `rader hooken ikke kan telle:\n${utellbare.join('\n')}`);
// Fare-ordene traeffer faktisk fila: uten dette kunne (7b) talt 0 av feil grunn.
assert.ok(rader.some((l) => fareOrd.test(l)), 'ingen rad baerer et fare-ord i det hele tatt');
});
// --- (7b) Patch-lane #1: hookens telling er seksjons-bevisst ---
//
// S60 lot denne testen assertere 3 og skrev i klartekst at tallet kodet hookens
// NAAVAERENDE oppfoersel, ikke den oenskede: en av de tre radene er et
// aspirational KR, og okr-framework.md:605 sier at de fleste stretch-KR BOER
// staa At Risk. Nagget pekte dermed feil vei for en sunn portefoelje.
//
// Assertionen kunne ikke bare endres fra 3 til 2: den SIMULERTE hookens
// aritmetikk med to skrapte rad-predikater, og en seksjons-bevisst regel er
// ikke uttrykkbar per rad. Den ekte kontrakten maales derfor ende-til-ende --
// generatorens fil, paa disk, lest av hooken selv.
//
// FULL gir: committed Off Track + committed At Risk (teller) og aspirational
// At Risk + aspirational On Track (teller ikke) = 2.
test('(7b) hooken teller committed i fare, men ikke et aspirational KR paa At Risk', (t) => {
const work = mkdtempSync(join(tmpdir(), 'okr-status-hook-'));
t.after(() => rmSync(work, { recursive: true, force: true }));
const syklusDir = join(work, '.claude', 'okr', 'syklus', 'T1-2026');
mkdirSync(syklusDir, { recursive: true });
writeFileSync(join(work, '.claude', 'okr.local.md'), '---\nid: "T1-2026"\n---\n');
writeFileSync(join(syklusDir, 'status.md'), bygg());
// Uke ~8 av T1 (jan-apr) -> midtveis-fasen, den ene som rapporterer telleren.
const r = spawnSync(process.execPath, [join(ROOT, 'hooks/scripts/coaching-hook.mjs')], {
cwd: work,
encoding: 'utf8',
env: { ...process.env, OKR_NOW: '2026-02-20' },
});
assert.equal(r.status, 0, `hooken feilet: ${r.stderr}`);
assert.match(
r.stdout,
/OBS: 2 KR er merket/,
'aspirational At Risk skal ikke telle -- kanon sier stretch-KR boer staa der',
);
});
// --- (8-12) CLI-kontrakten ---
test('(8) CLI skriver status.md som et OKF-konsept med provenans', (t) => {
const dir = lagSyklus(t);
const r = kjor(dir, FULL);
assert.equal(r.status, 0, `exit ${r.status}: ${r.stderr}`);
const md = readFileSync(join(dir, 'status.md'), 'utf8');
// type: Status -- ellers feller okf-check hele roten.
assert.match(md, /^type:\s*Status$/m, 'mangler OKF type: Status');
// Provenansen er det kuratert-fil-vernet leser.
assert.match(md, /^kilde:\s*sporing$/m, 'mangler provenans-noekkel');
assert.match(md, new RegExp(`^timestamp:.*${NAA}`, 'm'), 'klokke-soemmen OKR_NOW er ikke fulgt');
assert.equal(krRader(md).length, 4, 'feil antall KR-rader i skrevet fil');
});
test('(9) determinisme: to kjoeringer med samme OKR_NOW gir byte-identisk fil', (t) => {
const dir = lagSyklus(t);
assert.equal(kjor(dir, FULL).status, 0);
const foerste = readFileSync(join(dir, 'status.md'), 'utf8');
assert.equal(kjor(dir, FULL).status, 0);
assert.equal(readFileSync(join(dir, 'status.md'), 'utf8'), foerste);
});
// Moenster fra lib/innboks-write.mjs:88-91: en eksisterende fil UTEN provenans
// er haandkuratert, og et stille datatap er verre enn en avvist kjoering.
test('(10) kuratert status.md avvises, og fila staar uendret', (t) => {
const dir = lagSyklus(t);
const haandholdt = '---\ntype: Status\n---\n\n# Skrevet for haand\n';
writeFileSync(join(dir, 'status.md'), haandholdt, 'utf8');
const r = kjor(dir, FULL);
assert.equal(r.status, 1, 'kuratert fil maa gi domenefeil');
assert.match(r.stderr, /kuratert|haandholdt|overskriv/i);
assert.equal(readFileSync(join(dir, 'status.md'), 'utf8'), haandholdt, 'fila ble endret likevel');
});
test('(11) egen tidligere status.md overskrives', (t) => {
const dir = lagSyklus(t);
assert.equal(kjor(dir, FULL).status, 0);
const endret = { ...FULL, 'Gjennomsnittlig saksbehandlingstid (dager)': 'Off Track' };
const r = kjor(dir, endret);
assert.equal(r.status, 0, `exit ${r.status}: ${r.stderr}`);
const rad = krRader(readFileSync(join(dir, 'status.md'), 'utf8'))
.find((l) => l.includes('Gjennomsnittlig saksbehandlingstid'));
assert.match(rad, /\|\s*Off Track\s*\|\s*$/);
});
test('(12) exit-koder: bruksfeil 2, domenefeil 1', (t) => {
const dir = lagSyklus(t);
assert.equal(spawnSync(process.execPath, [CLI], { input: '{}', encoding: 'utf8' }).status, 2, 'manglende argument');
assert.equal(kjor(join(dir, 'finnes-ikke'), FULL).status, 2, 'katalog finnes ikke');
assert.equal(kjor(dir, 'ikke json').status, 2, 'ugyldig JSON er bruksfeil');
assert.equal(kjor(dir, { ...FULL, 'Andel digitale soeknader (prosent)': 'Groenn' }).status, 1, 'ugyldig etikett er domenefeil');
});