Files
XZBT-NGN/test-fixtures/reference-exhibits
Labyricorn 745912e451 Steps 6.4-6.7B — Local surfaces, reference-exhibit validation, SciFi Observation surface
One commit for the work accumulated in the working tree since Step 6.3,
which had never been split into per-step commits:

- src/local-surfaces.js + src/surface-url.js (new); src/ui.js,
  src/validation.js, src/connection.js and public/index.html updated for
  local-surface hosting and generic surface rendering
- tests: local-surfaces (20), scifi-surfaces (24) and postmessage-interop (7)
  new; connection/museum-gallery/surface-validation suites updated
- reference exhibits: shared/contract-core.js defaults to Contract 5.3
  (major 5, minor 3, xzbt 5.3); museum-gallery advertises its surface
  catalog; aquarium/haunted-house/planetarium adapters updated
- SciFi-XZBT (Step 6.7A/6.7B): surface-mode.js + surface-bus.js,
  Observation-surface boot branch, local-change hooks, view.pillars /
  view.warp-flight targets; fixture byte-identical to G:/.vibe/SciFi-XZBT
- SciFi-XZBT contract adapter handshake fix: the inbound bridge filter no
  longer gates on an exact advisory xzbt value (Contract 5.3 §6.5), only on
  its presence/type, matching the host's own envelope validation; the
  adapter now advertises contract minor 3 / version 5.3.0, which it already
  implemented via the 5.3 surfaces field. Root cause of the five failing
  postmessage-interop tests (host hello was silently dropped).
- docs: architecture 6.4 and 6.7A, reference 6.6 and 6.7; evidence logs;
  test-fixtures/PROVENANCE.md resync record

Test results: NGN 154/154 (was 149/154); postmessage-interop 7/7 (was 2/7);
SciFi contract harness 21/21, real-adapter suite 32/32. git diff --check
clean for changed files; two pre-existing trailing-whitespace lines remain
in test-fixtures/reference-exhibits/scifi/index.html, copied verbatim from
the authoritative SciFi source.

Step 6.7 live verification (browser Observation, packaged standalone) is
still pending and is not claimed here.
2026-09-14 19:45:27 -07:00
..
2026-09-14 07:57:18 -07:00

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.