From 94a5d1bb14c5880a6c786eb07de299698cd3f993 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Fri, 19 Jun 2026 22:31:25 +0200 Subject: [PATCH] =?UTF-8?q?feat(ms-ai-architect):=20Sesjon=205=20carry-for?= =?UTF-8?q?ward=20B=20=E2=80=94=20datol=C3=B8se=20filer=20=E2=86=92=20unve?= =?UTF-8?q?rified-tier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01REiKFhP4w6xGXXqWKpPCJJ --- scripts/kb-update/data/domain-taxonomy.json | 3 +- scripts/kb-update/lib/taxonomy.mjs | 12 +++++- scripts/kb-update/report-changes.mjs | 13 +++--- .../test-priority-unverified.test.mjs | 43 +++++++++++++++++++ 4 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 tests/kb-update/test-priority-unverified.test.mjs diff --git a/scripts/kb-update/data/domain-taxonomy.json b/scripts/kb-update/data/domain-taxonomy.json index 7202dbc..da0700b 100644 --- a/scripts/kb-update/data/domain-taxonomy.json +++ b/scripts/kb-update/data/domain-taxonomy.json @@ -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" } } diff --git a/scripts/kb-update/lib/taxonomy.mjs b/scripts/kb-update/lib/taxonomy.mjs index c0e11c4..75c376f 100644 --- a/scripts/kb-update/lib/taxonomy.mjs +++ b/scripts/kb-update/lib/taxonomy.mjs @@ -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; diff --git a/scripts/kb-update/report-changes.mjs b/scripts/kb-update/report-changes.mjs index ca6d99c..9dff37d 100644 --- a/scripts/kb-update/report-changes.mjs +++ b/scripts/kb-update/report-changes.mjs @@ -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:'); diff --git a/tests/kb-update/test-priority-unverified.test.mjs b/tests/kb-update/test-priority-unverified.test.mjs new file mode 100644 index 0000000..da96281 --- /dev/null +++ b/tests/kb-update/test-priority-unverified.test.mjs @@ -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'); +});