Add Unity scene capture request contract

This commit is contained in:
Meta Vens 2026-07-09 11:19:04 +01:00
commit 0f701cab8e
11 changed files with 186 additions and 13 deletions

View file

@ -403,9 +403,12 @@ Recently cut:
lifecycle smoke at `scripts/run-eveunity-scene-lifecycle-smoke.ps1`. The
manifest claims a provider-agnostic scene graph command surface for
`unity-scene`, types the package release, provider-advertisement scene smoke,
command boundary, and scene capture contracts, and keeps plugin projection
adapters unsupported until Sai, Norn, or TeX sidecar projection support exists
in the Unity scene runtime.
command boundary, and scene capture contracts. It now reuses the shared
EveUnity capture request builder through
`scripts/run-eveunity-scene-capture-contract-smoke.ps1`, while the actual
Unity scene PNG/frame artifact remains pending. Plugin projection adapters
stay unsupported until Sai, Norn, or TeX sidecar projection support exists in
the Unity scene runtime.
- Unity scene now has a direct split handoff smoke at
`scripts/run-eveunity-scene-split-handoff-smoke.ps1`. The smoke verifies the
scene runtime's current source paths for runtime body, scene graph lowering,
@ -506,6 +509,14 @@ Recently cut:
`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.
- Unity scene capture now uses that same request-contract shape. The
`unity-scene` runtime capability manifest names
`gamecult.eve.runtime_capture_request.v1`, the shared
`tools/eveunity/eveunity-capture-contract.mjs` request builder, the Aetheria
provider advertisement input, and the scene PNG artifact pattern.
`scripts/run-eveunity-scene-capture-contract-smoke.ps1` proves the request
without pretending a Unity scene frame has been captured. The actual frame or
screenshot artifact remains a split blocker for EveUnity.
- Runtime-owner conformance consumption now asserts EveUnity lifecycle stage
evidence directly. The generic consumer supports
`--expect-runtime-lifecycle-status` and

View file

@ -312,8 +312,9 @@ capture, embedded document rendering, plugin projection, and repo graduation
remain blockers. EveUnity includes both the active `unity-uitoolkit` package proof
and the pending `unity-scene` runtime boundary; UI Toolkit support is not
treated as full Unity scene/world lowering. The `unity-scene` runtime now claims
a narrow scene graph command surface, while capture, release, and plugin
projection adapters remain explicit blockers.
a narrow scene graph command surface and exposes a typed capture request
contract, while actual scene capture, release, and plugin projection adapters
remain explicit blockers.
The Flutter widget smoke also exercises `EveProviderCatalog` and
`EveProviderPicker` against a conformance-export-shaped provider list. That is

View file

@ -113,6 +113,10 @@
"captureContract": {
"ownerRepo": "EveUnity",
"runtimeId": "unity-scene",
"targetId": "unity-scene",
"requestSchema": "gamecult.eve.runtime_capture_request.v1",
"requestBuilder": "tools/eveunity/eveunity-capture-contract.mjs",
"advertisementPath": "web/fixtures/aetheria.provider-advertisement.json",
"captureKind": "unity-scene-frame-png",
"artifactKind": "png",
"artifactPattern": "artifacts/eveunity-scene-capture/{stamp}/unity-scene.png",
@ -122,7 +126,11 @@
"authority": "Capture observes Unity scene projection only; provider state, command acceptance, and receipts stay provider-owned.",
"publishProof": "Unity scene screenshot or frame-capture artifact is attached to the runtime conformance report from EveUnity."
},
"evidencePaths": [],
"evidencePaths": [
"tools/eveunity/eveunity-capture-contract.mjs",
"tools/eveunity/eveunity-capture-contract.test.mjs",
"scripts/run-eveunity-scene-capture-contract-smoke.ps1"
],
"pendingProofs": [
"Unity scene screenshot or frame-capture artifact is attached to the runtime conformance report."
]

View file

@ -47,9 +47,13 @@
{
"id": "unity-scene-capture-lifecycle",
"stage": "capture",
"currentPaths": [],
"currentPaths": [
"tools/eveunity/eveunity-capture-contract.mjs",
"tools/eveunity/eveunity-capture-contract.test.mjs",
"scripts/run-eveunity-scene-capture-contract-smoke.ps1"
],
"destinationOwner": "EveUnity",
"replacementProof": "EveUnity attaches Unity scene screenshot or frame-capture artifacts to conformance output for at least one provider-authored world surface."
"replacementProof": "EveUnity builds a provider-advertisement-backed Unity scene capture request now; the split is not complete until EveUnity attaches Unity scene screenshot or frame-capture artifacts to conformance output for at least one provider-authored world surface."
}
],
"contractInputs": [

View file

@ -0,0 +1,99 @@
param(
[string] $CapabilityManifestPath = "runtimes\incubating\eve-unity-scene\eve-runtime-capability.json",
[string] $AdvertisementPath = "web\fixtures\aetheria.provider-advertisement.json",
[string] $OutputPath = "artifacts\eveunity-scene-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 "EveUnity scene capture contract input not found: $path"
}
}
$manifest = Get-Content -LiteralPath $absoluteCapabilityPath -Raw | ConvertFrom-Json
$captureContract = $manifest.lifecycle.capture.captureContract
if ($null -eq $captureContract) {
throw "EveUnity scene 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 "EveUnity scene capture contract missing $field"
}
}
if ($captureContract.ownerRepo -ne "EveUnity") {
throw "Unexpected EveUnity scene capture owner: $($captureContract.ownerRepo)"
}
if ($captureContract.runtimeId -ne "unity-scene") {
throw "Unexpected EveUnity scene capture runtime: $($captureContract.runtimeId)"
}
if ($captureContract.targetId -ne "unity-scene") {
throw "Unexpected EveUnity scene capture target: $($captureContract.targetId)"
}
if ($captureContract.requestSchema -ne "gamecult.eve.runtime_capture_request.v1") {
throw "Unexpected EveUnity scene capture request schema: $($captureContract.requestSchema)"
}
$builderPath = Join-Path $projectRoot $captureContract.requestBuilder
if (-not (Test-Path -LiteralPath $builderPath)) {
throw "EveUnity scene capture request builder not found: $($captureContract.requestBuilder)"
}
if ((Join-Path $projectRoot $captureContract.advertisementPath) -ne $absoluteAdvertisementPath) {
throw "EveUnity scene capture advertisement path mismatch: $($captureContract.advertisementPath) vs $AdvertisementPath"
}
node --test (Join-Path $projectRoot "tools\eveunity\eveunity-capture-contract.test.mjs")
if ($LASTEXITCODE -ne 0) {
throw "EveUnity capture contract tests failed with exit code $LASTEXITCODE"
}
node $builderPath --capability $absoluteCapabilityPath --advertisement $absoluteAdvertisementPath --output $absoluteOutputPath --stamp $Stamp
if ($LASTEXITCODE -ne 0) {
throw "EveUnity scene capture request builder failed with exit code $LASTEXITCODE"
}
if (-not (Test-Path -LiteralPath $absoluteOutputPath)) {
throw "EveUnity scene 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 = "unity-scene" },
@{ field = "ownerRepo"; value = "EveUnity" },
@{ field = "providerId"; value = "aetheria" },
@{ field = "surfaceId"; value = "aetheria.daemon.game" },
@{ field = "targetId"; value = "unity-scene" },
@{ field = "commandBoundary"; value = "aetheria.daemon.commands" },
@{ field = "receiptSchema"; value = "aetheria.eve_command_acceptance_status.v1" },
@{ field = "captureKind"; value = "unity-scene-frame-png" },
@{ field = "artifactKind"; value = "png" },
@{ field = "conformanceAttachment"; value = "runtime.captureArtifacts[]" }
)) {
if ($request.($expectation.field) -ne $expectation.value) {
throw "EveUnity scene capture request $($expectation.field) expected $($expectation.value) got $($request.($expectation.field))"
}
}
if ($request.artifactPath -ne "artifacts/eveunity-scene-capture/$Stamp/unity-scene.png") {
throw "Unexpected EveUnity scene capture artifact path: $($request.artifactPath)"
}
Write-Host "EveUnity scene capture contract smoke passed: $absoluteOutputPath"

View file

@ -137,7 +137,7 @@ 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 "EveUnity scene capture contract missing $field"
}
@ -145,8 +145,25 @@ foreach ($field in @("ownerRepo", "runtimeId", "captureKind", "artifactKind", "a
if ($captureContract.runtimeId -ne "unity-scene") {
throw "EveUnity scene capture contract has unexpected runtime: $($captureContract.runtimeId)"
}
if ($captureContract.targetId -ne "unity-scene") {
throw "EveUnity scene capture contract has unexpected target: $($captureContract.targetId)"
}
if ($captureContract.requestSchema -ne "gamecult.eve.runtime_capture_request.v1") {
throw "EveUnity scene capture contract has unexpected request schema: $($captureContract.requestSchema)"
}
if ($captureContract.captureKind -ne "unity-scene-frame-png") {
throw "EveUnity scene 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 "EveUnity scene capture contract $pathProperty does not exist: $relativePath"
}
}
& (Join-Path $projectRoot "scripts\run-eveunity-scene-split-handoff-smoke.ps1")
& (Join-Path $projectRoot "scripts\run-eveunity-scene-provider-shell-smoke.ps1")
& (Join-Path $projectRoot "scripts\run-eveunity-scene-capture-contract-smoke.ps1")
Write-Host "EveUnity scene lifecycle smoke passed: $absoluteManifestPath"

View file

@ -65,7 +65,7 @@ foreach ($id in $expectedMoveSets.Keys) {
}
$currentPaths = if ($null -eq $moveSet.currentPaths) { @() } else { @($moveSet.currentPaths) }
$observedProviderPaths = if ($null -eq $moveSet.observedProviderPaths) { @() } else { @($moveSet.observedProviderPaths) }
if ($id -in @("unity-scene-runtime-body", "unity-scene-world-surface-lowering", "unity-scene-command-transport")) {
if ($id -in @("unity-scene-runtime-body", "unity-scene-world-surface-lowering", "unity-scene-command-transport", "unity-scene-capture-lifecycle")) {
if ($currentPaths.Count -eq 0) {
throw "EveUnity scene split handoff move set $id must name current Eve incubation paths"
}
@ -75,8 +75,6 @@ foreach ($id in $expectedMoveSets.Keys) {
throw "EveUnity scene split handoff move set $id path missing: $relativePath"
}
}
} elseif ($currentPaths.Count -ne 0) {
throw "EveUnity scene split handoff move set $id must not claim existing Eve source paths before its proof exists"
}
if ($observedProviderPaths.Count -ne 0) {
throw "EveUnity scene split handoff move set $id must not treat provider product paths as generic scene runtime source"

View file

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

View file

@ -50,6 +50,7 @@ try {
--expect-split-handoff-move EveUnity:unity-scene:unity-scene-runtime-body:current:exists:EveUnitySceneSurfaceLowerer.cs `
--expect-split-handoff-move EveUnity:unity-scene:unity-scene-world-surface-lowering:current:exists:EveUnitySceneSurfaceLowerer.cs `
--expect-split-handoff-move EveUnity:unity-scene:unity-scene-command-transport:current:exists:EveUnitySceneSurfaceLowerer.cs `
--expect-split-handoff-move EveUnity:unity-scene:unity-scene-capture-lifecycle:current:exists:eveunity-capture-contract.mjs `
--expect-split-target-blocker "EveUnity:Tagged UPM release" `
--expect-split-target-blocker "EveUnity:Unity batchmode EditMode runner" `
--expect-split-target-blocker "EveUnity:Unity batchmode or editor capture artifact" `

View file

@ -5,6 +5,7 @@ import { buildUnityCaptureRequest } from "./eveunity-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("../../packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json", import.meta.url), "utf8"));
const sceneCapabilityManifest = JSON.parse(readFileSync(new URL("../../runtimes/incubating/eve-unity-scene/eve-runtime-capability.json", import.meta.url), "utf8"));
test("builds Unity UI Toolkit capture request from provider advertisement", () => {
const request = buildUnityCaptureRequest({
@ -38,3 +39,25 @@ test("rejects a capture request when the surface does not advertise the runtime
/does not advertise lowering target unity-uitoolkit/,
);
});
test("builds Unity scene capture request from provider advertisement", () => {
const request = buildUnityCaptureRequest({
advertisement,
capabilityManifest: sceneCapabilityManifest,
advertisementPath: "web/fixtures/aetheria.provider-advertisement.json",
capabilityManifestPath: "runtimes/incubating/eve-unity-scene/eve-runtime-capability.json",
stamp: "smoke",
});
assert.equal(request.schema, "gamecult.eve.runtime_capture_request.v1");
assert.equal(request.runtimeId, "unity-scene");
assert.equal(request.ownerRepo, "EveUnity");
assert.equal(request.providerId, "aetheria");
assert.equal(request.surfaceId, "aetheria.daemon.game");
assert.equal(request.targetId, "unity-scene");
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/eveunity-scene-capture/smoke/unity-scene.png");
assert.match(request.authority, /provider state/);
});

View file

@ -911,7 +911,10 @@
"runtimes/incubating/eve-unity-scene/eve-runtime-capability.json",
"runtimes/incubating/eve-unity-scene/eveunity-scene-split-handoff.json",
"scripts/run-eveunity-scene-lifecycle-smoke.ps1",
"scripts/run-eveunity-scene-provider-shell-smoke.ps1"
"scripts/run-eveunity-scene-provider-shell-smoke.ps1",
"scripts/run-eveunity-scene-capture-contract-smoke.ps1",
"tools/eveunity/eveunity-capture-contract.mjs",
"tools/eveunity/eveunity-capture-contract.test.mjs"
],
"expectedSourceSymbols": [
{
@ -921,6 +924,10 @@
{
"path": "runtimes/incubating/eve-unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs",
"contains": ["LowerCarriesProviderAdvertisedWorldBoundary", "LowerBuildsProviderAgnosticSceneGraphFromSurfaceTree", "CommandIntentCarriesAdvertisedBoundaryWithoutOwningReceipts", "world-projection-node", "plugin-placeholder", "aetheria.daemon.commands", "aetheria.eve_command_acceptance_status.v1"]
},
{
"path": "tools/eveunity/eveunity-capture-contract.test.mjs",
"contains": ["builds Unity scene capture request from provider advertisement", "unity-scene", "artifacts/eveunity-scene-capture/smoke/unity-scene.png"]
}
],
"capture": {