48 KiB
XZBT Exhibit Authoring Guide
Version 5.2
Status: Authoring guidance (non-normative except where explicitly marked) Companion to: XZBT Exhibit Contract Specification 5.2 Audience: developers and coding agents building a new XZBT-compatible exhibit
A. Purpose and Audience
This guide explains how to structure a new, standalone exhibit so that it correctly implements the XZBT Exhibit Contract from the start, without absorbing responsibilities that belong to the XZBT-NGN Exhibit Engine.
It is written for two kinds of reader: a human developer building an exhibit by hand, and a coding agent implementing one from a specification. Both should be able to follow it without prior exposure to any specific existing exhibit.
Two documents work together and answer different questions:
- The XZBT Exhibit Contract Specification defines the external behavior a conforming exhibit MUST, SHOULD, or MAY exhibit at the message and semantics level: the handshake, the message envelope, target kinds, revisions, events, capabilities, and error codes.
- This Authoring Guide explains a recommended internal implementation structure that makes satisfying that contract straightforward, consistent, and maintainable, and calls out the mistakes that make it hard.
Where this guide uses MUST, MUST NOT, SHOULD, or SHOULD NOT for something that is not already a contract requirement, that is an authoring recommendation, not a new contract rule. Every such case is called out explicitly. Nothing in this guide expands, narrows, or reinterprets Contract 5.2.
An example exhibit, SciFi-XZBT, is referenced throughout for concreteness. Nothing here is specific to that exhibit's science-fiction subject matter, and none of its vocabulary should be treated as universal. A train exhibit, an aviation exhibit, a castle exhibit, and any future XZBT exhibit are equally valid, and this guide is written so that none of them inherit assumptions that only make sense for a starship bridge.
B. Core Design Principle: Standalone First
CONTRACT REQUIREMENT. Contract 5.2 §2 states that an XZBT-compatible exhibit MUST remain operable without XZBT-NGN, and that connection to XZBT-NGN is additive.
This is the single governing constraint on everything else in this guide. In practice it means an exhibit:
- MUST run meaningfully with no host attached at all — full native UI, full native behavior, full native content.
- MUST NOT require a handshake, a session, or any host message to initialize, animate, play audio, or respond to local input.
- MUST NOT require network access, a local server, or any orchestration layer for ordinary operation.
- MUST preserve its native UI's own behavior and feel regardless of whether a host is attached.
- MUST remain a complete, self-contained artifact a person can open and use with nothing else running.
AUTHORING RECOMMENDATION. The contract layer should be dormant, not merely tolerant, when no host is present. Concretely: the exhibit boots, and only when (and if) a host later sends hello does any session-related code path activate. A useful test during development is to delete every host-detection code path and confirm the double-clicked file behaves identically. If a future host connects, its presence should feel like an enhancement layered on top of a complete experience, never like flipping the exhibit into a different mode of existence.
C. Recommended Internal Architecture
AUTHORING RECOMMENDATION. No specific framework, build system, or language is required. Plain HTML, CSS, and JavaScript with no framework at all remains completely valid, and a conforming exhibit may equally be built with a modern framework, a game engine's UI layer, or a native application shell rendering through the same message-passing conventions where applicable. What matters is the separation of concerns below, not the tooling used to express it.
A recommended internal separation, roughly in dependency order (each layer may call the ones above it, and generally should not be called by them):
- Exhibit state/model. The actual data that constitutes the exhibit's condition: is the transport running, what is the master volume, which scene or theme is selected, what alert state is active. This is the source of truth
state.geteventually reads from. - Exhibit services/behavior. The logic that changes the model in response to something happening: starting playback, applying a scene, running the simulation loop, synthesizing a sound. This layer owns the actual mutation of state 1 and should not care who or what asked for the mutation.
- Native UI. Buttons, sliders, dropdowns, canvases, whatever the exhibit's own screen presents. This layer calls into layer 2 and renders the current state from layer 1. It has no privileged access to contract internals and no special powers the contract adapter lacks.
- Canonical control layer. One set of functions — the mutation/invoke chokepoint described in section H — that every input source (UI, hotkeys, host) calls to change state or trigger an action. This is the layer that makes source attribution, transaction semantics, and idempotency actually enforceable, because everything funnels through it.
- Contract adapter. The layer that knows about XZBT vocabulary: canonical target IDs, descriptors,
describe,state.get, event formatting, capability reporting. It translates between the canonical control layer's internal calls and the wire-level contract semantics. It does not itself contain exhibit behavior. - Optional host transport. The thin layer that moves contract messages across whatever channel is in use (same-origin
postMessage, a wrapper binding, a local IPC channel). It has no knowledge of exhibit semantics; it only frames, validates origin, and forwards.
The dependency direction that matters most: layers 1–3 must work with layers 4–6 entirely absent. Layer 6 must be swappable without touching layers 1–4. A helpful gut check while designing any new piece of functionality is to ask which of these six things it is, and to resist the temptation to let a contract concern (layer 5–6) leak down into exhibit behavior (layer 1–3), or an exhibit behavior concern leak up into the adapter.
D. Canonical Target Registry
CONTRACT REQUIREMENT. Target IDs use a canonical dotted namespace (Contract §8): lowercase ASCII letters, digits, hyphens and dots, beginning with a lowercase letter, at least one dot-separated segment boundary, no whitespace, no empty segments, and never interpreted as a JavaScript property path.
AUTHORING RECOMMENDATION. Design the registry as one stable, deliberately curated public catalog: canonical dotted IDs, each with a declared kind (state, range, selection, or impulse), current value where applicable, and a clear category. This catalog is what a generic host discovers through describe; it should describe the exhibit's meaning, not its markup.
Target IDs should name what a thing means to an operator, not where it lives in the DOM or how it happens to be wired internally.
Good examples:
transport.playing
environment.intensity
mode.selected
event.pulse
Bad examples:
button-14
slider-left
panelB.knob2
The difference is not cosmetic. A meaningful ID survives a UI redesign, a relayout, or a rewrite of the exhibit's internals, because it describes a concept the exhibit owns rather than a widget the exhibit happens to render today. button-14 tells a host nothing about what will happen if it is invoked and becomes meaningless the day the button moves.
AUTHORING RECOMMENDATION. Adopt a fixed catalog rather than a catalog that changes shape as the exhibit's internal context changes. A target that only makes sense in one mode, theme, or scene should still generally remain in the catalog and simply report CAPABILITY_UNAVAILABLE (or an equivalent contextual error) when invoked outside that context, rather than disappearing and reappearing. Registry churn driven by ordinary context changes forces every attached host to rediscover the whole catalog constantly, which is expensive and provides no real benefit — the fixed-catalog approach lets a host build its control surface once and treat temporary unavailability as a normal, expected state rather than a structural surprise. See Section N for the discoverable-versus-invokable distinction this depends on.
E. Target Descriptor Design
CONTRACT REQUIREMENT. Each target returned by describe must include enough metadata for a generic host to inspect and operate it (Contract §9): id, kind, readable, writable, restorable, category, requires, and kind-specific fields — min/max/step for range, valueType for state, options for selection. Where a target accepts structured arguments, those arguments must be validated against a declared schema (Contract §11).
CONTRACT REQUIREMENT. Descriptors must reflect real behavior. The contract must not advertise fake actions, unsupported values, or nonexistent capabilities. A published target that can never successfully execute in any context, for reasons no declared capability explains, is a describe-accuracy defect, not a permissible "discoverable but unavailable" case — that allowance exists specifically for context-gated unavailability tied to a real, declared capability (Contract §9, §17).
AUTHORING RECOMMENDATION — the lesson from Step 3. During SciFi-XZBT's Step 3 verification, several range descriptors advertised a wider span than the exhibit's own native slider could select, and in one case a native slider could select a value the descriptor explicitly forbade. The forbidden-but-selectable case is the dangerous direction: it lets a normal, local, unremarkable action (dragging a slider) produce a value the exhibit has told every host is impossible, which the canonical mutation path then has to silently reject — the thumb moves, nothing else does, and the operator gets no explanation.
The general rule this produced: the native UI and the public contract range for the same underlying value should represent one coherent operator-facing range. If the exhibit's own control can only usefully reach a narrower or coarser range than the descriptor claims, that is usually fine and often desirable — the descriptor can legitimately describe more of the backing capability than any one local widget exposes. If the native control can reach further or finer than the descriptor claims, that is a real defect regardless of how it happened, because a local, ordinary interaction is now able to produce a contract-invalid state. Author descriptors and native controls together, and treat "control broader than contract" as a bug class to check for explicitly during development, not just at final review.
F. State Design
CONTRACT REQUIREMENT. Persistent state belongs in state.get; impulses do not appear in a restorable snapshot (Contract §10, §13). A state snapshot must not imply that every internal exhibit variable is externally exposed — only readable persistent targets belong in values.
AUTHORING RECOMMENDATION. Snapshots should contain stable, externally meaningful state: the things an operator or an authored scenario would reasonably want to read back or restore. Do not build a parallel shadow model purely to satisfy the contract's shape. If the exhibit's real internal state cannot yet answer a question the contract needs answered (for example, "is Observation mode currently on"), add a real readable getter to the exhibit's own model rather than fabricating a separate value the adapter tracks independently. A contract adapter that keeps its own belief about exhibit state, disconnected from the exhibit's actual state, will eventually disagree with it.
CONTRACT REQUIREMENT. No-op writes must not produce a new stateRevision (Contract §12, §14.3).
CONTRACT REQUIREMENT — transaction semantics. One top-level contract-visible mutation is one mutation transaction. All contract-visible state changes committed by that transaction share one resulting stateRevision; the exhibit computes and commits the transaction, increments stateRevision exactly once if anything externally visible changed, and only then emits the resulting state.changed/selection.changed events carrying that revision (Contract §14).
AUTHORING RECOMMENDATION. When a single operation changes several values coherently — selecting a scene that also selects that scene's default preset is the canonical example — commit all of them together, increment the revision once, and emit every changed target's event carrying that same shared revision. Treat "how many logically-connected values changed as one gesture" as the boundary of a transaction, not "how many individual setter functions happened to run."
G. Absolute Setters and Idempotency
AUTHORING RECOMMENDATION, reflecting how Contract 5.2's state model is meant to be exercised (Contract §12 defines set as changing writable persistent state to a given value, not toggling it): public setters for persistent state should be absolute and idempotent. Setting a value to its current value twice should leave the exhibit in exactly the same state, with no second state change and no second revision.
Good:
set mute = true
set mode = "night"
set intensity = 0.5
Bad:
toggle mute
click mute button
move slider +10%
An exhibit's native UI is free to use toggle buttons or relative gestures internally — that is a legitimate presentation choice, and native UI is exhibit-owned. But the canonical contract path for the same underlying state must convert any such relative or toggle-shaped local action into an absolute value before it reaches the canonical mutation service. A host, a scenario, or another integration needs to be able to say "make this true" without knowing or caring what the current state was, and a relative-only setter makes that impossible to do reliably.
H. Canonical Mutation / Invoke Path
AUTHORING RECOMMENDATION, and the single most load-bearing pattern in this guide: route every externally visible state change and every externally visible action through one canonical internal path.
Host control, native UI, hotkeys, and any other local control surface should converge on the same underlying services wherever practically possible. Concretely, something shaped like:
applyMutation(targetId, value, source, context)
invokeAction(targetId, args, source, context)
should be the only way contract-visible state changes or actions actually happen, regardless of what triggered them.
AUTHORING RECOMMENDATION — avoid DOM-click proxying. Do not use synthetic DOM events (dispatching a fake click, input, or change event to make something happen) as the canonical mechanism for state mutation. This pattern shows up naturally when a hotkey handler is implemented as "find the button that does this and click it programmatically" — it is easy to write and easy to get wrong. It breaks source attribution (the event now looks identical to a real user click, because the code path is the click handler), it can double-apply if both the synthetic dispatch and a canonical call happen, and it makes the actual mutation logic impossible to test or reason about independently of the DOM. This was, concretely, the root cause of a real hotkey source-attribution defect in SciFi-XZBT: two keyboard shortcuts synthesized a button click to trigger their action, which meant the resulting event reported source=ui no matter how the action was actually triggered. The fix was to extract the shared logic into one function that both the button's own listener and the hotkey handler call directly, each passing its own true source.
Direct business/state methods — not DOM proxies — should be authoritative. The DOM is a rendering of state, not a control bus.
I. Native UI Integration
AUTHORING RECOMMENDATION.
- The UI must remain fully functional with no host attached — this is a restatement of Section B at the UI layer specifically.
- UI code should call the canonical services described in Section H, not reimplement mutation logic inline in an event handler.
- Changes originating from the UI should emit the same normalized events a host-originated change would, with
source=ui. - UI state and contract state must stay synchronized: if a host sets a value, the UI must faithfully reflect it, and if the UI changes a value, the contract's readable state must reflect that immediately.
- Host-set values must render in the native UI exactly as they would if a local operator had set them — the UI must not have a way to silently distinguish or discard a host-originated value it doesn't like the look of. If a native control cannot exactly represent a value (see the range lesson in Section E), it must not simply drop or revert that value on the next unrelated interaction.
CONTRACT REQUIREMENT. Not every public target needs a visible UI control (Contract §19: sound and visual actions "should be exposed as impulse targets where useful," which does not imply UI parity). A target may legitimately be external-only when it maps to real, intentional exhibit behavior that simply has no dedicated on-screen control — for example, an event that exists as a synthesis function the exhibit already performs in another context, exposed to hosts without inventing a redundant button for it. What is not acceptable is a target whose only path to actually happening is a UI element that does not exist; the target must resolve to a real, verified, callable action regardless of whether a button triggers it too.
J. Source Attribution
CONTRACT REQUIREMENT. Normalized events must identify the authoritative source of the action when known (Contract §15). The base source values are ui, midi, hotkey, host, scenario, internal, and system. A source value supplied inside a host command must be ignored; the receiving bridge assigns the authoritative source, never the sender.
AUTHORING RECOMMENDATION. Assign source at the trusted boundary that actually knows the truth, never by trusting a caller's claim about itself. The button's own click listener knows it is ui. The keydown handler knows it is hotkey. The host transport, after validating a message came through the negotiated session, knows it is host. None of these should accept an override from the thing they're receiving input from.
Do not teach host-provided source passthrough as authoritative: a host cannot spoof local provenance, by design, because source is assigned by the exhibit's own receiving path, not read out of the incoming message.
For future external control routed through XZBT-NGN — MIDI is the concrete example, following its removal from the standalone exhibit in favor of NGN-owned integration — the exhibit sees these operations arrive as ordinary contract requests over its host transport, and correctly reports source=host, because from the exhibit's point of view that is exactly what they are. NGN is free to separately retain finer-grained provenance (which connector, which physical controller) on its own side of the boundary; that detail is an NGN concern and never needs to cross into the exhibit's own event stream as a new base source value.
K. Events
CONTRACT REQUIREMENT. A conforming exhibit must publish normalized events for contract-visible operations. The base event types are:
state.changed
action.executed
selection.changed
capability.changed
registry.changed
error
CONTRACT REQUIREMENT — sequencing. Event sequence must increase monotonically within one session and is session-scoped: it resets when a new sessionId is issued, and a host must reset its own expected sequence after a reconnect (Contract §16.1). stateRevision tracks committed persistent-state history and does not reset with a new session; sequence tracks delivery order of every emitted event, including impulses and errors. The two counters serve different purposes and must not be treated as interchangeable (Contract §14.4).
CONTRACT REQUIREMENT. Impulses may emit action.executed without changing stateRevision, because an impulse by definition is not persistent state (Contract §10.4, §14).
CONTRACT REQUIREMENT — correlation. When an event is caused directly by a host request, it should carry that request's requestId as correlationId (Contract §16.2).
AUTHORING RECOMMENDATION. Do not introduce new canonical event types beyond the base six without very strong justification, and never as a routine part of building a new exhibit — the base set is deliberately exhibit-generic and sufficient for state, selection, action, capability, registry, and error reporting across very different subject matter.
L. Session and Transport Separation
CONTRACT REQUIREMENT. The contract defines semantics and normalized message envelopes, not one mandatory transport. Permitted transports include same-origin postMessage, trusted wrapper bindings, a local WebSocket transport, local application IPC, and future host-specific bindings (Contract §4). The exhibit must not need to know whether a request originated from a webhook, an automation tool, a simulator, a telemetry source, an administrative UI, a scenario engine, or any other integration — NGN translates all of these into contract operations before they ever reach the exhibit.
AUTHORING RECOMMENDATION. Keep session semantics — handshake, sessionId issuance, sequence tracking — cleanly separated from exhibit logic, so that swapping the transport later (say, from postMessage to a packaged in-process adapter, per the NGN packaging model) requires changing only the transport layer described in Section C, not the contract adapter or the exhibit itself. Do not let postMessage-specific assumptions (message event shape, origin checks) leak into the contract adapter's own logic; keep them isolated in the transport layer that happens to implement that one binding.
CONTRACT REQUIREMENT — same-origin security. Where postMessage is used, both event.origin and event.source must be validated against the expected host relationship on every message (Contract §25).
M. Capabilities
CONTRACT REQUIREMENT. Capabilities are discoverable and stateful. The base lifecycle states are unsupported, available, loading, ready, busy, and error (Contract §17). A capability may change state during a session, and any such change must emit capability.changed. A host must treat the current discovered state as authoritative.
AUTHORING RECOMMENDATION. Do not advertise a capability that does not exist, and do not invent lifecycle transitions the exhibit cannot honestly report. Speech is a useful worked example: available should mean the exhibit implements speech at all; loading should reflect real initialization in progress (voice model loading, engine startup); ready should mean speech can actually be used right now; error should reflect a genuine initialization or operation failure. busy is optional — it describes ongoing occupancy (mid-playback, for instance) and is not required by the contract; wiring it costs real engineering effort (start/end hooks, interruption handling) for a state most hosts do not strictly need. It is entirely acceptable to wire available → loading → ready/error for a capability and leave busy unimplemented; what is not acceptable is a capability that is hard-coded to a static state that is never actually verified against real subsystem behavior. Not every capability needs to exercise every lifecycle state — use only the states that reflect something real for that capability.
N. Contextual Availability
CONTRACT REQUIREMENT. When a required capability is not usable, a target remains discoverable but must not be operated successfully; a generic host should present it as unavailable rather than removing it (Contract §9). registryRevision should not change merely because a fixed target becomes contextually unavailable (Contract §23).
AUTHORING RECOMMENDATION. Keep discoverable and currently invokable as two distinct questions. A sound effect can remain permanently in the fixed catalog while returning CAPABILITY_UNAVAILABLE in the wrong mode or theme — that is normal, expected, and cheap for a host to handle. What must not happen is the registry itself churning (targets appearing and disappearing, registryRevision incrementing) merely because the exhibit switched modes; that is exactly the unnecessary rediscovery cost Section D warns against. Reserve registryRevision increments for cases where the target set or its descriptor metadata genuinely changed — not for ordinary state transitions the exhibit goes through as part of normal operation.
O. Safe Text Handling
CONTRACT REQUIREMENT. Any text surface the exhibit exposes must treat text strictly as data (Contract §20). The contract must not permit arbitrary HTML, JavaScript, CSS, selectors, or executable expressions through a text target. Each text target should declare a maximum accepted length.
AUTHORING RECOMMENDATION. Use explicit, narrowly-typed text arguments in a target's schema rather than an open-ended payload object. Reject unknown argument keys (Contract §11: "unexpected argument keys should be rejected"). Render accepted text using safe mechanisms — textContent assignment or an equivalent that cannot be interpreted as markup — never innerHTML or an equivalent that would let a string become executable content. This applies equally to text arriving through a live contract message and text arriving embedded in scenario data (Section R): scenario data must remain inert, and no contract or scenario argument should ever be capable of causing arbitrary script execution inside the exhibit.
P. Optional Telemetry
CONTRACT REQUIREMENT. Telemetry support is entirely optional; the contract defines how telemetry is represented if an exhibit exposes it, without requiring any exhibit to implement it (Contract §21).
AUTHORING RECOMMENDATION. Do not invent telemetry fields merely to look complete against the contract. A telemetry target should exist only when the exhibit actually has a meaningful structured value behind it — a real number, from a real subsystem, that changes in a way worth exposing. Manufacturing a telemetry.* target with no real backing state produces exactly the kind of describe-accuracy problem Section E warns against for ordinary targets. If the exhibit's current telemetry-like presentation is really just audio activity levels or scrolling text rather than a genuine structured numeric model, building real structured telemetry is a new feature to design deliberately, not a checkbox to tick during initial contract compliance.
Acquisition, mapping, unit conversion, smoothing, and staleness policy for telemetry sourced from something external to the exhibit are XZBT-NGN's responsibility, not the exhibit's (Contract §22, NGN Plan §22–23). The exhibit's job is to declare what it can meaningfully receive or report and to behave sensibly according to its own declared contract when values arrive; it does not need to know or care where a telemetry value ultimately originated.
Q. External Integrations
CONTRACT REQUIREMENT. The exhibit exposes what it can do; the contract defines how that is described, observed, and invoked; XZBT-NGN decides how to orchestrate and integrate it (Contract §30). NGN, not the exhibit, owns discovery, coordination, automation, external connectors, recording, and administration (Contract §3.2, NGN Plan §3, §40).
AUTHORING RECOMMENDATION. A standalone exhibit generally should not own MIDI, webhooks, external automation buses, game integrations, or other connector-specific logic. The architectural test to apply to any proposed piece of functionality, borrowed directly from the settled NGN/exhibit boundary: if it coordinates, automates, connects, authors, records, distributes, or externally controls the exhibit, it belongs primarily in XZBT-NGN. If it is required for the exhibit to remain a complete standalone experience, it belongs in the exhibit.
This is not an absolute prohibition — an integration that is truly intrinsic to what makes a specific exhibit a complete standalone artifact could still belong in the exhibit itself. But that should be a deliberate, justified exception, argued explicitly against the test above, not a default. When in doubt, expose a clean primitive on the exhibit side (a target that does the underlying thing) and let external integration logic live entirely in NGN, translating whatever external signal into a normal contract operation against that primitive. This is precisely the shape of the resolution used when MIDI-specific code was separated from the generic control-bus abstraction it had been bundled with: the generic bus mechanism is a legitimate exhibit-side primitive; the MIDI-specific binding and mapping logic is not.
R. Scenarios
CONTRACT REQUIREMENT. The XZBT Exhibit Contract does not require a general-purpose scenario engine in the standalone exhibit; a scenario is an orchestration concept, and XZBT-NGN executes scenarios by issuing ordinary contract operations over time (Contract §26). Scenario data must remain inert and must never be interpreted as arbitrary executable code.
AUTHORING RECOMMENDATION. General scenario authoring, recording, and playback belong to XZBT-NGN, not the exhibit. What the exhibit should provide is a set of clean, controllable primitives — well-designed targets — that a scenario can drive; it does not need to know a scenario is happening at all, and from the exhibit's point of view a scenario-driven operation is just a set or invoke arriving through its host transport like any other.
A separately packaged exhibit build, produced by NGN for self-running distribution, may later include a minimal runtime that drives the exhibit's own canonical target surface from embedded scenario data (NGN Plan §26–27). That compact runtime is still a consumer of the same contract surface the exhibit already exposes — it is not a reason to build a general scenario editor or player into every ordinary exhibit. Do not embed scenario authoring or playback tooling in the exhibit as a default; if a packaged build needs it, that tooling is added at packaging time by NGN's shared runtime core, not maintained as a permanent part of the standalone artifact.
S. Packaging and Offline Operation
AUTHORING RECOMMENDATION, following directly from Section B: a standalone exhibit intended to run as a single distributable artifact should remain fully self-contained wherever that is the intended distribution shape — no server required for normal standalone use, no external script or stylesheet dependency that would break when opened offline or from a local file. Any optional network-dependent enhancement (checking for updates, an optional cloud feature) should fail gracefully and silently fall back to fully local behavior rather than degrading the core experience or throwing a visible error.
Packaging should preserve contract behavior: whatever process turns exhibit source into a distributable artifact should not need special-case handling to keep the contract adapter, canonical mutation path, or event surface intact — if packaging strips or reorders code in a way that breaks contract compliance, that is a packaging defect, not an acceptable tradeoff.
Do not treat SciFi-XZBT's own specific offline/packaging policy (its particular single-file HTML packaging approach, or any Web3D-related constraint it happens to carry) as a universal requirement. A different exhibit might reasonably ship as a small set of files, or run inside a different host shell entirely. The universal principle is graceful, complete standalone operation; the specific packaging mechanics are exhibit-owned implementation choices.
T. Error Handling
CONTRACT REQUIREMENT. Implementations should use stable, machine-readable error codes rather than relying on message text (Contract §24). The recommended base codes are:
UNSUPPORTED_VERSION
INVALID_MESSAGE
INVALID_SESSION
UNKNOWN_TARGET
INVALID_VALUE
INVALID_ARGUMENTS
CAPABILITY_UNAVAILABLE
TARGET_READ_ONLY
TARGET_NOT_INVOKABLE
TARGET_NOT_SETTABLE
INTERNAL_ERROR
Human-readable error text is advisory only; hosts should branch on codes, never on message strings.
AUTHORING RECOMMENDATION.
- Reject invalid input cleanly and immediately, with a specific code, rather than accepting it and behaving unpredictably.
- Never apply a partial mutation — if any part of a validated transaction cannot be committed, commit none of it.
- Do not silently clamp an out-of-range value to the nearest valid one unless the target's own descriptor explicitly documents clamping as its defined behavior; silent clamping without a declared contract for it produces exactly the kind of state/host disagreement Section E warns about.
- Never report success when the requested state did not actually change as requested. A response of
ok: trueis a claim that the operation was accepted for execution (Contract §11) — it must correspond to something real actually happening, not to the request merely being well-formed.
U. Testing Strategy
AUTHORING RECOMMENDATION. A new exhibit's XZBT compliance should be exercised at several distinct levels, each catching a different class of defect:
- Synthetic/unit tests. Exercise the canonical mutation/invoke path and the contract adapter directly, without a browser or a real host — target validation, no-op detection, revision increment logic, event shape.
- Contract adapter tests. Verify
describe,state.get, capability reporting, and error codes conform to the wire-level contract shape, independent of any specific transport. - Real browser host tests. Attach an actual host (or a minimal test harness acting as one) over the real transport and verify handshake, discovery, state read/write, invoke, and event delivery end to end.
- Native UI convergence tests. Confirm that UI-originated changes and host-originated changes both flow through the same canonical path and produce equivalent, correctly-sourced events — this is where a lingering DOM-click proxy (Section H) or a UI/contract range mismatch (Section E) tends to surface.
- Standalone no-host tests. Load the exhibit with no host attached at all and confirm every feature works exactly as it would with one — this is the direct verification of Section B's governing principle.
- Packaging tests. Confirm the packaged/distributable artifact behaves identically to the development build with respect to contract compliance.
- Local-file/offline tests, where applicable. For an exhibit intended to run as a double-clicked local file or fully offline, confirm it actually does — including any browser security constraints on
file://access that a development environment's own tooling may not replicate.
A minimal conformance checklist for quick reference during development is provided in Appendix A.
V. Reference Exhibit Expectations
AUTHORING RECOMMENDATION, describing what a later reference-exhibit pass (not undertaken here) should aim to demonstrate. Reference exhibits such as a train exposition or an aviation exposition should be:
- small — deliberately minimal in scope, not a showcase of every contract feature at once;
- intentionally simple — easy to read end to end as a worked example;
- fully standalone, per Section B;
- visibly different in subject domain from any existing exhibit, to prove the contract generalizes rather than merely repeating one domain's shape;
- contract-correct against every applicable MUST in the specification;
- free of any dependency on science-fiction-specific vocabulary or assumptions;
- useful primarily as an example implementation for future exhibit authors to read.
The point of a reference exhibit is to prove genericity, not to demonstrate feature richness. A train exhibit exposing engine.throttle, brake.pressure, event.whistle, and telemetry.speed (the exact example given in Contract §27) makes the architectural point far better than a large, feature-complete build would.
W. Anti-Patterns
The following are recurring mistakes to avoid when building a new exhibit, several of them drawn directly from real defects found during SciFi-XZBT's contract verification:
- Using DOM element IDs, CSS selectors, or internal widget names as public contract target IDs.
- Implementing a persistent, toggle-shaped piece of state as a toggle-only contract action instead of an absolute setter.
- Maintaining a separate "shadow" adapter state that can drift from the exhibit's real internal state.
- Using synthetic DOM clicks or dispatched events as the canonical control-mutation mechanism.
- Trusting a host-supplied
sourcevalue instead of assigning source at the trusted receiving boundary. - Publishing a target in
describethat has no real, verified, callable backing action in any context. - Changing the registry, or incrementing
registryRevision, on every ordinary mode or context switch. - Embedding external integrations (MIDI, webhooks, third-party connectors) directly into the standalone exhibit by default.
- Making any part of ordinary standalone operation depend on XZBT-NGN being present.
- Putting arbitrary executable code, HTML, or expressions into scenario data or contract text arguments.
- Adding telemetry fields that have no real backing state, purely to look complete against the contract.
- Letting a native UI control accept or produce values outside the range its own public descriptor advertises.
- Silently reverting a committed host- or scenario-set value the next time a local control is touched.
X. Recommended New-Exhibit Build Order
AUTHORING RECOMMENDATION. A practical sequence for building a new XZBT-compatible exhibit from scratch:
- Build the exhibit's own standalone behavior first — visuals, audio, simulation, native UI — with no contract awareness at all.
- Define stable internal state and services (layers 1–2 of Section C) that the exhibit's own UI already uses.
- Define the canonical public target catalog (Section D) against that real internal model.
- Implement absolute setters and idempotent actions (Section G) backing each target.
- Route the native UI through the canonical services (Section H) so UI-originated and future host-originated changes share one path.
- Add the contract adapter: descriptors, target metadata, capability model (Sections E, M).
- Add discovery, state, and event surfaces:
describe,state.get,state.changed/action.executed/etc. (Sections F, K). - Add the transport layer (Section L), keeping it isolated from exhibit and adapter logic.
- Add capability reporting wired to real subsystem lifecycle, not static claims (Section M).
- Run the conformance tests in Section U, including the checklist in Appendix A.
- Test standalone and offline operation explicitly, with no host present at all.
- Only after all of the above is solid, integrate with a real XZBT-NGN instance.
Building in this order keeps the exhibit correct and complete on its own at every step, and treats contract compliance as a layer added on top of a working exhibit rather than a scaffold the exhibit is built inside of.
Y. Minimal Example Architecture
The following are short, conceptual, JavaScript-like pseudocode fragments illustrating the patterns above. They are not a working application and deliberately omit error handling detail already covered in Section T.
Target registry (Section D, E):
const TARGET_REGISTRY = {
'transport.playing': { kind: 'state', valueType: 'boolean', readable: true, writable: true, restorable: true, category: 'transport', requires: [] },
'mix.master': { kind: 'range', min: 0, max: 1, step: 0.01, readable: true, writable: true, restorable: true, category: 'mix', requires: ['audio'] },
'mode.selected': { kind: 'selection', options: [{ value: 'day', label: 'Day' }, { value: 'night', label: 'Night' }], readable: true, writable: true, restorable: true, category: 'mode', requires: [] },
'event.pulse': { kind: 'impulse', readable: false, writable: false, restorable: false, category: 'event', requires: [] },
};
Absolute setter (Section G):
function setMuted(on) {
if (exhibitState.muted === on) return { changed: false };
exhibitState.muted = on;
audioEngine.setMuted(on);
return { changed: true };
}
Canonical mutation transaction (Section F, H):
function applyMutation(targetId, value, source) {
const before = snapshotRelevantState(targetId);
const result = dispatchToSetter(targetId, value); // calls setMuted(), etc. directly
if (!result.changed) return { ok: true, revisionChanged: false };
const changedTargets = diffState(before, snapshotRelevantState(targetId));
stateRevision += 1;
for (const t of changedTargets) {
emitEvent('state.changed', { target: t.id, value: t.value, stateRevision, source });
}
return { ok: true, revisionChanged: true };
}
Impulse action (Section G, K):
function invokeAction(targetId, args, source) {
const descriptor = TARGET_REGISTRY[targetId];
if (!descriptor || descriptor.kind !== 'impulse') return { ok: false, code: 'TARGET_NOT_INVOKABLE' };
if (!capabilityReady(descriptor.requires)) return { ok: false, code: 'CAPABILITY_UNAVAILABLE' };
performRealAction(targetId, args); // no synthetic DOM dispatch
emitEvent('action.executed', { target: targetId, args, source });
return { ok: true };
}
State snapshot (Section F):
function getStateSnapshot() {
const values = {};
for (const [id, d] of Object.entries(TARGET_REGISTRY)) {
if (d.readable && d.kind !== 'impulse') values[id] = readCurrentValue(id);
}
return { stateRevision, values };
}
Event emission (Section K):
let sequence = 0;
function emitEvent(type, payload) {
sequence += 1;
hostTransport.send({ xzbt: '5.2', type, sessionId: currentSessionId, sequence, timestamp: Date.now(), ...payload });
}
Host transport adapter (Section C, L):
window.addEventListener('message', (event) => {
if (event.source !== window.parent || !isTrustedOrigin(event.origin)) return;
const msg = event.data;
if (!isValidEnvelope(msg)) return respondError(msg, 'INVALID_MESSAGE');
if (msg.type !== 'hello' && msg.sessionId !== currentSessionId) return respondError(msg, 'INVALID_SESSION');
switch (msg.type) {
case 'hello': return handleHello(msg);
case 'describe': return respondDescribe(msg);
case 'state.get': return respondState(msg);
case 'set': return respond(msg, applyMutation(msg.target, msg.value, 'host'));
case 'invoke': return respond(msg, invokeAction(msg.target, msg.args, 'host'));
}
});
Z. Final Author Checklist
Before calling a new exhibit XZBT-compatible, confirm:
- It runs completely, with full native behavior, with no host attached.
- Every public target ID is meaningful, lowercase-dotted, and free of DOM/widget-specific naming.
- Every descriptor reflects a real, verified backing action or state — nothing fake, nothing aspirational.
- Persistent setters are absolute and idempotent; setting the same value twice produces one state, zero extra revisions.
- All contract-visible mutation and invocation, from every input source, passes through one canonical path.
- No synthetic DOM dispatch is used as the mutation mechanism.
- Source is assigned only at the trusted receiving boundary, never trusted from the caller.
- One mutation transaction produces at most one
stateRevisionincrement, and no-ops produce none. - Native UI and public contract ranges represent one coherent operator-facing range.
- Capability states reflect real subsystem lifecycle, using only the states that are genuinely meaningful for that capability.
- The registry is fixed; context changes produce contextual errors, not registry churn.
- All text targets are handled as safe data, with declared length limits and no markup/script injection path.
- Telemetry, if present at all, is backed by real structured values — never fabricated for completeness.
- MIDI, webhooks, and other external integrations are not embedded in the exhibit by default.
- No general scenario authoring/recording/playback system has been built into the exhibit.
- Errors use stable machine-readable codes; no partial mutations; no silent clamping without a declared contract for it.
- The exhibit has been tested standalone, offline (where applicable), and through a real host, per Section U.
Appendix A: Minimal Conformance Checklist
- Handshake: exhibit responds to
hellowith a session, orUNSUPPORTED_VERSIONwhen incompatible. describereturns identity, contract version,registryRevision,stateRevision, capabilities, and targets.- At least one discoverable target of each kind actually in use (state/range/selection/impulse).
state.getreturns only readable persistent targets, matchingdescribe.seton a persistent target is idempotent; a no-op set does not changestateRevision.invokeon an impulse target executes a real action and emitsaction.executed.- One transaction, one revision: a multi-value change increments
stateRevisionexactly once. - Events carry monotonic
sequence, reset on new session;stateRevisiondoes not reset with session. sourceis accurate forui,hotkey, andhostorigins in real, physically-triggered tests.- Capability states reflect real initialization/readiness, not static placeholders.
- Unknown target, invalid value, invalid session, and invalid arguments each return the correct distinct error code.
postMessagetransport (if used) validates bothevent.originandevent.source.- The exhibit runs fully standalone with no host, network, or server present.
- The packaged/distributable build behaves identically to the development build for all of the above.
Appendix B: Recommended Project Structure
This is an illustrative layout, not a required one — see Section C for the underlying separation of concerns it expresses.
/exhibit-root
index.html # entry point; loads with no host required
/js
model.js # exhibit state (layer 1)
services.js # exhibit behavior / mutation logic (layer 2)
ui.js # native UI, calls canonical services (layer 3)
canonical-control.js # applyMutation / invokeAction chokepoint (layer 4)
contract-adapter.js # describe/state.get/events/capabilities (layer 5)
host-transport.js # postMessage or other binding (layer 6)
/css
style.css
/assets
...
A single-file packaged build may inline all of the above; the separation should still exist logically within the source even when the distributable artifact is one file.
Appendix C: Glossary
XZBT Exhibit Contract — the versioned specification defining the message envelope, target model, revisions, events, capabilities, and error codes an exhibit and a host share.
XZBT-compatible exhibit — any exhibit implementing the XZBT Exhibit Contract.
Standalone exhibit — an exhibit's normal, complete, independently usable form, with no host attached.
XZBT-NGN Exhibit Engine — the separate orchestration, integration, recording, authoring, and administration layer that connects to one or more exhibits through the contract; never required for standalone exhibit operation.
Canonical target — a publicly registered, dotted-ID addressable unit of exhibit state or action.
State — a target kind representing persistent value (boolean, string, or bounded scalar).
Range — a target kind representing persistent bounded numeric state, with min/max/step.
Selection — a target kind representing a persistent value chosen from a declared set of options.
Impulse — a target kind representing a non-persistent action or event; never part of a restorable snapshot.
Capability — a discoverable, stateful description of subsystem availability (unsupported/available/loading/ready/busy/error).
stateRevision — a monotonically increasing counter over committed persistent-state history, incremented at most once per mutation transaction.
registryRevision — a counter over the current set and metadata of discoverable targets; should not change for ordinary contextual availability shifts.
Sequence — a per-session, monotonically increasing counter over all emitted events, reset on new session.
Session — the negotiated context, established by hello, under which all other contract requests after the handshake must carry a valid sessionId.