From 995f64ad8cf41ed56e25762d0377d278c7725f1b Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Sun, 3 May 2026 17:59:01 +0200 Subject: [PATCH] feat(ms-ai-architect): playground v3 export/import with eager migrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 3/17 av Playground v3-leveransen. Eksport: - buildEnvelope(): { appId, schemaVersion, exportedAt, shared, projects, activeProjectId, activeSurface, preferences } — JSON.parse(JSON.stringify(...)) for å strippe Proxy-wrappere - exportState(): Blob + URL.createObjectURL + programmatisk -klikk + revokeObjectURL etter 0ms timeout. File System Access API krever HTTPS (secure context) og er ikke tilgjengelig på file:// — derfor Blob-pattern. - Filnavn-format: ms-ai-architect-playground-.json Import: - importState(File): file.text() -> JSON.parse -> envelope-validering (appId + schemaVersion required) -> migrateState() -> persistence.save() -> in-place state-update (Proxy-binding må bevares — kan ikke bytte raw-referansen) -> manuell 'change'-event-dispatch så subscribers re-rendrer - file.text() er Promise som fungerer på file:// uten secure context MIGRATIONS-pipeline: - Eager: alle migrasjoner kjøres sekvensielt fra fil-versjon til SCHEMA_VERSION ved import (ikke lazy ved access) - Nøkkel-format: 'N->M' (fortløpende). Aldri hopp over et steg. - Kaster eksplisitt feil ved manglende migrasjons-funksjon eller ved funksjon som ikke setter schemaVersion korrekt — silent corruption unngås (brief Risk High). Eksponerte globals: __buildEnvelope, __exportState, __importState, __MIGRATIONS. Verify-assert: JSON.parse(JSON.stringify(window.__buildEnvelope())).schemaVersion === 1 Plan: .claude/projects/2026-05-03-playground-v3-architecture/plan.md (Step 3) Co-Authored-By: Claude Opus 4.7 --- .../playground/ms-ai-architect-v3.html | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/plugins/ms-ai-architect/playground/ms-ai-architect-v3.html b/plugins/ms-ai-architect/playground/ms-ai-architect-v3.html index c4ba165..6f54f87 100644 --- a/plugins/ms-ai-architect/playground/ms-ai-architect-v3.html +++ b/plugins/ms-ai-architect/playground/ms-ai-architect-v3.html @@ -350,6 +350,144 @@ // Step 6+ vil trigger første render her. } + // ============================================================ + // EXPORT / IMPORT (Step 3) + // ============================================================ + // + // Brukeren kan eksportere hele state som JSON-fil og re-importere på en + // annen enhet (eller etter localStorage.clear()). Format er en envelope + // med schemaVersion + appId — så fremtidige versjoner kan lese gamle + // eksporter via MIGRATIONS-pipeline. + // + // File System Access API krever HTTPS (secure context) og er ikke + // tilgjengelig på file:// — vi bruker Blob + URL.createObjectURL + + // for eksport, og + File.text() for + // import. Begge fungerer på file:// i alle target-browsers. + // + // MIGRATIONS er en eager pipeline: ved import (eller cold-load fra + // gammel state) kjøres alle migrasjoner sekvensielt fra fil-versjon til + // gjeldende SCHEMA_VERSION. Aldri hopp over et steg — selv tomme + // migrasjoner skal være registrert (no-op) for å bevise at hoppet er + // håndtert. + + const APP_ID = 'ms-ai-architect-playground'; + + function buildEnvelope() { + // Snapshot av rå state. JSON.stringify(JSON.parse(...)) sørger for + // at Proxy-er er stripped; vi vil ikke at envelopet skal beholde + // wrapper-referanser. + const snapshot = store ? JSON.parse(JSON.stringify(store.raw)) : JSON.parse(JSON.stringify(INITIAL_STATE)); + return { + appId: APP_ID, + schemaVersion: snapshot.schemaVersion || SCHEMA_VERSION, + exportedAt: new Date().toISOString(), + shared: snapshot.shared, + projects: snapshot.projects, + activeProjectId: snapshot.activeProjectId, + activeSurface: snapshot.activeSurface, + preferences: snapshot.preferences + }; + } + + function exportState() { + const envelope = buildEnvelope(); + const json = JSON.stringify(envelope, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + const stamp = envelope.exportedAt.replace(/[:.]/g, '-'); + a.href = url; + a.download = APP_ID + '-' + stamp + '.json'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + // revokeObjectURL etter at klikket har skjedd; setTimeout 0 er nok i + // alle target-browsers (Blob URL holdes så lenge nedlastningen står + // for å initieres). + setTimeout(function () { URL.revokeObjectURL(url); }, 0); + return envelope; + } + + // MIGRATIONS-pipeline. Nøkkel-format: 'N->M' der N og M er fortløpende + // SCHEMA_VERSION-tall. Funksjon tar et state-objekt og returnerer et + // nytt state-objekt på neste versjon. Aldri muter input. + // + // Når SCHEMA_VERSION bumpes til 2: legg til '1->2'-funksjon som + // transformerer v1-state til v2-form. importState plukker opp + // migrasjonen automatisk. + const MIGRATIONS = { + // Eksempel for fremtid (no-op stub): + // '1->2': function (state) { return Object.assign({}, state, { schemaVersion: 2 }); } + }; + + function migrateState(state) { + let current = state; + let from = current.schemaVersion || 1; + while (from < SCHEMA_VERSION) { + const key = from + '->' + (from + 1); + const fn = MIGRATIONS[key]; + if (!fn) { + throw new Error('[playground v3] mangler migrasjon ' + key + ' — kan ikke trygt oppgradere import-fil'); + } + current = fn(current); + if (current.schemaVersion !== from + 1) { + throw new Error('[playground v3] migrasjon ' + key + ' satte ikke schemaVersion til ' + (from + 1)); + } + from = current.schemaVersion; + } + return current; + } + + async function importState(file) { + // file er File-objekt fra change-event. + // file.text() er Promise — fungerer på file:// uten secure context. + const text = await file.text(); + let envelope; + try { + envelope = JSON.parse(text); + } catch (err) { + throw new Error('Ugyldig JSON: ' + err.message); + } + if (envelope.appId !== APP_ID) { + throw new Error('Fil-en er ikke en ' + APP_ID + '-eksport (appId=' + envelope.appId + ')'); + } + if (typeof envelope.schemaVersion !== 'number') { + throw new Error('Mangler schemaVersion i envelope'); + } + // Migrer envelope opp til gjeldende SCHEMA_VERSION før vi commit-er. + const migrated = migrateState({ + schemaVersion: envelope.schemaVersion, + shared: envelope.shared || INITIAL_STATE.shared, + projects: envelope.projects || [], + activeProjectId: envelope.activeProjectId || null, + activeSurface: envelope.activeSurface || 'home', + preferences: envelope.preferences || INITIAL_STATE.preferences + }); + // Skriv direkte til persistens for å unngå at debounce-vinduet + // svelger import-en ved en samtidig page-unload. + if (persistence) { + await persistence.save(migrated); + } + // Erstatt store-state in-place. Vi kan ikke bytte ut store.raw + // sin referanse fordi Proxy-en er bundet til den; muter feltvis. + const target = store.raw; + target.schemaVersion = migrated.schemaVersion; + target.shared = migrated.shared; + target.projects = migrated.projects; + target.activeProjectId = migrated.activeProjectId; + target.activeSurface = migrated.activeSurface; + target.preferences = migrated.preferences; + // Trigger en change-event manuelt så subscribers re-rendrer. + sharedBus.dispatchEvent(new CustomEvent('change', { detail: { paths: ['*'] } })); + return migrated; + } + + // Eksponer for UI-handlere (Step 5+) og DevTools-debugging. + window.__buildEnvelope = buildEnvelope; + window.__exportState = exportState; + window.__importState = importState; + window.__MIGRATIONS = MIGRATIONS; + // Auto-bootstrap. Kjør så snart DOM er parsed; vi er på slutten av // så DOM er allerede klar. bootstrap().catch(function (err) {