feat(ms-ai-architect): add v1→v2 MIGRATIONS handler with snapshot fixture and idempotency test

This commit is contained in:
Kjell Tore Guttormsen 2026-05-04 03:14:46 +02:00
commit 502faa97d5
3 changed files with 354 additions and 0 deletions

View file

@ -502,6 +502,10 @@
const loaded = await persistence.load();
// Sørg for at schemaVersion finnes (cold start kan returnere uten).
if (!loaded.schemaVersion) loaded.schemaVersion = SCHEMA_VERSION;
// Data-version migrasjon (v1.9.0 -> v1.10.0): utled verdict/keyStats
// for eksisterende parser-output. Idempotent via state.dataVersion-guard.
try { migrateDataVersion(loaded, defaultArchetypeFor); }
catch (e) { console.warn('[playground v3] migrateDataVersion failed:', e); }
store = createStore(loaded, sharedBus);
scheduleWrite = makeThrottledWriter(function () {
return persistence.save(store.raw);
@ -625,12 +629,17 @@
// Migrer envelope opp til gjeldende SCHEMA_VERSION før vi commit-er.
const migrated = migrateState({
schemaVersion: envelope.schemaVersion,
dataVersion: envelope.dataVersion,
shared: envelope.shared || INITIAL_STATE.shared,
projects: envelope.projects || [],
activeProjectId: envelope.activeProjectId || null,
activeSurface: envelope.activeSurface || 'home',
preferences: envelope.preferences || INITIAL_STATE.preferences
});
// Data-version migrasjon (additive — verdict/keyStats utledes for
// pre-v1.10.0 envelope-er). Idempotent via dataVersion=2 guard.
try { migrateDataVersion(migrated, defaultArchetypeFor); }
catch (e) { console.warn('[playground v3] migrateDataVersion (import) failed:', e); }
// Skriv direkte til persistens for å unngå at debounce-vinduet
// svelger import-en ved en samtidig page-unload.
if (persistence) {
@ -640,6 +649,7 @@
// sin referanse fordi Proxy-en er bundet til den; muter feltvis.
const target = store.raw;
target.schemaVersion = migrated.schemaVersion;
if (migrated.dataVersion != null) target.dataVersion = migrated.dataVersion;
target.shared = migrated.shared;
target.projects = migrated.projects;
target.activeProjectId = migrated.activeProjectId;
@ -3237,6 +3247,7 @@
slot.innerHTML = '<div class="diff">' + summaryHtml + headerHtml + rowsHtml + '</div>';
}
// === V2_FOUNDATION_BEGIN ===
// ============================================================
// FOUNDATION HELPERS (v1.10.0 Sesjon 1)
// ============================================================
@ -3511,6 +3522,50 @@
);
}
// ============================================================
// DATA-VERSION MIGRATION (v1->v2)
// ============================================================
//
// State.dataVersion sporer parser-output-format separat fra
// state.schemaVersion (som sporer state-shape). v1.9.0 produserte
// parser-output uten verdict/keyStats; v1.10.0 utvider med felles
// grunnskjelett-data. Migrasjonen er additive — eksisterende felter
// forblir uendret.
//
// v1_to_v2-handler: itererer projects[].reports[cmdId].parsed; hvis
// verdict eller keyStats mangler, utled fra eksisterende felter via
// inferVerdict + inferKeyStats. Setter state.dataVersion = 2 så
// migrasjonen er idempotent (re-kjøring er no-op).
function migrateDataVersion(state, archetypeFor) {
if (!state) return state;
if (state.dataVersion === 2) return state;
const projects = state.projects || [];
for (let i = 0; i < projects.length; i++) {
const reports = (projects[i] && projects[i].reports) || {};
const ids = Object.keys(reports);
for (let j = 0; j < ids.length; j++) {
const cmdId = ids[j];
const r = reports[cmdId];
if (!r || !r.parsed) continue;
const arche = typeof archetypeFor === 'function' ? archetypeFor(cmdId) : null;
if (!arche) continue;
if (r.parsed.verdict == null) r.parsed.verdict = inferVerdict(r.parsed, arche);
if (!Array.isArray(r.parsed.keyStats)) r.parsed.keyStats = inferKeyStats(r.parsed, arche);
}
}
state.dataVersion = 2;
return state;
}
function defaultArchetypeFor(commandId) {
const cmds = (CATALOG && CATALOG.commands) || [];
for (let i = 0; i < cmds.length; i++) {
if (cmds[i].id === commandId) return cmds[i].report_archetype || null;
}
return null;
}
// Eksponer for tester og fremtidig renderer-iterasjon (Sesjon 3-5)
window.__renderPageShell = renderPageShell;
window.__renderVerdictPill = renderVerdictPill;
@ -3518,6 +3573,9 @@
window.__inferVerdict = inferVerdict;
window.__inferKeyStats = inferKeyStats;
window.__KEY_STATS_CONFIG = KEY_STATS_CONFIG;
window.__migrateDataVersion = migrateDataVersion;
window.__defaultArchetypeFor = defaultArchetypeFor;
// === V2_FOUNDATION_END ===
// ---- RENDERERS routing-objekt (17 commands) ----

View file

@ -0,0 +1,111 @@
{
"schemaVersion": 1,
"shared": {
"organization": {
"name": "Acme AS",
"sector": "Statlig",
"regulatory_requirements": ["Personopplysningsloven/GDPR"]
},
"technology": {
"cloud_platform": ["Azure"],
"license_type": "E5",
"ai_services_in_use": ["Azure OpenAI"]
},
"security": {
"data_classification": ["Intern"],
"dpia_practice": "Systematisk"
},
"architecture": {
"annual_ai_budget": "500k-2M"
},
"business": {}
},
"projects": [
{
"id": "p-snapshot-classify",
"name": "Demosystem A — klassifisering",
"description": "Fiktiv test-prosjekt for v1->v2 migrasjons-test.",
"scenarios": [],
"createdAt": "2026-04-15T10:00:00.000Z",
"reports": {
"classify": {
"input": { "system_name": "Demosystem A" },
"raw_markdown": "# AI Act-klassifisering\n\nRisikonivå: Høy\nRolle: deployer",
"parsed": {
"risk_level": "Høy",
"role": "deployer",
"reasoning": "Beslutningsstøtte i forvaltningsbehandling.",
"obligations": ["Art. 13 — transparens", "Art. 14 — menneskelig tilsyn", "Art. 27 — FRIA"]
}
},
"ros": {
"input": { "system_name": "Demosystem A" },
"raw_markdown": "# ROS-analyse\n\nNS 5814 / ISO 31000.",
"parsed": {
"matrix_cells": [
{ "row": 4, "col": 4, "count": 2 },
{ "row": 3, "col": 5, "count": 1 }
],
"threats": [
{ "id": "T1", "description": "Modell-bias mot minoriteter", "severity": "Høy", "mitigation": "Bias-audit + kalibrering" },
{ "id": "T2", "description": "Privacy leak via prompts", "severity": "Kritisk", "mitigation": "DLP + redaction" },
{ "id": "T3", "description": "Hallusinerte fakta", "severity": "Medium", "mitigation": "Citation-grounding + reviewer" }
],
"radar_axes": [
{ "name": "Tilgjengelighet", "score": 3 },
{ "name": "Konfidensialitet", "score": 4 },
{ "name": "Integritet", "score": 4 },
{ "name": "Robusthet", "score": 3 },
{ "name": "Sporbarhet", "score": 2 },
{ "name": "Fairness", "score": 2 },
{ "name": "Transparens", "score": 3 }
]
}
}
}
},
{
"id": "p-snapshot-cost",
"name": "Demosystem B — kostnadsestimat",
"description": "Fiktiv kostnadsestimat-rapport for migrasjons-test.",
"scenarios": [],
"createdAt": "2026-04-20T09:30:00.000Z",
"reports": {
"cost": {
"input": { "system_name": "Demosystem B" },
"raw_markdown": "# Kostnadsestimat\n\nP10/P50/P90 i NOK/mnd.",
"parsed": {
"p10": 45000,
"p50": 82000,
"p90": 165000,
"monthly_breakdown": [
{ "component": "Azure OpenAI gpt-4o", "cost": 48000 },
{ "component": "Azure AI Search", "cost": 12000 },
{ "component": "Storage + log", "cost": 8000 }
],
"tco_table": [],
"tco_headers": []
}
},
"summary": {
"input": {},
"raw_markdown": "# Sammendrag\n\nBetinget anbefaling.",
"parsed": {
"verdict": "go-with-conditions",
"sub": "Med betingelser",
"rationale": "Kostnaden er innenfor rammen, men avhengig av governance-modning.",
"key_metrics": [
{ "label": "P50/mnd", "value": "82 000 NOK" },
{ "label": "Risikonivå", "value": "Høy" }
],
"metrics_headers": [],
"next_steps": ["Etabler DPIA", "Avklar dataleverandør-kontrakt"]
}
}
}
}
],
"activeProjectId": "p-snapshot-classify",
"activeSurface": "project",
"preferences": { "theme": "dark" }
}