feat(knowledge): knowledge-refresh — living register, deterministic stale core + web poll (v5.7 Chunk 3)
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>
This commit is contained in:
parent
ba66f1fc17
commit
0f9c091a14
7 changed files with 601 additions and 3 deletions
94
scanners/lib/knowledge-refresh.mjs
Normal file
94
scanners/lib/knowledge-refresh.mjs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* 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 },
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue