Define Aetheria world surface contract

This commit is contained in:
Meta Vens 2026-07-09 02:27:35 +01:00
commit be05146883
12 changed files with 255 additions and 1 deletions

View file

@ -280,6 +280,14 @@ Exit criteria:
Recently cut:
- Aetheria interactive world surfaces now carry a structured provider-owned
`worldInteraction` contract. The provider advertisement names projection
kind, provider state schemas, daemon command boundary, receipt schema,
lowering targets, and ownership rules for `aetheria.daemon.game` and
`aetheria.daemon.editor`. The provider scenario requires the daemon game
world-surface contract, parity validates it against the advertisement, the
conformance export carries `surfaceContracts`, and the Aetheria consumer
smoke asserts those fields without importing Aetheria or Eve internals.
- EveUnity test lifecycle now carries a structured Unity EditMode runner
contract. `packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json`
names the runner script, consumer Unity project, default Unity editor path,

View file

@ -78,6 +78,12 @@ Required top-level meanings:
the advertised surface, schemas, and command boundary.
- `surfaces[].interactionModel`: short name for how interaction returns to the
provider, such as `provider-command-receipts`.
- `surfaces[].worldInteraction`: optional structured contract for
provider-authored world surfaces. It names the projection kind, provider-owned
state schemas, command boundary, receipt schema, expected lowering targets,
and ownership rule. Runtimes use it to decide how to lower the surface and
where command intents must return; they do not gain authority over world
state because they can render it.
- `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

@ -76,6 +76,23 @@ World surfaces should be able to describe:
The renderer chooses the best native projection it can support. It must not
invent provider semantics to compensate for missing contract information.
Provider advertisements attach the reusable boundary to world surfaces through
`surfaces[].worldInteraction`. That block names:
- `projectionKind`: the generic world presentation role, not a game-specific
rule name;
- `stateSchemas`: provider-owned state documents that describe the world;
- `commandBoundary`: the provider command route that receives interaction
intents;
- `receiptSchema`: the provider-owned receipt document schema;
- `loweringTargets`: runtime families expected to consume the surface;
- `ownership`: the plain-language authority rule for state, assets, command
acceptance, and receipts.
Conformance scenarios can require `worldSurfaces[]` entries and the parity
harness checks them against the advertisement. This makes an interactive world
surface portable without giving Eve or a runtime private Aetheria knowledge.
## Quality Tiers
A low-end or debugging renderer may lower a world surface into labels, simple
@ -120,6 +137,13 @@ missing a generic scalar-field primitive or the provider surface is
underspecified. If a renderer must know "Aetheria planet" to draw a body, the
object row or asset contract is underspecified.
The incubating Aetheria fixture advertises `aetheria.daemon.game` as
`interactive-world` with `worldInteraction.projectionKind:
provider-authored-world-surface`, `commandBoundary: aetheria.daemon.commands`,
and `receiptSchema: aetheria.eve_command_acceptance_status.v1`. Web, Unity UI
Toolkit, Unity scene, and TUI lowerers are lowering targets; Aetheria remains
the owner of world state, assets, command acceptance, and receipts.
## Non-Goals
Eve does not own provider gameplay rules.

View file

@ -144,6 +144,50 @@
"type": "string",
"minLength": 1
},
"worldInteraction": {
"type": "object",
"required": [
"projectionKind",
"stateSchemas",
"commandBoundary",
"receiptSchema",
"loweringTargets",
"ownership"
],
"properties": {
"projectionKind": {
"type": "string",
"minLength": 1
},
"stateSchemas": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"commandBoundary": {
"type": "string",
"minLength": 1
},
"receiptSchema": {
"type": "string",
"minLength": 1
},
"loweringTargets": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"ownership": {
"type": "string",
"minLength": 1
}
},
"additionalProperties": true
},
"requiresPlugins": {
"type": "array",
"items": {

View file

@ -51,6 +51,42 @@
"minLength": 1
}
},
"worldSurfaces": {
"type": "array",
"items": {
"type": "object",
"required": [
"surfaceId",
"surfaceKind",
"projectionKind",
"commandBoundary",
"receiptSchema"
],
"properties": {
"surfaceId": {
"type": "string",
"minLength": 1
},
"surfaceKind": {
"type": "string",
"minLength": 1
},
"projectionKind": {
"type": "string",
"minLength": 1
},
"commandBoundary": {
"type": "string",
"minLength": 1
},
"receiptSchema": {
"type": "string",
"minLength": 1
}
},
"additionalProperties": true
}
},
"commands": {
"type": "array",
"items": {

View file

@ -45,6 +45,13 @@ try {
--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-surface-field aetheria:aetheria.daemon.game:worldInteraction.projectionKind:provider-authored-world-surface `
--expect-provider-surface-field aetheria:aetheria.daemon.game:worldInteraction.commandBoundary:aetheria.daemon.commands `
--expect-provider-surface-field aetheria:aetheria.daemon.game:worldInteraction.receiptSchema:aetheria.eve_command_acceptance_status.v1 `
--expect-provider-surface-field aetheria:aetheria.daemon.game:worldInteraction.ownership:provider-owns-world-state-assets-command-acceptance-and-receipts `
--expect-provider-surface-field aetheria:aetheria.daemon.editor:worldInteraction.projectionKind:provider-authored-world-editor-surface `
--expect-provider-surface-field aetheria:aetheria.daemon.editor:worldInteraction.commandBoundary:aetheria.daemon.commands `
--expect-provider-surface-field aetheria:aetheria.daemon.editor:worldInteraction.receiptSchema:aetheria.eve_command_acceptance_status.v1 `
--expect-provider-command aetheria:aetheria.daemon.commands `
--expect-provider-receipt-state aetheria:accepted `
--expect-provider-receipt-state aetheria:reconciled `

View file

@ -54,6 +54,30 @@ if ($scenario.ownerRepo -ne $handoff.ownerRepo) {
throw "Aetheria scenario owner does not match handoff owner: $($scenario.ownerRepo)"
}
$worldSurfaces = @{}
foreach ($surface in @($advertisement.surfaces)) {
if ($surface.surfaceId -and $surface.worldInteraction) {
$worldSurfaces[$surface.surfaceId] = $surface
}
}
foreach ($requirement in @($scenario.requires.worldSurfaces)) {
if (-not $worldSurfaces.ContainsKey($requirement.surfaceId)) {
throw "Aetheria scenario requires world surface not advertised with worldInteraction: $($requirement.surfaceId)"
}
$advertisedSurface = $worldSurfaces[$requirement.surfaceId]
if ($advertisedSurface.surfaceKind -ne $requirement.surfaceKind) {
throw "Aetheria world surface kind mismatch for $($requirement.surfaceId): $($advertisedSurface.surfaceKind)"
}
foreach ($property in @("projectionKind", "commandBoundary", "receiptSchema")) {
if ($advertisedSurface.worldInteraction.$property -ne $requirement.$property) {
throw "Aetheria world surface $($requirement.surfaceId) $property mismatch: $($advertisedSurface.worldInteraction.$property)"
}
}
if (-not $advertisedSurface.worldInteraction.ownership) {
throw "Aetheria world surface $($requirement.surfaceId) missing ownership rule"
}
}
foreach ($fixtureId in @($handoff.fixtureIds)) {
if (-not (@($scenario.requires.fixtures) -contains $fixtureId)) {
throw "Aetheria provider handoff fixture is not required by scenario: $fixtureId"

View file

@ -28,6 +28,7 @@ if (!exportDirectory) {
" --expect-plugin-handoff <pluginId>",
" --expect-provider-surface <providerId:surfaceId>",
" --expect-provider-surface-kind <providerId:surfaceId:surfaceKind>",
" --expect-provider-surface-field <providerId:surfaceId:field.path:value>",
" --expect-provider-command <providerId:command>",
" --expect-provider-receipt-state <providerId:state>",
" --expect-provider-handoff <providerId>",
@ -239,6 +240,23 @@ function validateIndex(index, directory, expectations, errors) {
errors.push(`providers:${expectation.providerId}:surfaceKind:${expectation.surfaceId}:expected ${expectation.surfaceKind} got ${surface.surfaceKind || ""}`);
}
}
for (const expectation of expectations.providerSurfaceFields) {
const provider = providers.find(candidate => candidate.providerId === expectation.providerId);
if (!provider) {
errors.push(`providers:${expectation.providerId}:missing`);
continue;
}
const surfaceContracts = Array.isArray(provider.surfaceContracts) ? provider.surfaceContracts : [];
const surface = surfaceContracts.find(candidate => candidate.surfaceId === expectation.surfaceId);
if (!surface) {
errors.push(`providers:${expectation.providerId}:surfaceContract:${expectation.surfaceId}:missing`);
continue;
}
const actual = readNestedField(surface, expectation.fieldPath);
if (actual !== expectation.value) {
errors.push(`providers:${expectation.providerId}:surfaceContract:${expectation.surfaceId}.${expectation.fieldPath}:expected ${expectation.value} got ${actual || ""}`);
}
}
for (const expectation of expectations.providerCommands) {
const provider = providers.find(candidate => candidate.providerId === expectation.providerId);
if (!provider) {
@ -514,6 +532,7 @@ function parseArguments(args) {
providers: [],
providerSurfaces: [],
providerSurfaceKinds: [],
providerSurfaceFields: [],
providerCommands: [],
providerReceiptStates: [],
providerHandoffs: [],
@ -552,6 +571,7 @@ function parseArguments(args) {
["--expect-plugin-handoff", expectations.pluginHandoffs],
["--expect-provider-surface", expectations.providerSurfaces],
["--expect-provider-surface-kind", expectations.providerSurfaceKinds],
["--expect-provider-surface-field", expectations.providerSurfaceFields],
["--expect-provider-command", expectations.providerCommands],
["--expect-provider-receipt-state", expectations.providerReceiptStates],
["--expect-provider-handoff", expectations.providerHandoffs],
@ -607,6 +627,8 @@ function parseArguments(args) {
target.push(parseProviderExpectation(value, "surfaceId"));
} else if (option === "--expect-provider-surface-kind") {
target.push(parseProviderSurfaceKindExpectation(value));
} else if (option === "--expect-provider-surface-field") {
target.push(parseProviderSurfaceFieldExpectation(value));
} else if (option === "--expect-provider-command") {
target.push(parseProviderExpectation(value, "command"));
} else if (option === "--expect-provider-receipt-state") {
@ -669,6 +691,20 @@ function parsePluginRuntimeFieldExpectation(value) {
};
}
function parseProviderSurfaceFieldExpectation(value) {
const parts = value.split(":");
if (parts.length < 4 || !parts[0] || !parts[1] || !parts[2] || !parts.slice(3).join(":")) {
console.error(`Expected provider surface field in <providerId:surfaceId:field.path:value> form, got: ${value}`);
process.exit(2);
}
return {
providerId: parts[0],
surfaceId: parts[1],
fieldPath: parts[2],
value: parts.slice(3).join(":"),
};
}
function parseProviderExpectation(value, field) {
const separator = value.indexOf(":");
if (separator <= 0 || separator === value.length - 1) {

View file

@ -629,6 +629,7 @@ async function evaluateProvider(provider, fixtureResults, pluginResults) {
])].sort() : [];
const surfaceIds = advertisement ? (advertisement.surfaces || []).map(surface => surface.surfaceId).filter(Boolean).sort() : [];
const surfaceKinds = advertisement ? collectProviderSurfaceKinds(advertisement) : [];
const surfaceContracts = advertisement ? collectProviderSurfaceContracts(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) : [];
@ -679,6 +680,7 @@ async function evaluateProvider(provider, fixtureResults, pluginResults) {
advertisedSchemaIds,
surfaceIds,
surfaceKinds,
surfaceContracts,
commandIds,
witnessKinds,
pluginRequirements,
@ -720,6 +722,18 @@ function collectProviderSurfaceKinds(advertisement) {
.sort((left, right) => left.surfaceId.localeCompare(right.surfaceId));
}
function collectProviderSurfaceContracts(advertisement) {
return (advertisement.surfaces || [])
.filter(surface => surface.surfaceId && surface.worldInteraction)
.map(surface => ({
surfaceId: surface.surfaceId,
surfaceKind: surface.surfaceKind || "",
interactionModel: surface.interactionModel || "",
worldInteraction: surface.worldInteraction || {},
}))
.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]));
@ -797,6 +811,25 @@ async function validateProviderScenario(provider, advertisement, fixtureResults,
errors.push(...missingMembers(scenario.requires?.surfaces || [], surfaceIds, `${provider.scenarioPath}:requires.surfaces`));
errors.push(...missingMembers(scenario.requires?.commands || [], commandIds, `${provider.scenarioPath}:requires.commands`));
errors.push(...missingMembers(scenario.requires?.schemas || [], advertisedSchemaIds, `${provider.scenarioPath}:requires.schemas`));
const advertisedWorldSurfaces = new Map((advertisement.surfaces || [])
.filter(surface => surface.surfaceId && surface.worldInteraction)
.map(surface => [surface.surfaceId, surface]));
for (const requirement of scenario.requires?.worldSurfaces || []) {
const advertisedSurface = advertisedWorldSurfaces.get(requirement.surfaceId);
if (!advertisedSurface) {
errors.push(`${provider.scenarioPath}:requires.worldSurfaces:${requirement.surfaceId}:missing`);
continue;
}
if (advertisedSurface.surfaceKind !== requirement.surfaceKind) {
errors.push(`${provider.scenarioPath}:requires.worldSurfaces:${requirement.surfaceId}:surfaceKind:expected ${requirement.surfaceKind} got ${advertisedSurface.surfaceKind || ""}`);
}
for (const key of ["projectionKind", "commandBoundary", "receiptSchema"]) {
const actual = advertisedSurface.worldInteraction?.[key] || "";
if (actual !== requirement[key]) {
errors.push(`${provider.scenarioPath}:requires.worldSurfaces:${requirement.surfaceId}:${key}:expected ${requirement[key]} got ${actual}`);
}
}
}
const receipts = scenario.expectedReceipts || [];
const receiptKeys = new Set(receipts.map(receipt => `${receipt.command}:${receipt.commandId}`));
@ -1548,6 +1581,7 @@ function buildConformanceExport(report) {
receiptStates: provider.scenarioReceiptStates || [],
surfaces: provider.surfaceIds,
surfaceKinds: provider.surfaceKinds,
surfaceContracts: provider.surfaceContracts,
commands: provider.commandIds,
pluginRequirements: provider.pluginRequirements,
})),
@ -1599,6 +1633,7 @@ function buildCapabilityMatrix(report) {
status: provider.status,
surfaces: provider.surfaceIds || [],
surfaceKinds: provider.surfaceKinds || [],
surfaceContracts: provider.surfaceContracts || [],
commands: provider.commandIds || [],
receiptStates: provider.scenarioReceiptStates || [],
handoffExportPath: makeHandoffExportPath("provider", provider.providerId, provider.handoffPath),

View file

@ -9,6 +9,15 @@
"requires": {
"fixtures": ["aetheria-world"],
"surfaces": ["aetheria.daemon.game"],
"worldSurfaces": [
{
"surfaceId": "aetheria.daemon.game",
"surfaceKind": "interactive-world",
"projectionKind": "provider-authored-world-surface",
"commandBoundary": "aetheria.daemon.commands",
"receiptSchema": "aetheria.eve_command_acceptance_status.v1"
}
],
"commands": ["aetheria.daemon.commands"],
"schemas": ["gamecult.eve.surface.v1", "gamecult.eve.command.v1", "aetheria.eve_command_acceptance_status.v1"]
},

View file

@ -14,6 +14,7 @@
"field.surface2d world projection",
"daemon command boundary",
"provider-owned bindings",
"provider-advertised world interaction contract",
"Aetheria style tokens"
],
"splitTarget": "Aetheria"

View file

@ -96,6 +96,18 @@
"mode": "interactive",
"surfaceKind": "interactive-world",
"interactionModel": "provider-command-receipts",
"worldInteraction": {
"projectionKind": "provider-authored-world-surface",
"stateSchemas": [
"aetheria.world_state.v1",
"aetheria.runtime.daemon.frame.v1",
"aetheria.runtime.daemon.soa_view.v1"
],
"commandBoundary": "aetheria.daemon.commands",
"receiptSchema": "aetheria.eve_command_acceptance_status.v1",
"loweringTargets": ["web-reference", "unity-uitoolkit", "unity-scene", "tui"],
"ownership": "provider-owns-world-state-assets-command-acceptance-and-receipts"
},
"summary": "Authoritative daemon game surface consumed by Unity and web lowerers."
},
{
@ -116,7 +128,19 @@
"audience": "operator",
"mode": "interactive",
"surfaceKind": "interactive-world-editor",
"interactionModel": "provider-command-receipts"
"interactionModel": "provider-command-receipts",
"worldInteraction": {
"projectionKind": "provider-authored-world-editor-surface",
"stateSchemas": [
"aetheria.world_state.v1",
"aetheria.runtime.daemon.editor_surface.v1",
"aetheria.runtime.daemon.command_boundary.v1"
],
"commandBoundary": "aetheria.daemon.commands",
"receiptSchema": "aetheria.eve_command_acceptance_status.v1",
"loweringTargets": ["web-reference", "unity-uitoolkit", "tui"],
"ownership": "provider-owns-editor-state-assets-command-acceptance-and-receipts"
}
}
],
"commands": [