/** * knowledge-refresh — deterministic freshness core for the best-practices register. * * The "living" half of the v5.7 living knowledge base (Chunk 3). This module is the * PURE, deterministic part of the hybrid `/config-audit knowledge-refresh` motor: given a * register and an injected reference date, it classifies each entry as `fresh` or `stale` * by the age of its `source.verified` stamp. It NEVER touches the network and NEVER writes * — candidate discovery (polling CC changelog + Anthropic blog) and the human-approved * writes live in the command layer (Verifiseringsplikt: no unverified claim is auto-written). * * `referenceDate` is injected (not read from the clock here) so the function is fully * deterministic and unit-testable; the CLI passes today's date. See * docs/v5.7-optimization-lens-plan.md. */ /** Default re-verify cadence: a confirmed best-practice older than this needs a re-check. */ export const STALE_AFTER_DAYS_DEFAULT = 90; const DATE_RE = /^\d{4}-\d{2}-\d{2}$/; const DAY_MS = 86_400_000; /** * Normalize a reference date (Date or YYYY-MM-DD string) to a UTC-midnight {iso, ms}. * Throws TypeError on anything else — the reference date is required and must be valid. */ function normalizeReferenceDate(value) { let iso; if (value instanceof Date) { if (Number.isNaN(value.getTime())) throw new TypeError('referenceDate is an invalid Date'); iso = value.toISOString().slice(0, 10); } else if (typeof value === 'string' && DATE_RE.test(value)) { iso = value; } else { throw new TypeError('referenceDate must be a Date or a YYYY-MM-DD string'); } const ms = Date.parse(`${iso}T00:00:00Z`); if (Number.isNaN(ms)) throw new TypeError(`referenceDate is not a real calendar date: ${iso}`); return { iso, ms }; } /** Parse an entry's `source.verified` to UTC-midnight ms, or null if missing/unparseable. */ function verifiedMs(entry) { const v = entry && entry.source && entry.source.verified; if (typeof v !== 'string' || !DATE_RE.test(v)) return null; const ms = Date.parse(`${v}T00:00:00Z`); return Number.isNaN(ms) ? null : ms; } /** * Classify every register entry as fresh or stale by the age of its source.verified stamp. * * @param {{entries:object[]}} register * @param {{ referenceDate: string|Date, staleAfterDays?: number }} opts * @returns {{ * referenceDate: string, * staleAfterDays: number, * stale: Array<{id:string, verified:string|undefined, ageDays:number|null, url:string|undefined, claim:string|undefined}>, * fresh: Array<{id:string, verified:string|undefined, ageDays:number}>, * counts: { total:number, stale:number, fresh:number } * }} */ export function assessFreshness(register, opts = {}) { const ref = normalizeReferenceDate(opts.referenceDate); const staleAfterDays = typeof opts.staleAfterDays === 'number' ? opts.staleAfterDays : STALE_AFTER_DAYS_DEFAULT; const entries = (register && Array.isArray(register.entries)) ? register.entries : []; const stale = []; const fresh = []; for (const e of entries) { const verified = e && e.source ? e.source.verified : undefined; const vms = verifiedMs(e); if (vms === null) { // No re-checkable date → needs attention. Stale with ageDays null. stale.push({ id: e && e.id, verified, ageDays: null, url: e && e.source && e.source.url, claim: e && e.claim }); continue; } const ageDays = Math.floor((ref.ms - vms) / DAY_MS); if (ageDays > staleAfterDays) { stale.push({ id: e.id, verified, ageDays, url: e.source && e.source.url, claim: e.claim }); } else { fresh.push({ id: e.id, verified, ageDays }); } } return { referenceDate: ref.iso, staleAfterDays, stale, fresh, counts: { total: entries.length, stale: stale.length, fresh: fresh.length }, }; }