feat(okr): deterministisk skriveledd for status.md (1.10.0)
Kjeden /okr:rapport -> /okr:sporing -> status.md var beskrevet i prosa, men manglet et mekanisk ledd: ingenting foerte en generert rad inn i status.md -- fila coaching-hook teller at-risk-KR i, /okr:moeter bygger agenda fra, og /okr:oppsett arkiver leser sluttscorer fra. Rot-aarsaken var at HELE sporings-stien var lesende (ingen Write/Edit/Bash i allowed-tools). scripts/syklus-status.mjs tar confidence som JSON paa stdin og bygger radene gjennom statusrapport() i lib/syklus-rapport.mjs -- samme kode som tertialrapporten. Aritmetikken kan derfor ikke drifte fra /okr:rapport, fordi det ikke finnes en andre implementasjon av den. En LLM skriver ikke av et tall paa veien, og en maskin utleder ikke et trafikklys: beslutning 9 staar, presist formulert som at generatoren PLASSERER en etikett den faar inn som data og UTLEDER aldri en. Haandhevet i kode, ikke i prosa: lukket vokabular (On Track/At Risk/Off Track), full dekning (en tom confidence-celle ville lest som groenn hos hver konsument), typo-fangst paa ukjente KR-navn, og kuratert-fil-vern -- en status.md uten provenans-noekkelen `kilde: sporing` er haandholdt og overskrives aldri (samme mekanikk som innboks-write bruker med `kilde: innboks`). Suite 329 -> 343. Fem vakter mutasjons-verifisert: rad-drift mot tertialrapporten, vokab, dekning (baade alene og sammen med vokab-vakten som overlapper den), og kuratert-vernet.
This commit is contained in:
parent
285d18ebfa
commit
3a0cbcfbf9
13 changed files with 618 additions and 50 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "okr",
|
||||
"version": "1.9.0",
|
||||
"version": "1.10.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"
|
||||
|
|
|
|||
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -5,6 +5,19 @@ 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.
|
||||
|
||||
## [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.9.0
|
||||
# OKR Offentlig Sektor v1.10.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 |
|
||||
| `/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: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,6 +72,7 @@ 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).
|
||||
|
|
@ -88,7 +89,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 ──→ Linear MCP (optional)
|
||||
/okr:sporing ──→ fremdriftssporer ──→ scripts/syklus-status.mjs (skriver status.md) ──→ Linear MCP (optional)
|
||||
/okr:møter ──→ møtefasilitator
|
||||
/okr:governance ──→ styringsrådgiver
|
||||
/okr:innføring ──→ (inline, no agent)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Stortingsmelding ned til team-OKR.
|
|||
|
||||
*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.*
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
|
@ -395,6 +395,7 @@ Full detalj i [CHANGELOG.md](CHANGELOG.md). Høydepunkter:
|
|||
|
||||
| Versjon | Dato | Høydepunkter |
|
||||
|---------|------|------------|
|
||||
| **1.10.0** | 2026-08-10 | Det uwirede leddet: `/okr:sporing` skriver nå `status.md` deterministisk via `scripts/syklus-status.mjs` i stedet for å formulere fila selv. Radene bygges med samme kode som tertialrapporten, så score og avvik er de samme bytene i begge — confidence kommer inn som data fra kommandoen, aldri utledet av tallene. Lukket vokabular (`On Track`/`At Risk`/`Off Track`) og full dekning håndheves i kode; en håndholdt `status.md` uten `kilde: sporing` overskrives aldri. Suite 329 → 343 |
|
||||
| **1.9.0** | 2026-08-09 | «Styringssløyfa lukkes» — tildelingsbrev til årsrapport i én kjede. Tre deterministiske rapportgeneratorer (tertialrapport, årsrapport del III, etatsstyringsmøte-underlag) bygget på en KR-datakontrakt der score alltid beregnes og aldri lagres; `/okr:arkivklar` som rent lesende vurderingsgrunnlag mot bevaringsforskrifta (§§ 7/30, safety-net § 3) med path-confinement i delt modul; committed/aspirational separert i alle tre rapportformene med sandbagging-vakt; leading/lagging og guardrail som KR-designmønster i rubrikk, `skriv` og `kvalitet`; ny referansefil `gevinstrealisering-okr.md`, KOSTRA-seksjon og Beyond Budgeting/strategi→OKR i framework (17 → 19 filer); `okf_version` flyttet til rot-indeksens frontmatter med permanent lesetoleranse for begge plasseringer. Suite 197 → 315 |
|
||||
| **1.8.2** | 2026-07-31 | Patch: siterte markørverdier korrumperte begge rot-markørene — `okf_version: "0.2"` (formen OKF-specen selv viser) ble avvist som ikke-upstream, flyttet til `okf_layout`, og `okf_version` nedgradert til vår konstant. Anførselstegn strippes nå ved parse (før tolkning og før emisjon) og verdien emitteres unquoted; unquote er konservativ (kun matchende par). Suite 187 → 192 |
|
||||
| **1.8.1** | 2026-07-25 | Patch: `okf_version`/`okf_layout`-splitt (OKF-spec §12) — rot-`index.md` bærer nå to distinkte markører i stedet for én som bar to urelaterte konsepter: `okf_version` = upstream OKF-versjonen (`0.1`, verdisett eid av Google), `okf_layout` = pluginens egen layout-revisjon (`kb-layout-2026-06`). Automatisk migrasjonssti for eksisterende bundles (layout-verdi i `okf_version` flyttes verbatim); `okf-check` ekkoer begge; `--okf-layout` erstatter `--okf-version` (beholdt som deprecated alias m/ stderr-varsel) |
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: okr:sporing
|
||||
description: Spor OKR-fremgang, beregn score og generer check-in rapporter
|
||||
allowed-tools: Read, AskUserQuestion, Task, ToolSearch, Glob
|
||||
allowed-tools: Read, Bash, AskUserQuestion, Task, ToolSearch, Glob
|
||||
argument-hint: "[OKR eller tall for oppdatering]"
|
||||
---
|
||||
|
||||
|
|
@ -65,7 +65,32 @@ 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. **Generer rapport** med anbefalte tiltak
|
||||
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.
|
||||
|
||||
### Ved ukentlig check-in
|
||||
|
||||
|
|
@ -75,52 +100,57 @@ Generer en strukturert check-in:
|
|||
3. Beslutninger som trengs
|
||||
4. Fokus neste periode
|
||||
|
||||
## Eksempel på output
|
||||
## Fila scriptet skriver
|
||||
|
||||
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:
|
||||
`.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:
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: Status
|
||||
syklus: T1-2026
|
||||
title: "Status T1-2026"
|
||||
description: "Fremdrift og scoring uke 8 av 16"
|
||||
timestamp: "[ISO-8601]"
|
||||
title: Statusrapport T1-2026
|
||||
timestamp: "2026-05-01T10:00:00.000Z"
|
||||
kilde: sporing
|
||||
---
|
||||
|
||||
## OKR Status - Uke 8 av 16
|
||||
# Statusrapport T1-2026
|
||||
|
||||
### Objective: Forbedre trafikksikkerhet i skolesoner
|
||||
## Committed Key Results
|
||||
|
||||
#### 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.
|
||||
|
||||
| KR | Baseline | Target | Nå | Score | Avvik | Confidence |
|
||||
### Trafikksikkerhet og tunnelsikkerhet
|
||||
|
||||
| KR | Baseline | Target | Naa | Score | Avvik | Confidence |
|
||||
|----|----------|--------|-----|-------|-------|------------|
|
||||
| KR1: Redusere ulykker | 40 | 30 | 35 | 0.50 | Ja | At Risk 🟡 |
|
||||
| KR2: Fartshumper installert | 0 | 100 | 60 | 0.60 | Ja | On Track 🟢 |
|
||||
| Tunneler med oppgradert sikkerhetsutrustning (antall) | 12 | 20 | 17 | 0.63 | Ja | At Risk |
|
||||
|
||||
**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.
|
||||
## Avvik som skal forklares
|
||||
|
||||
#### Aspirational Key Results
|
||||
## Aspirational Key Results
|
||||
|
||||
| KR | Baseline | Target | Nå | Score | Avvik | Confidence |
|
||||
| KR | Baseline | Target | Naa | Score | Avvik | Confidence |
|
||||
|----|----------|--------|-----|-------|-------|------------|
|
||||
| KR3: Foreldre-tilfredshet | 60 | 90 | 75 | 0.50 | - | At Risk 🟡 |
|
||||
| Andel digitale soeknader (prosent) | 60 | 85 | 63 | 0.12 | - | At Risk |
|
||||
|
||||
**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
|
||||
**Snitt aspirational: 0.23** over 2 KR.
|
||||
```
|
||||
|
||||
Rammeteksten om committed, avvik og aspirational kommer fra generatoren — du skal ikke
|
||||
skrive den om. Det du eier, og som ikke finnes i fila, er **tiltakene**: presenter dem i
|
||||
svaret ditt.
|
||||
|
||||
**Eksempel på anbefalte tiltak:**
|
||||
1. Prioriter oppgraderingen av de tre gjenstående tunnelene — kravet er et avvik,
|
||||
ikke et delresultat.
|
||||
2. Kjør brukerundersøkelse på selvbetjeningsløsningen før neste måling.
|
||||
|
||||
## Linear-integrasjon
|
||||
|
||||
Hvis Linear er konfigurert (sjekk med ToolSearch):
|
||||
|
|
|
|||
|
|
@ -13,6 +13,16 @@
|
|||
// 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").
|
||||
|
|
@ -50,27 +60,30 @@ const formatAvvik = (kr, erCommitted) => {
|
|||
return score >= 1 ? 'Nei' : 'Ja';
|
||||
};
|
||||
|
||||
function krRad(kr, erCommitted) {
|
||||
function krRad(kr, erCommitted, confidence = '') {
|
||||
const score = formatScore(beregnScore(kr));
|
||||
// Siste celle (Confidence) staar bevisst tom -- se seem-notatet over.
|
||||
return `| ${kr.navn} | ${kr.baseline} | ${kr.target} | ${kr.naa} | ${score} | ${formatAvvik(kr, erCommitted)} | |`;
|
||||
// Siste celle (Confidence) staar tom for rapportformene -- se seem-notatet
|
||||
// over. Er den fylt, kommer verdien fra /okr:sporing via `statusrapport`, og
|
||||
// resten av raden er byte for byte den samme.
|
||||
const siste = confidence === '' ? ' ' : ` ${confidence} `;
|
||||
return `| ${kr.navn} | ${kr.baseline} | ${kr.target} | ${kr.naa} | ${score} | ${formatAvvik(kr, erCommitted)} |${siste}|`;
|
||||
}
|
||||
|
||||
const krTabell = (krer, erCommitted) => [
|
||||
const krTabell = (krer, erCommitted, hentConfidence = () => '') => [
|
||||
...TABELLHODE,
|
||||
...krer.map((kr) => krRad(kr, erCommitted)),
|
||||
...krer.map((kr) => krRad(kr, erCommitted, hentConfidence(kr))),
|
||||
];
|
||||
|
||||
// Ett avsnitt per OKR, med KR-ene som tabellrader. Rekkefolgen arves fra
|
||||
// lesSyklus (sortert filnavn) og er del av determinisme-kontrakten.
|
||||
function seksjon(okrer, type, erCommitted) {
|
||||
function seksjon(okrer, type, erCommitted, hentConfidence = () => '') {
|
||||
const linjer = [];
|
||||
let antallKr = 0;
|
||||
for (const okr of okrer) {
|
||||
const krer = okr.krer.filter((kr) => kr.type === type);
|
||||
if (krer.length === 0) continue;
|
||||
antallKr += krer.length;
|
||||
linjer.push(`### ${okr.tittel}`, '', ...krTabell(krer, erCommitted), '');
|
||||
linjer.push(`### ${okr.tittel}`, '', ...krTabell(krer, erCommitted, hentConfidence), '');
|
||||
}
|
||||
return { linjer, antallKr };
|
||||
}
|
||||
|
|
@ -178,19 +191,35 @@ 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 SKALAFORKLARING = [
|
||||
const SKALA_TALL = [
|
||||
// Governance-invariant 3: skalaen forklares, og score presenteres ALDRI som
|
||||
// prosent maaloppnaaelse -- en leser som tror 0.7 betyr "70 % av maalet"
|
||||
// trekker feil konklusjon om baade ambisjonsniva og resultat.
|
||||
'Score er en andel paa skalaen 0 til 1.0, beregnet som',
|
||||
'(naa - baseline) / (target - baseline). Den er ikke prosent maaloppnaaelse.',
|
||||
`Et KR der target er lik baseline har ingen definert andel og staar som ${UDEFINERT}.`,
|
||||
];
|
||||
|
||||
const SKALAFORKLARING = [
|
||||
...SKALA_TALL,
|
||||
'',
|
||||
'Confidence fylles ut av /okr:sporing. Den utledes ikke av tallene her --',
|
||||
'confidence er en sannsynlighetsvurdering, ikke en funksjon av score.',
|
||||
'',
|
||||
];
|
||||
|
||||
// Samme tall-avsnitt, men i statusrapporten ER confidence fylt. Setningen maa
|
||||
// da si hvor den kom fra, ellers leser mottakeren et trafikklys uten aa vite om
|
||||
// det er vurdert eller regnet ut.
|
||||
const SKALAFORKLARING_STATUS = [
|
||||
...SKALA_TALL,
|
||||
'',
|
||||
'Confidence er satt av /okr:sporing og gjengitt her uendret. Den er en',
|
||||
'sannsynlighetsvurdering mot den kanoniske skalaen i okr-framework.md, ikke',
|
||||
'en funksjon av score, og er aldri utledet av tallene i tabellene.',
|
||||
'',
|
||||
];
|
||||
|
||||
const klokke = (opts) => opts.naa || process.env.OKR_NOW || new Date().toISOString();
|
||||
|
||||
/**
|
||||
|
|
@ -451,3 +480,124 @@ 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.9.0",
|
||||
"version": "1.10.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "okr-offentlig-sektor",
|
||||
"version": "1.9.0",
|
||||
"version": "1.10.0",
|
||||
"dependencies": {
|
||||
"mammoth": "1.12.0",
|
||||
"postal-mime": "2.7.5",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "okr-offentlig-sektor",
|
||||
"version": "1.9.0",
|
||||
"version": "1.10.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).",
|
||||
|
|
|
|||
160
scripts/syklus-status.mjs
Normal file
160
scripts/syklus-status.mjs
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
#!/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.9.0"
|
||||
version: "1.10.0"
|
||||
---
|
||||
|
||||
# OKR Skill for Offentlig Sektor (Norge)
|
||||
|
|
|
|||
|
|
@ -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.9.0"
|
||||
version: "1.10.0"
|
||||
---
|
||||
|
||||
# OKR Second-Brain Search
|
||||
|
|
|
|||
|
|
@ -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.9.0 (fase D: styringssloeyfa lukkes) paa ALLE shippede flater, og at .npmrc slaar av
|
||||
// er 1.10.0 (S60: det uwirede leddet) 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.9.0 (minor)', () => {
|
||||
test('package.json: type module + version 1.10.0 (minor)', () => {
|
||||
const pkg = readPkg();
|
||||
assert.equal(pkg.type, 'module');
|
||||
assert.equal(pkg.version, '1.9.0');
|
||||
assert.equal(pkg.version, '1.10.0');
|
||||
});
|
||||
|
||||
test('package.json: engines.node-gulv satt (unpdf krever >= 22)', () => {
|
||||
|
|
|
|||
213
tests/syklus-status.test.mjs
Normal file
213
tests/syklus-status.test.mjs
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
// 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, 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).
|
||||
function hookPredikater() {
|
||||
const src = readFileSync(join(ROOT, 'hooks/scripts/coaching-hook.mjs'), 'utf8');
|
||||
const funnet = [...src.matchAll(/(\/(?:[^/\\\n]|\\.)+\/[gimsuy]*)\.test\(line\)/g)].map((m) => m[1]);
|
||||
assert.equal(funnet.length, 2, `parser-sanity: forventet 2 rad-predikater, fant ${funnet.length}`);
|
||||
const kompiler = (lit) => {
|
||||
const siste = lit.lastIndexOf('/');
|
||||
return new RegExp(lit.slice(1, siste), lit.slice(siste + 1));
|
||||
};
|
||||
return { radForm: kompiler(funnet[0]), fareOrd: kompiler(funnet[1]) };
|
||||
}
|
||||
|
||||
// --- (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 ---
|
||||
test('(7) hooken kan telle radene, og teller kun de ikke-groenne', () => {
|
||||
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')}`);
|
||||
// FULL har 1 Off Track + 2 At Risk = 3 ikke-groenne, 1 On Track.
|
||||
const talt = rader.filter((l) => radForm.test(l) && fareOrd.test(l));
|
||||
assert.equal(talt.length, 3, 'hooken ville talt feil antall KR i fare');
|
||||
});
|
||||
|
||||
// --- (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