diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index 2f095a0..548db21 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -614,6 +614,14 @@ Recently cut: `artifacts/eveelectron-capture/latest/electron-shell-projection.json`, and exports it as `runtime.captureArtifacts[]`. The actual packaged window PNG remains a split blocker for EveElectron. +- Runtime capture probe coverage is now exported as + `runtimeCaptureProbeCoverage[]`. It records the lifecycle capture contract, + capture owner, requested artifact kind, current semantic/cell-grid artifact, + request path, pending proofs, and validation errors for each runtime with + capture evidence. The consumer smokes assert that Unity UI Toolkit, Unity + scene, and Electron currently have `json-projection` semantic artifacts while + their contracted `png` captures remain owner-repo work; TUI's current + `json-grid` artifact satisfies its contract but still needs EveTui ownership. - 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`, diff --git a/docs/parity-testing-harness.md b/docs/parity-testing-harness.md index 109cf0a..9a68ae9 100644 --- a/docs/parity-testing-harness.md +++ b/docs/parity-testing-harness.md @@ -454,6 +454,14 @@ It records scene graph projection, embedded slots, and sidecar plugin projection metadata. The remaining blocker is Unity screenshot or frame-capture PNG production from EveUnity. +Runtime capture probe coverage is exported as `runtimeCaptureProbeCoverage[]`. +Each record joins runtime status, capture owner, lifecycle capture contract, +current semantic/cell-grid artifact, pending proofs, and artifact errors. This +keeps `json-projection` evidence useful without letting it impersonate the +contracted `png` artifact for Unity or Electron. Runtime-owner consumers can +assert the ledger with +`--expect-runtime-capture-probe `. + EveUnity split handoff evidence: ```powershell @@ -579,7 +587,8 @@ Pending runtimes are allowed. Silent fake parity is not. ## Next Cuts 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. + blockers with actual owner-produced image artifacts 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/schemas/gamecult.eve.conformance_export.v1.schema.json b/schemas/gamecult.eve.conformance_export.v1.schema.json index 5857394..716eec5 100644 --- a/schemas/gamecult.eve.conformance_export.v1.schema.json +++ b/schemas/gamecult.eve.conformance_export.v1.schema.json @@ -21,6 +21,7 @@ "worldSurfaceLoweringCoverage", "commandBoundaryCoverage", "screenshotComparisonMetrics", + "runtimeCaptureProbeCoverage", "splitTargetBlockers", "splitHandoffMoveCoverage", "pluginHandoffMoveCoverage", @@ -820,6 +821,118 @@ "additionalProperties": true } }, + "runtimeCaptureProbeCoverage": { + "type": "array", + "items": { + "type": "object", + "required": [ + "runtimeId", + "runtimeStatus", + "runtimeOwnerRepo", + "splitTarget", + "captureStatus", + "captureOwnerRepo", + "contractArtifactKind", + "currentArtifactKind", + "currentArtifactPath", + "pendingProofs", + "errors", + "status", + "severity", + "detail" + ], + "properties": { + "runtimeId": { + "type": "string", + "minLength": 1 + }, + "runtimeStatus": { + "type": "string" + }, + "runtimeOwnerRepo": { + "type": "string", + "minLength": 1 + }, + "splitTarget": { + "type": "string" + }, + "captureStatus": { + "type": "string" + }, + "lifecycleStatus": { + "type": "string" + }, + "captureOwnerRepo": { + "type": "string", + "minLength": 1 + }, + "captureKind": { + "type": "string" + }, + "contractArtifactKind": { + "type": "string" + }, + "contractRequestSchema": { + "type": "string" + }, + "requestBuilder": { + "type": "string" + }, + "advertisementPath": { + "type": "string" + }, + "artifactPattern": { + "type": "string" + }, + "conformanceAttachment": { + "type": "string" + }, + "requiredProvider": { + "type": "string" + }, + "requiredSurface": { + "type": "string" + }, + "authority": { + "type": "string" + }, + "currentArtifactKind": { + "type": "string" + }, + "currentArtifactSchema": { + "type": "string" + }, + "currentArtifactPath": { + "type": "string" + }, + "requestPath": { + "type": "string" + }, + "sourceSurfacePath": { + "type": "string" + }, + "pendingProofs": { + "type": "array" + }, + "errors": { + "type": "array" + }, + "status": { + "type": "string", + "minLength": 1 + }, + "severity": { + "type": "string", + "minLength": 1 + }, + "detail": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": true + } + }, "plugins": { "type": "array", "items": { diff --git a/scripts/run-conformance-consumer-smoke.ps1 b/scripts/run-conformance-consumer-smoke.ps1 index 7da72ea..1bae901 100644 --- a/scripts/run-conformance-consumer-smoke.ps1 +++ b/scripts/run-conformance-consumer-smoke.ps1 @@ -79,6 +79,10 @@ try { --expect-screenshot-metric web:sai-vn:color-tokens:pass ` --expect-screenshot-metric web:sai-vn:bounding-boxes:pass ` --expect-screenshot-metric electron-shell:aetheria-world:bounding-boxes:pending-capture ` + --expect-runtime-capture-probe unity-uitoolkit:semantic-artifact-present-capture-pending:png:EveUnity ` + --expect-runtime-capture-probe unity-scene:semantic-artifact-present-capture-pending:png:EveUnity ` + --expect-runtime-capture-probe electron-shell:semantic-artifact-present-capture-pending:png:EveElectron ` + --expect-runtime-capture-probe tui:contract-artifact-present:json-grid:EveTui ` --expect-capability-gap "split-target:EveUnity:EveUnity:proof:Tagged UPM release" ` --expect-conformance-handoff if ($LASTEXITCODE -ne 0) { diff --git a/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 b/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 index 08358b6..a190748 100644 --- a/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 +++ b/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 @@ -142,6 +142,10 @@ node $consumerScript $consumerExport ` --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-runtime-capture-probe unity-uitoolkit:semantic-artifact-present-capture-pending:png:EveUnity ` + --expect-runtime-capture-probe unity-scene:semantic-artifact-present-capture-pending:png:EveUnity ` + --expect-runtime-capture-probe electron-shell:semantic-artifact-present-capture-pending:png:EveElectron ` + --expect-runtime-capture-probe tui:contract-artifact-present:json-grid:EveTui ` --expect-screenshot-metric web:embedded-surface:structure:pass ` --expect-screenshot-metric web:embedded-surface:color-tokens:pass ` --expect-screenshot-metric web:embedded-surface:text-presence:pass ` diff --git a/tools/conformance/consume-export.mjs b/tools/conformance/consume-export.mjs index 30826fe..45f2bce 100644 --- a/tools/conformance/consume-export.mjs +++ b/tools/conformance/consume-export.mjs @@ -29,6 +29,7 @@ if (!exportDirectory) { " --expect-command-boundary-coverage ", " --expect-world-lowering-gap ", " --expect-screenshot-metric ", + " --expect-runtime-capture-probe ", " --expect-conformance-handoff", " --expect-plugin-operation ", " --expect-plugin-capability ", @@ -151,6 +152,7 @@ function validateIndex(index, directory, expectations, errors) { const worldSurfaceLoweringCoverage = Array.isArray(index.worldSurfaceLoweringCoverage) ? index.worldSurfaceLoweringCoverage : []; const commandBoundaryCoverage = Array.isArray(index.commandBoundaryCoverage) ? index.commandBoundaryCoverage : []; const screenshotComparisonMetrics = Array.isArray(index.screenshotComparisonMetrics) ? index.screenshotComparisonMetrics : []; + const runtimeCaptureProbeCoverage = Array.isArray(index.runtimeCaptureProbeCoverage) ? index.runtimeCaptureProbeCoverage : []; const splitTargetBlockers = Array.isArray(index.splitTargetBlockers) ? index.splitTargetBlockers : []; const splitHandoffMoveCoverage = Array.isArray(index.splitHandoffMoveCoverage) ? index.splitHandoffMoveCoverage : []; const pluginHandoffMoveCoverage = Array.isArray(index.pluginHandoffMoveCoverage) ? index.pluginHandoffMoveCoverage : []; @@ -186,6 +188,7 @@ function validateIndex(index, directory, expectations, errors) { validateWorldSurfaceLoweringCoverage(index.worldSurfaceLoweringCoverage, errors); validateCommandBoundaryCoverage(index.commandBoundaryCoverage, errors); validateScreenshotComparisonMetrics(index.screenshotComparisonMetrics, errors); + validateRuntimeCaptureProbeCoverage(index.runtimeCaptureProbeCoverage, errors); validateWorldSurfaceLoweringGaps(index.worldSurfaceLoweringGaps, errors); for (const expectedGap of expectations.capabilityGaps) { if (!capabilityGaps.some(gap => capabilityGapText(gap).includes(expectedGap))) { @@ -339,6 +342,22 @@ function validateIndex(index, directory, expectations, errors) { errors.push(`screenshotComparisonMetrics:${expectation.runtimeId}:${expectation.fixtureId}:${expectation.metricKind}:status:expected ${expectation.status} got ${metric.status || ""}`); } } + for (const expectation of expectations.runtimeCaptureProbes) { + const record = runtimeCaptureProbeCoverage.find(candidate => candidate.runtimeId === expectation.runtimeId); + if (!record) { + errors.push(`runtimeCaptureProbeCoverage:${expectation.runtimeId}:missing`); + continue; + } + if (record.status !== expectation.status) { + errors.push(`runtimeCaptureProbeCoverage:${expectation.runtimeId}:status:expected ${expectation.status} got ${record.status || ""}`); + } + if (record.contractArtifactKind !== expectation.contractArtifactKind) { + errors.push(`runtimeCaptureProbeCoverage:${expectation.runtimeId}:contractArtifactKind:expected ${expectation.contractArtifactKind} got ${record.contractArtifactKind || ""}`); + } + if (record.captureOwnerRepo !== expectation.captureOwnerRepo) { + errors.push(`runtimeCaptureProbeCoverage:${expectation.runtimeId}:captureOwnerRepo:expected ${expectation.captureOwnerRepo} got ${record.captureOwnerRepo || ""}`); + } + } for (const expectedFixture of expectations.fixtures) { if (!fixtureIds.has(expectedFixture)) errors.push(`fixture:${expectedFixture}:missing`); @@ -1030,6 +1049,31 @@ function validateScreenshotComparisonMetrics(records, errors) { } } +function validateRuntimeCaptureProbeCoverage(records, errors) { + if (!Array.isArray(records)) { + errors.push("runtimeCaptureProbeCoverage:missing"); + return; + } + if (!records.length) { + errors.push("runtimeCaptureProbeCoverage:empty"); + return; + } + for (const [index, record] of records.entries()) { + for (const field of ["runtimeId", "runtimeOwnerRepo", "captureOwnerRepo", "status", "severity", "detail"]) { + if (!record?.[field]) errors.push(`runtimeCaptureProbeCoverage:${index}:${field}:missing`); + } + for (const field of ["pendingProofs", "errors"]) { + if (!Array.isArray(record?.[field])) errors.push(`runtimeCaptureProbeCoverage:${index}:${field}:expected array`); + } + if (record?.currentArtifactPath && !record.currentArtifactKind) { + errors.push(`runtimeCaptureProbeCoverage:${index}:currentArtifactKind:missing`); + } + if (record?.contractArtifactKind && !record.contractRequestSchema) { + errors.push(`runtimeCaptureProbeCoverage:${index}:contractRequestSchema:missing`); + } + } +} + function validateWorldSurfaceLoweringGaps(gaps, errors) { if (!Array.isArray(gaps)) { errors.push("worldSurfaceLoweringGaps:missing"); @@ -1261,6 +1305,7 @@ function parseArguments(args) { commandBoundaryCoverage: [], worldLoweringGaps: [], screenshotMetrics: [], + runtimeCaptureProbes: [], conformanceHandoff: false, }; const optionTargets = new Map([ @@ -1318,6 +1363,7 @@ function parseArguments(args) { ["--expect-command-boundary-coverage", expectations.commandBoundaryCoverage], ["--expect-world-lowering-gap", expectations.worldLoweringGaps], ["--expect-screenshot-metric", expectations.screenshotMetrics], + ["--expect-runtime-capture-probe", expectations.runtimeCaptureProbes], ]); for (let index = 1; index < args.length; index += 1) { @@ -1418,6 +1464,8 @@ function parseArguments(args) { target.push(parseCommandBoundaryCoverageExpectation(value)); } else if (option === "--expect-screenshot-metric") { target.push(parseScreenshotMetricExpectation(value)); + } else if (option === "--expect-runtime-capture-probe") { + target.push(parseRuntimeCaptureProbeExpectation(value)); } else if (option === "--expect-runtime-plugin-gap") { target.push(parseRuntimePluginGapExpectation(value)); } else if (option === "--expect-runtime-plugin-projection") { @@ -1453,6 +1501,20 @@ function parseScreenshotMetricExpectation(value) { }; } +function parseRuntimeCaptureProbeExpectation(value) { + const parts = value.split(":"); + if (parts.length !== 4 || parts.some(part => !part)) { + console.error(`Expected runtime capture probe in form, got: ${value}`); + process.exit(2); + } + return { + runtimeId: parts[0], + status: parts[1], + contractArtifactKind: parts[2], + captureOwnerRepo: parts[3], + }; +} + function parseLocalProviderCatalogExpectation(value) { const parts = value.split(":"); if (parts.length !== 3 || parts.some(part => !part)) { diff --git a/tools/parity/run-parity.mjs b/tools/parity/run-parity.mjs index 72e5eba..b14ed7d 100644 --- a/tools/parity/run-parity.mjs +++ b/tools/parity/run-parity.mjs @@ -2003,6 +2003,7 @@ function buildConformanceExport(report) { worldSurfaceLoweringCoverage: collectWorldSurfaceLoweringCoverage(report), commandBoundaryCoverage: collectCommandBoundaryCoverage(report), screenshotComparisonMetrics: collectScreenshotComparisonMetrics(report), + runtimeCaptureProbeCoverage: collectRuntimeCaptureProbeCoverage(report), worldSurfaceLoweringGaps: collectWorldSurfaceLoweringGaps(report), splitTargetBlockers: collectSplitTargetBlockers(report), splitHandoffMoveCoverage: collectSplitHandoffMoveCoverage(report), @@ -2560,6 +2561,94 @@ function collectScreenshotComparisonMetrics(report) { `${left.runtimeId}:${left.fixtureId}:${left.metricKind}`.localeCompare(`${right.runtimeId}:${right.fixtureId}:${right.metricKind}`)); } +function collectRuntimeCaptureProbeCoverage(report) { + return (report.runtimes || []) + .filter(runtime => (runtime.capture?.artifacts || []).length || runtime.lifecycle?.capture?.captureContract) + .map(runtime => buildRuntimeCaptureProbeRecord(runtime)) + .sort((left, right) => left.runtimeId.localeCompare(right.runtimeId)); +} + +function buildRuntimeCaptureProbeRecord(runtime) { + const capture = runtime.capture || {}; + const lifecycleCapture = runtime.lifecycle?.capture || {}; + const contract = lifecycleCapture.captureContract || {}; + const artifacts = Array.isArray(capture.artifacts) ? capture.artifacts : []; + const artifact = selectCaptureArtifactForContract(artifacts, contract) || artifacts[0] || {}; + const errors = runtime.captureArtifactErrors || []; + const contractArtifactKind = contract.artifactKind || ""; + const currentArtifactKind = artifact.kind || ""; + const hasContract = Boolean(contractArtifactKind || contract.captureKind || contract.requestSchema); + const artifactPresent = Boolean(currentArtifactKind && artifact.path); + const contractArtifactPresent = hasContract && artifactPresent && currentArtifactKind === contractArtifactKind && !errors.length; + const semanticArtifactOnly = hasContract && artifactPresent && currentArtifactKind !== contractArtifactKind && !errors.length; + + let status = "missing-capture-contract"; + let severity = "blocker"; + let detail = "Runtime has capture evidence but no lifecycle capture contract."; + if (contractArtifactPresent) { + status = "contract-artifact-present"; + severity = "info"; + detail = "Runtime capture artifact satisfies the lifecycle capture contract."; + } else if (semanticArtifactOnly) { + status = "semantic-artifact-present-capture-pending"; + severity = contractArtifactKind === "png" ? "blocker" : "gap"; + detail = `Runtime publishes ${currentArtifactKind} semantic evidence; ${contractArtifactKind || "contract"} capture remains owner-repo work.`; + } else if (hasContract && errors.length) { + status = "invalid-current-artifact"; + severity = "blocker"; + detail = "Runtime capture artifact is declared but invalid or missing."; + } else if (hasContract && !artifactPresent) { + status = "contract-artifact-missing"; + severity = "blocker"; + detail = "Runtime lifecycle capture contract exists, but no capture artifact is attached."; + } + + return { + runtimeId: runtime.id, + runtimeStatus: runtime.status, + runtimeOwnerRepo: runtime.ownerRepo || "", + splitTarget: runtime.splitTarget || "", + captureStatus: capture.status || "", + lifecycleStatus: lifecycleCapture.status || "", + captureOwnerRepo: contract.ownerRepo || lifecycleCapture.ownerRepo || runtime.splitTarget || runtime.ownerRepo || "", + captureKind: contract.captureKind || "", + contractArtifactKind, + contractRequestSchema: contract.requestSchema || "", + requestBuilder: contract.requestBuilder || "", + advertisementPath: contract.advertisementPath || "", + artifactPattern: contract.artifactPattern || "", + conformanceAttachment: contract.conformanceAttachment || "", + requiredProvider: contract.requiredProvider || "", + requiredSurface: contract.requiredSurface || "", + authority: contract.authority || "", + currentArtifactKind, + currentArtifactSchema: artifact.schema || "", + currentArtifactPath: artifact.path || "", + requestPath: artifact.requestPath || "", + sourceSurfacePath: artifact.sourceSurfacePath || "", + pendingProofs: lifecycleCapture.pendingProofs || [], + errors, + status, + severity, + detail, + }; +} + +function selectCaptureArtifactForContract(artifacts, contract) { + if (!artifacts.length) return null; + const requiredProvider = contract.requiredProvider || ""; + const requiredSurface = contract.requiredSurface || ""; + const contractArtifactKind = contract.artifactKind || ""; + return artifacts.find(artifact => + (!requiredProvider || artifact.providerId === requiredProvider) + && (!requiredSurface || artifact.surfaceId === requiredSurface) + && (!contractArtifactKind || artifact.kind === contractArtifactKind)) + || artifacts.find(artifact => + (!requiredProvider || artifact.providerId === requiredProvider) + && (!requiredSurface || artifact.surfaceId === requiredSurface)) + || null; +} + function buildStructureMetric(runtime, fixture) { const checks = (fixture.checks || []).filter(check => check.id.startsWith("component:")