The 'living' half of the v5.7 living knowledge base. Same hybrid split as the
optimization lens (Chunk 2b): a deterministic, byte-stable, unit-tested core +
a web/judgment command shell.
- scanners/lib/knowledge-refresh.mjs: pure assessFreshness(register,
{referenceDate, staleAfterDays=90}) — age-based fresh/stale classification of
source.verified; referenceDate injected (never reads the clock) → fully
deterministic. 15 tests.
- scanners/knowledge-refresh-cli.mjs: -cli (NOT an orchestrated scanner →
scanner count stays 15, suite byte-stable). Read-only — never writes the
register, never hits the network. --reference-date/--stale-after/--dry-run,
exit 0/1/3. 8 tests.
- commands/knowledge-refresh.md (opus): CLI stale-report → re-verify each stale
entry by re-reading its source.url → poll CC changelog + Anthropic blog →
apply ONLY human-approved writes, then re-validate the register. No unverified
claim is ever auto-written (Verifiseringsplikt). Web-driven → not byte-stable.
No new agent, no new orchestrated scanner. Docs/badges: commands 19→20,
tests 1068→1091 (20 lib + 32 scanner test files). self-audit A/A, readmeCheck passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
94 lines
3.8 KiB
JavaScript
94 lines
3.8 KiB
JavaScript
/**
|
|
* 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 },
|
|
};
|
|
}
|