From dc2146aa7a566e45833a330a69cf208f9b806750 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Fri, 3 Jul 2026 00:50:57 +0200 Subject: [PATCH] =?UTF-8?q?feat(ms-ai-architect):=20Spor=201=20=E2=80=94?= =?UTF-8?q?=20korpus-header-audit=20(verified/base-felt/dialekt-fordeling,?= =?UTF-8?q?=20TDD)=20[skip-docs]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/kb-update/audit-corpus-headers.mjs | 189 ++++++++++++++++++ .../test-audit-corpus-headers.test.mjs | 136 +++++++++++++ 2 files changed, 325 insertions(+) create mode 100644 scripts/kb-update/audit-corpus-headers.mjs create mode 100644 tests/kb-update/test-audit-corpus-headers.test.mjs diff --git a/scripts/kb-update/audit-corpus-headers.mjs b/scripts/kb-update/audit-corpus-headers.mjs new file mode 100644 index 0000000..66f1c4a --- /dev/null +++ b/scripts/kb-update/audit-corpus-headers.mjs @@ -0,0 +1,189 @@ +#!/usr/bin/env node +// audit-corpus-headers.mjs — Spor 1 Step 1: read-only audit of the corpus header state. +// +// Measures, per non-advisor reference file and in aggregate, the empirical ground +// truth the migration plan rests on: header-region **Verified:** values (clean date +// vs stale "MCP "), body-duplicates and pipe-delimited meta rows inside the +// 500-byte header window, base-field presence (English **Last updated:**, **Status:**, +// title) and the header dialect (**Category:** / **Kategori:** / pipe). Mutates nothing. +// +// Usage: +// node scripts/kb-update/audit-corpus-headers.mjs # human summary +// node scripts/kb-update/audit-corpus-headers.mjs --json # full report to stdout +// node scripts/kb-update/audit-corpus-headers.mjs --write # persist data/corpus-header-audit.json + +import { readdirSync, readFileSync, existsSync, realpathSync } from 'node:fs'; +import { join, relative, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { atomicWriteJson } from './lib/atomic-write.mjs'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const PLUGIN_ROOT = join(__dirname, '..', '..'); +const SKILLS_DIR = join(PLUGIN_ROOT, 'skills'); +const OUT_PATH = join(__dirname, 'data', 'corpus-header-audit.json'); + +// The four non-advisor skills — advisor is out of audit scope (S-Cosmo: never mutated, +// and its header state is irrelevant to the Step-5/9 blast radius this audit pins). +const NON_ADVISOR_SKILLS = [ + 'ms-ai-engineering', + 'ms-ai-governance', + 'ms-ai-infrastructure', + 'ms-ai-security', +]; + +// Mirrors kb-headers.mjs — the Port-1 contract reads only the top 500 bytes. +const HEADER_REGION_BYTES = 500; + +// Base-field regexes mirror transform.mjs validateKbFile (RE_LAST_UPDATED:261 is not +// exported; replicated verbatim so the audit measures exactly what the guard reads). +const RE_TITLE = /^#\s+\S/m; +const RE_LAST_UPDATED = /\*\*Last updated:\*\*\s*[\d-]+/i; +const RE_STATUS = /\*\*Status:\*\*\s*\S/i; + +// Verified value = everything after the label up to a pipe separator or line end, +// so "MCP 2026-06" survives intact while a pipe row stops at its sibling token. +const RE_VERIFIED = /\*\*Verified:\*\*\s*([^|\n]*)/; +const RE_CLEAN_DATE = /^\d{4}-\d{2}(-\d{2})?$/; + +/** Audit a single file's content. Pure. */ +function auditContent(content) { + const head = content.slice(0, HEADER_REGION_BYTES); + + const verifiedMatches = [...head.matchAll(new RegExp(RE_VERIFIED.source, 'g'))]; + let verified = null; + if (verifiedMatches.length > 0) { + const value = verifiedMatches[0][1].trim(); + const line = head.slice(0, verifiedMatches[0].index).split('\n').length - 1; + const lineText = head.split('\n')[line] ?? ''; + verified = { + value, + isDate: RE_CLEAN_DATE.test(value), + pipe: lineText.includes(' | '), + duplicateWithinHeaderRegion: verifiedMatches.length > 1, + }; + } + + let dialect = 'none'; + if (/\*\*[^*\n]+:\*\*[^\n]* \| \*\*/.test(head)) dialect = 'pipe'; + else if (/\*\*Category:\*\*/i.test(head)) dialect = 'category'; + else if (/\*\*Kategori:\*\*/i.test(head)) dialect = 'kategori'; + + return { + title: RE_TITLE.test(content), + status: RE_STATUS.test(content), + lastUpdatedEnglish: RE_LAST_UPDATED.test(content), + dialect, + verified, + }; +} + +/** + * Audit a batch of files. Pure + dependency-injected reader (validate-kb-file.mjs + * pattern) so tests never touch disk. An unreadable file is reported, not thrown. + * + * @param {string[]} paths + * @param {(p: string) => string} [readFile] + * @returns {{files: Record, aggregate: object}} + */ +export function auditHeaders(paths, readFile = (p) => readFileSync(p, 'utf8')) { + const files = {}; + const aggregate = { + files: 0, + readErrors: 0, + verifiedPresent: 0, + verifiedDate: 0, + verifiedNonDate: 0, + verifiedPipe: 0, + verifiedDuplicate: 0, + missingTitle: 0, + missingStatus: 0, + missingEnglishLastUpdated: 0, + dialects: { category: 0, kategori: 0, pipe: 0, none: 0 }, + }; + + for (const path of paths ?? []) { + aggregate.files++; + let content; + try { + content = readFile(path); + } catch (err) { + files[path] = { error: err.message }; + aggregate.readErrors++; + continue; + } + const r = auditContent(content); + files[path] = r; + if (r.verified) { + aggregate.verifiedPresent++; + if (r.verified.isDate) aggregate.verifiedDate++; + else aggregate.verifiedNonDate++; + if (r.verified.pipe) aggregate.verifiedPipe++; + if (r.verified.duplicateWithinHeaderRegion) aggregate.verifiedDuplicate++; + } + if (!r.title) aggregate.missingTitle++; + if (!r.status) aggregate.missingStatus++; + if (!r.lastUpdatedEnglish) aggregate.missingEnglishLastUpdated++; + aggregate.dialects[r.dialect]++; + } + + return { files, aggregate }; +} + +// Walk directory recursively for .md files (build-registry.mjs shape). +function walkMd(dir) { + const results = []; + if (!existsSync(dir)) return results; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) { + results.push(...walkMd(full)); + } else if (entry.name.endsWith('.md') && entry.name !== 'SKILL.md') { + results.push(full); + } + } + return results; +} + +function main(argv) { + const json = argv.includes('--json'); + const write = argv.includes('--write'); + + const paths = NON_ADVISOR_SKILLS + .flatMap((skill) => walkMd(join(SKILLS_DIR, skill, 'references'))) + .sort(); + const report = auditHeaders(paths); + + // Re-key on plugin-relative paths for a stable, machine-diffable artifact. + const relFiles = {}; + for (const [abs, r] of Object.entries(report.files)) { + relFiles[relative(PLUGIN_ROOT, abs)] = r; + } + const out = { generated_by: 'audit-corpus-headers.mjs', files: relFiles, aggregate: report.aggregate }; + + if (write) { + atomicWriteJson(OUT_PATH, out); + console.log(`Wrote ${relative(PLUGIN_ROOT, OUT_PATH)}`); + } + if (json) { + process.stdout.write(JSON.stringify(out, null, 2) + '\n'); + } + if (!json) { + const a = report.aggregate; + console.log(`Corpus header audit — ${a.files} non-advisor reference files`); + console.log(` Verified header present: ${a.verifiedPresent} (date: ${a.verifiedDate}, stale non-date: ${a.verifiedNonDate})`); + console.log(` Verified pipe rows: ${a.verifiedPipe}, duplicates within 500B: ${a.verifiedDuplicate}`); + console.log(` Missing English Last updated: ${a.missingEnglishLastUpdated}`); + console.log(` Missing Status: ${a.missingStatus}`); + console.log(` Missing title: ${a.missingTitle}`); + console.log(` Dialects: category=${a.dialects.category} kategori=${a.dialects.kategori} pipe=${a.dialects.pipe} none=${a.dialects.none}`); + } +} + +const isMain = (() => { + try { + return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); + } catch { + return false; + } +})(); +if (isMain) main(process.argv.slice(2)); diff --git a/tests/kb-update/test-audit-corpus-headers.test.mjs b/tests/kb-update/test-audit-corpus-headers.test.mjs new file mode 100644 index 0000000..e49155c --- /dev/null +++ b/tests/kb-update/test-audit-corpus-headers.test.mjs @@ -0,0 +1,136 @@ +// tests/kb-update/test-audit-corpus-headers.test.mjs +// Spor 1 Step 1 — the read-only corpus-header audit that pins the migration's +// empirical ground truth: which files carry a stale (non-date) **Verified:**, +// which have a body-duplicate or pipe-delimited shape, which lack the English +// **Last updated:** / **Status:** base fields, and which header dialect each uses. +// +// auditHeaders is a pure function with an injectable reader (validate-kb-file.mjs +// pattern), so these tests never touch disk — synthetic fixtures only. + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { auditHeaders } from '../../scripts/kb-update/audit-corpus-headers.mjs'; + +// Stale verified: value is "MCP ", not a clean date. Norwegian dialect, +// no English **Last updated:** (the 45-file shape). +const V_NONDATE = + '# T\n\n**Kategori:** governance\n**Status:** GA\n**Sist oppdatert:** 2026-06\n' + + '**Verified:** MCP 2026-06\n\n---\n\n## A\n\ntekst\n'; + +// Clean date verified: must NOT be flagged stale. +const V_DATE = + '# T\n\n**Category:** governance\n**Status:** GA\n**Last updated:** 2026-06\n' + + '**Verified:** 2026-06\n\n---\n\n## A\n\ntekst\n'; + +// Body-duplicate: a second **Verified:** below the first `---` but still inside +// the 500-byte header window (the mlops-fundamentals-overview.md:10 shape). +const V_BODYDUP = + '# T\n\n**Status:** GA\n**Last updated:** 2026-06\n**Verified:** MCP 2026-06\n\n---\n\n' + + '## A\n\n**Verified:** MCP 2026-06\n\ntekst\n'; + +// Pipe-delimited meta row (the ai-center-of-excellence-setup.md:4 shape). +const V_PIPE = + '# T\n\n**Sist oppdatert:** 2026-06-17 | **Verified:** MCP 2026-06-17\n\n---\n\n' + + '## A\n\ntekst\n'; + +// Base-field gap: no **Status:** at all. +const NO_STATUS = + '# T\n\n**Category:** x\n**Last updated:** 2026-06\n\n---\n\n## A\n\ntekst\n'; + +// Fully conformant, no Verified — the quiet majority. +const CLEAN = + '# T\n\n**Category:** x\n**Status:** GA\n**Last updated:** 2026-06\n\n---\n\n## A\n\ntekst\n'; + +function reader(map) { + return (p) => { + if (!(p in map)) throw new Error(`unexpected read: ${p}`); + return map[p]; + }; +} + +test('auditHeaders — non-date **Verified:** MCP is flagged stale (isDate:false)', () => { + const r = auditHeaders(['a.md'], reader({ 'a.md': V_NONDATE })); + const f = r.files['a.md']; + assert.ok(f.verified, 'verified header detected'); + assert.equal(f.verified.isDate, false); + assert.equal(f.verified.value, 'MCP 2026-06'); + assert.equal(r.aggregate.verifiedNonDate, 1); + assert.equal(r.aggregate.verifiedDate, 0); +}); + +test('auditHeaders — clean date **Verified:** is NOT flagged stale', () => { + const r = auditHeaders(['b.md'], reader({ 'b.md': V_DATE })); + const f = r.files['b.md']; + assert.ok(f.verified); + assert.equal(f.verified.isDate, true); + assert.equal(r.aggregate.verifiedNonDate, 0); + assert.equal(r.aggregate.verifiedDate, 1); +}); + +test('auditHeaders — body-duplicate within 500B is flagged separately from the header hit', () => { + const r = auditHeaders(['c.md'], reader({ 'c.md': V_BODYDUP })); + const f = r.files['c.md']; + assert.ok(f.verified); + assert.equal(f.verified.duplicateWithinHeaderRegion, true); + assert.equal(r.aggregate.verifiedDuplicate, 1); + // the single-occurrence fixtures must not trip the duplicate flag + const r2 = auditHeaders(['a.md'], reader({ 'a.md': V_NONDATE })); + assert.equal(r2.files['a.md'].verified.duplicateWithinHeaderRegion, false); +}); + +test('auditHeaders — pipe-delimited **Verified:** row is flagged pipe, value stops at the separator', () => { + const r = auditHeaders(['d.md'], reader({ 'd.md': V_PIPE })); + const f = r.files['d.md']; + assert.ok(f.verified); + assert.equal(f.verified.pipe, true); + assert.equal(f.verified.value, 'MCP 2026-06-17'); + assert.equal(f.verified.isDate, false); + assert.equal(r.aggregate.verifiedPipe, 1); + assert.equal(f.dialect, 'pipe'); +}); + +test('auditHeaders — Norwegian **Sist oppdatert:** does not satisfy the English Last-updated check', () => { + const r = auditHeaders(['a.md'], reader({ 'a.md': V_NONDATE })); + const f = r.files['a.md']; + assert.equal(f.lastUpdatedEnglish, false); + assert.equal(r.aggregate.missingEnglishLastUpdated, 1); + assert.equal(f.dialect, 'kategori'); +}); + +test('auditHeaders — missing **Status:** is flagged', () => { + const r = auditHeaders(['e.md'], reader({ 'e.md': NO_STATUS })); + const f = r.files['e.md']; + assert.equal(f.status, false); + assert.equal(r.aggregate.missingStatus, 1); + assert.equal(f.dialect, 'category'); +}); + +test('auditHeaders — conformant file raises no flags; aggregate counts add up over a batch', () => { + const map = { + 'a.md': V_NONDATE, 'b.md': V_DATE, 'c.md': V_BODYDUP, + 'd.md': V_PIPE, 'e.md': NO_STATUS, 'f.md': CLEAN, + }; + const r = auditHeaders(Object.keys(map), reader(map)); + assert.equal(r.aggregate.files, 6); + const clean = r.files['f.md']; + assert.equal(clean.verified, null); + assert.equal(clean.title, true); + assert.equal(clean.status, true); + assert.equal(clean.lastUpdatedEnglish, true); + // a/c/d carry a non-date verified; b is the only clean date + assert.equal(r.aggregate.verifiedPresent, 4); + assert.equal(r.aggregate.verifiedNonDate, 3); + assert.equal(r.aggregate.verifiedDate, 1); + // e has no Status; d's pipe row carries no Status either + assert.equal(r.aggregate.missingStatus, 2); + // a (Sist oppdatert) and d (pipe row, no English Last updated) lack the English field + assert.equal(r.aggregate.missingEnglishLastUpdated, 2); + assert.equal(r.aggregate.missingTitle, 0); +}); + +test('auditHeaders — an unreadable file is reported, not thrown', () => { + const r = auditHeaders(['x.md'], () => { throw new Error('ENOENT'); }); + assert.equal(r.files['x.md'].error !== undefined, true); + assert.equal(r.aggregate.files, 1); + assert.equal(r.aggregate.readErrors, 1); +});