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:
Kjell Tore Guttormsen 2026-08-03 11:36:57 +02:00
commit d66035ed86
3 changed files with 213 additions and 23 deletions

View file

@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
import {
assessFreshness,
STALE_AFTER_DAYS_DEFAULT,
EVIDENCE_STALE_AFTER_DAYS_DEFAULT,
} from '../../scanners/lib/knowledge-refresh.mjs';
// Build a register with one entry verified on `verified`.
@ -118,6 +119,76 @@ describe('assessFreshness — referenceDate input forms', () => {
});
});
// The BP-SUB-001 defect class (2026-08-03): an entry re-verified against an OLD
// source stayed green while a newer source (published 2026-07-24) sat unnoticed.
// Freshness must therefore also look at the EVIDENCE (source publication age and
// explicit supersession), not just the entry's verified stamp. The cure for old
// evidence is a newer source in `sources[]`, not another re-read of the old one.
describe('assessFreshness — evidence age (green-but-outdated defect)', () => {
it('flags a recently-verified entry whose only evidence is older than evidenceStaleAfterDays', () => {
const e = entry('BP-A-001', '2026-06-01'); // verified 20 days before REF: green by the old rule
e.source.published = '2025-01-01';
const r = assessFreshness(reg([e]), { referenceDate: REF, evidenceStaleAfterDays: 365 });
assert.equal(r.counts.stale, 1);
assert.deepStrictEqual(r.stale[0].reasons, ['evidence-age']);
});
it('a newer corroborating source in sources[] clears evidence-age (re-verifying does not)', () => {
const e = entry('BP-A-001', '2026-06-01');
e.source.published = '2025-01-01';
e.sources = [{ url: 'https://example.com/newer', published: '2026-05-30' }];
const r = assessFreshness(reg([e]), { referenceDate: REF, evidenceStaleAfterDays: 365 });
assert.equal(r.counts.stale, 0);
assert.equal(r.counts.fresh, 1);
});
it('stays silent when no source carries a published date (rule is opt-in per entry)', () => {
const r = assessFreshness(reg([entry('BP-A-001', '2026-06-01')]), {
referenceDate: REF,
evidenceStaleAfterDays: 1,
});
assert.equal(r.counts.stale, 0);
});
it('exports a default evidence cadence of 365 days and echoes it in the result', () => {
assert.equal(EVIDENCE_STALE_AFTER_DAYS_DEFAULT, 365);
const r = assessFreshness(reg([]), { referenceDate: REF });
assert.equal(r.evidenceStaleAfterDays, 365);
});
});
describe('assessFreshness — supersededBy', () => {
it('flags a superseded source regardless of a fresh verified stamp', () => {
const e = entry('BP-A-001', '2026-06-20'); // verified 1 day before REF
e.source.supersededBy = { url: 'https://example.com/successor', published: '2026-05-01' };
const r = assessFreshness(reg([e]), { referenceDate: REF });
assert.equal(r.counts.stale, 1);
assert.ok(r.stale[0].reasons.includes('superseded'));
});
it('an entry can be stale for both verified-age and superseded at once', () => {
const e = entry('BP-A-001', '2026-01-01'); // 171 days > 90
e.source.supersededBy = { url: 'https://example.com/successor' };
const r = assessFreshness(reg([e]), { referenceDate: REF });
assert.equal(r.counts.stale, 1);
assert.deepStrictEqual([...r.stale[0].reasons].sort(), ['superseded', 'verified-age']);
});
});
describe('assessFreshness — reasons on existing rules (back-compat shape)', () => {
it('verified-age staleness carries reasons ["verified-age"]', () => {
const r = assessFreshness(reg([entry('BP-A-001', '2026-01-01')]), { referenceDate: REF });
assert.deepStrictEqual(r.stale[0].reasons, ['verified-age']);
});
it('missing/unparseable verified carries reasons ["no-verified-date"]', () => {
const bad = entry('BP-A-001', '2026-06-01');
bad.source.verified = 'nope';
const r = assessFreshness(reg([bad]), { referenceDate: REF });
assert.deepStrictEqual(r.stale[0].reasons, ['no-verified-date']);
});
});
describe('assessFreshness — defensive cases', () => {
it('returns all-zero counts for an empty register', () => {
const r = assessFreshness(reg([]), { referenceDate: REF });