Compare commits
No commits in common. "main" and "v1.9.0" have entirely different histories.
28 changed files with 296 additions and 1583 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "okr",
|
||||
"version": "1.10.0",
|
||||
"version": "1.9.0",
|
||||
"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": {
|
||||
"name": "Kjell Tore Guttormsen"
|
||||
|
|
|
|||
18
CHANGELOG.md
18
CHANGELOG.md
|
|
@ -5,24 +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/),
|
||||
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** på `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, D4–D8). Ingen nye npm-avhengigheter; de fire eksisterende er fortsatt exact-pinnet. Suite 197 → 315 cases.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# OKR Offentlig Sektor v1.10.0
|
||||
# OKR Offentlig Sektor v1.9.0
|
||||
|
||||
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:kvalitet` | Quality check OKR against rubric and antipatterns |
|
||||
| `/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:innføring` | OKR adoption planning, resistance handling, maturity |
|
||||
| `/okr:governance` | Tildelingsbrev translation, Riksrevisjon compliance |
|
||||
|
|
@ -72,7 +72,6 @@ Context files carry OKF v0.1 Documents-style frontmatter (conforms to the OKF v0
|
|||
- `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/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.
|
||||
|
||||
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 +88,7 @@ Retrieval is on-demand via the `okr-second-brain-search` skill. The UserPromptSu
|
|||
/okr:skriv ──→ kvalitetssjekker (auto)
|
||||
/okr:kvalitet ──→ kvalitetssjekker
|
||||
/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:governance ──→ styringsrådgiver
|
||||
/okr:innføring ──→ (inline, no agent)
|
||||
|
|
|
|||
392
README.md
392
README.md
|
|
@ -1,291 +1,281 @@
|
|||
# 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
|
||||
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.
|
||||
Turning a strategy document into goals teams actually work toward is the hard
|
||||
part, and it is where most adoptions stall. This plugin is an AI coach for that
|
||||
translation, built for the vocabulary the work already happens in: tertial
|
||||
cycles, tildelingsbrev, mål- og resultatstyring, and the governance chain from
|
||||
Stortingsmelding down to 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.
|
||||
|
||||
*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.*
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Install](#install)
|
||||
- [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
|
||||
|
||||
Bruk `https://`-formen. Klone-knappen i forgeens UI gir en `ssh://`-URL, og
|
||||
`marketplace add` svarer med `Invalid git URL` — en melding som aldri nevner protokollen.
|
||||
Use the `https://` form. The forge UI's clone button hands out an `ssh://` URL,
|
||||
and `marketplace add` answers it with `Invalid git URL` — a message that never
|
||||
mentions the protocol.
|
||||
|
||||
```bash
|
||||
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
|
||||
claude plugin install okr@ktg-plugin-marketplace
|
||||
```
|
||||
|
||||
Eller aktiver den direkte i `~/.claude/settings.json` — en fungerende alternativ vei,
|
||||
ikke en erstatning for de to kommandoene over:
|
||||
Or enable it directly in `~/.claude/settings.json` — a working second path, not
|
||||
a replacement for the two commands above:
|
||||
|
||||
```json
|
||||
{ "enabledPlugins": { "okr@ktg-plugin-marketplace": true } }
|
||||
```
|
||||
|
||||
Binære innboks-formater (`docx`/`pdf`/`eml`) krever i tillegg
|
||||
`npm install --ignore-scripts` i plugin-mappen; `txt`/`md`-ingestion og alle andre
|
||||
kommandoer kjører uten avhengigheter.
|
||||
Binary inbox formats (`docx`/`pdf`/`eml`) additionally need
|
||||
`npm install --ignore-scripts` in the plugin directory; `txt`/`md` ingestion and
|
||||
every other command run dependency-free.
|
||||
|
||||
---
|
||||
|
||||
## Hvorfor denne finnes
|
||||
## Why This Exists
|
||||
|
||||
Hver organisasjon har en strategi. Få lykkes med å omsette den til mål teamene faktisk arbeider mot.
|
||||
Every organization has a strategy. Few manage to turn it into goals that teams actually work toward.
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
- *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?*
|
||||
- *How do we connect our OKR to the goals in our tildelingsbrev?*
|
||||
- *What's a good Key Result vs. just an activity disguised as one?*
|
||||
- *How do we handle it when political priorities shift mid-cycle?*
|
||||
- *Are we actually getting better at this, or just going through the motions?*
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
### Hva gjør den annerledes enn en metodikk-guide?
|
||||
### What makes it different from a methodology guide?
|
||||
|
||||
En metodikk-guide forklarer OKR-teori. Denne pluginen *kjenner organisasjonen din*.
|
||||
A methodology guide explains OKR theory. This plugin *knows your organization*.
|
||||
|
||||
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.
|
||||
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 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.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Hva den gjør
|
||||
## What It Does
|
||||
|
||||
### Fra strategi til OKR
|
||||
### From Strategy to OKR
|
||||
|
||||
Kjerneflyten: ta dine strategiske mål (fra virksomhetsplan, tildelingsbrev, etatsstrategi, eller et annet strategisk dokument) og omsett dem til godt strukturerte OKR.
|
||||
The core workflow: take your strategic goals (from virksomhetsplan, tildelingsbrev, etatsstrategi, or any strategic document) and transform them into well-structured 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
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
Beregner score (0.0–1.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.0–1.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
|
||||
```
|
||||
|
||||
Etter din første fullførte syklus er dette der den egentlige verdien begynner. Leser dine arkiverte sykluser og viser:
|
||||
- **Scoretrender** — blir du bedre? Visualisert med diagrammer.
|
||||
- **Gjentatte antipatterns** — de samme feilene over sykluser blir flagget eksplisitt.
|
||||
- **Alignment-progresjon** — blir teamenes dekning av org-målene bedre?
|
||||
After your first completed cycle, this is where the real value begins. Reads your archived cycles and shows:
|
||||
- **Score trends** — are you improving? Visualized with charts.
|
||||
- **Recurring antipatterns** — the same mistakes across cycles get flagged explicitly.
|
||||
- **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
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
### Archive Readiness
|
||||
|
||||
```
|
||||
> /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.
|
||||
Reports which categories of bevaringsforskrifta (FOR-2025-12-19-2729) the tree's contents likely fall under — § 7 for the state governance line, § 30 for the municipal one, and the § 3 safety net for everything not named — and what should be *considered* for transfer to the organization's case/archive system. The two governance lines are parallel, not alternative: the same file can match a category in both, and both are reported.
|
||||
|
||||
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.
|
||||
The command decides nothing. Kassasjon requires authority from Nasjonalarkivet under arkivlova § 13, and the assessment itself belongs to the organization's documentation plan (arkivlova § 8, arkivforskrifta § 12 b). There is **no deletion model in this plugin and none will be added** — the command declares neither `Write` nor `Edit`, so it is read-only in the tool layer, not merely in prose. Falling under § 3 means the material *must be preserved*, not the opposite. The report also names what it cannot see: arkivforskrifta § 1 letter c makes the assessment conditional on whether the documentation is already managed as archive in another system, and that state is reported as unknown rather than assumed away.
|
||||
|
||||
### Innboks-ingestion
|
||||
### Inbox Ingestion
|
||||
|
||||
```
|
||||
> /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
|
||||
### Cycle Reporting
|
||||
|
||||
```
|
||||
> /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å 0–1.0-skalaen, aldri som prosent måloppnåelse; en forpliktende KR under mål flagges som et avvik, ikke som en middels score).
|
||||
Generates a report straight from the KR numbers in the cycle's `okr-*.md` frontmatter — deterministic, so the same data yields byte-identical output. Three forms, one command argument: `tertial` for interim reporting, `arsrapport` for part III of the statutory annual report ("Årets aktiviteter og resultater"), and `etatsstyring` for the agency-governance meeting brief. Committed and aspirational Key Results are reported **separately** in all three, never aggregated into one figure: the two are measured against different standards, and a combined average is ambiguous by construction. The generator owns the arithmetic, the table structure and the formatting invariants (score is stated on its 0–1.0 scale, never as percent goal attainment; a committed KR below target is flagged as a deviation, not a middling 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.
|
||||
An aspirational KR with a low score is **never** presented as a deviation — landing near 0.7 is expected attainment for that type, and letting the aspirational score carry consequences in reporting would re-bundle target and forecast, which is what makes sandbagging structurally rational in the next cycle.
|
||||
|
||||
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å.
|
||||
It does **not** own confidence, nor the assessment fields it reserves — the per-Objective evaluation of goal attainment in the annual report, and the mapping from Objectives to the letter of allocation's governance parameters. Those are left as marked placeholders because they require judgement or sources the generator does not read. The Confidence column is likewise left empty by design and filled in during `/okr:sporing`, because confidence is a probability judgement — the canon designates one source of truth for the On Track / At Risk / Off Track scale and explicitly rejects deriving it mechanically from score. Scores themselves are never written to disk; they are recomputed from `baseline`/`target`/`naa` each run so a second, drifting source of truth cannot arise.
|
||||
|
||||
### Hjelp og vedlikehold
|
||||
### Help and Maintenance
|
||||
|
||||
```
|
||||
> /okr:help
|
||||
> /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 16 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 18 of the 19 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
|
||||
|
||||
Ting denne pluginen bevisst ikke gjør, så du kan avgjøre tidlig om den passer:
|
||||
Things this plugin deliberately does not do, so you can tell early whether it fits:
|
||||
|
||||
- **Den er ikke et OKR-oppfølgingssystem.** Det finnes ingen database, ingen server
|
||||
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.
|
||||
- **It is not an OKR tracking system.** There is no database, no server and no
|
||||
web UI. All state is Markdown files under `.claude/okr/` in your own project,
|
||||
plus a machine-global org profile in `~/.claude/okr/org/`. Linear is the only
|
||||
tracking integration, and it is optional.
|
||||
- **It does not make the organization's assessments.** `/okr:rapport` owns the
|
||||
arithmetic and the formatting invariants; the per-Objective evaluation of goal
|
||||
attainment and the mapping to the letter of allocation's governance parameters
|
||||
are left as marked placeholders. Confidence is a judgement, never derived from
|
||||
score.
|
||||
- **It does not decide archiving, and never deletes.** `/okr:arkivklar` produces
|
||||
an assessment basis against bevaringsforskrifta — what should be *considered*
|
||||
for transfer to the case/archive system. It is read-only by tool list.
|
||||
- **It is not a multi-user collaboration tool.** One operator, local files, no
|
||||
accounts, no shared state.
|
||||
- **It does not replace the methodology literature.** The knowledge base is a
|
||||
sourced working reference, not a substitute for Doerr, Wodtke or DFØ's own
|
||||
guidance.
|
||||
|
||||
---
|
||||
|
||||
## Kom i gang
|
||||
## Getting Started
|
||||
|
||||
Installer først — se [Install](#install) øverst.
|
||||
Install first — see [Install](#install) at the top.
|
||||
|
||||
### Første samtale
|
||||
### First Conversation
|
||||
|
||||
```
|
||||
> /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:skriv # Skriv din første OKR
|
||||
/okr:kvalitet # Kvalitetssjekk
|
||||
/okr:kaskade # Alignér med org-mål
|
||||
/okr:oppsett full # One-time setup
|
||||
/okr:skriv # Write your first OKR
|
||||
/okr:kvalitet # Quality check
|
||||
/okr:kaskade # Align with org goals
|
||||
```
|
||||
|
||||
**Under en syklus:**
|
||||
**During a cycle:**
|
||||
```
|
||||
/okr:sporing # Oppdater fremdrift
|
||||
/okr:møter # Forbered check-in-møte
|
||||
/okr:sporing # Update progress
|
||||
/okr:møter # Prepare check-in meeting
|
||||
```
|
||||
|
||||
**Ved syklusslutt:**
|
||||
**End of cycle:**
|
||||
```
|
||||
/okr:oppsett arkiver # Arkiver med retrospektiv
|
||||
/okr:analyse # Se hvordan trenden går
|
||||
/okr:skriv # Start neste syklus
|
||||
/okr:oppsett arkiver # Archive with retrospective
|
||||
/okr:analyse # See how you're trending
|
||||
/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 |
|
||||
| Tildelingsbrevet driver prioriteringer | `/okr:governance` og `/okr:gap` oversetter og sjekker dekning |
|
||||
| DFØ bruker annen terminologi | Innebygget kobling mellom «mål- og resultatstyring» og OKR-begreper |
|
||||
| Riksrevisjonen krever dokumentasjon | Revisjonsklart format med baseline, mål og datakilde per KR |
|
||||
| Ingen bonusordninger | CFR-rammeverk designet for anerkjennelse uten økonomiske incentiver |
|
||||
| Tillitsvalgte må involveres | Veiledning om drøftingsplikt, transparente KR, og involvering av tillitsvalgte |
|
||||
| Konsensuskultur | Råd kalibrert for organisasjoner der stretch-mål kjennes risikabelt |
|
||||
| Politiske prioriteringer kan skifte midt i syklusen | Strategier for å håndtere mandatendringer midt i syklusen |
|
||||
| 4-month tertial cycles (not quarterly) | Built-in tertial support with correct week counting and phase detection |
|
||||
| Tildelingsbrev drives priorities | `/okr:governance` and `/okr:gap` translate and check coverage |
|
||||
| DFØ uses different terminology | Built-in mapping between "mål- og resultatstyring" and OKR concepts |
|
||||
| Riksrevisjonen requires documentation | Audit-ready format with baseline, target, data source per KR |
|
||||
| No performance bonuses | CFR framework designed for recognition without financial incentives |
|
||||
| Tillitsvalgte must be involved | Guidance on drøftingsplikt, transparent KR, and union involvement |
|
||||
| Consensus culture | Advice calibrated for organizations where stretch goals feel risky |
|
||||
| Political priorities can shift mid-cycle | Strategies for handling mid-cycle mandate changes |
|
||||
|
||||
### Styringslinjen
|
||||
### The governance chain
|
||||
|
||||
```
|
||||
Stortingsmelding / NTP
|
||||
|
|
@ -294,121 +284,121 @@ Tildelingsbrev fra departement
|
|||
|
|
||||
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
|
||||
### The documented problem class
|
||||
|
||||
Designet retter seg mot en problemklasse som er beskrevet av revisjonsmyndigheten
|
||||
selv, ikke en generell påstand om styring i offentlig sektor.
|
||||
The design targets a problem class that has been described by the audit
|
||||
authority itself, not a general assertion about public-sector management.
|
||||
|
||||
**Les datoene før funnene.** Alle de fire revisjonene under ble gjennomført i
|
||||
**2019–2020**. 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.
|
||||
**Read the dates before the findings.** All four audits below were carried out in
|
||||
**2019–2020**. Riksrevisjonen has since **closed these cases following
|
||||
improvements in mål- og resultatstyring** (DFØ-notat 2026:2, fn. 152 and fn. 217).
|
||||
They are cited here as historically documented weaknesses that the administration
|
||||
has addressed — not as current criticism of any agency.
|
||||
|
||||
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.
|
||||
All four are chapters of **Dokument 1** (the annual accounting and compliance
|
||||
audit), not of the Dokument 3 forvaltningsrevisjon series. Stable per-chapter URLs
|
||||
do not exist, so each is cited through DFØ's aggregation with page and footnote.
|
||||
|
||||
| Dokumentert funn (2019–2020) | Kilde | Problemklasse pluginen arbeider på |
|
||||
| Documented finding (2019–2020) | Source | Problem class the plugin works on |
|
||||
|---|---|---|
|
||||
| Å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 (2019–2020), 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. 47–48 (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 (2019–2020), 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 (2019–2020), som gjengitt i DFØ-notat 2026:2 s. 47 (fn. 210–211) | Gap-analyse mot tildelingsbrev (`/okr:gap`) |
|
||||
| Annual reports give information about activities and service deliveries, and to a limited degree about effects — attributed to measurement problems, including missing statistics | Riksrevisjonen (2019), part of Dokument 1 (2019–2020), as rendered in DFØ-notat 2026:2 p. 53 (fn. 253) | Leading/lagging balance as a rubric dimension; annual-report generation (`/okr:rapport arsrapport`) |
|
||||
| The annual report presentation showed the agency's own analysis of goal attainment to a lesser degree, appearing instead as a snapshot of activities and results (NIBIO, 2020) | Riksrevisjonen (2020), part of Dokument 1, as rendered in DFØ-notat 2026:2 pp. 47–48 (fn. 216) | The antipattern report generation is built to avoid |
|
||||
| The department had not established a mål- og resultatstyring system giving sufficient information about how effectively the agency used its resources (Havforskningsinstituttet, 2019) | Riksrevisjonen (2019), part of Dokument 1 (2019–2020), as rendered in DFØ-notat 2026:2 p. 48 (fn. 222) | Benefit realisation tied to OKR rather than treated as a separate track |
|
||||
| Steering information did not give a sufficient basis for prioritisation (Luftfartstilsynet and Havforskningsinstituttet, 2019) | Riksrevisjonen (2019), part of Dokument 1 (2019–2020), as rendered in DFØ-notat 2026:2 p. 47 (fn. 210–211) | Gap analysis against 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).
|
||||
The positive mandate for what `/okr:governance` and `/okr:gap` read out of a
|
||||
tildelingsbrev is separate: the department **shall** set styringsparametere in
|
||||
order to assess goal attainment and results, and set requirements for the annual
|
||||
report — *Bestemmelser om økonomistyring i staten* point 1.5 (DFØ-notat 2026:2
|
||||
p. 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.
|
||||
**What is not claimed here.** These quotations are DFØ's rendering of
|
||||
Riksrevisjonen, not Riksrevisjonen's own wording; the NIBIO finding in particular
|
||||
has not been verified against the primary source. Nothing above says the plugin
|
||||
resolves these findings, that the findings are open, or that any agency is
|
||||
currently deficient. A separate 2021 consultancy figure sometimes cited in this
|
||||
context is omitted deliberately: it is not Riksrevisjonen's, and its sample is
|
||||
unknown.
|
||||
|
||||
---
|
||||
|
||||
## Under panseret
|
||||
## Under the Hood
|
||||
|
||||
### Agenter
|
||||
### Agents
|
||||
|
||||
7 spesialiserte agenter aktiveres automatisk:
|
||||
7 specialized agents activate automatically:
|
||||
|
||||
| Agent | Rolle |
|
||||
|-------|-------|
|
||||
| kvalitetssjekker | Scorer kvalitet, oppdager antipatterns, sjekker alignment |
|
||||
| kaskadebygger | Bygger kaskader mellom organisasjonsnivåer |
|
||||
| fremdriftssporer | Beregner score, prognoserer fullføring, flagger risiko |
|
||||
| møtefasilitator | Genererer møteagendaer og fasiliteringsmateriale |
|
||||
| styringsrådgiver | Styringsanalyse, tildelingsbrev-oversettelse, revisjonsetterlevelse |
|
||||
| gapanalytiker | Dekningsmatrise mellom strategidokumenter og OKR |
|
||||
| trendanalytiker | Tverrsyklus-mønsteranalyse med trendvisualisering |
|
||||
| Agent | Role |
|
||||
|-------|------|
|
||||
| kvalitetssjekker | Score quality, detect antipatterns, check alignment |
|
||||
| kaskadebygger | Build cascades between organizational levels |
|
||||
| fremdriftssporer | Calculate scores, forecast completion, flag risks |
|
||||
| møtefasilitator | Generate meeting agendas and facilitation materials |
|
||||
| styringsrådgiver | Governance analysis, tildelingsbrev translation, audit compliance |
|
||||
| gapanalytiker | Coverage matrix between strategic documents and OKR |
|
||||
| trendanalytiker | Cross-cycle pattern analysis with trend visualization |
|
||||
|
||||
### 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å |
|
||||
| UserPromptSubmit | Injiserer organisasjonsprofilen din og tilgjengelige kontekstfiler i hver interaksjon |
|
||||
| PreCompact | Bevarer OKR-utkasttilstand hvis samtalen blir lang |
|
||||
| SessionStart | Proactive coaching — tells you where you are in the cycle and what to focus on |
|
||||
| UserPromptSubmit | Injects your organization profile and available context files into every interaction |
|
||||
| PreCompact | Preserves OKR draft state if the conversation gets long |
|
||||
|
||||
### Skills
|
||||
|
||||
| Skill | Rolle |
|
||||
|-------|-------|
|
||||
| okr-offentlig-sektor | Kjerne-OKR-metodikk og domenekunnskap om norsk offentlig sektor |
|
||||
| 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 |
|
||||
| Skill | Role |
|
||||
|-------|------|
|
||||
| okr-offentlig-sektor | Core OKR methodology and Norwegian public-sector domain knowledge |
|
||||
| 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.
|
||||
19 reference files covering OKR methodology, Norwegian public sector governance (state and municipal steering lines), 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/
|
||||
okr.local.md # Din organisasjonsprofil
|
||||
okr.local.md # Your organizational profile
|
||||
okr/
|
||||
strategisk-kontekst/ # Strategiske dokumenter du oppgir
|
||||
syklus/T1-2026/ # Aktiv syklus' OKR og status
|
||||
historikk/ # Arkiverte sykluser med retrospektiver
|
||||
dokumenter/ # Andre relevante dokumenter
|
||||
strategisk-kontekst/ # Strategic documents you provide
|
||||
syklus/T1-2026/ # Active cycle OKR and status
|
||||
historikk/ # Archived cycles with retrospectives
|
||||
dokumenter/ # Any other relevant documents
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Begrensninger
|
||||
## Limitations
|
||||
|
||||
- Designet for norsk offentlig sektor — kan kreve tilpasning for andre kontekster
|
||||
- Ikke testet i et reelt OKR-program over flere sykluser
|
||||
- Linear er den eneste støttede oppfølgingsintegrasjonen
|
||||
- Norsk språk for alle brukervendte interaksjoner
|
||||
- Designed for Norwegian public sector — may not fit other contexts without adaptation
|
||||
- Not yet tested in a real multi-cycle OKR program
|
||||
- Linear is the only supported tracking integration
|
||||
- Norwegian language for all user-facing interactions
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
Full detalj i [CHANGELOG.md](CHANGELOG.md). Høydepunkter:
|
||||
Full detail in [CHANGELOG.md](CHANGELOG.md). Highlights:
|
||||
|
||||
| Versjon | Dato | Høydepunkter |
|
||||
| Version | Date | Highlights |
|
||||
|---------|------|------------|
|
||||
| **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.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) |
|
||||
|
|
@ -421,15 +411,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.3.2 | 2026-06-24 | Fix: loopende Stop-hook fjernet |
|
||||
| 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.1.0** | 2026-04-08 | Persistent kontekst, dypere onboarding, kontekstbevisste kommandoer, syklusarkivering |
|
||||
| **1.0.0** | 2026-04-08 | Arkitekturoverhaling, selvstendige kommandoer, hooks, markedsplass-klar |
|
||||
| 0.5.0 | 2026-02-07 | Første formelle versjon |
|
||||
| **1.3.0** | 2026-04-08 | Gap analysis, cross-cycle analytics with Mermaid visualizations, proactive SessionStart coaching |
|
||||
| **1.1.0** | 2026-04-08 | Persistent context, deep onboarding, context-aware commands, cycle archival |
|
||||
| **1.0.0** | 2026-04-08 | Architecture overhaul, self-contained commands, hooks, marketplace-ready |
|
||||
| 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)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
If you discover a security vulnerability, please:
|
||||
|
||||
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:
|
||||
- Description of the vulnerability
|
||||
- Steps to reproduce
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: okr:sporing
|
||||
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]"
|
||||
---
|
||||
|
||||
|
|
@ -65,32 +65,7 @@ Score = (Nåværende - Baseline) / (Target - Baseline)
|
|||
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?
|
||||
|
||||
4. **Skriv `status.md` med scriptet — aldri for hånd.** Du eier vurderingen;
|
||||
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.
|
||||
4. **Generer rapport** med anbefalte tiltak
|
||||
|
||||
### Ved ukentlig check-in
|
||||
|
||||
|
|
@ -100,57 +75,52 @@ Generer en strukturert check-in:
|
|||
3. Beslutninger som trengs
|
||||
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
|
||||
markdown. Scriptet setter frontmatteren selv — `type: Status` (uten den feller
|
||||
`okf-check` hele roten) og `kilde: sporing`, provenans-nøkkelen som gjør at en senere
|
||||
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:
|
||||
Når statusrapporten lagres som `.claude/okr/syklus/[id]/status.md` i second-brain-treet,
|
||||
bær filen **OKF-frontmatter** (påkrevd `type: Status` + anbefalt `title`/`description`/
|
||||
`timestamp`); rapportinnholdet under følger frontmatteren:
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: Status
|
||||
title: Statusrapport T1-2026
|
||||
timestamp: "2026-05-01T10:00:00.000Z"
|
||||
kilde: sporing
|
||||
syklus: T1-2026
|
||||
title: "Status T1-2026"
|
||||
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
|
||||
under 1.0 er et avvik som skal forklares, ikke et godt resultat.
|
||||
#### Committed Key Results
|
||||
|
||||
### Trafikksikkerhet og tunnelsikkerhet
|
||||
|
||||
| KR | Baseline | Target | Naa | Score | Avvik | Confidence |
|
||||
| KR | Baseline | Target | Nå | Score | Avvik | Confidence |
|
||||
|----|----------|--------|-----|-------|-------|------------|
|
||||
| Tunneler med oppgradert sikkerhetsutrustning (antall) | 12 | 20 | 17 | 0.63 | Ja | At Risk |
|
||||
| KR1: Redusere ulykker | 40 | 30 | 35 | 0.50 | Ja | At Risk 🟡 |
|
||||
| KR2: Fartshumper installert | 0 | 100 | 60 | 0.60 | Ja | On Track 🟢 |
|
||||
|
||||
## Avvik som skal forklares
|
||||
**Committed: 0 av 2 KR har nådd kravet.** Et committed KR er nådd eller ikke;
|
||||
avvikene over skal forklares, ikke rundes av til et snitt.
|
||||
|
||||
## Aspirational Key Results
|
||||
#### Aspirational Key Results
|
||||
|
||||
| KR | Baseline | Target | Naa | Score | Avvik | Confidence |
|
||||
| KR | Baseline | Target | Nå | Score | Avvik | Confidence |
|
||||
|----|----------|--------|-----|-------|-------|------------|
|
||||
| Andel digitale søknader (prosent) | 60 | 85 | 63 | 0.12 | - | At Risk |
|
||||
| KR3: Foreldre-tilfredshet | 60 | 90 | 75 | 0.50 | - | At Risk 🟡 |
|
||||
|
||||
**Snitt aspirational: 0.23** over 2 KR.
|
||||
**Snitt aspirational: 0.50** over 1 KR. Rundt 0.7 er forventet måloppnåelse for
|
||||
aspirational — en lav score her er ikke et avvik og skal ikke rapporteres som ett.
|
||||
|
||||
**Confidence, committed:** At Risk 🟡 — KR1 trenger fokus, KR2 ligger foran plan.
|
||||
**Confidence, aspirational:** At Risk 🟡 — KR3 flater ut.
|
||||
|
||||
**Anbefalte tiltak:**
|
||||
1. Prioriter tiltak for KR1 (sikkerhet er kritisk)
|
||||
2. Kjør brukerundersøkelse for KR3 før neste måned
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
Hvis Linear er konfigurert (sjekk med ToolSearch):
|
||||
|
|
|
|||
|
|
@ -1,220 +0,0 @@
|
|||
# Pilot T3-2026 — protokoll
|
||||
|
||||
> Protokollen for den reelle tertialsyklusen (T3 2026, september–desember) 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 0–1.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 | 49–50 (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.*
|
||||
|
|
@ -9,27 +9,6 @@ import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|||
import { join } from 'node:path';
|
||||
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 configPath = join(cwd, '.claude', 'okr.local.md');
|
||||
|
||||
|
|
@ -97,34 +76,20 @@ try {
|
|||
try {
|
||||
// M1/m1 (B2): tell status-MARKERTE tabellrader, ikke raaforekomster --
|
||||
// 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
|
||||
// sier at de fleste stretch-KR BOER staa At Risk, og at permanent groent er
|
||||
// sandbagging-signalet. Talte vi alle ikke-groenne rader under ett, ba vi
|
||||
// operatoeren om tiltak for at aspirational-portefoeljen oppfoerer seg
|
||||
// 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.
|
||||
// R1 (1.8.0): status-malen bruker den kanoniske confidence-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 alternativene og telles ikke.
|
||||
const statusContent = readFileSync(statusPath, 'utf8');
|
||||
let iAspirational = false;
|
||||
for (const line of statusContent.split('\n')) {
|
||||
const overskrift = ER_TOPPSEKSJON.exec(line);
|
||||
if (overskrift) {
|
||||
iAspirational = ER_ASPIRATIONAL.test(overskrift[1].trim());
|
||||
continue;
|
||||
}
|
||||
if (!ER_KR_RAD.test(line)) continue;
|
||||
if ((iAspirational ? STOPPET_ORD : FARE_ORD).test(line)) atRiskCount += 1;
|
||||
}
|
||||
atRiskCount = statusContent
|
||||
.split('\n')
|
||||
.filter(
|
||||
(line) => /^\s*\|.*\|\s*$/.test(line)
|
||||
&& /at risk|off track|i fare|blokkert/i.test(line),
|
||||
)
|
||||
.length;
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,17 +11,6 @@
|
|||
// beholder en intern '#' ("A #B" -> A #B). Retter comment-leak-bugen der
|
||||
// `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
|
||||
// en list-key returnerer null (rest-av-linja er tom); list-elementer paa
|
||||
// foelgende linjer konsumeres aldri (ingen konsument leser tre-filenes `tags`).
|
||||
|
|
@ -37,7 +26,7 @@ export function parseFrontmatter(content) {
|
|||
|
||||
const get = (key) => {
|
||||
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;
|
||||
let v = m[1].trim();
|
||||
if (v === '') return null;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ 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)) {
|
||||
if (verdi === null || verdi === undefined || String(verdi).trim() === '' || Number.isNaN(n)) {
|
||||
throw new Error(`${hvor}: ${felt} maa vaere et rent tall uten enhet (fikk: ${verdi})`);
|
||||
}
|
||||
return n;
|
||||
|
|
|
|||
|
|
@ -13,16 +13,6 @@
|
|||
// 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").
|
||||
|
|
@ -60,30 +50,27 @@ const formatAvvik = (kr, erCommitted) => {
|
|||
return score >= 1 ? 'Nei' : 'Ja';
|
||||
};
|
||||
|
||||
function krRad(kr, erCommitted, confidence = '') {
|
||||
function krRad(kr, erCommitted) {
|
||||
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}|`;
|
||||
// Siste celle (Confidence) staar bevisst tom -- se seem-notatet over.
|
||||
return `| ${kr.navn} | ${kr.baseline} | ${kr.target} | ${kr.naa} | ${score} | ${formatAvvik(kr, erCommitted)} | |`;
|
||||
}
|
||||
|
||||
const krTabell = (krer, erCommitted, hentConfidence = () => '') => [
|
||||
const krTabell = (krer, erCommitted) => [
|
||||
...TABELLHODE,
|
||||
...krer.map((kr) => krRad(kr, erCommitted, hentConfidence(kr))),
|
||||
...krer.map((kr) => krRad(kr, erCommitted)),
|
||||
];
|
||||
|
||||
// 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 = () => '') {
|
||||
function seksjon(okrer, type, erCommitted) {
|
||||
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), '');
|
||||
linjer.push(`### ${okr.tittel}`, '', ...krTabell(krer, erCommitted), '');
|
||||
}
|
||||
return { linjer, antallKr };
|
||||
}
|
||||
|
|
@ -191,35 +178,19 @@ function krevSyklus(syklus, hvor) {
|
|||
// 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 = [
|
||||
const SKALAFORKLARING = [
|
||||
// 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();
|
||||
|
||||
/**
|
||||
|
|
@ -480,124 +451,3 @@ export function etatsstyringsunderlag(syklus, opts = {}) {
|
|||
|
||||
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
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "okr-offentlig-sektor",
|
||||
"version": "1.10.0",
|
||||
"version": "1.9.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "okr-offentlig-sektor",
|
||||
"version": "1.10.0",
|
||||
"version": "1.9.0",
|
||||
"dependencies": {
|
||||
"mammoth": "1.12.0",
|
||||
"postal-mime": "2.7.5",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "okr-offentlig-sektor",
|
||||
"version": "1.10.0",
|
||||
"version": "1.9.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "OKR-plugin for norsk offentlig sektor. Pure-JS konverterings-deps for innboks-ingestion (bevisst zero-dep-brudd, se README).",
|
||||
|
|
|
|||
|
|
@ -23,26 +23,6 @@ import { unquote } from './okf-index.mjs';
|
|||
|
||||
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,
|
||||
// on-disk bundle-root-relativ .md (anti-RAG-poison). B3 (A1): ALLE fire
|
||||
// standard lenkeformer fanges -- inline, referanse-definisjon, autolink og
|
||||
|
|
@ -104,22 +84,20 @@ function rootMarkers(root) {
|
|||
const idx = join(root, 'index.md');
|
||||
if (!existsSync(idx)) return { okfVersion: null, okfLayout: null };
|
||||
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);
|
||||
const { raw: fmRaw, get: fmGet } = parseFrontmatter(raw);
|
||||
// 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.
|
||||
// AERLIG OM DEKNINGEN: uttrekket er ikke produksjonsobserverbart i dag. Naar
|
||||
// noekkelen finnes i frontmatter i en form parseFrontmatter i det hele tatt
|
||||
// returnerer, vinner fm-laget foer fallbacken kjoeres -- saa `body = raw`
|
||||
// roedner ingen test (maalt, ikke antatt). Det beholdes likevel: fjernes det,
|
||||
// ligger en latent defekt og venter paa at lib/frontmatter.mjs's list-key-
|
||||
// kontrakt (:13-15) blir sann. I dag returnerer get() paa en list-key foerste
|
||||
// LIST-ELEMENT, ikke null, fordi `\s*` i :29-regexen spiser linjeskiftet.
|
||||
// Fikses det (patch-lane), begynner fallbacken aa kjoere for list-keys -- og
|
||||
// uten dette uttrekket ville den plukket «- a» ut av frontmatter.
|
||||
const body = fmRaw === null ? raw : raw.slice(raw.indexOf('\n---', 3) + 4);
|
||||
const pick = (key) => {
|
||||
const fromFm = fmGet(key);
|
||||
|
|
@ -146,7 +124,7 @@ export function checkBundle(root, { strictIngest = false, files } = {}) {
|
|||
const strictErrors = [];
|
||||
for (const f of concepts) {
|
||||
const raw = readFileSync(f, 'utf8');
|
||||
const get = rootLevelGet(parseFrontmatter(raw).raw);
|
||||
const { get } = parseFrontmatter(raw);
|
||||
const rel = relative(root, f);
|
||||
const type = get('type');
|
||||
if (!type) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
name: okr-offentlig-sektor
|
||||
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".
|
||||
version: "1.10.0"
|
||||
version: "1.9.0"
|
||||
---
|
||||
|
||||
# OKR Skill for Offentlig Sektor (Norge)
|
||||
|
|
|
|||
|
|
@ -235,13 +235,12 @@ Neste steg: [liste]
|
|||
|
||||
```
|
||||
Objective: [navn]
|
||||
Committed: [X] av [Y] KR nådd
|
||||
Aspirational: [0.0-1.0] (snitt over [N] KR)
|
||||
Samlet score: [0.0-1.0]
|
||||
|
||||
Key Results:
|
||||
1. [KR1 navn] (committed): Target [X], Actual [Y], Score [Z], Avvik [ja/nei]
|
||||
2. [KR2 navn] (committed): Target [X], Actual [Y], Score [Z], Avvik [ja/nei]
|
||||
3. [KR3 navn] (aspirational): Target [X], Actual [Y], Score [Z]
|
||||
1. [KR1 navn]: Target [X], Actual [Y], Score [Z]
|
||||
2. [KR2 navn]: Target [X], Actual [Y], Score [Z]
|
||||
3. [KR3 navn]: Target [X], Actual [Y], Score [Z]
|
||||
|
||||
Hva gikk bra:
|
||||
- [bullet]
|
||||
|
|
|
|||
|
|
@ -232,8 +232,7 @@ Oppsummering av alle OKR for et team:
|
|||
├────────────────────────────────────────────────────────────────┤
|
||||
│ TEAM TOTALT │
|
||||
│ │
|
||||
│ Committed: ____ av ____ KR nådd │
|
||||
│ Aspirational: ______ (snitt over ____ KR) │
|
||||
│ Samlet score: ______ (______%) │
|
||||
│ │
|
||||
│ On Track: ____ OKR At Risk: ____ OKR Off Track: ____ OKR │
|
||||
│ │
|
||||
|
|
|
|||
|
|
@ -548,11 +548,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 noen KR-er er tydelig viktigere enn andre
|
||||
- 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.
|
||||
|
||||
**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
|
||||
|
||||
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 +687,7 @@ For team-nivå rapportering:
|
|||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ TEAM: [Teamnavn] Syklus: [X] 20XX │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Committed: X av Y KR nådd │
|
||||
│ Aspirational: 0.XX (snitt over N KR) │
|
||||
│ Samlet score: 0.XX (XX%) │
|
||||
│ On Track: X av Y OKR-er │
|
||||
│ At Risk: X av Y OKR-er │
|
||||
│ Off Track: X av Y OKR-er │
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ description: >-
|
|||
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",
|
||||
"strategi", "styringssignaler".
|
||||
version: "1.10.0"
|
||||
version: "1.9.0"
|
||||
---
|
||||
|
||||
# OKR Second-Brain Search
|
||||
|
|
|
|||
|
|
@ -170,17 +170,9 @@ test('(a2) F-c: ingen parallelle confidence-etikettsett utenfor kanon', () => {
|
|||
// (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".
|
||||
// Skanne-settet er commands/ + agents/ -- MALENE pluginen emitterer. references/
|
||||
// staar utenfor med vilje: der foeres en annen diskusjon (per-Objective-aggregering
|
||||
// i kalkulatoren), og aa dra den inn her ville blandet to spoersmaal i en vakt.
|
||||
//
|
||||
// To former, fordi bruddet har to skrivemaater:
|
||||
// ETIKETT -- "Samlet score" sammen med en VERDI paa linjen (tall, X-plassholder
|
||||
|
|
@ -201,8 +193,8 @@ 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 maler = [...mdFiles('commands'), ...mdFiles('agents')];
|
||||
assert.ok(maler.length >= 10, `parser-sanity: forventet >= 10 maler, fant ${maler.length}`);
|
||||
|
||||
const violations = [];
|
||||
for (const f of maler) {
|
||||
|
|
@ -226,50 +218,6 @@ test('(k) separasjonsregelen: ingen mal emitterer et udifferensiert samlet-aggre
|
|||
);
|
||||
});
|
||||
|
||||
// --- (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) ---
|
||||
// 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,
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
withWork((work) => {
|
||||
writeConfig(work, '---\nid: "T2-2026"\n---\n');
|
||||
|
|
|
|||
|
|
@ -123,54 +123,11 @@ test('writeFrontmatter: array-verdi (tags) -> OKF multi-linje liste (Step 3)', (
|
|||
const { get } = parseFrontmatter(block);
|
||||
assert.equal(get('type'), 'OKR', 'skalar FOR list-blokk resolver');
|
||||
assert.equal(get('kilde'), 'innboks', 'skalar ETTER innrykket list-blokk resolver fortsatt');
|
||||
// get() paa en list-key returnerer null -- tolerance-kontrakten i
|
||||
// frontmatter.mjs:14-16 er sann etter at verdi-grenen ble strammet fra \s*
|
||||
// til [ \t]* (se (L1)). Fram til da returnerte den foerste list-ELEMENT.
|
||||
assert.equal(get('tags'), null, 'get paa list-key gir null');
|
||||
});
|
||||
|
||||
// --- 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');
|
||||
// Lese-siden uendret (Step 3 scope = kun skrive-siden): get() paa en list-key
|
||||
// krasjer ikke (eksisterende tolerance-kontrakt, frontmatter.mjs:14-16). Den
|
||||
// returnerer foerste list-element fordi parser-\s* spiser newline -- IKKE
|
||||
// null; konsumentene leser aldri tag-VERDIER, kun at nabo-skalarer resolver.
|
||||
assert.doesNotThrow(() => get('tags'), 'get paa list-key krasjer ikke');
|
||||
});
|
||||
|
||||
test('writeFrontmatter: skalar uendret av array-gren (additiv)', () => {
|
||||
|
|
|
|||
|
|
@ -1108,149 +1108,3 @@ test('(21e) okf-index -> okf-check: det emitteren skriver, leser checkeren', ()
|
|||
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 });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// Step 10 (A1): package.json-kontrakten for det bevisste zero-dep-bruddet.
|
||||
// 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
|
||||
// er 1.10.0 (S60: det uwirede leddet) paa ALLE shippede flater, og at .npmrc slaar av
|
||||
// er 1.9.0 (fase D: styringssloeyfa lukkes) paa ALLE shippede flater, og at .npmrc slaar av
|
||||
// install-scripts (Shai-Hulud / supply-chain). Zero npm deps i selve testen.
|
||||
// Moenster: tests/frontmatter.test.mjs (les fil, assert struktur).
|
||||
|
||||
|
|
@ -24,10 +24,10 @@ function readPkg() {
|
|||
return JSON.parse(readFileSync(PKG, 'utf8'));
|
||||
}
|
||||
|
||||
test('package.json: type module + version 1.10.0 (minor)', () => {
|
||||
test('package.json: type module + version 1.9.0 (minor)', () => {
|
||||
const pkg = readPkg();
|
||||
assert.equal(pkg.type, 'module');
|
||||
assert.equal(pkg.version, '1.10.0');
|
||||
assert.equal(pkg.version, '1.9.0');
|
||||
});
|
||||
|
||||
test('package.json: engines.node-gulv satt (unpdf krever >= 22)', () => {
|
||||
|
|
|
|||
|
|
@ -163,13 +163,6 @@ test('(B9) lesSyklus kaster ved ufullstendig KR i frontmatter', async (t) => {
|
|||
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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -44,19 +44,21 @@ const byggFor = (bygger, opts = {}) => bygger(lesSyklus(SYKLUS_DIR), { naa: NAA,
|
|||
// 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 funnet = [...src.matchAll(/(\/(?:[^/\\\n]|\\.)+\/[gimsuy]*)\.test\(line\)/g)].map(
|
||||
(m) => m[1],
|
||||
);
|
||||
assert.equal(
|
||||
funnet.length,
|
||||
2,
|
||||
`parser-sanity: forventet 2 rad-predikater i coaching-hook.mjs, fant ${funnet.length}`,
|
||||
);
|
||||
const kompiler = (lit) => {
|
||||
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') };
|
||||
return { radForm: kompiler(funnet[0]), fareOrd: kompiler(funnet[1]) };
|
||||
}
|
||||
|
||||
const erKrRad = (linje) => /^\s*\|/.test(linje) && !/^\s*\|[\s|:-]+\|\s*$/.test(linje)
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue