diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index f65b7c2..ffbfbc4 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -490,9 +490,12 @@ Recently cut: `tools/eveelectron/eveelectron-release-contract.mjs` and `scripts/run-eveelectron-release-contract-smoke.ps1`; capture uses `tools/eveelectron/eveelectron-capture-contract.mjs` and - `scripts/run-eveelectron-capture-contract-smoke.ps1`. The packaged Electron - app, packaged window rendering, and actual Electron window capture artifacts - remain pending EveElectron work. + `scripts/run-eveelectron-capture-contract-smoke.ps1`, plus a deterministic + `gamecult.eve.electron_shell_projection.v1` JSON projection artifact through + `tools/eveelectron/eveelectron-capture-artifact.mjs` and + `scripts/run-eveelectron-capture-smoke.ps1`. The packaged Electron app, + packaged window rendering, and actual Electron window PNG artifacts remain + pending EveElectron work. - Electron pending lifecycle details and TUI active-incubation lifecycle details now live in their runtime capability manifests rather than being duplicated in `tools/parity/parity-manifest.json`. The parity ledger points at the @@ -575,9 +578,12 @@ Recently cut: authority rule. `tools/eveelectron/eveelectron-capture-contract.mjs` builds a `gamecult.eve.runtime_capture_request.v1` request from the runtime capability manifest and provider advertisement, and - `scripts/run-eveelectron-capture-contract-smoke.ps1` proves that request path - without pretending an Electron window PNG has been captured. The actual - capture artifact remains a split blocker for EveElectron. + `scripts/run-eveelectron-capture-contract-smoke.ps1` proves that request path. + `tools/eveelectron/eveelectron-capture-artifact.mjs` now also lowers the + Aetheria world surface through `EveElectronShell`, writes + `artifacts/eveelectron-capture/latest/electron-shell-projection.json`, and + exports it as `runtime.captureArtifacts[]`. The actual packaged window PNG + remains a split blocker for EveElectron. - EveTui capture lifecycle now attaches a typed terminal-grid artifact instead of stopping at a request contract. `tools/evetui/evetui-capture-artifact.mjs` lowers `web/fixtures/aetheria-world-surface.json` through `EveTuiShell`, diff --git a/docs/parity-testing-harness.md b/docs/parity-testing-harness.md index cb48e46..a46edd1 100644 --- a/docs/parity-testing-harness.md +++ b/docs/parity-testing-harness.md @@ -325,9 +325,9 @@ Electron source paths named by the handoff currently exist as `observed-provider` sources, so the extraction pressure is visible without turning those paths into EveElectron authority. The Electron runtime now claims a narrow provider-advertised surface-tree command surface and exposes a typed -capture request contract. It preserves embedded document slot identity in the -generic shell projection, while packaged rendering, actual window capture, and -plugin projection remain blockers. +capture request contract plus a schema-backed JSON shell projection artifact. +It preserves embedded document slot identity in the generic shell projection, +while packaged rendering and actual window PNG capture remain blockers. EveTui is now an active incubating runtime: it claims a lossy provider-advertised terminal-grid command surface, preserves embedded document slot identity in the grid artifact, projects Sai/Norn/TeX fallback shells, and @@ -404,6 +404,13 @@ it without depending on Eve's source layout. The remaining blocker is owner-repo production of the same capture path, not absence of a cell-grid artifact. +Electron semantic capture evidence follows the same evidence rule without +pretending it is a packaged window PNG. `tools/eveelectron/eveelectron-capture-artifact.mjs` +lowers the Aetheria world surface through `EveElectronShell` into +`gamecult.eve.electron_shell_projection.v1` and writes +`artifacts/eveelectron-capture/latest/electron-shell-projection.json`. The +remaining blocker is packaged Electron window capture. + EveUnity split handoff evidence: ```powershell diff --git a/runtimes/incubating/eve-electron/eve-runtime-capability.json b/runtimes/incubating/eve-electron/eve-runtime-capability.json index d85cb4d..081237e 100644 --- a/runtimes/incubating/eve-electron/eve-runtime-capability.json +++ b/runtimes/incubating/eve-electron/eve-runtime-capability.json @@ -149,7 +149,10 @@ "evidencePaths": [ "tools/eveelectron/eveelectron-capture-contract.mjs", "tools/eveelectron/eveelectron-capture-contract.test.mjs", - "scripts/run-eveelectron-capture-contract-smoke.ps1" + "tools/eveelectron/eveelectron-capture-artifact.mjs", + "tools/eveelectron/eveelectron-capture-artifact.test.mjs", + "scripts/run-eveelectron-capture-contract-smoke.ps1", + "scripts/run-eveelectron-capture-smoke.ps1" ], "pendingProofs": [ "Electron window capture artifact is attached to the runtime conformance report." @@ -157,7 +160,19 @@ } }, "capture": { - "status": "missing", - "note": "Electron capture is a split blocker until EveElectron attaches provider-agnostic shell window artifacts." + "status": "json-projection", + "note": "Electron attaches a provider-advertisement-backed gamecult.eve.electron_shell_projection.v1 JSON projection artifact; packaged window PNG capture remains an EveElectron split blocker.", + "artifacts": [ + { + "kind": "json-projection", + "schema": "gamecult.eve.electron_shell_projection.v1", + "path": "artifacts/eveelectron-capture/latest/electron-shell-projection.json", + "runtimeId": "electron-shell", + "providerId": "aetheria", + "surfaceId": "aetheria.daemon.game", + "sourceSurfacePath": "web/fixtures/aetheria-world-surface.json", + "requestPath": "artifacts/eveelectron-capture/latest/capture-request.json" + } + ] } } diff --git a/runtimes/incubating/eve-electron/eveelectron-split-handoff.json b/runtimes/incubating/eve-electron/eveelectron-split-handoff.json index faf21c5..12106e7 100644 --- a/runtimes/incubating/eve-electron/eveelectron-split-handoff.json +++ b/runtimes/incubating/eve-electron/eveelectron-split-handoff.json @@ -73,10 +73,13 @@ "currentPaths": [ "tools/eveelectron/eveelectron-capture-contract.mjs", "tools/eveelectron/eveelectron-capture-contract.test.mjs", - "scripts/run-eveelectron-capture-contract-smoke.ps1" + "tools/eveelectron/eveelectron-capture-artifact.mjs", + "tools/eveelectron/eveelectron-capture-artifact.test.mjs", + "scripts/run-eveelectron-capture-contract-smoke.ps1", + "scripts/run-eveelectron-capture-smoke.ps1" ], "destinationOwner": "EveElectron", - "replacementProof": "EveElectron builds a provider-advertisement-backed capture request now; the split is not complete until the EveElectron repo attaches desktop-window capture artifacts to conformance output for at least one non-Aetheria provider and one Aetheria scenario." + "replacementProof": "EveElectron builds a provider-advertisement-backed capture request and deterministic gamecult.eve.electron_shell_projection.v1 JSON projection artifact now; the split is not complete until the EveElectron repo attaches desktop-window capture artifacts to conformance output for at least one non-Aetheria provider and one Aetheria scenario." } ], "contractInputs": [ @@ -103,6 +106,6 @@ "EveElectron lowers at least one generic Eve provider surface that is not Aetheria.", "EveElectron lowers Aetheria world/editor surfaces through provider advertisements without importing Aetheria internals.", "Aetheria keeps Starbridge-specific UI, assets, authored flows, and receipts in the Aetheria repo.", - "EveElectron attaches Electron capture artifacts to conformance output." + "EveElectron attaches packaged Electron window PNG capture artifacts to conformance output." ] } diff --git a/scripts/run-eveelectron-capture-smoke.ps1 b/scripts/run-eveelectron-capture-smoke.ps1 new file mode 100644 index 0000000..52c3564 --- /dev/null +++ b/scripts/run-eveelectron-capture-smoke.ps1 @@ -0,0 +1,73 @@ +param( + [string] $CapabilityManifestPath = "runtimes\incubating\eve-electron\eve-runtime-capability.json", + [string] $AdvertisementPath = "web\fixtures\aetheria.provider-advertisement.json", + [string] $SurfacePath = "web\fixtures\aetheria-world-surface.json", + [string] $OutputPath = "artifacts\eveelectron-capture\latest\electron-shell-projection.json", + [string] $RequestOutputPath = "artifacts\eveelectron-capture\latest\capture-request.json", + [string] $Stamp = "latest" +) + +$ErrorActionPreference = "Stop" + +$projectRoot = Split-Path -Parent $PSScriptRoot +function Resolve-RepoPath([string] $candidate) { + if ([System.IO.Path]::IsPathRooted($candidate)) { + return $candidate + } + return Join-Path $projectRoot $candidate +} + +$absoluteCapabilityPath = Resolve-RepoPath $CapabilityManifestPath +$absoluteAdvertisementPath = Resolve-RepoPath $AdvertisementPath +$absoluteSurfacePath = Resolve-RepoPath $SurfacePath +$absoluteOutputPath = Resolve-RepoPath $OutputPath +$absoluteRequestOutputPath = Resolve-RepoPath $RequestOutputPath + +foreach ($path in @($absoluteCapabilityPath, $absoluteAdvertisementPath, $absoluteSurfacePath)) { + if (-not (Test-Path -LiteralPath $path)) { + throw "EveElectron capture input not found: $path" + } +} + +node --test (Join-Path $projectRoot "tools\eveelectron\eveelectron-capture-artifact.test.mjs") +if ($LASTEXITCODE -ne 0) { + throw "EveElectron capture artifact tests failed with exit code $LASTEXITCODE" +} + +node (Join-Path $projectRoot "tools\eveelectron\eveelectron-capture-artifact.mjs") ` + --capability $absoluteCapabilityPath ` + --advertisement $absoluteAdvertisementPath ` + --surface $absoluteSurfacePath ` + --output $absoluteOutputPath ` + --request-output $absoluteRequestOutputPath ` + --stamp $Stamp +if ($LASTEXITCODE -ne 0) { + throw "EveElectron capture artifact builder failed with exit code $LASTEXITCODE" +} + +foreach ($path in @($absoluteOutputPath, $absoluteRequestOutputPath)) { + if (-not (Test-Path -LiteralPath $path)) { + throw "EveElectron capture artifact output missing: $path" + } +} + +$projection = Get-Content -LiteralPath $absoluteOutputPath -Raw | ConvertFrom-Json +foreach ($expectation in @( + @{ field = "schema"; value = "gamecult.eve.electron_shell_projection.v1" }, + @{ field = "runtimeId"; value = "electron-shell" }, + @{ field = "providerId"; value = "aetheria" }, + @{ field = "surfaceId"; value = "aetheria.daemon.game" }, + @{ field = "commandBoundary"; value = "aetheria.daemon.commands" }, + @{ field = "receiptSchema"; value = "aetheria.eve_command_acceptance_status.v1" }, + @{ field = "artifactKind"; value = "json-projection" }, + @{ field = "captureKind"; value = "electron-shell-projection-json" } +)) { + if ($projection.($expectation.field) -ne $expectation.value) { + throw "EveElectron projection $($expectation.field) expected $($expectation.value) got $($projection.($expectation.field))" + } +} +if (-not $projection.root.id) { + throw "EveElectron projection capture must contain a root shell node" +} + +Write-Host "EveElectron capture smoke passed: $absoluteOutputPath" diff --git a/scripts/run-eveelectron-lifecycle-smoke.ps1 b/scripts/run-eveelectron-lifecycle-smoke.ps1 index b4d52de..1e0615c 100644 --- a/scripts/run-eveelectron-lifecycle-smoke.ps1 +++ b/scripts/run-eveelectron-lifecycle-smoke.ps1 @@ -194,5 +194,6 @@ foreach ($pathProperty in @("requestBuilder", "advertisementPath")) { & (Join-Path $projectRoot "scripts\run-eveelectron-release-contract-smoke.ps1") & (Join-Path $projectRoot "scripts\run-eveelectron-provider-shell-smoke.ps1") & (Join-Path $projectRoot "scripts\run-eveelectron-capture-contract-smoke.ps1") +& (Join-Path $projectRoot "scripts\run-eveelectron-capture-smoke.ps1") Write-Host "EveElectron lifecycle smoke passed: $absoluteManifestPath" diff --git a/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 b/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 index ef095a3..3e7708d 100644 --- a/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 +++ b/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 @@ -133,7 +133,8 @@ node $consumerScript $consumerExport ` --expect-runtime-capture-status android-flutter:adb-png ` --expect-runtime-capture-status unity-uitoolkit:semantic ` --expect-runtime-capture-status unity-scene:missing ` - --expect-runtime-capture-status electron-shell:missing ` + --expect-runtime-capture-status electron-shell:json-projection ` + --expect-runtime-capture-artifact electron-shell:json-projection:gamecult.eve.electron_shell_projection.v1:aetheria:aetheria.daemon.game ` --expect-runtime-capture-status tui:json-grid ` --expect-runtime-capture-artifact tui:json-grid:gamecult.eve.tui_grid.v1:aetheria:aetheria.daemon.game ` --expect-runtime-capture-status direct2d:missing ` diff --git a/tools/eveelectron/eveelectron-capture-artifact.mjs b/tools/eveelectron/eveelectron-capture-artifact.mjs new file mode 100644 index 0000000..7e140c2 --- /dev/null +++ b/tools/eveelectron/eveelectron-capture-artifact.mjs @@ -0,0 +1,113 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { EveElectronShell } from "../../runtimes/incubating/eve-electron/src/eve-electron-shell.mjs"; +import { buildElectronCaptureRequest, loadJsonFile, writeCaptureRequest } from "./eveelectron-capture-contract.mjs"; + +export function buildElectronCaptureArtifact({ + advertisement, + capabilityManifest, + surfaceDocument, + advertisementPath = "", + capabilityManifestPath = "", + stamp = "latest", +} = {}) { + const request = buildElectronCaptureRequest({ + advertisement, + capabilityManifest, + advertisementPath, + capabilityManifestPath, + stamp, + }); + const shell = new EveElectronShell(); + const projection = shell.lowerSurface(surfaceDocument, advertisement, request.surfaceId); + validateProjectionArtifact(projection, request); + + return { + request, + projection: { + ...projection, + runtimeId: request.runtimeId, + artifactKind: "json-projection", + captureKind: "electron-shell-projection-json", + sourceAdvertisementPath: request.sourceAdvertisementPath, + sourceCapabilityManifestPath: request.sourceCapabilityManifestPath, + issuedAtUtc: request.issuedAtUtc, + }, + }; +} + +export function writeElectronCaptureArtifact({ request, projection }, { outputPath, requestOutputPath = "" } = {}) { + if (!outputPath) throw new Error("outputPath is required"); + mkdirSync(path.dirname(outputPath), { recursive: true }); + writeFileSync(outputPath, `${JSON.stringify(projection, null, 2)}\n`, "utf8"); + if (requestOutputPath) { + writeCaptureRequest(request, requestOutputPath); + } +} + +function validateProjectionArtifact(projection, request) { + if (projection.schema !== "gamecult.eve.electron_shell_projection.v1") { + throw new Error(`Unexpected Electron projection schema: ${projection.schema || ""}`); + } + for (const [field, expected] of [ + ["providerId", request.providerId], + ["surfaceId", request.surfaceId], + ["projectionKind", request.projectionKind], + ["commandBoundary", request.commandBoundary], + ["receiptSchema", request.receiptSchema], + ]) { + if ((projection[field] || "") !== (expected || "")) { + throw new Error(`Electron projection ${field} expected ${expected || ""} got ${projection[field] || ""}`); + } + } + if (!projection.root || !projection.root.id) { + throw new Error("Electron projection artifact must contain a root shell node"); + } +} + +function parseArgs(argv) { + const args = {}; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (!value.startsWith("--")) continue; + const key = value.slice(2); + const next = argv[index + 1]; + if (!next || next.startsWith("--")) { + args[key] = true; + } else { + args[key] = next; + index += 1; + } + } + return args; +} + +function runCli() { + const args = parseArgs(process.argv.slice(2)); + for (const required of ["advertisement", "capability", "surface", "output"]) { + if (!args[required]) { + throw new Error(`missing required --${required}`); + } + } + const stamp = args.stamp || "latest"; + const artifact = buildElectronCaptureArtifact({ + advertisement: loadJsonFile(args.advertisement), + capabilityManifest: loadJsonFile(args.capability), + surfaceDocument: loadJsonFile(args.surface), + advertisementPath: args.advertisement, + capabilityManifestPath: args.capability, + stamp, + }); + writeElectronCaptureArtifact(artifact, { + outputPath: args.output, + requestOutputPath: args["request-output"] || "", + }); + process.stdout.write(`${args.output}\n`); +} + +const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : ""; +const modulePath = fileURLToPath(import.meta.url); +if (invokedPath === modulePath) { + runCli(); +} diff --git a/tools/eveelectron/eveelectron-capture-artifact.test.mjs b/tools/eveelectron/eveelectron-capture-artifact.test.mjs new file mode 100644 index 0000000..3f00264 --- /dev/null +++ b/tools/eveelectron/eveelectron-capture-artifact.test.mjs @@ -0,0 +1,30 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; +import { buildElectronCaptureArtifact } from "./eveelectron-capture-artifact.mjs"; + +const advertisement = JSON.parse(readFileSync(new URL("../../web/fixtures/aetheria.provider-advertisement.json", import.meta.url), "utf8")); +const capabilityManifest = JSON.parse(readFileSync(new URL("../../runtimes/incubating/eve-electron/eve-runtime-capability.json", import.meta.url), "utf8")); +const surfaceDocument = JSON.parse(readFileSync(new URL("../../web/fixtures/aetheria-world-surface.json", import.meta.url), "utf8")); + +test("builds Electron shell projection capture artifact from provider advertisement and surface", () => { + const { request, projection } = buildElectronCaptureArtifact({ + advertisement, + capabilityManifest, + surfaceDocument, + advertisementPath: "web/fixtures/aetheria.provider-advertisement.json", + capabilityManifestPath: "runtimes/incubating/eve-electron/eve-runtime-capability.json", + stamp: "smoke", + }); + + assert.equal(request.artifactPath, "artifacts/eveelectron-capture/smoke/electron-shell.png"); + assert.equal(projection.schema, "gamecult.eve.electron_shell_projection.v1"); + assert.equal(projection.runtimeId, "electron-shell"); + assert.equal(projection.providerId, "aetheria"); + assert.equal(projection.surfaceId, "aetheria.daemon.game"); + assert.equal(projection.commandBoundary, "aetheria.daemon.commands"); + assert.equal(projection.receiptSchema, "aetheria.eve_command_acceptance_status.v1"); + assert.equal(projection.artifactKind, "json-projection"); + assert.equal(projection.captureKind, "electron-shell-projection-json"); + assert.equal(projection.root.id, "aetheria.daemon.game.root"); +}); diff --git a/tools/parity/parity-manifest.json b/tools/parity/parity-manifest.json index 8fed3ff..65f61e3 100644 --- a/tools/parity/parity-manifest.json +++ b/tools/parity/parity-manifest.json @@ -676,7 +676,7 @@ "pendingProofs": [ "Electron shell runtime body graduates to EveElectron outside Aetheria product code.", "Electron renders embedded Eve documents in a packaged Electron window.", - "Electron capture artifacts are attached to conformance output." + "Electron packaged window PNG capture artifacts are attached to conformance output." ] }, { @@ -782,10 +782,13 @@ "scripts/run-eveelectron-provider-shell-smoke.ps1", "scripts/run-eveelectron-release-contract-smoke.ps1", "scripts/run-eveelectron-capture-contract-smoke.ps1", + "scripts/run-eveelectron-capture-smoke.ps1", "tools/eveelectron/eveelectron-release-contract.mjs", "tools/eveelectron/eveelectron-release-contract.test.mjs", "tools/eveelectron/eveelectron-capture-contract.mjs", - "tools/eveelectron/eveelectron-capture-contract.test.mjs" + "tools/eveelectron/eveelectron-capture-contract.test.mjs", + "tools/eveelectron/eveelectron-capture-artifact.mjs", + "tools/eveelectron/eveelectron-capture-artifact.test.mjs" ], "expectedSourceSymbols": [ { @@ -804,6 +807,14 @@ "path": "tools/eveelectron/eveelectron-capture-contract.test.mjs", "contains": ["builds Electron shell capture request from provider advertisement", "rejects capture request when the surface does not advertise Electron shell", "electron-shell", "aetheria.daemon.commands"] }, + { + "path": "tools/eveelectron/eveelectron-capture-artifact.mjs", + "contains": ["buildElectronCaptureArtifact", "EveElectronShell", "gamecult.eve.electron_shell_projection.v1", "json-projection", "writeElectronCaptureArtifact", "request-output"] + }, + { + "path": "tools/eveelectron/eveelectron-capture-artifact.test.mjs", + "contains": ["builds Electron shell projection capture artifact", "gamecult.eve.electron_shell_projection.v1", "electron-shell-projection-json"] + }, { "path": "tools/eveelectron/eveelectron-release-contract.mjs", "contains": ["buildElectronReleaseRequest", "gamecult.eve.runtime_release_request.v1", "eveelectron-release", "writeReleaseRequest"] @@ -814,8 +825,20 @@ } ], "capture": { - "status": "missing", - "note": "Electron capture is a split blocker until EveElectron attaches provider-agnostic shell window artifacts." + "status": "json-projection", + "note": "Electron attaches a provider-advertisement-backed gamecult.eve.electron_shell_projection.v1 JSON projection artifact; packaged window PNG capture remains an EveElectron split blocker.", + "artifacts": [ + { + "kind": "json-projection", + "schema": "gamecult.eve.electron_shell_projection.v1", + "path": "artifacts/eveelectron-capture/latest/electron-shell-projection.json", + "runtimeId": "electron-shell", + "providerId": "aetheria", + "surfaceId": "aetheria.daemon.game", + "sourceSurfacePath": "web/fixtures/aetheria-world-surface.json", + "requestPath": "artifacts/eveelectron-capture/latest/capture-request.json" + } + ] } }, {