# Museum Gallery — Step 6.2 multi-surface reference exhibit A deliberately small XZBT Contract 5.3 exhibit built to prove the multi-surface presentation model from `docs/architecture/XZBT-Multi-Surface-Model-Step6.1.md` (Revision 2) and `docs/contract/XZBT-Exhibit-Contract-Specification-v5.3.md` Section 31 — not to be visually elaborate. See `docs/reference/Museum-Gallery-Step6.2-Verification.md` for the full verification writeup; this file documents the exhibit's own structure and its chosen internal attachment transport. ## Entry point `control.html` is the primary surface (`surface.control`, `primary: true`) and the exhibit's canonical entry point — open it directly for full standalone operation, exactly like any other reference exhibit's `index.html`. `index.html` in this directory is a plain redirect to `control.html`, kept only for directory-listing consistency; it has no behavior of its own. ## Surfaces | id | label | primary | url | role | | --- | --- | --- | --- | --- | | `surface.control` | Control Room | `true` | `control.html` | control | | `surface.artifact` | Artifact Display | `false` | `artifact.html` | ambient | | `surface.info-wall` | Information Wall | `false` | `info-wall.html` | information | ## Exactly one Exhibit State Core `control.html` is the only document that loads `contract-adapter.js` (and therefore the only document that constructs an `XZBTContractCore.ContractCore` instance). `artifact.html` and `info-wall.html` load only `surface-bus.js` — they are structurally incapable of constructing a second Core; the shared `tests/museum-gallery.test.js` suite asserts this directly (see "Exactly one Exhibit State Core exists" in that file). ## Chosen internal attachment transport: `BroadcastChannel` `surface-bus.js` implements the generic attachment sequence from Step 6.1 §7.2 using a same-origin `BroadcastChannel` named `xzbt-museum-gallery-core-v1`, with `control.html` as the single, clearly identified authoritative owner document (Contract 5.3 §31.7's "cross-document attachment" shape). Why `BroadcastChannel` over the other two permitted shapes: - it needs no window-handle bookkeeping (unlike `postMessage` to a specific `window` reference, which breaks if the owner window reference is lost or the surface was opened independently rather than via `window.open`); - it needs no separate worker lifecycle (unlike `SharedWorker`, which is unsupported in some embedding contexts and adds a process to reason about for a reference fixture this small); - every participant — owner or surface — only ever needs to know one string (the channel name), which keeps `control.html`, `artifact.html`, and `info-wall.html` fully decoupled from each other; none of them reference the other documents by name or handle. This transport is exhibit-internal. It carries no XZBT contract envelope, is never observed by a host, and is not mentioned anywhere in Contract 5.3 — per Step 6.1 §7.2 and Contract 5.3 §31.7/§31.9, the contract only needs to know that surfaces exist and how a host opens one. A different exhibit is free to choose `SharedWorker`, in-process attachment, or another same-origin mechanism entirely. ### Message shapes (informal, exhibit-internal only) - surface → owner: `{ type: 'attach', requestId }` - owner → surface: `{ type: 'attach.snapshot', inReplyTo, snapshot, registryRevision }` - owner → all: `{ type: 'core-event', event }` — one relayed copy of every normalized event the Core already emits (`state.changed`, `selection.changed`, `action.executed`, …) - surface → owner: `{ type: 'mutate', kind: 'set'|'invoke', target, value|args }` — routed straight into `core.applyMutation` / `core.invokeAction` with `source: 'ui'`, the same chokepoint the primary UI's own controls use - surface → owner: `{ type: 'detach' }` — bookkeeping only; never mutates state ## Shared state `artifact.selected` (selection), `lighting.level` (range 0–1), `rotation.speed` (range 0–2), `labels.enabled` (state/boolean), plus one impulse, `action.spotlight-flash`, to prove `action.executed` propagation across surfaces. ## Native interactions proving the canonical mutation path - Artifact Display's "Cycle artifact" button calls `link.mutate('set', 'artifact.selected', …)`. - Information Wall's "Toggle labels" button calls `link.mutate('set', 'labels.enabled', …)`. Both are relayed by the bus into `core.applyMutation` on the one Core that `control.html` owns — the same call the Control Room's own controls and a contract `set` from NGN would make. See the verification document for the event-sequence and stateRevision proof. ## Running it directly (no test harness) Serve this repository with `npm start` and open: ``` http://127.0.0.1:4173/test-fixtures/reference-exhibits/museum-gallery/control.html ``` Then open `artifact.html` and `info-wall.html` in separate tabs/windows from the same origin. Changing state in any one window updates the other two. Closing and reopening a non-primary surface reflects current state immediately. Opening `artifact.html` or `info-wall.html` alone, with `control.html` not open anywhere, shows a "Waiting for the Control Room surface to be open…" state rather than inventing its own state. ## Known limitation: NGN attachment is not exercised in Step 6.2 `control.html` includes the optional `XZBTHostTransport`, matching every other reference exhibit. Because Museum Gallery declares `xzbtVersion: '5.3'` while the current (pre-6.3) NGN host always sends the envelope tag `xzbt: '5.2'`, an actual attach attempt from today's NGN would be rejected at the envelope-version check rather than negotiating a Contract-major-5 session. This is expected and intentional: NGN's generic surface discovery and version negotiation are Phase 6.3+ work, explicitly out of scope for Step 6.2. See the verification document's known-limitations section.