> **Local copy notice.** This directory contains locally corrected copies of > the upstream reference exhibits. The corrections are documented in > `../PROVENANCE.md`. The text below is the original upstream README; its > verification claims describe the upstream state, not this corrected copy. > Re-copying from the upstream source without reapplying corrections will > reintroduce known defects. # XZBT Exhibit Contract 5.2 — Reference Exhibits Three unrelated exhibits and one generic host, all speaking XZBT Exhibit Contract 5.2. The point of the set is not the exhibits; it is that a single unmodified host drives all three, and that the contract layer is shared rather than reimplemented per exhibit. ``` reference-exhibits/ shared/ contract-core.js generic contract engine (no domain knowledge) host-transport.js same-origin postMessage transport (optional) exhibit-shell.js generic DOM helpers (optional) exhibit-shell.css generic page chrome (optional) aquarium/ exhibit 1 — reef tank planetarium/ exhibit 2 — sky dome haunted-house/ exhibit 3 — the one with a real capability lifecycle host-harness/ a generic host that knows nothing about any of them tests/ Node test suites + a static server for manual checks ``` ## Running it No build step, no dependencies, no package manager. **Standalone.** Open any exhibit's `index.html` directly in a browser. It works from `file://` with the network off. The header reads `standalone` and nothing is ever posted anywhere. **With a host.** The harness uses `postMessage` with an origin check, and `file://` documents report origin `null`, so serve over HTTP: ``` node tests/serve.js ``` Then open `http://localhost:8731/host-harness/index.html`, pick an exhibit, and press Connect. The harness performs the handshake, reads `describe`, and builds its entire control surface from the descriptors the exhibit published. **Tests.** ``` node tests/run-all.js ``` 137 checks, no dependencies. See *Verification* below for what they do and do not cover. ## The layering Each exhibit is split into four files, and the split is the whole argument: | File | Knows about | Does not know about | | --- | --- | --- | | `exhibit.js` | the domain, the canvas, the native controls | the contract, target IDs, events | | `contract-adapter.js` | the contract, the target catalog | rendering, DOM | | `boot.js` | composition only | everything else | | `index.html` | markup and script order | behaviour | `exhibit.js` is a complete, working exhibit on its own. Delete `contract-adapter.js`, `host-transport.js`, and `boot.js`'s last three lines and you still have a functioning aquarium. That is deliberate: the contract is a layer added on top of a working exhibit, not a scaffold the exhibit is built inside of. The shared `contract-core.js` is domain-free. It knows the *shape* of the contract — envelopes, target kinds, revisions, sequences, error codes, capability lifecycle — and nothing about fish, stars, or ghosts. Each adapter supplies three things and gets a conforming surface: 1. a target catalog (canonical dotted IDs → descriptors) 2. a setter table (target ID → absolute, idempotent setter) 3. an action table (target ID → real impulse implementation) ## The three exhibits | | Aquarium | Planetarium | Haunted House | | --- | --- | --- | --- | | Targets | 12 | 14 | 13 | | Capabilities | `render` | `render` | `render`, `audio` | | Notable | step-validated ranges | action with a typed argument | a genuinely gated action | The Haunted House is the interesting one. Its audio is synthesized with the Web Audio API, and browsers refuse to start an audio context until the user has interacted with the page. So `audio` really does begin `available`, really does pass through `loading` to `ready` on the first gesture, and really does report `error` if the context cannot be created. The adapter mirrors that real state into the capability registry; it never invents a transition to make the contract look busy. `action.moan` and `action.slam-door` declare `requires: ['audio']` and return `CAPABILITY_UNAVAILABLE` until the engine is actually ready — while staying discoverable in `describe` the whole time. ## Contract decisions worth stating **One mutation chokepoint.** Every persistent-state change — native UI, hotkey, or host `set` — goes through `applyMutation`. One call is one transaction: commit, increment `stateRevision` at most once, then emit the resulting events carrying that revision. A host `set` and a slider drag are the same transaction with a different `source`. **No silent clamping.** An out-of-range value is rejected with `INVALID_VALUE`, not quietly pinned to the boundary. A value off the declared `step` is rejected too. A host that sends nonsense should be told. **No-ops do not move the revision.** Setters are absolute and idempotent and report whether anything actually changed. Setting a value to what it already is returns `changed: false` and leaves `stateRevision` alone, so a host can poll without inflating history. **`stateRevision` survives reconnects; `sequence` does not.** A new session resets the event sequence but deliberately leaves `stateRevision` intact, so a reconnecting host can tell whether it missed anything. **Source is assigned at the trusted boundary.** A `source` field inside an incoming message is ignored. The transport stamps `host`; the UI path stamps `ui`. **Event shape is identical on every path.** `correlationId` is always present (null when the change did not originate from a request) so a host can rely on one schema regardless of what caused the change. **Capabilities are honest.** An exhibit that cannot fail declares `ready` once. An exhibit with a real lifecycle reports it. Nothing fakes a transition. **Text is data.** Every string that reaches the DOM goes through `textContent`. Nothing in this project assigns `innerHTML`, so a string arriving from a host or a scenario can never become markup or script. ## Verification ### Automated — `node tests/run-all.js` **`conformance.test.js` (126 checks).** Loads the real `contract-core.js` and the real `contract-adapter.js` from each exhibit into a sandbox, with a small stub standing in for the exhibit's own services. The adapter under test is the genuine article; only the domain object behind it is a stub. Covers handshake and session validation, `describe` shape, canonical target IDs, `state.get` filtering, mutation semantics (commit, revision, no-op, absolute, rejection), action semantics, event envelopes and monotonic sequence, session reset, source trust, UI/host parity, malformed input, the capability lifecycle, and action arguments. A final cross-exhibit group runs one generic request sequence against all three. **`transport.test.js` (11 checks).** Covers the postMessage layer against a fake window: origin and source validation, version filtering, and — the reason the file exists — that events are *pushed* to a connected host, not just responses. ### Manual — browser Verified in a real browser against a local server: - All three exhibits render (canvas confirmed painted, not blank) and load with a clean console. - Every native control on all three exhibits was exercised; each routes through the canonical path and produces the expected event with `source: 'ui'`. - The generic host connected to all three exhibits unmodified, built its UI from their descriptors, and drove them: host `set` → exhibit commits → event pushed back → host revision, sequence, and readout all update. - The Haunted House capability lifecycle was observed end to end from a real button click: `available → loading → ready`, with two `capability.changed` events, after which the gated action succeeded. - Standalone: each exhibit opened from `file://` with the server stopped. Renders, contract works, native UI works, and `performance.getEntriesByType('resource')` shows zero external resources. ### Not covered - No automated browser tests. Rendering and the browser's audio policy are verified by hand, as above. - The exhibits are reference implementations, not production art. The simulation is simple by design; the contract is the subject. ## Bugs found and fixed during verification Recorded because they are the kind that pass a unit suite and fail a user: 1. **Target IDs were not canonical.** `action.cleanGlass`, `telemetry.fishCount`, `sky.magnitudeLimit` and others used camelCase segments. The catalog validator rejected them at construction. All IDs are now lowercase-hyphenated (`action.clean-glass`, `telemetry.fish-count`, `sky.magnitude-limit`). 2. **The transport never forwarded events.** It sent responses only, so a host could issue a `set`, receive an acknowledgement, and never learn that anything had changed. The exhibit looked correct in isolation and the conformance suite passed; only the host/exhibit link was broken. The transport now forwards the core's events to a connected host, and `transport.test.js` pins it down. 3. **The harness raced its own navigation.** `connect()` called `disconnect()` first, which queued an `about:blank` load that then beat the real navigation, leaving an empty frame. 4. **The harness page never loaded `exhibit-shell.js`.** `harness.js` uses `XZBTShell` for element lookup, so it threw on load and the page was inert. 5. **`correlationId` was conditionally present.** It was omitted for UI-originated changes and included for host-originated ones, so the event shape differed by path. It is now always present, null when not applicable. ## Adding a fourth exhibit 1. Write `exhibit.js` — a working exhibit with absolute, idempotent setters that return `{ changed }`, a `read(targetId)`, and real actions. No contract awareness. 2. Write `contract-adapter.js` — declare the catalog, bind setters, readers, and actions, and route the UI through `core.applyMutation` / `core.invokeAction`. 3. Write `boot.js` and `index.html` following an existing exhibit. 4. Add the exhibit to `EXHIBITS` in `tests/conformance.test.js` and to `ADAPTER_EXPORTS`. The shared conformance group then runs against it automatically. 5. Add it to the harness dropdown. No harness code changes — that is the test.