Export plugin ABI operation contracts
This commit is contained in:
parent
05e506ef60
commit
8d9f78e107
6 changed files with 105 additions and 1 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ if (!exportDirectory) {
|
|||
" --expect-plugin-runtime-transport <pluginId:transport>",
|
||||
" --expect-plugin-runtime-authority <pluginId:authority>",
|
||||
" --expect-plugin-runtime-field <pluginId:field.path:value>",
|
||||
" --expect-plugin-abi-field <pluginId:operation:field.path:value>",
|
||||
" --expect-plugin-handoff <pluginId>",
|
||||
" --expect-provider-surface <providerId:surfaceId>",
|
||||
" --expect-provider-surface-kind <providerId:surfaceId:surfaceKind>",
|
||||
|
|
@ -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 <pluginId:operation:field.path:value> 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(":")) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue