Compare commits
5 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c240af7ea | |||
| 140179f8b5 | |||
| 38c80a6f49 | |||
| 248262d7a7 | |||
| 43a84d2326 |
8 changed files with 467 additions and 39 deletions
|
|
@ -21,6 +21,8 @@ Minor-release: **det uwirede leddet**. Kjeden `/okr:rapport` → `/okr:sporing`
|
|||
### 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.
|
||||
|
|
|
|||
13
README.md
13
README.md
|
|
@ -16,6 +16,19 @@ Stortingsmelding ned til team-OKR.
|
|||

|
||||

|
||||
|
||||
## 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
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
If you discover a security vulnerability, please:
|
||||
|
||||
1. **Do not** open a public issue
|
||||
2. Email the maintainer directly, or report privately via the Forgejo repository (git.fromaitochitta.com/open/okr)
|
||||
2. Email <security@fromaitochitta.com>, or report privately via the Forgejo repository: https://git.fromaitochitta.com/open/okr
|
||||
3. Include:
|
||||
- Description of the vulnerability
|
||||
- Steps to reproduce
|
||||
|
|
|
|||
220
docs/pilot-t3-2026.md
Normal file
220
docs/pilot-t3-2026.md
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
# 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,6 +9,27 @@ 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');
|
||||
|
||||
|
|
@ -76,20 +97,34 @@ 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 (`| ... |`).
|
||||
//
|
||||
// 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.
|
||||
// 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.
|
||||
const statusContent = readFileSync(statusPath, 'utf8');
|
||||
atRiskCount = statusContent
|
||||
.split('\n')
|
||||
.filter(
|
||||
(line) => /^\s*\|.*\|\s*$/.test(line)
|
||||
&& /at risk|off track|i fare|blokkert/i.test(line),
|
||||
)
|
||||
.length;
|
||||
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;
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -148,6 +148,133 @@ 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');
|
||||
|
|
|
|||
|
|
@ -44,21 +44,19 @@ 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 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 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: kompiler(funnet[0]), fareOrd: kompiler(funnet[1]) };
|
||||
return { radForm: hent('ER_KR_RAD'), fareOrd: hent('FARE_ORD') };
|
||||
}
|
||||
|
||||
const erKrRad = (linje) => /^\s*\|/.test(linje) && !/^\s*\|[\s|:-]+\|\s*$/.test(linje)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
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 { 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';
|
||||
|
|
@ -71,15 +71,17 @@ 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 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 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: kompiler(funnet[0]), fareOrd: kompiler(funnet[1]) };
|
||||
return { radForm: hent('ER_KR_RAD'), fareOrd: hent('FARE_ORD') };
|
||||
}
|
||||
|
||||
// --- (1) LOAD-BEARING: aritmetikken kommer fra generatoren, aldri fra input ---
|
||||
|
|
@ -149,21 +151,52 @@ test('(6b) de tre kanoniske etikettene godtas', () => {
|
|||
});
|
||||
|
||||
// --- (7) Hook-kontrakten: fila MAA vaere tellbar for coaching-hook.mjs ---
|
||||
// MERK (S60, kjent avvik): tallet 3 under koder 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 -- saa
|
||||
// hookens «OBS: N KR er At Risk/Off Track, vurder tiltak» peker feil vei for en
|
||||
// sunn aspirational-portefoelje. Fikset hoerer hjemme i hooken (patch-lane), og
|
||||
// naar det kommer skal denne assertionen endres MED den. Testen her vokter at
|
||||
// fila er TELLBAR, ikke at 3 er riktig svar.
|
||||
test('(7) hooken kan telle radene, og teller kun de ikke-groenne', () => {
|
||||
// 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')}`);
|
||||
// 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');
|
||||
// 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 ---
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue