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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pUfkQ1YyH75z9y6RBaBHG
This commit is contained in:
Kjell Tore Guttormsen 2026-07-25 12:33:55 +02:00
commit 44ceec7205
16 changed files with 189 additions and 27 deletions

View file

@ -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')}`,
);
});

View file

@ -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');

View file

@ -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')}`);
});