start here

Describe once, serve both.

Never heard of gamestruct? This is the five-minute on-ramp. It ends with a surface that both a screen reader and an agent can operate.

1. Describe

A node carries a handle (a stable id), a role (what it is), an accessible name (what a screen reader says), and the actions it accepts. Use a stable handle - it is the same one both lanes use.

import { Bridge, ROLES } from 'gamestruct'

const bridge = new Bridge({ name: 'pause menu' })
bridge.tree.add(null, { id: 'ui:start', role: ROLES.BUTTON, name: 'Start game' })
bridge.tree.add(null, { id: 'ui:volume', role: ROLES.SLIDER, name: 'Volume', value: 50 })

2. Wire

One handler per actionable handle. The verb is the action the node declares - activate for a button, set_valuefor a slider.

bridge.on('ui:start',  { activate: () => startGame() })
bridge.on('ui:volume', { set_value: (v) => setVolume(v) })

3. Read both lanes

The same bridge now renders to a screen reader and to an MCP client. Either lane drives the same act() - there is no agent rail and no second protocol.

human.a11yDump(bridge)      // the screen-reader text
agent.toolCatalog(bridge)     // the MCP tool list
await bridge.act('ui:start')    // one code path

4. Gate it

Run the a11y audit in CI. It flags the failures that actually break a screen reader: an interactive node with no name, one with no action, an image with no alt text.

const { ok, problems } = human.audit(bridge)

# or see both lanes + the audit at once:
npx gamestruct doctor