diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index f1ad25e..8afa1b7 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -437,6 +437,11 @@ Recently cut: wrapped and validated through the same schema, and the conformance export carries `lifecycleManifestPath` plus `lifecycleErrors` so runtime owners can consume lifecycle status without guessing from prose. +- Electron and TUI provider-shell outputs are now schema-backed as + `gamecult.eve.electron_shell_projection.v1` and + `gamecult.eve.tui_grid.v1`. Their runtime tests load the schema documents and + validate actual lowered projection objects, so Electron and TUI cannot claim + generic provider-shell lowering by string constant alone. - EveElectron now has a pending split target and handoff at `runtimes/incubating/eve-electron/eveelectron-split-handoff.json`. It does claim a provider-agnostic Electron shell surface-tree projection and command diff --git a/docs/parity-testing-harness.md b/docs/parity-testing-harness.md index dc7fd79..9f177bf 100644 --- a/docs/parity-testing-harness.md +++ b/docs/parity-testing-harness.md @@ -228,6 +228,10 @@ Runtime lifecycle records use `gamecult.eve.runtime_lifecycle.v1`. Standalone lifecycle documents such as `flutter/eve_parity/eveflutter-lifecycle.json` and embedded runtime capability lifecycle blocks validate against the same stage shape, and exported runtime records carry `lifecycleErrors`. +Runtime shell output contracts are schema-backed too: +`gamecult.eve.electron_shell_projection.v1` covers the Electron shell tree, and +`gamecult.eve.tui_grid.v1` covers the compact terminal grid. The Electron and +TUI runtime tests validate real lowered objects against those schemas. After generating the parity report, `run-parity-harness.ps1` copies `artifacts/conformance/latest` into diff --git a/runtimes/incubating/eve-electron/test/eve-electron-shell.test.mjs b/runtimes/incubating/eve-electron/test/eve-electron-shell.test.mjs index 0047db7..d038351 100644 --- a/runtimes/incubating/eve-electron/test/eve-electron-shell.test.mjs +++ b/runtimes/incubating/eve-electron/test/eve-electron-shell.test.mjs @@ -1,7 +1,17 @@ import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import path from "node:path"; import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { validateSchemaSubset } from "../../test-support/schema-subset.mjs"; import { EveElectronShell } from "../src/eve-electron-shell.mjs"; +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../.."); +const electronProjectionSchema = JSON.parse(readFileSync( + path.join(repoRoot, "schemas/gamecult.eve.electron_shell_projection.v1.schema.json"), + "utf8", +)); + test("selects the active advertised provider surface", () => { const shell = new EveElectronShell(); const selected = shell.selectSurface(advertisement(), "aetheria.daemon.editor"); @@ -40,6 +50,7 @@ test("lowers provider surface trees into an Electron shell projection", () => { const shell = new EveElectronShell(); const projection = shell.lowerSurface(surfaceDocument(), advertisement(), "aetheria.daemon.game"); + assert.deepEqual(validateSchemaSubset(electronProjectionSchema, projection), []); assert.equal(projection.schema, "gamecult.eve.electron_shell_projection.v1"); assert.equal(projection.providerId, "aetheria"); assert.equal(projection.surfaceId, "aetheria.daemon.game"); diff --git a/runtimes/incubating/eve-tui/test/eve-tui-shell.test.mjs b/runtimes/incubating/eve-tui/test/eve-tui-shell.test.mjs index d480395..3be97a0 100644 --- a/runtimes/incubating/eve-tui/test/eve-tui-shell.test.mjs +++ b/runtimes/incubating/eve-tui/test/eve-tui-shell.test.mjs @@ -1,7 +1,17 @@ import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import path from "node:path"; import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { validateSchemaSubset } from "../../test-support/schema-subset.mjs"; import { EveTuiShell } from "../src/eve-tui-shell.mjs"; +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../.."); +const tuiGridSchema = JSON.parse(readFileSync( + path.join(repoRoot, "schemas/gamecult.eve.tui_grid.v1.schema.json"), + "utf8", +)); + test("selects advertised provider surfaces for compact projection", () => { const shell = new EveTuiShell({ width: 48 }); const selected = shell.selectSurface(advertisement(), "aetheria.daemon.editor"); @@ -40,6 +50,7 @@ test("summary grid is explicitly lossy provider-shell evidence", () => { const shell = new EveTuiShell({ width: 36 }); const grid = shell.renderSummary(advertisement(), "aetheria.daemon.game"); + assert.deepEqual(validateSchemaSubset(tuiGridSchema, grid), []); assert.equal(grid.schema, "gamecult.eve.tui_grid.v1"); assert.equal(grid.runtimeId, "tui"); assert.equal(grid.surfaceId, "aetheria.daemon.game"); @@ -51,6 +62,7 @@ test("lowers provider surface trees into a terminal grid", () => { const shell = new EveTuiShell({ width: 44 }); const grid = shell.lowerSurface(surfaceDocument(), advertisement(), "aetheria.daemon.game"); + assert.deepEqual(validateSchemaSubset(tuiGridSchema, grid), []); assert.equal(grid.schema, "gamecult.eve.tui_grid.v1"); assert.equal(grid.runtimeId, "tui"); assert.equal(grid.providerId, "aetheria"); diff --git a/runtimes/incubating/test-support/schema-subset.mjs b/runtimes/incubating/test-support/schema-subset.mjs new file mode 100644 index 0000000..e2484f8 --- /dev/null +++ b/runtimes/incubating/test-support/schema-subset.mjs @@ -0,0 +1,53 @@ +export function validateSchemaSubset(schema, value, pointer = "$") { + const errors = []; + if (!schema || typeof schema !== "object") return errors; + + if (schema.const !== undefined && value !== schema.const) { + errors.push(`${pointer}:const:${schema.const}`); + } + if (schema.type && !matchesSchemaType(value, schema.type)) { + errors.push(`${pointer}:type:${schema.type}`); + return errors; + } + if (schema.minLength !== undefined && typeof value === "string" && value.length < schema.minLength) { + errors.push(`${pointer}:minLength:${schema.minLength}`); + } + if (schema.minItems !== undefined && Array.isArray(value) && value.length < schema.minItems) { + errors.push(`${pointer}:minItems:${schema.minItems}`); + } + if (schema.required && value && typeof value === "object" && !Array.isArray(value)) { + for (const key of schema.required) { + if (value[key] === undefined) errors.push(`${pointer}.${key}:required`); + } + } + if (schema.properties && value && typeof value === "object" && !Array.isArray(value)) { + for (const [key, childSchema] of Object.entries(schema.properties)) { + if (value[key] !== undefined) { + errors.push(...validateSchemaSubset(childSchema, value[key], `${pointer}.${key}`)); + } + } + } + if (schema.items && Array.isArray(value)) { + value.forEach((item, index) => { + errors.push(...validateSchemaSubset(schema.items, item, `${pointer}[${index}]`)); + }); + } + return errors; +} + +function matchesSchemaType(value, type) { + switch (type) { + case "array": + return Array.isArray(value); + case "object": + return value !== null && typeof value === "object" && !Array.isArray(value); + case "string": + return typeof value === "string"; + case "number": + return typeof value === "number"; + case "boolean": + return typeof value === "boolean"; + default: + return true; + } +} diff --git a/schemas/gamecult.eve.electron_shell_projection.v1.schema.json b/schemas/gamecult.eve.electron_shell_projection.v1.schema.json new file mode 100644 index 0000000..b4edf2b --- /dev/null +++ b/schemas/gamecult.eve.electron_shell_projection.v1.schema.json @@ -0,0 +1,93 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "gamecult.eve.electron_shell_projection.v1", + "title": "Eve Electron Shell Projection", + "type": "object", + "required": [ + "type", + "schema", + "providerId", + "surfaceId", + "projectionKind", + "commandBoundary", + "receiptSchema", + "ownership", + "root" + ], + "properties": { + "type": { + "const": "electron-shell-projection" + }, + "schema": { + "const": "gamecult.eve.electron_shell_projection.v1" + }, + "providerId": { + "type": "string", + "minLength": 1 + }, + "surfaceId": { + "type": "string", + "minLength": 1 + }, + "projectionKind": { + "type": "string", + "minLength": 1 + }, + "commandBoundary": { + "type": "string", + "minLength": 1 + }, + "receiptSchema": { + "type": "string", + "minLength": 1 + }, + "ownership": { + "type": "string", + "minLength": 1 + }, + "root": { + "type": "object", + "required": [ + "id", + "componentKind", + "shellElementKind", + "stateBindingCount", + "embeddedDocumentCount", + "children" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "componentKind": { + "type": "string" + }, + "shellElementKind": { + "type": "string", + "minLength": 1 + }, + "props": { + "type": "object" + }, + "layout": { + "type": "object" + }, + "style": { + "type": "object" + }, + "stateBindingCount": { + "type": "number" + }, + "embeddedDocumentCount": { + "type": "number" + }, + "children": { + "type": "array" + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true +} diff --git a/schemas/gamecult.eve.tui_grid.v1.schema.json b/schemas/gamecult.eve.tui_grid.v1.schema.json new file mode 100644 index 0000000..dae985e --- /dev/null +++ b/schemas/gamecult.eve.tui_grid.v1.schema.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "gamecult.eve.tui_grid.v1", + "title": "Eve TUI Grid", + "type": "object", + "required": [ + "schema", + "runtimeId", + "providerId", + "surfaceId", + "width", + "lines", + "lossiness" + ], + "properties": { + "schema": { + "const": "gamecult.eve.tui_grid.v1" + }, + "runtimeId": { + "const": "tui" + }, + "providerId": { + "type": "string", + "minLength": 1 + }, + "surfaceId": { + "type": "string", + "minLength": 1 + }, + "projectionKind": { + "type": "string" + }, + "commandBoundary": { + "type": "string" + }, + "receiptSchema": { + "type": "string" + }, + "ownership": { + "type": "string" + }, + "width": { + "type": "number" + }, + "lines": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + }, + "lossiness": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": true +} diff --git a/scripts/run-conformance-consumer-smoke.ps1 b/scripts/run-conformance-consumer-smoke.ps1 index 35d249b..44b6c25 100644 --- a/scripts/run-conformance-consumer-smoke.ps1 +++ b/scripts/run-conformance-consumer-smoke.ps1 @@ -45,6 +45,8 @@ try { --expect-schema gamecult.eve.runtime_split_handoff.v1 ` --expect-schema gamecult.eve.plugin_handoff.v1 ` --expect-schema gamecult.eve.provider_handoff.v1 ` + --expect-schema gamecult.eve.electron_shell_projection.v1 ` + --expect-schema gamecult.eve.tui_grid.v1 ` --expect-capability-gap runtime:Fensalir:direct2d:capture:missing ` --expect-world-lowering-coverage aetheria:aetheria.daemon.game:web-reference:claimed:Eve:web ` --expect-world-lowering-coverage aetheria:aetheria.daemon.game:unity-uitoolkit:claimed:EveUnity:unity-uitoolkit ` diff --git a/scripts/run-eveconformance-handoff-smoke.ps1 b/scripts/run-eveconformance-handoff-smoke.ps1 index d238873..49835c6 100644 --- a/scripts/run-eveconformance-handoff-smoke.ps1 +++ b/scripts/run-eveconformance-handoff-smoke.ps1 @@ -90,6 +90,8 @@ foreach ($contract in @( "gamecult.eve.runtime_capture_request.v1", "gamecult.eve.provider_advertisement.v1", "gamecult.eve.provider_handoff.v1", + "gamecult.eve.electron_shell_projection.v1", + "gamecult.eve.tui_grid.v1", "gamecult.eve.runtime_capability.v1", "gamecult.eve.runtime_lifecycle.v1", "gamecult.eve.runtime_split_handoff.v1" @@ -124,6 +126,8 @@ if (Test-Path -LiteralPath (Join-Path $absoluteExportDirectory "index.json")) { "gamecult.eve.provider_scenario.v1", "gamecult.eve.provider_advertisement.v1", "gamecult.eve.provider_handoff.v1", + "gamecult.eve.electron_shell_projection.v1", + "gamecult.eve.tui_grid.v1", "gamecult.eve.runtime_capability.v1", "gamecult.eve.runtime_lifecycle.v1", "gamecult.eve.runtime_split_handoff.v1", diff --git a/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 b/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 index b815908..27b92f2 100644 --- a/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 +++ b/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 @@ -45,6 +45,8 @@ node $consumerScript $consumerExport ` --expect-schema gamecult.eve.runtime_capture_request.v1 ` --expect-schema gamecult.eve.runtime_lifecycle.v1 ` --expect-schema gamecult.eve.runtime_split_handoff.v1 ` + --expect-schema gamecult.eve.electron_shell_projection.v1 ` + --expect-schema gamecult.eve.tui_grid.v1 ` --expect-runtime-status web:active ` --expect-runtime-status windows-flutter:active ` --expect-runtime-status linux-flutter:active ` diff --git a/tools/conformance/eveconformance-handoff.json b/tools/conformance/eveconformance-handoff.json index 681a514..280bd52 100644 --- a/tools/conformance/eveconformance-handoff.json +++ b/tools/conformance/eveconformance-handoff.json @@ -26,6 +26,8 @@ "schemas/gamecult.eve.provider_scenario.v1.schema.json", "schemas/gamecult.eve.provider_advertisement.v1.schema.json", "schemas/gamecult.eve.provider_handoff.v1.schema.json", + "schemas/gamecult.eve.electron_shell_projection.v1.schema.json", + "schemas/gamecult.eve.tui_grid.v1.schema.json", "schemas/gamecult.eve.runtime_capability.v1.schema.json", "schemas/gamecult.eve.runtime_lifecycle.v1.schema.json", "schemas/gamecult.eve.runtime_split_handoff.v1.schema.json", @@ -88,6 +90,8 @@ "gamecult.eve.runtime_capture_request.v1", "gamecult.eve.provider_advertisement.v1", "gamecult.eve.provider_handoff.v1", + "gamecult.eve.electron_shell_projection.v1", + "gamecult.eve.tui_grid.v1", "gamecult.eve.runtime_capability.v1", "gamecult.eve.runtime_lifecycle.v1", "gamecult.eve.runtime_split_handoff.v1" diff --git a/tools/parity/parity-manifest.json b/tools/parity/parity-manifest.json index e2cec32..712eb83 100644 --- a/tools/parity/parity-manifest.json +++ b/tools/parity/parity-manifest.json @@ -27,7 +27,9 @@ "gamecult.eve.provider_scenario.v1": "schemas/gamecult.eve.provider_scenario.v1.schema.json", "gamecult.eve.provider_advertisement.v1": "schemas/gamecult.eve.provider_advertisement.v1.schema.json", "gamecult.eve.provider_handoff.v1": "schemas/gamecult.eve.provider_handoff.v1.schema.json", - "gamecult.eve.conformance_handoff.v1": "schemas/gamecult.eve.conformance_handoff.v1.schema.json" + "gamecult.eve.conformance_handoff.v1": "schemas/gamecult.eve.conformance_handoff.v1.schema.json", + "gamecult.eve.electron_shell_projection.v1": "schemas/gamecult.eve.electron_shell_projection.v1.schema.json", + "gamecult.eve.tui_grid.v1": "schemas/gamecult.eve.tui_grid.v1.schema.json" }, "conformancePacks": [ {