fix(okr): skjerp separasjonsvakten og lukk tre defekter i D6-leveransen
This commit is contained in:
parent
785385baa3
commit
d2ac153a68
5 changed files with 100 additions and 14 deletions
|
|
@ -175,12 +175,21 @@ test('(a2) F-c: ingen parallelle confidence-etikettsett utenfor kanon', () => {
|
|||
// i kalkulatoren), og aa dra den inn her ville blandet to spoersmaal i en vakt.
|
||||
//
|
||||
// To former, fordi bruddet har to skrivemaater:
|
||||
// ETIKETT -- "Samlet score: <verdi>" i en malblokk. De andre aksene gaar klar:
|
||||
// rubrikkens 0-10-skala har ingen verdi paa linjen, og det arkiverte
|
||||
// datafeltet `samlet_score` er snake_case, ikke "samlet score".
|
||||
// ETIKETT -- "Samlet score" sammen med en VERDI paa linjen (tall, X-plassholder
|
||||
// eller utfyllingsfelt). Verdien og etiketten bindes bevisst IKKE
|
||||
// til hverandre posisjonelt: "Samlet score (committed +
|
||||
// aspirational): 0.53" har noeyaktig det aggregatet vakten finnes
|
||||
// for, og et moenster som krevde verdien rett etter etiketten slapp
|
||||
// den gjennom (mutasjons-verifisert -- den gjorde det).
|
||||
// INSTRUKS -- "samlet (vektet gjennomsnitt)", som ber leseren lage aggregatet.
|
||||
// En linje som navngir typen er per definisjon differensiert og gaar klar.
|
||||
const AGGREGAT_ETIKETT = /samlet\s+score[:*\s]*[[\]0-9X_]/i;
|
||||
//
|
||||
// De andre aksene gaar klar: rubrikkens 0-10-skala har ingen verdi paa linjen,
|
||||
// og det arkiverte datafeltet `samlet_score` er snake_case, ikke "samlet score".
|
||||
// Mermaids akselabel rendrer det samme datafeltet i Title Case og unntas
|
||||
// eksplisitt -- arkiv-kontrakten er en annen sak enn maleneS rapportform.
|
||||
const AGGREGAT_ORD = /samlet\s+score/i;
|
||||
const VERDISLOT = /\d[.,]\d|\d\.XX|_{3,}|\bX{1,2}\b/;
|
||||
const MERMAID_AKSE = /^\s*[xy]-axis\b/;
|
||||
const AGGREGAT_INSTRUKS = /\bsamlet\b[^\n]{0,25}vektet\s+gjennomsnitt/i;
|
||||
|
||||
test('(k) separasjonsregelen: ingen mal emitterer et udifferensiert samlet-aggregat', () => {
|
||||
|
|
@ -190,8 +199,14 @@ test('(k) separasjonsregelen: ingen mal emitterer et udifferensiert samlet-aggre
|
|||
const violations = [];
|
||||
for (const f of maler) {
|
||||
readDoc(f).split('\n').forEach((line, i) => {
|
||||
if (/committed|aspirational/i.test(line)) return;
|
||||
if (AGGREGAT_ETIKETT.test(line) || AGGREGAT_INSTRUKS.test(line)) {
|
||||
// Differensiert = linjen navngir NOEYAKTIG EN type. En linje som nevner
|
||||
// begge ("Samlet score (committed + aspirational)") er nettopp det
|
||||
// udifferensierte aggregatet vakten finnes for -- den skal ikke slippe
|
||||
// gjennom paa aa ha uttalt ordene.
|
||||
if (/committed/i.test(line) !== /aspirational/i.test(line)) return;
|
||||
if (MERMAID_AKSE.test(line)) return;
|
||||
const etikett = AGGREGAT_ORD.test(line) && VERDISLOT.test(line);
|
||||
if (etikett || AGGREGAT_INSTRUKS.test(line)) {
|
||||
violations.push(`${f}:${i + 1}: ${line.trim()}`);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -383,6 +383,33 @@ for (const [navn, bygger] of FORMER) {
|
|||
});
|
||||
}
|
||||
|
||||
// Avviks-seksjonen viser til «forventningen til aspirational under». Den
|
||||
// seksjonen finnes bare naar syklusen HAR aspirational-KR. En syklus med bare
|
||||
// committed -- fullt lovlig, og typisk for en ren etterlevelses-syklus -- ville
|
||||
// faatt et styringsdokument som peker paa en seksjon som ikke er der. Fixturen
|
||||
// kan ikke fange det; den har begge typene.
|
||||
const KUN_COMMITTED = {
|
||||
id: 'T-kun-committed',
|
||||
okrer: [{
|
||||
fil: 'okr-etterlevelse.md',
|
||||
tittel: 'Etterlevelse',
|
||||
krer: [{ n: 1, navn: 'Palagte tilsyn gjennomfoert (antall)', baseline: 0, target: 12, naa: 9, type: 'committed' }],
|
||||
}],
|
||||
};
|
||||
|
||||
for (const [navn, bygger] of FORMER) {
|
||||
test(`(10f/${navn}) ingen henvisning til en aspirational-seksjon som ikke finnes`, () => {
|
||||
const md = bygger(KUN_COMMITTED, { naa: NAA });
|
||||
const viser = /forventningen til aspirational under/i.test(md);
|
||||
const finnes = /^##+\s+Aspirational/m.test(md);
|
||||
assert.equal(
|
||||
viser && !finnes,
|
||||
false,
|
||||
'avviks-seksjonen viser til en aspirational-seksjon rapporten ikke har',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Generatoren handhever regelen; governance-fila baerer begrunnelsen. Staar
|
||||
// begrunnelsen bare i koden, kan en leser overproeve vakten uten aa moete
|
||||
// argumentet -- og det er nettopp en Riksrevisjon-vendt leser som vil sporre.
|
||||
|
|
@ -586,6 +613,35 @@ test('(7i) CLI: etatsstyring skrives til sin egen fil', async (t) => {
|
|||
assert.equal(lesSyklus(dir).okrer.length, 2, 'rapporten ble lest som en OKR');
|
||||
});
|
||||
|
||||
// Fixturens historikk er FLAT prosa-filer. Den EKTE formen kommer fra
|
||||
// /okr:oppsett arkiver (commands/oppsett.md:514-518), som lager BEGGE deler:
|
||||
// katalogen `historikk/[id]/` med syklusens filer, OG den flate
|
||||
// `historikk/[id]-resultater.md` med sammendraget. Sammendraget er riktig
|
||||
// granularitet for et flerarig perspektiv; katalogen er raamaterialet under.
|
||||
// Uten denne testen ville fixtur, kode og (7h) vaert tre artefakter som er enige
|
||||
// med hverandre -- og ingen av dem den kommandoen som faktisk skriver treet.
|
||||
test('(7j) historikk leses fra arkiveringens EKTE form: sammendragsfil, ikke katalog', async (t) => {
|
||||
const { rmSync, mkdirSync, mkdtempSync, writeFileSync, copyFileSync, readdirSync, readFileSync: les } = await import('node:fs');
|
||||
const { tmpdir } = await import('node:os');
|
||||
const rot = mkdtempSync(join(tmpdir(), 'okr-arkiv-'));
|
||||
const dir = join(rot, 'syklus', 'T1-2026');
|
||||
const hist = join(rot, 'historikk');
|
||||
mkdirSync(dir, { recursive: true });
|
||||
mkdirSync(join(hist, 'T3-2025'), { recursive: true });
|
||||
for (const f of readdirSync(SYKLUS_DIR)) copyFileSync(join(SYKLUS_DIR, f), join(dir, f));
|
||||
// Kopien av syklusen ligger i katalogen; sammendraget flatt ved siden av.
|
||||
writeFileSync(join(hist, 'T3-2025', 'okr-gammel.md'), ['---', 'type: OKR', 'title: Gammel OKR', '---', ''].join('\n'));
|
||||
writeFileSync(join(hist, 'T3-2025-resultater.md'), ['---', 'type: Retrospektiv', 'title: Resultater T3-2025', '---', '# Resultater T3-2025', ''].join('\n'));
|
||||
t.after(() => rmSync(rot, { recursive: true, force: true }));
|
||||
|
||||
assert.equal((await kjoerCli([dir, 'arsrapport'])).code, 0);
|
||||
const kropp = les(join(dir, 'rapport-arsrapport.md'), 'utf8');
|
||||
assert.match(kropp, /Resultater T3-2025/, 'sammendragsfila fra arkiveringen ble ikke funnet');
|
||||
// Den arkiverte syklus-katalogen er raamateriale, ikke en post i det flerarige
|
||||
// perspektivet -- og `[id]/` ender ikke paa .md, saa den filtreres bort som seg selv.
|
||||
assert.doesNotMatch(kropp, /Gammel OKR/, 'generatoren gikk inn i den arkiverte syklus-katalogen');
|
||||
});
|
||||
|
||||
// Idempotens: rapporten skal kunne kjoeres om igjen uten aa endre bundlen, og
|
||||
// uten aa bli lest som en OKR neste gang (filnavnet starter ikke med okr-).
|
||||
test('(7f) CLI: to kjoeringer gir byte-identisk fil', async (t) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue