The contract
A door exposes exactly four primitives. A client that uses only these four can operate the surface.
| primitive | signature | meaning |
|---|---|---|
observe |
() -> Snapshot |
the surface’s state for this seat |
act |
(handle, verb?, value?) -> Result |
do one thing, by stable handle |
wait_for |
(predicate, timeoutMs) -> Outcome |
resolve when a condition holds, or time out honestly |
events |
(sinceFrame) -> { frame, events[] } |
what changed while you were not looking |
Stable handles
An act names a handle — a stable id like ui:start or
world:target:wolf_1. Never a pixel, a rect, or a positional index. The whole
point is that the same handle works before and after a re-render, for the human
lane and the agent lane alike.
import { mintHandle } from 'gamestruct'
mintHandle('ui', 'start') // 'ui:start'
The node shape
{
id: 'ui:volume', // stable handle, unique in the tree
role: 'slider', // ARIA-aligned role
name: 'Volume', // the accessible name
value: 50, // current value
actions: ['set_value'], // the verbs this node accepts
min: 0,
max: 100,
parent: 'root',
children: [],
}
Interactive roles (button link textbox checkbox radio slider switch combobox
menuitem tab listitem) must carry a non-empty name and at least one action.
Live roles (status log alert) announce on change.
Honesty
Every refusal is a value, never a thrown exception and never a silent no-op:
bridge.act('ui:nope')
// { ok: false, reason: 'no such handle: ui:nope' }
That is SPEC rule N21. A door that fails quietly is worse than one that fails loudly — an agent cannot recover from silence.