Ren lib full-pass-worklist.mjs (classifyFullPassDue + buildFullPassReport, skriver aldri til disk) + CLI report-full-pass-worklist.mjs (gitignored full-pass-worklist.json). Lastmod-UAVHENGIG: gater paa Port 1-kontrakt (type:reference + source) + cadence-alder paa Verified, fanger drift-klassen inkrementell bommer (staleness-recall 0/40). Buckets: never-verified / cadence-due / fresh / unsourced / unmigrated / out-of-scope. Arbeidsliste (operatoer-valgt design), IKKE inline judge: enumererer + rangerer, operatoer kickstarter judging separat (par.5 ikke auto-lansert, par.4c aldri auto-fiks). Ekte kjoering: 389 ref-filer alle unmigrated = dormant til Spor 1-migrering. Suite 637/637.
197 lines
8.6 KiB
JavaScript
197 lines
8.6 KiB
JavaScript
// tests/kb-update/test-full-pass-worklist.test.mjs
|
||
// Spor 3 Port 3 (kadens-judge) — PERIODIC full-pass WORKLIST (P3d).
|
||
//
|
||
// The incremental pass (P3a) only re-judges files whose source sitemap_lastmod
|
||
// moved past **Verified:** — it catches lastmod-VISIBLE drift. The full-pass
|
||
// catches the class staleness misses (base-rate showed staleness-recall 0/40):
|
||
// MS can change a page without bumping its sitemap lastmod. So the full-pass is
|
||
// deliberately LASTMOD-INDEPENDENT — it gates on the Port 1 frontmatter contract
|
||
// (type:reference + a Source URL to anchor against) and on cadence age of the
|
||
// **Verified:** stamp, NOT on lastmod. buildFileSourceMap (which skips sources
|
||
// without a lastmod) is reused only to ANNOTATE entries, never to gate them.
|
||
//
|
||
// classifyFullPassDue is PURE (mirrors classifyVerifiedStaleness): no disk, today
|
||
// + cadence injected. The operator-chosen design is a WORKLIST (not an inline
|
||
// judge): this produces the ranked list of files due for re-judge; the operator
|
||
// kicks off the actual judging separately (§5 "ikke auto-lansert", §4c "aldri
|
||
// auto-fiks", and the honest ~2700-fetch/multi-session cost forecloses inline).
|
||
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import {
|
||
classifyFullPassDue,
|
||
buildFullPassReport,
|
||
} from '../../scripts/kb-update/lib/full-pass-worklist.mjs';
|
||
|
||
const TODAY = '2026-06-29';
|
||
const MAX_AGE = 90; // threshold = 2026-03-31 (verified strictly before → cadence-due)
|
||
|
||
const f = (over) => ({
|
||
path: 'skills/x/references/a.md',
|
||
type: 'reference',
|
||
source: 'https://learn.microsoft.com/x',
|
||
verified: '2026-06-01',
|
||
newestSourceLastmod: null,
|
||
...over,
|
||
});
|
||
|
||
const opts = { today: TODAY, maxAgeDays: MAX_AGE };
|
||
|
||
// --- classifyFullPassDue: core scope + cadence classification ---
|
||
|
||
test('reference + source + never verified → due, reason never-verified', () => {
|
||
const out = classifyFullPassDue([f({ verified: null })], opts);
|
||
assert.equal(out.worklist.length, 1);
|
||
assert.equal(out.worklist[0].reason, 'never-verified');
|
||
assert.equal(out.worklist[0].ageDays, null);
|
||
assert.equal(out.counts.neverVerified, 1);
|
||
assert.equal(out.counts.due, 1);
|
||
});
|
||
|
||
test('reference + source + verified OLDER than cadence window → due, reason cadence-due', () => {
|
||
const out = classifyFullPassDue([f({ verified: '2026-01-01' })], opts);
|
||
assert.equal(out.worklist.length, 1);
|
||
assert.equal(out.worklist[0].reason, 'cadence-due');
|
||
assert.equal(out.worklist[0].ageDays, 179); // 2026-01-01 → 2026-06-29
|
||
assert.equal(out.counts.cadenceDue, 1);
|
||
assert.equal(out.counts.due, 1);
|
||
});
|
||
|
||
test('reference + source + verified WITHIN cadence window → fresh, not in worklist', () => {
|
||
const out = classifyFullPassDue([f({ verified: '2026-06-01' })], opts);
|
||
assert.equal(out.worklist.length, 0);
|
||
assert.equal(out.counts.fresh, 1);
|
||
});
|
||
|
||
test('verified EXACTLY at threshold (today − maxAgeDays) → fresh (strict <, mirrors incremental)', () => {
|
||
// 2026-06-29 minus 90 days = 2026-03-31
|
||
const out = classifyFullPassDue([f({ verified: '2026-03-31' })], opts);
|
||
assert.equal(out.worklist.length, 0);
|
||
assert.equal(out.counts.fresh, 1);
|
||
});
|
||
|
||
test('verified one day before threshold → cadence-due', () => {
|
||
const out = classifyFullPassDue([f({ verified: '2026-03-30' })], opts);
|
||
assert.equal(out.worklist.length, 1);
|
||
assert.equal(out.worklist[0].reason, 'cadence-due');
|
||
});
|
||
|
||
test('reference declared but NO source → unsourced (cannot anchor full-pass), counted not listed', () => {
|
||
const out = classifyFullPassDue([f({ source: null, verified: null })], opts);
|
||
assert.equal(out.worklist.length, 0);
|
||
assert.equal(out.counts.unsourced, 1);
|
||
});
|
||
|
||
test('template / methodology / regulatory → out-of-scope, never listed', () => {
|
||
const out = classifyFullPassDue([
|
||
f({ type: 'template', source: null, verified: null }),
|
||
f({ type: 'methodology', source: null, verified: null }),
|
||
f({ type: 'regulatory', source: null, verified: null }),
|
||
], opts);
|
||
assert.equal(out.worklist.length, 0);
|
||
assert.equal(out.counts.outOfScope, 3);
|
||
});
|
||
|
||
test('no Type header (legacy corpus) → unmigrated, counted but not listed (Spor 1 territory)', () => {
|
||
const out = classifyFullPassDue([f({ type: null, source: null, verified: null })], opts);
|
||
assert.equal(out.worklist.length, 0);
|
||
assert.equal(out.counts.unmigrated, 1);
|
||
});
|
||
|
||
test('YYYY-MM verified normalizes to -01 for the cadence boundary', () => {
|
||
// 2026-03 → 2026-03-01, which is before the 2026-03-31 threshold → due
|
||
const out = classifyFullPassDue([f({ verified: '2026-03' })], opts);
|
||
assert.equal(out.worklist.length, 1);
|
||
assert.equal(out.worklist[0].reason, 'cadence-due');
|
||
});
|
||
|
||
test('worklist entry carries path, reason, verified, ageDays, source for the human', () => {
|
||
const out = classifyFullPassDue([
|
||
f({ path: 'skills/s/references/p.md', verified: '2026-02-01', source: 'https://learn.microsoft.com/p' }),
|
||
], opts);
|
||
const e = out.worklist[0];
|
||
assert.equal(e.path, 'skills/s/references/p.md');
|
||
assert.equal(e.reason, 'cadence-due');
|
||
assert.equal(e.verified, '2026-02-01');
|
||
assert.equal(e.source, 'https://learn.microsoft.com/p');
|
||
assert.equal(typeof e.ageDays, 'number');
|
||
});
|
||
|
||
test('ranking: never-verified before cadence-due; within cadence-due oldest verified first', () => {
|
||
const out = classifyFullPassDue([
|
||
f({ path: 'b-recent-stale.md', verified: '2026-02-01' }), // cadence-due, 148d
|
||
f({ path: 'a-never.md', verified: null }), // never-verified
|
||
f({ path: 'c-oldest-stale.md', verified: '2026-01-01' }), // cadence-due, 179d
|
||
], opts);
|
||
assert.deepEqual(out.worklist.map((e) => e.path), ['a-never.md', 'c-oldest-stale.md', 'b-recent-stale.md']);
|
||
});
|
||
|
||
test('mixed corpus aggregates counts; total = input length; due = worklist length', () => {
|
||
const out = classifyFullPassDue([
|
||
f({ verified: '2026-01-01' }), // cadence-due
|
||
f({ verified: null }), // never-verified
|
||
f({ verified: '2026-06-20' }), // fresh
|
||
f({ source: null, verified: null }), // unsourced
|
||
f({ type: 'template', source: null, verified: null }), // out-of-scope
|
||
f({ type: null, source: null, verified: null }), // unmigrated
|
||
], opts);
|
||
assert.equal(out.counts.total, 6);
|
||
assert.equal(out.counts.cadenceDue, 1);
|
||
assert.equal(out.counts.neverVerified, 1);
|
||
assert.equal(out.counts.fresh, 1);
|
||
assert.equal(out.counts.unsourced, 1);
|
||
assert.equal(out.counts.outOfScope, 1);
|
||
assert.equal(out.counts.unmigrated, 1);
|
||
assert.equal(out.counts.due, 2);
|
||
assert.equal(out.worklist.length, 2);
|
||
});
|
||
|
||
test('empty input → empty worklist, zeroed counts', () => {
|
||
const out = classifyFullPassDue([], opts);
|
||
assert.equal(out.worklist.length, 0);
|
||
assert.equal(out.counts.total, 0);
|
||
assert.equal(out.counts.due, 0);
|
||
});
|
||
|
||
// --- buildFullPassReport: assemble the report (pure; corpus paths + reader injected) ---
|
||
|
||
const SOURCE_MAP = new Map([
|
||
['due.md', '2026-06-15'], // annotation only — does NOT affect due-ness
|
||
['fresh.md', '2026-01-01'],
|
||
]);
|
||
const PATHS = ['due.md', 'fresh.md', 'gone.md', 'legacy.md'];
|
||
const READER = (p) => ({
|
||
'due.md': { type: 'reference', source: 'https://learn.microsoft.com/due', verified: '2026-01-01' },
|
||
'fresh.md': { type: 'reference', source: 'https://learn.microsoft.com/fresh', verified: '2026-06-20' },
|
||
'gone.md': null, // deleted on disk
|
||
'legacy.md': { type: null, source: null, verified: null },
|
||
}[p] ?? null);
|
||
|
||
test('buildFullPassReport — stamps metadata, joins headers, classifies by cadence', () => {
|
||
const r = buildFullPassReport(PATHS, READER, SOURCE_MAP, opts);
|
||
assert.equal(r.generated_at, TODAY);
|
||
assert.equal(r.cadence_max_age_days, MAX_AGE);
|
||
assert.equal(r.counts.due, 1);
|
||
assert.equal(r.counts.fresh, 1);
|
||
assert.equal(r.counts.unmigrated, 1);
|
||
assert.equal(r.worklist.length, 1);
|
||
assert.equal(r.worklist[0].path, 'due.md');
|
||
assert.equal(r.worklist[0].reason, 'cadence-due');
|
||
});
|
||
|
||
test('buildFullPassReport — annotates worklist entry with newestSourceLastmod from the source map', () => {
|
||
const r = buildFullPassReport(PATHS, READER, SOURCE_MAP, opts);
|
||
assert.equal(r.worklist[0].newestSourceLastmod, '2026-06-15');
|
||
});
|
||
|
||
test('buildFullPassReport — deleted files (reader null) skipped entirely, not counted', () => {
|
||
const r = buildFullPassReport(PATHS, READER, SOURCE_MAP, opts);
|
||
assert.equal(r.counts.total, 3); // gone.md skipped
|
||
assert.ok(!r.worklist.some((e) => e.path === 'gone.md'));
|
||
});
|
||
|
||
test('buildFullPassReport — no source map still works (annotation null)', () => {
|
||
const r = buildFullPassReport(['due.md'], READER, new Map(), opts);
|
||
assert.equal(r.worklist.length, 1);
|
||
assert.equal(r.worklist[0].newestSourceLastmod, null);
|
||
});
|