Add Unity UI Toolkit projection capture evidence
This commit is contained in:
parent
7e18a3e067
commit
08da3eddd2
13 changed files with 686 additions and 8 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
134
schemas/gamecult.eve.unity_uitoolkit_projection.v1.schema.json
Normal file
134
schemas/gamecult.eve.unity_uitoolkit_projection.v1.schema.json
Normal 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
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
76
scripts/run-eveunity-uitoolkit-capture-smoke.ps1
Normal file
76
scripts/run-eveunity-uitoolkit-capture-smoke.ps1
Normal 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"
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 `
|
||||
|
|
|
|||
321
tools/eveunity/eveunity-uitoolkit-capture-artifact.mjs
Normal file
321
tools/eveunity/eveunity-uitoolkit-capture-artifact.mjs
Normal 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();
|
||||
}
|
||||
60
tools/eveunity/eveunity-uitoolkit-capture-artifact.test.mjs
Normal file
60
tools/eveunity/eveunity-uitoolkit-capture-artifact.test.mjs
Normal 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");
|
||||
});
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue