Schema-back runtime lifecycle evidence

This commit is contained in:
Meta Vens 2026-07-09 12:07:25 +01:00
commit 3f47ac0f31
11 changed files with 323 additions and 4 deletions

View file

@ -430,6 +430,13 @@ Recently cut:
demand those contracts explicitly, so Sai, Norn, EvePlugins, Aetheria, and
EveConformance can consume handoff shape without depending on Eve's local
smoke script folklore.
- Runtime lifecycle shape is now schema-backed as
`gamecult.eve.runtime_lifecycle.v1`. Flutter's standalone
`eveflutter-lifecycle.json` validates against that contract, embedded
release/test/capture lifecycle blocks in runtime capability manifests are
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.
- 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

@ -224,6 +224,10 @@ Plugin, provider, and EveConformance handoff documents are also schema-backed:
`gamecult.eve.plugin_handoff.v1`, `gamecult.eve.provider_handoff.v1`, and
`gamecult.eve.conformance_handoff.v1` are exported through the schema catalog.
Owner consumer smokes assert those schemas before trusting handoff paths.
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`.
After generating the parity report, `run-parity-harness.ps1` copies
`artifacts/conformance/latest` into

View file

@ -1020,6 +1020,12 @@
"capabilityManifestErrors": {
"type": "array"
},
"lifecycleManifestPath": {
"type": "string"
},
"lifecycleErrors": {
"type": "array"
},
"splitHandoffPath": {
"type": "string"
},

View file

@ -0,0 +1,178 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "gamecult.eve.runtime_lifecycle.v1",
"title": "Eve Runtime Lifecycle",
"type": "object",
"required": [
"schema",
"splitTarget",
"ownerRepo",
"currentHostRepo",
"runtimes",
"release",
"test",
"capture"
],
"properties": {
"schema": {
"const": "gamecult.eve.runtime_lifecycle.v1"
},
"splitTarget": {
"type": "string",
"minLength": 1
},
"ownerRepo": {
"type": "string",
"minLength": 1
},
"currentHostRepo": {
"type": "string",
"minLength": 1
},
"status": {
"type": "string"
},
"runtimes": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1
}
},
"release": {
"type": "object",
"required": [
"ownerRepo",
"status",
"evidencePaths"
],
"properties": {
"ownerRepo": {
"type": "string",
"minLength": 1
},
"status": {
"type": "string",
"minLength": 1
},
"command": {
"type": "string"
},
"commands": {
"type": "array"
},
"releaseContract": {
"type": "object"
},
"evidencePaths": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"pendingProofs": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
}
},
"additionalProperties": true
},
"test": {
"type": "object",
"required": [
"ownerRepo",
"status",
"evidencePaths"
],
"properties": {
"ownerRepo": {
"type": "string",
"minLength": 1
},
"status": {
"type": "string",
"minLength": 1
},
"command": {
"type": "string"
},
"commands": {
"type": "array"
},
"testContract": {
"type": "object"
},
"evidencePaths": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"pendingProofs": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
}
},
"additionalProperties": true
},
"capture": {
"type": "object",
"required": [
"ownerRepo",
"status",
"evidencePaths"
],
"properties": {
"ownerRepo": {
"type": "string",
"minLength": 1
},
"status": {
"type": "string",
"minLength": 1
},
"command": {
"type": "string"
},
"commands": {
"type": "array"
},
"captureContract": {
"type": "object"
},
"evidencePaths": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"pendingProofs": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
}
},
"additionalProperties": true
},
"remainingSplitBlockers": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
}
},
"additionalProperties": true
}

View file

@ -41,6 +41,7 @@ try {
--expect-schema gamecult.eve.plugin_abi.response.v1 `
--expect-schema gamecult.eve.runtime_release_request.v1 `
--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.plugin_handoff.v1 `
--expect-schema gamecult.eve.provider_handoff.v1 `

View file

@ -91,6 +91,7 @@ foreach ($contract in @(
"gamecult.eve.provider_advertisement.v1",
"gamecult.eve.provider_handoff.v1",
"gamecult.eve.runtime_capability.v1",
"gamecult.eve.runtime_lifecycle.v1",
"gamecult.eve.runtime_split_handoff.v1"
)) {
if (-not (@($handoff.contractInputs) -contains $contract)) {
@ -124,6 +125,7 @@ if (Test-Path -LiteralPath (Join-Path $absoluteExportDirectory "index.json")) {
"gamecult.eve.provider_advertisement.v1",
"gamecult.eve.provider_handoff.v1",
"gamecult.eve.runtime_capability.v1",
"gamecult.eve.runtime_lifecycle.v1",
"gamecult.eve.runtime_split_handoff.v1",
"gamecult.eve.command_receipt.v1",
"gamecult.eve.plugin.v1",

View file

@ -43,6 +43,7 @@ node $consumerScript $consumerExport `
--expect-runtime direct2d `
--expect-schema gamecult.eve.runtime_release_request.v1 `
--expect-schema gamecult.eve.runtime_capture_request.v1 `
--expect-schema gamecult.eve.runtime_lifecycle.v1 `
--expect-schema gamecult.eve.runtime_split_handoff.v1 `
--expect-runtime-status web:active `
--expect-runtime-status windows-flutter:active `

View file

@ -974,7 +974,7 @@ function validateRuntimeRecords(runtimes, errors) {
for (const field of ["runtimeId", "status", "ownerRepo", "splitTarget", "captureStatus"]) {
if (!runtime?.[field]) errors.push(`${label}:${field}:missing`);
}
for (const field of ["supportedFeatures", "supportedPlugins", "unsupportedPlugins", "capabilityManifestErrors", "worldSurfaceLowering"]) {
for (const field of ["supportedFeatures", "supportedPlugins", "unsupportedPlugins", "capabilityManifestErrors", "lifecycleErrors", "worldSurfaceLowering"]) {
if (!Array.isArray(runtime?.[field])) errors.push(`${label}:${field}:expected array`);
}
}

View file

@ -27,6 +27,7 @@
"schemas/gamecult.eve.provider_advertisement.v1.schema.json",
"schemas/gamecult.eve.provider_handoff.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",
"schemas/gamecult.eve.command_receipt.v1.schema.json",
"schemas/gamecult.eve.plugin.v1.schema.json",
@ -88,6 +89,7 @@
"gamecult.eve.provider_advertisement.v1",
"gamecult.eve.provider_handoff.v1",
"gamecult.eve.runtime_capability.v1",
"gamecult.eve.runtime_lifecycle.v1",
"gamecult.eve.runtime_split_handoff.v1"
],
"forbiddenImports": [

View file

@ -20,6 +20,7 @@
"gamecult.eve.plugin_advertisement.v1": "schemas/gamecult.eve.plugin_advertisement.v1.schema.json",
"gamecult.eve.plugin_handoff.v1": "schemas/gamecult.eve.plugin_handoff.v1.schema.json",
"gamecult.eve.runtime_capability.v1": "schemas/gamecult.eve.runtime_capability.v1.schema.json",
"gamecult.eve.runtime_lifecycle.v1": "schemas/gamecult.eve.runtime_lifecycle.v1.schema.json",
"gamecult.eve.runtime_split_handoff.v1": "schemas/gamecult.eve.runtime_split_handoff.v1.schema.json",
"gamecult.eve.runtime_release_request.v1": "schemas/gamecult.eve.runtime_release_request.v1.schema.json",
"gamecult.eve.runtime_capture_request.v1": "schemas/gamecult.eve.runtime_capture_request.v1.schema.json",
@ -1120,6 +1121,10 @@
"repoRole": "runtime-incubating",
"splitTarget": "EveFlutter",
"graduationTrigger": "Split when Flutter has provider picker, command transport, plugin projection adapters, and its own build/release lifecycle.",
"lifecycleManifest": {
"schemaPath": "schemas/gamecult.eve.runtime_lifecycle.v1.schema.json",
"manifestPath": "flutter/eve_parity/eveflutter-lifecycle.json"
},
"requiredFixtures": ["embedded-surface"],
"pluginFixtures": ["sai-vn"],
"supportedFeatures": ["embeddedDocuments"],
@ -1171,6 +1176,10 @@
"repoRole": "runtime-incubating",
"splitTarget": "EveFlutter",
"graduationTrigger": "Split with EveFlutter when Linux capture and runner health become runtime-owned.",
"lifecycleManifest": {
"schemaPath": "schemas/gamecult.eve.runtime_lifecycle.v1.schema.json",
"manifestPath": "flutter/eve_parity/eveflutter-lifecycle.json"
},
"requiredFixtures": ["embedded-surface"],
"pluginFixtures": ["sai-vn"],
"supportedFeatures": ["embeddedDocuments"],
@ -1259,6 +1268,10 @@
"repoRole": "runtime-incubating",
"splitTarget": "EveFlutter",
"graduationTrigger": "Split with EveFlutter when Android APK smoke, provider discovery, command transport, and plugin projection adapters are runtime-owned.",
"lifecycleManifest": {
"schemaPath": "schemas/gamecult.eve.runtime_lifecycle.v1.schema.json",
"manifestPath": "flutter/eve_parity/eveflutter-lifecycle.json"
},
"requiredFixtures": ["embedded-surface"],
"pluginFixtures": ["sai-vn"],
"supportedFeatures": ["embeddedDocuments"],

View file

@ -310,6 +310,9 @@ async function evaluateRuntime(runtime, fixtureResults) {
const commandTransportSmokeErrors = await validateRuntimeCommandTransportSmoke(runtime);
const capabilityManifestErrors = await validateRuntimeCapabilityManifest(runtime);
const capabilityManifestDocument = await readRuntimeCapabilityManifestDocument(runtime);
const lifecycleManifestDocument = await readRuntimeLifecycleManifestDocument(runtime);
const lifecycle = runtime.lifecycle || capabilityManifestDocument?.lifecycle || readLifecycleStages(lifecycleManifestDocument);
const lifecycleErrors = await validateRuntimeLifecycle(runtime, lifecycle, lifecycleManifestDocument);
const splitHandoffPath = await readRuntimeSplitHandoffPath(runtime);
const splitHandoffErrors = await validateRuntimeSplitHandoff(runtime, splitHandoffPath);
const localProviderCatalogErrors = await validateRuntimeLocalProviderCatalog(runtime);
@ -323,6 +326,7 @@ async function evaluateRuntime(runtime, fixtureResults) {
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" && lifecycleErrors.length) status = "invalid-runtime-lifecycle";
if (runtime.kind === "active" && splitHandoffErrors.length) status = "invalid-runtime-split-handoff";
if (runtime.kind === "active" && localProviderCatalogErrors.length) status = "invalid-local-provider-catalog";
if (runtime.kind === "active" && missingIncubationFields.length) status = "missing-incubation-metadata";
@ -361,11 +365,13 @@ async function evaluateRuntime(runtime, fixtureResults) {
commandTransportSmokeErrors,
capabilityManifest: runtime.capabilityManifest || null,
capabilityManifestErrors,
lifecycleManifest: runtime.lifecycleManifest || null,
lifecycleErrors,
splitHandoffPath,
splitHandoffErrors,
localProviderCatalog: runtime.localProviderCatalog || null,
localProviderCatalogErrors,
lifecycle: runtime.lifecycle || capabilityManifestDocument?.lifecycle || null,
lifecycle,
ownerRepo: runtime.ownerRepo || "",
repoRole: runtime.repoRole || "",
graduationTrigger: runtime.graduationTrigger || "",
@ -393,6 +399,82 @@ async function readRuntimeCapabilityManifestDocument(runtime) {
}
}
async function readRuntimeLifecycleManifestDocument(runtime) {
const lifecyclePath = runtime.lifecycleManifest?.manifestPath;
if (!lifecyclePath) return null;
try {
return await readJsonDocument(lifecyclePath);
} catch {
return null;
}
}
function readLifecycleStages(lifecycleDocument) {
if (!lifecycleDocument) return null;
return {
release: lifecycleDocument.release,
test: lifecycleDocument.test,
capture: lifecycleDocument.capture,
};
}
async function validateRuntimeLifecycle(runtime, lifecycle, lifecycleDocument) {
if (!lifecycle && !runtime.lifecycleManifest) return [];
const schemaPath = runtime.lifecycleManifest?.schemaPath || manifest.schemas?.["gamecult.eve.runtime_lifecycle.v1"];
const errors = [];
if (runtime.lifecycleManifest?.manifestPath) {
errors.push(...await validateJsonDocument(schemaPath, runtime.lifecycleManifest.manifestPath, {
schema: "gamecult.eve.runtime_lifecycle.v1",
}));
if (!lifecycleDocument) {
errors.push(`${runtime.lifecycleManifest.manifestPath}:unreadable`);
} else if (!Array.isArray(lifecycleDocument.runtimes) || !lifecycleDocument.runtimes.includes(runtime.id)) {
errors.push(`${runtime.lifecycleManifest.manifestPath}:runtimes:${runtime.id}:missing`);
}
}
if (!lifecycle) {
errors.push("lifecycle:missing");
return errors;
}
const ownerRepo = lifecycleDocument?.ownerRepo
|| lifecycle.release?.ownerRepo
|| lifecycle.test?.ownerRepo
|| lifecycle.capture?.ownerRepo
|| runtime.splitTarget
|| runtime.ownerRepo
|| "";
const currentHostRepo = lifecycleDocument?.currentHostRepo || runtime.ownerRepo || "Eve";
const lifecycleEnvelope = {
schema: "gamecult.eve.runtime_lifecycle.v1",
splitTarget: lifecycleDocument?.splitTarget || runtime.splitTarget || runtime.id,
ownerRepo,
currentHostRepo,
status: lifecycleDocument?.status || runtime.status || "",
runtimes: lifecycleDocument?.runtimes || [runtime.id],
release: lifecycle.release,
test: lifecycle.test,
capture: lifecycle.capture,
remainingSplitBlockers: lifecycleDocument?.remainingSplitBlockers || [],
};
const schemaErrors = await validateJsonDocumentAgainstSchemaPath(schemaPath, lifecycleEnvelope, `runtimeLifecycle:${runtime.id}`);
errors.push(...schemaErrors);
for (const stage of ["release", "test", "capture"]) {
const stageDocument = lifecycle[stage];
for (const evidencePath of stageDocument?.evidencePaths || []) {
if (!existsSync(path.join(repoRoot, evidencePath))) {
errors.push(`lifecycle.${stage}.evidence:${evidencePath}:missing`);
}
}
}
return errors;
}
async function validateRuntimeLocalProviderCatalog(runtime) {
const catalogConfig = runtime.localProviderCatalog;
if (!catalogConfig) return [];
@ -1116,6 +1198,23 @@ async function validateJsonDocument(schemaPath, documentPath, expected = {}) {
return errors;
}
async function validateJsonDocumentAgainstSchemaPath(schemaPath, document, label) {
const errors = [];
if (!schemaPath) return ["schemaPath:missing"];
const absoluteSchemaPath = path.join(repoRoot, schemaPath);
if (!existsSync(absoluteSchemaPath)) return [`${schemaPath}:missing`];
try {
const schema = JSON.parse(await readFile(absoluteSchemaPath, "utf8"));
errors.push(...validateSchemaSubset(schema, document, label));
} catch (error) {
errors.push(`${label}:invalid-json:${error instanceof Error ? error.message : String(error)}`);
}
return errors;
}
function extractProviderSchemaIds(schemas) {
return schemas.map(schema => {
if (typeof schema === "string") return schema;
@ -1783,15 +1882,18 @@ function buildConformanceExport(report) {
requiredFixtures: runtime.requiredFixtures,
pluginFixtures: runtime.pluginFixtures,
capabilityManifestPath: runtime.capabilityManifest?.manifestPath || "",
lifecycleManifestPath: runtime.lifecycleManifest?.manifestPath || "",
commandTransportSchema: runtimeCommandTransportSchema(runtime),
captureStatus: runtime.capture?.status || "",
lifecycle: runtime.lifecycle,
missingEvidence: [
lifecycle: runtime.lifecycle,
lifecycleErrors: runtime.lifecycleErrors || [],
missingEvidence: [
...runtime.missingPaths,
...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.lifecycleErrors.map(id => `runtime-lifecycle:${id}`),
...runtime.localProviderCatalogErrors.map(id => `local-provider-catalog:${id}`),
],
}))
@ -1874,6 +1976,8 @@ function buildConformanceExport(report) {
unsupportedPlugins: runtime.unsupportedPlugins,
capabilityManifestPath: runtime.capabilityManifest?.manifestPath || "",
capabilityManifestErrors: runtime.capabilityManifestErrors || [],
lifecycleManifestPath: runtime.lifecycleManifest?.manifestPath || "",
lifecycleErrors: runtime.lifecycleErrors || [],
splitHandoffPath: runtime.splitHandoffPath || "",
splitHandoffErrors: runtime.splitHandoffErrors || [],
splitHandoffExportPath: makeHandoffExportPath("runtime", runtime.id, runtime.splitHandoffPath || ""),
@ -2049,6 +2153,7 @@ function collectCapabilityGaps(report) {
...(runtime.missingRequiredFeatures || []).map(id => `feature:${id}`),
...(runtime.commandTransportSmokeErrors || []).map(id => `command-smoke:${id}`),
...(runtime.capabilityManifestErrors || []).map(id => `runtime-capability:${id}`),
...(runtime.lifecycleErrors || []).map(id => `runtime-lifecycle:${id}`),
...(runtime.splitHandoffErrors || []).map(id => `runtime-split-handoff:${id}`),
...(runtime.localProviderCatalogErrors || []).map(id => `local-provider-catalog:${id}`),
...(runtime.pluginCapabilityGaps || []).map(id => `plugin-capability:${id}`),