diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index d7cbace..20105fa 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -304,6 +304,10 @@ Recently cut: can rely on exported ids, owners, statuses, split targets, manifest or advertisement paths, feature/plugin arrays, and blocker/proof arrays instead of treating those sections as untyped bags. +- Plugin ABI fixture operations are now exported as + `plugins[].abiOperationContracts`. Sai and Norn can consume operation-level + `input` and `expect` contracts for `describe`, `validate`, `project`, + `lower`, `measure`, and `apply` without reading Eve-local fixture files. - EveUnity now has a pending Unity scene handoff at `runtimes/incubating/eve-unity-scene/eveunity-scene-split-handoff.json`. The active `unity-uitoolkit` runtime remains a semantic UI surface lowerer; diff --git a/docs/parity-testing-harness.md b/docs/parity-testing-harness.md index 6018ec0..fc5c4dc 100644 --- a/docs/parity-testing-harness.md +++ b/docs/parity-testing-harness.md @@ -123,6 +123,10 @@ 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. +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`, +`measure`, and `apply` without opening Eve-local fixture files. The runtime conformance pack is evidence-shaped, not fixture-shaped. Its `packs/runtime.json` document includes `runtimeTargets` so runtime repos can diff --git a/schemas/gamecult.eve.conformance_export.v1.schema.json b/schemas/gamecult.eve.conformance_export.v1.schema.json index ca21073..1733583 100644 --- a/schemas/gamecult.eve.conformance_export.v1.schema.json +++ b/schemas/gamecult.eve.conformance_export.v1.schema.json @@ -206,6 +206,30 @@ "abiOperations": { "type": "array" }, + "abiOperationContracts": { + "type": "array", + "items": { + "type": "object", + "required": [ + "operation", + "input", + "expect" + ], + "properties": { + "operation": { + "type": "string", + "minLength": 1 + }, + "input": { + "type": "object" + }, + "expect": { + "type": "object" + } + }, + "additionalProperties": true + } + }, "runtimeBoundary": { "type": "object" }, diff --git a/scripts/run-plugin-owner-conformance-consumer-smoke.ps1 b/scripts/run-plugin-owner-conformance-consumer-smoke.ps1 index af598f2..a66f1c8 100644 --- a/scripts/run-plugin-owner-conformance-consumer-smoke.ps1 +++ b/scripts/run-plugin-owner-conformance-consumer-smoke.ps1 @@ -63,6 +63,8 @@ node $consumerScript $consumerExport ` --expect-plugin-runtime-field sai.vn:sidecar.requestSchema:gamecult.eve.plugin_abi.request.v1 ` --expect-plugin-runtime-field sai.vn:sidecar.responseSchema:gamecult.eve.plugin_abi.response.v1 ` --expect-plugin-runtime-field sai.vn:sidecar.stateAuthority:proposes-plugin-state-only-provider-accepts ` + --expect-plugin-abi-field sai.vn:lower:expect.commandEnvelope:gamecult.eve.command.v1 ` + --expect-plugin-abi-field sai.vn:apply:expect.receiptSchema:gamecult.eve.command_receipt.v1 ` --expect-plugin-runtime norn.graph:executable-sidecar ` --expect-plugin-runtime-transport norn.graph:cultmesh ` --expect-plugin-runtime-authority norn.graph:no-provider-state-mutation ` @@ -70,7 +72,9 @@ node $consumerScript $consumerExport ` --expect-plugin-runtime-field norn.graph:sidecar.protocol:cultmesh-rpc-with-stdio-dev-transport ` --expect-plugin-runtime-field norn.graph:sidecar.requestSchema:gamecult.eve.plugin_abi.request.v1 ` --expect-plugin-runtime-field norn.graph:sidecar.responseSchema:gamecult.eve.plugin_abi.response.v1 ` - --expect-plugin-runtime-field norn.graph:sidecar.stateAuthority:proposes-plugin-state-only-provider-accepts + --expect-plugin-runtime-field norn.graph:sidecar.stateAuthority:proposes-plugin-state-only-provider-accepts ` + --expect-plugin-abi-field norn.graph:lower:expect.commandEnvelope:gamecult.eve.command.v1 ` + --expect-plugin-abi-field norn.graph:apply:expect.receiptSchema:gamecult.eve.command_receipt.v1 if ($LASTEXITCODE -ne 0) { throw "Plugin owner conformance consumer smoke failed with exit code $LASTEXITCODE" diff --git a/tools/conformance/consume-export.mjs b/tools/conformance/consume-export.mjs index e998704..54d3962 100644 --- a/tools/conformance/consume-export.mjs +++ b/tools/conformance/consume-export.mjs @@ -26,6 +26,7 @@ if (!exportDirectory) { " --expect-plugin-runtime-transport ", " --expect-plugin-runtime-authority ", " --expect-plugin-runtime-field ", + " --expect-plugin-abi-field ", " --expect-plugin-handoff ", " --expect-provider-surface ", " --expect-provider-surface-kind ", @@ -228,6 +229,23 @@ function validateIndex(index, directory, expectations, errors) { errors.push(`plugins:${expectation.pluginId}:runtime.${expectation.fieldPath}:expected ${expectation.value} got ${actual || ""}`); } } + for (const expectation of expectations.pluginAbiFields) { + const plugin = plugins.find(candidate => candidate.pluginId === expectation.pluginId); + if (!plugin) { + errors.push(`plugins:${expectation.pluginId}:missing`); + continue; + } + const contracts = Array.isArray(plugin.abiOperationContracts) ? plugin.abiOperationContracts : []; + const contract = contracts.find(candidate => candidate.operation === expectation.operation); + if (!contract) { + errors.push(`plugins:${expectation.pluginId}:abiOperation:${expectation.operation}:missing`); + continue; + } + const actual = readNestedField(contract, expectation.fieldPath); + if (actual !== expectation.value) { + errors.push(`plugins:${expectation.pluginId}:abiOperation:${expectation.operation}.${expectation.fieldPath}:expected ${expectation.value} got ${actual || ""}`); + } + } for (const expectedPlugin of expectations.pluginHandoffs) { const plugin = plugins.find(candidate => candidate.pluginId === expectedPlugin); if (!plugin) { @@ -580,6 +598,18 @@ function validatePluginRecords(plugins, errors) { for (const field of ["capabilities", "abiOperations", "optionalPlugins"]) { if (!Array.isArray(plugin?.[field])) errors.push(`${label}:${field}:expected array`); } + if (!Array.isArray(plugin?.abiOperationContracts)) { + errors.push(`${label}:abiOperationContracts:expected array`); + } + for (const contract of plugin?.abiOperationContracts || []) { + if (!contract.operation) errors.push(`${label}:abiOperationContracts:operation:missing`); + if (!contract.input || typeof contract.input !== "object" || Array.isArray(contract.input)) { + errors.push(`${label}:abiOperationContracts:${contract.operation || "unknown"}:input:expected object`); + } + if (!contract.expect || typeof contract.expect !== "object" || Array.isArray(contract.expect)) { + errors.push(`${label}:abiOperationContracts:${contract.operation || "unknown"}:expect:expected object`); + } + } } } @@ -642,6 +672,7 @@ function parseArguments(args) { pluginRuntimeTransports: [], pluginRuntimeAuthorities: [], pluginRuntimeFields: [], + pluginAbiFields: [], pluginHandoffs: [], providers: [], providerSurfaces: [], @@ -685,6 +716,7 @@ function parseArguments(args) { ["--expect-plugin-runtime-transport", expectations.pluginRuntimeTransports], ["--expect-plugin-runtime-authority", expectations.pluginRuntimeAuthorities], ["--expect-plugin-runtime-field", expectations.pluginRuntimeFields], + ["--expect-plugin-abi-field", expectations.pluginAbiFields], ["--expect-plugin-handoff", expectations.pluginHandoffs], ["--expect-provider-surface", expectations.providerSurfaces], ["--expect-provider-surface-kind", expectations.providerSurfaceKinds], @@ -741,6 +773,8 @@ function parseArguments(args) { target.push(parsePluginExpectation(value, "authority")); } else if (option === "--expect-plugin-runtime-field") { target.push(parsePluginRuntimeFieldExpectation(value)); + } else if (option === "--expect-plugin-abi-field") { + target.push(parsePluginAbiFieldExpectation(value)); } else if (option === "--expect-plugin-handoff") { target.push(value); } else if (option === "--expect-provider-surface") { @@ -832,6 +866,20 @@ function parsePluginRuntimeFieldExpectation(value) { }; } +function parsePluginAbiFieldExpectation(value) { + const parts = value.split(":"); + if (parts.length < 4 || !parts[0] || !parts[1] || !parts[2] || !parts.slice(3).join(":")) { + console.error(`Expected plugin ABI field in form, got: ${value}`); + process.exit(2); + } + return { + pluginId: parts[0], + operation: parts[1], + fieldPath: parts[2], + value: parts.slice(3).join(":"), + }; +} + function parseProviderSurfaceFieldExpectation(value) { const parts = value.split(":"); if (parts.length < 4 || !parts[0] || !parts[1] || !parts[2] || !parts.slice(3).join(":")) { diff --git a/tools/parity/run-parity.mjs b/tools/parity/run-parity.mjs index 7e930f0..45ea882 100644 --- a/tools/parity/run-parity.mjs +++ b/tools/parity/run-parity.mjs @@ -449,6 +449,7 @@ async function evaluatePlugin(plugin, fixtureResults) { const runtimeBoundary = await readPluginRuntimeBoundary(plugin); const runtimeBoundaryErrors = validatePluginRuntimeBoundary(plugin, runtimeBoundary); const abiOperations = await readPluginAbiOperations(plugin.abiFixturePath); + const abiOperationContracts = await readPluginAbiOperationContracts(plugin.abiFixturePath); const abiErrors = await validatePluginAbiFixture(plugin); const status = missingPaths.length ? "missing-body" @@ -487,6 +488,7 @@ async function evaluatePlugin(plugin, fixtureResults) { abiFixturePath: plugin.abiFixturePath || "", handoffPath: plugin.handoffPath || "", abiOperations, + abiOperationContracts, advertisementSchemaPath: plugin.advertisementSchemaPath || "", advertisementPath: plugin.advertisementPath || "", schemaErrors, @@ -512,6 +514,23 @@ async function readPluginAbiOperations(abiFixturePath) { } } +async function readPluginAbiOperationContracts(abiFixturePath) { + if (!abiFixturePath) return []; + try { + const abiFixture = await readJsonDocument(abiFixturePath); + return (abiFixture.operations || []) + .filter(operation => operation.operation) + .map(operation => ({ + operation: operation.operation, + input: operation.input || {}, + expect: operation.expect || {}, + })) + .sort((left, right) => left.operation.localeCompare(right.operation)); + } catch { + return []; + } +} + async function readPluginRuntimeBoundary(plugin) { try { const manifestDocument = await readJsonDocument(plugin.manifestPath); @@ -1602,6 +1621,7 @@ function buildConformanceExport(report) { handoffExportPath: makeHandoffExportPath("plugin", plugin.pluginId, plugin.handoffPath), abiFixturePath: plugin.abiFixturePath, abiOperations: plugin.abiOperations || [], + abiOperationContracts: plugin.abiOperationContracts || [], runtimeBoundary: plugin.runtimeBoundary, optionalPlugins: plugin.optionalPlugins || [], capabilities: plugin.capabilities,