ms-ai-architect/scripts/kb-update/report-changes.mjs
Kjell Tore Guttormsen 94a5d1bb14 feat(ms-ai-architect): Sesjon 5 carry-forward B — datoløse filer → unverified-tier
Rotårsak: en datoløs ref-fil fikk effectiveDate=0000-01-01 (alltid «stale») OG en path-basert prioritet (kunne bli critical) → flommet critical-bøtta og druknet genuint kritiske filer. Fix: ny unverified-tier deklarert i domain-taxonomy.json (lag 0, eneste sannhetskilde for prioritet).

makePriorityFn(tax) tar nå valgfri hasDate (default true → bakoverkompatibelt for eksisterende én-args-kallere); report-changes sender Boolean(fileDate). unverified rangerer sist i sorteringen — manglende dato er metadata-hygiene, ikke currency-hast. Ekte registry: 1 fil skilt ut som Unverified.

TDD: 4 tester FØR kode (test-priority-unverified.test.mjs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REiKFhP4w6xGXXqWKpPCJJ
2026-06-19 22:31:25 +02:00

142 lines
5.4 KiB
JavaScript

#!/usr/bin/env node
// report-changes.mjs — Compare sitemap lastmod to reference file "Last updated:" headers.
// Generates change-report.json and prints human-readable summary.
// Usage: node report-changes.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 { loadTaxonomy, makePriorityFn } from './lib/taxonomy.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');
// Priority classification by file path patterns — rules from the taxonomy (lag 0).
const getFilePriority = makePriorityFn(loadTaxonomy(DATA_DIR));
// Parse "Last updated:" header from a reference file
const LAST_UPDATED_PATTERNS = [
/\*\*Last updated:\*\*\s*([\d-]+)/i,
/\*\*Sist (?:oppdatert|verifisert):\*\*\s*([\d-]+)/i,
/\*\*Dato:\*\*\s*([\d-]+)/i,
];
function parseLastUpdated(filePath) {
const fullPath = join(PLUGIN_ROOT, filePath);
if (!existsSync(fullPath)) return null;
// Only read first 500 bytes — header is always at the top
const content = readFileSync(fullPath, 'utf8').slice(0, 500);
for (const pattern of LAST_UPDATED_PATTERNS) {
const match = content.match(pattern);
if (match) {
const raw = match[1].trim();
// YYYY-MM → YYYY-MM-01, YYYY-MM-DD → as-is
return raw.length === 7 ? raw + '-01' : raw;
}
}
return null; // No date found — treat as always stale
}
// Priority sort order. 'unverified' (dateless files — no "Last updated:" header)
// ranks last: a missing date is a metadata-hygiene gap, not a currency-urgency
// signal, and must not masquerade as critical (carry-forward B).
const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3, unverified: 4 };
// --- Main ---
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);
}
// Group changed URLs by reference file
const fileChanges = new Map(); // filePath → { changedUrls, newestChange }
for (const [url, entry] of Object.entries(registry.urls)) {
if (!entry.sitemap_lastmod || entry.status !== 'tracked') continue;
for (const refFile of entry.reference_files) {
const fileDate = parseLastUpdated(refFile);
// If no date found, treat as always stale (date "0000-01-01")
const effectiveDate = fileDate || '0000-01-01';
if (entry.sitemap_lastmod > effectiveDate) {
if (!fileChanges.has(refFile)) {
fileChanges.set(refFile, { changedUrls: [], newestChange: entry.sitemap_lastmod, fileDate });
}
const fc = fileChanges.get(refFile);
fc.changedUrls.push({ url, sitemap_lastmod: entry.sitemap_lastmod });
if (entry.sitemap_lastmod > fc.newestChange) {
fc.newestChange = entry.sitemap_lastmod;
}
}
}
}
// Build report entries
const files = [];
for (const [path, changes] of fileChanges) {
// Dateless files (no "Last updated:" header) → 'unverified', not path-based.
const priority = getFilePriority(path, Boolean(changes.fileDate));
const pathParts = path.split('/');
files.push({
path,
priority,
file_last_updated: changes.fileDate || 'unknown',
newest_source_change: changes.newestChange,
changed_url_count: changes.changedUrls.length,
changed_urls: changes.changedUrls.map(u => u.url),
skill: pathParts[1] || 'unknown',
category: pathParts[3] || 'unknown',
});
}
// Sort: priority first, then newest source change descending
files.sort((a, b) => {
const pDiff = PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority];
if (pDiff !== 0) return pDiff;
return b.newest_source_change.localeCompare(a.newest_source_change);
});
// Count by priority
const byPriority = { critical: 0, high: 0, medium: 0, low: 0, unverified: 0 };
for (const f of files) byPriority[f.priority]++;
const report = {
generated_at: new Date().toISOString().split('T')[0],
last_poll: registry.last_poll,
total_tracked: Object.values(registry.urls).filter(u => u.status === 'tracked').length,
total_not_in_sitemap: Object.values(registry.urls).filter(u => u.status === 'not_in_sitemap').length,
total_files_needing_update: files.length,
by_priority: byPriority,
files,
};
saveReport('change-report.json', report, DATA_DIR);
if (jsonOnly) {
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
} else {
console.log(`\n=== KB Change Report (${report.generated_at}) ===`);
console.log(`Sources last polled: ${registry.last_poll}`);
console.log(`URLs tracked: ${report.total_tracked}/${Object.keys(registry.urls).length} (${report.total_not_in_sitemap} not in sitemap)`);
console.log(`Files needing update: ${files.length} (Critical: ${byPriority.critical}, High: ${byPriority.high}, Medium: ${byPriority.medium}, Low: ${byPriority.low}, Unverified: ${byPriority.unverified})`);
if (files.length > 0) {
console.log('\nTop 20 by priority:');
for (const f of files.slice(0, 20)) {
console.log(` [${f.priority.toUpperCase()}] ${f.path}`);
console.log(` ${f.changed_url_count} source(s) changed. Latest: ${f.newest_source_change}. File: ${f.file_last_updated}`);
}
if (files.length > 20) {
console.log(` ... and ${files.length - 20} more`);
}
}
console.log('\nRun: /architect:generate-skills --update to process updates');
}