Assert Aetheria interactive world surfaces
This commit is contained in:
parent
18de835f79
commit
0381da7463
9 changed files with 102 additions and 10 deletions
|
|
@ -349,11 +349,12 @@ Recently cut:
|
|||
capture.
|
||||
- Provider owner conformance consumption now verifies the interactive-world
|
||||
contract instead of only provider presence. The generic conformance consumer
|
||||
supports `--expect-provider-surface`, `--expect-provider-command`, and
|
||||
`--expect-provider-receipt-state`; the export now carries provider receipt
|
||||
states; and Aetheria's consumer smoke asserts the daemon game/editor surfaces,
|
||||
daemon command boundary, and accepted/reconciled receipts from the exported
|
||||
provider pack.
|
||||
supports `--expect-provider-surface`, `--expect-provider-surface-kind`,
|
||||
`--expect-provider-command`, and `--expect-provider-receipt-state`; the
|
||||
export now carries provider receipt states and surface-kind claims; and
|
||||
Aetheria's consumer smoke asserts the daemon game/editor surfaces,
|
||||
interactive world surface roles, daemon command boundary, and
|
||||
accepted/reconciled receipts from the exported provider pack.
|
||||
- Plugin owner conformance consumption now has a smoke runner at
|
||||
`scripts/run-plugin-owner-conformance-consumer-smoke.ps1`. The generic
|
||||
conformance consumer can assert plugin ABI operations and capabilities with
|
||||
|
|
|
|||
|
|
@ -143,9 +143,10 @@ powershell -ExecutionPolicy Bypass -File .\scripts\run-aetheria-conformance-cons
|
|||
|
||||
That script runs the same export consumer from `E:\Projects\Aetheria` and
|
||||
asserts the provider pack, `aetheria-world` fixture, `aetheria` provider entry,
|
||||
`aetheria-world-command-replay` scenario, and provider handoff path. Generated
|
||||
copies remain under Eve's `artifacts/aetheria-conformance-consumer-smoke` so
|
||||
the provider worktree does not become dirty merely by proving the boundary.
|
||||
`aetheria-world-command-replay` scenario, daemon game/editor surface-kind
|
||||
claims, and provider handoff path. Generated copies remain under Eve's
|
||||
`artifacts/aetheria-conformance-consumer-smoke` so the provider worktree does
|
||||
not become dirty merely by proving the boundary.
|
||||
|
||||
Plugin owner handoff smoke:
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,12 @@ Required top-level meanings:
|
|||
- `witnesses`: CultCache `.cc` files, database exports, or document keys that
|
||||
witness durable state.
|
||||
- `surfaces`: Eve surface documents or endpoints the provider publishes.
|
||||
- `surfaces[].surfaceKind`: provider-declared role for a surface, such as
|
||||
`interactive-world` or `interactive-world-editor`. This is discovery and
|
||||
conformance evidence for runtimes; provider state and semantics remain behind
|
||||
the advertised surface, schemas, and command boundary.
|
||||
- `surfaces[].interactionModel`: short name for how interaction returns to the
|
||||
provider, such as `provider-command-receipts`.
|
||||
- `surfaces[].requiresPlugins`: sidecar plugin capabilities required to render
|
||||
or interact with that surface. Runtimes compare these requirements with their
|
||||
local projection support and report visible capability gaps; they must not
|
||||
|
|
|
|||
|
|
@ -136,6 +136,14 @@
|
|||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"surfaceKind": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"interactionModel": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"requiresPlugins": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ try {
|
|||
--expect-scenario aetheria-world-command-replay `
|
||||
--expect-provider-surface aetheria:aetheria.daemon.game `
|
||||
--expect-provider-surface aetheria:aetheria.daemon.editor `
|
||||
--expect-provider-surface-kind aetheria:aetheria.daemon.game:interactive-world `
|
||||
--expect-provider-surface-kind aetheria:aetheria.daemon.editor:interactive-world-editor `
|
||||
--expect-provider-command aetheria:aetheria.daemon.commands `
|
||||
--expect-provider-receipt-state aetheria:accepted `
|
||||
--expect-provider-receipt-state aetheria:reconciled `
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ if (!exportDirectory) {
|
|||
" --expect-plugin-capability <pluginId:capability>",
|
||||
" --expect-plugin-handoff <pluginId>",
|
||||
" --expect-provider-surface <providerId:surfaceId>",
|
||||
" --expect-provider-surface-kind <providerId:surfaceId:surfaceKind>",
|
||||
" --expect-provider-command <providerId:command>",
|
||||
" --expect-provider-receipt-state <providerId:state>",
|
||||
" --expect-provider-handoff <providerId>",
|
||||
|
|
@ -176,6 +177,20 @@ function validateIndex(index, directory, expectations, errors) {
|
|||
errors.push(`providers:${expectation.providerId}:surface:${expectation.surfaceId}:missing`);
|
||||
}
|
||||
}
|
||||
for (const expectation of expectations.providerSurfaceKinds) {
|
||||
const provider = providers.find(candidate => candidate.providerId === expectation.providerId);
|
||||
if (!provider) {
|
||||
errors.push(`providers:${expectation.providerId}:missing`);
|
||||
continue;
|
||||
}
|
||||
const surfaceKinds = Array.isArray(provider.surfaceKinds) ? provider.surfaceKinds : [];
|
||||
const surface = surfaceKinds.find(candidate => candidate.surfaceId === expectation.surfaceId);
|
||||
if (!surface) {
|
||||
errors.push(`providers:${expectation.providerId}:surfaceKind:${expectation.surfaceId}:missing`);
|
||||
} else if (surface.surfaceKind !== expectation.surfaceKind) {
|
||||
errors.push(`providers:${expectation.providerId}:surfaceKind:${expectation.surfaceId}:expected ${expectation.surfaceKind} got ${surface.surfaceKind || ""}`);
|
||||
}
|
||||
}
|
||||
for (const expectation of expectations.providerCommands) {
|
||||
const provider = providers.find(candidate => candidate.providerId === expectation.providerId);
|
||||
if (!provider) {
|
||||
|
|
@ -410,6 +425,7 @@ function parseArguments(args) {
|
|||
pluginHandoffs: [],
|
||||
providers: [],
|
||||
providerSurfaces: [],
|
||||
providerSurfaceKinds: [],
|
||||
providerCommands: [],
|
||||
providerReceiptStates: [],
|
||||
providerHandoffs: [],
|
||||
|
|
@ -440,6 +456,7 @@ function parseArguments(args) {
|
|||
["--expect-plugin-capability", expectations.pluginCapabilities],
|
||||
["--expect-plugin-handoff", expectations.pluginHandoffs],
|
||||
["--expect-provider-surface", expectations.providerSurfaces],
|
||||
["--expect-provider-surface-kind", expectations.providerSurfaceKinds],
|
||||
["--expect-provider-command", expectations.providerCommands],
|
||||
["--expect-provider-receipt-state", expectations.providerReceiptStates],
|
||||
["--expect-provider-handoff", expectations.providerHandoffs],
|
||||
|
|
@ -482,6 +499,8 @@ function parseArguments(args) {
|
|||
target.push(value);
|
||||
} else if (option === "--expect-provider-surface") {
|
||||
target.push(parseProviderExpectation(value, "surfaceId"));
|
||||
} else if (option === "--expect-provider-surface-kind") {
|
||||
target.push(parseProviderSurfaceKindExpectation(value));
|
||||
} else if (option === "--expect-provider-command") {
|
||||
target.push(parseProviderExpectation(value, "command"));
|
||||
} else if (option === "--expect-provider-receipt-state") {
|
||||
|
|
@ -537,6 +556,19 @@ function parseProviderExpectation(value, field) {
|
|||
};
|
||||
}
|
||||
|
||||
function parseProviderSurfaceKindExpectation(value) {
|
||||
const parts = value.split(":");
|
||||
if (parts.length !== 3 || parts.some(part => !part)) {
|
||||
console.error(`Expected provider surface kind in <providerId:surfaceId:surfaceKind> form, got: ${value}`);
|
||||
process.exit(2);
|
||||
}
|
||||
return {
|
||||
providerId: parts[0],
|
||||
surfaceId: parts[1],
|
||||
surfaceKind: parts[2],
|
||||
};
|
||||
}
|
||||
|
||||
function parseRuntimeExpectation(value, field) {
|
||||
const separator = value.indexOf(":");
|
||||
if (separator <= 0 || separator === value.length - 1) {
|
||||
|
|
|
|||
|
|
@ -349,6 +349,10 @@
|
|||
"requiredFixtures": ["aetheria-world"],
|
||||
"expectedSchemas": ["gamecult.eve.surface.v1", "gamecult.eve.command.v1", "aetheria.runtime.daemon.frame.v1"],
|
||||
"expectedSurfaces": ["aetheria.daemon.game", "aetheria.daemon.editor"],
|
||||
"expectedSurfaceKinds": {
|
||||
"aetheria.daemon.game": "interactive-world",
|
||||
"aetheria.daemon.editor": "interactive-world-editor"
|
||||
},
|
||||
"expectedCommands": ["aetheria.daemon.commands"],
|
||||
"expectedPaths": [
|
||||
"web/fixtures/aetheria-world-scenario.json",
|
||||
|
|
|
|||
|
|
@ -549,12 +549,14 @@ async function evaluateProvider(provider, fixtureResults, pluginResults) {
|
|||
...(advertisement.commands || []).map(command => command.schema).filter(Boolean),
|
||||
])].sort() : [];
|
||||
const surfaceIds = advertisement ? (advertisement.surfaces || []).map(surface => surface.surfaceId).filter(Boolean).sort() : [];
|
||||
const surfaceKinds = advertisement ? collectProviderSurfaceKinds(advertisement) : [];
|
||||
const commandIds = advertisement ? (advertisement.commands || []).map(command => command.command).filter(Boolean).sort() : [];
|
||||
const witnessKinds = advertisement ? (advertisement.witnesses || []).map(witness => witness.kind).filter(Boolean).sort() : [];
|
||||
const pluginRequirements = advertisement ? collectProviderPluginRequirements(advertisement) : [];
|
||||
const pluginRequirementErrors = validateProviderPluginRequirements(provider, pluginRequirements, pluginResults);
|
||||
const missingSchemas = (provider.expectedSchemas || []).filter(schema => !advertisedSchemaIds.includes(schema));
|
||||
const missingSurfaces = (provider.expectedSurfaces || []).filter(surface => !surfaceIds.includes(surface));
|
||||
const missingSurfaceKinds = validateProviderSurfaceKinds(provider, surfaceKinds);
|
||||
const missingCommands = (provider.expectedCommands || []).filter(command => !commandIds.includes(command));
|
||||
const scenarioErrors = await validateProviderScenario(provider, advertisement, fixtureResults, advertisedSchemaIds, surfaceIds, commandIds);
|
||||
const scenario = scenarioErrors.length || !provider.scenarioPath ? null : await readJsonDocument(provider.scenarioPath);
|
||||
|
|
@ -566,7 +568,7 @@ async function evaluateProvider(provider, fixtureResults, pluginResults) {
|
|||
? "missing-ownership-metadata"
|
||||
: advertisementErrors.length
|
||||
? "invalid-provider-advertisement"
|
||||
: missingSchemas.length || missingSurfaces.length || missingCommands.length || pluginRequirementErrors.length
|
||||
: missingSchemas.length || missingSurfaces.length || missingSurfaceKinds.length || missingCommands.length || pluginRequirementErrors.length
|
||||
? "capability-gap"
|
||||
: scenarioErrors.length
|
||||
? "invalid-provider-scenario"
|
||||
|
|
@ -592,16 +594,19 @@ async function evaluateProvider(provider, fixtureResults, pluginResults) {
|
|||
missingIncubationFields,
|
||||
expectedSchemas: provider.expectedSchemas || [],
|
||||
expectedSurfaces: provider.expectedSurfaces || [],
|
||||
expectedSurfaceKinds: provider.expectedSurfaceKinds || {},
|
||||
expectedCommands: provider.expectedCommands || [],
|
||||
schemaIds,
|
||||
advertisedSchemaIds,
|
||||
surfaceIds,
|
||||
surfaceKinds,
|
||||
commandIds,
|
||||
witnessKinds,
|
||||
pluginRequirements,
|
||||
pluginRequirementErrors,
|
||||
missingSchemas,
|
||||
missingSurfaces,
|
||||
missingSurfaceKinds,
|
||||
missingCommands,
|
||||
scenarioId: scenario?.scenarioId || "",
|
||||
scenarioReceiptStates: scenario ? [...new Set((scenario.expectedReceipts || []).map(receipt => receipt.state).filter(Boolean))].sort() : [],
|
||||
|
|
@ -625,6 +630,31 @@ function collectProviderPluginRequirements(advertisement) {
|
|||
return requirements;
|
||||
}
|
||||
|
||||
function collectProviderSurfaceKinds(advertisement) {
|
||||
return (advertisement.surfaces || [])
|
||||
.filter(surface => surface.surfaceId && surface.surfaceKind)
|
||||
.map(surface => ({
|
||||
surfaceId: surface.surfaceId,
|
||||
surfaceKind: surface.surfaceKind,
|
||||
interactionModel: surface.interactionModel || "",
|
||||
}))
|
||||
.sort((left, right) => left.surfaceId.localeCompare(right.surfaceId));
|
||||
}
|
||||
|
||||
function validateProviderSurfaceKinds(provider, surfaceKinds) {
|
||||
const errors = [];
|
||||
const kindsBySurface = new Map(surfaceKinds.map(surface => [surface.surfaceId, surface.surfaceKind]));
|
||||
for (const [surfaceId, expectedKind] of Object.entries(provider.expectedSurfaceKinds || {})) {
|
||||
const actualKind = kindsBySurface.get(surfaceId);
|
||||
if (!actualKind) {
|
||||
errors.push(`${surfaceId}:surfaceKind:missing`);
|
||||
} else if (actualKind !== expectedKind) {
|
||||
errors.push(`${surfaceId}:surfaceKind:expected ${expectedKind} got ${actualKind}`);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateProviderPluginRequirements(provider, requirements, pluginResults) {
|
||||
const errors = [];
|
||||
const pluginById = new Map((pluginResults || []).map(plugin => [plugin.pluginId, plugin]));
|
||||
|
|
@ -1383,6 +1413,7 @@ function buildConformanceExport(report) {
|
|||
scenarioId: provider.scenarioId,
|
||||
receiptStates: provider.scenarioReceiptStates || [],
|
||||
surfaces: provider.surfaceIds,
|
||||
surfaceKinds: provider.surfaceKinds,
|
||||
commands: provider.commandIds,
|
||||
pluginRequirements: provider.pluginRequirements,
|
||||
})),
|
||||
|
|
@ -1433,6 +1464,7 @@ function buildCapabilityMatrix(report) {
|
|||
ownerRepo: provider.ownerRepo,
|
||||
status: provider.status,
|
||||
surfaces: provider.surfaceIds || [],
|
||||
surfaceKinds: provider.surfaceKinds || [],
|
||||
commands: provider.commandIds || [],
|
||||
receiptStates: provider.scenarioReceiptStates || [],
|
||||
handoffExportPath: makeHandoffExportPath("provider", provider.providerId, provider.handoffPath),
|
||||
|
|
@ -1520,6 +1552,7 @@ function collectCapabilityGaps(report) {
|
|||
...(provider.scenarioErrors || []).map(id => `scenario:${id}`),
|
||||
...(provider.missingSchemas || []).map(id => `schema:${id}`),
|
||||
...(provider.missingSurfaces || []).map(id => `surface:${id}`),
|
||||
...(provider.missingSurfaceKinds || []).map(id => `surface-kind:${id}`),
|
||||
...(provider.missingCommands || []).map(id => `command:${id}`),
|
||||
...(provider.pluginRequirementErrors || []).map(id => `plugin:${id}`),
|
||||
]) {
|
||||
|
|
@ -1775,6 +1808,7 @@ function renderMarkdown(report) {
|
|||
...provider.scenarioErrors.map(id => `scenario:${id}`),
|
||||
...provider.missingSchemas.map(id => `schema:${id}`),
|
||||
...provider.missingSurfaces.map(id => `surface:${id}`),
|
||||
...provider.missingSurfaceKinds.map(id => `surface-kind:${id}`),
|
||||
...provider.missingCommands.map(id => `command:${id}`),
|
||||
...provider.pluginRequirementErrors.map(id => `plugin:${id}`),
|
||||
].join(", ");
|
||||
|
|
|
|||
|
|
@ -94,6 +94,8 @@
|
|||
"status": "available",
|
||||
"audience": "player",
|
||||
"mode": "interactive",
|
||||
"surfaceKind": "interactive-world",
|
||||
"interactionModel": "provider-command-receipts",
|
||||
"summary": "Authoritative daemon game surface consumed by Unity and web lowerers."
|
||||
},
|
||||
{
|
||||
|
|
@ -112,7 +114,9 @@
|
|||
"transport": "cultmesh-record",
|
||||
"status": "available",
|
||||
"audience": "operator",
|
||||
"mode": "interactive"
|
||||
"mode": "interactive",
|
||||
"surfaceKind": "interactive-world-editor",
|
||||
"interactionModel": "provider-command-receipts"
|
||||
}
|
||||
],
|
||||
"commands": [
|
||||
|
|
|
|||
Loading…
Reference in a new issue