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.
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:
- a target catalog (canonical dotted IDs → descriptors)
- a setter table (target ID → absolute, idempotent setter)
- 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 twocapability.changedevents, after which the gated action succeeded. - Standalone: each exhibit opened from
file://with the server stopped. Renders, contract works, native UI works, andperformance.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:
- Target IDs were not canonical.
action.cleanGlass,telemetry.fishCount,sky.magnitudeLimitand 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). - 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, andtransport.test.jspins it down. - The harness raced its own navigation.
connect()calleddisconnect()first, which queued anabout:blankload that then beat the real navigation, leaving an empty frame. - The harness page never loaded
exhibit-shell.js.harness.jsusesXZBTShellfor element lookup, so it threw on load and the page was inert. correlationIdwas 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
- Write
exhibit.js— a working exhibit with absolute, idempotent setters that return{ changed }, aread(targetId), and real actions. No contract awareness. - Write
contract-adapter.js— declare the catalog, bind setters, readers, and actions, and route the UI throughcore.applyMutation/core.invokeAction. - Write
boot.jsandindex.htmlfollowing an existing exhibit. - Add the exhibit to
EXHIBITSintests/conformance.test.jsand toADAPTER_EXPORTS. The shared conformance group then runs against it automatically. - Add it to the harness dropdown. No harness code changes — that is the test.