diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index 5cbac82..e432cd7 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -274,11 +274,18 @@ Exit criteria: ## Active Work Queue -1. Add a real Unity plugin-host proof for either Sai or Norn, or keep - `EveUnity` blocked on explicit unsupported-plugin declarations. +1. Add a real Unity plugin-host proof for either Sai or Norn, backed by a + runtime-owned capability manifest and Unity test/capture lifecycle evidence. Recently cut: +- Unity UI Toolkit now publishes a runtime-owned capability manifest at + `packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json`. The + parity harness validates that manifest against + `gamecult.eve.runtime_capability.v1`, cross-checks supported features, + unsupported Sai/Norn/TeX plugin declarations, command transport schema, and + incubation metadata, then exports the manifest path and validation errors + through the conformance report. - Aetheria now consumes the exported conformance pack from its own working directory through `scripts/run-aetheria-conformance-consumer-smoke.ps1`. The smoke asserts the provider pack, `aetheria-world` fixture, `aetheria` diff --git a/docs/parity-testing-harness.md b/docs/parity-testing-harness.md index 3effd94..7f539c6 100644 --- a/docs/parity-testing-harness.md +++ b/docs/parity-testing-harness.md @@ -18,6 +18,14 @@ style-token expectations, same control anatomy, and explicit capture gaps. The manifest is the cut line. If a fixture or runtime is not in the manifest, it is not part of the parity claim. +Runtime capability manifests are the runtime-owned side of that claim. When a +runtime entry has `capabilityManifest`, the harness validates the JSON document +against `gamecult.eve.runtime_capability.v1` and cross-checks runtime id, +supported features, supported and unsupported plugin declarations, command +transport schema, and incubation metadata. The central parity manifest may +coordinate split-readiness, but the runtime body must publish the capability +claim it expects other repos to consume. + ## Run Semantic checks: @@ -64,6 +72,10 @@ The semantic runner writes: - `artifacts/conformance/latest/packs/*.json` - `artifacts/conformance//...` +The conformance export includes runtime `capabilityManifestPath` and +`capabilityManifestErrors` fields so external consumers can distinguish a +generic projection runtime from a runtime that actually hosts plugin semantics. + After generating the parity report, `run-parity-harness.ps1` copies `artifacts/conformance/latest` into `artifacts/conformance-consumer-smoke/export` and runs diff --git a/docs/plugin-architecture.md b/docs/plugin-architecture.md index 1e80220..0aa2bfc 100644 --- a/docs/plugin-architecture.md +++ b/docs/plugin-architecture.md @@ -45,6 +45,8 @@ Renderer clients own: - input capture and command emission; - local sensor/media capture when relevant; - runtime-specific capability gaps; +- runtime capability manifests that declare supported features, plugin-host + support, unsupported plugin semantics, command transport, and split metadata; - screenshots, frame capture, and performance telemetry. Providers own: diff --git a/packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json b/packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json new file mode 100644 index 0000000..82482bd --- /dev/null +++ b/packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json @@ -0,0 +1,39 @@ +{ + "schema": "gamecult.eve.runtime_capability.v1", + "runtimeId": "unity-uitoolkit", + "title": "Unity / UI Toolkit", + "owner": "EveUnity", + "incubation": { + "ownerRepo": "EveUnity", + "currentHostRepo": "Eve", + "splitTarget": "EveUnity", + "graduationTrigger": "Split when Unity package has independent UPM release, plugin host behavior, and Unity test/capture lifecycle." + }, + "supportedFeatures": [ + "embeddedDocuments" + ], + "supportedPlugins": [], + "unsupportedPlugins": [ + { + "pluginId": "sai.vn", + "reason": "Unity UI Toolkit currently lowers Sai VN component kinds as generic Eve containers; no Sai plugin host exists in this runtime." + }, + { + "pluginId": "norn.graph", + "reason": "Unity UI Toolkit currently preserves embed.norn as generic Eve structure; no Norn graph plugin host or layout adapter exists in this runtime." + }, + { + "pluginId": "tex.math", + "reason": "Unity UI Toolkit currently preserves embed.tex as generic Eve structure; no TeX render plugin host exists in this runtime." + } + ], + "commandTransport": { + "schema": "gamecult.eve.command.v1", + "emitter": "EveUiToolkitSurfaceLowerer.EmitCommand", + "clientId": "unity-uitoolkit" + }, + "capture": { + "status": "semantic", + "note": "Unity UI Toolkit lowers embedded document slots through a resolver-backed child surface host." + } +} diff --git a/schemas/gamecult.eve.runtime_capability.v1.schema.json b/schemas/gamecult.eve.runtime_capability.v1.schema.json new file mode 100644 index 0000000..519929f --- /dev/null +++ b/schemas/gamecult.eve.runtime_capability.v1.schema.json @@ -0,0 +1,128 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "gamecult.eve.runtime_capability.v1", + "title": "Eve Runtime Capability Manifest", + "type": "object", + "required": [ + "schema", + "runtimeId", + "title", + "owner", + "incubation", + "supportedFeatures", + "supportedPlugins", + "unsupportedPlugins", + "commandTransport" + ], + "properties": { + "schema": { + "const": "gamecult.eve.runtime_capability.v1" + }, + "runtimeId": { + "type": "string", + "minLength": 1 + }, + "title": { + "type": "string", + "minLength": 1 + }, + "owner": { + "type": "string", + "minLength": 1 + }, + "incubation": { + "type": "object", + "required": [ + "ownerRepo", + "currentHostRepo", + "splitTarget", + "graduationTrigger" + ], + "properties": { + "ownerRepo": { + "type": "string", + "minLength": 1 + }, + "currentHostRepo": { + "type": "string", + "minLength": 1 + }, + "splitTarget": { + "type": "string", + "minLength": 1 + }, + "graduationTrigger": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": true + }, + "supportedFeatures": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "supportedPlugins": { + "type": "array", + "items": { + "type": "object", + "required": [ + "pluginId", + "capabilities" + ], + "properties": { + "pluginId": { + "type": "string", + "minLength": 1 + }, + "capabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "additionalProperties": true + } + }, + "unsupportedPlugins": { + "type": "array", + "items": { + "type": "object", + "required": [ + "pluginId", + "reason" + ], + "properties": { + "pluginId": { + "type": "string", + "minLength": 1 + }, + "reason": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": true + } + }, + "commandTransport": { + "type": "object", + "required": [ + "schema" + ], + "properties": { + "schema": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true +} diff --git a/tools/parity/parity-manifest.json b/tools/parity/parity-manifest.json index e9d84df..fe17f24 100644 --- a/tools/parity/parity-manifest.json +++ b/tools/parity/parity-manifest.json @@ -12,6 +12,7 @@ "gamecult.eve.plugin.v1": "schemas/gamecult.eve.plugin.v1.schema.json", "gamecult.eve.plugin_abi_fixture.v1": "schemas/gamecult.eve.plugin_abi_fixture.v1.schema.json", "gamecult.eve.plugin_advertisement.v1": "schemas/gamecult.eve.plugin_advertisement.v1.schema.json", + "gamecult.eve.runtime_capability.v1": "schemas/gamecult.eve.runtime_capability.v1.schema.json", "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" }, @@ -679,6 +680,10 @@ "repoRole": "runtime-incubating", "splitTarget": "EveUnity", "graduationTrigger": "Split when Unity package has independent UPM release, plugin host behavior, and Unity test/capture lifecycle.", + "capabilityManifest": { + "schemaPath": "schemas/gamecult.eve.runtime_capability.v1.schema.json", + "manifestPath": "packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json" + }, "requiredFixtures": ["embedded-surface"], "pluginFixtures": ["sai-vn"], "supportedFeatures": ["embeddedDocuments"], diff --git a/tools/parity/run-parity.mjs b/tools/parity/run-parity.mjs index ef3abac..e3588b5 100644 --- a/tools/parity/run-parity.mjs +++ b/tools/parity/run-parity.mjs @@ -302,6 +302,7 @@ async function evaluateRuntime(runtime, fixtureResults) { missingRequiredFeatures.push("embeddedDocuments"); } const commandTransportSmokeErrors = await validateRuntimeCommandTransportSmoke(runtime); + const capabilityManifestErrors = await validateRuntimeCapabilityManifest(runtime); const missingIncubationFields = requiredIncubationFields(runtime).filter(field => !runtime[field]); const pluginCapabilityGaps = collectPluginCapabilityGaps(runtime, fixtureResults); const unsupportedPluginNotes = collectUnsupportedPluginNotes(runtime, fixtureResults); @@ -311,6 +312,7 @@ async function evaluateRuntime(runtime, fixtureResults) { if (runtime.kind === "active" && missingRequiredFixtures.length) status = "missing-required-fixture"; if (runtime.kind === "active" && missingRequiredFeatures.length) status = "missing-required-feature"; 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" && missingIncubationFields.length) status = "missing-incubation-metadata"; if (runtime.kind === "active" && pluginCapabilityGaps.length && status === "active") status = "active-with-capability-gaps"; if (runtime.kind === "pending" && runtime.adapterSpike === "external" && !missingExternalPaths.length && !missingExternalSourceSymbols.length) { @@ -343,6 +345,8 @@ async function evaluateRuntime(runtime, fixtureResults) { missingRequiredFeatures, commandTransportSmoke: runtime.commandTransportSmoke || null, commandTransportSmokeErrors, + capabilityManifest: runtime.capabilityManifest || null, + capabilityManifestErrors, ownerRepo: runtime.ownerRepo || "", repoRole: runtime.repoRole || "", graduationTrigger: runtime.graduationTrigger || "", @@ -830,6 +834,87 @@ async function validateRuntimeCommandTransportSmoke(runtime) { return errors; } +async function validateRuntimeCapabilityManifest(runtime) { + const capabilityManifest = runtime.capabilityManifest; + if (!capabilityManifest) return []; + + const errors = await validateJsonDocument(capabilityManifest.schemaPath, capabilityManifest.manifestPath, { + schema: "gamecult.eve.runtime_capability.v1", + runtimeId: runtime.id, + }); + if (errors.length) return errors; + + const documentPath = path.join(repoRoot, capabilityManifest.manifestPath); + const document = JSON.parse(await readFile(documentPath, "utf8")); + const supportedFeatures = document.supportedFeatures || []; + const supportedPlugins = document.supportedPlugins || []; + const unsupportedPlugins = document.unsupportedPlugins || []; + const commandTransport = document.commandTransport || {}; + + errors.push(...missingMembers(runtime.supportedFeatures || [], supportedFeatures, `${capabilityManifest.manifestPath}:supportedFeatures`)); + errors.push(...comparePluginCapabilityClaims( + runtime.supportedPlugins || [], + supportedPlugins, + `${capabilityManifest.manifestPath}:supportedPlugins`, + )); + errors.push(...compareUnsupportedPluginClaims( + runtime.unsupportedPlugins || [], + unsupportedPlugins, + `${capabilityManifest.manifestPath}:unsupportedPlugins`, + )); + + const expectedCommandSchema = runtime.commandTransportSmoke?.schema || ""; + if (expectedCommandSchema && commandTransport.schema !== expectedCommandSchema) { + errors.push(`${capabilityManifest.manifestPath}:commandTransport.schema:expected ${expectedCommandSchema} got ${commandTransport.schema || ""}`); + } + + const incubation = document.incubation || {}; + for (const [key, value] of Object.entries({ + ownerRepo: runtime.splitTarget || runtime.ownerRepo || "", + currentHostRepo: runtime.ownerRepo || "", + splitTarget: runtime.splitTarget || "", + graduationTrigger: runtime.graduationTrigger || "", + })) { + if (value && incubation[key] !== value) { + errors.push(`${capabilityManifest.manifestPath}:incubation.${key}:expected ${value} got ${incubation[key] || ""}`); + } + } + + return errors; +} + +function comparePluginCapabilityClaims(expectedPlugins, actualPlugins, label) { + const errors = []; + const actual = new Map(actualPlugins.map(plugin => [plugin.pluginId, new Set(plugin.capabilities || [])])); + for (const plugin of expectedPlugins) { + const capabilities = actual.get(plugin.pluginId); + if (!capabilities) { + errors.push(`${label}:${plugin.pluginId}:missing`); + continue; + } + for (const capability of plugin.capabilities || []) { + if (!capabilities.has(capability)) errors.push(`${label}:${plugin.pluginId}:${capability}:missing`); + } + } + return errors; +} + +function compareUnsupportedPluginClaims(expectedPlugins, actualPlugins, label) { + const errors = []; + const actual = new Map(actualPlugins.map(plugin => [plugin.pluginId, plugin.reason || ""])); + for (const plugin of expectedPlugins) { + const reason = actual.get(plugin.pluginId); + if (!reason) { + errors.push(`${label}:${plugin.pluginId}:missing`); + continue; + } + if (plugin.reason && reason !== plugin.reason) { + errors.push(`${label}:${plugin.pluginId}:reason:expected ${plugin.reason} got ${reason}`); + } + } + return errors; +} + function evaluateSplitTargets(splitTargets, runtimeResults) { const runtimeById = new Map(runtimeResults.map(runtime => [runtime.id, runtime])); return splitTargets.map(target => { @@ -1078,6 +1163,8 @@ function buildConformanceExport(report) { supportedFeatures: runtime.supportedFeatures, supportedPlugins: runtime.supportedPlugins, unsupportedPlugins: runtime.unsupportedPlugins, + capabilityManifestPath: runtime.capabilityManifest?.manifestPath || "", + capabilityManifestErrors: runtime.capabilityManifestErrors || [], commandTransportSchema: runtime.commandTransportSmoke?.schema || "", })), splitTargets: report.splitTargets || [], @@ -1235,6 +1322,7 @@ function renderMarkdown(report) { ...runtime.missingRequiredFixtures.map(id => `fixture:${id}`), ...runtime.missingRequiredFeatures.map(id => `feature:${id}`), ...runtime.commandTransportSmokeErrors.map(id => `command-smoke:${id}`), + ...runtime.capabilityManifestErrors.map(id => `runtime-capability:${id}`), ...runtime.missingIncubationFields.map(id => `metadata:${id}`), ].join(", "); lines.push(`| ${runtime.title} | ${runtime.status} | ${runtime.ownerRepo} | ${runtime.capture?.status || "unknown"} | ${runtime.commandTransportSmoke?.schema || ""} | ${runtime.requiredFixtures.join(", ")} | ${runtime.pluginFixtures.join(", ")} | ${runtime.supportedFeatures.join(", ")} | ${runtime.pluginCapabilityGaps.join(", ")} | ${runtime.unsupportedPluginNotes.join(", ")} | ${missing} |`);