ms-ai-architect/scripts/kb-update/report-verified-staleness.mjs
Kjell Tore Guttormsen fea5df5e68 feat(ms-ai-architect): Spor 3 Port 3 (inkrementell) — stale-since-verified klassifiserer + CLI [skip-docs]
Port 3s billige, lastmod-synlige halvdel av kadens-judgen. Forutsetningen kom
med Port 2: filene bærer nå **Verified:**, så «sitemap_lastmod > verified»-
sammenligningen er endelig mulig.

lib/verified-staleness.mjs (rent, speiler verify-out — skriver aldri til disk):
- classifyVerifiedStaleness: reference-scope (Port 1-kontrakt). Buckets:
  stale-since-verified (kilde endret strikt etter verify → flagg), unverified
  (type=reference men verified=null, kontraktbrudd → flagg), unmigrated
  (type=null legacy → Spor 1-bro, telles), out-of-scope (template/methodology/
  regulatory), fresh. YYYY-MM→-01-normalisering, strikt > (lik dato = fresh).
- buildFileSourceMap: registry-inversjon (URL→filer ⇒ fil→nyeste lastmod),
  kun tracked + lastmod. Delt med full-pass (P3d).
- buildStalenessReport: join header↔lastmod, klassifiser, sorter (stale før
  unverified, nyeste drift først); reader injisert (testbar uten disk).

report-verified-staleness.mjs: tynn I/O-glue (speiler report-changes), skriver
gitignored verified-staleness-report.json. Kjøres på KB-refresh-kadens, IKKE
SessionStart (hooken bare LESER cachen — P3b).

Ekte kjøring: 306 ref-filer m/ tracked kilder, alle unmigrated (Port 1 ikke
backfilllet til legacy-korpus ennå = Spor 1). Mekanismen rapporterer ærlig
grunntilstand korrekt.

TDD (Iron Law): 18 nye tester. Suite 604/604.
2026-06-29 14:53:51 +02:00

68 lines
3.2 KiB
JavaScript

#!/usr/bin/env node
// report-verified-staleness.mjs — Spor 3 Port 3, INCREMENTAL cadence pass.
// Compares each tracked source's sitemap_lastmod to the reference file's
// **Verified:** header (the Port 1 born-verified stamp) and reports the files
// whose source changed AFTER they were last verified — the cheap, lastmod-
// visible drift the periodic full-pass (P3d) complements.
//
// Run on the KB-refresh cadence, NOT on SessionStart (the SessionStart hook
// only READS the cached report this writes — the reporting floor, P3b). All
// logic lives in lib/verified-staleness.mjs (pure, unit-tested); this file is
// I/O glue mirroring report-changes.mjs.
//
// Usage: node report-verified-staleness.mjs [--json]
import { readFileSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { loadRegistry, saveReport } from './lib/registry-io.mjs';
import { parseTypeHeader, parseVerifiedHeader } from './lib/kb-headers.mjs';
import { buildStalenessReport } from './lib/verified-staleness.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const PLUGIN_ROOT = join(__dirname, '..', '..');
const DATA_DIR = join(__dirname, 'data');
const jsonOnly = process.argv.includes('--json');
// Read a reference file's Port 1 headers, or null if the file is gone on disk
// (registry hygiene, not a staleness signal — buildStalenessReport skips it).
// Only the top 500 bytes are read; both header parsers scan that region.
function headerReader(refFile) {
const full = join(PLUGIN_ROOT, refFile);
if (!existsSync(full)) return null;
const head = readFileSync(full, 'utf8').slice(0, 500);
return { type: parseTypeHeader(head), verified: parseVerifiedHeader(head) };
}
const registry = loadRegistry(DATA_DIR);
if (!registry.last_poll) {
console.error('Registry has not been polled yet. Run poll-sitemaps.mjs first.');
process.exit(1);
}
const today = new Date().toISOString().split('T')[0];
const report = buildStalenessReport(registry, headerReader, { today });
saveReport('verified-staleness-report.json', report, DATA_DIR);
if (jsonOnly) {
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
} else {
const c = report.counts;
console.log(`\n=== Verified-staleness Report (${report.generated_at}) — incremental ===`);
console.log(`Sources last polled: ${report.last_poll}`);
console.log(
`Reference files w/ tracked sources: ${c.total} ` +
`(fresh: ${c.fresh}, stale-since-verified: ${c.stale}, unverified: ${c.unverified}, unmigrated: ${c.unmigrated}, out-of-scope: ${c.outOfScope})`,
);
if (report.flagged.length > 0) {
console.log(`\nFlagged for re-judge (${report.flagged.length}):`);
for (const f of report.flagged.slice(0, 30)) {
console.log(` [${f.reason}] ${f.path}`);
console.log(` verified: ${f.verified ?? '—'} source changed: ${f.newestSourceLastmod ?? '—'}`);
}
if (report.flagged.length > 30) console.log(` ... and ${report.flagged.length - 30} more`);
console.log('\nNext: human-confirm each flag against its source, fix, then bump **Verified:** (never auto-fix).');
} else {
console.log('\nNo lastmod-visible drift since last verification.');
}
}