feat(ms-ai-architect): Spor 3 Port 3 (P3d) — periodisk full-pass worklist (kadens-judge enumerering) [skip-docs]
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.
This commit is contained in:
parent
3d6fab12f7
commit
a1d72fadc0
3 changed files with 485 additions and 0 deletions
109
scripts/kb-update/report-full-pass-worklist.mjs
Normal file
109
scripts/kb-update/report-full-pass-worklist.mjs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
#!/usr/bin/env node
|
||||
// report-full-pass-worklist.mjs — Spor 3 Port 3, PERIODIC full-pass (P3d).
|
||||
// Enumerates the sourced `reference` files DUE for a cadence full-pass re-judge
|
||||
// and writes a ranked worklist. Unlike the incremental pass (which keys off
|
||||
// sitemap_lastmod), the full-pass is lastmod-INDEPENDENT: it gates on the Port 1
|
||||
// contract (type:reference + a Source URL) and on how long since **Verified:**,
|
||||
// catching the drift class the incremental pass misses (staleness-recall 0/40).
|
||||
//
|
||||
// Operator-chosen design (2026-06-29): a WORKLIST, not an inline judge. This
|
||||
// script ONLY enumerates and ranks; the operator kicks off the actual judging
|
||||
// separately (§5 "ikke auto-lansert", §4c "aldri auto-fiks", and the honest
|
||||
// ~2700-fetch/multi-session full-pass cost forecloses an in-session run). Run on
|
||||
// the KB-refresh cadence, NOT on SessionStart. All logic lives in
|
||||
// lib/full-pass-worklist.mjs (pure, unit-tested); this file is I/O glue
|
||||
// mirroring report-verified-staleness.mjs.
|
||||
//
|
||||
// Usage: node report-full-pass-worklist.mjs [--max-age-days N] [--json]
|
||||
|
||||
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { loadRegistry, saveReport } from './lib/registry-io.mjs';
|
||||
import { parseTypeHeader, parseVerifiedHeader, parseSourceHeader } from './lib/kb-headers.mjs';
|
||||
import { buildFileSourceMap } from './lib/verified-staleness.mjs';
|
||||
import { buildFullPassReport } from './lib/full-pass-worklist.mjs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const PLUGIN_ROOT = join(__dirname, '..', '..');
|
||||
const DATA_DIR = join(__dirname, 'data');
|
||||
const SKILLS_DIR = join(PLUGIN_ROOT, 'skills');
|
||||
|
||||
const jsonOnly = process.argv.includes('--json');
|
||||
const ageIdx = process.argv.indexOf('--max-age-days');
|
||||
const MAX_AGE_DAYS = ageIdx !== -1 ? Number(process.argv[ageIdx + 1]) : 90; // quarterly default
|
||||
|
||||
if (!Number.isFinite(MAX_AGE_DAYS) || MAX_AGE_DAYS < 0) {
|
||||
console.error('--max-age-days must be a non-negative number.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Recursively collect .md paths under dir, returned PLUGIN_ROOT-relative so they
|
||||
// match the registry's reference_files keys (mirrors eval.mjs' listMarkdown).
|
||||
function listMarkdownRel(dir) {
|
||||
const out = [];
|
||||
if (!existsSync(dir)) return out;
|
||||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||
const p = join(dir, e.name);
|
||||
if (e.isDirectory()) out.push(...listMarkdownRel(p));
|
||||
else if (e.isFile() && e.name.endsWith('.md')) out.push(p.slice(PLUGIN_ROOT.length + 1));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// The reference corpus = skills/<skill>/references/**/*.md ONLY (not SKILL.md or
|
||||
// other skill files). Walking each skill's references/ dir keeps the count
|
||||
// honest at the 389-file corpus the correctness scope is defined over.
|
||||
function listReferenceCorpus() {
|
||||
const out = [];
|
||||
if (!existsSync(SKILLS_DIR)) return out;
|
||||
for (const skill of readdirSync(SKILLS_DIR, { withFileTypes: true })) {
|
||||
if (!skill.isDirectory()) continue;
|
||||
out.push(...listMarkdownRel(join(SKILLS_DIR, skill.name, 'references')));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Read a reference file's Port 1 headers, or null if gone on disk (the report
|
||||
// skips it — registry hygiene, not a due signal). Only the top 500 bytes hold
|
||||
// the bold-label header region all three parsers scan.
|
||||
function headerReader(refFile) {
|
||||
const full = join(PLUGIN_ROOT, refFile);
|
||||
if (!existsSync(full)) return null;
|
||||
const head = readFileSync(full, 'utf8').slice(0, 500);
|
||||
return { type: parseTypeHeader(head), source: parseSourceHeader(head), verified: parseVerifiedHeader(head) };
|
||||
}
|
||||
|
||||
const registry = loadRegistry(DATA_DIR);
|
||||
const sourceMap = buildFileSourceMap(registry); // annotation only — never gates due-ness
|
||||
const paths = listReferenceCorpus();
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
const report = buildFullPassReport(paths, headerReader, sourceMap, { today, maxAgeDays: MAX_AGE_DAYS });
|
||||
saveReport('full-pass-worklist.json', report, DATA_DIR);
|
||||
|
||||
if (jsonOnly) {
|
||||
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
||||
} else {
|
||||
const c = report.counts;
|
||||
console.log(`\n=== Full-pass Worklist (${report.generated_at}) — cadence ${MAX_AGE_DAYS}d ===`);
|
||||
console.log(
|
||||
`Reference files: ${c.total} ` +
|
||||
`(due: ${c.due} [never-verified: ${c.neverVerified}, cadence-due: ${c.cadenceDue}], ` +
|
||||
`fresh: ${c.fresh}, unsourced: ${c.unsourced}, unmigrated: ${c.unmigrated}, out-of-scope: ${c.outOfScope})`,
|
||||
);
|
||||
if (report.worklist.length > 0) {
|
||||
console.log(`\nDue for full-pass re-judge (${report.worklist.length}, ranked):`);
|
||||
for (const e of report.worklist.slice(0, 30)) {
|
||||
const age = e.ageDays == null ? 'never verified' : `verified ${e.ageDays}d ago`;
|
||||
console.log(` [${e.reason}] ${e.path}`);
|
||||
console.log(` ${age} source: ${e.source}`);
|
||||
}
|
||||
if (report.worklist.length > 30) console.log(` ... and ${report.worklist.length - 30} more`);
|
||||
console.log('\nNext: kick off the judging separately (cadence run / Voyage). Each flag → human-confirm against source → fix → bump **Verified:** (never auto-fix).');
|
||||
} else if (c.unmigrated > 0 && c.due === 0 && c.fresh === 0) {
|
||||
console.log('\nNo sourced reference files yet — full-pass is dormant until Spor 1 migration backfills Source/Verified.');
|
||||
} else {
|
||||
console.log('\nNo files due for full-pass: every sourced reference file was verified within the cadence window.');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue