// arkivklar.mjs // D7 steg 17: traverser en OKF-bundle og klassifiser innholdet mot // bevaringsforskrifta (FOR-2025-12-19-2729). REN modul -- den leser filer den // faar rota til, men tar ingen beslutning og skriver ingenting. // // Disklesing hoerer til scripts/, ikke lib/ (D6-beslutning 1). Denne modulen er // grensetilfellet: den MAA lese for aa klassifisere, men den bestemmer ikke hvor // rota ligger og formaterer ingen rapport. Den tar rota som argument, akkurat som // arsrapportDelIII tar {historikk} som data. // // PATH-CONFINEMENT SOM LESEGRENSE (NFR med navngitt suksesskriterium): // hvert lesemaal realpath-bekreftes under bundle-rota, via den DELTE regelen i // lib/path-confinement.mjs -- samme kode som ingestion bruker paa skrivesiden. // Tolags fordi de to lagene fanger ulike former; se path-confinement.mjs. // // MODULEN RETURNERER VURDERINGSGRUNNLAG, ALDRI EN AVGJOERELSE. Den sier hvilken // kategori noe TROLIG faller under, aldri hva som skal skje med det. Kassasjon // krever Nasjonalarkivets hjemmel (arkivlova § 13) etter en vurdering loven // legger til virksomhetens dokumentasjonsplan (arkivlova § 8, arkivforskrifta // § 12 b) -- ingen av delene kan avledes fra en filtype. import { readFileSync, readdirSync, realpathSync, statSync } from 'node:fs'; import path from 'node:path'; import { parseFrontmatter } from './frontmatter.mjs'; import { resolveUnderBundle, assertRealUnderBundle } from './path-confinement.mjs'; const HVEM = 'arkivklar'; // Bevaringsforskrifta § 7 (statleg sektor) navngir «tildelingsbrev, rapportar, // etatsstyringsmoeter og evalueringa»; § 30 (kommunal sektor) navngir // «handlingsprogram, tertialrapportering, aarsmelding». // https://lovdata.no/dokument/SF/forskrift/2025-12-19-2729 // // Laast arkitektur-beslutning 8: linjene er PARALLELLE, ikke alternative. En type // kan treffe en kategori i begge, og da rapporteres begge -- modulen velger ikke // styringslinje paa virksomhetens vegne. const KATEGORI_BY_TYPE = { Tildelingsbrev: { stat: 'tildelingsbrev', kommune: null }, Virksomhetsplan: { stat: null, kommune: 'handlingsprogram' }, Status: { stat: 'rapportar', kommune: 'tertialrapportering' }, Retrospektiv: { stat: 'evalueringa', kommune: 'aarsmelding' }, }; export const PARAGRAF = { stat: '§ 7', kommune: '§ 30', sikring: '§ 3', }; // Klassifiser en OKF-type mot begge styringslinjer. // // Sikringsparagrafen (§ 3) lukker resten: «All dokumentasjon som gjeld nye // oppgaaver [...] skal takast vare paa inntil Nasjonalarkivet har fastsett // reglar», og det samme for oppgaver som «av andre aarsaker ikkje er nemnde i // forskrifta». Arkivfaglig oppsummert: det som ikke er nevnt, skal bevares. // Ukjent type betyr derfor ALDRI «kan slettes» -- den er den mest forsiktige // kategorien, ikke den minst. export function klassifiser(type) { const treff = KATEGORI_BY_TYPE[type]; if (!treff) return { stat: null, kommune: null, sikring: true }; return { stat: treff.stat, kommune: treff.kommune, sikring: false }; } // Resolver et bundle-relativt lesemaal og bekreft at det faktisk ligger under // rota. Kaster ved confinement-brudd; lar ENOENT boble uendret (de to feilene // krever ulik handling hos kalleren -- se (17d)). export function lesemaal(bundleRoot, rel) { const resolvedBundle = path.resolve(bundleRoot); const realBundle = realpathSync(resolvedBundle); const maal = resolveUnderBundle(resolvedBundle, rel, HVEM); return assertRealUnderBundle(realBundle, maal, `lesemaal ${rel}`, HVEM); } // index.md er navigasjon (OKF-indeksformatet), ikke et konsept. Den skal ikke // klassifiseres -- ellers ville hver katalog produsert en falsk «Dokument»-rad. const ER_INDEKS = (navn) => navn === 'index.md'; function lesType(absolutt) { const { get } = parseFrontmatter(readFileSync(absolutt, 'utf8')); return get('type') ?? null; } // Traverser bundlen og returner vurderingsgrunnlag per konseptfil. // // readdirSync sorteres eksplisitt: traverseringsrekkefoelgen er en del av // kontrakten, siden rapporten nedstroems skal vaere byte-identisk mellom to // kjoeringer (samme determinisme-krav som historikk-lesingen i D6). export function traverserBundle(bundleRoot) { const resolvedBundle = path.resolve(bundleRoot); const realBundle = realpathSync(resolvedBundle); const filer = []; function gaa(relDir) { const absDir = relDir === '' ? resolvedBundle : resolveUnderBundle(resolvedBundle, relDir, HVEM); assertRealUnderBundle(realBundle, absDir, `katalog ${relDir || '.'}`, HVEM); for (const entry of readdirSync(absDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) { const rel = relDir === '' ? entry.name : `${relDir}/${entry.name}`; // Symlenker foelges ikke blindt: lesemaal realpath-bekrefter hver node, // saa en symlenket katalog som peker ut stopper traverseringen. const abs = lesemaal(resolvedBundle, rel); if (entry.isDirectory() || (entry.isSymbolicLink() && statSync(abs).isDirectory())) { gaa(rel); continue; } if (!entry.name.endsWith('.md') || ER_INDEKS(entry.name)) continue; const type = lesType(abs); filer.push({ rel, type, ...klassifiser(type) }); } } gaa(''); return { rot: realBundle, filer }; } // Klokke-soem: OKR_NOW (ISO-8601) overstyrer veggklokka, saa to kjoeringer over // samme tre gir byte-identisk rapport. Speiler lib/syklus-rapport.mjs:194. const klokke = (opts) => opts?.naa || process.env.OKR_NOW || new Date().toISOString(); // Rendrer vurderingsgrunnlaget. Formuleringen er en INVARIANT, ikke stil: // // - Hver kategori presenteres som «faller trolig under ... § N», med // paragrafhenvisning, og vurderingen legges eksplisitt til virksomhetens // dokumentasjonsplan (arkivlova § 8, arkivforskrifta § 12 b). // - Rapporten sier hva som boer VURDERES OVERFOERT til sak-arkivet. Den sier // aldri hva som skal slettes eller kasseres -- kassasjon krever // Nasjonalarkivets hjemmel (arkivlova § 13). Det finnes INGEN slettemodell i // denne pluginen, og ingen skal innfoeres: retensjonsbehovet er dekket av // /okr:oppsett arkiver og /okr:export. // - Arkivforskrifta § 1 bokstav c gjoer vurderingen betinget av om // dokumentasjonen allerede forvaltes som arkiv i et ANNET system. Det kan // verktoeyet ikke se, og rapporten navngir derfor tilstanden som ukjent i // stedet for aa anta den bort. export function rapport(grunnlag, opts) { const { rot, filer } = grunnlag; const l = []; l.push('# Arkivklar — vurderingsgrunnlag'); l.push(''); l.push(`Bundle: \`${rot}\``); l.push(`Generert: ${klokke(opts)}`); l.push(`Konseptfiler gjennomgaatt: ${filer.length}`); l.push(''); l.push('> **Dette er et vurderingsgrunnlag, ikke en avgjoerelse.** Rapporten viser hvilke'); l.push('> kategorier i bevaringsforskrifta (FOR-2025-12-19-2729) innholdet trolig faller'); l.push('> under, og hva som boer vurderes overfoert til virksomhetens sak-/arkivsystem.'); l.push('> Selve vurderingen hoerer til virksomhetens dokumentasjonsplan, jf. arkivlova § 8'); l.push('> og arkivforskrifta § 12 b. Kassasjon krever hjemmel fra Nasjonalarkivet etter'); l.push('> arkivlova § 13, og avgjoeres ikke her.'); l.push(''); l.push('## Ukjent forutsetning'); l.push(''); l.push('Arkivforskrifta § 1 bokstav c gjoer vurderingen betinget av om denne'); l.push('dokumentasjonen **allerede forvaltes som arkiv i et annet system**. Verktoeyet'); l.push('leser kun dette treet og kan ikke se sak-/arkivsystemet. Tilstanden er derfor'); l.push('**ukjent**, og maa avklares av virksomheten foer grunnlaget brukes.'); l.push(''); const seksjoner = [ ['stat', `Statleg styringslinje — bevaringsforskrifta ${PARAGRAF.stat}`, (f) => f.stat !== null], ['kommune', `Kommunal styringslinje — bevaringsforskrifta ${PARAGRAF.kommune}`, (f) => f.kommune !== null], ]; for (const [noekkel, tittel, filter] of seksjoner) { const treff = filer.filter(filter); l.push(`## ${tittel}`); l.push(''); if (treff.length === 0) { l.push('Ingen filer i treet traff en navngitt kategori i denne paragrafen.'); } else { l.push('| Fil | OKF-type | Navngitt kategori | Vurdering |'); l.push('|-----|----------|-------------------|-----------|'); for (const f of treff) { l.push(`| \`${f.rel}\` | ${f.type ?? '(ingen type)'} | ${f[noekkel]} | Boer vurderes overfoert til sak-arkivet |`); } } l.push(''); } const sikring = filer.filter((f) => f.sikring); l.push(`## Ikke navngitt — sikringsparagrafen ${PARAGRAF.sikring}`); l.push(''); l.push('Bevaringsforskrifta § 3 fastsetter at dokumentasjon som ikke er nevnt i'); l.push('forskrifta skal takast vare paa inntil Nasjonalarkivet har fastsett reglar.'); l.push('At en fil staar her betyr derfor at den skal **bevares**, ikke det motsatte.'); l.push(''); if (sikring.length === 0) { l.push('Ingen filer falt utenfor de navngitte kategoriene.'); } else { l.push('| Fil | OKF-type | Vurdering |'); l.push('|-----|----------|-----------|'); for (const f of sikring) { l.push(`| \`${f.rel}\` | ${f.type ?? '(ingen type)'} | Bevares inntil Nasjonalarkivet har fastsett reglar |`); } } l.push(''); return `${l.join('\n')}\n`; }