Capture provider worlds in native Electron windows

This commit is contained in:
Meta Vens 2026-07-10 08:01:32 +01:00
commit e4b26c5654
8 changed files with 279 additions and 10 deletions

View file

@ -12,4 +12,10 @@ does not import provider state or plugin semantic implementations.
npm install
npm test
npm run pack:check
npm run capture:conformance
```
The capture runner uses a consumer-supplied Electron executable and emits
native offscreen-painted PNGs plus `gamecult.eve.runtime_witness.v1` documents
for the generic world fixture and Aetheria. Provider state remains input data;
the runtime owns the window, projection, pixels, and evidence.

94
capture/main.mjs Normal file
View file

@ -0,0 +1,94 @@
import { app, BrowserWindow, shell } from "electron";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { performance } from "node:perf_hooks";
import { buildCaptureDocument, writeCaptureWitness } from "../src/capture-model.mjs";
import { createEveElectronWindow } from "../src/window-host.mjs";
const options = parseArguments(process.argv.slice(2));
const tracePath = `${options.witness}.trace.log`;
mkdirSync(path.dirname(tracePath), { recursive: true });
trace("starting");
process.on("uncaughtException", error => fail(error));
process.on("unhandledRejection", error => fail(error));
app.on("ready", () => trace("ready-event"));
app.on("will-quit", () => trace("will-quit"));
void app.whenReady().then(runCapture, fail);
async function runCapture() {
trace("electron-ready");
const started = performance.now();
let window;
try {
const advertisement = parseJson(readFileSync(options.advertisement, "utf8"));
const surface = parseJson(readFileSync(options.surface, "utf8"));
const { projection, html } = buildCaptureDocument(advertisement, surface, options.surfaceId);
trace("projection-built");
mkdirSync(path.dirname(options.output), { recursive: true });
window = createEveElectronWindow({
width: 1280,
height: 800,
show: false,
frame: true,
title: `EveElectron ${projection.surfaceId}`,
webPreferences: { offscreen: true },
}, { BrowserWindow, shell });
const imagePromise = nextPaint(window, 10000);
await window.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
trace("window-loaded");
await window.webContents.executeJavaScript("document.fonts.ready.then(() => true)");
window.webContents.invalidate();
const image = await imagePromise;
trace("page-captured");
if (image.isEmpty()) throw new Error("Electron capture produced an empty native image.");
const png = image.toPNG();
if (png.length < 1024) throw new Error(`Electron capture is suspiciously small: ${png.length} bytes.`);
writeFileSync(options.output, png);
const size = image.getSize();
writeCaptureWitness({
outputPath: options.witness,
imagePath: options.output,
imageSize: size,
durationMs: Math.round((performance.now() - started) * 1000) / 1000,
advertisement,
surface,
projection,
});
console.log(options.witness);
trace("complete");
} finally {
window?.destroy();
app.exit(0);
}
}
function parseArguments(args) {
const values = {};
for (let index = 0; index < args.length; index += 2) values[args[index].slice(2)] = path.resolve(args[index + 1]);
for (const field of ["advertisement", "surface", "surface-id", "output", "witness"]) {
if (!values[field]) throw new Error(`Missing --${field}.`);
}
return { ...values, surfaceId: args[args.indexOf("--surface-id") + 1] };
}
function parseJson(value) { return JSON.parse(value.replace(/^\uFEFF/, "")); }
function trace(value) { writeFileSync(tracePath, `${new Date().toISOString()} ${value}\n`, { encoding: "utf8", flag: "a" }); }
function nextPaint(window, timeoutMs) {
return new Promise((resolve, reject) => {
const onPaint = (_event, _dirty, image) => {
if (image.isEmpty() || image.toPNG().length < 1024) return;
clearTimeout(timer);
window.webContents.off("paint", onPaint);
resolve(image);
};
const timer = setTimeout(() => {
window.webContents.off("paint", onPaint);
reject(new Error(`Electron offscreen paint timed out after ${timeoutMs}ms.`));
}, timeoutMs);
window.webContents.on("paint", onPaint);
});
}
function fail(error) {
trace(`failed ${error instanceof Error ? error.stack || error.message : String(error)}`);
app.exit(1);
}

6
capture/package.json Normal file
View file

@ -0,0 +1,6 @@
{
"name": "eve-electron-capture-host",
"private": true,
"type": "module",
"main": "main.mjs"
}

View file

@ -99,33 +99,57 @@
},
"capture": {
"ownerRepo": "EveElectron",
"status": "pending-owner-window-capture",
"status": "owner-window-captures",
"captureContract": {
"ownerRepo": "EveElectron",
"runtimeId": "electron-shell",
"targetId": "electron-shell",
"requestSchema": "gamecult.eve.runtime_capture_request.v1",
"requestBuilder": "pending",
"requestBuilder": "scripts/capture-conformance.ps1",
"advertisementPath": "provider-supplied",
"captureKind": "electron-window-png",
"artifactKind": "png",
"artifactPattern": "artifacts/capture/{providerId}/{surfaceId}.png",
"conformanceAttachment": "runtime.captureArtifacts[]",
"requiredProvider": "aetheria",
"requiredSurface": "aetheria.game",
"requiredSurface": "aetheria.daemon.game",
"additionalProviderSurfaces": [
{ "providerId": "eve.world-smoke", "surfaceId": "eve.world-smoke.surface", "purpose": "Non-product control." }
],
"authority": "Capture observes Electron projection only; provider state and receipts remain provider-owned.",
"publishProof": "Owner-repo Electron PNGs attach to Eve conformance."
},
"evidencePaths": [],
"pendingProofs": ["Capture one non-product and one Aetheria window from the owner runtime."]
"evidencePaths": [
"../EveElectron/scripts/capture-conformance.ps1",
"../EveElectron/capture/main.mjs",
"../EveElectron/artifacts/capture/generic-world/window.png",
"../EveElectron/artifacts/capture/aetheria-world/window.png"
],
"pendingProofs": []
}
},
"capture": {
"status": "pending-owner-window-capture",
"note": "Aetheria live lowering passes through the owner package; portable owner-repo PNG capture remains pending.",
"artifacts": []
"status": "owner-window-png",
"note": "EveElectron produces native offscreen-painted window PNGs and runtime witnesses for a generic provider and Aetheria.",
"artifacts": [
{
"kind": "png",
"schema": "image/png",
"path": "../EveElectron/artifacts/capture/generic-world/window.png",
"providerId": "eve.world-smoke",
"surfaceId": "eve.world-smoke.surface",
"sourceSurfacePath": "../Eve/web/fixtures/eve-world-smoke-surface.json",
"requestPath": "../EveElectron/artifacts/capture/generic-world/runtime-witness.json"
},
{
"kind": "png",
"schema": "image/png",
"path": "../EveElectron/artifacts/capture/aetheria-world/window.png",
"providerId": "aetheria",
"surfaceId": "aetheria.daemon.game",
"sourceSurfacePath": "../Eve/web/fixtures/aetheria-world-surface.json",
"requestPath": "../EveElectron/artifacts/capture/aetheria-world/runtime-witness.json"
}
]
}
}

View file

@ -13,10 +13,11 @@
},
"./preload": "./src/eve-provider-preload.cjs"
},
"files": ["src", "eve-runtime-capability.json", "README.md", "LICENSE"],
"files": ["src", "capture", "scripts", "eve-runtime-capability.json", "README.md", "LICENSE"],
"scripts": {
"test": "node --test test/*.test.mjs",
"pack:check": "npm pack --dry-run"
"pack:check": "npm pack --dry-run",
"capture:conformance": "powershell -ExecutionPolicy Bypass -File scripts/capture-conformance.ps1"
},
"peerDependencies": {
"electron": ">=30"

View file

@ -0,0 +1,50 @@
param(
[string] $EveRoot = "E:\Projects\Eve",
[string] $ElectronPath = "E:\Projects\Aetheria\Aetheria.Rts.Web\node_modules\electron\dist\electron.exe",
[string] $OutputRoot = "artifacts\capture"
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$output = if ([IO.Path]::IsPathRooted($OutputRoot)) { $OutputRoot } else { Join-Path $repoRoot $OutputRoot }
$captureApp = Join-Path $repoRoot "capture"
if (-not (Test-Path -LiteralPath $ElectronPath)) { throw "Electron executable not found: $ElectronPath" }
$cases = @(
@{
id = "generic-world"
advertisement = Join-Path $EveRoot "web\fixtures\eve-world-smoke.provider-advertisement.json"
surface = Join-Path $EveRoot "web\fixtures\eve-world-smoke-surface.json"
surfaceId = "eve.world-smoke.surface"
},
@{
id = "aetheria-world"
advertisement = Join-Path $EveRoot "web\fixtures\aetheria.provider-advertisement.json"
surface = Join-Path $EveRoot "web\fixtures\aetheria-world-surface.json"
surfaceId = "aetheria.daemon.game"
}
)
foreach ($case in $cases) {
$caseRoot = Join-Path $output $case.id
New-Item -ItemType Directory -Force -Path $caseRoot | Out-Null
$stdout = Join-Path $caseRoot "capture.stdout.log"
$stderr = Join-Path $caseRoot "capture.stderr.log"
$arguments = @(
$captureApp,
"--advertisement", $case.advertisement,
"--surface", $case.surface,
"--surface-id", $case.surfaceId,
"--output", (Join-Path $caseRoot "window.png"),
"--witness", (Join-Path $caseRoot "runtime-witness.json")
)
$process = Start-Process -FilePath $ElectronPath -ArgumentList $arguments -WorkingDirectory $repoRoot `
-WindowStyle Hidden -RedirectStandardOutput $stdout -RedirectStandardError $stderr -Wait -PassThru
if ($process.ExitCode -ne 0) {
$diagnosticText = Get-Content -LiteralPath $stderr -Raw -ErrorAction SilentlyContinue
$diagnostic = if ($diagnosticText) { $diagnosticText.Trim() } else { "See runtime-witness.json.trace.log" }
throw "EveElectron capture failed for $($case.id) with exit code $($process.ExitCode): $diagnostic"
}
}
Write-Host "EveElectron captures: $output"

72
src/capture-model.mjs Normal file
View file

@ -0,0 +1,72 @@
import { createHash } from "node:crypto";
import { readFileSync, statSync, writeFileSync } from "node:fs";
import path from "node:path";
import { EveElectronShell } from "./eve-electron-shell.mjs";
export function buildCaptureDocument(advertisement, surface, requestedSurfaceId) {
const shell = new EveElectronShell({ clientId: "eve-electron-capture" });
const projection = shell.lowerSurface(surface, advertisement, requestedSurfaceId);
return { projection, html: renderProjectionHtml(projection) };
}
export function renderProjectionHtml(projection) {
const title = escapeHtml(`${projection.providerId} / ${projection.surfaceId}`);
return `<!doctype html>
<html><head><meta charset="utf-8"><style>
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;font-family:Segoe UI,sans-serif;background:#101214;color:#f4f5ef}
body{display:grid;grid-template-rows:54px 1fr}.bar{display:flex;align-items:center;justify-content:space-between;padding:0 22px;background:#191d20;border-bottom:1px solid #394047}
.brand{font-weight:700}.route{font:12px Consolas,monospace;color:#9fe3ca}.surface{position:relative;overflow:hidden;padding:22px;background:#111619}
.node{border:1px solid #46515a;background:#1b2226;padding:12px;margin:8px;min-height:44px}.world-projection-node{position:relative;min-height:420px;background:#172126;border-color:#57a58a}
.world-projection-node:before{content:"INTERACTIVE WORLD SURFACE";display:block;font:12px Consolas,monospace;color:#79d9b8;margin-bottom:16px}
.world-projection-node:after{content:"";position:absolute;inset:58px 24px 24px;background:linear-gradient(90deg,transparent 49%,#334149 50%,transparent 51%),linear-gradient(transparent 49%,#334149 50%,transparent 51%);background-size:48px 48px;border:1px solid #334149}
.command-control{border-color:#d1aa58}.plugin-placeholder,.norn-graph-shell,.tex-math-shell,.sai-vn-stage-shell{border-color:#79a9d9}.kind{font:11px Consolas,monospace;color:#aeb9c0}.id{font-weight:600;margin-top:5px}.children{margin-left:12px}
</style></head><body><header class="bar"><span class="brand">EveElectron</span><span class="route">${title}</span></header><main class="surface">${renderNode(projection.root)}</main></body></html>`;
}
export function writeCaptureWitness({ outputPath, imagePath, imageSize, durationMs, advertisement, surface, projection }) {
const bytes = readFileSync(imagePath);
const sha256 = createHash("sha256").update(bytes).digest("hex");
const witness = {
schema: "gamecult.eve.runtime_witness.v1",
witnessId: `eveelectron.${projection.providerId}.${projection.surfaceId}`,
runtimeId: "electron-shell",
runtimeOwnerRepo: "EveElectron",
providerId: projection.providerId,
surfaceId: projection.surfaceId,
projectionKind: projection.projectionKind || "electron-shell",
status: "pass",
generatedAtUtc: new Date().toISOString(),
execution: { cacheState: "cold", durationMs, testDurationMs: durationMs },
assertions: {
providerAdvertisement: advertisement?.schema === "gamecult.eve.provider_advertisement.v1",
surfaceMatched: surface?.surface?.id === projection.surfaceId,
nativeWindowCapture: true,
nonblankPixels: true,
sourceVersion: Number(surface?.version || 0),
},
screenshotMetrics: { width: imageSize.width, height: imageSize.height, encodedSizeBytes: bytes.length, sha256 },
artifacts: [{
kind: "electron-window-png",
path: path.relative(path.dirname(outputPath), imagePath).replaceAll("\\", "/"),
sha256,
sizeBytes: statSync(imagePath).size,
width: imageSize.width,
height: imageSize.height,
}],
authority: "eveelectron-owns-window-and-lowering-provider-owns-surface-state-and-command-acceptance",
};
writeFileSync(outputPath, `${JSON.stringify(witness, null, 2)}\n`, "utf8");
return witness;
}
function renderNode(node) {
const kind = escapeHtml(node.shellElementKind || "shell-node");
const component = escapeHtml(node.componentKind || "unknown");
const id = escapeHtml(node.id || "unnamed");
const children = (node.children || []).map(renderNode).join("");
return `<section class="node ${kind}"><div class="kind">${component} / ${kind}</div><div class="id">${id}</div>${children ? `<div class="children">${children}</div>` : ""}</section>`;
}
function escapeHtml(value) {
return String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
}

View file

@ -0,0 +1,16 @@
import assert from "node:assert/strict";
import test from "node:test";
import { buildCaptureDocument } from "../src/capture-model.mjs";
test("builds an owner-rendered capture document from a generic provider", () => {
const advertisement = {
schema: "gamecult.eve.provider_advertisement.v1",
providerId: "fixture",
surfaces: [{ surfaceId: "fixture.world", worldInteraction: { projectionKind: "provider-authored-world-surface" } }],
};
const surface = { schema: "gamecult.eve.surface.v1", surface: { id: "fixture.world", root: { id: "root", kind: "world.scene3d", children: [] } } };
const result = buildCaptureDocument(advertisement, surface, "fixture.world");
assert.equal(result.projection.providerId, "fixture");
assert.match(result.html, /INTERACTIVE WORLD SURFACE/);
assert.match(result.html, /EveElectron/);
});