Validate Unity runtime capability manifest
This commit is contained in:
parent
5879d4953a
commit
5e650ec337
7 changed files with 283 additions and 2 deletions
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
128
schemas/gamecult.eve.runtime_capability.v1.schema.json
Normal file
128
schemas/gamecult.eve.runtime_capability.v1.schema.json
Normal 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
|
||||
}
|
||||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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} |`);
|
||||
|
|
|
|||
Loading…
Reference in a new issue