Add Electron capture request contract

This commit is contained in:
Meta Vens 2026-07-09 11:15:59 +01:00
commit cc556c2c0c
13 changed files with 348 additions and 15 deletions

View file

@ -416,10 +416,10 @@ Recently cut:
`runtimes/incubating/eve-electron/eveelectron-split-handoff.json`. It does
claim a provider-agnostic Electron shell surface-tree projection and command
surface, but not packaged rendering, embedded document rendering, plugin
projection, or capture. The handoff draws the line between the generic shell
and Aetheria's Starbridge RTS product client, and the split-target consumer
smoke keeps release, packaged rendering, embedded document rendering, and
capture proofs visible as blockers.
projection, or captured window artifacts. The handoff draws the line between
the generic shell and Aetheria's Starbridge RTS product client, and the
split-target consumer smoke keeps release, packaged rendering, embedded
document rendering, and capture artifact proofs visible as blockers.
- EveElectron now has a direct split handoff smoke at
`scripts/run-eveelectron-split-handoff-smoke.ps1`. The smoke verifies the
current generic shell, command, and world-surface lowering paths, the observed
@ -431,8 +431,11 @@ Recently cut:
lifecycle smoke at `scripts/run-eveelectron-lifecycle-smoke.ps1`. The
manifest declares provider advertisement consumption, command transport, and
surface-tree projection, claims `electron-shell` world/editor lowering, and
still keeps package release, packaged window rendering, plugin projection,
and Electron window capture as pending EveElectron work.
now carries an executable capture request contract through
`tools/eveelectron/eveelectron-capture-contract.mjs` and
`scripts/run-eveelectron-capture-contract-smoke.ps1`. Package release,
packaged window rendering, plugin projection, and actual Electron window
capture artifacts remain pending EveElectron work.
- Electron and TUI pending 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
@ -493,6 +496,16 @@ Recently cut:
`scripts/run-eveunity-capture-contract-smoke.ps1` proves that request path
without pretending a Unity PNG has been captured. The actual capture artifact
remains a split blocker for EveUnity.
- EveElectron capture lifecycle now carries the same kind of structured pending
capture contract. `captureContract` names the Electron shell runtime, capture
kind, PNG artifact pattern, conformance attachment point, required Aetheria
provider surface, request schema, request builder, advertisement input, and
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.
- Runtime-owner conformance consumption now asserts EveUnity lifecycle stage
evidence directly. The generic consumer supports
`--expect-runtime-lifecycle-status` and

View file

@ -303,9 +303,10 @@ provider-agnostic shell exists. The same smoke asserts that the Aetheria
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, while packaged
rendering, embedded document rendering, capture, and plugin projection remain
blockers. EveTui is also intentionally pending: it now claims a lossy
a narrow provider-advertised surface-tree command surface and exposes a typed
capture request contract, while packaged rendering, embedded document
rendering, actual window capture, and plugin projection remain blockers.
EveTui is also intentionally pending: it now claims a lossy
provider-advertised terminal-grid command surface, while transcript/cell-grid
capture, embedded document rendering, plugin projection, and repo graduation
remain blockers. EveUnity includes both the active `unity-uitoolkit` package proof

View file

@ -110,6 +110,10 @@
"captureContract": {
"ownerRepo": "EveElectron",
"runtimeId": "electron-shell",
"targetId": "electron-shell",
"requestSchema": "gamecult.eve.runtime_capture_request.v1",
"requestBuilder": "tools/eveelectron/eveelectron-capture-contract.mjs",
"advertisementPath": "web/fixtures/aetheria.provider-advertisement.json",
"captureKind": "electron-window-png",
"artifactKind": "png",
"artifactPattern": "artifacts/eveelectron-capture/{stamp}/electron-shell.png",
@ -119,7 +123,11 @@
"authority": "Capture observes Electron projection only; provider state, command acceptance, and receipts stay provider-owned.",
"publishProof": "Electron window capture artifact is attached to the runtime conformance report from EveElectron."
},
"evidencePaths": [],
"evidencePaths": [
"tools/eveelectron/eveelectron-capture-contract.mjs",
"tools/eveelectron/eveelectron-capture-contract.test.mjs",
"scripts/run-eveelectron-capture-contract-smoke.ps1"
],
"pendingProofs": [
"Electron window capture artifact is attached to the runtime conformance report."
]

View file

@ -57,9 +57,13 @@
{
"id": "electron-capture-lifecycle",
"stage": "capture",
"currentPaths": [],
"currentPaths": [
"tools/eveelectron/eveelectron-capture-contract.mjs",
"tools/eveelectron/eveelectron-capture-contract.test.mjs",
"scripts/run-eveelectron-capture-contract-smoke.ps1"
],
"destinationOwner": "EveElectron",
"replacementProof": "EveElectron 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 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": [

View file

@ -0,0 +1,99 @@
param(
[string] $CapabilityManifestPath = "runtimes\incubating\eve-electron\eve-runtime-capability.json",
[string] $AdvertisementPath = "web\fixtures\aetheria.provider-advertisement.json",
[string] $OutputPath = "artifacts\eveelectron-capture-contract\latest\capture-request.json",
[string] $Stamp = "smoke"
)
$ErrorActionPreference = "Stop"
$projectRoot = Split-Path -Parent $PSScriptRoot
$absoluteCapabilityPath = if ([System.IO.Path]::IsPathRooted($CapabilityManifestPath)) {
$CapabilityManifestPath
} else {
Join-Path $projectRoot $CapabilityManifestPath
}
$absoluteAdvertisementPath = if ([System.IO.Path]::IsPathRooted($AdvertisementPath)) {
$AdvertisementPath
} else {
Join-Path $projectRoot $AdvertisementPath
}
$absoluteOutputPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) {
$OutputPath
} else {
Join-Path $projectRoot $OutputPath
}
foreach ($path in @($absoluteCapabilityPath, $absoluteAdvertisementPath)) {
if (-not (Test-Path -LiteralPath $path)) {
throw "EveElectron capture contract input not found: $path"
}
}
$manifest = Get-Content -LiteralPath $absoluteCapabilityPath -Raw | ConvertFrom-Json
$captureContract = $manifest.lifecycle.capture.captureContract
if ($null -eq $captureContract) {
throw "EveElectron capture contract missing lifecycle.capture.captureContract"
}
foreach ($field in @("ownerRepo", "runtimeId", "targetId", "requestSchema", "requestBuilder", "advertisementPath", "captureKind", "artifactKind", "artifactPattern", "conformanceAttachment", "requiredProvider", "requiredSurface", "authority", "publishProof")) {
if (-not $captureContract.$field) {
throw "EveElectron capture contract missing $field"
}
}
if ($captureContract.ownerRepo -ne "EveElectron") {
throw "Unexpected EveElectron capture owner: $($captureContract.ownerRepo)"
}
if ($captureContract.runtimeId -ne "electron-shell") {
throw "Unexpected EveElectron capture runtime: $($captureContract.runtimeId)"
}
if ($captureContract.targetId -ne "electron-shell") {
throw "Unexpected EveElectron capture target: $($captureContract.targetId)"
}
if ($captureContract.requestSchema -ne "gamecult.eve.runtime_capture_request.v1") {
throw "Unexpected EveElectron capture request schema: $($captureContract.requestSchema)"
}
$builderPath = Join-Path $projectRoot $captureContract.requestBuilder
if (-not (Test-Path -LiteralPath $builderPath)) {
throw "EveElectron capture request builder not found: $($captureContract.requestBuilder)"
}
if ((Join-Path $projectRoot $captureContract.advertisementPath) -ne $absoluteAdvertisementPath) {
throw "EveElectron capture advertisement path mismatch: $($captureContract.advertisementPath) vs $AdvertisementPath"
}
node --test (Join-Path $projectRoot "tools\eveelectron\eveelectron-capture-contract.test.mjs")
if ($LASTEXITCODE -ne 0) {
throw "EveElectron capture contract tests failed with exit code $LASTEXITCODE"
}
node $builderPath --capability $absoluteCapabilityPath --advertisement $absoluteAdvertisementPath --output $absoluteOutputPath --stamp $Stamp
if ($LASTEXITCODE -ne 0) {
throw "EveElectron capture request builder failed with exit code $LASTEXITCODE"
}
if (-not (Test-Path -LiteralPath $absoluteOutputPath)) {
throw "EveElectron capture request was not written: $absoluteOutputPath"
}
$request = Get-Content -LiteralPath $absoluteOutputPath -Raw | ConvertFrom-Json
foreach ($expectation in @(
@{ field = "schema"; value = "gamecult.eve.runtime_capture_request.v1" },
@{ field = "runtimeId"; value = "electron-shell" },
@{ field = "ownerRepo"; value = "EveElectron" },
@{ field = "providerId"; value = "aetheria" },
@{ field = "surfaceId"; value = "aetheria.daemon.game" },
@{ field = "targetId"; value = "electron-shell" },
@{ field = "commandBoundary"; value = "aetheria.daemon.commands" },
@{ field = "receiptSchema"; value = "aetheria.eve_command_acceptance_status.v1" },
@{ field = "captureKind"; value = "electron-window-png" },
@{ field = "artifactKind"; value = "png" },
@{ field = "conformanceAttachment"; value = "runtime.captureArtifacts[]" }
)) {
if ($request.($expectation.field) -ne $expectation.value) {
throw "EveElectron capture request $($expectation.field) expected $($expectation.value) got $($request.($expectation.field))"
}
}
if ($request.artifactPath -ne "artifacts/eveelectron-capture/$Stamp/electron-shell.png") {
throw "Unexpected EveElectron capture artifact path: $($request.artifactPath)"
}
Write-Host "EveElectron capture contract smoke passed: $absoluteOutputPath"

View file

@ -138,13 +138,33 @@ foreach ($pathProperty in @("runnerScript", "consumerProject")) {
}
$captureContract = $manifest.lifecycle.capture.captureContract
foreach ($field in @("ownerRepo", "runtimeId", "captureKind", "artifactKind", "artifactPattern", "conformanceAttachment", "requiredProvider", "requiredSurface", "authority", "publishProof")) {
foreach ($field in @("ownerRepo", "runtimeId", "targetId", "requestSchema", "requestBuilder", "advertisementPath", "captureKind", "artifactKind", "artifactPattern", "conformanceAttachment", "requiredProvider", "requiredSurface", "authority", "publishProof")) {
if (-not $captureContract.$field) {
throw "EveElectron capture contract missing $field"
}
}
if ($captureContract.runtimeId -ne "electron-shell") {
throw "EveElectron capture contract has unexpected runtime: $($captureContract.runtimeId)"
}
if ($captureContract.targetId -ne "electron-shell") {
throw "EveElectron capture contract has unexpected target: $($captureContract.targetId)"
}
if ($captureContract.requestSchema -ne "gamecult.eve.runtime_capture_request.v1") {
throw "EveElectron capture contract has unexpected request schema: $($captureContract.requestSchema)"
}
if ($captureContract.captureKind -ne "electron-window-png") {
throw "EveElectron capture contract has unexpected capture kind: $($captureContract.captureKind)"
}
foreach ($pathProperty in @("requestBuilder", "advertisementPath")) {
$relativePath = $captureContract.$pathProperty
$absolutePath = Join-Path $projectRoot $relativePath
if (-not (Test-Path -LiteralPath $absolutePath)) {
throw "EveElectron capture contract $pathProperty does not exist: $relativePath"
}
}
& (Join-Path $projectRoot "scripts\run-eveelectron-split-handoff-smoke.ps1")
& (Join-Path $projectRoot "scripts\run-eveelectron-provider-shell-smoke.ps1")
& (Join-Path $projectRoot "scripts\run-eveelectron-capture-contract-smoke.ps1")
Write-Host "EveElectron lifecycle smoke passed: $absoluteManifestPath"

View file

@ -67,7 +67,9 @@ foreach ($evidencePath in @($worldSurfaceLoweringClaim.evidencePaths)) {
$expectedFiles = @(
"runtimes\incubating\eve-electron\package.json",
"runtimes\incubating\eve-electron\src\eve-electron-shell.mjs",
"runtimes\incubating\eve-electron\test\eve-electron-shell.test.mjs"
"runtimes\incubating\eve-electron\test\eve-electron-shell.test.mjs",
"scripts\run-eveelectron-capture-contract-smoke.ps1",
"tools\eveelectron\eveelectron-capture-contract.mjs"
)
foreach ($relativePath in $expectedFiles) {

View file

@ -69,6 +69,14 @@ foreach ($id in @("electron-shell-runtime", "electron-command-transport", "elect
}
}
$captureMoveSet = $moveSets | Where-Object { $_.id -eq "electron-capture-lifecycle" } | Select-Object -First 1
if (-not $captureMoveSet) {
throw "EveElectron split handoff missing move set: electron-capture-lifecycle"
}
if (@($captureMoveSet.currentPaths).Count -eq 0) {
throw "EveElectron capture lifecycle move set must declare currentPaths"
}
foreach ($contract in @(
"gamecult.eve.surface.v1",
"gamecult.eve.command.v1",

View file

@ -167,6 +167,10 @@ node $consumerScript $consumerExport `
--expect-runtime-lifecycle-field "electron-shell:test:testContract.runnerKind:electron-provider-shell-smoke" `
--expect-runtime-lifecycle-field "electron-shell:test:testContract.runnerScript:scripts/run-eveelectron-provider-shell-smoke.ps1" `
--expect-runtime-lifecycle-field "electron-shell:capture:captureContract.captureKind:electron-window-png" `
--expect-runtime-lifecycle-field "electron-shell:capture:captureContract.targetId:electron-shell" `
--expect-runtime-lifecycle-field "electron-shell:capture:captureContract.requestSchema:gamecult.eve.runtime_capture_request.v1" `
--expect-runtime-lifecycle-field "electron-shell:capture:captureContract.requestBuilder:tools/eveelectron/eveelectron-capture-contract.mjs" `
--expect-runtime-lifecycle-field "electron-shell:capture:captureContract.advertisementPath:web/fixtures/aetheria.provider-advertisement.json" `
--expect-runtime-lifecycle-field "electron-shell:capture:captureContract.requiredSurface:aetheria.daemon.game" `
--expect-runtime-lifecycle-status tui:release:pending-tui-package `
--expect-runtime-lifecycle-status tui:test:provider-shell-contract-skeleton `

View file

@ -64,6 +64,7 @@ try {
--expect-split-handoff-move EveElectron:electron-shell:electron-shell-runtime:current:exists:eve-electron-shell.mjs `
--expect-split-handoff-move EveElectron:electron-shell:electron-command-transport:current:exists:eve-electron-shell.mjs `
--expect-split-handoff-move EveElectron:electron-shell:electron-world-surface-lowering:current:exists:eve-electron-shell.mjs `
--expect-split-handoff-move EveElectron:electron-shell:electron-capture-lifecycle:current:exists:eveelectron-capture-contract.mjs `
--expect-split-handoff-move EveElectron:electron-shell:electron-shell-runtime:observed-provider:exists:Aetheria.Rts.Web/Electron `
--expect-split-handoff-move EveElectron:electron-shell:electron-command-transport:observed-provider:exists:Aetheria.Rts.Web/Client/app.ts `
--expect-split-handoff-move EveElectron:electron-shell:electron-world-surface-lowering:observed-provider:exists:Aetheria.Rts.Web/Client `

View file

@ -0,0 +1,122 @@
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
export function buildElectronCaptureRequest({
advertisement,
capabilityManifest,
advertisementPath = "",
capabilityManifestPath = "",
stamp = new Date().toISOString().replace(/[:.]/g, "-"),
} = {}) {
if (!advertisement || typeof advertisement !== "object") {
throw new Error("advertisement is required");
}
if (!capabilityManifest || typeof capabilityManifest !== "object") {
throw new Error("capabilityManifest is required");
}
const captureContract = capabilityManifest.lifecycle?.capture?.captureContract;
if (!captureContract) {
throw new Error("capability manifest is missing lifecycle.capture.captureContract");
}
if (advertisement.providerId !== captureContract.requiredProvider) {
throw new Error(
`capture provider mismatch: expected ${captureContract.requiredProvider} got ${advertisement.providerId || "missing"}`,
);
}
const targetId = captureContract.targetId || captureContract.runtimeId;
const surface = (advertisement.surfaces || []).find(candidate => candidate.surfaceId === captureContract.requiredSurface);
if (!surface) {
throw new Error(`capture surface not advertised: ${captureContract.requiredSurface}`);
}
const loweringTargets = surface.worldInteraction?.loweringTargets || [];
if (!loweringTargets.includes(targetId)) {
throw new Error(`surface ${surface.surfaceId} does not advertise lowering target ${targetId}`);
}
const artifactPath = replaceStamp(captureContract.artifactPattern, stamp);
return {
schema: captureContract.requestSchema || "gamecult.eve.runtime_capture_request.v1",
runtimeId: capabilityManifest.runtimeId,
ownerRepo: captureContract.ownerRepo,
providerId: advertisement.providerId,
surfaceId: surface.surfaceId,
surfaceKind: surface.surfaceKind || "",
targetId,
projectionKind: surface.worldInteraction?.projectionKind || "",
commandBoundary: surface.worldInteraction?.commandBoundary || "",
receiptSchema: surface.worldInteraction?.receiptSchema || "",
captureKind: captureContract.captureKind,
artifactKind: captureContract.artifactKind,
artifactPath,
conformanceAttachment: captureContract.conformanceAttachment,
authority: captureContract.authority,
sourceAdvertisementPath: advertisementPath,
sourceCapabilityManifestPath: capabilityManifestPath,
issuedAtUtc: stamp,
};
}
export function loadJsonFile(filePath) {
return JSON.parse(readFileSync(filePath, "utf8"));
}
export function writeCaptureRequest(request, outputPath) {
mkdirSync(path.dirname(outputPath), { recursive: true });
writeFileSync(outputPath, `${JSON.stringify(request, null, 2)}\n`, "utf8");
}
function replaceStamp(pattern, stamp) {
if (!pattern || !pattern.includes("{stamp}")) {
throw new Error(`capture artifact pattern must include {stamp}: ${pattern || "missing"}`);
}
return pattern.replace("{stamp}", stamp);
}
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", "output"]) {
if (!args[required]) {
throw new Error(`missing required --${required}`);
}
}
const stamp = args.stamp || new Date().toISOString().replace(/[:.]/g, "-");
const request = buildElectronCaptureRequest({
advertisement: loadJsonFile(args.advertisement),
capabilityManifest: loadJsonFile(args.capability),
advertisementPath: args.advertisement,
capabilityManifestPath: args.capability,
stamp,
});
writeCaptureRequest(request, args.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();
}

View file

@ -0,0 +1,40 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { buildElectronCaptureRequest } from "./eveelectron-capture-contract.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"));
test("builds Electron shell capture request from provider advertisement", () => {
const request = buildElectronCaptureRequest({
advertisement,
capabilityManifest,
advertisementPath: "web/fixtures/aetheria.provider-advertisement.json",
capabilityManifestPath: "runtimes/incubating/eve-electron/eve-runtime-capability.json",
stamp: "smoke",
});
assert.equal(request.schema, "gamecult.eve.runtime_capture_request.v1");
assert.equal(request.runtimeId, "electron-shell");
assert.equal(request.ownerRepo, "EveElectron");
assert.equal(request.providerId, "aetheria");
assert.equal(request.surfaceId, "aetheria.daemon.game");
assert.equal(request.targetId, "electron-shell");
assert.equal(request.projectionKind, "provider-authored-world-surface");
assert.equal(request.commandBoundary, "aetheria.daemon.commands");
assert.equal(request.receiptSchema, "aetheria.eve_command_acceptance_status.v1");
assert.equal(request.artifactPath, "artifacts/eveelectron-capture/smoke/electron-shell.png");
assert.match(request.authority, /provider state/);
});
test("rejects capture request when the surface does not advertise Electron shell", () => {
const withoutTarget = structuredClone(advertisement);
const surface = withoutTarget.surfaces.find(candidate => candidate.surfaceId === "aetheria.daemon.game");
surface.worldInteraction.loweringTargets = surface.worldInteraction.loweringTargets.filter(target => target !== "electron-shell");
assert.throws(
() => buildElectronCaptureRequest({ advertisement: withoutTarget, capabilityManifest, stamp: "smoke" }),
/does not advertise lowering target electron-shell/,
);
});

View file

@ -758,7 +758,10 @@
"runtimes/incubating/eve-electron/eveelectron-split-handoff.json",
"runtimes/incubating/eve-electron/eve-runtime-capability.json",
"scripts/run-eveelectron-lifecycle-smoke.ps1",
"scripts/run-eveelectron-provider-shell-smoke.ps1"
"scripts/run-eveelectron-provider-shell-smoke.ps1",
"scripts/run-eveelectron-capture-contract-smoke.ps1",
"tools/eveelectron/eveelectron-capture-contract.mjs",
"tools/eveelectron/eveelectron-capture-contract.test.mjs"
],
"expectedSourceSymbols": [
{
@ -768,6 +771,14 @@
{
"path": "runtimes/incubating/eve-electron/test/eve-electron-shell.test.mjs",
"contains": ["selects the active advertised provider surface", "lowers provider surface trees into an Electron shell projection", "rejects surface documents that do not match the advertised target", "command intents carry provider-advertised boundaries", "aetheria.daemon.commands", "aetheria.eve_command_acceptance_status.v1"]
},
{
"path": "tools/eveelectron/eveelectron-capture-contract.mjs",
"contains": ["buildElectronCaptureRequest", "gamecult.eve.runtime_capture_request.v1", "loweringTargets", "artifactPattern", "writeCaptureRequest"]
},
{
"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"]
}
],
"capture": {