Assert Aetheria interactive world surfaces

This commit is contained in:
Meta Vens 2026-07-09 02:05:59 +01:00
commit 0381da7463
9 changed files with 102 additions and 10 deletions

View file

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

View file

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

View file

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

View file

@ -136,6 +136,14 @@
"type": "string",
"minLength": 1
},
"surfaceKind": {
"type": "string",
"minLength": 1
},
"interactionModel": {
"type": "string",
"minLength": 1
},
"requiresPlugins": {
"type": "array",
"items": {

View file

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

View file

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

View file

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

View file

@ -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(", ");

View file

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