#!/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 { writeOutputFile } from './lib/write-output.mjs'; 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}$/; /** * Usage error. Throws rather than calling process.exit(): exit() discards * unflushed stdout when stdout is a pipe. The top-level catch prints the same * `Error: ` text and sets the same exit code 3, so callers see no difference. */ class CliUsageError extends Error {} function fail(message) { throw new CliUsageError(message); } 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'); } // A flag we do not understand must fail loudly. Silently dropping it is how // `--stale-after 30` — arriving as ONE argv entry from a shell that does not // word-split — became "all 14 entries are fresh within 90 days": a confident // answer to a question the caller did not ask. else if (a.startsWith('--')) fail(`unknown flag "${a}"`); } // 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 writeOutputFile(outputFile, json, 'utf-8'); else process.stdout.write(json + '\n'); process.exitCode = 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) => { const prefix = err instanceof CliUsageError ? 'Error' : 'Fatal'; process.stderr.write(`${prefix}: ${err.message}\n`); process.exitCode = 3; }); }