test(okr): kanon-konsistensvakt RED-baseline (F-a..F-i)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZ4qHkTDUn9tDRi8YS8cF8
This commit is contained in:
parent
056b6522f1
commit
5166dbf772
1 changed files with 227 additions and 0 deletions
227
tests/canon-consistency.test.mjs
Normal file
227
tests/canon-consistency.test.mjs
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
// canon-consistency.test.mjs
|
||||
// 1.8.0 "En kanon": deterministisk konsistensvakt som laaser F-a..F-i-konsolideringen
|
||||
// (review-2026-07-16.md §3). Offline, zero-dep, node:test. Hver case sporer til ett
|
||||
// review-funn og blir GROENN naar dens SISTE bidragende fil refererer kanon; RED-baseline
|
||||
// etableres foer konsumentene refererer kanon (TDD Iron Law).
|
||||
//
|
||||
// Norsk markdown-prosa matches via \uXXXX-escapes (test-kilde holdes ASCII-ren; unngaar
|
||||
// bash 3.2 set -u multibyte-krasj). Kollekter-til-array -> assert.deepEqual([], msg) som B1.
|
||||
// Moenster: tests/reference-integrity.test.mjs + tests/package-shape.test.mjs
|
||||
// Kilde: review-2026-07-16.md §3 (F-a..F-i, fil:linje)
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const REF = 'skills/okr-offentlig-sektor/references';
|
||||
|
||||
// --- delte parse-hjelpere (hver med egen sanity-case, jf. B1 refs.length >= 10) ---
|
||||
|
||||
function readDoc(rel) {
|
||||
return readFileSync(join(ROOT, rel), 'utf8');
|
||||
}
|
||||
|
||||
function mdFiles(dir) {
|
||||
return readdirSync(join(ROOT, dir))
|
||||
.filter((n) => n.endsWith('.md'))
|
||||
.sort()
|
||||
.map((n) => join(dir, n));
|
||||
}
|
||||
|
||||
// Kontig. pipe-tabell-blokker: hver blokk = sammenhengende linjer som (trimmet) starter med '|'.
|
||||
function mdTables(body) {
|
||||
const tables = [];
|
||||
let cur = [];
|
||||
for (const line of body.split('\n')) {
|
||||
if (line.trim().startsWith('|')) {
|
||||
cur.push(line);
|
||||
} else if (cur.length) {
|
||||
tables.push(cur);
|
||||
cur = [];
|
||||
}
|
||||
}
|
||||
if (cur.length) tables.push(cur);
|
||||
return tables;
|
||||
}
|
||||
|
||||
function linesMatching(body, re) {
|
||||
return body.split('\n').filter((l) => re.test(l));
|
||||
}
|
||||
|
||||
function headingsOf(body, re) {
|
||||
return body
|
||||
.split('\n')
|
||||
.filter((l) => re.test(l))
|
||||
.map((l) => l.replace(/^#+\s+/, '').trim());
|
||||
}
|
||||
|
||||
// Confidence-tabell-signatur: en pipe-tabell hvis label-vokabular baerer alle tre nivaaer.
|
||||
// ALDRI match paa tokenet "50%" eller ordet "confidence" (unngaar falsk-positiv paa
|
||||
// score-aritmetikk / prosa).
|
||||
function isConfidenceTable(tableLines) {
|
||||
const txt = tableLines.join('\n');
|
||||
return /on track/i.test(txt) && /at risk/i.test(txt) && /off track/i.test(txt);
|
||||
}
|
||||
|
||||
// ==================== parser-sanity (jf. B1) ====================
|
||||
|
||||
test('parser-sanity: headingsOf finner 10 rubrikk-dimensjoner', () => {
|
||||
const dims = headingsOf(readDoc(`${REF}/okr-quality-rubrics.md`), /^### /);
|
||||
assert.equal(dims.length, 10, `forventet 10 dims, fant ${dims.length}`);
|
||||
});
|
||||
|
||||
test('parser-sanity: mdTables finner confidence-tabellen i okr-framework.md', () => {
|
||||
const conf = mdTables(readDoc(`${REF}/okr-framework.md`)).filter(isConfidenceTable);
|
||||
assert.ok(conf.length >= 1, `forventet minst 1 confidence-tabell i framework, fant ${conf.length}`);
|
||||
});
|
||||
|
||||
test('parser-sanity: linesMatching finner check-in-linjer i okr-framework.md', () => {
|
||||
const hits = linesMatching(readDoc(`${REF}/okr-framework.md`), /check-?in/i);
|
||||
assert.ok(hits.length >= 2, `forventet >= 2 check-in-linjer, fant ${hits.length}`);
|
||||
});
|
||||
|
||||
// ==================== F-a..F-i konsistensvakt ====================
|
||||
|
||||
// (a) F-c confidence EN gang. RED til calculator-dedup (Step 6).
|
||||
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) {
|
||||
if (mdTables(readDoc(f)).some(isConfidenceTable)) found.push(f);
|
||||
}
|
||||
assert.deepEqual(
|
||||
found,
|
||||
[`${REF}/okr-framework.md`],
|
||||
`confidence-tabell skal finnes noeyaktig EN gang (okr-framework.md); fant: ${found.join(', ')}`,
|
||||
);
|
||||
});
|
||||
|
||||
// (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')];
|
||||
// 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;
|
||||
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()}`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(violations, [], `kadens-motsigelse (kadens x publikum):\n${violations.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', () => {
|
||||
const files = ['commands/kvalitet.md', 'agents/kvalitetssjekker-agent.md'];
|
||||
const hits = [];
|
||||
for (const f of files) {
|
||||
for (const l of linesMatching(readDoc(f), /8-10/)) hits.push(`${f}: ${l.trim()}`);
|
||||
}
|
||||
assert.deepEqual(hits, [], `eget scoreband/inline-rubrikk (8-10) skal referere okr-quality-rubrics.md:5:\n${hits.join('\n')}`);
|
||||
});
|
||||
|
||||
// (d) F-g antipattern-antall drift-laas. GROENN i dag (B1 lukket 19->20). Aldri hardkod 20.
|
||||
test('(d) F-g: prosa-omtaler av antipattern-antall == dynamisk telling (drift-laas)', () => {
|
||||
const anti = readDoc(`${REF}/okr-antipatterns.md`);
|
||||
const antipatternCount = linesMatching(anti, /^### \d+\.\d+/).length;
|
||||
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 drift = [];
|
||||
for (const f of scan) {
|
||||
for (const m of readDoc(f).matchAll(/(\d+)\s+antipatterns\b/gi)) {
|
||||
if (Number(m[1]) !== antipatternCount) drift.push(`${f}: "${m[0].trim()}" != ${antipatternCount}`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(drift, [], `antipattern-antall drift (telt = ${antipatternCount}):\n${drift.join('\n')}`);
|
||||
});
|
||||
|
||||
// (e) F-g kategori-navn. RED til Step 7 (analyse/trendanalytiker bruker de 5 ekte navnene).
|
||||
test('(e) F-g: analyse + trendanalytiker uten oppdiktede antipattern-kategorier', () => {
|
||||
const files = ['commands/analyse.md', 'agents/trendanalytiker-agent.md'];
|
||||
const invented = /Ambisjonsbalanse|Organisatoriske|Offentlig sektor-spesifikke|Offentlig-spesifikke/;
|
||||
const hits = [];
|
||||
for (const f of files) {
|
||||
for (const l of linesMatching(readDoc(f), invented)) hits.push(`${f}: ${l.trim()}`);
|
||||
}
|
||||
assert.deepEqual(hits, [], `oppdiktede kategorier (ekte: Formulering/Prosess/Kultur/Struktur/Ledelse):\n${hits.join('\n')}`);
|
||||
});
|
||||
|
||||
// (f) F-h binaer/milepael. RED til Step 10. antipatterns = 0 binaer (laas); kvalitet uten Ja/Nei-bullet.
|
||||
test('(f) F-h: antipatterns 0 binaer-omtaler (laas) + kvalitet uten Ja/Nei-binaerbullet', () => {
|
||||
const antiBinaer = linesMatching(readDoc(`${REF}/okr-antipatterns.md`), /bin\u00e6r/i);
|
||||
const kvalBinaer = linesMatching(readDoc('commands/kvalitet.md'), /Ja\/Nei|Bin\u00e6re KR/);
|
||||
const problems = [
|
||||
...antiBinaer.map((l) => `okr-antipatterns.md (skal ha 0 binaer): ${l.trim()}`),
|
||||
...kvalBinaer.map((l) => `kvalitet.md antipattern-bullet: ${l.trim()}`),
|
||||
];
|
||||
assert.deepEqual(problems, [], `binaer/milepael-inkonsistens:\n${problems.join('\n')}`);
|
||||
});
|
||||
|
||||
// (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));
|
||||
assert.deepEqual(missing, [], `kvalitetssjekker-agent mangler rubrikk-dims: ${missing.join(', ')}`);
|
||||
});
|
||||
|
||||
// (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 = [];
|
||||
for (const f of ['commands/analyse.md', 'agents/trendanalytiker-agent.md']) {
|
||||
const body = readDoc(f);
|
||||
// Avlednings-stigen identifiseres av de unike etikettene Utforsker + Skalering (ikke
|
||||
// "modenhet"-prosa, som selvrapportfeltet beholder).
|
||||
if (/Utforsker/i.test(body) && /Skalering/i.test(body)) {
|
||||
problems.push(`${f}: score->modenhet-avledning til stede (Utforsker+Skalering)`);
|
||||
}
|
||||
}
|
||||
const trend = readDoc('agents/trendanalytiker-agent.md');
|
||||
if (!(/0\.85/.test(trend) && /sandbagging/i.test(trend))) {
|
||||
problems.push('trendanalytiker-agent.md: mangler sandbagging-trendsignal (>0.85 over 2+ sykluser)');
|
||||
}
|
||||
assert.deepEqual(problems, [], `F-a auto-regel I:\n${problems.join('\n')}`);
|
||||
});
|
||||
|
||||
// (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)).
|
||||
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')}`);
|
||||
});
|
||||
|
||||
// (j) score-grenser doc-invariant (ingen kode beregner score). RED til Step 6. To distinkte asserts.
|
||||
test('(j) score-grenser: okr-calculator dokumenterer kapp [0,1.0] OG div-paa-null-regel', () => {
|
||||
const calc = readDoc(`${REF}/okr-calculator.md`);
|
||||
const problems = [];
|
||||
const hasCap = /kapp/i.test(calc) || /\[0,?\s*1[.,]0\]/.test(calc) || /maksimalt\s+1[.,]0/i.test(calc);
|
||||
const hasDivNull =
|
||||
/udefinert/i.test(calc) || /Target\s*==\s*Baseline/i.test(calc) || /0 m\u00e5lbare/i.test(calc);
|
||||
if (!hasCap) problems.push('mangler kapp-regel [0, 1.0] (over-/underoppnaaelse)');
|
||||
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')}`);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue