diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index f8a2271..ae6ce69 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -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 diff --git a/docs/parity-testing-harness.md b/docs/parity-testing-harness.md index 4ba2421..e882c29 100644 --- a/docs/parity-testing-harness.md +++ b/docs/parity-testing-harness.md @@ -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 diff --git a/runtimes/incubating/eve-unity-scene/eve-runtime-capability.json b/runtimes/incubating/eve-unity-scene/eve-runtime-capability.json index c06cd78..1f007ab 100644 --- a/runtimes/incubating/eve-unity-scene/eve-runtime-capability.json +++ b/runtimes/incubating/eve-unity-scene/eve-runtime-capability.json @@ -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." ] diff --git a/runtimes/incubating/eve-unity-scene/eveunity-scene-split-handoff.json b/runtimes/incubating/eve-unity-scene/eveunity-scene-split-handoff.json index 162c4d6..eecd860 100644 --- a/runtimes/incubating/eve-unity-scene/eveunity-scene-split-handoff.json +++ b/runtimes/incubating/eve-unity-scene/eveunity-scene-split-handoff.json @@ -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": [ diff --git a/scripts/run-eveunity-scene-capture-contract-smoke.ps1 b/scripts/run-eveunity-scene-capture-contract-smoke.ps1 new file mode 100644 index 0000000..19417b0 --- /dev/null +++ b/scripts/run-eveunity-scene-capture-contract-smoke.ps1 @@ -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" diff --git a/scripts/run-eveunity-scene-lifecycle-smoke.ps1 b/scripts/run-eveunity-scene-lifecycle-smoke.ps1 index 925b985..65f9d05 100644 --- a/scripts/run-eveunity-scene-lifecycle-smoke.ps1 +++ b/scripts/run-eveunity-scene-lifecycle-smoke.ps1 @@ -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" diff --git a/scripts/run-eveunity-scene-split-handoff-smoke.ps1 b/scripts/run-eveunity-scene-split-handoff-smoke.ps1 index 54958bc..0611fb8 100644 --- a/scripts/run-eveunity-scene-split-handoff-smoke.ps1 +++ b/scripts/run-eveunity-scene-split-handoff-smoke.ps1 @@ -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" diff --git a/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 b/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 index 4f30488..2396795 100644 --- a/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 +++ b/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 @@ -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 ` diff --git a/scripts/run-split-target-conformance-consumer-smoke.ps1 b/scripts/run-split-target-conformance-consumer-smoke.ps1 index ec61f37..5e2972f 100644 --- a/scripts/run-split-target-conformance-consumer-smoke.ps1 +++ b/scripts/run-split-target-conformance-consumer-smoke.ps1 @@ -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" ` diff --git a/tools/eveunity/eveunity-capture-contract.test.mjs b/tools/eveunity/eveunity-capture-contract.test.mjs index d65d892..4848f33 100644 --- a/tools/eveunity/eveunity-capture-contract.test.mjs +++ b/tools/eveunity/eveunity-capture-contract.test.mjs @@ -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/); +}); diff --git a/tools/parity/parity-manifest.json b/tools/parity/parity-manifest.json index 58d2492..7b8aa31 100644 --- a/tools/parity/parity-manifest.json +++ b/tools/parity/parity-manifest.json @@ -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": {