feat(ms-ai-architect): Spor 1 — korpus-header-audit (verified/base-felt/dialekt-fordeling, TDD) [skip-docs]
This commit is contained in:
parent
f3d52e0692
commit
dc2146aa7a
2 changed files with 325 additions and 0 deletions
189
scripts/kb-update/audit-corpus-headers.mjs
Normal file
189
scripts/kb-update/audit-corpus-headers.mjs
Normal file
|
|
@ -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 <date>"), 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<string, object>, 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));
|
||||
136
tests/kb-update/test-audit-corpus-headers.test.mjs
Normal file
136
tests/kb-update/test-audit-corpus-headers.test.mjs
Normal file
|
|
@ -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 <date>", 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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue