ms-ai-architect/tests/kb-update/test-verified-staleness.test.mjs
Kjell Tore Guttormsen fea5df5e68 feat(ms-ai-architect): Spor 3 Port 3 (inkrementell) — stale-since-verified klassifiserer + CLI [skip-docs]
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.
2026-06-29 14:53:51 +02:00

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']);
});