generated from Labyricorn/labyricorn-project-template
220 lines
10 KiB
Markdown
220 lines
10 KiB
Markdown
> **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.
|