Validate Unity runtime capability manifest

This commit is contained in:
Meta Vens 2026-07-09 00:30:43 +01:00
commit 5e650ec337
7 changed files with 283 additions and 2 deletions

View file

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

View file

@ -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/<timestamp>/...`
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

View file

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

View file

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

View file

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

View file

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

View file

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