Port 3s billige, lastmod-synlige halvdel av kadens-judgen. Forutsetningen kom med Port 2: filene bærer nå **Verified:**, så «sitemap_lastmod > verified»- sammenligningen er endelig mulig. lib/verified-staleness.mjs (rent, speiler verify-out — skriver aldri til disk): - classifyVerifiedStaleness: reference-scope (Port 1-kontrakt). Buckets: stale-since-verified (kilde endret strikt etter verify → flagg), unverified (type=reference men verified=null, kontraktbrudd → flagg), unmigrated (type=null legacy → Spor 1-bro, telles), out-of-scope (template/methodology/ regulatory), fresh. YYYY-MM→-01-normalisering, strikt > (lik dato = fresh). - buildFileSourceMap: registry-inversjon (URL→filer ⇒ fil→nyeste lastmod), kun tracked + lastmod. Delt med full-pass (P3d). - buildStalenessReport: join header↔lastmod, klassifiser, sorter (stale før unverified, nyeste drift først); reader injisert (testbar uten disk). report-verified-staleness.mjs: tynn I/O-glue (speiler report-changes), skriver gitignored verified-staleness-report.json. Kjøres på KB-refresh-kadens, IKKE SessionStart (hooken bare LESER cachen — P3b). Ekte kjøring: 306 ref-filer m/ tracked kilder, alle unmigrated (Port 1 ikke backfilllet til legacy-korpus ennå = Spor 1). Mekanismen rapporterer ærlig grunntilstand korrekt. TDD (Iron Law): 18 nye tester. Suite 604/604.
195 lines
8.9 KiB
JavaScript
195 lines
8.9 KiB
JavaScript
// tests/kb-update/test-verified-staleness.test.mjs
|
|
// Spor 3 Port 3 (kadens-judge) — INCREMENTAL stale-since-verified classifier.
|
|
//
|
|
// classifyVerifiedStaleness is a PURE function (mirrors verify-out.mjs: it never
|
|
// touches disk). It answers the cheap, lastmod-visible question Port 3's
|
|
// incremental pass needs: "which reference files have a source that changed
|
|
// AFTER we last verified them?" The expensive full-pass (P3d) catches the
|
|
// drift class this misses. The reporting floor (P3b) reads its output.
|
|
//
|
|
// Scope is the Port 1 contract: only `type: reference` files are in correctness
|
|
// scope. template/methodology/regulatory are exempt (out-of-scope). Files with
|
|
// no Type header are `unmigrated` (legacy corpus — Spor 1 territory, not judged
|
|
// here but counted so the migration gap is an honest number).
|
|
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
classifyVerifiedStaleness,
|
|
buildFileSourceMap,
|
|
buildStalenessReport,
|
|
} from '../../scripts/kb-update/lib/verified-staleness.mjs';
|
|
|
|
const f = (over) => ({
|
|
path: 'skills/x/references/a.md',
|
|
type: 'reference',
|
|
verified: '2026-06-01',
|
|
newestSourceLastmod: '2026-05-01',
|
|
...over,
|
|
});
|
|
|
|
test('reference + source changed AFTER verified → flagged stale-since-verified', () => {
|
|
const out = classifyVerifiedStaleness([f({ verified: '2026-06-01', newestSourceLastmod: '2026-06-15' })]);
|
|
assert.equal(out.flagged.length, 1);
|
|
assert.equal(out.flagged[0].reason, 'stale-since-verified');
|
|
assert.equal(out.counts.stale, 1);
|
|
});
|
|
|
|
test('reference + source older than verified → fresh, not flagged', () => {
|
|
const out = classifyVerifiedStaleness([f({ verified: '2026-06-01', newestSourceLastmod: '2026-05-01' })]);
|
|
assert.equal(out.flagged.length, 0);
|
|
assert.equal(out.counts.fresh, 1);
|
|
});
|
|
|
|
test('reference + source lastmod EQUALS verified → fresh (strict >, mirrors report-changes)', () => {
|
|
const out = classifyVerifiedStaleness([f({ verified: '2026-06-15', newestSourceLastmod: '2026-06-15' })]);
|
|
assert.equal(out.flagged.length, 0);
|
|
assert.equal(out.counts.fresh, 1);
|
|
});
|
|
|
|
test('reference declared but verified missing → flagged unverified (contract breach)', () => {
|
|
const out = classifyVerifiedStaleness([f({ verified: null, newestSourceLastmod: '2026-05-01' })]);
|
|
assert.equal(out.flagged.length, 1);
|
|
assert.equal(out.flagged[0].reason, 'unverified');
|
|
assert.equal(out.counts.unverified, 1);
|
|
});
|
|
|
|
test('reference + verified set + NO tracked source → fresh (incremental is lastmod-only; full-pass covers)', () => {
|
|
const out = classifyVerifiedStaleness([f({ verified: '2026-06-01', newestSourceLastmod: null })]);
|
|
assert.equal(out.flagged.length, 0);
|
|
assert.equal(out.counts.fresh, 1);
|
|
});
|
|
|
|
test('template / methodology / regulatory → out-of-scope, never flagged', () => {
|
|
const out = classifyVerifiedStaleness([
|
|
f({ type: 'template', verified: null }),
|
|
f({ type: 'methodology', verified: null }),
|
|
f({ type: 'regulatory', verified: null }),
|
|
]);
|
|
assert.equal(out.flagged.length, 0);
|
|
assert.equal(out.counts.outOfScope, 3);
|
|
});
|
|
|
|
test('no Type header (legacy) → unmigrated, counted but not flagged', () => {
|
|
const out = classifyVerifiedStaleness([f({ type: null, verified: null, newestSourceLastmod: '2026-06-15' })]);
|
|
assert.equal(out.flagged.length, 0);
|
|
assert.equal(out.counts.unmigrated, 1);
|
|
});
|
|
|
|
test('YYYY-MM verified normalizes to -01 → June-15 source is stale against June-month verify', () => {
|
|
const out = classifyVerifiedStaleness([f({ verified: '2026-06', newestSourceLastmod: '2026-06-15' })]);
|
|
assert.equal(out.flagged.length, 1);
|
|
assert.equal(out.flagged[0].reason, 'stale-since-verified');
|
|
});
|
|
|
|
test('flagged carries path + verified + newestSourceLastmod for the report/floor', () => {
|
|
const out = classifyVerifiedStaleness([
|
|
f({ path: 'skills/s/references/p.md', verified: '2026-01-01', newestSourceLastmod: '2026-06-15' }),
|
|
]);
|
|
const e = out.flagged[0];
|
|
assert.equal(e.path, 'skills/s/references/p.md');
|
|
assert.equal(e.verified, '2026-01-01');
|
|
assert.equal(e.newestSourceLastmod, '2026-06-15');
|
|
});
|
|
|
|
test('mixed corpus aggregates counts; total = input length', () => {
|
|
const out = classifyVerifiedStaleness([
|
|
f({ verified: '2026-01-01', newestSourceLastmod: '2026-06-15' }), // stale
|
|
f({ verified: '2026-06-01', newestSourceLastmod: '2026-05-01' }), // fresh
|
|
f({ verified: null }), // unverified
|
|
f({ type: 'template', verified: null }), // out-of-scope
|
|
f({ type: null, verified: null }), // unmigrated
|
|
]);
|
|
assert.equal(out.counts.total, 5);
|
|
assert.equal(out.counts.stale, 1);
|
|
assert.equal(out.counts.fresh, 1);
|
|
assert.equal(out.counts.unverified, 1);
|
|
assert.equal(out.counts.outOfScope, 1);
|
|
assert.equal(out.counts.unmigrated, 1);
|
|
assert.equal(out.flagged.length, 2); // stale + unverified
|
|
});
|
|
|
|
test('empty input → empty flagged, zeroed counts', () => {
|
|
const out = classifyVerifiedStaleness([]);
|
|
assert.equal(out.flagged.length, 0);
|
|
assert.equal(out.counts.total, 0);
|
|
assert.equal(out.counts.stale, 0);
|
|
});
|
|
|
|
// --- buildFileSourceMap: invert the registry (URL→files ⇒ file→newest lastmod) ---
|
|
// Mirrors report-changes' grouping loop, but extracted as a PURE helper so the
|
|
// incremental pass and the full-pass (P3d) share one inversion.
|
|
|
|
test('buildFileSourceMap — single tracked URL maps its file to the lastmod', () => {
|
|
const reg = { urls: { 'https://x': { status: 'tracked', sitemap_lastmod: '2026-06-01', reference_files: ['a.md'] } } };
|
|
const m = buildFileSourceMap(reg);
|
|
assert.equal(m.get('a.md'), '2026-06-01');
|
|
});
|
|
|
|
test('buildFileSourceMap — multiple URLs for one file → newest lastmod wins', () => {
|
|
const reg = { urls: {
|
|
'https://x': { status: 'tracked', sitemap_lastmod: '2026-03-01', reference_files: ['a.md'] },
|
|
'https://y': { status: 'tracked', sitemap_lastmod: '2026-06-15', reference_files: ['a.md'] },
|
|
'https://z': { status: 'tracked', sitemap_lastmod: '2026-05-01', reference_files: ['a.md'] },
|
|
} };
|
|
assert.equal(buildFileSourceMap(reg).get('a.md'), '2026-06-15');
|
|
});
|
|
|
|
test('buildFileSourceMap — non-tracked status and missing lastmod are skipped', () => {
|
|
const reg = { urls: {
|
|
'https://x': { status: 'not_in_sitemap', sitemap_lastmod: '2026-06-01', reference_files: ['a.md'] },
|
|
'https://y': { status: 'tracked', sitemap_lastmod: null, reference_files: ['a.md'] },
|
|
} };
|
|
assert.equal(buildFileSourceMap(reg).has('a.md'), false);
|
|
});
|
|
|
|
test('buildFileSourceMap — empty / garbage registry → empty map', () => {
|
|
assert.equal(buildFileSourceMap({}).size, 0);
|
|
assert.equal(buildFileSourceMap(null).size, 0);
|
|
assert.equal(buildFileSourceMap({ urls: {} }).size, 0);
|
|
});
|
|
|
|
// --- buildStalenessReport: assemble the full incremental report (pure; reader injected) ---
|
|
|
|
const REG = { last_poll: '2026-06-23T18:09:30Z', urls: {
|
|
'https://x': { status: 'tracked', sitemap_lastmod: '2026-06-15', reference_files: ['stale.md'] },
|
|
'https://y': { status: 'tracked', sitemap_lastmod: '2026-01-01', reference_files: ['fresh.md'] },
|
|
'https://z': { status: 'tracked', sitemap_lastmod: '2026-06-10', reference_files: ['gone.md'] },
|
|
} };
|
|
const READER = (p) => ({
|
|
'stale.md': { type: 'reference', verified: '2026-02-01' },
|
|
'fresh.md': { type: 'reference', verified: '2026-06-01' },
|
|
'gone.md': null, // deleted on disk — reader signals absence
|
|
}[p] ?? null);
|
|
|
|
test('buildStalenessReport — stamps metadata, joins headers to lastmod, classifies', () => {
|
|
const r = buildStalenessReport(REG, READER, { today: '2026-06-29' });
|
|
assert.equal(r.generated_at, '2026-06-29');
|
|
assert.equal(r.last_poll, '2026-06-23T18:09:30Z');
|
|
assert.equal(r.counts.stale, 1);
|
|
assert.equal(r.counts.fresh, 1);
|
|
assert.equal(r.flagged.length, 1);
|
|
assert.equal(r.flagged[0].path, 'stale.md');
|
|
assert.equal(r.flagged[0].reason, 'stale-since-verified');
|
|
});
|
|
|
|
test('buildStalenessReport — files the reader returns null for (deleted) are skipped entirely', () => {
|
|
const r = buildStalenessReport(REG, READER, { today: '2026-06-29' });
|
|
assert.equal(r.counts.total, 2); // gone.md skipped, not counted
|
|
assert.ok(!r.flagged.some((f) => f.path === 'gone.md'));
|
|
});
|
|
|
|
test('buildStalenessReport — flagged sorted: stale-since-verified before unverified, newest drift first', () => {
|
|
const reg = { last_poll: null, urls: {
|
|
'https://a': { status: 'tracked', sitemap_lastmod: '2026-06-10', reference_files: ['s-old.md'] },
|
|
'https://b': { status: 'tracked', sitemap_lastmod: '2026-06-20', reference_files: ['s-new.md'] },
|
|
'https://c': { status: 'tracked', sitemap_lastmod: '2026-06-15', reference_files: ['u.md'] },
|
|
} };
|
|
const reader = (p) => ({
|
|
's-old.md': { type: 'reference', verified: '2026-01-01' },
|
|
's-new.md': { type: 'reference', verified: '2026-01-01' },
|
|
'u.md': { type: 'reference', verified: null },
|
|
}[p] ?? null);
|
|
const r = buildStalenessReport(reg, reader, { today: '2026-06-29' });
|
|
assert.deepEqual(r.flagged.map((f) => f.path), ['s-new.md', 's-old.md', 'u.md']);
|
|
});
|