Define Aetheria world surface contract
This commit is contained in:
parent
f505b7e86c
commit
be05146883
12 changed files with 255 additions and 1 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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 `
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"field.surface2d world projection",
|
||||
"daemon command boundary",
|
||||
"provider-owned bindings",
|
||||
"provider-advertised world interaction contract",
|
||||
"Aetheria style tokens"
|
||||
],
|
||||
"splitTarget": "Aetheria"
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
Loading…
Reference in a new issue