Export plugin handoff move coverage

This commit is contained in:
Meta Vens 2026-07-09 15:52:05 +01:00
commit e0bb031b66
6 changed files with 213 additions and 13 deletions

View file

@ -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

View file

@ -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 <pluginId:moveSetId:pathKind:status:path-substring>`.
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`,

View file

@ -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": {

View file

@ -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 `

View file

@ -36,6 +36,7 @@ if (!exportDirectory) {
" --expect-plugin-abi-field <pluginId:operation:field.path:value>",
" --expect-plugin-abi-operation-coverage <pluginId:operation:status:ownerRepo>",
" --expect-plugin-handoff <pluginId>",
" --expect-plugin-handoff-move <pluginId:moveSetId:pathKind:status:path-substring>",
" --expect-provider-surface <providerId:surfaceId>",
" --expect-provider-surface-kind <providerId:surfaceId:surfaceKind>",
" --expect-provider-surface-field <providerId:surfaceId:field.path:value>",
@ -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 <pluginId:moveSetId:pathKind:status:path-substring> 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) {

View file

@ -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 || []) {