generated from Labyricorn/labyricorn-project-template
Post Step 4 Completion
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
> **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.
|
||||
Reference in New Issue
Block a user