feat(ms-ai-architect): Spor 1 — klassifiserer-CLI + registry-merge (389 klassifisert, union reconciled) [skip-docs]

This commit is contained in:
Kjell Tore Guttormsen 2026-07-03 00:55:15 +02:00
commit 9e4bf2a4f9
2 changed files with 252 additions and 0 deletions

View file

@ -6,12 +6,20 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import {
classifyRefType,
classifyCorpus,
VALID_TYPES,
} from '../../scripts/kb-update/lib/classify-ref-type.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const CLI = join(__dirname, '..', '..', 'scripts', 'kb-update', 'classify-ref-type.mjs');
const MS_CITING =
'# Azure AI Search\n\n**Category:** rag\n\n---\n\n## A\n\n' +
'Se https://learn.microsoft.com/azure/search/hybrid-search-overview for detaljer.\n';
@ -103,3 +111,112 @@ test('classifyCorpus — relpath-keyed, key-sorted, deterministic across runs',
assert.equal(typeof entry.mscite, 'boolean');
}
});
// ---------------------------------------------------------------------------
// Step 3 — the IO-shell CLI: full-corpus classification, --write persistence
// and the manifest re-entrance guard (plan v1.8: --write refuses exit 3 on an
// enriched manifest unless --merge; --merge preserves enrichment verbatim).
// ---------------------------------------------------------------------------
// A real corpus file (stable since 2026-06) used as the enriched-entry key.
const REAL_RELPATH = 'skills/ms-ai-engineering/references/mlops-genaiops/mlops-fundamentals-overview.md';
const GONE_RELPATH = 'skills/ms-ai-engineering/references/zzz-does-not-exist.md';
// Deliberately different from what fresh classification would produce, so
// verbatim preservation is distinguishable from re-classification.
const ENRICHED_ENTRY = {
type: 'methodology',
signal: 'test-signal',
mscite: true,
reviewFlag: false,
resolvedBy: 'human-test',
source: 'https://learn.microsoft.com/test-authority',
};
test('CLI --json — exactly 389 relpath-keyed entries, every type valid, zero unclassified', () => {
const out = execFileSync('node', [CLI, '--json'], { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
const manifest = JSON.parse(out);
const keys = Object.keys(manifest);
assert.equal(keys.length, 389);
assert.deepEqual(keys, [...keys].sort());
for (const [path, entry] of Object.entries(manifest)) {
assert.ok(path.startsWith('skills/'), path);
assert.ok(VALID_TYPES.includes(entry.type), `${path}: ${entry.type}`);
}
});
test('CLI — without --write no manifest file is created', () => {
const tmp = mkdtempSync(join(tmpdir(), 'clsrt-'));
const out = join(tmp, 'manifest.json');
try {
execFileSync('node', [CLI, '--out', out], { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
assert.equal(existsSync(out), false);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
test('CLI --write — persists a parseable, key-sorted manifest to --out', () => {
const tmp = mkdtempSync(join(tmpdir(), 'clsrt-'));
const out = join(tmp, 'manifest.json');
try {
execFileSync('node', [CLI, '--write', '--out', out], { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
const manifest = JSON.parse(readFileSync(out, 'utf8'));
assert.equal(Object.keys(manifest).length, 389);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
test('CLI --write — refuses (exit 3, file untouched) when the existing manifest is enriched', () => {
const tmp = mkdtempSync(join(tmpdir(), 'clsrt-'));
const out = join(tmp, 'manifest.json');
try {
const existing = { [REAL_RELPATH]: ENRICHED_ENTRY };
writeFileSync(out, JSON.stringify(existing, null, 2) + '\n');
const before = readFileSync(out, 'utf8');
let status = 0;
try {
execFileSync('node', [CLI, '--write', '--out', out], { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
} catch (err) {
status = err.status;
}
assert.equal(status, 3);
assert.equal(readFileSync(out, 'utf8'), before);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
test('CLI --write --merge — preserves enrichment verbatim, adds disk-new, drops deleted', () => {
const tmp = mkdtempSync(join(tmpdir(), 'clsrt-'));
const out = join(tmp, 'manifest.json');
try {
const existing = {
[REAL_RELPATH]: ENRICHED_ENTRY,
[GONE_RELPATH]: { type: 'reference', signal: 'ms-citation', mscite: true, reviewFlag: false },
};
writeFileSync(out, JSON.stringify(existing, null, 2) + '\n');
execFileSync('node', [CLI, '--write', '--merge', '--out', out], { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
const manifest = JSON.parse(readFileSync(out, 'utf8'));
// deleted-on-disk entry dropped; disk-new files added → the live 389
assert.equal(manifest[GONE_RELPATH], undefined);
assert.equal(Object.keys(manifest).length, 389);
// enriched entry preserved verbatim — type/reviewFlag/resolvedBy/source/signal intact
assert.deepEqual(manifest[REAL_RELPATH], ENRICHED_ENTRY);
const keys = Object.keys(manifest);
assert.deepEqual(keys, [...keys].sort());
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
test('CLI — unknown flag → usage error exit 2', () => {
let status = 0;
try {
execFileSync('node', [CLI, '--bogus'], { encoding: 'utf8' });
} catch (err) {
status = err.status;
}
assert.equal(status, 2);
});