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
215 lines
8.9 KiB
JavaScript
215 lines
8.9 KiB
JavaScript
import { describe, it } from 'node:test';
|
|
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`.
|
|
const entry = (id, verified, extra = {}) => ({
|
|
id,
|
|
claim: `claim for ${id}`,
|
|
confidence: 'confirmed',
|
|
source: { url: `https://example.com/${id}`, title: id, verified },
|
|
...extra,
|
|
});
|
|
const reg = (entries) => ({ version: 1, entries });
|
|
|
|
const REF = '2026-06-21';
|
|
|
|
describe('STALE_AFTER_DAYS_DEFAULT', () => {
|
|
it('is 90 (quarterly re-verify cadence)', () => {
|
|
assert.equal(STALE_AFTER_DAYS_DEFAULT, 90);
|
|
});
|
|
});
|
|
|
|
describe('assessFreshness — classification', () => {
|
|
it('classifies a recently-verified entry as fresh', () => {
|
|
const r = assessFreshness(reg([entry('BP-A-001', '2026-06-01')]), { referenceDate: REF });
|
|
assert.equal(r.counts.fresh, 1);
|
|
assert.equal(r.counts.stale, 0);
|
|
assert.equal(r.fresh[0].id, 'BP-A-001');
|
|
});
|
|
|
|
it('classifies an entry older than the threshold as stale', () => {
|
|
// 2026-01-01 → 2026-06-21 is 171 days > 90
|
|
const r = assessFreshness(reg([entry('BP-A-001', '2026-01-01')]), { referenceDate: REF });
|
|
assert.equal(r.counts.stale, 1);
|
|
assert.equal(r.counts.fresh, 0);
|
|
assert.equal(r.stale[0].id, 'BP-A-001');
|
|
assert.equal(r.stale[0].ageDays, 171);
|
|
});
|
|
|
|
it('treats exactly staleAfterDays as still fresh (strictly-greater is stale)', () => {
|
|
const r = assessFreshness(reg([entry('BP-A-001', '2026-03-23')]), {
|
|
referenceDate: REF,
|
|
staleAfterDays: 90,
|
|
});
|
|
// 2026-03-23 → 2026-06-21 is exactly 90 days
|
|
assert.equal(r.fresh[0].ageDays, 90);
|
|
assert.equal(r.counts.fresh, 1);
|
|
assert.equal(r.counts.stale, 0);
|
|
});
|
|
|
|
it('treats one day past the threshold as stale', () => {
|
|
const r = assessFreshness(reg([entry('BP-A-001', '2026-03-22')]), {
|
|
referenceDate: REF,
|
|
staleAfterDays: 90,
|
|
});
|
|
assert.equal(r.stale[0].ageDays, 91);
|
|
assert.equal(r.counts.stale, 1);
|
|
});
|
|
|
|
it('respects a custom staleAfterDays', () => {
|
|
const entries = [entry('BP-A-001', '2026-06-01')]; // 20 days old
|
|
assert.equal(assessFreshness(reg(entries), { referenceDate: REF, staleAfterDays: 90 }).counts.stale, 0);
|
|
assert.equal(assessFreshness(reg(entries), { referenceDate: REF, staleAfterDays: 10 }).counts.stale, 1);
|
|
});
|
|
|
|
it('treats a future verified date as fresh (negative age)', () => {
|
|
const r = assessFreshness(reg([entry('BP-A-001', '2026-12-31')]), { referenceDate: REF });
|
|
assert.equal(r.counts.fresh, 1);
|
|
assert.ok(r.fresh[0].ageDays < 0);
|
|
});
|
|
});
|
|
|
|
describe('assessFreshness — output shape', () => {
|
|
it('stale entries carry id, verified, ageDays, url, claim', () => {
|
|
const r = assessFreshness(reg([entry('BP-A-001', '2026-01-01')]), { referenceDate: REF });
|
|
const s = r.stale[0];
|
|
assert.equal(s.id, 'BP-A-001');
|
|
assert.equal(s.verified, '2026-01-01');
|
|
assert.equal(typeof s.ageDays, 'number');
|
|
assert.equal(s.url, 'https://example.com/BP-A-001');
|
|
assert.equal(s.claim, 'claim for BP-A-001');
|
|
});
|
|
|
|
it('counts.total equals stale + fresh and matches entry count', () => {
|
|
const r = assessFreshness(
|
|
reg([entry('BP-A-001', '2026-01-01'), entry('BP-A-002', '2026-06-10'), entry('BP-A-003', '2025-01-01')]),
|
|
{ referenceDate: REF }
|
|
);
|
|
assert.equal(r.counts.total, 3);
|
|
assert.equal(r.counts.stale + r.counts.fresh, 3);
|
|
});
|
|
|
|
it('normalizes referenceDate + echoes staleAfterDays in the result', () => {
|
|
const r = assessFreshness(reg([entry('BP-A-001', '2026-06-01')]), { referenceDate: REF, staleAfterDays: 42 });
|
|
assert.equal(r.referenceDate, '2026-06-21');
|
|
assert.equal(r.staleAfterDays, 42);
|
|
});
|
|
});
|
|
|
|
describe('assessFreshness — referenceDate input forms', () => {
|
|
it('accepts a Date and a YYYY-MM-DD string identically', () => {
|
|
const entries = [entry('BP-A-001', '2026-01-01')];
|
|
const fromString = assessFreshness(reg(entries), { referenceDate: REF });
|
|
const fromDate = assessFreshness(reg(entries), { referenceDate: new Date('2026-06-21T00:00:00Z') });
|
|
assert.deepStrictEqual(fromDate.stale, fromString.stale);
|
|
assert.equal(fromDate.referenceDate, '2026-06-21');
|
|
});
|
|
|
|
it('throws a TypeError on a missing or invalid referenceDate', () => {
|
|
assert.throws(() => assessFreshness(reg([entry('BP-A-001', '2026-01-01')]), {}), TypeError);
|
|
assert.throws(
|
|
() => assessFreshness(reg([entry('BP-A-001', '2026-01-01')]), { referenceDate: 'not-a-date' }),
|
|
TypeError
|
|
);
|
|
});
|
|
});
|
|
|
|
// 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 });
|
|
assert.deepStrictEqual(r.counts, { total: 0, stale: 0, fresh: 0 });
|
|
assert.deepStrictEqual(r.stale, []);
|
|
assert.deepStrictEqual(r.fresh, []);
|
|
});
|
|
|
|
it('classifies an entry with an unparseable verified date as stale with ageDays null', () => {
|
|
const bad = entry('BP-A-001', '2026-06-01');
|
|
bad.source.verified = 'nope';
|
|
const r = assessFreshness(reg([bad]), { referenceDate: REF });
|
|
assert.equal(r.counts.stale, 1);
|
|
assert.equal(r.stale[0].ageDays, null);
|
|
});
|
|
|
|
it('classifies an entry with a missing source as stale with ageDays null', () => {
|
|
const r = assessFreshness(reg([{ id: 'BP-A-001', claim: 'x', confidence: 'confirmed' }]), {
|
|
referenceDate: REF,
|
|
});
|
|
assert.equal(r.counts.stale, 1);
|
|
assert.equal(r.stale[0].ageDays, null);
|
|
});
|
|
});
|