Add Unity UI Toolkit projection capture evidence

This commit is contained in:
Meta Vens 2026-07-09 15:46:55 +01:00
commit 08da3eddd2
13 changed files with 686 additions and 8 deletions

View file

@ -575,6 +575,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.
- Unity UI Toolkit now also has schema-backed semantic projection evidence.
`tools/eveunity/eveunity-uitoolkit-capture-artifact.mjs` lowers
`web/fixtures/aetheria-world-surface.json` into
`gamecult.eve.unity_uitoolkit_projection.v1`, writes
`artifacts/eveunity-uitoolkit-capture/latest/unity-uitoolkit-projection.json`,
and exports it as `runtime.captureArtifacts[]`. The builder also proves Sai,
Norn, and TeX stay sidecar plugin projections when a visual-novel surface is
lowered through UI Toolkit. The remaining EveUnity capture blocker is an
owner-repo Unity editor or batchmode PNG, not absence of a typed projection
artifact.
- Unity scene capture now has the same semantic artifact boundary as Electron
without pretending it is a rendered Unity frame. `tools/eveunity/eveunity-scene-capture-artifact.mjs`
lowers `web/fixtures/aetheria-world-surface.json` into
@ -853,6 +863,14 @@ Recently cut:
`scripts/run-split-target-conformance-consumer-smoke.ps1`, and
`scripts/run-aetheria-conformance-consumer-smoke.ps1` assert that boundary
from the manifest/export.
- Unity UI Toolkit now attaches a schema-backed semantic projection capture
artifact at
`artifacts/eveunity-uitoolkit-capture/latest/unity-uitoolkit-projection.json`.
Parity validates it against
`gamecult.eve.unity_uitoolkit_projection.v1`, exports it as
`runtime.captureArtifacts[]`, and the runtime-owner consumer smoke asserts it.
This proves the UI Toolkit command-surface projection is typed evidence while
leaving real Unity editor or batchmode PNG capture as the EveUnity blocker.
- Unity UI Toolkit now has a first-party `norn.graph` projection-adapter proof
for `embed.norn`. `NornGraphUiToolkitProjectionAdapter` owns the native
embedded graph shell and graph command emission path, while the Norn sidecar

View file

@ -400,7 +400,13 @@ exist. The current lifecycle proof is deliberately split:
- capture: `tools/eveunity/eveunity-capture-contract.mjs` builds a typed
`gamecult.eve.runtime_capture_request.v1` request from the runtime capability
manifest and Aetheria provider advertisement; Unity editor or batchmode PNG
capture remains a split blocker.
capture remains a split blocker. `tools/eveunity/eveunity-uitoolkit-capture-artifact.mjs`
also writes a schema-backed
`gamecult.eve.unity_uitoolkit_projection.v1` JSON projection artifact at
`artifacts/eveunity-uitoolkit-capture/latest/unity-uitoolkit-projection.json`.
That artifact proves provider-advertised UI Toolkit projection, embedded
slots, and sidecar plugin projection metadata without pretending to be a
rendered Unity frame.
TUI capture evidence is already typed even though EveTui remains incubating.
`tools/evetui/evetui-capture-artifact.mjs` lowers the Aetheria world surface

View file

@ -66,6 +66,10 @@ construction through `tools/eveunity/eveunity-capture-contract.mjs`. That
request is derived from the runtime capability manifest and Aetheria's provider
advertisement, then points at the PNG artifact EveUnity must produce later. It
does not fake a screenshot; it makes the capture input contract executable.
`tools/eveunity/eveunity-uitoolkit-capture-artifact.mjs` also emits a typed
`gamecult.eve.unity_uitoolkit_projection.v1` JSON projection artifact for the
Aetheria world surface. That artifact is runtime projection evidence, not
provider state and not a Unity frame capture.
The manifest also declares `worldSurfaceLowering` target `unity-uitoolkit`.
That means this package can lower provider-advertised `interactive-world` and
@ -100,6 +104,12 @@ graduate to `EveUnity`. The capture contract smoke is:
powershell -ExecutionPolicy Bypass -File .\scripts\run-eveunity-capture-contract-smoke.ps1
```
The semantic projection capture smoke is:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\run-eveunity-uitoolkit-capture-smoke.ps1
```
The release contract smoke is:
```powershell

View file

@ -147,6 +147,9 @@
"tools/eveunity/eveunity-capture-contract.mjs",
"tools/eveunity/eveunity-capture-contract.test.mjs",
"scripts/run-eveunity-capture-contract-smoke.ps1",
"tools/eveunity/eveunity-uitoolkit-capture-artifact.mjs",
"tools/eveunity/eveunity-uitoolkit-capture-artifact.test.mjs",
"scripts/run-eveunity-uitoolkit-capture-smoke.ps1",
"web/fixtures/aetheria.provider-advertisement.json"
],
"pendingProofs": [
@ -155,7 +158,19 @@
}
},
"capture": {
"status": "semantic",
"note": "Unity UI Toolkit lowers embedded document slots through a resolver-backed child surface host."
"status": "json-projection",
"note": "Unity UI Toolkit attaches a provider-advertisement-backed gamecult.eve.unity_uitoolkit_projection.v1 JSON projection artifact; Unity editor or batchmode PNG capture remains an EveUnity split blocker.",
"artifacts": [
{
"kind": "json-projection",
"schema": "gamecult.eve.unity_uitoolkit_projection.v1",
"path": "artifacts/eveunity-uitoolkit-capture/latest/unity-uitoolkit-projection.json",
"runtimeId": "unity-uitoolkit",
"providerId": "aetheria",
"surfaceId": "aetheria.daemon.game",
"sourceSurfacePath": "web/fixtures/aetheria-world-surface.json",
"requestPath": "artifacts/eveunity-uitoolkit-capture/latest/capture-request.json"
}
]
}
}

View file

@ -43,7 +43,10 @@
"currentPaths": [
"tools/eveunity/eveunity-capture-contract.mjs",
"tools/eveunity/eveunity-capture-contract.test.mjs",
"scripts/run-eveunity-capture-contract-smoke.ps1"
"scripts/run-eveunity-capture-contract-smoke.ps1",
"tools/eveunity/eveunity-uitoolkit-capture-artifact.mjs",
"tools/eveunity/eveunity-uitoolkit-capture-artifact.test.mjs",
"scripts/run-eveunity-uitoolkit-capture-smoke.ps1"
],
"destinationOwner": "EveUnity",
"replacementProof": "Unity batchmode or editor capture artifact attached to the runtime conformance report."

View file

@ -0,0 +1,134 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "gamecult.eve.unity_uitoolkit_projection.v1",
"title": "Eve Unity UI Toolkit Projection",
"type": "object",
"required": [
"type",
"schema",
"runtimeId",
"providerId",
"surfaceId",
"projectionKind",
"commandBoundary",
"receiptSchema",
"ownership",
"root"
],
"properties": {
"type": {
"const": "unity-uitoolkit-projection"
},
"schema": {
"const": "gamecult.eve.unity_uitoolkit_projection.v1"
},
"runtimeId": {
"const": "unity-uitoolkit"
},
"providerId": {
"type": "string",
"minLength": 1
},
"surfaceId": {
"type": "string",
"minLength": 1
},
"projectionKind": {
"type": "string",
"minLength": 1
},
"commandBoundary": {
"type": "string",
"minLength": 1
},
"receiptSchema": {
"type": "string",
"minLength": 1
},
"ownership": {
"type": "string",
"minLength": 1
},
"root": {
"type": "object",
"required": [
"id",
"componentKind",
"visualElementKind",
"classNames",
"stateBindingCount",
"embeddedDocumentCount",
"children"
],
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"componentKind": {
"type": "string"
},
"visualElementKind": {
"type": "string",
"minLength": 1
},
"classNames": {
"type": "array",
"items": {
"type": "string"
}
},
"props": {
"type": "object"
},
"layout": {
"type": "object"
},
"style": {
"type": "object"
},
"stateBindingCount": {
"type": "number"
},
"embeddedDocumentCount": {
"type": "number"
},
"embeddedDocuments": {
"type": "array",
"items": {
"type": "object",
"required": [
"slotId",
"documentId"
],
"properties": {
"slotId": {
"type": "string",
"minLength": 1
},
"documentId": {
"type": "string",
"minLength": 1
},
"schemaId": {
"type": "string"
},
"presentationKind": {
"type": "string"
}
},
"additionalProperties": true
}
},
"pluginProjection": {
"type": "object"
},
"children": {
"type": "array"
}
},
"additionalProperties": true
}
},
"additionalProperties": true
}

View file

@ -216,6 +216,11 @@ if ($LASTEXITCODE -ne 0) {
throw "EveUnity capture contract smoke failed with exit code $LASTEXITCODE"
}
& (Join-Path $projectRoot "scripts\run-eveunity-uitoolkit-capture-smoke.ps1")
if ($LASTEXITCODE -ne 0) {
throw "EveUnity UI Toolkit capture smoke failed with exit code $LASTEXITCODE"
}
& (Join-Path $projectRoot "scripts\run-aetheria-unity-package-smoke.ps1")
if ($LASTEXITCODE -ne 0) {
throw "Aetheria Unity package smoke failed with exit code $LASTEXITCODE"

View file

@ -0,0 +1,76 @@
param(
[string] $CapabilityManifestPath = "packages\org.gamecult.eve.unity-uitoolkit\eve-runtime-capability.json",
[string] $AdvertisementPath = "web\fixtures\aetheria.provider-advertisement.json",
[string] $SurfacePath = "web\fixtures\aetheria-world-surface.json",
[string] $OutputPath = "artifacts\eveunity-uitoolkit-capture\latest\unity-uitoolkit-projection.json",
[string] $RequestOutputPath = "artifacts\eveunity-uitoolkit-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 "EveUnity UI Toolkit capture input not found: $path"
}
}
node --test (Join-Path $projectRoot "tools\eveunity\eveunity-uitoolkit-capture-artifact.test.mjs")
if ($LASTEXITCODE -ne 0) {
throw "EveUnity UI Toolkit capture artifact tests failed with exit code $LASTEXITCODE"
}
node (Join-Path $projectRoot "tools\eveunity\eveunity-uitoolkit-capture-artifact.mjs") `
--capability $absoluteCapabilityPath `
--advertisement $absoluteAdvertisementPath `
--surface $absoluteSurfacePath `
--output $absoluteOutputPath `
--request-output $absoluteRequestOutputPath `
--stamp $Stamp
if ($LASTEXITCODE -ne 0) {
throw "EveUnity UI Toolkit capture artifact builder failed with exit code $LASTEXITCODE"
}
foreach ($path in @($absoluteOutputPath, $absoluteRequestOutputPath)) {
if (-not (Test-Path -LiteralPath $path)) {
throw "EveUnity UI Toolkit capture artifact output missing: $path"
}
}
$projection = Get-Content -LiteralPath $absoluteOutputPath -Raw | ConvertFrom-Json
foreach ($expectation in @(
@{ field = "schema"; value = "gamecult.eve.unity_uitoolkit_projection.v1" },
@{ field = "runtimeId"; value = "unity-uitoolkit" },
@{ 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 = "unity-uitoolkit-projection-json" }
)) {
if ($projection.($expectation.field) -ne $expectation.value) {
throw "EveUnity UI Toolkit projection $($expectation.field) expected $($expectation.value) got $($projection.($expectation.field))"
}
}
if (-not $projection.root.id) {
throw "EveUnity UI Toolkit projection capture must contain a root UI node"
}
if (-not (@($projection.root.classNames) -contains "eve-component")) {
throw "EveUnity UI Toolkit projection capture root missing eve-component class"
}
Write-Host "EveUnity UI Toolkit capture smoke passed: $absoluteOutputPath"

View file

@ -20,6 +20,7 @@ try {
-OutputPath "artifacts\web-reference-layout-probe\latest\sai-vn.json" `
-Port 8893 `
-SkipBuild
& (Join-Path $PSScriptRoot "run-eveunity-uitoolkit-capture-smoke.ps1")
& (Join-Path $PSScriptRoot "run-evetui-capture-smoke.ps1")
node .\tools\parity\run-parity.mjs
& (Join-Path $PSScriptRoot "run-eveconformance-handoff-smoke.ps1")

View file

@ -46,6 +46,7 @@ node $consumerScript $consumerExport `
--expect-schema gamecult.eve.runtime_lifecycle.v1 `
--expect-schema gamecult.eve.runtime_split_handoff.v1 `
--expect-schema gamecult.eve.electron_shell_projection.v1 `
--expect-schema gamecult.eve.unity_uitoolkit_projection.v1 `
--expect-schema gamecult.eve.unity_scene_projection.v1 `
--expect-schema gamecult.eve.tui_grid.v1 `
--expect-schema gamecult.eve.web_layout_probe.v1 `
@ -132,7 +133,8 @@ node $consumerScript $consumerExport `
--expect-runtime-capture-status windows-flutter:golden `
--expect-runtime-capture-status linux-flutter:ssh-golden `
--expect-runtime-capture-status android-flutter:adb-png `
--expect-runtime-capture-status unity-uitoolkit:semantic `
--expect-runtime-capture-status unity-uitoolkit:json-projection `
--expect-runtime-capture-artifact unity-uitoolkit:json-projection:gamecult.eve.unity_uitoolkit_projection.v1:aetheria:aetheria.daemon.game `
--expect-runtime-capture-status unity-scene:json-projection `
--expect-runtime-capture-artifact unity-scene:json-projection:gamecult.eve.unity_scene_projection.v1:aetheria:aetheria.daemon.game `
--expect-runtime-capture-status electron-shell:json-projection `

View file

@ -0,0 +1,321 @@
import { mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { buildUnityCaptureRequest, loadJsonFile, writeCaptureRequest } from "./eveunity-capture-contract.mjs";
export function buildUnityUiToolkitCaptureArtifact({
advertisement,
capabilityManifest,
surfaceDocument,
advertisementPath = "",
capabilityManifestPath = "",
stamp = "latest",
} = {}) {
const request = buildUnityCaptureRequest({
advertisement,
capabilityManifest,
advertisementPath,
capabilityManifestPath,
stamp,
});
const projection = buildUnityUiToolkitProjection(surfaceDocument, advertisement, request.surfaceId);
validateProjectionArtifact(projection, request);
return {
request,
projection: {
...projection,
artifactKind: "json-projection",
captureKind: "unity-uitoolkit-projection-json",
sourceAdvertisementPath: request.sourceAdvertisementPath,
sourceCapabilityManifestPath: request.sourceCapabilityManifestPath,
issuedAtUtc: request.issuedAtUtc,
},
};
}
export function buildUnityUiToolkitProjection(surfaceDocument, providerAdvertisement, requestedSurfaceId = "") {
const selected = selectSurface(providerAdvertisement, requestedSurfaceId);
const document = normalizeSurfaceDocument(surfaceDocument);
if (document.surfaceId !== selected.surfaceId) {
throw new Error(`Surface document ${document.surfaceId} does not match advertised Unity UI Toolkit surface ${selected.surfaceId}.`);
}
return {
type: "unity-uitoolkit-projection",
schema: "gamecult.eve.unity_uitoolkit_projection.v1",
runtimeId: "unity-uitoolkit",
providerId: selected.providerId,
surfaceId: selected.surfaceId,
projectionKind: selected.worldInteraction.projectionKind,
commandBoundary: selected.worldInteraction.commandBoundary,
receiptSchema: selected.worldInteraction.receiptSchema,
ownership: selected.worldInteraction.ownership,
root: buildUiNode(document.root),
};
}
export function writeUnityUiToolkitCaptureArtifact({ 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 selectSurface(providerAdvertisement, requestedSurfaceId = "") {
const provider = normalizeProviderAdvertisement(providerAdvertisement);
const surface = requestedSurfaceId
? provider.surfaces.find(candidate => candidate.surfaceId === requestedSurfaceId)
: provider.surfaces[0];
if (!surface) {
throw new Error(`Provider ${provider.providerId} does not advertise surface ${requestedSurfaceId || "(first)"}`);
}
return {
providerId: provider.providerId,
surfaceId: surface.surfaceId,
worldInteraction: normalizeWorldInteraction(surface.worldInteraction),
};
}
function normalizeProviderAdvertisement(providerAdvertisement) {
if (!providerAdvertisement || typeof providerAdvertisement !== "object") {
throw new Error("Provider advertisement is required.");
}
const providerId = firstString(providerAdvertisement.providerId);
if (!providerId) throw new Error("Provider advertisement missing providerId.");
const surfaces = Array.isArray(providerAdvertisement.surfaces)
? providerAdvertisement.surfaces
.filter(surface => surface && typeof surface === "object" && surface.surfaceId)
.map(surface => ({
...surface,
surfaceId: String(surface.surfaceId),
}))
: [];
if (!surfaces.length) throw new Error(`Provider ${providerId} advertises no surfaces.`);
return { ...providerAdvertisement, providerId, surfaces };
}
function normalizeSurfaceDocument(surfaceDocument) {
if (!surfaceDocument || typeof surfaceDocument !== "object") {
throw new Error("Surface document is required.");
}
if (surfaceDocument.schema !== "gamecult.eve.surface.v1") {
throw new Error(`Unexpected surface schema: ${surfaceDocument.schema || ""}`);
}
const surface = objectValue(surfaceDocument.surface);
const surfaceId = firstString(surface.id);
if (!surfaceId) throw new Error("Surface document missing surface.id.");
const root = objectValue(surface.root);
if (!root.id) throw new Error(`Surface document ${surfaceId} missing surface.root.`);
return { ...surfaceDocument, surfaceId, root };
}
function buildUiNode(component) {
const source = objectValue(component);
const children = Array.isArray(source.children) ? source.children.map(buildUiNode) : [];
const embeddedDocuments = normalizeEmbeddedDocuments(source.embeddedDocuments);
const componentKind = firstString(source.kind);
const pluginProjection = buildPluginProjection(componentKind, source);
const classNames = [
"eve-component",
`eve-kind-${safeClass(componentKind)}`,
...pluginClassNames(pluginProjection),
];
return {
id: firstString(source.id),
componentKind,
visualElementKind: visualElementKind(componentKind),
classNames,
props: objectValue(source.props),
layout: objectValue(source.layout),
style: objectValue(source.style),
stateBindingCount: Array.isArray(source.stateBindings) ? source.stateBindings.length : 0,
embeddedDocumentCount: embeddedDocuments.length,
embeddedDocuments,
...(pluginProjection ? { pluginProjection } : {}),
children,
};
}
function normalizeEmbeddedDocuments(value) {
return Array.isArray(value)
? value
.filter(document => document && typeof document === "object")
.map(document => ({
slotId: firstString(document.slotId, document.id),
documentId: firstString(document.documentId, document.href, document.url),
schemaId: firstString(document.schemaId, document.schema),
presentationKind: firstString(document.presentationKind, document.kind),
}))
.filter(document => document.slotId || document.documentId)
: [];
}
function visualElementKind(componentKind) {
if (!componentKind) return "VisualElement";
if (componentKind === "text" || componentKind === "label" || componentKind.startsWith("text.")) return "Label";
if (componentKind === "control.button" || componentKind === "control.popup") return "Button";
if (componentKind === "control.text" || componentKind === "control.input.text") return "TextField";
if (componentKind === "vn.stage") return "SaiVisualNovelStageElement";
if (componentKind === "panel.dialogue" || componentKind === "text.dialogue") return "SaiVisualNovelDialogueElement";
if (componentKind === "rail.actions") return "SaiVisualNovelActionRail";
if (componentKind === "embed.norn") return "NornGraphElement";
if (componentKind === "embed.tex") return "TeXMathElement";
return "VisualElement";
}
function pluginClassNames(pluginProjection) {
if (!pluginProjection) return [];
return [
"eve-plugin-projection",
`eve-plugin-${safeClass(pluginProjection.pluginId)}`,
safeClass(pluginProjection.projectionKind),
].filter(Boolean);
}
function buildPluginProjection(componentKind, component) {
const props = objectValue(component.props);
if (componentKind === "vn.stage") {
return {
pluginId: "sai.vn",
projectionKind: "sai-vn-stage",
abiSchema: "gamecult.eve.plugin_abi.v1",
commandBoundary: "sidecar-advertised-plugin-abi",
capabilities: ["vn.stage", "story.choose", "story.continue", "story.jump"],
command: "story.choose",
documentId: firstString(props.storyId, component.id),
semanticOwner: "Sai",
};
}
if (componentKind === "embed.norn") {
return {
pluginId: "norn.graph",
projectionKind: "norn-graph",
abiSchema: "gamecult.eve.plugin_abi.v1",
commandBoundary: "sidecar-advertised-plugin-abi",
capabilities: ["embed.norn"],
command: "graph.focus",
documentId: firstString(props.sourceUri, props.documentId, component.id),
semanticOwner: "Norn",
};
}
if (componentKind === "embed.tex") {
return {
pluginId: "tex.math",
projectionKind: texProjectionKind(props),
abiSchema: "gamecult.eve.plugin_abi.v1",
commandBoundary: "sidecar-advertised-plugin-abi",
capabilities: ["embed.tex", "tex.inline", "tex.block"],
command: "",
documentId: firstString(props.sourceUri, props.source, component.id),
semanticOwner: "EvePlugins",
};
}
return null;
}
function texProjectionKind(props) {
const display = firstString(props.display, "inline");
return display === "block" ? "tex-math-block" : "tex-math-inline";
}
function normalizeWorldInteraction(value) {
const source = objectValue(value);
return {
projectionKind: firstString(source.projectionKind),
commandBoundary: firstString(source.commandBoundary),
receiptSchema: firstString(source.receiptSchema),
ownership: firstString(source.ownership),
};
}
function validateProjectionArtifact(projection, request) {
if (projection.schema !== "gamecult.eve.unity_uitoolkit_projection.v1") {
throw new Error(`Unexpected Unity UI Toolkit projection schema: ${projection.schema || ""}`);
}
for (const [field, expected] of [
["runtimeId", request.runtimeId],
["providerId", request.providerId],
["surfaceId", request.surfaceId],
["projectionKind", request.projectionKind],
["commandBoundary", request.commandBoundary],
["receiptSchema", request.receiptSchema],
]) {
if ((projection[field] || "") !== (expected || "")) {
throw new Error(`Unity UI Toolkit projection ${field} expected ${expected || ""} got ${projection[field] || ""}`);
}
}
if (!projection.root || !projection.root.id) {
throw new Error("Unity UI Toolkit projection artifact must contain a root UI node");
}
}
function safeClass(value) {
const text = firstString(value, "unknown");
return text.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "unknown";
}
function objectValue(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
}
function firstString(...values) {
for (const value of values) {
if (typeof value === "string" && value.trim()) return value;
if (typeof value === "number" && Number.isFinite(value)) return String(value);
}
return "";
}
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 = buildUnityUiToolkitCaptureArtifact({
advertisement: loadJsonFile(args.advertisement),
capabilityManifest: loadJsonFile(args.capability),
surfaceDocument: loadJsonFile(args.surface),
advertisementPath: args.advertisement,
capabilityManifestPath: args.capability,
stamp,
});
writeUnityUiToolkitCaptureArtifact(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();
}

View file

@ -0,0 +1,60 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { buildUnityUiToolkitCaptureArtifact, buildUnityUiToolkitProjection } from "./eveunity-uitoolkit-capture-artifact.mjs";
const aetheriaAdvertisement = JSON.parse(readFileSync(new URL("../../web/fixtures/aetheria.provider-advertisement.json", import.meta.url), "utf8"));
const capabilityManifest = JSON.parse(readFileSync(new URL("../../packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json", import.meta.url), "utf8"));
const aetheriaSurfaceDocument = JSON.parse(readFileSync(new URL("../../web/fixtures/aetheria-world-surface.json", import.meta.url), "utf8"));
const saiAdvertisement = JSON.parse(readFileSync(new URL("../../web/fixtures/sai-vn.provider-advertisement.json", import.meta.url), "utf8"));
const saiSurfaceDocument = JSON.parse(readFileSync(new URL("../../web/fixtures/sai-vn-surface.json", import.meta.url), "utf8"));
test("builds Unity UI Toolkit projection capture artifact from provider advertisement and surface", () => {
const { request, projection } = buildUnityUiToolkitCaptureArtifact({
advertisement: aetheriaAdvertisement,
capabilityManifest,
surfaceDocument: aetheriaSurfaceDocument,
advertisementPath: "web/fixtures/aetheria.provider-advertisement.json",
capabilityManifestPath: "packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json",
stamp: "smoke",
});
assert.equal(request.artifactPath, "artifacts/eveunity-uitoolkit-capture/smoke/unity-uitoolkit.png");
assert.equal(projection.schema, "gamecult.eve.unity_uitoolkit_projection.v1");
assert.equal(projection.runtimeId, "unity-uitoolkit");
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, "unity-uitoolkit-projection-json");
assert.equal(projection.root.id, "aetheria.daemon.game.root");
assert.equal(projection.root.visualElementKind, "VisualElement");
assert.deepEqual(projection.root.classNames.slice(0, 2), ["eve-component", "eve-kind-surface"]);
});
test("keeps Sai, Norn, and TeX semantics in sidecar plugin UI projections", () => {
const advertisement = structuredClone(saiAdvertisement);
advertisement.surfaces[0].worldInteraction = {
projectionKind: "provider-authored-visual-novel-surface",
commandBoundary: "sai.vn.plugin.commands",
receiptSchema: "gamecult.eve.command_receipt.v1",
loweringTargets: ["unity-uitoolkit"],
ownership: "provider-owns-story-state-plugin-sidecars-own-nested-semantics",
};
const projection = buildUnityUiToolkitProjection(saiSurfaceDocument, advertisement, "sai.visual_novel.surface");
const graphNode = projection.root.children.find(child => child.id === "sai.graph");
const texLayer = projection.root.children.find(child => child.id === "sai.embeds");
const texNode = texLayer.children.find(child => child.id === "sai.tex.log-power");
assert.equal(projection.root.pluginProjection.pluginId, "sai.vn");
assert.equal(projection.root.pluginProjection.semanticOwner, "Sai");
assert.equal(projection.root.visualElementKind, "SaiVisualNovelStageElement");
assert.equal(graphNode.pluginProjection.pluginId, "norn.graph");
assert.equal(graphNode.pluginProjection.semanticOwner, "Norn");
assert.equal(graphNode.visualElementKind, "NornGraphElement");
assert.equal(texNode.pluginProjection.pluginId, "tex.math");
assert.equal(texNode.pluginProjection.semanticOwner, "EvePlugins");
assert.equal(texNode.visualElementKind, "TeXMathElement");
});

View file

@ -30,6 +30,7 @@
"gamecult.eve.provider_handoff.v1": "schemas/gamecult.eve.provider_handoff.v1.schema.json",
"gamecult.eve.conformance_handoff.v1": "schemas/gamecult.eve.conformance_handoff.v1.schema.json",
"gamecult.eve.electron_shell_projection.v1": "schemas/gamecult.eve.electron_shell_projection.v1.schema.json",
"gamecult.eve.unity_uitoolkit_projection.v1": "schemas/gamecult.eve.unity_uitoolkit_projection.v1.schema.json",
"gamecult.eve.unity_scene_projection.v1": "schemas/gamecult.eve.unity_scene_projection.v1.schema.json",
"gamecult.eve.tui_grid.v1": "schemas/gamecult.eve.tui_grid.v1.schema.json"
},
@ -1498,7 +1499,10 @@
"packages/org.gamecult.eve.unity-uitoolkit/Tests/Editor/EveUiToolkitSurfaceLowererTests.cs",
"packages/org.gamecult.eve.unity-uitoolkit/eveunity-split-handoff.json",
"scripts/run-aetheria-unity-editmode-tests.ps1",
"scripts/run-eveunity-split-handoff-smoke.ps1"
"scripts/run-eveunity-split-handoff-smoke.ps1",
"scripts/run-eveunity-uitoolkit-capture-smoke.ps1",
"tools/eveunity/eveunity-uitoolkit-capture-artifact.mjs",
"tools/eveunity/eveunity-uitoolkit-capture-artifact.test.mjs"
],
"expectedSourceSymbols": [
{
@ -1524,6 +1528,14 @@
{
"path": "packages/org.gamecult.eve.unity-uitoolkit/Tests/Editor/EveUiToolkitSurfaceLowererTests.cs",
"contains": ["FindPluginProjectionAdapter", "SaiStageLowersThroughRuntimeProjectionAdapter", "NornGraphLowersThroughRuntimeProjectionAdapter", "TeXMathLowersThroughRuntimeProjectionAdapterAsSourceFallback", "EmbeddedDocumentSlotMountsResolverBackedChildSurface", "eve-plugin-projection"]
},
{
"path": "tools/eveunity/eveunity-uitoolkit-capture-artifact.mjs",
"contains": ["buildUnityUiToolkitCaptureArtifact", "buildUnityUiToolkitProjection", "gamecult.eve.unity_uitoolkit_projection.v1", "unity-uitoolkit-projection-json", "writeUnityUiToolkitCaptureArtifact", "request-output"]
},
{
"path": "tools/eveunity/eveunity-uitoolkit-capture-artifact.test.mjs",
"contains": ["builds Unity UI Toolkit projection capture artifact", "keeps Sai, Norn, and TeX semantics in sidecar plugin UI projections", "gamecult.eve.unity_uitoolkit_projection.v1", "unity-uitoolkit-projection-json"]
}
],
"commandTransportSmoke": {
@ -1623,6 +1635,9 @@
"tools/eveunity/eveunity-capture-contract.mjs",
"tools/eveunity/eveunity-capture-contract.test.mjs",
"scripts/run-eveunity-capture-contract-smoke.ps1",
"tools/eveunity/eveunity-uitoolkit-capture-artifact.mjs",
"tools/eveunity/eveunity-uitoolkit-capture-artifact.test.mjs",
"scripts/run-eveunity-uitoolkit-capture-smoke.ps1",
"web/fixtures/aetheria.provider-advertisement.json"
],
"pendingProofs": [
@ -1631,8 +1646,20 @@
}
},
"capture": {
"status": "semantic",
"note": "Unity UI Toolkit lowers embedded document slots through a resolver-backed child surface host."
"status": "json-projection",
"note": "Unity UI Toolkit attaches a provider-advertisement-backed gamecult.eve.unity_uitoolkit_projection.v1 JSON projection artifact; Unity editor or batchmode PNG capture remains an EveUnity split blocker.",
"artifacts": [
{
"kind": "json-projection",
"schema": "gamecult.eve.unity_uitoolkit_projection.v1",
"path": "artifacts/eveunity-uitoolkit-capture/latest/unity-uitoolkit-projection.json",
"runtimeId": "unity-uitoolkit",
"providerId": "aetheria",
"surfaceId": "aetheria.daemon.game",
"sourceSurfacePath": "web/fixtures/aetheria-world-surface.json",
"requestPath": "artifacts/eveunity-uitoolkit-capture/latest/capture-request.json"
}
]
}
},
{