feat(ms-ai-architect): Sesjon 2 - registry-skjema (authority_source + course reservert)

build-registry leser na **Source:**/**Primary source:**-header via ny
lib/kb-headers.mjs (parseSourceHeader, 7 enhetstester) og logger dekning
(0/N i dag). Hver URL-entry far reserverte felt authority_source:null +
course:null (bakoverkompatibelt). Populering med korrekt semantikk utsatt
til verifiseringslaget (lag 3) - minimal-reservert per operator-valg.

Eksisterende status-felt (poll: tracked/not_in_sitemap/unpolled) urort for
a unnga kollisjon. kb-update 56 tester gronne.

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:
Kjell Tore Guttormsen 2026-06-19 21:05:54 +02:00
commit eb2a002c2c
3 changed files with 90 additions and 0 deletions

View file

@ -9,6 +9,7 @@ import { join, relative, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { normalizeUrl, extractUrls } from './lib/url-normalize.mjs';
import { loadRegistry, saveRegistry } from './lib/registry-io.mjs';
import { parseSourceHeader } from './lib/kb-headers.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const PLUGIN_ROOT = join(__dirname, '..', '..');
@ -34,6 +35,7 @@ function walkMd(dir) {
const existing = merge ? loadRegistry() : null;
const urlToFiles = new Map(); // normalizedUrl → Set<relativePath>
let totalFiles = 0;
let filesWithSourceHeader = 0; // coverage of the optional **Source:** authority header
const skillDirs = readdirSync(SKILLS_DIR, { withFileTypes: true })
.filter(d => d.isDirectory())
@ -49,6 +51,11 @@ for (const skill of skillDirs) {
const urls = extractUrls(content);
const relPath = relative(PLUGIN_ROOT, file);
// Read the optional **Source:**/**Primary source:** authority header.
// 0% coverage today; tracked here so we can populate authority_source as
// files adopt it (population semantics designed in the verification layer).
if (parseSourceHeader(content)) filesWithSourceHeader++;
for (const url of urls) {
if (!urlToFiles.has(url)) urlToFiles.set(url, new Set());
urlToFiles.get(url).add(relPath);
@ -72,6 +79,10 @@ for (const [url, files] of urlToFiles) {
sitemap_lastmod: prev?.sitemap_lastmod || null,
reference_files: [...files].sort(),
status: prev?.status || 'unpolled',
// Reserved schema (lag 0): authority_source = declared verification source
// (**Source:** header, 0% coverage today); course = Learn course-track ref.
authority_source: prev?.authority_source ?? null,
course: prev?.course ?? null,
};
}
@ -81,6 +92,7 @@ saveRegistry(registry);
const multiRef = [...urlToFiles.values()].filter(s => s.size > 1).length;
console.log(`Registry built: ${urlToFiles.size} unique URLs from ${totalFiles} files`);
console.log(` URLs referenced by multiple files: ${multiRef}`);
console.log(` Files declaring **Source:** header: ${filesWithSourceHeader}/${totalFiles}`);
if (merge && existing?.urls) {
const newUrls = [...urlToFiles.keys()].filter(u => !existing.urls[u]).length;
console.log(` New URLs added (merge): ${newUrls}`);

View file

@ -0,0 +1,26 @@
// kb-headers.mjs — Parse optional authority headers from KB reference files.
// Zero dependencies. Scans only the top of the file (header region) where the
// **Source:** / **Primary source:** line lives, mirroring report-changes'
// parseLastUpdated convention.
const HEADER_REGION_BYTES = 500;
const SOURCE_PATTERNS = [
/\*\*Source:\*\*\s*(\S+)/i,
/\*\*Primary source:\*\*\s*(\S+)/i,
];
/**
* Extract the declared authority source URL from a KB reference file's header.
* @param {string} content full file content (only the first 500 bytes are scanned)
* @returns {string|null} the source URL, or null if no header is present
*/
export function parseSourceHeader(content) {
if (!content || typeof content !== 'string') return null;
const head = content.slice(0, HEADER_REGION_BYTES);
for (const pattern of SOURCE_PATTERNS) {
const match = head.match(pattern);
if (match) return match[1].trim();
}
return null;
}

View file

@ -0,0 +1,52 @@
// tests/kb-update/test-kb-headers.test.mjs
// Unit tests for scripts/kb-update/lib/kb-headers.mjs — parses the optional
// **Source:** / **Primary source:** authority header from a KB reference file.
// Coverage is 0% today (no file declares one yet); the parser is wired into
// build-registry so authority_source can populate as files adopt the header.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { parseSourceHeader } from '../../scripts/kb-update/lib/kb-headers.mjs';
test('parseSourceHeader — **Source:** header', () => {
const content = '# Title\n\n**Source:** https://learn.microsoft.com/azure/foundry/concepts/x\n**Status:** GA\n';
assert.equal(
parseSourceHeader(content),
'https://learn.microsoft.com/azure/foundry/concepts/x'
);
});
test('parseSourceHeader — **Primary source:** alias', () => {
const content = '# Title\n\n**Primary source:** https://learn.microsoft.com/azure/ai-services/y\n';
assert.equal(
parseSourceHeader(content),
'https://learn.microsoft.com/azure/ai-services/y'
);
});
test('parseSourceHeader — case-insensitive label', () => {
const content = '**source:** https://learn.microsoft.com/azure/z\n';
assert.equal(parseSourceHeader(content), 'https://learn.microsoft.com/azure/z');
});
test('parseSourceHeader — no header returns null', () => {
const content = '# Title\n\n**Last updated:** 2026-04\n**Status:** GA\n**Category:** RAG\n';
assert.equal(parseSourceHeader(content), null);
});
test('parseSourceHeader — empty/garbage input returns null', () => {
assert.equal(parseSourceHeader(''), null);
assert.equal(parseSourceHeader(null), null);
assert.equal(parseSourceHeader(undefined), null);
});
test('parseSourceHeader — only scans the top of the file (header region)', () => {
// A Source-like line buried far below the header region must NOT be picked up.
const buried = '# Title\n' + 'x'.repeat(800) + '\n**Source:** https://learn.microsoft.com/late\n';
assert.equal(parseSourceHeader(buried), null);
});
test('parseSourceHeader — trims trailing whitespace from URL', () => {
const content = '**Source:** https://learn.microsoft.com/azure/trim \n';
assert.equal(parseSourceHeader(content), 'https://learn.microsoft.com/azure/trim');
});