From b9f7592edf18fd6dd2f5499c5faa6acb57fd1949 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 9 Jul 2026 03:16:59 +0100 Subject: [PATCH] Export split handoff move coverage --- docs/eve-migration-roadmap.md | 6 ++ docs/parity-testing-harness.md | 25 ++++-- ...cult.eve.conformance_export.v1.schema.json | 80 +++++++++++++++++++ ...plit-target-conformance-consumer-smoke.ps1 | 4 + tools/conformance/consume-export.mjs | 50 ++++++++++++ tools/parity/run-parity.mjs | 71 +++++++++++++++- 6 files changed, 229 insertions(+), 7 deletions(-) diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index 1f5b141..dc65c34 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -310,6 +310,12 @@ Recently cut: and Unity UI Toolkit are `covered` because they advertise `gamecult.eve.command.v1`; Unity scene and TUI stay visible as `missing-runtime-claim` until their generic lowerers exist. +- The conformance export carries `splitHandoffMoveCoverage[]`, joining runtime + split handoff move sets to source path status. EveElectron now exposes the + existing Aetheria `Aetheria.Rts.Web/Electron` and command transport paths as + `observed-provider` sources that must be replaced by a provider-agnostic + shell; Unity scene exposes `replacement-required` move sets because no + generic scene body or command transport source exists yet. - The conformance export schema and consumer smoke now require minimum typed item shapes for plugins, providers, runtimes, and split targets. Owner repos can rely on exported ids, owners, statuses, split targets, manifest or diff --git a/docs/parity-testing-harness.md b/docs/parity-testing-harness.md index cb04786..e6711e1 100644 --- a/docs/parity-testing-harness.md +++ b/docs/parity-testing-harness.md @@ -183,6 +183,15 @@ typed `blockerRecords[]` / root `splitTargetBlockers[]` entries. Split-target consumers can assert blocker kind and subject with `--expect-split-target-blocker-record ` instead of scraping prose from handoff notes. +Runtime split handoff move/source coverage is exported as +`splitHandoffMoveCoverage[]`. Each record joins a split target, runtime id, +handoff move set, destination owner, replacement proof, and source path status. +`current` paths are Eve-incubated code that still needs to leave, +`observed-provider` paths are provider-owned pressure sources such as +Aetheria's current Electron client, and `replacement-required` records mark +move sets where no generic runtime source exists yet. Consumer smokes can +assert them with +`--expect-split-handoff-move `. After generating the parity report, `run-parity-harness.ps1` copies `artifacts/conformance/latest` into @@ -266,12 +275,16 @@ target being present is not enough; future owner repos must be able to consume why it is or is not ready to leave Eve incubation. EveElectron is intentionally pending: its handoff names the generic Electron shell boundary, while the Starbridge RTS client remains Aetheria-owned product code until a -provider-agnostic shell exists. EveTui is also intentionally pending: Aetheria -advertises `tui` as an agent-facing lowering target, but Eve should not claim -that target until a real terminal/grid lowerer and transcript capture exist. -EveUnity includes both the active `unity-uitoolkit` package proof and the -pending `unity-scene` runtime boundary; UI Toolkit support is not treated as -full Unity scene/world lowering. +provider-agnostic shell exists. The same smoke asserts that the Aetheria +Electron source paths named by the handoff currently exist as +`observed-provider` sources, so the extraction pressure is visible without +turning those paths into EveElectron authority. EveTui is also intentionally +pending: Aetheria advertises `tui` as an agent-facing lowering target, but Eve +should not claim that target until a real terminal/grid lowerer and transcript +capture exist. EveUnity includes both the active `unity-uitoolkit` package proof +and the pending `unity-scene` runtime boundary; UI Toolkit support is not +treated as full Unity scene/world lowering, and `unity-scene` move sets remain +`replacement-required` until a generic scene body exists. The Flutter widget smoke also exercises `EveProviderCatalog` and `EveProviderPicker` against a conformance-export-shaped provider list. That is diff --git a/schemas/gamecult.eve.conformance_export.v1.schema.json b/schemas/gamecult.eve.conformance_export.v1.schema.json index 5c500bf..7a02095 100644 --- a/schemas/gamecult.eve.conformance_export.v1.schema.json +++ b/schemas/gamecult.eve.conformance_export.v1.schema.json @@ -17,6 +17,7 @@ "worldSurfaceLoweringCoverage", "commandBoundaryCoverage", "splitTargetBlockers", + "splitHandoffMoveCoverage", "splitTargets" ], "properties": { @@ -838,6 +839,85 @@ "additionalProperties": true } }, + "splitHandoffMoveCoverage": { + "type": "array", + "items": { + "type": "object", + "required": [ + "splitTarget", + "runtimeId", + "runtimeOwnerRepo", + "handoffPath", + "handoffExportPath", + "moveSetId", + "stage", + "destinationOwner", + "replacementProof", + "pathKind", + "sourcePath", + "status", + "severity" + ], + "properties": { + "splitTarget": { + "type": "string", + "minLength": 1 + }, + "runtimeId": { + "type": "string", + "minLength": 1 + }, + "runtimeOwnerRepo": { + "type": "string", + "minLength": 1 + }, + "handoffPath": { + "type": "string", + "minLength": 1 + }, + "handoffExportPath": { + "type": "string", + "minLength": 1 + }, + "moveSetId": { + "type": "string", + "minLength": 1 + }, + "stage": { + "type": "string", + "minLength": 1 + }, + "destinationOwner": { + "type": "string", + "minLength": 1 + }, + "replacementProof": { + "type": "string", + "minLength": 1 + }, + "pathKind": { + "type": "string", + "minLength": 1 + }, + "sourcePath": { + "type": "string", + "minLength": 1 + }, + "pathExists": { + "type": "boolean" + }, + "status": { + "type": "string", + "minLength": 1 + }, + "severity": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": true + } + }, "splitTargets": { "type": "array", "items": { diff --git a/scripts/run-split-target-conformance-consumer-smoke.ps1 b/scripts/run-split-target-conformance-consumer-smoke.ps1 index 06e969e..52b2bcb 100644 --- a/scripts/run-split-target-conformance-consumer-smoke.ps1 +++ b/scripts/run-split-target-conformance-consumer-smoke.ps1 @@ -45,6 +45,8 @@ try { --expect-split-target-blocker-record EveUnity:runtime-status:unity-scene ` --expect-split-target-blocker-record EveUnity:runtime-plugin-projection:unity-scene:sai.vn ` --expect-split-target-blocker-record "EveUnity:pending-proof:Unity scene runtime body exists outside Aetheria product code." ` + --expect-split-handoff-move EveUnity:unity-scene:unity-scene-runtime-body:replacement-required:no-source-paths:none ` + --expect-split-handoff-move EveUnity:unity-scene:unity-scene-command-transport:replacement-required:no-source-paths:none ` --expect-split-target-blocker "EveUnity:Tagged UPM release" ` --expect-split-target-blocker "EveUnity:Unity batchmode EditMode runner" ` --expect-split-target-blocker "EveUnity:Unity batchmode or editor capture artifact" ` @@ -55,6 +57,8 @@ try { --expect-split-target-blocker "EveElectron:runtime:electron-shell:status:pending" ` --expect-split-target-blocker-record EveElectron:runtime-status:electron-shell ` --expect-split-target-blocker-record EveElectron:runtime-feature:electron-shell:providerAdvertisements ` + --expect-split-handoff-move EveElectron:electron-shell:electron-shell-runtime:observed-provider:exists:Aetheria.Rts.Web/Electron ` + --expect-split-handoff-move EveElectron:electron-shell:electron-command-transport:observed-provider:exists:Aetheria.Rts.Web/Client/app.ts ` --expect-split-target-blocker "EveElectron:Electron shell runtime body exists outside Aetheria product code" ` --expect-split-target EveTui ` --expect-split-target-status EveTui:incubating ` diff --git a/tools/conformance/consume-export.mjs b/tools/conformance/consume-export.mjs index 9c1220c..e463a98 100644 --- a/tools/conformance/consume-export.mjs +++ b/tools/conformance/consume-export.mjs @@ -53,6 +53,7 @@ if (!exportDirectory) { " --expect-split-target-status ", " --expect-split-target-blocker ", " --expect-split-target-blocker-record ", + " --expect-split-handoff-move ", " --expect-split-target-proof ", ].join("\n")); process.exit(2); @@ -136,6 +137,7 @@ function validateIndex(index, directory, expectations, errors) { const worldSurfaceLoweringCoverage = Array.isArray(index.worldSurfaceLoweringCoverage) ? index.worldSurfaceLoweringCoverage : []; const commandBoundaryCoverage = Array.isArray(index.commandBoundaryCoverage) ? index.commandBoundaryCoverage : []; const splitTargetBlockers = Array.isArray(index.splitTargetBlockers) ? index.splitTargetBlockers : []; + const splitHandoffMoveCoverage = Array.isArray(index.splitHandoffMoveCoverage) ? index.splitHandoffMoveCoverage : []; const worldSurfaceLoweringGaps = Array.isArray(index.worldSurfaceLoweringGaps) ? index.worldSurfaceLoweringGaps : []; validatePluginRecords(plugins, errors); @@ -158,6 +160,7 @@ function validateIndex(index, directory, expectations, errors) { validateCapabilityGaps(index.capabilityGaps, errors); validateRuntimePluginProjectionGaps(index.runtimePluginProjectionGaps, errors); validateSplitTargetBlockers(index.splitTargetBlockers, errors); + validateSplitHandoffMoveCoverage(index.splitHandoffMoveCoverage, errors); validateWorldSurfaceLoweringCoverage(index.worldSurfaceLoweringCoverage, errors); validateCommandBoundaryCoverage(index.commandBoundaryCoverage, errors); validateWorldSurfaceLoweringGaps(index.worldSurfaceLoweringGaps, errors); @@ -609,6 +612,18 @@ function validateIndex(index, directory, expectations, errors) { errors.push(`splitTargetBlockers:${expectation.targetId}:${expectation.kind}:${expectation.subject}:missing`); } } + for (const expectation of expectations.splitHandoffMoves) { + const record = splitHandoffMoveCoverage.find(candidate => + candidate.splitTarget === expectation.splitTarget && + candidate.runtimeId === expectation.runtimeId && + candidate.moveSetId === expectation.moveSetId && + candidate.pathKind === expectation.pathKind && + candidate.status === expectation.status && + candidate.sourcePath?.includes(expectation.pathSubstring)); + if (!record) { + errors.push(`splitHandoffMoveCoverage:${expectation.splitTarget}:${expectation.runtimeId}:${expectation.moveSetId}:${expectation.pathKind}:${expectation.status}:${expectation.pathSubstring}:missing`); + } + } for (const expectation of expectations.splitTargetProofs) { const target = splitTargets.find(candidate => candidate.id === expectation.targetId); if (!target) { @@ -736,6 +751,21 @@ function validateSplitTargetBlockers(blockers, errors) { } } +function validateSplitHandoffMoveCoverage(records, errors) { + if (!Array.isArray(records)) { + errors.push("splitHandoffMoveCoverage:missing"); + return; + } + for (const [index, record] of records.entries()) { + for (const field of ["splitTarget", "runtimeId", "runtimeOwnerRepo", "handoffPath", "handoffExportPath", "moveSetId", "stage", "destinationOwner", "replacementProof", "pathKind", "sourcePath", "status", "severity"]) { + if (!record?.[field]) errors.push(`splitHandoffMoveCoverage:${index}:${field}:missing`); + } + if (typeof record?.pathExists !== "boolean") { + errors.push(`splitHandoffMoveCoverage:${index}:pathExists:expected boolean`); + } + } +} + function validateWorldSurfaceLoweringCoverage(records, errors) { if (!Array.isArray(records)) { errors.push("worldSurfaceLoweringCoverage:missing"); @@ -931,6 +961,7 @@ function parseArguments(args) { splitTargetStatuses: [], splitTargetBlockers: [], splitTargetBlockerRecords: [], + splitHandoffMoves: [], splitTargetProofs: [], capabilityMatrix: false, capabilityGaps: [], @@ -978,6 +1009,7 @@ function parseArguments(args) { ["--expect-split-target-status", expectations.splitTargetStatuses], ["--expect-split-target-blocker", expectations.splitTargetBlockers], ["--expect-split-target-blocker-record", expectations.splitTargetBlockerRecords], + ["--expect-split-handoff-move", expectations.splitHandoffMoves], ["--expect-split-target-proof", expectations.splitTargetProofs], ["--expect-capability-gap", expectations.capabilityGaps], ["--expect-runtime-plugin-gap", expectations.runtimePluginGaps], @@ -1066,6 +1098,8 @@ function parseArguments(args) { target.push(parseSplitTargetExpectation(value, "blocker")); } else if (option === "--expect-split-target-blocker-record") { target.push(parseSplitTargetBlockerRecordExpectation(value)); + } else if (option === "--expect-split-handoff-move") { + target.push(parseSplitHandoffMoveExpectation(value)); } else if (option === "--expect-split-target-proof") { target.push(parseSplitTargetExpectation(value, "proof")); } else if (option === "--expect-world-lowering-gap") { @@ -1158,6 +1192,22 @@ function parseSplitTargetBlockerRecordExpectation(value) { }; } +function parseSplitHandoffMoveExpectation(value) { + const parts = value.split(":"); + if (parts.length < 6 || parts.slice(0, 5).some(part => !part) || !parts.slice(5).join(":")) { + console.error(`Expected split handoff move in form, got: ${value}`); + process.exit(2); + } + return { + splitTarget: parts[0], + runtimeId: parts[1], + moveSetId: parts[2], + pathKind: parts[3], + status: parts[4], + pathSubstring: parts.slice(5).join(":"), + }; +} + function parsePluginExpectation(value, field) { const separator = value.indexOf(":"); if (separator <= 0 || separator === value.length - 1) { diff --git a/tools/parity/run-parity.mjs b/tools/parity/run-parity.mjs index 7ff8efe..0d1cf7f 100644 --- a/tools/parity/run-parity.mjs +++ b/tools/parity/run-parity.mjs @@ -1,5 +1,5 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { existsSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { compileEveDsl } from "../../web/eve-dsl.js"; @@ -1675,6 +1675,7 @@ function buildConformanceExport(report) { commandBoundaryCoverage: collectCommandBoundaryCoverage(report), worldSurfaceLoweringGaps: collectWorldSurfaceLoweringGaps(report), splitTargetBlockers: collectSplitTargetBlockers(report), + splitHandoffMoveCoverage: collectSplitHandoffMoveCoverage(report), pluginAbiOperationCoverage: collectPluginAbiOperationCoverage(report), providerPluginRequirementCoverage: collectProviderPluginRequirementCoverage(report), capabilityGaps: collectCapabilityGaps(report), @@ -2017,6 +2018,74 @@ function collectSplitTargetBlockers(report) { return (report.splitTargets || []).flatMap(target => target.blockerRecords || []); } +function collectSplitHandoffMoveCoverage(report) { + const records = []; + for (const runtime of report.runtimes || []) { + if (!runtime.splitHandoffPath) continue; + const absoluteHandoffPath = path.join(repoRoot, runtime.splitHandoffPath); + if (!existsSync(absoluteHandoffPath)) continue; + + let handoff; + try { + handoff = JSON.parse(readFileSync(absoluteHandoffPath, "utf8")); + } catch { + continue; + } + + for (const moveSet of handoff.moveSets || []) { + const currentPaths = Array.isArray(moveSet.currentPaths) ? moveSet.currentPaths : []; + const observedProviderPaths = Array.isArray(moveSet.observedProviderPaths) ? moveSet.observedProviderPaths : []; + for (const sourcePath of currentPaths) { + const absoluteSourcePath = path.join(repoRoot, sourcePath); + records.push(buildSplitHandoffMoveRecord(runtime, handoff, moveSet, { + pathKind: "current", + sourcePath, + pathExists: existsSync(absoluteSourcePath), + })); + } + for (const sourcePath of observedProviderPaths) { + records.push(buildSplitHandoffMoveRecord(runtime, handoff, moveSet, { + pathKind: "observed-provider", + sourcePath, + pathExists: existsSync(sourcePath), + })); + } + if (!currentPaths.length && !observedProviderPaths.length) { + records.push(buildSplitHandoffMoveRecord(runtime, handoff, moveSet, { + pathKind: "replacement-required", + sourcePath: "(none)", + pathExists: false, + })); + } + } + } + return records.sort((left, right) => + `${left.splitTarget}:${left.runtimeId}:${left.moveSetId}:${left.pathKind}:${left.sourcePath}` + .localeCompare(`${right.splitTarget}:${right.runtimeId}:${right.moveSetId}:${right.pathKind}:${right.sourcePath}`)); +} + +function buildSplitHandoffMoveRecord(runtime, handoff, moveSet, source) { + const status = source.pathKind === "replacement-required" + ? "no-source-paths" + : source.pathExists ? "exists" : "missing"; + return { + splitTarget: handoff.splitTarget || runtime.splitTarget || "", + runtimeId: handoff.runtimeId || runtime.id || "", + runtimeOwnerRepo: handoff.ownerRepo || resolveRuntimeProjectionOwnerRepo(runtime), + handoffPath: runtime.splitHandoffPath || "", + handoffExportPath: makeHandoffExportPath("runtime", runtime.id, runtime.splitHandoffPath || ""), + moveSetId: moveSet.id || "", + stage: moveSet.stage || "", + destinationOwner: moveSet.destinationOwner || "", + replacementProof: moveSet.replacementProof || "", + pathKind: source.pathKind, + sourcePath: normalizePath(source.sourcePath), + pathExists: source.pathExists, + status, + severity: "blocker", + }; +} + function collectInteractiveWorldSurfaces(report) { const surfaces = []; for (const provider of report.providers || []) {