feat(ms-ai-architect): Sesjon 2 - ai-foundry->foundry registry-remap

Microsoft rebranet Azure AI Foundry -> Microsoft Foundry; doc-URLer flyttet
/azure/ai-foundry/ -> /azure/foundry/. Ny lib/registry-migrate.mjs (rene
funksjoner remapKey + migrateRegistry, 10 enhetstester) + one-shot
migrate-ai-foundry.mjs (backup via lib/backup.mjs + atomisk saveRegistry).

Remap: 164 ai-foundry-URLer -> foundry (2 probede DEAD-sider far eksplisitte
mal: evaluation-evaluators -> built-in-evaluators; agent-service ->
agents/overview). Remappede entries resettes til status=unpolled/lastmod=null
sa neste poll re-evaluerer. 1 kollisjon merget (ref_files unionert).
Schema-migrering: authority_source+course lagt til alle 1343 entries.
Idempotent. Registry er gitignored (regenererbar) - kun kode committes.

kb-update 66 tester gronne, validate 239.

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:09:00 +02:00
commit d3af3f73c8
3 changed files with 238 additions and 0 deletions

View file

@ -0,0 +1,80 @@
// registry-migrate.mjs — One-shot ai-foundry -> foundry URL remap + reserved
// schema migration. Pure functions (no I/O) so they are unit-testable; the
// migrate-ai-foundry.mjs script wires backup + atomic write around them.
//
// Microsoft rebranded "Azure AI Foundry" -> "Microsoft Foundry"; doc URLs moved
// /azure/ai-foundry/ -> /azure/foundry/. Two pages probed in Sesjon 1 are DEAD
// on a naive prefix swap and need explicit targets.
const AI_FOUNDRY_PREFIX = 'https://learn.microsoft.com/azure/ai-foundry/';
const FOUNDRY_PREFIX = 'https://learn.microsoft.com/azure/foundry/';
// Probed DEAD pages (Sesjon 1): naive prefix swap 404s, these are the live targets.
const DEAD_REMAPS = {
'https://learn.microsoft.com/azure/ai-foundry/concepts/evaluation-evaluators':
'https://learn.microsoft.com/azure/foundry/concepts/built-in-evaluators',
'https://learn.microsoft.com/azure/ai-foundry/agent-service':
'https://learn.microsoft.com/azure/foundry/agents/overview',
};
/**
* Remap a single registry URL key. DEAD overrides win; otherwise prefix-swap
* ai-foundry -> foundry; non-ai-foundry URLs are returned unchanged.
* @param {string} url
* @returns {string}
*/
export function remapKey(url) {
if (DEAD_REMAPS[url]) return DEAD_REMAPS[url];
if (url.startsWith(AI_FOUNDRY_PREFIX)) {
return FOUNDRY_PREFIX + url.slice(AI_FOUNDRY_PREFIX.length);
}
return url;
}
/**
* Migrate a registry: remap ai-foundry keys (resetting their tracking state so
* the next poll re-evaluates them), merge any target collisions, and ensure the
* reserved schema fields (authority_source, course) exist on every entry.
* Pure does not mutate the input.
* @param {object} registry
* @returns {{registry: object, stats: {remapped: number, deadRemapped: number, collisions: number}}}
*/
export function migrateRegistry(registry) {
const out = { ...registry, urls: {} };
const stats = { remapped: 0, deadRemapped: 0, collisions: 0 };
for (const [url, entry] of Object.entries(registry.urls)) {
const target = remapKey(url);
const isRemap = target !== url;
// Normalize every entry to the full schema (reserved fields included).
const base = {
sitemap_lastmod: entry.sitemap_lastmod ?? null,
reference_files: entry.reference_files ?? [],
status: entry.status ?? 'unpolled',
authority_source: entry.authority_source ?? null,
course: entry.course ?? null,
};
let next = base;
if (isRemap) {
stats.remapped++;
if (DEAD_REMAPS[url]) stats.deadRemapped++;
// The URL changed — old poll state is invalid; let the next poll decide.
next = { ...base, sitemap_lastmod: null, status: 'unpolled' };
}
if (out.urls[target]) {
// Two source keys collapsed onto one target — union the reference files.
stats.collisions++;
const merged = [
...new Set([...out.urls[target].reference_files, ...next.reference_files]),
].sort();
out.urls[target] = { ...out.urls[target], ...next, reference_files: merged };
} else {
out.urls[target] = next;
}
}
return { registry: out, stats };
}

View file

@ -0,0 +1,35 @@
#!/usr/bin/env node
// migrate-ai-foundry.mjs — One-shot migration: back up data/, remap registry
// keys ai-foundry -> foundry (+ 2 probed DEAD targets), migrate reserved schema
// fields, write atomically. Idempotent: re-running finds nothing to remap.
// Usage: node migrate-ai-foundry.mjs
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { loadRegistry, saveRegistry } from './lib/registry-io.mjs';
import { backupDir } from './lib/backup.mjs';
import { migrateRegistry } from './lib/registry-migrate.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const DATA_DIR = join(__dirname, 'data');
const PLUGIN_ROOT = join(__dirname, '..', '..');
const BACKUP_ROOT = join(PLUGIN_ROOT, '.kb-backup');
const registry = loadRegistry(DATA_DIR);
const before = Object.keys(registry.urls).length;
const { backupPath } = backupDir(DATA_DIR, BACKUP_ROOT);
console.log(`Backed up data/ -> ${backupPath}`);
const { registry: migrated, stats } = migrateRegistry(registry);
const after = Object.keys(migrated.urls).length;
saveRegistry(migrated, DATA_DIR);
const remainingAiFoundry = Object.keys(migrated.urls)
.filter((u) => u.includes('/azure/ai-foundry/')).length;
console.log('=== ai-foundry -> foundry remap ===');
console.log(`URLs: ${before} -> ${after} (collisions merged: ${stats.collisions})`);
console.log(`Remapped: ${stats.remapped} (DEAD explicit targets: ${stats.deadRemapped})`);
console.log(`Remaining ai-foundry keys: ${remainingAiFoundry} (expect 0)`);