fix(okr): fase D patch-lane D1-D3 (vakter som ikke voktet)
Tre C7-review-funn der vakten var groenn fordi den ikke KUNNE se defekten sin.
Alle tre TDD: vakten skrevet om foerst, mutasjons-verifisert roed mot ekte
filer, deretter kilden rettet. Suite 179 -> 182.
D1 (607313e3) tests/canon-consistency.test.mjs case (b) -- F-d-kadensvakten
kunne ikke feile paa den KANONISKE kadens-tabellen. teamMonthly bandt kadens-
adjektivet direkte til check-in med \s+, men etter "Maanedlig" kommer "**" og
en cellevegg, aldri whitespace; ledWeekly krevde /review/, mens den kanoniske
raden sier "statusgjennomgang". Vakten var altsaa blind paa nettopp det
artefaktet F-d produserer. Erstattet av to former med ulik struktur: prosa
(kadens + binding i SAMME klausul -- setning/komma/celle/" og ") og kadens-rad
(kadens-adjektivet alene i foerste celle, binding paa tvers av celleveggene).
Klausul-splittingen er det som skiller korrekt dobbeltrytme ("Ukentlig 15-min
check-in + maanedlig 30-min review", okr-implementation.md:181) fra motsigelse.
To nye fixture-caser holder hverandre i sjakk: (b2) de fire muterte kanoniske
linjene MAA flagges, (b3) ti legitime former MAA ikke.
D2 (7579d59c) case (g) -- agent.includes(d) er substring-containment, saa
rubrikk-dimensjonen "Outcome" var subsumert av "Outcome-fokus" og kunne
slettes uten at vakten falt. Ni av ti dims var ekte dekket; den tiende var
utestbar. Matcher naa token-grenser (uthevede listeledd + overskrifter).
Ny fixture (g2) pinner skillet.
D3 (029ef814) case (i) + commands/freshen-references.md:24 -- vakten
ekskluderte to filer permanent under en kommentar som kalte dem
"allerede-fiksede". freshen-references.md var aldri fikset; den falt utenfor
review-lista paa ni, og bar fortsatt "OKR-kontekst injiseres automatisk via
hook" -- en eksakt match for vaktens egen frase. Begge ekskluderinger fjernet
(analyse.md-ekskluderingen var dead code: fila matcher ingenting), kontekst-
blokken skrevet om til analyse.md-moensteret (oppdag fra disk via Glob).
Mutasjons-verifisering (alle fem forble groenne FOER, roede ETTER):
okr-framework.md:54 -> team maanedlig check-in -> (b) RED
okr-framework.md:55 -> ledelse ukentlig -> (b) RED
okr-framework.md:58 -> prosa "holdes maanedlig" -> (b) RED
okr-framework.md:59 -> prosa "holdes ukentlig" -> (b) RED
kvalitetssjekker-agent.md:48 slettet (Outcome) -> (g) RED
Artefakt: .claude/projects/2026-07-23-en-kanon-metodekonsolidering/review.md
This commit is contained in:
parent
52129b566f
commit
6b61d63196
2 changed files with 132 additions and 22 deletions
|
|
@ -163,26 +163,112 @@ test('(a2) F-c: ingen parallelle confidence-etikettsett utenfor kanon', () => {
|
|||
);
|
||||
});
|
||||
|
||||
// --- F-d kadens x publikum (D1 / review.md 607313e3) ---
|
||||
// Den forrige vakten bandt kadens-adjektivet DIREKTE til check-in (\s+), og kunne derfor
|
||||
// ikke feile paa den KANONISKE tabellen: etter "Maanedlig" kommer "**" og en cellevegg,
|
||||
// aldri whitespace -- vakten var blind paa nettopp det artefaktet F-d produserer. ledWeekly
|
||||
// krevde i tillegg /review/, mens den kanoniske raden sier "statusgjennomgang".
|
||||
// To former, fordi strukturen er ulik:
|
||||
// PROSA -- kadens og binding i SAMME klausul. Klausul-splittingen er det som skiller
|
||||
// "Ukentlig 15-min check-in + maanedlig 30-min review" (korrekt dobbeltrytme,
|
||||
// okr-implementation.md:181) fra "Team-check-ins holdes maanedlig".
|
||||
// TABELL -- kadens-adjektivet ALENE i foerste celle; da baerer resten av raden publikum
|
||||
// og innhold, og bindingen gaar PAA TVERS av celleveggene.
|
||||
// Team er default; publikum-markoer ledelse/ledergruppe hever til ledelses-rytme.
|
||||
const WEEKLY = /\bukentlige?\b/i;
|
||||
const MONTHLY = /\bm\u00e5nedlige?\b/i;
|
||||
const CHECKIN = /check-?ins?\b/i;
|
||||
const LEAD_AUDIENCE = /ledelse|ledergruppe|ledelses/i;
|
||||
const LEAD_REVIEW = /review|statusgjennomgang|okr-status/i;
|
||||
|
||||
// Klausul = setning / listeledd / celle / konjunksjon. Uten " og "-splittet blir "ukentlige
|
||||
// team check-ins og maanedlige reviews" (okr-implementation.md:376) en falsk positiv.
|
||||
function clauses(line) {
|
||||
return line.split(/[.,;|+]|\sog\s/i);
|
||||
}
|
||||
|
||||
function cadenceProseViolation(line) {
|
||||
for (const c of clauses(line)) {
|
||||
if (MONTHLY.test(c) && CHECKIN.test(c) && !LEAD_AUDIENCE.test(c)) return 'team-check-in maanedlig';
|
||||
if (WEEKLY.test(c) && LEAD_AUDIENCE.test(c) && LEAD_REVIEW.test(c)) return 'ledelsesrytme ukentlig';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Kadens-noekklet rad = foerste celle ER kadens-adjektivet (etter stripping av utheving).
|
||||
// Rader som bare BEGYNNER med et kadensord ("| Ukentlig check-in | Team OKR-eier |",
|
||||
// okr-implementation.md:490) er ikke kadens-noekklede og hoerer til prosa-formen.
|
||||
function cadenceRowViolation(row) {
|
||||
const cells = row
|
||||
.split('|')
|
||||
.map((c) => c.replace(/[*_`]/g, '').trim())
|
||||
.filter((c) => c.length > 0);
|
||||
if (cells.length < 2) return null;
|
||||
const key = cells[0];
|
||||
const rest = cells.slice(1).join(' ');
|
||||
if (/^m\u00e5nedlige?$/i.test(key) && CHECKIN.test(rest) && !LEAD_AUDIENCE.test(rest)) {
|
||||
return 'team-check-in maanedlig (kadens-rad)';
|
||||
}
|
||||
if (/^ukentlige?$/i.test(key) && LEAD_AUDIENCE.test(rest)) {
|
||||
return 'ledelsesrytme ukentlig (kadens-rad)';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// (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 = 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.
|
||||
const teamMonthly = /(m\u00e5nedlig|m\u00e5nedlige)\s+(okr\s+|team[-\s])?check-?ins?/i;
|
||||
const ledWeekly = /(ukentlig|ukentlige)[^\n]*\b(ledelse|ledergruppe|ledelses)\b/i;
|
||||
test('(b) F-d: ingen team-check-in maanedlig, ingen ledelsesrytme ukentlig', () => {
|
||||
const violations = [];
|
||||
for (const f of scan) {
|
||||
const body = readDoc(f);
|
||||
for (const l of linesMatching(body, teamMonthly)) violations.push(`${f}: ${l.trim()}`);
|
||||
for (const l of linesMatching(body, ledWeekly)) {
|
||||
if (/review/i.test(l)) violations.push(`${f}: ${l.trim()}`);
|
||||
}
|
||||
for (const f of canonScan()) { // R4: utvidet fra references/+commands/
|
||||
readDoc(f)
|
||||
.split('\n')
|
||||
.forEach((line, i) => {
|
||||
const why = cadenceProseViolation(line) ?? cadenceRowViolation(line);
|
||||
if (why) violations.push(`${f}:${i + 1}: ${why}: ${line.trim()}`);
|
||||
});
|
||||
}
|
||||
assert.deepEqual(violations, [], `kadens-motsigelse (kadens x publikum):\n${violations.join('\n')}`);
|
||||
});
|
||||
|
||||
// (b2) D1 negative fixtures: de tre muterte kanoniske linjene review.md 607313e3 beviste at
|
||||
// (b) IKKE kunne se (okr-framework.md:54, :55, :58 flippet), + prosa-tvillingen av rad-
|
||||
// mutasjonen. Syntetiske strenger -- muterer aldri en ekte fil, men faller hvis moensteret
|
||||
// slakkes tilbake til noe som ikke naar den kanoniske tabellen.
|
||||
test('(b2) F-d: vakten flagger de muterte kanoniske kadens-linjene', () => {
|
||||
const mutations = [
|
||||
'| **M\u00e5nedlig** | Team | Team-check-in (15 min): fremdrift, blokkere, neste steg |',
|
||||
'| **Ukentlig** | Ledelse/ledergruppe | OKR-statusgjennomgang: retning, prioritering |',
|
||||
'Team-check-ins holdes **m\u00e5nedlig** (teamet selv).',
|
||||
'Ledelsens OKR-statusgjennomgang holdes **ukentlig**.',
|
||||
];
|
||||
const unflagged = mutations.filter((l) => !(cadenceProseViolation(l) ?? cadenceRowViolation(l)));
|
||||
assert.deepEqual(unflagged, [], `mutert kanon slapp gjennom vakten:\n${unflagged.join('\n')}`);
|
||||
});
|
||||
|
||||
// (b3) D1 positive fixtures: de legitime formene vakten IKKE skal roere. Uten denne er (b2)
|
||||
// oppfylt av en vakt som flagger alt -- de to casene holder hverandre i sjakk.
|
||||
test('(b3) F-d: vakten flagger IKKE kanoniske/legitime kadens-linjer', () => {
|
||||
const legit = [
|
||||
'| **Ukentlig** | Team | Team-check-in (15 min): fremdrift, blokkere, neste steg |',
|
||||
'| **M\u00e5nedlig** | Ledelse/ledergruppe | OKR-statusgjennomgang: retning, eskalering |',
|
||||
'| **Progresjon** | M\u00e5nedlig status | Oboard check-ins |',
|
||||
'| Ukentlig check-in | Team OKR-eier | x4 |',
|
||||
'Ukentlig 15-min check-in + m\u00e5nedlig 30-min review er mindre enn mange bruker i dag',
|
||||
'Etabler rytme med ukentlige team check-ins og m\u00e5nedlige reviews.',
|
||||
'5. **Continuous Tracking**: Ukentlig i team, m\u00e5nedlig til ledelsen',
|
||||
'Tracking: Ukentlig team-check-in, m\u00e5nedlig status til ledergruppe',
|
||||
'Team-check-ins holdes **ukentlig** (teamet selv).',
|
||||
'Ledelsens OKR-statusgjennomgang holdes **m\u00e5nedlig**.',
|
||||
];
|
||||
const flagged = legit
|
||||
.map((l) => [l, cadenceProseViolation(l) ?? cadenceRowViolation(l)])
|
||||
.filter(([, why]) => why);
|
||||
assert.deepEqual(
|
||||
flagged,
|
||||
[],
|
||||
`legitim kadens-form flagget:\n${flagged.map(([l, w]) => `${w}: ${l}`).join('\n')}`,
|
||||
);
|
||||
});
|
||||
|
||||
// (c) F-e scoreband == rubrics. RED til kvalitet/kvalitetssjekker refererer rubrics:5 (Step 10).
|
||||
// 8-10-baand ELLER "Score 8-10"-kolonne = eget scoreband/inline-rubrikk (rubrics-toppbaand = 9-10).
|
||||
test('(c) F-e: kvalitet + kvalitetssjekker baerer ingen egen 8-10 scoreband/inline-rubrikk', () => {
|
||||
|
|
@ -233,15 +319,36 @@ test('(f) F-h: antipatterns 0 binaer-omtaler (laas) + kvalitet uten Ja/Nei-binae
|
|||
assert.deepEqual(problems, [], `binaer/milepael-inkonsistens:\n${problems.join('\n')}`);
|
||||
});
|
||||
|
||||
// --- F-e rubrikk-dimensjoner (D2 / review.md 7579d59c) ---
|
||||
// agent.includes(d) er substring-containment: "Outcome" er subsumert av "Outcome-fokus", saa
|
||||
// den dimensjonen kunne slettes uten at vakten falt -- den ENE dimensjonen F-e-kriteriet
|
||||
// finnes for aa garantere var den ene vakten ikke kunne se. Dimensjonene er skrevet som
|
||||
// uthevede listeledd (- **Navn** ...) eller overskrifter; begge er token-grenser.
|
||||
function dimTokens(body) {
|
||||
const tokens = new Set();
|
||||
for (const m of body.matchAll(/\*\*([^*\n]+)\*\*/g)) tokens.add(m[1].trim());
|
||||
for (const h of headingsOf(body, /^#{2,6} /)) tokens.add(h);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
// (g) F-e agent 10 dims. RED til Step 10. Utled dimensjonsnavnene, ikke hardkod.
|
||||
test('(g) F-e: kvalitetssjekker-agent daekker alle 10 rubrikk-dimensjoner', () => {
|
||||
const dims = headingsOf(readDoc(`${REF}/okr-quality-rubrics.md`), /^### /);
|
||||
assert.equal(dims.length, 10, `parser-sanity: forventet 10 dims, fant ${dims.length}`);
|
||||
const agent = readDoc('agents/kvalitetssjekker-agent.md');
|
||||
const missing = dims.filter((d) => !agent.includes(d));
|
||||
const tokens = dimTokens(readDoc('agents/kvalitetssjekker-agent.md'));
|
||||
const missing = dims.filter((d) => !tokens.has(d));
|
||||
assert.deepEqual(missing, [], `kvalitetssjekker-agent mangler rubrikk-dims: ${missing.join(', ')}`);
|
||||
});
|
||||
|
||||
// (g2) D2 fixture: token-grensen skiller "Outcome" fra "Outcome-fokus". Med substring-
|
||||
// containment var denne dimensjonen utestbar -- sletting av agent-linjen forble groenn.
|
||||
test('(g2) F-e: dim-vakten skiller Outcome fra Outcome-fokus', () => {
|
||||
const tokens = dimTokens([' - **Outcome-fokus** - oensket tilstand', '### Datakilde'].join('\n'));
|
||||
assert.ok(tokens.has('Outcome-fokus'), 'uthevet listeledd skal gi token');
|
||||
assert.ok(tokens.has('Datakilde'), 'overskrift skal gi token');
|
||||
assert.ok(!tokens.has('Outcome'), '"Outcome" skal IKKE dekkes av "Outcome-fokus"');
|
||||
});
|
||||
|
||||
// (h) F-a fjerning. RED til Step 7. Score->modenhet-avledning borte; sandbagging-trendsignal til stede.
|
||||
test('(h) F-a: ingen score->modenhet-avledning; sandbagging-trendsignal til stede', () => {
|
||||
const problems = [];
|
||||
|
|
@ -261,13 +368,15 @@ test('(h) F-a: ingen score->modenhet-avledning; sandbagging-trendsignal til sted
|
|||
});
|
||||
|
||||
// (i) F-i kontekst. RED til Step 13. Spesifikke fjernede-betingelse-fraser, ALDRI bar "er listet".
|
||||
// Ekskluderer allerede-fiksede analyse.md + freshen-references.md (plan §3 case (i)).
|
||||
// D3 (review.md 029ef814): begge ekskluderingene er FJERNET. Kommentaren kalte filene
|
||||
// "allerede-fiksede"; commands/freshen-references.md var aldri fikset -- den falt utenfor
|
||||
// review-lista paa ni, saa vakten kunne PER KONSTRUKSJON aldri fange den ene levende
|
||||
// forekomsten den var skrevet for. commands/analyse.md-ekskluderingen var dead code
|
||||
// (fila matcher ingen av frasene). En permanent ekskludering er en vakt som ikke vokter.
|
||||
test('(i) F-i: ingen foreldede kontekst-injeksjons-fraser i commands/', () => {
|
||||
const phrases = /injiseres automatisk via hook|Hvis relevante filer er listet|aktive OKR-filer er listet|listet i system-kontekst/;
|
||||
const exclude = new Set(['commands/analyse.md', 'commands/freshen-references.md']);
|
||||
const hits = [];
|
||||
for (const f of mdFiles('commands')) {
|
||||
if (exclude.has(f)) continue;
|
||||
for (const l of linesMatching(readDoc(f), phrases)) hits.push(`${f}: ${l.trim()}`);
|
||||
}
|
||||
assert.deepEqual(hits, [], `foreldet post-1.6.0 kontekst-blokk (bruk analyse.md:13-20-moenster):\n${hits.join('\n')}`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue