feat(ms-ai-architect): Sesjon 5 carry-forward B — datoløse filer → unverified-tier
Rotårsak: en datoløs ref-fil fikk effectiveDate=0000-01-01 (alltid «stale») OG en path-basert prioritet (kunne bli critical) → flommet critical-bøtta og druknet genuint kritiske filer. Fix: ny unverified-tier deklarert i domain-taxonomy.json (lag 0, eneste sannhetskilde for prioritet). makePriorityFn(tax) tar nå valgfri hasDate (default true → bakoverkompatibelt for eksisterende én-args-kallere); report-changes sender Boolean(fileDate). unverified rangerer sist i sorteringen — manglende dato er metadata-hygiene, ikke currency-hast. Ekte registry: 1 fil skilt ut som Unverified. TDD: 4 tester FØR kode (test-priority-unverified.test.mjs). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01REiKFhP4w6xGXXqWKpPCJJ
This commit is contained in:
parent
97aa5d3378
commit
94a5d1bb14
4 changed files with 63 additions and 8 deletions
|
|
@ -90,6 +90,7 @@
|
|||
{ "match": "responsible-ai|governance|ai-security-(?:engineering|scoring)", "priority": "high" },
|
||||
{ "match": "platforms|copilot|azure-ai-services|agent-orchestration|rag|mlops|prompt-engineering|monitoring|performance", "priority": "medium" }
|
||||
],
|
||||
"default": "low"
|
||||
"default": "low",
|
||||
"dateless": "unverified"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,14 +68,22 @@ export function makeClassifier(tax) {
|
|||
/**
|
||||
* Build a file-priority function mirroring report-changes getFilePriority:
|
||||
* ordered regex rules over the lowercased path; first match wins; else default.
|
||||
*
|
||||
* A file with NO "Last updated:" header (hasDate=false) cannot have its currency
|
||||
* verified, so it lands in the dedicated `dateless` tier ('unverified') instead
|
||||
* of inheriting a path-based priority — otherwise dateless files flood the
|
||||
* critical bucket and drown the genuinely-stale critical files (carry-forward B).
|
||||
* hasDate defaults to true so existing single-arg callers keep path-based scoring.
|
||||
* @param {object} tax
|
||||
* @returns {(filePath: string) => string}
|
||||
* @returns {(filePath: string, hasDate?: boolean) => string}
|
||||
*/
|
||||
export function makePriorityFn(tax) {
|
||||
const rules = tax.priority.rules.map((r) => ({ re: new RegExp(r.match), priority: r.priority }));
|
||||
const fallback = tax.priority.default;
|
||||
const datelessTier = tax.priority.dateless ?? fallback;
|
||||
|
||||
return function getFilePriority(filePath) {
|
||||
return function getFilePriority(filePath, hasDate = true) {
|
||||
if (!hasDate) return datelessTier;
|
||||
const lower = filePath.toLowerCase();
|
||||
for (const r of rules) {
|
||||
if (r.re.test(lower)) return r.priority;
|
||||
|
|
|
|||
|
|
@ -41,8 +41,10 @@ function parseLastUpdated(filePath) {
|
|||
return null; // No date found — treat as always stale
|
||||
}
|
||||
|
||||
// Priority sort order
|
||||
const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
||||
// Priority sort order. 'unverified' (dateless files — no "Last updated:" header)
|
||||
// ranks last: a missing date is a metadata-hygiene gap, not a currency-urgency
|
||||
// signal, and must not masquerade as critical (carry-forward B).
|
||||
const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3, unverified: 4 };
|
||||
|
||||
// --- Main ---
|
||||
const registry = loadRegistry(DATA_DIR);
|
||||
|
|
@ -79,7 +81,8 @@ for (const [url, entry] of Object.entries(registry.urls)) {
|
|||
// Build report entries
|
||||
const files = [];
|
||||
for (const [path, changes] of fileChanges) {
|
||||
const priority = getFilePriority(path);
|
||||
// Dateless files (no "Last updated:" header) → 'unverified', not path-based.
|
||||
const priority = getFilePriority(path, Boolean(changes.fileDate));
|
||||
const pathParts = path.split('/');
|
||||
files.push({
|
||||
path,
|
||||
|
|
@ -101,7 +104,7 @@ files.sort((a, b) => {
|
|||
});
|
||||
|
||||
// Count by priority
|
||||
const byPriority = { critical: 0, high: 0, medium: 0, low: 0 };
|
||||
const byPriority = { critical: 0, high: 0, medium: 0, low: 0, unverified: 0 };
|
||||
for (const f of files) byPriority[f.priority]++;
|
||||
|
||||
const report = {
|
||||
|
|
@ -122,7 +125,7 @@ if (jsonOnly) {
|
|||
console.log(`\n=== KB Change Report (${report.generated_at}) ===`);
|
||||
console.log(`Sources last polled: ${registry.last_poll}`);
|
||||
console.log(`URLs tracked: ${report.total_tracked}/${Object.keys(registry.urls).length} (${report.total_not_in_sitemap} not in sitemap)`);
|
||||
console.log(`Files needing update: ${files.length} (Critical: ${byPriority.critical}, High: ${byPriority.high}, Medium: ${byPriority.medium}, Low: ${byPriority.low})`);
|
||||
console.log(`Files needing update: ${files.length} (Critical: ${byPriority.critical}, High: ${byPriority.high}, Medium: ${byPriority.medium}, Low: ${byPriority.low}, Unverified: ${byPriority.unverified})`);
|
||||
|
||||
if (files.length > 0) {
|
||||
console.log('\nTop 20 by priority:');
|
||||
|
|
|
|||
43
tests/kb-update/test-priority-unverified.test.mjs
Normal file
43
tests/kb-update/test-priority-unverified.test.mjs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// tests/kb-update/test-priority-unverified.test.mjs
|
||||
// TDD for carry-forward B (Sesjon 5): a reference file WITHOUT a "Last updated:"
|
||||
// header must land in the 'unverified' priority bucket — not masquerade as
|
||||
// critical-priority noise (the bug: dateless files got effectiveDate 0000-01-01
|
||||
// AND a path-based priority, so they flooded the critical tier and drowned the
|
||||
// genuinely-stale critical files).
|
||||
//
|
||||
// The decision lives in lag 0 (taxonomy.mjs makePriorityFn, the single source of
|
||||
// truth for priority); report-changes only supplies whether the file carried a
|
||||
// date header. The 'unverified' tier is declared in domain-taxonomy.json.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { loadTaxonomy, makePriorityFn } from '../../scripts/kb-update/lib/taxonomy.mjs';
|
||||
|
||||
const tax = loadTaxonomy();
|
||||
|
||||
test('dateless file → unverified, NOT its path-based critical priority', () => {
|
||||
const prio = makePriorityFn(tax);
|
||||
const path = 'skills/ms-ai-security/references/cost-optimization/budget.md';
|
||||
// Same path: dated → critical (path rule), dateless → unverified.
|
||||
assert.equal(prio(path, true), 'critical');
|
||||
assert.equal(prio(path, false), 'unverified');
|
||||
// The regression this fixes: dateless must never reach critical.
|
||||
assert.notEqual(prio(path, false), 'critical');
|
||||
});
|
||||
|
||||
test('backward-compat: single-arg call behaves as before (hasDate defaults true)', () => {
|
||||
const prio = makePriorityFn(tax);
|
||||
assert.equal(prio('skills/ms-ai-security/references/cost-optimization/budget.md'), 'critical');
|
||||
assert.equal(prio('skills/ms-ai-engineering/references/data-engineering/cosmos.md'), 'low');
|
||||
});
|
||||
|
||||
test('dateless overrides every path tier (high/medium/low → unverified)', () => {
|
||||
const prio = makePriorityFn(tax);
|
||||
assert.equal(prio('skills/ms-ai-governance/references/responsible-ai/fairness.md', false), 'unverified');
|
||||
assert.equal(prio('skills/ms-ai-advisor/references/platforms/copilot-studio.md', false), 'unverified');
|
||||
assert.equal(prio('skills/ms-ai-engineering/references/data-engineering/cosmos.md', false), 'unverified');
|
||||
});
|
||||
|
||||
test('taxonomy declares the dateless tier in lag 0 (source of truth)', () => {
|
||||
assert.equal(tax.priority.dateless, 'unverified');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue