Schema-back shell projection outputs

This commit is contained in:
Meta Vens 2026-07-09 12:13:07 +01:00
commit d4e101c225
12 changed files with 251 additions and 1 deletions

View file

@ -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

View file

@ -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

View file

@ -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");

View file

@ -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");

View file

@ -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;
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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 `

View file

@ -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",

View file

@ -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 `

View file

@ -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"

View file

@ -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": [
{