# The data-socket contract

This is the whole point: **every world object is a socket** that live data plugs into. The art is the skin; the bindings make the village a dashboard you walk around in.

## Schema

A binding is a plain object. Keep them in a registry (`bindings.json` / DB) so non-engineers can rewire the world without touching render code.

```jsonc
{
  "id":     "crew.cowork",        // the world object to drive (crew.* | bb.* | light.* | world.*)
  "socket": "agent.status",       // the socket TYPE — declares what shape of data this object accepts
  "bind": {
    "source": "bridge-state",     // a registered feed (see Feeds)
    "path":   "agents.cowork",    // dot-path into the feed's payload
    "poll":   3000,               // ms between pulls; or "stream" for push/SSE/WS
    "map":    { "presence": "presence", "activity": "state", "badge": "buildStatus" },
                                   //  ^ object-prop  ^ field in the feed payload
    "transform": "optional named transform id (e.g. 'wmoToWeather', 'loadToGlow')"
  }
}
```

A tiny runtime resolves it: pull `source`, read `path`, apply `map` (and `transform`), write the resulting props onto the live world object, which the renderer already reads each frame. One generic resolver drives all socket types.

## Socket types

| socket | drives | accepts (after `map`) | visual effect |
|---|---|---|---|
| `agent.status` | a crew member | `presence` (online/away/busy/offline), `activity` (idle/working/building/testing/…), `badge` (number/string) | presence → opacity + parked-at-door; activity → walk/idle/work animation + station; badge → nametag chip |
| `screen.feed` | a billboard | `title`, `lines[]`, `link{label,url}`, `tone` | re-renders the screen overlay; `tone` tints the bezel glow |
| `zone.metric` | a room's light(s) | `value` 0..1, optional `color` | scales that room's emissive `alpha` and/or hue (load → brighter/redder) |
| `prop.state` | a single prop | `on` bool, `level` 0..1 | e.g. server LEDs, cog spin speed, fire size |
| `world.weather` | global weather | `code` (WMO) or `key` (clear/rain/…), `temp`, `loc` | sets the weather system (already real via open-meteo) |
| `world.clock` | global time | `hour` 0..24 or ISO time | sets time-of-day (default: device clock) |

`socket` is a contract, not code per object — adding a 13th agent is one more `agent.status` row, no new rendering.

## The mapping DSL

`map` is `{ objectProp: feedField }`. Two escapes when a straight rename isn't enough:

- **`transform`** — a named pure function `(mappedObject, rawPayload) => objectProps`. Ship a small library: `wmoToWeather`, `loadToGlow`, `presenceToOpacity`, `countToSwarm`. Keep them data-driven and testable.
- **literal** — prefix a `map` value with `=` to inject a constant: `"tone": "=#2EE6C8"`.

Nested/array fields use the same dot/bracket path syntax as `path` (`tasks[0].title`).

## Feeds

Register sources once; bindings reference them by name.

```jsonc
{
  "bridge-state": { "kind": "http", "url": "https://<bridge>/bridge-state", "auth": "pin", "shape": "{ agents: { <id>: { presence, state, ... } }, zones: {...} }" },
  "calendar":     { "kind": "http", "url": "...", "poll": 60000 },
  "ci":           { "kind": "stream", "url": "wss://...", "event": "build" }
}
```

For `bridge-state`, `agent.status` bindings just point `path` at `agents.<id>`. Your roster IDs (`dispatch`, `hermes`, `codex`, `cowork`, `fable`, `jarvisPrime`, `jarvisAnchor`, `qlaJarvis`, `r13Foreman`, `optiplexNode05`, `chatgpt`, `erikasMinions`) are already stubbed in `crew.json` — adjust the exact `path`/field names to your real payload.

## Worked examples

**Cowork's avatar reflects the live build:**
```json
{ "id": "crew.cowork", "socket": "agent.status",
  "bind": { "source": "bridge-state", "path": "agents.cowork", "poll": 3000,
            "map": { "presence": "presence", "activity": "state", "badge": "buildStatus" } } }
```
`presence:"away"` → avatar dims to 55% and parks by the Mechanical door; `state:"building"` → walks to the cog and the badge shows `buildStatus`.

**The Workfloor billboard shows the real standup + a live link:**
```json
{ "id": "bb.workfloor", "socket": "screen.feed",
  "bind": { "source": "calendar", "path": "today.standup", "poll": 60000,
            "map": { "title": "=TODAY", "lines": "agenda", "link": "meetingLink" } } }
```

**The Movement room glows with server load:**
```json
{ "id": "light.movement.racks", "socket": "zone.metric",
  "bind": { "source": "bridge-state", "path": "zones.movement.load", "poll": 2000,
            "map": { "value": "value" }, "transform": "loadToGlow" } }
```
`loadToGlow(v)` → `{ alpha: 0.3 + v*0.5, color: lerp('#2EE6C8','#F0479E', v) }` (calm teal → hot magenta under load).

**Weather follows your station instead of the browser:**
```json
{ "id": "world.weather", "socket": "world.weather",
  "bind": { "source": "bridge-state", "path": "env.weatherCode", "poll": 300000,
            "map": { "code": "code", "temp": "tempC", "loc": "site" }, "transform": "wmoToWeather" } }
```

## Events (optional, nice-to-have)

Let the world emit too: `onClick(crew.id)` → focus + open that agent's panel/log; `onEnter(zone.key)` → subscribe higher-rate feeds for that room only (cheap when zoomed out). A small event bus (`world.on('select', cb)`) keeps it decoupled from render.

## Why this shape

- **One resolver, N objects** — bindings are data; the world scales by adding rows.
- **Designers rewire without code** — point a screen at a new URL, bind a light to a new metric.
- **Degrades gracefully** — a dead feed just leaves the object at its `default`/idle state (exactly how weather already falls back offline).
