fix(knowledge): freshness can no longer be green on outdated evidence
assessFreshness aged only the entry's own source.verified stamp, so an entry re-verified against an old source stayed green while a newer source sat unnoticed (BP-SUB-001 was stamped 2026-07-31, a week after the superseding-grade article of 2026-07-24 was published). The stamp certifies the old source; it says nothing about the evidence. - entries may carry corroborating sources[] with published dates - new evidence-age rule: stale when the NEWEST published date across all sources exceeds evidenceStaleAfterDays (default 365); re-verifying the old source never clears it, only newer evidence does - source.supersededBy marks a replaced source: stale regardless of stamp - stale items now carry reasons[] (verified-age / no-verified-date / superseded / evidence-age) - BP-SUB-001 gains the 2026-07-24 context-engineering article as a verified corroborating source (near-verbatim coverage). NOT added to BP-MECH-*/BP-SIZE-001: own verification found no mechanism-choice or size-limit content in the article, contrary to the brief's assumption. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019NeaMRXVGzh9oSwigJDjE9
This commit is contained in:
parent
caea8aca23
commit
d66035ed86
3 changed files with 213 additions and 23 deletions
|
|
@ -16,6 +16,14 @@
|
|||
/** Default re-verify cadence: a confirmed best-practice older than this needs a re-check. */
|
||||
export const STALE_AFTER_DAYS_DEFAULT = 90;
|
||||
|
||||
/**
|
||||
* Default evidence cadence: when the NEWEST `published` date across an entry's
|
||||
* sources is older than this, the entry is flagged even if recently re-verified.
|
||||
* Re-verifying the old source does not clear it — only a newer source does
|
||||
* (the BP-SUB-001 defect class: green stamp, substantially outdated evidence).
|
||||
*/
|
||||
export const EVIDENCE_STALE_AFTER_DAYS_DEFAULT = 365;
|
||||
|
||||
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
||||
const DAY_MS = 86_400_000;
|
||||
|
||||
|
|
@ -46,15 +54,40 @@ function verifiedMs(entry) {
|
|||
return Number.isNaN(ms) ? null : ms;
|
||||
}
|
||||
|
||||
/** Parse a YYYY-MM-DD string to UTC-midnight ms, or null. */
|
||||
function dateMs(v) {
|
||||
if (typeof v !== 'string' || !DATE_RE.test(v)) return null;
|
||||
const ms = Date.parse(`${v}T00:00:00Z`);
|
||||
return Number.isNaN(ms) ? null : ms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Newest `published` across the primary `source` and any corroborating `sources[]`.
|
||||
* Null when no source carries a parseable published date (the evidence-age rule
|
||||
* is then silent for that entry — it cannot judge evidence it cannot date).
|
||||
*/
|
||||
function newestEvidenceMs(entry) {
|
||||
const candidates = [];
|
||||
if (entry && entry.source) candidates.push(entry.source);
|
||||
if (entry && Array.isArray(entry.sources)) candidates.push(...entry.sources);
|
||||
let newest = null;
|
||||
for (const s of candidates) {
|
||||
const ms = dateMs(s && s.published);
|
||||
if (ms !== null && (newest === null || ms > newest)) newest = ms;
|
||||
}
|
||||
return newest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {{ referenceDate: string|Date, staleAfterDays?: number, evidenceStaleAfterDays?: number }} opts
|
||||
* @returns {{
|
||||
* referenceDate: string,
|
||||
* staleAfterDays: number,
|
||||
* stale: Array<{id:string, verified:string|undefined, ageDays:number|null, url:string|undefined, claim:string|undefined}>,
|
||||
* evidenceStaleAfterDays: number,
|
||||
* stale: Array<{id:string, verified:string|undefined, ageDays:number|null, url:string|undefined, claim:string|undefined, reasons:string[]}>,
|
||||
* fresh: Array<{id:string, verified:string|undefined, ageDays:number}>,
|
||||
* counts: { total:number, stale:number, fresh:number }
|
||||
* }}
|
||||
|
|
@ -63,6 +96,10 @@ export function assessFreshness(register, opts = {}) {
|
|||
const ref = normalizeReferenceDate(opts.referenceDate);
|
||||
const staleAfterDays =
|
||||
typeof opts.staleAfterDays === 'number' ? opts.staleAfterDays : STALE_AFTER_DAYS_DEFAULT;
|
||||
const evidenceStaleAfterDays =
|
||||
typeof opts.evidenceStaleAfterDays === 'number'
|
||||
? opts.evidenceStaleAfterDays
|
||||
: EVIDENCE_STALE_AFTER_DAYS_DEFAULT;
|
||||
|
||||
const entries = (register && Array.isArray(register.entries)) ? register.entries : [];
|
||||
const stale = [];
|
||||
|
|
@ -71,14 +108,30 @@ export function assessFreshness(register, opts = {}) {
|
|||
for (const e of entries) {
|
||||
const verified = e && e.source ? e.source.verified : undefined;
|
||||
const vms = verifiedMs(e);
|
||||
const reasons = [];
|
||||
let ageDays = null;
|
||||
|
||||
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;
|
||||
// No re-checkable date → needs attention.
|
||||
reasons.push('no-verified-date');
|
||||
} else {
|
||||
ageDays = Math.floor((ref.ms - vms) / DAY_MS);
|
||||
if (ageDays > staleAfterDays) reasons.push('verified-age');
|
||||
}
|
||||
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 });
|
||||
|
||||
// A source explicitly marked as superseded is stale no matter how fresh the
|
||||
// verified stamp is — the stamp certifies the OLD source.
|
||||
if (e && e.source && e.source.supersededBy) reasons.push('superseded');
|
||||
|
||||
// Evidence age: keyed on the newest published date across all sources, so a
|
||||
// re-read of the old source never clears it — only newer evidence does.
|
||||
const evMs = newestEvidenceMs(e);
|
||||
if (evMs !== null && Math.floor((ref.ms - evMs) / DAY_MS) > evidenceStaleAfterDays) {
|
||||
reasons.push('evidence-age');
|
||||
}
|
||||
|
||||
if (reasons.length > 0) {
|
||||
stale.push({ id: e && e.id, verified, ageDays, url: e && e.source && e.source.url, claim: e && e.claim, reasons });
|
||||
} else {
|
||||
fresh.push({ id: e.id, verified, ageDays });
|
||||
}
|
||||
|
|
@ -87,6 +140,7 @@ export function assessFreshness(register, opts = {}) {
|
|||
return {
|
||||
referenceDate: ref.iso,
|
||||
staleAfterDays,
|
||||
evidenceStaleAfterDays,
|
||||
stale,
|
||||
fresh,
|
||||
counts: { total: entries.length, stale: stale.length, fresh: fresh.length },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue