diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index d4e31b5..91e8f1e 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -507,9 +507,11 @@ Recently cut: summary, terminal-grid lowering, and the `tui` world/editor lowering claim; it now carries an executable terminal capture request contract through `tools/evetui/evetui-capture-contract.mjs` and - `scripts/run-evetui-capture-contract-smoke.ps1`. Plugin projection, terminal - package release, and actual transcript/cell-grid capture artifacts remain - pending. + `scripts/run-evetui-capture-contract-smoke.ps1`, plus a deterministic + `gamecult.eve.tui_grid.v1` JSON capture artifact through + `tools/evetui/evetui-capture-artifact.mjs` and + `scripts/run-evetui-capture-smoke.ps1`. Plugin projection, terminal package + release, and owner-repo production of the capture path remain pending. - EveTui release lifecycle now carries a structured pending release request contract. The runtime capability manifest reads version truth from `runtimes/incubating/eve-tui/package.json`, names the @@ -568,6 +570,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. +- EveTui capture lifecycle now attaches a typed terminal-grid artifact instead + of stopping at a request contract. `tools/evetui/evetui-capture-artifact.mjs` + lowers `web/fixtures/aetheria-world-surface.json` through `EveTuiShell`, + writes `artifacts/evetui-capture/latest/tui-grid.json`, and stores the paired + capture request beside it. Parity validates the artifact schema, provider, + surface, and linked request path, then exports it as `runtime.captureArtifacts[]`. + The split blocker is no longer "no artifact"; it is "EveTui must own this + capture path outside Eve incubation." - EveElectron release lifecycle now carries a structured pending release request contract. `releaseContract` names `gamecult.eve.runtime_release_request.v1`, the diff --git a/docs/parity-testing-harness.md b/docs/parity-testing-harness.md index b0b49fa..76216e2 100644 --- a/docs/parity-testing-harness.md +++ b/docs/parity-testing-harness.md @@ -389,6 +389,15 @@ exist. The current lifecycle proof is deliberately split: manifest and Aetheria provider advertisement; Unity editor or batchmode PNG capture remains a split blocker. +TUI capture evidence is already typed even though EveTui remains incubating. +`tools/evetui/evetui-capture-artifact.mjs` lowers the Aetheria world surface +through `EveTuiShell` into `gamecult.eve.tui_grid.v1` and writes +`artifacts/evetui-capture/latest/tui-grid.json`. The parity export includes +that record as `runtime.captureArtifacts[]`; runtime-owner consumers can assert +it without depending on Eve's source layout. The remaining blocker is +owner-repo production of the same capture path, not absence of a cell-grid +artifact. + EveUnity split handoff evidence: ```powershell @@ -511,8 +520,8 @@ Pending runtimes are allowed. Silent fake parity is not. ## Next Cuts -1. Extend runtime-owned image/layout probes to the Unity/Electron/TUI capture - blockers. +1. Extend runtime-owned image/layout probes to the Unity/Electron capture + blockers and move EveTui's JSON grid capture path into the EveTui owner repo. 2. Normalize text scale and font loading across web, Flutter Android, Flutter desktop, and iOS. 3. Give iOS a real `vn.stage` scene compositor instead of compact stacked diff --git a/runtimes/incubating/eve-tui/eve-runtime-capability.json b/runtimes/incubating/eve-tui/eve-runtime-capability.json index 5948988..a00cabb 100644 --- a/runtimes/incubating/eve-tui/eve-runtime-capability.json +++ b/runtimes/incubating/eve-tui/eve-runtime-capability.json @@ -114,7 +114,7 @@ }, "capture": { "ownerRepo": "EveTui", - "status": "pending-terminal-capture", + "status": "cell-grid-json-capture", "captureContract": { "ownerRepo": "EveTui", "runtimeId": "tui", @@ -122,27 +122,41 @@ "requestSchema": "gamecult.eve.runtime_capture_request.v1", "requestBuilder": "tools/evetui/evetui-capture-contract.mjs", "advertisementPath": "web/fixtures/aetheria.provider-advertisement.json", - "captureKind": "terminal-transcript-or-cell-grid", - "artifactKind": "ansi-transcript-or-json-grid", - "artifactPattern": "artifacts/evetui-capture/{stamp}/tui-transcript.ansi", + "captureKind": "terminal-cell-grid", + "artifactKind": "json-grid", + "artifactPattern": "artifacts/evetui-capture/{stamp}/tui-grid.json", "conformanceAttachment": "runtime.captureArtifacts[]", "requiredProvider": "aetheria", "requiredSurface": "aetheria.daemon.game", "authority": "Capture observes terminal projection only; provider state, command acceptance, and receipts stay provider-owned.", - "publishProof": "Terminal transcript or cell-grid capture artifact is attached to the runtime conformance report from EveTui." + "publishProof": "Terminal cell-grid JSON capture artifact is attached to the runtime conformance report from EveTui." }, "evidencePaths": [ "tools/evetui/evetui-capture-contract.mjs", "tools/evetui/evetui-capture-contract.test.mjs", - "scripts/run-evetui-capture-contract-smoke.ps1" + "tools/evetui/evetui-capture-artifact.mjs", + "tools/evetui/evetui-capture-artifact.test.mjs", + "scripts/run-evetui-capture-contract-smoke.ps1", + "scripts/run-evetui-capture-smoke.ps1" ], "pendingProofs": [ - "TUI terminal transcript or cell-grid capture artifact is attached to the runtime conformance report." + "TUI capture artifact is produced from the EveTui owner repo rather than Eve incubation." ] } }, "capture": { - "status": "missing", - "note": "TUI transcript or cell-grid capture is a split blocker until EveTui attaches durable terminal artifacts." + "status": "json-grid", + "note": "TUI attaches a provider-advertisement-backed gamecult.eve.tui_grid.v1 JSON capture artifact; packaged runtime ownership remains an EveTui split blocker.", + "artifacts": [ + { + "kind": "json-grid", + "schema": "gamecult.eve.tui_grid.v1", + "path": "artifacts/evetui-capture/latest/tui-grid.json", + "providerId": "aetheria", + "surfaceId": "aetheria.daemon.game", + "sourceSurfacePath": "web/fixtures/aetheria-world-surface.json", + "requestPath": "artifacts/evetui-capture/latest/capture-request.json" + } + ] } } diff --git a/runtimes/incubating/eve-tui/evetui-split-handoff.json b/runtimes/incubating/eve-tui/evetui-split-handoff.json index ee4a39d..981190b 100644 --- a/runtimes/incubating/eve-tui/evetui-split-handoff.json +++ b/runtimes/incubating/eve-tui/evetui-split-handoff.json @@ -47,10 +47,13 @@ "currentPaths": [ "tools/evetui/evetui-capture-contract.mjs", "tools/evetui/evetui-capture-contract.test.mjs", - "scripts/run-evetui-capture-contract-smoke.ps1" + "tools/evetui/evetui-capture-artifact.mjs", + "tools/evetui/evetui-capture-artifact.test.mjs", + "scripts/run-evetui-capture-contract-smoke.ps1", + "scripts/run-evetui-capture-smoke.ps1" ], "destinationOwner": "EveTui", - "replacementProof": "EveTui builds a provider-advertisement-backed terminal capture request now; the split is not complete until EveTui attaches deterministic terminal transcript or cell-grid capture artifacts to conformance output." + "replacementProof": "EveTui builds a provider-advertisement-backed terminal capture request and deterministic gamecult.eve.tui_grid.v1 JSON capture artifact now; the split is not complete until EveTui owns that capture path outside Eve incubation." } ], "contractInputs": [ @@ -75,6 +78,6 @@ "EveTui consumes at least one non-Aetheria provider advertisement.", "EveTui consumes Aetheria's agent-facing TUI surface through provider advertisements without importing Aetheria internals.", "EveTui emits gamecult.eve.command.v1 intents and renders provider receipts.", - "EveTui attaches terminal transcript or cell-grid capture artifacts to conformance output." + "EveTui attaches terminal transcript or cell-grid capture artifacts to conformance output from its owner repo." ] } diff --git a/schemas/gamecult.eve.conformance_export.v1.schema.json b/schemas/gamecult.eve.conformance_export.v1.schema.json index c969631..5297565 100644 --- a/schemas/gamecult.eve.conformance_export.v1.schema.json +++ b/schemas/gamecult.eve.conformance_export.v1.schema.json @@ -1113,6 +1113,12 @@ "type": "string", "minLength": 1 }, + "captureArtifacts": { + "type": "array" + }, + "captureArtifactErrors": { + "type": "array" + }, "lifecycle": { "type": "object" }, diff --git a/schemas/gamecult.eve.runtime_capability.v1.schema.json b/schemas/gamecult.eve.runtime_capability.v1.schema.json index ed9e3c4..ec689e9 100644 --- a/schemas/gamecult.eve.runtime_capability.v1.schema.json +++ b/schemas/gamecult.eve.runtime_capability.v1.schema.json @@ -489,6 +489,61 @@ } }, "additionalProperties": true + }, + "capture": { + "type": "object", + "properties": { + "status": { + "type": "string", + "minLength": 1 + }, + "note": { + "type": "string" + }, + "artifacts": { + "type": "array", + "items": { + "type": "object", + "required": [ + "kind", + "schema", + "path", + "providerId", + "surfaceId" + ], + "properties": { + "kind": { + "type": "string", + "minLength": 1 + }, + "schema": { + "type": "string", + "minLength": 1 + }, + "path": { + "type": "string", + "minLength": 1 + }, + "providerId": { + "type": "string", + "minLength": 1 + }, + "surfaceId": { + "type": "string", + "minLength": 1 + }, + "sourceSurfacePath": { + "type": "string" + }, + "requestPath": { + "type": "string" + } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true } }, "additionalProperties": true diff --git a/scripts/run-evetui-capture-contract-smoke.ps1 b/scripts/run-evetui-capture-contract-smoke.ps1 index 2902777..72c8c5f 100644 --- a/scripts/run-evetui-capture-contract-smoke.ps1 +++ b/scripts/run-evetui-capture-contract-smoke.ps1 @@ -84,15 +84,15 @@ foreach ($expectation in @( @{ field = "targetId"; value = "tui" }, @{ field = "commandBoundary"; value = "aetheria.daemon.commands" }, @{ field = "receiptSchema"; value = "aetheria.eve_command_acceptance_status.v1" }, - @{ field = "captureKind"; value = "terminal-transcript-or-cell-grid" }, - @{ field = "artifactKind"; value = "ansi-transcript-or-json-grid" }, + @{ field = "captureKind"; value = "terminal-cell-grid" }, + @{ field = "artifactKind"; value = "json-grid" }, @{ field = "conformanceAttachment"; value = "runtime.captureArtifacts[]" } )) { if ($request.($expectation.field) -ne $expectation.value) { throw "EveTui capture request $($expectation.field) expected $($expectation.value) got $($request.($expectation.field))" } } -if ($request.artifactPath -ne "artifacts/evetui-capture/$Stamp/tui-transcript.ansi") { +if ($request.artifactPath -ne "artifacts/evetui-capture/$Stamp/tui-grid.json") { throw "Unexpected EveTui capture artifact path: $($request.artifactPath)" } diff --git a/scripts/run-evetui-capture-smoke.ps1 b/scripts/run-evetui-capture-smoke.ps1 new file mode 100644 index 0000000..496fdb1 --- /dev/null +++ b/scripts/run-evetui-capture-smoke.ps1 @@ -0,0 +1,73 @@ +param( + [string] $CapabilityManifestPath = "runtimes\incubating\eve-tui\eve-runtime-capability.json", + [string] $AdvertisementPath = "web\fixtures\aetheria.provider-advertisement.json", + [string] $SurfacePath = "web\fixtures\aetheria-world-surface.json", + [string] $OutputPath = "artifacts\evetui-capture\latest\tui-grid.json", + [string] $RequestOutputPath = "artifacts\evetui-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 "EveTui capture input not found: $path" + } +} + +node --test (Join-Path $projectRoot "tools\evetui\evetui-capture-artifact.test.mjs") +if ($LASTEXITCODE -ne 0) { + throw "EveTui capture artifact tests failed with exit code $LASTEXITCODE" +} + +node (Join-Path $projectRoot "tools\evetui\evetui-capture-artifact.mjs") ` + --capability $absoluteCapabilityPath ` + --advertisement $absoluteAdvertisementPath ` + --surface $absoluteSurfacePath ` + --output $absoluteOutputPath ` + --request-output $absoluteRequestOutputPath ` + --stamp $Stamp +if ($LASTEXITCODE -ne 0) { + throw "EveTui capture artifact builder failed with exit code $LASTEXITCODE" +} + +foreach ($path in @($absoluteOutputPath, $absoluteRequestOutputPath)) { + if (-not (Test-Path -LiteralPath $path)) { + throw "EveTui capture artifact output missing: $path" + } +} + +$grid = Get-Content -LiteralPath $absoluteOutputPath -Raw | ConvertFrom-Json +foreach ($expectation in @( + @{ field = "schema"; value = "gamecult.eve.tui_grid.v1" }, + @{ field = "runtimeId"; value = "tui" }, + @{ 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-grid" }, + @{ field = "captureKind"; value = "terminal-cell-grid" } +)) { + if ($grid.($expectation.field) -ne $expectation.value) { + throw "EveTui grid $($expectation.field) expected $($expectation.value) got $($grid.($expectation.field))" + } +} +if (@($grid.lines).Count -lt 1) { + throw "EveTui grid capture must contain terminal lines" +} + +Write-Host "EveTui capture smoke passed: $absoluteOutputPath" diff --git a/scripts/run-evetui-lifecycle-smoke.ps1 b/scripts/run-evetui-lifecycle-smoke.ps1 index fbeff0c..6a23b51 100644 --- a/scripts/run-evetui-lifecycle-smoke.ps1 +++ b/scripts/run-evetui-lifecycle-smoke.ps1 @@ -159,7 +159,7 @@ if ($captureContract.targetId -ne "tui") { if ($captureContract.requestSchema -ne "gamecult.eve.runtime_capture_request.v1") { throw "EveTui capture contract has unexpected request schema: $($captureContract.requestSchema)" } -if ($captureContract.captureKind -ne "terminal-transcript-or-cell-grid") { +if ($captureContract.captureKind -ne "terminal-cell-grid") { throw "EveTui capture contract has unexpected capture kind: $($captureContract.captureKind)" } foreach ($pathProperty in @("requestBuilder", "advertisementPath")) { @@ -174,5 +174,6 @@ foreach ($pathProperty in @("requestBuilder", "advertisementPath")) { & (Join-Path $projectRoot "scripts\run-evetui-release-contract-smoke.ps1") & (Join-Path $projectRoot "scripts\run-evetui-provider-shell-smoke.ps1") & (Join-Path $projectRoot "scripts\run-evetui-capture-contract-smoke.ps1") +& (Join-Path $projectRoot "scripts\run-evetui-capture-smoke.ps1") Write-Host "EveTui lifecycle smoke passed: $absoluteManifestPath" diff --git a/scripts/run-parity-harness.ps1 b/scripts/run-parity-harness.ps1 index 8ca87c4..28bc5b9 100644 --- a/scripts/run-parity-harness.ps1 +++ b/scripts/run-parity-harness.ps1 @@ -20,6 +20,7 @@ try { -OutputPath "artifacts\web-reference-layout-probe\latest\sai-vn.json" ` -Port 8893 ` -SkipBuild + & (Join-Path $PSScriptRoot "run-evetui-capture-smoke.ps1") node .\tools\parity\run-parity.mjs & (Join-Path $PSScriptRoot "run-eveconformance-handoff-smoke.ps1") & (Join-Path $PSScriptRoot "run-plugin-handoff-smoke.ps1") diff --git a/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 b/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 index 97c320f..2467f7f 100644 --- a/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 +++ b/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 @@ -122,7 +122,8 @@ node $consumerScript $consumerExport ` --expect-runtime-capture-status unity-uitoolkit:semantic ` --expect-runtime-capture-status unity-scene:missing ` --expect-runtime-capture-status electron-shell:missing ` - --expect-runtime-capture-status tui:missing ` + --expect-runtime-capture-status tui:json-grid ` + --expect-runtime-capture-artifact tui:json-grid:gamecult.eve.tui_grid.v1:aetheria:aetheria.daemon.game ` --expect-runtime-capture-status direct2d:missing ` --expect-screenshot-metric web:embedded-surface:structure:pass ` --expect-screenshot-metric web:embedded-surface:color-tokens:pass ` @@ -133,7 +134,7 @@ node $consumerScript $consumerExport ` --expect-screenshot-metric web:sai-vn:bounding-boxes:pass ` --expect-screenshot-metric unity-scene:aetheria-world:bounding-boxes:pending-capture ` --expect-screenshot-metric electron-shell:aetheria-world:bounding-boxes:pending-capture ` - --expect-screenshot-metric tui:aetheria-world:bounding-boxes:pending-capture ` + --expect-screenshot-metric tui:aetheria-world:bounding-boxes:terminal-grid-capture ` --expect-runtime-lifecycle-status unity-uitoolkit:release:incubating-upm-package ` --expect-runtime-lifecycle-status unity-uitoolkit:test:batchmode-editmode-tests-and-consumer-build-smoke ` --expect-runtime-lifecycle-status unity-uitoolkit:capture:pending-editor-capture ` @@ -203,10 +204,10 @@ node $consumerScript $consumerExport ` --expect-runtime-lifecycle-field "electron-shell:capture:captureContract.requiredSurface:aetheria.daemon.game" ` --expect-runtime-lifecycle-status tui:release:pending-tui-package ` --expect-runtime-lifecycle-status tui:test:provider-shell-contract-skeleton ` - --expect-runtime-lifecycle-status tui:capture:pending-terminal-capture ` + --expect-runtime-lifecycle-status tui:capture:cell-grid-json-capture ` --expect-runtime-lifecycle-pending "tui:release:TUI package release" ` --expect-runtime-lifecycle-pending "tui:test:TUI provider-advertisement smoke runs from EveTui with durable terminal transcript or cell-grid artifacts" ` - --expect-runtime-lifecycle-pending "tui:capture:TUI terminal transcript or cell-grid capture artifact" ` + --expect-runtime-lifecycle-pending "tui:capture:TUI capture artifact is produced from the EveTui owner repo" ` --expect-runtime-lifecycle-field "tui:release:releaseContract.artifactKind:terminal-runtime" ` --expect-runtime-lifecycle-field "tui:release:releaseContract.versionSource:runtimes/incubating/eve-tui/package.json" ` --expect-runtime-lifecycle-field "tui:release:releaseContract.requestSchema:gamecult.eve.runtime_release_request.v1" ` @@ -214,7 +215,9 @@ node $consumerScript $consumerExport ` --expect-runtime-lifecycle-field "tui:release:releaseContract.artifactPattern:artifacts/evetui-release/{version}/eve-tui-{version}.tgz" ` --expect-runtime-lifecycle-field "tui:test:testContract.runnerKind:terminal-provider-shell-smoke" ` --expect-runtime-lifecycle-field "tui:test:testContract.runnerScript:scripts/run-evetui-provider-shell-smoke.ps1" ` - --expect-runtime-lifecycle-field "tui:capture:captureContract.captureKind:terminal-transcript-or-cell-grid" ` + --expect-runtime-lifecycle-field "tui:capture:captureContract.captureKind:terminal-cell-grid" ` + --expect-runtime-lifecycle-field "tui:capture:captureContract.artifactKind:json-grid" ` + --expect-runtime-lifecycle-field "tui:capture:captureContract.artifactPattern:artifacts/evetui-capture/{stamp}/tui-grid.json" ` --expect-runtime-lifecycle-field "tui:capture:captureContract.targetId:tui" ` --expect-runtime-lifecycle-field "tui:capture:captureContract.requestSchema:gamecult.eve.runtime_capture_request.v1" ` --expect-runtime-lifecycle-field "tui:capture:captureContract.requestBuilder:tools/evetui/evetui-capture-contract.mjs" ` diff --git a/tools/conformance/consume-export.mjs b/tools/conformance/consume-export.mjs index 4f9acf4..80415ac 100644 --- a/tools/conformance/consume-export.mjs +++ b/tools/conformance/consume-export.mjs @@ -51,6 +51,7 @@ if (!exportDirectory) { " --expect-runtime-handoff ", " --expect-runtime-command-schema ", " --expect-runtime-capture-status ", + " --expect-runtime-capture-artifact ", " --expect-runtime-lifecycle-status ", " --expect-runtime-lifecycle-pending ", " --expect-runtime-lifecycle-field ", @@ -588,6 +589,26 @@ function validateIndex(index, directory, expectations, errors) { errors.push(`runtimes:${expectation.runtimeId}:captureStatus:expected ${expectation.status} got ${runtime.captureStatus || ""}`); } } + for (const expectation of expectations.runtimeCaptureArtifacts) { + const runtime = runtimes.find(candidate => candidate.runtimeId === expectation.runtimeId); + if (!runtime) { + errors.push(`runtimes:${expectation.runtimeId}:missing`); + continue; + } + const artifacts = Array.isArray(runtime.captureArtifacts) ? runtime.captureArtifacts : []; + const artifact = artifacts.find(candidate => + candidate.kind === expectation.kind + && candidate.schema === expectation.schema + && candidate.providerId === expectation.providerId + && candidate.surfaceId === expectation.surfaceId); + if (!artifact) { + errors.push(`runtimes:${expectation.runtimeId}:captureArtifacts:${expectation.kind}:${expectation.schema}:${expectation.providerId}:${expectation.surfaceId}:missing`); + } + const artifactErrors = Array.isArray(runtime.captureArtifactErrors) ? runtime.captureArtifactErrors : []; + if (artifactErrors.length) { + errors.push(`runtimes:${expectation.runtimeId}:captureArtifactErrors:${artifactErrors.join(",")}`); + } + } for (const expectation of expectations.runtimeLifecycleStatuses) { const runtime = runtimes.find(candidate => candidate.runtimeId === expectation.runtimeId); if (!runtime) { @@ -1100,6 +1121,7 @@ function parseArguments(args) { runtimeHandoffs: [], runtimeCommandSchemas: [], runtimeCaptureStatuses: [], + runtimeCaptureArtifacts: [], runtimeLifecycleStatuses: [], runtimeLifecyclePendingProofs: [], runtimeLifecycleFields: [], @@ -1154,6 +1176,7 @@ function parseArguments(args) { ["--expect-runtime-handoff", expectations.runtimeHandoffs], ["--expect-runtime-command-schema", expectations.runtimeCommandSchemas], ["--expect-runtime-capture-status", expectations.runtimeCaptureStatuses], + ["--expect-runtime-capture-artifact", expectations.runtimeCaptureArtifacts], ["--expect-runtime-lifecycle-status", expectations.runtimeLifecycleStatuses], ["--expect-runtime-lifecycle-pending", expectations.runtimeLifecyclePendingProofs], ["--expect-runtime-lifecycle-field", expectations.runtimeLifecycleFields], @@ -1240,6 +1263,8 @@ function parseArguments(args) { target.push(parseRuntimeExpectation(value, "schema")); } else if (option === "--expect-runtime-capture-status") { target.push(parseRuntimeExpectation(value, "status")); + } else if (option === "--expect-runtime-capture-artifact") { + target.push(parseRuntimeCaptureArtifactExpectation(value)); } else if (option === "--expect-runtime-lifecycle-status") { target.push(parseRuntimeLifecycleExpectation(value, "status")); } else if (option === "--expect-runtime-lifecycle-pending") { @@ -1293,6 +1318,21 @@ function parseScreenshotMetricExpectation(value) { }; } +function parseRuntimeCaptureArtifactExpectation(value) { + const parts = value.split(":"); + if (parts.length !== 5 || parts.some(part => !part)) { + console.error(`Expected runtime capture artifact in form, got: ${value}`); + process.exit(2); + } + return { + runtimeId: parts[0], + kind: parts[1], + schema: parts[2], + providerId: parts[3], + surfaceId: parts[4], + }; +} + function parseWorldLoweringGapExpectation(value) { const parts = value.split(":"); if (parts.length !== 5 || parts.some(part => !part)) { diff --git a/tools/evetui/evetui-capture-artifact.mjs b/tools/evetui/evetui-capture-artifact.mjs new file mode 100644 index 0000000..41c96e6 --- /dev/null +++ b/tools/evetui/evetui-capture-artifact.mjs @@ -0,0 +1,115 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { EveTuiShell } from "../../runtimes/incubating/eve-tui/src/eve-tui-shell.mjs"; +import { buildTuiCaptureRequest, loadJsonFile, writeCaptureRequest } from "./evetui-capture-contract.mjs"; + +export function buildTuiCaptureArtifact({ + advertisement, + capabilityManifest, + surfaceDocument, + advertisementPath = "", + capabilityManifestPath = "", + stamp = "latest", + width = 80, +} = {}) { + const request = buildTuiCaptureRequest({ + advertisement, + capabilityManifest, + advertisementPath, + capabilityManifestPath, + stamp, + }); + const shell = new EveTuiShell({ width }); + const grid = shell.lowerSurface(surfaceDocument, advertisement, request.surfaceId); + validateGridArtifact(grid, request); + + return { + request, + grid: { + ...grid, + artifactKind: request.artifactKind, + captureKind: request.captureKind, + sourceAdvertisementPath: request.sourceAdvertisementPath, + sourceCapabilityManifestPath: request.sourceCapabilityManifestPath, + issuedAtUtc: request.issuedAtUtc, + }, + }; +} + +export function writeTuiCaptureArtifact({ request, grid }, { outputPath, requestOutputPath = "" } = {}) { + if (!outputPath) throw new Error("outputPath is required"); + mkdirSync(path.dirname(outputPath), { recursive: true }); + writeFileSync(outputPath, `${JSON.stringify(grid, null, 2)}\n`, "utf8"); + if (requestOutputPath) { + writeCaptureRequest(request, requestOutputPath); + } +} + +function validateGridArtifact(grid, request) { + if (grid.schema !== "gamecult.eve.tui_grid.v1") { + throw new Error(`Unexpected TUI grid schema: ${grid.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 ((grid[field] || "") !== (expected || "")) { + throw new Error(`TUI grid ${field} expected ${expected || ""} got ${grid[field] || ""}`); + } + } + if (!Array.isArray(grid.lines) || !grid.lines.length) { + throw new Error("TUI grid artifact must contain at least one line"); + } +} + +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 = buildTuiCaptureArtifact({ + advertisement: loadJsonFile(args.advertisement), + capabilityManifest: loadJsonFile(args.capability), + surfaceDocument: loadJsonFile(args.surface), + advertisementPath: args.advertisement, + capabilityManifestPath: args.capability, + stamp, + width: Number(args.width || 80), + }); + writeTuiCaptureArtifact(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(); +} diff --git a/tools/evetui/evetui-capture-artifact.test.mjs b/tools/evetui/evetui-capture-artifact.test.mjs new file mode 100644 index 0000000..4371d00 --- /dev/null +++ b/tools/evetui/evetui-capture-artifact.test.mjs @@ -0,0 +1,33 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; +import { buildTuiCaptureArtifact } from "./evetui-capture-artifact.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("../../runtimes/incubating/eve-tui/eve-runtime-capability.json", import.meta.url), "utf8")); +const surfaceDocument = JSON.parse(readFileSync(new URL("../../web/fixtures/aetheria-world-surface.json", import.meta.url), "utf8")); + +test("builds TUI JSON grid capture artifact from provider advertisement and surface", () => { + const { request, grid } = buildTuiCaptureArtifact({ + advertisement, + capabilityManifest, + surfaceDocument, + advertisementPath: "web/fixtures/aetheria.provider-advertisement.json", + capabilityManifestPath: "runtimes/incubating/eve-tui/eve-runtime-capability.json", + stamp: "smoke", + width: 72, + }); + + assert.equal(request.artifactKind, "json-grid"); + assert.equal(request.artifactPath, "artifacts/evetui-capture/smoke/tui-grid.json"); + assert.equal(grid.schema, "gamecult.eve.tui_grid.v1"); + assert.equal(grid.runtimeId, "tui"); + assert.equal(grid.providerId, "aetheria"); + assert.equal(grid.surfaceId, "aetheria.daemon.game"); + assert.equal(grid.commandBoundary, "aetheria.daemon.commands"); + assert.equal(grid.receiptSchema, "aetheria.eve_command_acceptance_status.v1"); + assert.equal(grid.artifactKind, "json-grid"); + assert.equal(grid.captureKind, "terminal-cell-grid"); + assert.equal(grid.width, 72); + assert.ok(grid.lines.some(line => line.includes("aetheria.daemon.game"))); +}); diff --git a/tools/evetui/evetui-capture-contract.test.mjs b/tools/evetui/evetui-capture-contract.test.mjs index 58ba6f4..fde68f8 100644 --- a/tools/evetui/evetui-capture-contract.test.mjs +++ b/tools/evetui/evetui-capture-contract.test.mjs @@ -24,9 +24,9 @@ test("builds TUI capture request from provider advertisement", () => { 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.captureKind, "terminal-transcript-or-cell-grid"); - assert.equal(request.artifactKind, "ansi-transcript-or-json-grid"); - assert.equal(request.artifactPath, "artifacts/evetui-capture/smoke/tui-transcript.ansi"); + assert.equal(request.captureKind, "terminal-cell-grid"); + assert.equal(request.artifactKind, "json-grid"); + assert.equal(request.artifactPath, "artifacts/evetui-capture/smoke/tui-grid.json"); assert.match(request.authority, /provider state/); }); diff --git a/tools/parity/parity-manifest.json b/tools/parity/parity-manifest.json index 9d81e53..a4eb6ff 100644 --- a/tools/parity/parity-manifest.json +++ b/tools/parity/parity-manifest.json @@ -716,7 +716,7 @@ ], "pendingProofs": [ "TUI runtime body graduates to EveTui outside provider product code.", - "TUI terminal transcript or cell-grid captures are attached to conformance output." + "TUI terminal transcript or cell-grid captures are attached to conformance output from the EveTui owner repo." ] } ], @@ -868,9 +868,12 @@ "scripts/run-evetui-lifecycle-smoke.ps1", "scripts/run-evetui-provider-shell-smoke.ps1", "scripts/run-evetui-capture-contract-smoke.ps1", + "scripts/run-evetui-capture-smoke.ps1", "scripts/run-evetui-release-contract-smoke.ps1", "tools/evetui/evetui-capture-contract.mjs", "tools/evetui/evetui-capture-contract.test.mjs", + "tools/evetui/evetui-capture-artifact.mjs", + "tools/evetui/evetui-capture-artifact.test.mjs", "tools/evetui/evetui-release-contract.mjs", "tools/evetui/evetui-release-contract.test.mjs" ], @@ -885,11 +888,19 @@ }, { "path": "tools/evetui/evetui-capture-contract.mjs", - "contains": ["buildTuiCaptureRequest", "gamecult.eve.runtime_capture_request.v1", "ansi-transcript-or-json-grid", "loweringTargets", "writeCaptureRequest"] + "contains": ["buildTuiCaptureRequest", "gamecult.eve.runtime_capture_request.v1", "artifactKind", "loweringTargets", "writeCaptureRequest"] }, { "path": "tools/evetui/evetui-capture-contract.test.mjs", - "contains": ["builds TUI capture request from provider advertisement", "rejects capture request when the surface does not advertise TUI", "artifacts/evetui-capture/smoke/tui-transcript.ansi"] + "contains": ["builds TUI capture request from provider advertisement", "rejects capture request when the surface does not advertise TUI", "artifacts/evetui-capture/smoke/tui-grid.json"] + }, + { + "path": "tools/evetui/evetui-capture-artifact.mjs", + "contains": ["buildTuiCaptureArtifact", "EveTuiShell", "gamecult.eve.tui_grid.v1", "writeTuiCaptureArtifact", "request-output"] + }, + { + "path": "tools/evetui/evetui-capture-artifact.test.mjs", + "contains": ["builds TUI JSON grid capture artifact", "gamecult.eve.tui_grid.v1", "artifacts/evetui-capture/smoke/tui-grid.json"] }, { "path": "tools/evetui/evetui-release-contract.mjs", @@ -901,8 +912,19 @@ } ], "capture": { - "status": "missing", - "note": "TUI transcript or cell-grid capture is a split blocker until EveTui attaches durable terminal artifacts." + "status": "json-grid", + "note": "TUI attaches a provider-advertisement-backed gamecult.eve.tui_grid.v1 JSON capture artifact; packaged runtime ownership remains an EveTui split blocker.", + "artifacts": [ + { + "kind": "json-grid", + "schema": "gamecult.eve.tui_grid.v1", + "path": "artifacts/evetui-capture/latest/tui-grid.json", + "providerId": "aetheria", + "surfaceId": "aetheria.daemon.game", + "sourceSurfacePath": "web/fixtures/aetheria-world-surface.json", + "requestPath": "artifacts/evetui-capture/latest/capture-request.json" + } + ] } }, { diff --git a/tools/parity/run-parity.mjs b/tools/parity/run-parity.mjs index ea034f9..c7979bb 100644 --- a/tools/parity/run-parity.mjs +++ b/tools/parity/run-parity.mjs @@ -318,6 +318,8 @@ async function evaluateRuntime(runtime, fixtureResults) { const capabilityManifestDocument = await readRuntimeCapabilityManifestDocument(runtime); const lifecycleManifestDocument = await readRuntimeLifecycleManifestDocument(runtime); const lifecycle = runtime.lifecycle || capabilityManifestDocument?.lifecycle || readLifecycleStages(lifecycleManifestDocument); + const capture = runtime.capture || capabilityManifestDocument?.capture || null; + const captureArtifactErrors = validateRuntimeCaptureArtifacts(capture); const lifecycleErrors = await validateRuntimeLifecycle(runtime, lifecycle, lifecycleManifestDocument); const splitHandoffPath = await readRuntimeSplitHandoffPath(runtime); const splitHandoffErrors = await validateRuntimeSplitHandoff(runtime, splitHandoffPath); @@ -333,6 +335,7 @@ async function evaluateRuntime(runtime, fixtureResults) { if (runtime.kind === "active" && commandTransportSmokeErrors.length) status = "missing-command-transport-smoke"; if (runtime.kind === "active" && capabilityManifestErrors.length) status = "invalid-runtime-capability"; if (runtime.kind === "active" && lifecycleErrors.length) status = "invalid-runtime-lifecycle"; + if (runtime.kind === "active" && captureArtifactErrors.length) status = "invalid-runtime-capture-artifact"; if (runtime.kind === "active" && splitHandoffErrors.length) status = "invalid-runtime-split-handoff"; if (runtime.kind === "active" && localProviderCatalogErrors.length) status = "invalid-local-provider-catalog"; if (runtime.kind === "active" && missingIncubationFields.length) status = "missing-incubation-metadata"; @@ -378,6 +381,7 @@ async function evaluateRuntime(runtime, fixtureResults) { localProviderCatalog: runtime.localProviderCatalog || null, localProviderCatalogErrors, lifecycle, + captureArtifactErrors, ownerRepo: runtime.ownerRepo || "", repoRole: runtime.repoRole || "", graduationTrigger: runtime.graduationTrigger || "", @@ -387,7 +391,7 @@ async function evaluateRuntime(runtime, fixtureResults) { activationCriteria: runtime.activationCriteria || [], missingIncubationFields, pluginCapabilityGaps, - capture: runtime.capture, + capture, }; } @@ -1914,6 +1918,8 @@ function buildConformanceExport(report) { lifecycleManifestPath: runtime.lifecycleManifest?.manifestPath || "", commandTransportSchema: runtimeCommandTransportSchema(runtime), captureStatus: runtime.capture?.status || "", + captureArtifacts: runtime.capture?.artifacts || [], + captureArtifactErrors: runtime.captureArtifactErrors || [], lifecycle: runtime.lifecycle, lifecycleErrors: runtime.lifecycleErrors || [], missingEvidence: [ @@ -1923,6 +1929,7 @@ function buildConformanceExport(report) { ...runtime.commandTransportSmokeErrors.map(id => `command-smoke:${id}`), ...runtime.capabilityManifestErrors.map(id => `runtime-capability:${id}`), ...runtime.lifecycleErrors.map(id => `runtime-lifecycle:${id}`), + ...runtime.captureArtifactErrors.map(id => `runtime-capture:${id}`), ...runtime.localProviderCatalogErrors.map(id => `local-provider-catalog:${id}`), ], })) @@ -2013,6 +2020,8 @@ function buildConformanceExport(report) { splitHandoffExportPath: makeHandoffExportPath("runtime", runtime.id, runtime.splitHandoffPath || ""), commandTransportSchema: runtimeCommandTransportSchema(runtime), captureStatus: runtime.capture?.status || "", + captureArtifacts: runtime.capture?.artifacts || [], + captureArtifactErrors: runtime.captureArtifactErrors || [], ...(runtime.lifecycle ? { lifecycle: runtime.lifecycle } : {}), worldSurfaceLowering: runtime.worldSurfaceLowering || [], })), @@ -2075,6 +2084,7 @@ function buildCapabilityMatrix(report) { worldSurfaceLowering: runtime.worldSurfaceLowering || [], commandTransportSchema: runtimeCommandTransportSchema(runtime), captureStatus: runtime.capture?.status || "", + captureArtifacts: runtime.capture?.artifacts || [], splitHandoffExportPath: makeHandoffExportPath("runtime", runtime.id, runtime.splitHandoffPath || ""), })); @@ -2184,6 +2194,7 @@ function collectCapabilityGaps(report) { ...(runtime.commandTransportSmokeErrors || []).map(id => `command-smoke:${id}`), ...(runtime.capabilityManifestErrors || []).map(id => `runtime-capability:${id}`), ...(runtime.lifecycleErrors || []).map(id => `runtime-lifecycle:${id}`), + ...(runtime.captureArtifactErrors || []).map(id => `runtime-capture:${id}`), ...(runtime.splitHandoffErrors || []).map(id => `runtime-split-handoff:${id}`), ...(runtime.localProviderCatalogErrors || []).map(id => `local-provider-catalog:${id}`), ...(runtime.pluginCapabilityGaps || []).map(id => `plugin-capability:${id}`), @@ -2545,6 +2556,29 @@ function buildBoundingBoxMetric(runtime, fixture) { } const captureStatus = runtime.capture?.status || ""; + if (captureStatus === "json-grid") { + const artifact = (runtime.capture?.artifacts || []).find(candidate => candidate.kind === "json-grid"); + const errors = runtime.captureArtifactErrors || []; + return buildScreenshotMetric(runtime, fixture, { + metricKind: "bounding-boxes", + status: errors.length ? "fail" : "terminal-grid-capture", + score: errors.length ? 0 : 1, + expected: { + source: "runtime terminal grid capture", + schema: artifact?.schema || "gamecult.eve.tui_grid.v1", + path: artifact?.path || "", + }, + actual: { + captureStatus, + artifactKind: artifact?.kind || "", + errors, + }, + evidenceLayer: "runtime-terminal-grid-capture", + detail: errors.length + ? "Declared terminal grid capture artifact is missing or invalid." + : "Terminal grid capture is present; graphical bounding boxes are not a TUI invariant.", + }); + } const runtimeHasCaptureBody = ["chrome-headless", "ssh-png", "adb-png", "golden", "ssh-golden"].includes(captureStatus); let status = "pass"; let score = 1; @@ -2645,6 +2679,54 @@ function readRuntimeLayoutProbe(runtime, fixtureId) { return { config, document, errors }; } +function validateRuntimeCaptureArtifacts(capture) { + const artifacts = Array.isArray(capture?.artifacts) ? capture.artifacts : []; + const errors = []; + for (const artifact of artifacts) { + const artifactPath = artifact.path || ""; + if (!artifactPath) { + errors.push("artifact.path:missing"); + continue; + } + const absoluteArtifactPath = path.join(repoRoot, artifactPath); + if (!existsSync(absoluteArtifactPath)) { + errors.push(`${artifactPath}:missing`); + continue; + } + let document; + try { + document = JSON.parse(readFileSync(absoluteArtifactPath, "utf8")); + } catch (error) { + errors.push(`${artifactPath}:invalid-json:${error instanceof Error ? error.message : String(error)}`); + continue; + } + for (const [field, expected] of [ + ["schema", artifact.schema], + ["runtimeId", artifact.runtimeId], + ["providerId", artifact.providerId], + ["surfaceId", artifact.surfaceId], + ]) { + if (expected && document[field] !== expected) { + errors.push(`${artifactPath}:${field}:expected ${expected} got ${document[field] || ""}`); + } + } + if (artifact.schema && manifest.schemas?.[artifact.schema]) { + try { + const schema = JSON.parse(readFileSync(path.join(repoRoot, manifest.schemas[artifact.schema]), "utf8")); + errors.push(...validateSchemaSubset(schema, document, `captureArtifact:${artifactPath}`)); + } catch (error) { + errors.push(`${manifest.schemas[artifact.schema]}:unreadable:${error instanceof Error ? error.message : String(error)}`); + } + } + for (const linkedPath of [artifact.sourceSurfacePath, artifact.requestPath].filter(Boolean)) { + if (!existsSync(path.join(repoRoot, linkedPath))) { + errors.push(`${artifactPath}:linked-path:${linkedPath}:missing`); + } + } + } + return errors; +} + function collectSplitTargetBlockers(report) { return (report.splitTargets || []).flatMap(target => target.blockerRecords || []); }