200 lines
9.2 KiB
JavaScript
200 lines
9.2 KiB
JavaScript
// 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`;
|
|
}
|