diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index 46c6c45..3b33ce6 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -285,9 +285,7 @@ Exit criteria: ## Active Work Queue -1. Turn the Unity live witness into a conformance-pack attachment with cold and - warm asset-transfer timings, command receipts, and screenshot metrics. -2. Replace Aetheria's specialized browser embedded-document methods with a +1. Replace Aetheria's specialized browser embedded-document methods with a generic CultMesh document handle once the field/plugin document contract is explicit; do not move Aetheria gravity or object schemas into Eve. @@ -336,6 +334,16 @@ Recently cut: witness requires the Eve-owned world scene, provider entity markers, and one controlled marker in the rendered DOM, so surrounding HUD text cannot stand in for interactive-world lowering proof. +- Eve now defines `gamecult.eve.runtime_witness.v1`, and EveUnity's live + Aetheria runner emits owner-repo witnesses with cache mode, wall/test timing, + provider advertisement and asset assertions, movement distance, initial and + final surface versions, terminal movement/target/action receipts, screenshot + dimensions/hash/size, and hashed test results. The conformance attachment + tool validates the schema, copies declared artifacts into export-relative + paths, and indexes the witness without retaining EveUnity source paths. + Export consumption passed for both `eveunity.aetheria.game.warm` (68.27 s + wall, 7.72 s test) and `eveunity.aetheria.game.cold` (138.90 s wall, + 111.52 s test), including all three reconciled command receipts. - The clean EveUnity PlayMode client now consumes Aetheria's independently running daemon without importing Aetheria code. Aetheria builds its authored @@ -1382,10 +1390,9 @@ Evidence on 2026-07-10: runtime surface serializer. Core surface wire fields use CultMesh transport records; provider-private surface documents remain separate authority. -Remaining before this gate is complete: - -- attach both PNGs, cold/warm transfer timings, and receipt/version evidence to the exported conformance - pack rather than leaving them only in EveUnity's local artifacts. +This gate is complete: cold and warm runtime-owned witnesses attach their PNG, +test result, receipt, version, movement, timing, and screenshot evidence to a +consumer-verifiable conformance export. ## Stop Conditions diff --git a/schemas/gamecult.eve.conformance_export.v1.schema.json b/schemas/gamecult.eve.conformance_export.v1.schema.json index 7969a60..e6ee523 100644 --- a/schemas/gamecult.eve.conformance_export.v1.schema.json +++ b/schemas/gamecult.eve.conformance_export.v1.schema.json @@ -46,6 +46,26 @@ "incubationPolicy": { "type": "string" }, + "runtimeWitnesses": { + "type": "array", + "items": { + "type": "object", + "required": ["witnessId", "runtimeId", "runtimeOwnerRepo", "providerId", "surfaceId", "projectionKind", "status", "cacheState", "witnessExportPath", "artifactCount"], + "properties": { + "witnessId": { "type": "string", "minLength": 1 }, + "runtimeId": { "type": "string", "minLength": 1 }, + "runtimeOwnerRepo": { "type": "string", "minLength": 1 }, + "providerId": { "type": "string", "minLength": 1 }, + "surfaceId": { "type": "string", "minLength": 1 }, + "projectionKind": { "type": "string", "minLength": 1 }, + "status": { "enum": ["pass", "fail"] }, + "cacheState": { "enum": ["cold", "warm"] }, + "witnessExportPath": { "type": "string", "minLength": 1 }, + "artifactCount": { "type": "integer", "minimum": 1 } + }, + "additionalProperties": true + } + }, "packs": { "type": "array", "items": { diff --git a/schemas/gamecult.eve.runtime_witness.v1.schema.json b/schemas/gamecult.eve.runtime_witness.v1.schema.json new file mode 100644 index 0000000..275927c --- /dev/null +++ b/schemas/gamecult.eve.runtime_witness.v1.schema.json @@ -0,0 +1,93 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "gamecult.eve.runtime_witness.v1", + "title": "Eve Runtime Witness", + "type": "object", + "required": [ + "schema", "witnessId", "runtimeId", "runtimeOwnerRepo", "providerId", + "surfaceId", "projectionKind", "status", "generatedAtUtc", "execution", + "assertions", "receipts", "screenshotMetrics", "artifacts", "authority" + ], + "properties": { + "schema": { "const": "gamecult.eve.runtime_witness.v1" }, + "witnessId": { "type": "string", "minLength": 1 }, + "runtimeId": { "type": "string", "minLength": 1 }, + "runtimeOwnerRepo": { "type": "string", "minLength": 1 }, + "providerId": { "type": "string", "minLength": 1 }, + "surfaceId": { "type": "string", "minLength": 1 }, + "projectionKind": { "type": "string", "minLength": 1 }, + "status": { "enum": ["pass", "fail"] }, + "generatedAtUtc": { "type": "string", "minLength": 1 }, + "execution": { + "type": "object", + "required": ["cacheState", "durationMs", "testDurationMs"], + "properties": { + "cacheState": { "enum": ["cold", "warm"] }, + "durationMs": { "type": "number", "minimum": 0 }, + "testDurationMs": { "type": "number", "minimum": 0 } + }, + "additionalProperties": true + }, + "assertions": { + "type": "object", + "required": ["providerAdvertisement", "providerAssets", "movement", "targeting", "action", "initialVersion", "finalVersion"], + "properties": { + "providerAdvertisement": { "type": "boolean" }, + "providerAssets": { "type": "boolean" }, + "movement": { "type": "boolean" }, + "targeting": { "type": "boolean" }, + "action": { "type": "boolean" }, + "initialVersion": { "type": "number", "minimum": 0 }, + "finalVersion": { "type": "number", "minimum": 0 }, + "movementDistance": { "type": "number", "minimum": 0 } + }, + "additionalProperties": true + }, + "receipts": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["commandKind", "commandId", "state", "sourceVersion"], + "properties": { + "commandKind": { "type": "string", "minLength": 1 }, + "commandId": { "type": "string", "minLength": 1 }, + "state": { "enum": ["accepted", "reconciled"] }, + "sourceVersion": { "type": "number", "minimum": 0 } + }, + "additionalProperties": true + } + }, + "screenshotMetrics": { + "type": "object", + "required": ["width", "height", "encodedSizeBytes", "sha256"], + "properties": { + "width": { "type": "integer", "minimum": 1 }, + "height": { "type": "integer", "minimum": 1 }, + "encodedSizeBytes": { "type": "integer", "minimum": 1 }, + "sha256": { "type": "string", "pattern": "^[a-fA-F0-9]{64}$" } + }, + "additionalProperties": true + }, + "artifacts": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["kind", "path", "sha256", "sizeBytes"], + "properties": { + "kind": { "type": "string", "minLength": 1 }, + "path": { "type": "string", "minLength": 1 }, + "sha256": { "type": "string", "pattern": "^[a-fA-F0-9]{64}$" }, + "sizeBytes": { "type": "integer", "minimum": 1 }, + "width": { "type": "integer", "minimum": 1 }, + "height": { "type": "integer", "minimum": 1 }, + "exportPath": { "type": "string" } + }, + "additionalProperties": true + } + }, + "authority": { "type": "string", "minLength": 1 } + }, + "additionalProperties": true +} diff --git a/tools/conformance/attach-runtime-witness.mjs b/tools/conformance/attach-runtime-witness.mjs new file mode 100644 index 0000000..3670df5 --- /dev/null +++ b/tools/conformance/attach-runtime-witness.mjs @@ -0,0 +1,78 @@ +import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { validateSchemaSubset } from "../../runtimes/incubating/test-support/schema-subset.mjs"; + +const [witnessArgument, exportArgument] = process.argv.slice(2); +if (!witnessArgument || !exportArgument) { + console.error("Usage: node tools/conformance/attach-runtime-witness.mjs "); + process.exit(2); +} + +const witnessPath = path.resolve(witnessArgument); +const exportDirectory = path.resolve(exportArgument); +const indexPath = path.join(exportDirectory, "index.json"); +const witness = parseJson(await readFile(witnessPath, "utf8")); +validateWitness(witness); +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const witnessSchema = parseJson(await readFile(path.join(repoRoot, "schemas/gamecult.eve.runtime_witness.v1.schema.json"), "utf8")); +const schemaErrors = validateSchemaSubset(witnessSchema, witness); +if (schemaErrors.length) throw new Error(`Runtime witness schema errors: ${schemaErrors.join("; ")}`); + +const attachmentRoot = path.join(exportDirectory, "attachments", "runtime", safeSegment(witness.witnessId)); +await mkdir(attachmentRoot, { recursive: true }); +const artifacts = []; +for (const artifact of witness.artifacts) { + const sourcePath = path.resolve(path.dirname(witnessPath), artifact.path); + const fileName = path.basename(sourcePath); + const destinationPath = path.join(attachmentRoot, fileName); + await copyFile(sourcePath, destinationPath); + artifacts.push({ + ...artifact, + exportPath: slash(path.relative(exportDirectory, destinationPath)), + }); +} + +const exportedWitness = { ...witness, artifacts }; +const exportedWitnessPath = path.join(attachmentRoot, "witness.json"); +await writeFile(exportedWitnessPath, `${JSON.stringify(exportedWitness, null, 2)}\n`); + +const index = parseJson(await readFile(indexPath, "utf8")); +const record = { + witnessId: witness.witnessId, + runtimeId: witness.runtimeId, + runtimeOwnerRepo: witness.runtimeOwnerRepo, + providerId: witness.providerId, + surfaceId: witness.surfaceId, + projectionKind: witness.projectionKind, + status: witness.status, + cacheState: witness.execution.cacheState, + witnessExportPath: slash(path.relative(exportDirectory, exportedWitnessPath)), + artifactCount: artifacts.length, +}; +index.runtimeWitnesses = [ + ...(Array.isArray(index.runtimeWitnesses) ? index.runtimeWitnesses : []).filter(candidate => candidate.witnessId !== record.witnessId), + record, +].sort((left, right) => left.witnessId.localeCompare(right.witnessId)); +await writeFile(indexPath, `${JSON.stringify(index, null, 2)}\n`); +console.log(exportedWitnessPath); + +function validateWitness(value) { + for (const field of ["witnessId", "runtimeId", "runtimeOwnerRepo", "providerId", "surfaceId", "projectionKind", "status", "generatedAtUtc", "authority"]) { + if (!value?.[field]) throw new Error(`Runtime witness missing ${field}.`); + } + if (value.schema !== "gamecult.eve.runtime_witness.v1") throw new Error(`Unexpected runtime witness schema: ${value.schema || ""}`); + if (!value.execution?.cacheState || !Number.isFinite(value.execution?.durationMs)) throw new Error("Runtime witness execution is incomplete."); + if (!value.assertions || typeof value.assertions !== "object") throw new Error("Runtime witness assertions are missing."); + if (!Array.isArray(value.receipts) || !value.receipts.length) throw new Error("Runtime witness receipts are missing."); + if (!Array.isArray(value.artifacts) || !value.artifacts.length) throw new Error("Runtime witness artifacts are missing."); + for (const artifact of value.artifacts) { + if (!artifact.path || !/^[a-f0-9]{64}$/i.test(artifact.sha256 || "") || !(artifact.sizeBytes > 0)) { + throw new Error(`Runtime witness artifact is invalid: ${artifact.path || "unknown"}`); + } + } +} + +function safeSegment(value) { return String(value).replace(/[^a-zA-Z0-9._-]+/g, "-"); } +function slash(value) { return value.replaceAll("\\", "/"); } +function parseJson(value) { return JSON.parse(value.replace(/^\uFEFF/, "")); } diff --git a/tools/conformance/consume-export.mjs b/tools/conformance/consume-export.mjs index 37e7cf5..71e820e 100644 --- a/tools/conformance/consume-export.mjs +++ b/tools/conformance/consume-export.mjs @@ -59,6 +59,7 @@ if (!exportDirectory) { " --expect-runtime-command-schema ", " --expect-runtime-capture-status ", " --expect-runtime-capture-artifact ", + " --expect-runtime-witness ", " --expect-runtime-lifecycle-status ", " --expect-runtime-lifecycle-pending ", " --expect-runtime-lifecycle-field ", @@ -154,6 +155,7 @@ function validateIndex(index, directory, expectations, errors) { const commandBoundaryCoverage = Array.isArray(index.commandBoundaryCoverage) ? index.commandBoundaryCoverage : []; const screenshotComparisonMetrics = Array.isArray(index.screenshotComparisonMetrics) ? index.screenshotComparisonMetrics : []; const runtimeCaptureProbeCoverage = Array.isArray(index.runtimeCaptureProbeCoverage) ? index.runtimeCaptureProbeCoverage : []; + const runtimeWitnesses = Array.isArray(index.runtimeWitnesses) ? index.runtimeWitnesses : []; const splitTargetBlockers = Array.isArray(index.splitTargetBlockers) ? index.splitTargetBlockers : []; const splitHandoffMoveCoverage = Array.isArray(index.splitHandoffMoveCoverage) ? index.splitHandoffMoveCoverage : []; const pluginHandoffMoveCoverage = Array.isArray(index.pluginHandoffMoveCoverage) ? index.pluginHandoffMoveCoverage : []; @@ -190,6 +192,7 @@ function validateIndex(index, directory, expectations, errors) { validateCommandBoundaryCoverage(index.commandBoundaryCoverage, errors); validateScreenshotComparisonMetrics(index.screenshotComparisonMetrics, errors); validateRuntimeCaptureProbeCoverage(index.runtimeCaptureProbeCoverage, errors); + validateRuntimeWitnesses(runtimeWitnesses, directory, errors); validateWorldSurfaceLoweringGaps(index.worldSurfaceLoweringGaps, errors); for (const expectedGap of expectations.capabilityGaps) { if (!capabilityGaps.some(gap => capabilityGapText(gap).includes(expectedGap))) { @@ -211,6 +214,20 @@ function validateIndex(index, directory, expectations, errors) { errors.push(`runtimePluginProjectionCoverage:${expectation.runtimeId}:${expectation.pluginId}:runtimeOwnerRepo:expected ${expectation.ownerRepo} got ${record.runtimeOwnerRepo || ""}`); } } + for (const expectation of expectations.runtimeWitnesses) { + const witness = runtimeWitnesses.find(candidate => + candidate.runtimeId === expectation.runtimeId && + candidate.providerId === expectation.providerId && + candidate.surfaceId === expectation.surfaceId && + candidate.cacheState === expectation.cacheState); + if (!witness) { + errors.push(`runtimeWitnesses:${expectation.runtimeId}:${expectation.providerId}:${expectation.surfaceId}:${expectation.cacheState}:missing`); + continue; + } + if (witness.status !== expectation.status) { + errors.push(`runtimeWitnesses:${witness.witnessId}:status:expected ${expectation.status} got ${witness.status || ""}`); + } + } for (const expectation of expectations.providerRuntimePluginProjectionCoverage) { const record = providerRuntimePluginProjectionCoverage.find(candidate => candidate.providerId === expectation.providerId && @@ -1239,6 +1256,28 @@ function validateRuntimeRecords(runtimes, errors) { } } +function validateRuntimeWitnesses(witnesses, directory, errors) { + if (!Array.isArray(witnesses)) return; + for (const [index, witness] of witnesses.entries()) { + for (const field of ["witnessId", "runtimeId", "runtimeOwnerRepo", "providerId", "surfaceId", "projectionKind", "status", "cacheState", "witnessExportPath", "artifactCount"]) { + if (witness?.[field] === undefined || witness?.[field] === "") errors.push(`runtimeWitnesses:${index}:${field}:missing`); + } + const witnessPath = witness?.witnessExportPath ? path.join(directory, witness.witnessExportPath) : ""; + if (!witnessPath || !existsSync(witnessPath)) { + errors.push(`runtimeWitnesses:${index}:witnessExportPath:missing`); + continue; + } + const document = JSON.parse(readFileSyncUtf8(witnessPath)); + if (document.schema !== "gamecult.eve.runtime_witness.v1") errors.push(`runtimeWitnesses:${index}:schema:unexpected`); + if ((document.artifacts || []).length !== witness.artifactCount) errors.push(`runtimeWitnesses:${index}:artifactCount:mismatch`); + for (const artifact of document.artifacts || []) { + if (!artifact.exportPath || !existsSync(path.join(directory, artifact.exportPath))) { + errors.push(`runtimeWitnesses:${index}:artifact:${artifact.kind || "unknown"}:missing`); + } + } + } +} + function validateSplitTargetRecords(splitTargets, errors) { for (const target of splitTargets) { const label = `splitTargets:${target.id || "unknown"}`; @@ -1325,6 +1364,7 @@ function parseArguments(args) { runtimeCommandSchemas: [], runtimeCaptureStatuses: [], runtimeCaptureArtifacts: [], + runtimeWitnesses: [], runtimeLifecycleStatuses: [], runtimeLifecyclePendingProofs: [], runtimeLifecycleFields: [], @@ -1387,6 +1427,7 @@ function parseArguments(args) { ["--expect-runtime-command-schema", expectations.runtimeCommandSchemas], ["--expect-runtime-capture-status", expectations.runtimeCaptureStatuses], ["--expect-runtime-capture-artifact", expectations.runtimeCaptureArtifacts], + ["--expect-runtime-witness", expectations.runtimeWitnesses], ["--expect-runtime-lifecycle-status", expectations.runtimeLifecycleStatuses], ["--expect-runtime-lifecycle-pending", expectations.runtimeLifecyclePendingProofs], ["--expect-runtime-lifecycle-field", expectations.runtimeLifecycleFields], @@ -1485,6 +1526,8 @@ function parseArguments(args) { target.push(parseRuntimeExpectation(value, "status")); } else if (option === "--expect-runtime-capture-artifact") { target.push(parseRuntimeCaptureArtifactExpectation(value)); + } else if (option === "--expect-runtime-witness") { + target.push(parseRuntimeWitnessExpectation(value)); } else if (option === "--expect-runtime-lifecycle-status") { target.push(parseRuntimeLifecycleExpectation(value, "status")); } else if (option === "--expect-runtime-lifecycle-pending") { @@ -1910,6 +1953,21 @@ function parseRuntimeExpectation(value, field) { }; } +function parseRuntimeWitnessExpectation(value) { + const parts = value.split(":"); + if (parts.length !== 5 || parts.some(part => !part)) { + console.error(`Expected runtime witness in form, got: ${value}`); + process.exit(2); + } + return { + runtimeId: parts[0], + providerId: parts[1], + surfaceId: parts[2], + cacheState: parts[3], + status: parts[4], + }; +} + function parseRuntimeWorldTargetExpectation(value) { const separator = value.indexOf(":"); if (separator <= 0 || separator === value.length - 1) { diff --git a/tools/conformance/eveconformance-handoff.json b/tools/conformance/eveconformance-handoff.json index 888c80b..b4b4189 100644 --- a/tools/conformance/eveconformance-handoff.json +++ b/tools/conformance/eveconformance-handoff.json @@ -43,7 +43,8 @@ "schemas/gamecult.eve.plugin_abi.request.v1.schema.json", "schemas/gamecult.eve.plugin_abi.response.v1.schema.json", "schemas/gamecult.eve.runtime_release_request.v1.schema.json", - "schemas/gamecult.eve.runtime_capture_request.v1.schema.json" + "schemas/gamecult.eve.runtime_capture_request.v1.schema.json", + "schemas/gamecult.eve.runtime_witness.v1.schema.json" ], "replacementProof": "EveConformance publishes the schema catalog and copied schema documents needed by conformance, plugin, provider, and runtime consumers." }, @@ -92,6 +93,7 @@ "gamecult.eve.plugin_abi.response.v1", "gamecult.eve.runtime_release_request.v1", "gamecult.eve.runtime_capture_request.v1", + "gamecult.eve.runtime_witness.v1", "gamecult.eve.provider_advertisement.v1", "gamecult.eve.provider_handoff.v1", "gamecult.eve.electron_shell_projection.v1", diff --git a/tools/parity/parity-manifest.json b/tools/parity/parity-manifest.json index ee55cc1..5bdd972 100644 --- a/tools/parity/parity-manifest.json +++ b/tools/parity/parity-manifest.json @@ -25,6 +25,7 @@ "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", + "gamecult.eve.runtime_witness.v1": "schemas/gamecult.eve.runtime_witness.v1.schema.json", "gamecult.eve.web_layout_probe.v1": "schemas/gamecult.eve.web_layout_probe.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",