diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index 22369b5..b1e82db 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -384,9 +384,14 @@ Recently cut: `lower`, `measure`, and `apply` without reading Eve-local fixture files. - Plugin ABI operation coverage is now exported as root `pluginAbiOperationCoverage[]` records. The plugin-owner smoke asserts Sai - and Norn `describe`, `lower`, and `apply` operations as `contracted`, so plugin - owners can consume the coverage ledger without walking Eve's nested plugin - records. + Norn, and TeX `describe`, `lower`, and `apply` operations as `contracted`, so + plugin owners can consume the coverage ledger without walking Eve's nested + plugin records. +- Plugin handoff move/source coverage is now exported as root + `pluginHandoffMoveCoverage[]` records. The plugin-owner smoke asserts Sai, + Norn, and TeX manifest and ABI-conformance move sets as current existing + sources, so owner repos can see what must leave Eve incubation without + reading Eve's parity manifest or plugin handoff files directly. - Provider/plugin requirement coverage is now exported as root `providerPluginRequirementCoverage[]` records with availability. The plugin-owner smoke asserts `sai.vn` as the required VN plugin and @@ -698,15 +703,16 @@ Recently cut: graduates. The conformance export carries `conformanceHandoffPath`, and the generic consumer smoke asserts it so EveConformance split readiness is not trapped in Eve's parity manifest. -- Sai and Norn now have plugin handoff manifests at - `plugins/incubating/sai-vn.plugin-handoff.json` and - `plugins/incubating/norn-graph.plugin-handoff.json`, plus a shared verifier - at `scripts/run-plugin-handoff-smoke.ps1`. The handoffs name the manifest, +- Sai, Norn, and TeX now have plugin handoff manifests at + `plugins/incubating/sai-vn.plugin-handoff.json`, + `plugins/incubating/norn-graph.plugin-handoff.json`, and + `plugins/incubating/tex-math.plugin-handoff.json`, plus a shared verifier at + `scripts/run-plugin-handoff-smoke.ps1`. The handoffs name the manifest, advertisement, ABI fixture, move sets, contract inputs, forbidden imports, and external proofs that must move to the owner repos. The conformance export - carries plugin `handoffPath`, and the plugin-owner smoke asserts it so Sai - and Norn can consume the plugin boundary without reading Eve's parity - manifest. + carries plugin `handoffPath` and `pluginHandoffMoveCoverage[]`, and the + plugin-owner smoke asserts them so Sai, Norn, and EvePlugins can consume the + plugin boundary without reading Eve's parity manifest. - Aetheria now has a provider handoff manifest at `web/fixtures/aetheria-provider-handoff.json` and a verifier at `scripts/run-aetheria-provider-handoff-smoke.ps1`. The handoff names the diff --git a/docs/parity-testing-harness.md b/docs/parity-testing-harness.md index df9fdc5..5cf34c3 100644 --- a/docs/parity-testing-harness.md +++ b/docs/parity-testing-harness.md @@ -172,9 +172,14 @@ generic projection runtime from a runtime that can project plugin-declared capabilities. Plugin handoff paths are exported as `plugins[].handoffPath` for owner repos -such as Sai and Norn. These paths are incubation evidence for where plugin -manifest, advertisement, ABI fixture, and conformance ownership must move; they -are not plugin runtime state. +such as Sai, Norn, and EvePlugins. These paths are incubation evidence for +where plugin manifest, advertisement, ABI fixture, and conformance ownership +must move; they are not plugin runtime state. +Plugin handoff move/source coverage is exported as +`pluginHandoffMoveCoverage[]`. Each record joins plugin id, owner repo, split +target, handoff move set, destination owner, replacement proof, and current +source path status. Consumer smokes can assert them with +`--expect-plugin-handoff-move `. Plugin ABI fixture operations are exported as `plugins[].abiOperationContracts` with `operation`, `input`, and `expect` blocks. Plugin owners can consume the operation-level contract for `describe`, `validate`, `project`, `lower`, diff --git a/schemas/gamecult.eve.conformance_export.v1.schema.json b/schemas/gamecult.eve.conformance_export.v1.schema.json index 5297565..176eab4 100644 --- a/schemas/gamecult.eve.conformance_export.v1.schema.json +++ b/schemas/gamecult.eve.conformance_export.v1.schema.json @@ -22,6 +22,7 @@ "screenshotComparisonMetrics", "splitTargetBlockers", "splitHandoffMoveCoverage", + "pluginHandoffMoveCoverage", "splitTargets" ], "properties": { @@ -910,6 +911,80 @@ "additionalProperties": true } }, + "pluginHandoffMoveCoverage": { + "type": "array", + "items": { + "type": "object", + "required": [ + "pluginId", + "pluginOwnerRepo", + "splitTarget", + "handoffPath", + "handoffExportPath", + "moveSetId", + "destinationOwner", + "replacementProof", + "pathKind", + "sourcePath", + "status", + "severity" + ], + "properties": { + "pluginId": { + "type": "string", + "minLength": 1 + }, + "pluginOwnerRepo": { + "type": "string", + "minLength": 1 + }, + "splitTarget": { + "type": "string", + "minLength": 1 + }, + "handoffPath": { + "type": "string", + "minLength": 1 + }, + "handoffExportPath": { + "type": "string", + "minLength": 1 + }, + "moveSetId": { + "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 + } + }, "providers": { "type": "array", "items": { diff --git a/scripts/run-plugin-owner-conformance-consumer-smoke.ps1 b/scripts/run-plugin-owner-conformance-consumer-smoke.ps1 index 22ce168..2d97887 100644 --- a/scripts/run-plugin-owner-conformance-consumer-smoke.ps1 +++ b/scripts/run-plugin-owner-conformance-consumer-smoke.ps1 @@ -47,6 +47,12 @@ node $consumerScript $consumerExport ` --expect-plugin-handoff sai.vn ` --expect-plugin-handoff norn.graph ` --expect-plugin-handoff tex.math ` + --expect-plugin-handoff-move sai.vn:plugin-manifest:current:exists:sai-vn.plugin.json ` + --expect-plugin-handoff-move sai.vn:abi-conformance:current:exists:sai-vn.plugin-abi-fixture.json ` + --expect-plugin-handoff-move norn.graph:plugin-manifest:current:exists:norn-graph.plugin.json ` + --expect-plugin-handoff-move norn.graph:abi-conformance:current:exists:norn-graph.plugin-abi-fixture.json ` + --expect-plugin-handoff-move tex.math:plugin-manifest:current:exists:tex-math.plugin.json ` + --expect-plugin-handoff-move tex.math:abi-conformance:current:exists:tex-math.plugin-abi-fixture.json ` --expect-plugin-operation sai.vn:describe ` --expect-plugin-operation sai.vn:validate ` --expect-plugin-operation sai.vn:project ` diff --git a/tools/conformance/consume-export.mjs b/tools/conformance/consume-export.mjs index 80415ac..b04d4b1 100644 --- a/tools/conformance/consume-export.mjs +++ b/tools/conformance/consume-export.mjs @@ -36,6 +36,7 @@ if (!exportDirectory) { " --expect-plugin-abi-field ", " --expect-plugin-abi-operation-coverage ", " --expect-plugin-handoff ", + " --expect-plugin-handoff-move ", " --expect-provider-surface ", " --expect-provider-surface-kind ", " --expect-provider-surface-field ", @@ -147,6 +148,7 @@ function validateIndex(index, directory, expectations, errors) { const screenshotComparisonMetrics = Array.isArray(index.screenshotComparisonMetrics) ? index.screenshotComparisonMetrics : []; const splitTargetBlockers = Array.isArray(index.splitTargetBlockers) ? index.splitTargetBlockers : []; const splitHandoffMoveCoverage = Array.isArray(index.splitHandoffMoveCoverage) ? index.splitHandoffMoveCoverage : []; + const pluginHandoffMoveCoverage = Array.isArray(index.pluginHandoffMoveCoverage) ? index.pluginHandoffMoveCoverage : []; const worldSurfaceLoweringGaps = Array.isArray(index.worldSurfaceLoweringGaps) ? index.worldSurfaceLoweringGaps : []; validatePluginRecords(plugins, errors); @@ -172,6 +174,7 @@ function validateIndex(index, directory, expectations, errors) { validateRuntimePluginProjectionGaps(index.runtimePluginProjectionGaps, errors); validateSplitTargetBlockers(index.splitTargetBlockers, errors); validateSplitHandoffMoveCoverage(index.splitHandoffMoveCoverage, errors); + validatePluginHandoffMoveCoverage(index.pluginHandoffMoveCoverage, errors); validateWorldSurfaceLoweringCoverage(index.worldSurfaceLoweringCoverage, errors); validateCommandBoundaryCoverage(index.commandBoundaryCoverage, errors); validateScreenshotComparisonMetrics(index.screenshotComparisonMetrics, errors); @@ -704,6 +707,17 @@ function validateIndex(index, directory, expectations, errors) { errors.push(`splitHandoffMoveCoverage:${expectation.splitTarget}:${expectation.runtimeId}:${expectation.moveSetId}:${expectation.pathKind}:${expectation.status}:${expectation.pathSubstring}:missing`); } } + for (const expectation of expectations.pluginHandoffMoves) { + const record = pluginHandoffMoveCoverage.find(candidate => + candidate.pluginId === expectation.pluginId && + candidate.moveSetId === expectation.moveSetId && + candidate.pathKind === expectation.pathKind && + candidate.status === expectation.status && + candidate.sourcePath?.includes(expectation.pathSubstring)); + if (!record) { + errors.push(`pluginHandoffMoveCoverage:${expectation.pluginId}:${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) { @@ -885,6 +899,21 @@ function validateSplitHandoffMoveCoverage(records, errors) { } } +function validatePluginHandoffMoveCoverage(records, errors) { + if (!Array.isArray(records)) { + errors.push("pluginHandoffMoveCoverage:missing"); + return; + } + for (const [index, record] of records.entries()) { + for (const field of ["pluginId", "pluginOwnerRepo", "splitTarget", "handoffPath", "handoffExportPath", "moveSetId", "destinationOwner", "replacementProof", "pathKind", "sourcePath", "status", "severity"]) { + if (!record?.[field]) errors.push(`pluginHandoffMoveCoverage:${index}:${field}:missing`); + } + if (typeof record?.pathExists !== "boolean") { + errors.push(`pluginHandoffMoveCoverage:${index}:pathExists:expected boolean`); + } + } +} + function validateWorldSurfaceLoweringCoverage(records, errors) { if (!Array.isArray(records)) { errors.push("worldSurfaceLoweringCoverage:missing"); @@ -1104,6 +1133,7 @@ function parseArguments(args) { pluginAbiFields: [], pluginAbiOperationCoverage: [], pluginHandoffs: [], + pluginHandoffMoves: [], providers: [], providerSurfaces: [], providerSurfaceKinds: [], @@ -1161,6 +1191,7 @@ function parseArguments(args) { ["--expect-plugin-abi-field", expectations.pluginAbiFields], ["--expect-plugin-abi-operation-coverage", expectations.pluginAbiOperationCoverage], ["--expect-plugin-handoff", expectations.pluginHandoffs], + ["--expect-plugin-handoff-move", expectations.pluginHandoffMoves], ["--expect-provider-surface", expectations.providerSurfaces], ["--expect-provider-surface-kind", expectations.providerSurfaceKinds], ["--expect-provider-surface-field", expectations.providerSurfaceFields], @@ -1233,6 +1264,8 @@ function parseArguments(args) { target.push(parsePluginAbiOperationCoverageExpectation(value)); } else if (option === "--expect-plugin-handoff") { target.push(value); + } else if (option === "--expect-plugin-handoff-move") { + target.push(parsePluginHandoffMoveExpectation(value)); } else if (option === "--expect-provider-surface") { target.push(parseProviderExpectation(value, "surfaceId")); } else if (option === "--expect-provider-surface-kind") { @@ -1452,6 +1485,21 @@ function parseSplitHandoffMoveExpectation(value) { }; } +function parsePluginHandoffMoveExpectation(value) { + const parts = value.split(":"); + if (parts.length < 5 || parts.slice(0, 4).some(part => !part) || !parts.slice(4).join(":")) { + console.error(`Expected plugin handoff move in form, got: ${value}`); + process.exit(2); + } + return { + pluginId: parts[0], + moveSetId: parts[1], + pathKind: parts[2], + status: parts[3], + pathSubstring: parts.slice(4).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 6ec55fe..3db9e5b 100644 --- a/tools/parity/run-parity.mjs +++ b/tools/parity/run-parity.mjs @@ -1967,6 +1967,7 @@ function buildConformanceExport(report) { worldSurfaceLoweringGaps: collectWorldSurfaceLoweringGaps(report), splitTargetBlockers: collectSplitTargetBlockers(report), splitHandoffMoveCoverage: collectSplitHandoffMoveCoverage(report), + pluginHandoffMoveCoverage: collectPluginHandoffMoveCoverage(report), pluginAbiOperationCoverage: collectPluginAbiOperationCoverage(report), providerPluginRequirementCoverage: collectProviderPluginRequirementCoverage(report), capabilityGaps: collectCapabilityGaps(report), @@ -2801,6 +2802,65 @@ function buildSplitHandoffMoveRecord(runtime, handoff, moveSet, source) { }; } +function collectPluginHandoffMoveCoverage(report) { + const records = []; + for (const plugin of report.plugins || []) { + if (!plugin.handoffPath) continue; + const absoluteHandoffPath = path.join(repoRoot, plugin.handoffPath); + 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 : []; + for (const sourcePath of currentPaths) { + const absoluteSourcePath = path.join(repoRoot, sourcePath); + records.push(buildPluginHandoffMoveRecord(plugin, handoff, moveSet, { + pathKind: "current", + sourcePath, + pathExists: existsSync(absoluteSourcePath), + })); + } + if (!currentPaths.length) { + records.push(buildPluginHandoffMoveRecord(plugin, handoff, moveSet, { + pathKind: "replacement-required", + sourcePath: "(none)", + pathExists: false, + })); + } + } + } + return records.sort((left, right) => + `${left.pluginId}:${left.moveSetId}:${left.pathKind}:${left.sourcePath}` + .localeCompare(`${right.pluginId}:${right.moveSetId}:${right.pathKind}:${right.sourcePath}`)); +} + +function buildPluginHandoffMoveRecord(plugin, handoff, moveSet, source) { + const status = source.pathKind === "replacement-required" + ? "no-source-paths" + : source.pathExists ? "exists" : "missing"; + return { + pluginId: handoff.pluginId || plugin.pluginId || "", + pluginOwnerRepo: handoff.ownerRepo || plugin.ownerRepo || "", + splitTarget: handoff.splitTarget || plugin.splitTarget || "", + handoffPath: plugin.handoffPath || "", + handoffExportPath: makeHandoffExportPath("plugin", plugin.pluginId, plugin.handoffPath || ""), + moveSetId: moveSet.id || "", + 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 || []) {