#!/usr/bin/env node /** * knowledge-refresh CLI — feeds the v5.7 `/config-audit knowledge-refresh` command * (Chunk 3: the "living" half of the living knowledge base). * * This is the DETERMINISTIC half of the hybrid motor: it loads the best-practices * register and classifies every entry as `fresh` or `stale` by the age of its * `source.verified` stamp (via the pure `assessFreshness` core). It is READ-ONLY — * it NEVER writes the register and NEVER touches the network. Candidate discovery * (polling the CC changelog + Anthropic blog) and the human-approved writes live in * the command layer (Verifiseringsplikt). `--dry-run` is implicit and the only mode; * the flag is accepted for explicitness and echoed back. * * Naming: `-cli` suffix → NOT an orchestrated scanner (the scan-orchestrator only * loads scanner modules), so the scanner count is unchanged and the snapshot suite * stays byte-stable. * * Usage: * node knowledge-refresh-cli.mjs [--output-file ] [--stale-after ] * [--reference-date ] [--dry-run] * * Exit codes: 0 = every entry fresh, 1 = one or more stale (advisory), 3 = error. */ import { resolve } from 'node:path'; import { writeFile } from 'node:fs/promises'; import { loadRegister, REGISTER_PATH } from './lib/best-practices-register.mjs'; import { assessFreshness, STALE_AFTER_DAYS_DEFAULT } from './lib/knowledge-refresh.mjs'; const DATE_RE = /^\d{4}-\d{2}-\d{2}$/; function fail(message) { process.stderr.write(`Error: ${message}\n`); process.exit(3); } async function main() { const args = process.argv.slice(2); let outputFile = null; let staleAfterDays = STALE_AFTER_DAYS_DEFAULT; let referenceDate = null; // null → today let dryRun = false; for (let i = 0; i < args.length; i++) { const a = args[i]; if (a === '--dry-run') dryRun = true; else if (a === '--output-file' && args[i + 1]) outputFile = args[++i]; else if (a === '--stale-after' && args[i + 1] !== undefined) { const n = Number.parseInt(args[++i], 10); if (!Number.isInteger(n) || n < 0) fail('--stale-after must be a non-negative integer (days)'); staleAfterDays = n; } else if (a === '--reference-date' && args[i + 1]) { referenceDate = args[++i]; if (!DATE_RE.test(referenceDate)) fail('--reference-date must be YYYY-MM-DD'); } } // The clock is read here ONLY — the core takes an injected date and stays pure. const ref = referenceDate || new Date(); let register; try { register = loadRegister(); } catch (err) { fail(`could not load register at ${REGISTER_PATH}: ${err.message}`); } let assessment; try { assessment = assessFreshness(register, { referenceDate: ref, staleAfterDays }); } catch (err) { fail(err.message); } const payload = { status: 'ok', registerPath: REGISTER_PATH, version: register.version, dryRun: true, // this CLI never writes; the flag is informational requestedDryRun: dryRun, referenceDate: assessment.referenceDate, staleAfterDays: assessment.staleAfterDays, counts: assessment.counts, stale: assessment.stale, fresh: assessment.fresh, }; const json = JSON.stringify(payload, null, 2); if (outputFile) await writeFile(outputFile, json, 'utf-8'); else process.stdout.write(json + '\n'); process.exit(assessment.counts.stale > 0 ? 1 : 0); } const isDirectRun = process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname); if (isDirectRun) { main().catch((err) => { process.stderr.write(`Fatal: ${err.message}\n`); process.exit(3); }); }