From 44ceec72050cbf5d4b4530e45025e425daca9511 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Sat, 25 Jul 2026 12:33:55 +0200 Subject: [PATCH] fix(okr): C7b remediering R1-R4 (release-relevante review-funn for 1.8.0) Lukker de fire release-relevante MAJOR-funnene fra /trekreview (S33). Alle TDD roed -> groenn, ingen fiks landet uten en test som feilet foerst. Suite 163 -> 167. R1 (af16d5e4) hooks/scripts/coaching-hook.mjs:83 -- LEVENDE REGRESJON innfoert av 1.8.0. Denne releasen skrev status-malen (commands/sporing.md:86-88) om til den kanoniske skalaen On Track/At Risk/Off Track, mens hooken fortsatt talte kun /i fare|blokkert/i over tabellrader. En status generert under 1.8.0 ga derfor atRiskCount = 0 og SessionStart-nudgen sluttet stille aa utloese seg. Hooken teller naa de kanoniske etikettene; de to norske er BEHOLDT som bakover- kompatibilitet for status-filer skrevet foer 1.8.0. Nudge-teksten bruker samme kanoniske vokabular. Ny testcase mates av malen slik den faktisk genereres i dag; den eksisterende casen beholder gammelt vokabular og daekker legacy-stien. R2 (7ec575be) F-i-omskrivingen ga hver kommando en Kontekstbevissthet-blokk som INSTRUERER Glob, men allowed-tools ble kun utvidet i kaskade.md. Glob lagt til i export, gap, governance, innfoering, kvalitet, moeter, skriv, sporing. Ny vakt- case (k): nevner BODY verktoeyet, maa frontmatter deklarere det (13 kommandoer instruerer Glob; alle 13 dekket). Case (i) grepper kun etter fjernede fraser og kunne ikke fange dette. R3 (5e61ae0d) tests/package-shape.test.mjs asserterte KUN package.json -- som er private:true og dermed den ene flaten som aldri shipper. En delvis bump ville shippet groenn. Ny versjonssync-case dekker .claude-plugin/plugin.json, README- badgen og begge SKILL.md, med forventet verdi UTLEDET fra package.json (ett sted aa endre ved neste bump). Mutasjonsbevist: hver av de fire flatene tilbakestilt til 1.7.1 en om gangen -> casen roed i alle fire tilfeller. R4 (ccff16e1 + 231c53fc) Tre parallelle confidence-etikettsett overlevde F-c: fremdriftssporer-agent.md:68 ("Paa sporet / I fare / Blokkert"), :98 ("Confidence: [Hoey/Medium/Lav]" -- en annen akse: stoerrelse, ikke sannsynlighet) og SKILL.md:48 ("blocked"; kanonisk er "off track"). Alle tre erstattet med referanse til kanon (okr-framework.md:389-392). sporing.md og agenten den delegerer til svarer naa i samme vokabular. Vakt-case (a)/(b) skanner naa samme sett som (d) allerede brukte (+ agents/ + SKILL.md), samlet i canonScan(). Ny case (a2) fanger etikettsett skrevet som bullet eller mal-linje -- tabell- signaturen alene fanget dem ikke, og det var nettopp formen driften overlevde i. Divergens gjenkjennes STRUKTURELT (skraastrek-enumerasjon av >= 2 etiketter, med >= 1 ikke-kanonisk), saa loepende prosa som "For KR i fare" og "Blokkert av eksterne faktorer" ikke gir falske positive. Verifisert: roed-listen var noeyaktig de 3 kjente linjene, ingen andre. CHANGELOG/README: vakt-antall 13 -> 15 cases, suite 149 -> 167, og de fire fiksene lagt inn under [1.8.0] (Added + Fixed). Ingen versjonsbump. GJENSTAAR fra reviewet (3 MAJOR, IKKE i denne bolgen): 607313e3 F-d-kadens- moensteret kan ikke feile paa den kanoniske kadens-tabellen; 029ef814 freshen-references.md:24 baerer fortsatt en foreldet kontekstblokk og vaktens exclude-kommentar kaller fila feilaktig "allerede-fikset" (den er utenfor 1.8.0-scope by design, jf. ba91fc2 -- kommentaren er usann, ikke ekskluderingen); 7579d59c "Outcome" er utestbar i 10-dims-casen pga. substring-containment. Verify: node --test --test-reporter=tap tests/*.test.mjs -> 167 pass / 0 fail. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016pUfkQ1YyH75z9y6RBaBHG --- CHANGELOG.md | 8 +- README.md | 2 +- agents/fremdriftssporer-agent.md | 6 +- commands/export.md | 2 +- commands/gap.md | 2 +- commands/governance.md | 2 +- commands/innføring.md | 2 +- commands/kvalitet.md | 2 +- commands/møter.md | 2 +- commands/skriv.md | 2 +- commands/sporing.md | 2 +- hooks/scripts/coaching-hook.mjs | 14 +++- skills/okr-offentlig-sektor/SKILL.md | 2 +- tests/canon-consistency.test.mjs | 108 ++++++++++++++++++++++++--- tests/coaching-hook.test.mjs | 33 ++++++++ tests/package-shape.test.mjs | 27 ++++++- 16 files changed, 189 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47805a6..7700d94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Minor-release **«Én kanon»**: metodekonsolidering av MAJOR-/metode-funnene fra dyp review 2026-07-16 (§3). Ingen nye kommandoer, agenter eller referansefiler — prinsippet er ÉN kanonisk definisjon per metodebegrep, der konsumentflater (kommandoer, agenter, øvrige referansefiler) refererer kanon i stedet for å redefinere den. Konsolideringen er låst av en ny strukturell konsistensvakt, ikke bare av prosa. ### Added -- **Kanon-konsistensvakt** (`tests/canon-consistency.test.mjs`, 13 cases: 10 vakt-cases F-a…F-i + score-grenser, pluss 3 parser-sanity-cases) — deterministisk, offline, zero-dep. Asserter strukturelt at hvert metodebegrep har én kilde: nøyaktig én confidence-tabell (kanonisk i `okr-framework.md`), ingen team-check-in med månedlig kadens og ingen ledelsesreview med ukentlig, ingen score→modenhet-avledning, ingen parallelle scorebånd utenfor kvalitetsrubrikken, kvalitetssjekker-agenten dekker alle 10 rubrikk-dimensjoner, antipattern-antall utledet av dynamisk telling (aldri hardkodet tall), ingen foreldede kontekst-injeksjons-fraser i `commands/`. Skrevet RED-først (Step 1); alle 10 vakt-cases flippet grønne underveis i Wave 1-5. +- **Kanon-konsistensvakt** (`tests/canon-consistency.test.mjs`, 15 cases: 12 vakt-cases — F-a…F-i, score-grenser, divergerende etikettsett og Glob-dekning — pluss 3 parser-sanity-cases) — deterministisk, offline, zero-dep. Asserter strukturelt at hvert metodebegrep har én kilde: nøyaktig én confidence-tabell (kanonisk i `okr-framework.md`), ingen team-check-in med månedlig kadens og ingen ledelsesreview med ukentlig, ingen score→modenhet-avledning, ingen parallelle scorebånd utenfor kvalitetsrubrikken, kvalitetssjekker-agenten dekker alle 10 rubrikk-dimensjoner, antipattern-antall utledet av dynamisk telling (aldri hardkodet tall), ingen foreldede kontekst-injeksjons-fraser i `commands/`. Skrevet RED-først (Step 1); alle 10 vakt-cases flippet grønne underveis i Wave 1-5. - **Referanse-integritet dekker skills-relative lenker** (`tests/reference-integrity.test.mjs`) — `SKILL.md`s 17 `references/.md`-lenker og krysslenkene mellom referansefilene valideres nå mot disk, slik at de nye krysslenkene konsolideringen innfører ikke kan råtne stille. -- **Testsuite 149 → 163 cases**, alle grønne. +- **Testsuite 149 → 167 cases**, alle grønne. - **`okr-sources.md` § 7 «Alternative rammeverk»** — NCT (Narrative, Commitments, Tasks) attribuert til Ravi Mehta / Reforge, med eksplisitt advarsel mot den vanlige feilattribusjonen til *Radical Focus* 2. utg.; Evidence-Based Management → 2024-guiden, der kun det verifiserte «what's new» er gjengitt. +- **Vaktutvidelser etter post-implementasjons-review** (`/trekreview`, 2026-07-25): kanon-skanningen i case (a)/(b) bruker nå samme sett som case (d) allerede gjorde — `references/` + `commands/` + `agents/` + `SKILL.md` — slik at etikett-drift i agentene og skillen ikke lenger er usynlig for vakten. Ny case (a2) fanger parallelle confidence-etikettsett skrevet som bullet eller mal-linje (tabell-signaturen alene fanget dem ikke), ny case (k) krever at enhver kommando som instruerer `Glob` også deklarerer det i `allowed-tools`, og en ny versjonssync-case i `tests/package-shape.test.mjs` asserterer at `.claude-plugin/plugin.json`, README-badgen og begge `SKILL.md` bærer samme versjon som `package.json` — tidligere ble kun `package.json` sjekket, altså den ene flaten som er `private: true` og aldri shipper. - **Avveiningsveiledning i kvalitetsrubrikken** for KR som treffer BÅDE Outcome- og Uavhengighet-ankeret (samfunnseffekt-klassen) — tidligere manglet en done-condition for den kollisjonen. ### Changed @@ -35,6 +36,9 @@ Minor-release **«Én kanon»**: metodekonsolidering av MAJOR-/metode-funnene fr - **NAV manglet som kildeoppslag** i `okr-sources.md` selv om README påstod bruken — lagt til med first-party-kilde (aksel.nav.no), scopet til team-/produktnivå siden etatsnivå-OKR ikke er dokumentert. - **`/okr:freshen-references`-omtalen presisert** (README + CLAUDE.md): kommandoen scorer 16 AV 17 domene-referansefiler — kvalitetsrubrikken er ekskludert fordi den selv er scoringsinstrumentet. - **Feil modulsti i dokumentasjonen**: `lib/innboks-convert.mjs` (CLAUDE.md og 1.7.0-noten under) → `lib/convert/index.mjs`. Modulen har aldri hatt det navnet; konverterings-adapterne har ligget i `lib/convert/` siden 1.7.0. +- **SessionStart-nudgen for KR i fare var død under 1.8.0** (`hooks/scripts/coaching-hook.mjs`): status-malen ble i denne releasen skrevet om til den kanoniske skalaen (On Track / At Risk / Off Track), mens hooken fortsatt talte kun «i fare» og «blokkert» — en status-rapport generert under 1.8.0 ga derfor 0 treff og nudgen sluttet stille å utløses. Hooken teller nå de kanoniske etikettene; de norske er beholdt som bakover-kompatibilitet for status-filer skrevet før 1.8.0. Nudge-teksten bruker også kanonisk vokabular. +- **F-c-resten i `agents/` og `SKILL.md`**: tre parallelle confidence-etikettsett overlevde konsolideringen — «På sporet / I fare / Blokkert» og «Confidence: [Høy/Medium/Lav]» (en helt annen akse: størrelse, ikke sannsynlighet) i `fremdriftssporer-agent.md`, og «on track / at risk / blocked» i `SKILL.md` (kanonisk er «off track»). Alle tre erstattet med referanse til den kanoniske tabellen. `sporing.md` og agenten den delegerer til svarer nå i samme vokabular. +- **Åtte kommandoer instruerte `Glob` uten å deklarere det**: F-i-omskrivingen ga hver kommando en Kontekstbevissthet-blokk som bruker `Glob`, men `allowed-tools` ble kun utvidet i `kaskade.md`. `export.md`, `gap.md`, `governance.md`, `innføring.md`, `kvalitet.md`, `møter.md`, `skriv.md` og `sporing.md` har nå `Glob` i `allowed-tools`. ## [1.7.1] - 2026-07-17 diff --git a/README.md b/README.md index a71bcac..1e87a91 100644 --- a/README.md +++ b/README.md @@ -301,7 +301,7 @@ The plugin core is zero-dependency (`node:` builtins only). The inbox-ingestion | Version | Date | Highlights | |---------|------|------------| -| **1.8.0** | 2026-07-25 | «Én kanon» — metodekonsolidering etter dyp review: én kanonisk confidence-tabell og én kadens-doktrine i `okr-framework.md` med konsument-dedup på tvers av referanser, kommandoer og agenter; score→modenhet-avledningen fjernet; `committed` = forpliktelse OG påvirkbarhet; kvalitetsvurdering ankret i rubrikken (alle 10 dimensjoner, ingen parallelle scorebånd); ekte antipattern-kategorier; milepæl-/binær-unntak; «align, don't cascade» i kaskadeflatene; markedsclaims kildebelagt + nye kilder (NCT, EBM 2024); ny konsistensvakt `tests/canon-consistency.test.mjs` (suite 149 → 163 cases) | +| **1.8.0** | 2026-07-25 | «Én kanon» — metodekonsolidering etter dyp review: én kanonisk confidence-tabell og én kadens-doktrine i `okr-framework.md` med konsument-dedup på tvers av referanser, kommandoer og agenter; score→modenhet-avledningen fjernet; `committed` = forpliktelse OG påvirkbarhet; kvalitetsvurdering ankret i rubrikken (alle 10 dimensjoner, ingen parallelle scorebånd); ekte antipattern-kategorier; milepæl-/binær-unntak; «align, don't cascade» i kaskadeflatene; markedsclaims kildebelagt + nye kilder (NCT, EBM 2024); ny konsistensvakt `tests/canon-consistency.test.mjs` (suite 149 → 167 cases) | | **1.7.1** | 2026-07-17 | Patch: restsanering etter dyp review — KB/doc-hygiene med referanse-integritetstest (B1) + ingestion-kode-hygiene (B2): æ/ø-translitterering i slugify, ikke-destruktiv circuit-breaker (merge, config overlever), sanitizeEntry mot bidi/zero-width/Unicode-tag, `--okf-version`-bump, BOM/CRLF-toleranse, presis at-risk-telling, bøyningsformer i topic-guard | | **1.7.0** | 2026-07-17 | Innboks-ingestion: `/okr:innboks` med deterministisk pipeline (convert/split/frontmatter/gate/relasjoner/indeks), per-dokument sikkerhetsgate mot RAG-poisoning, `kilde: innboks`-provenans + untrusted-envelope i retrieval-skillen, 4 exact-pinnede pure-JS-avhengigheter (eneste zero-dep-unntak) | | **1.6.1** | 2026-06-26 | Patch: credibility-sanering etter 360-re-evaluering (C+ → A−) — ASCII-strippet kvalitetsrubrikk omskrevet til korrekt norsk, `okr-second-brain-search` `~`-glob søker nå hjemme-roten, uverifisert kb-search-sitat fjernet, regnefeil i oboard-eksempel rettet | diff --git a/agents/fremdriftssporer-agent.md b/agents/fremdriftssporer-agent.md index 72c5084..a068b9b 100644 --- a/agents/fremdriftssporer-agent.md +++ b/agents/fremdriftssporer-agent.md @@ -65,7 +65,9 @@ Score = (Nåværende - Baseline) / (Target - Baseline) 3. **Vurder confidence**: - Basert på trend og gjenstående tid - - På sporet / I fare / Blokkert + - Sett nivået fra den **kanoniske confidence-tabellen i `okr-framework.md`** + (On Track 🟢 / At Risk 🟡 / Off Track 🔴, sannsynlighet for å nå target). + Innfør ingen egne nivåer eller terskler her. 4. **Generer prognose**: - Gitt nåværende trend, hva blir sluttresultat? @@ -95,7 +97,7 @@ Score = (Nåværende - Baseline) / (Target - Baseline) | KR3: [kort] | X | Y | Z | 0.XX | ↗️/→/↘️ | ✅/⚠️/❌ | **Samlet score:** 0.XX -**Confidence:** [Høy/Medium/Lav] +**Confidence:** [On Track 🟢 | At Risk 🟡 | Off Track 🔴] --- diff --git a/commands/export.md b/commands/export.md index 1ce893c..0a52c5d 100644 --- a/commands/export.md +++ b/commands/export.md @@ -1,7 +1,7 @@ --- name: okr:export description: Eksporter OKR-dokumenter (kvalitetsvurdering, gap-matrise, statusrapport, retrospektiv) til print-klar PDF -allowed-tools: Read, Bash +allowed-tools: Read, Bash, Glob argument-hint: "[dokumenttype eller filsti]" --- diff --git a/commands/gap.md b/commands/gap.md index 580a8b7..84c878b 100644 --- a/commands/gap.md +++ b/commands/gap.md @@ -1,7 +1,7 @@ --- name: okr:gap description: Automatisk gap-analyse mellom tildelingsbrev og gjeldende OKR -allowed-tools: Read, AskUserQuestion, Task +allowed-tools: Read, AskUserQuestion, Task, Glob argument-hint: "[tildelingsbrev-fil eller tomt for auto-deteksjon]" --- diff --git a/commands/governance.md b/commands/governance.md index bfac016..7717250 100644 --- a/commands/governance.md +++ b/commands/governance.md @@ -1,7 +1,7 @@ --- name: okr:governance description: Koble OKR til tildelingsbrev, politisk styring og Riksrevisjon-krav -allowed-tools: Read, AskUserQuestion, Task +allowed-tools: Read, AskUserQuestion, Task, Glob argument-hint: "[tildelingsbrev, revisjonsrapport, eller spørsmål]" --- diff --git a/commands/innføring.md b/commands/innføring.md index 10308c0..5827205 100644 --- a/commands/innføring.md +++ b/commands/innføring.md @@ -1,7 +1,7 @@ --- name: okr:innføring description: Planlegg OKR-innføring, håndter motstand og vurder organisasjonens modenhet -allowed-tools: Read, AskUserQuestion, Task +allowed-tools: Read, AskUserQuestion, Task, Glob argument-hint: "[fase, utfordring, eller modenhetsvurdering]" --- diff --git a/commands/kvalitet.md b/commands/kvalitet.md index f694c52..3bdf1b7 100644 --- a/commands/kvalitet.md +++ b/commands/kvalitet.md @@ -1,7 +1,7 @@ --- name: okr:kvalitet description: Vurder og forbedre eksisterende OKR med kvalitetssjekk og antipattern-deteksjon -allowed-tools: Read, AskUserQuestion, Task +allowed-tools: Read, AskUserQuestion, Task, Glob argument-hint: "[OKR å vurdere]" --- diff --git a/commands/møter.md b/commands/møter.md index 7afe5fe..001e481 100644 --- a/commands/møter.md +++ b/commands/møter.md @@ -1,7 +1,7 @@ --- name: okr:møter description: Planlegg og fasiliter OKR-møter, workshops og 1:1-samtaler -allowed-tools: Read, AskUserQuestion, Task +allowed-tools: Read, AskUserQuestion, Task, Glob argument-hint: "[møtetype eller kontekst]" --- diff --git a/commands/skriv.md b/commands/skriv.md index 7fb40dd..fb16623 100644 --- a/commands/skriv.md +++ b/commands/skriv.md @@ -1,7 +1,7 @@ --- name: okr:skriv description: Skriv nye OKR med veiledning for Objectives og Key Results -allowed-tools: Read, AskUserQuestion, Task +allowed-tools: Read, AskUserQuestion, Task, Glob argument-hint: "[mål, strategi, eller tildelingsbrev-kontekst]" --- diff --git a/commands/sporing.md b/commands/sporing.md index 5c094e6..8e99de3 100644 --- a/commands/sporing.md +++ b/commands/sporing.md @@ -1,7 +1,7 @@ --- name: okr:sporing description: Spor OKR-fremgang, beregn score og generer check-in rapporter -allowed-tools: Read, AskUserQuestion, Task, ToolSearch +allowed-tools: Read, AskUserQuestion, Task, ToolSearch, Glob argument-hint: "[OKR eller tall for oppdatering]" --- diff --git a/hooks/scripts/coaching-hook.mjs b/hooks/scripts/coaching-hook.mjs index a1bc1a6..62632fc 100644 --- a/hooks/scripts/coaching-hook.mjs +++ b/hooks/scripts/coaching-hook.mjs @@ -77,10 +77,18 @@ 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. const statusContent = readFileSync(statusPath, 'utf8'); atRiskCount = statusContent .split('\n') - .filter((line) => /^\s*\|.*\|\s*$/.test(line) && /i fare|blokkert/i.test(line)) + .filter( + (line) => /^\s*\|.*\|\s*$/.test(line) + && /at risk|off track|i fare|blokkert/i.test(line), + ) .length; } catch { /* skip */ } } @@ -120,7 +128,7 @@ try { parts.push('Midtveis i syklusen — tid for fremdriftssjekk.'); parts.push('Anbefalt: /okr:sporing (statusoppdatering og scoring).'); if (atRiskCount > 0) { - parts.push(`OBS: ${atRiskCount} KR er merket som i fare/blokkert i siste status.`); + parts.push(`OBS: ${atRiskCount} KR er merket At Risk/Off Track i siste status.`); } } else if (phase === 'late') { parts.push('Syklusen nærmer seg slutt — fokus på sluttspurt og forberedelse.'); @@ -129,7 +137,7 @@ try { parts.push('Mindre enn 2 uker igjen. Vurder /okr:oppsett arkiver for retrospektiv.'); } if (atRiskCount > 0) { - parts.push(`OBS: ${atRiskCount} KR er i fare — vurder tiltak eller juster forventninger.`); + parts.push(`OBS: ${atRiskCount} KR er At Risk/Off Track — vurder tiltak eller juster forventninger.`); } } else { // between cycles diff --git a/skills/okr-offentlig-sektor/SKILL.md b/skills/okr-offentlig-sektor/SKILL.md index 5456f17..55f5f4e 100644 --- a/skills/okr-offentlig-sektor/SKILL.md +++ b/skills/okr-offentlig-sektor/SKILL.md @@ -45,7 +45,7 @@ When users present existing OKR, evaluate against these criteria and provide con To help update OKR status: - Collect current numbers for each KR - Calculate progression (0.0-1.0 scale): 0.7 = expected for aspirational, 0.6-0.7 = sweet spot (Google/Doerr), <0.5 = needs intervention -- Assess status: on track / at risk / blocked +- Assess status using the canonical three-level scale in `references/okr-framework.md` — on track / at risk / off track — and introduce no parallel scale here - Suggest corrective actions and generate update text for meetings/reports ### 4. Cascade OKR diff --git a/tests/canon-consistency.test.mjs b/tests/canon-consistency.test.mjs index 2e28a3a..ce9fbf8 100644 --- a/tests/canon-consistency.test.mjs +++ b/tests/canon-consistency.test.mjs @@ -31,6 +31,16 @@ function mdFiles(dir) { .map((n) => join(dir, n)); } +const SKILL = 'skills/okr-offentlig-sektor/SKILL.md'; + +// R4 (review.md 231c53fc): kanon-skanningen skal dekke ALLE konsumentflater -- +// references/, commands/, agents/ OG SKILL.md (som ligger ETT nivaa over references/). +// Case (d) brukte allerede dette settet; (a)/(b) var smalere enn sine egne case-navn +// lovet, saa etikett-drift i agents/ + SKILL.md var usynlig. Ett felles sett, ett sted. +function canonScan() { + return [...mdFiles(REF), ...mdFiles('commands'), ...mdFiles('agents'), SKILL]; +} + // Kontig. pipe-tabell-blokker: hver blokk = sammenhengende linjer som (trimmet) starter med '|'. function mdTables(body) { const tables = []; @@ -66,6 +76,36 @@ function isConfidenceTable(tableLines) { return /on track/i.test(txt) && /at risk/i.test(txt) && /off track/i.test(txt); } +// --- F-c: divergerende etikettSETT (R4 / review.md ccff16e1) --- +// Et parallelt etikettsett gjenkjennes STRUKTURELT som en skraastrek-enumerasjon av +// statusetiketter -- ikke som loepende prosa. "For KR i fare" og "Blokkert av eksterne +// faktorer" er legitim prosa; det doktrinen forbyr er den parallelle SKALAEN +// ("Paa sporet / I fare / Blokkert"). Kanoniske etiketter: okr-framework.md:389-392. +const CANON_LABELS = ['on track', 'at risk', 'off track']; +const DIVERGENT_LABELS = ['p\u00e5 sporet', 'i fare', 'blokkert', 'blocked']; +// Magnitude-aksen (Hoey/Medium/Lav) er en ANNEN akse enn sannsynlighet. Kun flagget naar +// linjen faktisk snakker om confidence -- en prioritetsskala med samme ord er legitim. +const MAGNITUDE_SCALE = /(h\u00f8y|medium|lav)\s*\/\s*(h\u00f8y|medium|lav)/i; + +// Segmenter mellom skraastreker; et segment "er" en etikett naar det (etter stripping av +// listemarkoer, utheving, klammer og emoji-hale) starter/slutter paa etiketten. +function labelSegments(line) { + return line + .split('/') + .map((s) => s.replace(/[*_`[\]()]/g, '').replace(/^[\s\-+]*(?:\d+\.)?\s*/, '').trim().toLowerCase()) + .map((s) => { + const all = [...CANON_LABELS, ...DIVERGENT_LABELS]; + return all.find((l) => s === l || s.startsWith(`${l} `) || s.endsWith(` ${l}`)) ?? null; + }); +} + +// Returnerer de divergerende etikettene i en enumerasjon (>= 2 etikett-segmenter), ellers []. +function divergentEnumeration(line) { + const hits = labelSegments(line).filter(Boolean); + if (hits.length < 2) return []; + return hits.filter((h) => DIVERGENT_LABELS.includes(h)); +} + // ==================== parser-sanity (jf. B1) ==================== test('parser-sanity: headingsOf finner 10 rubrikk-dimensjoner', () => { @@ -86,10 +126,10 @@ test('parser-sanity: linesMatching finner check-in-linjer i okr-framework.md', ( // ==================== F-a..F-i konsistensvakt ==================== // (a) F-c confidence EN gang. RED til calculator-dedup (Step 6). +// R4: skanne-settet utvidet fra references/+commands/ til canonScan() (som case (d)). test('(a) F-c: noeyaktig EN confidence-tabell, kanonisk i okr-framework.md', () => { - const scan = [...mdFiles(REF), ...mdFiles('commands')]; const found = []; - for (const f of scan) { + for (const f of canonScan()) { if (mdTables(readDoc(f)).some(isConfidenceTable)) found.push(f); } assert.deepEqual( @@ -99,10 +139,34 @@ test('(a) F-c: noeyaktig EN confidence-tabell, kanonisk i okr-framework.md', () ); }); +// (a2) F-c divergerende etikettSETT (R4 / review.md ccff16e1). En tabell-signatur alene +// fanger ikke parallelle skalaer skrevet som bullet/mal-linje -- det var nettopp formen +// driften overlevde i (agents/ + SKILL.md). +test('(a2) F-c: ingen parallelle confidence-etikettsett utenfor kanon', () => { + const violations = []; + for (const f of canonScan()) { + const body = readDoc(f); + body.split('\n').forEach((line, i) => { + const divergent = divergentEnumeration(line); + if (divergent.length > 0) { + violations.push(`${f}:${i + 1}: divergerende etikettsett (${divergent.join(', ')}): ${line.trim()}`); + } + if (/confidence/i.test(line) && MAGNITUDE_SCALE.test(line)) { + violations.push(`${f}:${i + 1}: magnitude-skala paa confidence-aksen: ${line.trim()}`); + } + }); + } + assert.deepEqual( + violations, + [], + `kanonisk sett = On Track / At Risk / Off Track (okr-framework.md:389-392):\n${violations.join('\n')}`, + ); +}); + // (b) F-d kadens strukturell (kadens x publikum). RED til kadens-konsumenter (Step 11-12). // Team er default; publikum-markoer ledelse/ledergruppe hever til ledelses-review. test('(b) F-d: ingen team-check-in maanedlig, ingen ledelsesreview ukentlig', () => { - const scan = [...mdFiles(REF), ...mdFiles('commands')]; + const scan = canonScan(); // R4: utvidet fra references/+commands/ // Kadens-adjektiv DIREKTE foran check-in (ev. via okr/team-kvalifikator) = team-maanedlig. // Strukturell: fanger IKKE "maanedlig 30-min review" (korrekt dobbeltrytme i implementation.md) // eller "Maanedlig status | Oboard check-ins" (governance-tabell) — kun kadens->check-in-binding. @@ -137,12 +201,7 @@ test('(d) F-g: prosa-omtaler av antipattern-antall == dynamisk telling (drift-la const categoryCount = linesMatching(anti, /^## \d+\./).length; assert.ok(antipatternCount > 0, `parser-sanity: fant ${antipatternCount} antipatterns`); assert.ok(categoryCount > 0, `parser-sanity: fant ${categoryCount} kategorier`); - const scan = [ - ...mdFiles(REF), - ...mdFiles('commands'), - ...mdFiles('agents'), - 'skills/okr-offentlig-sektor/SKILL.md', - ]; + const scan = canonScan(); const drift = []; for (const f of scan) { for (const m of readDoc(f).matchAll(/(\d+)\s+antipatterns\b/gi)) { @@ -225,3 +284,34 @@ test('(j) score-grenser: okr-calculator dokumenterer kapp [0,1.0] OG div-paa-nul if (!hasDivNull) problems.push('mangler div-paa-null-regel (Target==Baseline / 0 maalbare KR -> udefinert)'); assert.deepEqual(problems, [], `score-grenser doc-invariant:\n${problems.join('\n')}`); }); + +// (k) R2 (review.md 7ec575be): F-i-omskrivingen ga hver kommando en Kontekstbevissthet-blokk +// som INSTRUERER `Glob`, men allowed-tools ble ikke utvidet tilsvarende -- et direktiv +// kommandoen ikke kan utfoere. Strukturell invariant: nevner BODY verktoeyet, maa +// frontmatter deklarere det. Case (i) grepper kun etter fjernede fraser og fanger ikke dette. +function frontmatterAndBody(rel) { + const raw = readDoc(rel); + const m = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(raw); + return m ? { fm: m[1], body: m[2] } : { fm: '', body: raw }; +} + +test('(k) R2: kommandoer som instruerer Glob deklarerer Glob i allowed-tools', () => { + const missing = []; + let instructing = 0; + for (const f of mdFiles('commands')) { + const { fm, body } = frontmatterAndBody(f); + if (!/\bGlob\b/.test(body)) continue; + instructing += 1; + const declared = /^allowed-tools:\s*(.+)$/m.exec(fm); + const tools = (declared ? declared[1] : '').split(',').map((t) => t.trim()); + if (!tools.includes('Glob')) { + missing.push(`${f}: allowed-tools = ${declared ? declared[1].trim() : '(mangler)'}`); + } + } + assert.ok(instructing >= 10, `parser-sanity: fant ${instructing} Glob-instruerende kommandoer`); + assert.deepEqual( + missing, + [], + `Glob instruert i body uten dekning i allowed-tools:\n${missing.join('\n')}`, + ); +}); diff --git a/tests/coaching-hook.test.mjs b/tests/coaching-hook.test.mjs index ff65499..37414c4 100644 --- a/tests/coaching-hook.test.mjs +++ b/tests/coaching-hook.test.mjs @@ -115,6 +115,39 @@ test('at-risk telles per status-RAD (tabell), ikke raaforekomster i prosa (B2/M1 }); }); +// R1 (review.md af16d5e4): 1.8.0 skrev om status-malen (commands/sporing.md:86-88) til den +// KANONISKE confidence-skalaen (okr-framework.md:389-392) — On Track / At Risk / Off Track. +// Hooken talte fortsatt kun det gamle norske vokabularet, saa nudgen doede stille under 1.8.0. +// Denne casen mater malen slik den faktisk genereres i dag; casen over beholder det gamle +// vokabularet og daekker dermed bakover-kompatibilitet for arkiverte status-filer. +test('at-risk telles paa kanonisk 1.8.0-vokabular (At Risk + Off Track, ikke On Track)', () => { + withWork((work) => { + writeConfig(work, '---\nid: "T2-2026"\n---\n'); + const statusDir = join(work, '.claude', 'okr', 'syklus', 'T2-2026'); + mkdirSync(statusDir, { recursive: true }); + // Emoji som \u-escapes: test-kilden holdes ASCII-ren (bash 3.2 set -u multibyte). + const GUL = '\u{1F7E1}'; + const GROENN = '\u{1F7E2}'; + const ROED = '\u{1F534}'; + writeFileSync(join(statusDir, 'status.md'), [ + '# Status T2-2026', + '', + '| KR | Baseline | Target | Naa | Score | Status |', + '|----|----------|--------|-----|-------|--------|', + `| KR1: Redusere ulykker | 40 | 30 | 35 | 0.50 | At Risk ${GUL} |`, + `| KR2: Fartshumper installert | 0% | 100% | 60% | 0.60 | On Track ${GROENN} |`, + `| KR3: Foreldre-tilfredshet | 60% | 90% | 65% | 0.17 | Off Track ${ROED} |`, + '', + ].join('\n')); + const out = runHook(work, '2026-06-20'); + assert.match( + out, + /OBS: 2 KR er merket/, + 'At Risk + Off Track teller (2); On Track skal IKKE telle', + ); + }); +}); + test('OKR_NOW sen fase: late-coaching', () => { withWork((work) => { writeConfig(work, '---\nid: "T2-2026"\n---\n'); diff --git a/tests/package-shape.test.mjs b/tests/package-shape.test.mjs index 5af73eb..8491391 100644 --- a/tests/package-shape.test.mjs +++ b/tests/package-shape.test.mjs @@ -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.8.0 (minor-lane C: En kanon), og at .npmrc slaar av +// er 1.8.0 (minor-lane C: En kanon) 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). @@ -67,3 +67,28 @@ test('package-lock.json: finnes og pinner transitive deps med integrity', () => assert.match(meta.version ?? '', /^\d/, `lockfile-oppfoering uten versjon: ${name}`); } }); + +// R3 (review.md 5e61ae0d): package.json er `private: true` og shipper ALDRI -- versjons- +// assertet over voktet dermed den ene flaten brukeren aldri ser. Polyrepo-ritualet +// (katalog-ref pinnet til release-tag) forutsetter at alle flater bumpes SAMTIDIG, saa en +// delvis bump skal bli ROED. Forventet versjon utledes fra package.json (ett sted aa endre). +const VERSION_SURFACES = [ + { file: '.claude-plugin/plugin.json', re: /"version":\s*"([^"]+)"/ }, + { file: 'README.md', re: /img\.shields\.io\/badge\/version-(\d+\.\d+\.\d+)-/ }, + { file: 'skills/okr-offentlig-sektor/SKILL.md', re: /^version:\s*"?([^"\s]+)"?\s*$/m }, + { file: 'skills/okr-second-brain-search/SKILL.md', re: /^version:\s*"?([^"\s]+)"?\s*$/m }, +]; + +test('versjonssync: alle shippede flater baerer package.json-versjonen', () => { + const expected = readPkg().version; + const drift = []; + for (const { file, re } of VERSION_SURFACES) { + const m = re.exec(readFileSync(join(ROOT, file), 'utf8')); + if (!m) { + drift.push(`${file}: fant ingen versjon (flaten flyttet? oppdater regexen)`); + continue; + } + if (m[1] !== expected) drift.push(`${file}: ${m[1]} != ${expected}`); + } + assert.deepEqual(drift, [], `delvis versjonsbump (forventet ${expected}):\n${drift.join('\n')}`); +});