feat(okr): arkivklar-rapport med bevaringskategorier som vurderingsgrunnlag

This commit is contained in:
Kjell Tore Guttormsen 2026-08-02 10:23:11 +02:00
commit a9aab508d2
3 changed files with 255 additions and 2 deletions

View file

@ -18,14 +18,17 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, symlinkSync } from 'node:fs';
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync, symlinkSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { lesemaal, klassifiser, traverserBundle } from '../lib/arkivklar.mjs';
import { lesemaal, klassifiser, traverserBundle, rapport } from '../lib/arkivklar.mjs';
const FIXTURE = fileURLToPath(new URL('./fixtures/okf-realistic', import.meta.url));
const SCRIPT = fileURLToPath(new URL('../scripts/arkivklar.mjs', import.meta.url));
const NAA = '2026-08-02T10:00:00.000Z';
function withTmp(fn) {
const tmp = mkdtempSync(join(tmpdir(), 'arkivklar-'));
@ -190,3 +193,100 @@ test('traverserBundle: symlenket underkatalog som peker ut avvises', () => {
);
});
});
// ==================== (18) rapporten tar ikke stilling ====================
// FORBUDTE ORD skannes i den GENERERTE RAPPORTEN, aldri i kildefila.
// scripts/arkivklar.mjs og lib/arkivklar.mjs baerer selv ordene «slett»/«kassasjon»
// i forklarende prosa («ingen slettemodell finnes») -- en vakt som skannet kilden
// ville felt sin egen forbydende tekst og maattet mykes opp til den ble verdiloes.
// Det er S48-defektklassen; skann utfallet, ikke intensjonen.
const IMPERATIV = /\b(slett|slette|slettes|kasser|kassere|kasseres|fjern|fjernes)\b/i;
test('(18a) rapporten inneholder ingen kassasjons- eller slette-imperativ', () => {
const linjer = rapport(traverserBundle(FIXTURE), { naa: NAA }).split('\n');
const treff = linjer.filter((l) => IMPERATIV.test(l)).map((l) => l.trim());
assert.deepEqual(
treff,
[],
`rapporten skal aldri instruere kassasjon/sletting:\n${treff.join('\n')}`,
);
});
test('(18b) parser-sanity: IMPERATIV-vakten fanger faktisk et slikt imperativ', () => {
// Uten denne ville (18a) bestaatt trivielt paa en regex som ikke matcher noe.
assert.ok(IMPERATIV.test('Disse filene kan slettes etter 2027.'), 'vakten skal treffe et ekte imperativ');
assert.ok(!IMPERATIV.test('Vurderingen hoerer til virksomhetens dokumentasjonsplan.'), 'legitim prosa skal gaa klar');
});
test('(18c) rapporten baerer paragrafhenvisning og vurderingsformulering', () => {
const ut = rapport(traverserBundle(FIXTURE), { naa: NAA });
for (const forventet of ['§ 7', '§ 30', '§ 3', 'arkivlova § 8', 'arkivforskrifta § 12 b', 'vurder']) {
assert.ok(ut.toLowerCase().includes(forventet.toLowerCase()), `rapporten mangler «${forventet}»`);
}
});
test('(18d) rapporten navngir den ukjente tilstanden etter arkivforskrifta § 1 bokstav c', () => {
// Vurderingen er betinget av om dokumentasjonen ALLEREDE forvaltes som arkiv i
// et annet system -- en tilstand verktoeyet ikke kan se. Rapporten maa si det,
// ikke stilltiende anta det ene eller det andre.
const ut = rapport(traverserBundle(FIXTURE), { naa: NAA });
assert.ok(/§ 1 bokstav c/.test(ut), 'rapporten skal navngi arkivforskrifta § 1 bokstav c');
assert.ok(/ukjent/i.test(ut), 'rapporten skal navngi tilstanden som ukjent');
});
test('(18e) rapporten er deterministisk (to kjoeringer byte-identisk)', () => {
const a = rapport(traverserBundle(FIXTURE), { naa: NAA });
const b = rapport(traverserBundle(FIXTURE), { naa: NAA });
assert.equal(a, b, 'samme input + samme klokke skal gi byte-identisk rapport');
});
// ==================== (18f-h) CLI-form og exit-koder ====================
function kjor(args, env = {}) {
try {
const stdout = execFileSync(process.execPath, [SCRIPT, ...args], {
encoding: 'utf8',
env: { ...process.env, OKR_NOW: NAA, ...env },
});
return { code: 0, stdout };
} catch (e) {
return { code: e.status, stdout: e.stdout ?? '', stderr: e.stderr ?? '' };
}
}
test('(18f) CLI paa fixturen gir exit 0 og skriver rapporten til stdout', () => {
const r = kjor([FIXTURE]);
assert.equal(r.code, 0, `forventet exit 0, fikk ${r.code}`);
assert.ok(/§ 7/.test(r.stdout), 'stdout skal baere rapporten');
assert.deepEqual(
r.stdout.split('\n').filter((l) => IMPERATIV.test(l)),
[],
'CLI-utskriften skal heller ikke baere slette-imperativ',
);
});
test('(18g) CLI uten argument gir exit 2 (bruksfeil)', () => {
assert.equal(kjor([]).code, 2, 'manglende bundle-rot skal gi exit 2');
});
test('(18h) CLI paa ikke-eksisterende rot gir exit 2', () => {
withTmp((tmp) => {
assert.equal(kjor([join(tmp, 'finnes-ikke')]).code, 2, 'ukjent rot skal gi exit 2');
});
});
test('(18i) CLI skriver rapport atomisk til oppgitt fil, uten aa roere bundlen', () => {
withTmp((tmp) => {
const ut = join(tmp, 'arkivklar-rapport.md');
const r = kjor([FIXTURE, ut]);
assert.equal(r.code, 0, `forventet exit 0, fikk ${r.code}`);
const skrevet = readFileSync(ut, 'utf8');
assert.ok(/§ 7/.test(skrevet), 'fila skal baere rapporten');
assert.deepEqual(
readdirSync(tmp).filter((n) => n.endsWith('.tmp')),
[],
'ingen .tmp skal lekke (atomisk temp + rename)',
);
});
});