From 0381da74634a5bc6fa34aa2afc1db458289336c8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 9 Jul 2026 02:05:59 +0100 Subject: [PATCH] Assert Aetheria interactive world surfaces --- docs/eve-migration-roadmap.md | 11 +++--- docs/parity-testing-harness.md | 7 ++-- docs/provider-advertisement-contract.md | 6 ++++ ....eve.provider_advertisement.v1.schema.json | 8 +++++ ...un-aetheria-conformance-consumer-smoke.ps1 | 2 ++ tools/conformance/consume-export.mjs | 32 +++++++++++++++++ tools/parity/parity-manifest.json | 4 +++ tools/parity/run-parity.mjs | 36 ++++++++++++++++++- .../aetheria.provider-advertisement.json | 6 +++- 9 files changed, 102 insertions(+), 10 deletions(-) diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index fb69712..966fbfa 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -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 diff --git a/docs/parity-testing-harness.md b/docs/parity-testing-harness.md index aee0c02..4097208 100644 --- a/docs/parity-testing-harness.md +++ b/docs/parity-testing-harness.md @@ -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: diff --git a/docs/provider-advertisement-contract.md b/docs/provider-advertisement-contract.md index a93d912..b4a9233 100644 --- a/docs/provider-advertisement-contract.md +++ b/docs/provider-advertisement-contract.md @@ -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 diff --git a/schemas/gamecult.eve.provider_advertisement.v1.schema.json b/schemas/gamecult.eve.provider_advertisement.v1.schema.json index f4d08b3..e298d3a 100644 --- a/schemas/gamecult.eve.provider_advertisement.v1.schema.json +++ b/schemas/gamecult.eve.provider_advertisement.v1.schema.json @@ -136,6 +136,14 @@ "type": "string", "minLength": 1 }, + "surfaceKind": { + "type": "string", + "minLength": 1 + }, + "interactionModel": { + "type": "string", + "minLength": 1 + }, "requiresPlugins": { "type": "array", "items": { diff --git a/scripts/run-aetheria-conformance-consumer-smoke.ps1 b/scripts/run-aetheria-conformance-consumer-smoke.ps1 index 35baf25..f7f8c34 100644 --- a/scripts/run-aetheria-conformance-consumer-smoke.ps1 +++ b/scripts/run-aetheria-conformance-consumer-smoke.ps1 @@ -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 ` diff --git a/tools/conformance/consume-export.mjs b/tools/conformance/consume-export.mjs index a53724f..f530ade 100644 --- a/tools/conformance/consume-export.mjs +++ b/tools/conformance/consume-export.mjs @@ -23,6 +23,7 @@ if (!exportDirectory) { " --expect-plugin-capability ", " --expect-plugin-handoff ", " --expect-provider-surface ", + " --expect-provider-surface-kind ", " --expect-provider-command ", " --expect-provider-receipt-state ", " --expect-provider-handoff ", @@ -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 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) { diff --git a/tools/parity/parity-manifest.json b/tools/parity/parity-manifest.json index 60d2ef9..be64379 100644 --- a/tools/parity/parity-manifest.json +++ b/tools/parity/parity-manifest.json @@ -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", diff --git a/tools/parity/run-parity.mjs b/tools/parity/run-parity.mjs index 1f356ae..0636f8b 100644 --- a/tools/parity/run-parity.mjs +++ b/tools/parity/run-parity.mjs @@ -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(", "); diff --git a/web/fixtures/aetheria.provider-advertisement.json b/web/fixtures/aetheria.provider-advertisement.json index f72e32f..0bc5abe 100644 --- a/web/fixtures/aetheria.provider-advertisement.json +++ b/web/fixtures/aetheria.provider-advertisement.json @@ -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": [