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.
68 lines
3.2 KiB
JavaScript
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.');
|
|
}
|
|
}
|