generated from Labyricorn/labyricorn-project-template
Step 6.2 Complete — Museum Gallery reference exhibit and Contract 5.3 spec
This commit is contained in:
@@ -0,0 +1,755 @@
|
||||
# XZBT Multi-Surface Presentation Model — Step 6.1
|
||||
|
||||
## Status
|
||||
|
||||
**Revision 2 — for final approval.** Contract 5.3 has been approved by the
|
||||
owner as the intended version for multi-surface presentation (Section 14).
|
||||
The versioned `v5.3.md` contract file has **not** been minted and Phase 6.2
|
||||
has **not** begun; both remain gated on approval of this revised document.
|
||||
|
||||
This revision addresses five items raised against Revision 1:
|
||||
|
||||
1. defines a generic surface-attachment mechanism (new Section 7.2);
|
||||
2. restores the canonical target-ID grammar for surface IDs, with no carve-out
|
||||
(Section 5.1);
|
||||
3. tightens `primary` and backward-compatibility semantics, resolving the
|
||||
ambiguity previously left as Open Question 1 (Section 6);
|
||||
4. relaxes the direct-open standalone requirement so it no longer conflicts
|
||||
with the new attachment mechanism (Section 10);
|
||||
5. adds an explicit URL-resolution algorithm for the surface `url` field
|
||||
(new Section 6.3).
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
Step 5 closed the XZBT-NGN MVP around a single implicit assumption: one
|
||||
exhibit has one renderable visual surface, shown in the one iframe the host
|
||||
loads.
|
||||
|
||||
Step 6 removes that assumption. An XZBT-compatible exhibit may have more than
|
||||
one full-screen visual presentation — a main interface, an observation
|
||||
display, an information wall, a second dome view — that are all views onto
|
||||
**one logical exhibit instance and one authoritative state**, not independent
|
||||
exhibits.
|
||||
|
||||
This document defines:
|
||||
|
||||
- what a presentation surface is and is not;
|
||||
- how an exhibit advertises its surfaces;
|
||||
- the generic mechanism by which a surface document attaches to the one
|
||||
authoritative exhibit state, regardless of exhibit implementation;
|
||||
- how surfaces stay synchronized to that one state;
|
||||
- the lifecycle a surface goes through;
|
||||
- who owns what (exhibit / NGN / transport / display endpoint);
|
||||
- whether this requires a new contract version;
|
||||
- what remains explicitly out of scope (Step 7).
|
||||
|
||||
It does not define casting, remote displays, or any display-transport
|
||||
mechanism. Those are Step 7.
|
||||
|
||||
---
|
||||
|
||||
## 2. Definitions
|
||||
|
||||
**Logical exhibit instance.** One running exhibit session, with one
|
||||
`sessionId`, one `stateRevision` history, and one authoritative set of
|
||||
contract-visible persistent state values, as already defined by Contract 5.2
|
||||
§13–§14. Multi-surface support does not change this definition; it depends on
|
||||
it remaining singular.
|
||||
|
||||
**Presentation surface.** A renderable, full-screen(-capable) visual view of
|
||||
the logical exhibit instance, addressable by a stable identifier, that an
|
||||
exhibit advertises as independently viewable. A surface displays a coherent
|
||||
subset (or the whole) of the exhibit's presentation, driven by the same
|
||||
underlying state and the same contract session as every other surface of the
|
||||
same exhibit.
|
||||
|
||||
**Surface descriptor.** The machine-readable metadata an exhibit publishes
|
||||
describing one presentation surface: identifier, label, lifecycle-relevant
|
||||
metadata, and enough information for a generic host to open it without any
|
||||
exhibit-specific knowledge.
|
||||
|
||||
**Exhibit State Core.** The single in-exhibit authority for contract-visible
|
||||
state, mutation, and event fan-out. Every exhibit already has exactly one of
|
||||
these today, whether or not it is named as such — it is whatever internal
|
||||
object/module the existing contract adapter (e.g.
|
||||
`test-fixtures/reference-exhibits/shared/contract-core.js`) reads and writes.
|
||||
Multi-surface support does not introduce a new core; it requires that every
|
||||
surface, however many documents or windows are involved, attach to that one
|
||||
existing core rather than instantiate a second one (Section 7.2).
|
||||
|
||||
**Control UI.** The operator-facing interface NGN renders from `describe`
|
||||
(targets, state, events). It is not a presentation surface. It may run
|
||||
alongside any number of open presentation surfaces.
|
||||
|
||||
**Display endpoint (Step 7, referenced only for contrast).** Where a surface
|
||||
is physically or logically shown — a local browser window, a monitor, a
|
||||
casting receiver. Step 6 defines *what* can be shown; Step 7 defines *where*.
|
||||
|
||||
---
|
||||
|
||||
## 3. Surface vs. Target Distinction
|
||||
|
||||
Contract 5.2 already has a generic extensibility mechanism: targets
|
||||
(state/range/selection/impulse) discovered through `describe`. It is tempting
|
||||
to model a surface as a `state`-kind target (e.g. `surface.observation.open:
|
||||
boolean`). This proposal rejects that approach for surfaces themselves, for
|
||||
four reasons:
|
||||
|
||||
1. **A target represents a piece of exhibit state or an action on it.** A
|
||||
surface is neither — it is a *view*. Modeling "is this surface open" as
|
||||
exhibit state conflates NGN-side presentation bookkeeping with exhibit
|
||||
domain state, which Contract §2 explicitly prohibits ("The contract MUST
|
||||
NOT be expanded merely to move Exhibit Engine responsibilities into the
|
||||
exhibit").
|
||||
2. **Targets have no place to carry surface-specific metadata** a generic
|
||||
host needs to *render a window* rather than *render a control* — no field
|
||||
in the base or kind-specific descriptors (§9) accommodates a document URL,
|
||||
an aspect ratio hint, or a role.
|
||||
3. **A target-based encoding would force every exhibit author to hand-roll an
|
||||
ad hoc convention** (e.g. a `surface.*` naming prefix) for something that
|
||||
should be a first-class, uniformly validated concept — exactly the kind of
|
||||
silent protocol behavior the Step 6 brief prohibits inventing.
|
||||
4. **Surfaces are discovered once per registry revision, not read/written
|
||||
like state.** Their lifecycle (open/visible/hidden/closed) is host-side
|
||||
presentation bookkeeping, not a `set`/`invoke` operation on exhibit state.
|
||||
|
||||
Surfaces are therefore modeled as a **new, parallel discovery construct**:
|
||||
a `surfaces` array returned by `describe`, structurally independent of
|
||||
`targets` and `capabilities`, but discovered through the same message and
|
||||
governed by the same `registryRevision`.
|
||||
|
||||
**What is NOT a presentation surface:**
|
||||
|
||||
- the control/admin UI NGN renders from targets — that is an NGN-owned
|
||||
concept and never appears in `surfaces`;
|
||||
- a capability (§17) — capabilities describe whether a subsystem is usable,
|
||||
not whether a view exists;
|
||||
- a target of any kind — including a hypothetical `kind: "surface"` target,
|
||||
which this proposal does not introduce;
|
||||
- a display endpoint — monitors, browser windows, casting receivers are
|
||||
Step 7 and are never named inside a surface descriptor;
|
||||
- a scenario, recording, or packaging artifact.
|
||||
|
||||
---
|
||||
|
||||
## 4. Ownership Model
|
||||
|
||||
| Concern | Owner |
|
||||
| --- | --- |
|
||||
| What surfaces exist, their identifiers, labels, and descriptors | **Exhibit** |
|
||||
| The actual rendering of each surface (HTML/CSS/JS/canvas/audio) | **Exhibit** |
|
||||
| One authoritative state, `stateRevision`, and event stream (the Exhibit State Core) | **Exhibit**, shared by all surfaces of that exhibit |
|
||||
| How surface documents attach to the Exhibit State Core internally | **Exhibit** (Section 7.2 defines the generic pattern; the specific transport choice is an exhibit implementation decision) |
|
||||
| Discovering surfaces from `describe` and reacting to `registry.changed` | **NGN** |
|
||||
| Deciding which surfaces are currently open and where | **NGN** (Step 6: local rendering only) |
|
||||
| Routing a native in-surface interaction back into one canonical mutation path | **Exhibit internally**, using the same contract-visible state mechanism regardless of which surface originated the interaction |
|
||||
| Transport connecting NGN's contract session to the exhibit | **Exhibit + transport binding**, unchanged from Contract §4 |
|
||||
| Where a surface is physically displayed (monitor, window, cast target) | **Step 7 — out of scope here** |
|
||||
|
||||
The exhibit remains the single owner of truth. NGN never maintains a second
|
||||
copy of exhibit state per surface; it discovers surfaces, opens/closes their
|
||||
presentation, and continues to observe the one session's state and events
|
||||
exactly as it does today. This is the direct application of Contract §2: the
|
||||
exhibit exposes capabilities and state, NGN decides how to coordinate
|
||||
presentation of them.
|
||||
|
||||
---
|
||||
|
||||
## 5. Surface Descriptor Proposal
|
||||
|
||||
### 5.1 Minimum required fields
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "surface.control",
|
||||
"label": "Control Room",
|
||||
"kind": "surface",
|
||||
"primary": true,
|
||||
"url": "control.html"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Requirement | Notes |
|
||||
| --- | --- | --- |
|
||||
| `id` | REQUIRED string | **MUST conform exactly to the canonical target-ID grammar in Contract §8.1** — lowercase ASCII letters, digits, hyphens, and dots; begins with a lowercase letter; one or more dot-separated segments; no whitespace; no empty segments; recommended grammar `[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+`. No surface-specific carve-out exists: a bare single-segment ID (e.g. `"observation"`) is **not** valid, exactly as it would not be valid for a target ID. Authors are expected to namespace surface IDs meaningfully (e.g. `surface.control`, `surface.observation`, or an exhibit-specific namespace such as `gallery.artifact-wall`), the same discretion Contract §8 already gives target authors. Stability rule of §8.2 applies identically: once published, a surface ID is part of the exhibit's compatibility surface and SHOULD NOT be renamed casually. |
|
||||
| `label` | REQUIRED string | Human-readable, for generic host display. |
|
||||
| `kind` | REQUIRED, constant `"surface"` | Distinguishes this array's entries from targets/capabilities defensively, even though they already live in a separate array; makes the descriptor self-describing if ever handled outside its parent array. |
|
||||
| `primary` | REQUIRED boolean | See Section 6.2 for the exact structural invariant and its enforcement. |
|
||||
| `url` | REQUIRED string | See Section 6.3 for the full resolution algorithm. Summary: a same-origin, relative reference (path, and/or query/hash) resolved against the exhibit's own base URL — never an absolute or cross-origin URL. |
|
||||
|
||||
### 5.2 Optional fields (useful, not yet mandatory)
|
||||
|
||||
| Field | Purpose |
|
||||
| --- | --- |
|
||||
| `description` | Longer human-readable text for admin display. |
|
||||
| `role` | Free-text or small controlled vocabulary hint (e.g. `"control"`, `"ambient"`, `"information"`) for host UI grouping. Advisory only; no contract behavior depends on it in 5.3. |
|
||||
| `aspectRatio` | e.g. `"16:9"`. Advisory hint for window sizing. |
|
||||
| `category` | Mirrors target `category` (§9) for consistent grouping conventions across the contract. |
|
||||
| `requires` | Array of capability IDs (§17), same semantics as target `requires`: the surface remains discoverable but SHOULD be presented as degraded/unavailable if a required capability is not `ready`. |
|
||||
|
||||
### 5.3 Explicitly excluded fields
|
||||
|
||||
No field in the surface descriptor may name a display transport, a casting
|
||||
protocol, a network endpoint, a device class, or a rendering target other than
|
||||
"a document this host can load." This is the Step 6/Step 7 boundary enforced
|
||||
at the schema level, not just by convention.
|
||||
|
||||
---
|
||||
|
||||
## 6. Discovery Proposal
|
||||
|
||||
Surfaces are discovered exactly where targets and capabilities are discovered:
|
||||
in the `describe.result` response, as a new top-level array:
|
||||
|
||||
```json
|
||||
{
|
||||
"xzbt": "5.3",
|
||||
"type": "describe.result",
|
||||
"requestId": "req-010",
|
||||
"sessionId": "sess-7f2a",
|
||||
"exhibit": { "product": "museum-gallery", "version": "0.1.0", "build": "dev" },
|
||||
"contract": { "major": 5, "minor": 3 },
|
||||
"registryRevision": 1,
|
||||
"stateRevision": 4,
|
||||
"capabilities": [],
|
||||
"targets": [],
|
||||
"surfaces": [
|
||||
{ "id": "surface.control", "label": "Control Room", "kind": "surface", "primary": true, "url": "control.html" },
|
||||
{ "id": "surface.artifact", "label": "Artifact Display", "kind": "surface", "primary": false, "url": "artifact.html" },
|
||||
{ "id": "surface.info-wall", "label": "Information Wall", "kind": "surface", "primary": false, "url": "info-wall.html" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `surfaces` is governed by `registryRevision`, identically to `targets`. A
|
||||
`registry.changed` event MUST cause NGN to re-run `describe`, which
|
||||
re-reads the current surface set along with the current target set. There
|
||||
is no separate "surface revision" counter — one registry, one revision,
|
||||
covering both targets and surfaces (Section 9).
|
||||
|
||||
### 6.1 Malformed individual entries
|
||||
|
||||
A malformed *individual* surface entry (missing `id`/`label`/`url`, invalid
|
||||
`id` grammar per §8.1, non-relative or unresolvable `url` per Section 6.3) is
|
||||
handled the same way a malformed target entry is already handled: the host
|
||||
SHOULD skip only that entry and log a diagnostic, rather than discarding the
|
||||
entire `surfaces` array. This mirrors the existing pattern in
|
||||
`src/validation.js` and must be proven, not merely asserted, in Phase 6.3.
|
||||
|
||||
### 6.2 `primary` — structural invariant and backward compatibility
|
||||
|
||||
This is tightened relative to the prior revision to remove ambiguity.
|
||||
|
||||
**Three, and only three, recognized forms of `surfaces`:**
|
||||
|
||||
1. **Field absent.** The exhibit does not advertise multi-surface support at
|
||||
all — an ordinary Contract 5.2 exhibit, or a 5.3 exhibit that has not
|
||||
adopted the feature. A 5.3-aware host MUST behave exactly as NGN does
|
||||
today: treat the exhibit's currently-loaded document/frame as one
|
||||
implicit primary surface, with no surface picker UI, no `surfaces`
|
||||
validation, and no behavior change from Step 5. This is the sole
|
||||
legacy/compatibility path and needs no special-casing beyond "the field
|
||||
isn't there."
|
||||
2. **Field present and empty (`[]`).** Treated **identically** to the field
|
||||
being absent (form 1). This removes the ambiguity left open in the prior
|
||||
revision: an exhibit MAY emit `surfaces: []` (e.g. because it computes the
|
||||
field programmatically) without that being treated as an error or as
|
||||
"zero surfaces available." A host MUST NOT distinguish `[]` from absence.
|
||||
3. **Field present and non-empty.** The array MUST contain **exactly one**
|
||||
entry with `primary: true`. This is a *structural* invariant over the
|
||||
whole array, not a per-entry validation concern (unlike the field-level
|
||||
checks in Section 6.1). If the array contains zero `primary: true`
|
||||
entries, or more than one, the **entire `surfaces` array is rejected as
|
||||
malformed** — not just the offending entries — and the host falls back to
|
||||
form 1 behavior (implicit single primary surface = current frame), logging
|
||||
one clear diagnostic identifying the exhibit and the violation. This
|
||||
fallback exists specifically so a broken multi-surface descriptor
|
||||
degrades to "acts like Step 5" rather than to an undefined or partially
|
||||
rendered surface list.
|
||||
|
||||
**Why this matters:** `primary` is what lets a 5.3-aware host that has *not*
|
||||
been updated with any surface-picker UI still do the right thing by default
|
||||
(open the primary surface, ignore the rest) — and it is what tells a fully
|
||||
updated host which surface to treat as "the" exhibit view when no operator
|
||||
choice has been made yet. Exactly one candidate for that role must exist
|
||||
whenever `surfaces` claims to describe more than nothing.
|
||||
|
||||
### 6.3 URL resolution
|
||||
|
||||
`url` MUST be one of:
|
||||
|
||||
- a path relative to the exhibit's own base document location — the same
|
||||
base URL NGN already resolved to load the exhibit's primary document in
|
||||
the first place (i.e., the same resolution basis `src/connection.js` uses
|
||||
today for the initial exhibit URL, not NGN's own admin-page location, and
|
||||
not the requesting surface's future location);
|
||||
- such a relative path with an appended query string and/or fragment
|
||||
(`index.html?view=observation`, `control.html#panel-2`);
|
||||
- a bare query string and/or fragment with no path segment
|
||||
(`#observation`, `?surface=observation`), for a single-page exhibit where
|
||||
surfaces are views within one already-served document rather than separate
|
||||
files. In this form, resolution yields the exhibit's own base document URL
|
||||
with that query/fragment applied.
|
||||
|
||||
**Resolution algorithm:**
|
||||
|
||||
1. Start from the exhibit's currently-established base URL — the same
|
||||
absolute URL already used to load the exhibit and already validated by
|
||||
`src/connection.js`'s same-origin/path-containment checks.
|
||||
2. Resolve `url` against that base using standard relative-URL resolution.
|
||||
3. The resolved absolute URL MUST remain same-origin with the base (Contract
|
||||
§25's same-origin requirement extended to surfaces).
|
||||
4. The resolved path component MUST pass the same path-containment check
|
||||
`server/serve.js` already applies to the primary exhibit path (confined to
|
||||
`public/`, `src/`, or `test-fixtures/`, no `..` escape).
|
||||
5. `url` values that are absolute (`https://…`), protocol-relative
|
||||
(`//…`), or resolve cross-origin or outside the served roots MUST be
|
||||
rejected at step 3 or 4 as a malformed individual entry (Section 6.1) —
|
||||
they do not invalidate the rest of the array.
|
||||
|
||||
This makes explicit what was previously only stated as a prohibition:
|
||||
surface URLs are resolved on exactly the same basis, and validated by
|
||||
exactly the same containment logic, as the primary exhibit document is
|
||||
today. No new resolution concept is introduced.
|
||||
|
||||
---
|
||||
|
||||
## 7. State Synchronization Model
|
||||
|
||||
This is the section the whole feature stands or falls on, per the Step 6
|
||||
brief's critical architectural rule.
|
||||
|
||||
### 7.1 One session, one state, many views
|
||||
|
||||
A surface does **not** open its own contract session. All surfaces of one
|
||||
exhibit instance share the exhibit's single `sessionId`, single
|
||||
`stateRevision` history, and single event `sequence` stream, exactly as
|
||||
defined in Contract §5, §14, and §16 today. Multi-surface support adds zero
|
||||
new session or state-authority concepts — it only adds more places that
|
||||
*read* the one existing truth and more places a native interaction can
|
||||
*originate from*.
|
||||
|
||||
### 7.2 The generic surface-attachment mechanism
|
||||
|
||||
Revision 1 left this as an unresolved implementation choice. This revision
|
||||
defines a concrete, generic pattern every multi-surface exhibit is expected
|
||||
to implement, independent of how many documents or windows are involved.
|
||||
|
||||
**The pattern has two required properties, one required sequence, and two
|
||||
permitted transport shapes.**
|
||||
|
||||
**Required properties:**
|
||||
|
||||
- Exactly one Exhibit State Core exists per exhibit instance (Section 2).
|
||||
Every surface attaches to that one Core. No surface ever instantiates a
|
||||
second Core, a cached copy of state, or an independent computation of
|
||||
state.
|
||||
- Attachment is a runtime relationship between a surface document and the
|
||||
Core, entirely internal to the exhibit. It is not part of the XZBT Exhibit
|
||||
Contract and does not appear in Contract 5.3 — the contract only needs to
|
||||
know that surfaces exist and how NGN opens one (Section 6). This mirrors
|
||||
Contract §4's transport independence: the contract defines semantics, not
|
||||
one mandatory internal wiring.
|
||||
|
||||
**Required attachment sequence** (identical regardless of transport shape
|
||||
below):
|
||||
|
||||
1. The surface document loads and requests attachment from the Core.
|
||||
2. The Core responds with the current authoritative state snapshot — sourced
|
||||
from the same internal read path the contract adapter itself already uses
|
||||
to answer `state.get`, not a second computation — and the current
|
||||
`stateRevision`.
|
||||
3. The Core registers the surface as a subscriber for ongoing
|
||||
`state.changed` / `selection.changed` / relevant event notifications.
|
||||
4. Any native interaction originating in the surface calls the Core's one
|
||||
canonical mutation entry point (Section 7.4) — the same entry point a
|
||||
contract `set`/`invoke` from NGN already uses.
|
||||
5. On detach (surface closed), the Core drops the subscription. No state is
|
||||
held by the surface, so nothing needs to be reconciled or discarded.
|
||||
|
||||
**Two permitted transport shapes for attachment** (an exhibit MUST implement
|
||||
at least one; which one is an exhibit implementation decision, not a
|
||||
contract requirement):
|
||||
|
||||
- **In-process attachment.** The surface is a view rendered within the same
|
||||
JavaScript runtime/document that owns the Core — e.g. a single-page
|
||||
exhibit that swaps visible panels, or a document that embeds other
|
||||
surfaces as same-document components. Attachment is a direct in-memory
|
||||
subscription; no cross-document transport is involved.
|
||||
- **Cross-document attachment.** The surface runs in a separate browsing
|
||||
context (its own window/tab, its own `url` per Section 6.3) and attaches
|
||||
to the Core over a same-origin, exhibit-internal channel — e.g.
|
||||
`BroadcastChannel`, a `SharedWorker`, or same-origin `postMessage` to
|
||||
whichever document hosts the Core (typically the primary surface's
|
||||
document). This channel is entirely separate from, and must not be
|
||||
confused with, the NGN↔exhibit contract transport (Contract §4); it
|
||||
carries no contract envelope and is never observed by NGN.
|
||||
|
||||
The Museum Gallery reference exhibit (Phase 6.2) is required to implement and
|
||||
prove one concrete instance of this pattern — expected to be cross-document
|
||||
attachment, since its surfaces are separate documents/windows opened by NGN
|
||||
(Section 8) — and to document it as the reusable template Phase 6.7 adapts
|
||||
for SciFi-XZBT's Observation surface. Phase 6.2 MAY also demonstrate
|
||||
in-process attachment if useful for the control surface specifically, but at
|
||||
least one cross-document instance must exist to prove the multi-window case
|
||||
the brief requires.
|
||||
|
||||
### 7.3 How a surface receives ongoing state changes
|
||||
|
||||
Covered by the attachment sequence in 7.2 (steps 2–3): a surface never
|
||||
polls; it subscribes at attachment time and receives the same
|
||||
`state.changed`/`selection.changed` notifications the Core already emits
|
||||
internally, over whichever transport shape (in-process or cross-document)
|
||||
its attachment uses.
|
||||
|
||||
### 7.4 Native interaction routing (the canonical mutation path)
|
||||
|
||||
A user acting on any surface — clicking a control on the "Artifact Display,"
|
||||
adjusting a slider on the "Information Wall" — MUST result in exactly the
|
||||
same internal mutation call the exhibit's existing single-surface UI would
|
||||
make, which is the same call the contract adapter uses to satisfy an
|
||||
NGN-issued `set`/`invoke`. There must be exactly **one** function/method per
|
||||
mutable target that changes exhibit state, on the Exhibit State Core itself;
|
||||
every UI element on every surface, and every contract `set`/`invoke` from
|
||||
NGN, calls into that same function. This is not new architecture — it is the
|
||||
existing single-authoritative-state requirement from Contract §14, restated
|
||||
for the case where more than one DOM document can trigger it.
|
||||
|
||||
Concretely for the reference exhibit: the same `contract-adapter.js` /
|
||||
`exhibit.js` core already used by the Step 5 reference fixtures (see
|
||||
`test-fixtures/reference-exhibits/shared/contract-core.js`) is the pattern to
|
||||
extend into a proper Exhibit State Core, not replace. A surface's document is
|
||||
a thin presentation layer attached to that core; it must not contain a
|
||||
second, parallel copy of state or mutation logic.
|
||||
|
||||
### 7.5 What "no duplicate authoritative state" rules out
|
||||
|
||||
Explicitly disallowed by this model, matching the brief's stated
|
||||
anti-pattern:
|
||||
|
||||
- launching a separate exhibit instance (a second `hello`/session with its
|
||||
own `stateRevision` history) per surface;
|
||||
- a surface document that keeps its own local copy of state and only
|
||||
periodically reconciles it, rather than attaching per Section 7.2;
|
||||
- a surface whose native interactions call an exhibit-internal function that
|
||||
bypasses the Core's one canonical mutation entry point;
|
||||
- two surfaces attaching to two different Core instances of the same
|
||||
exhibit, however attachment is transported.
|
||||
|
||||
---
|
||||
|
||||
## 8. Lifecycle
|
||||
|
||||
A surface descriptor's existence in `surfaces` only establishes that the
|
||||
surface is **available**. Whether it is currently shown is host-side,
|
||||
per-surface, per-session bookkeeping — not exhibit state, and not persisted
|
||||
across a full exhibit reload.
|
||||
|
||||
| State | Meaning | Who tracks it |
|
||||
| --- | --- | --- |
|
||||
| `available` | The surface is discoverable via `describe` but no host action has been taken on it. | Derived from `surfaces` array; not separately tracked. |
|
||||
| `opened` | NGN has loaded the surface's document (Step 6: in a local browser window/view), which then performs the attachment sequence in Section 7.2. | NGN, per session. |
|
||||
| `visible` | The opened surface's window/view currently has visual presence (not minimized/backgrounded). Best-effort; browsers do not always expose this reliably. | NGN, best-effort. |
|
||||
| `hidden` | Opened but not currently visible (backgrounded, minimized). | NGN, best-effort. |
|
||||
| `closed` | NGN has torn down the surface's window/view; the surface's document detaches from the Core (Section 7.2, step 5). The surface remains `available` for reopening. | NGN, per session. |
|
||||
| `unavailable` | A `requires` capability (Section 5.2) is not `ready`. The surface stays listed but SHOULD be presented as non-operable. | NGN, derived from `capabilities`. |
|
||||
|
||||
Lifecycle transitions are entirely local bookkeeping on the NGN side about
|
||||
*presentation*, not contract-visible exhibit state. Opening, closing, or
|
||||
reopening a surface MUST NOT itself cause a `set`/`invoke`, MUST NOT change
|
||||
`stateRevision`, and MUST NOT be recorded as an `action.executed` event. It
|
||||
may optionally be logged as an NGN-local diagnostic entry, exactly as
|
||||
`src/host.js` already logs connection lifecycle events today.
|
||||
|
||||
**Reopen behavior.** When a previously-closed surface is opened again, its
|
||||
document performs the same attachment sequence as a first open (Section
|
||||
7.2). Because state lives in the Core, not in the surface, a reopened
|
||||
surface reflects current state automatically — there is nothing to "restore"
|
||||
at the surface level.
|
||||
|
||||
---
|
||||
|
||||
## 9. Session/Revision Implications
|
||||
|
||||
- `registryRevision` covers `surfaces` in addition to `targets`/`capabilities`.
|
||||
No separate surface-registry counter is introduced. A `registry.changed`
|
||||
event already means "re-run `describe`"; that now also refreshes the known
|
||||
surface set.
|
||||
- `stateRevision` and event `sequence` are entirely unaffected by
|
||||
multi-surface support — they remain properties of the one session, per
|
||||
Contract §14 and §16, regardless of how many surfaces are open or how they
|
||||
attach to the Core.
|
||||
- **Reconnect.** On session loss and reconnect (existing `src/connection.js`
|
||||
behavior), NGN re-runs the full negotiation and rediscovery, which
|
||||
naturally re-obtains the current `surfaces` array. Any currently-open
|
||||
surface windows are host-side presentation state and are NOT automatically
|
||||
torn down by a reconnect; whether they should be is a Phase 6.5/6.6
|
||||
implementation question (recommend: leave open, let the surface's own
|
||||
document's attachment to the Core recover independently, rather than NGN
|
||||
force-closing surface windows on transient session loss).
|
||||
- **Are surface definitions static per registry revision?** Yes. Within one
|
||||
`registryRevision`, the `surfaces` array is a stable snapshot, exactly like
|
||||
`targets`. An exhibit that wants to change its surface set (add/remove a
|
||||
surface) MUST bump `registryRevision` and emit `registry.changed`, per the
|
||||
existing §23 mechanism — no new mechanism needed.
|
||||
|
||||
---
|
||||
|
||||
## 10. Standalone Behavior
|
||||
|
||||
A standalone exhibit — opened directly in a browser tab, with no NGN attached
|
||||
— MUST continue to work with zero dependency on this model, per Contract §2's
|
||||
governing rule. This revision relaxes the prior draft's requirement that
|
||||
*every* surface be independently openable without any other document present,
|
||||
because that conflicted with the cross-document attachment shape defined in
|
||||
Section 7.2 (a surface attaching via `postMessage`/`BroadcastChannel` to
|
||||
whichever document hosts the Core may genuinely need that document to already
|
||||
be open).
|
||||
|
||||
The precise, tightened requirement:
|
||||
|
||||
- **The primary surface** (Section 6.2) MUST remain fully, unconditionally
|
||||
standalone-capable: a person navigating directly to the exhibit's normal
|
||||
entry point (its existing `index.html`) gets a complete, independently
|
||||
usable experience with zero dependency on NGN, on any other surface being
|
||||
open, or on any attachment sequence having occurred. This is the exhibit's
|
||||
existing standalone guarantee, entirely unchanged by Step 6. Nothing about
|
||||
standalone use of the primary surface requires a person to know surfaces
|
||||
exist.
|
||||
- **Non-primary surfaces** SHOULD remain directly openable without NGN
|
||||
whenever the exhibit's chosen attachment mechanism allows it:
|
||||
- an exhibit using **in-process attachment**, or a **cross-document**
|
||||
mechanism where the surface can bootstrap its own Core connection
|
||||
on demand (e.g. attaching to a `SharedWorker` that starts itself), can
|
||||
and should support a non-primary surface being opened first, with no
|
||||
other document present — matching how SciFi-XZBT's existing Observation
|
||||
view already behaves today;
|
||||
- an exhibit using **cross-document attachment that depends on the primary
|
||||
document already being open** (e.g. plain `postMessage` to a specific
|
||||
existing window) MAY require the primary surface to be open first for a
|
||||
non-primary surface to attach successfully. This is an accepted
|
||||
consequence of an exhibit's internal transport choice (Section 7.2), not
|
||||
a contract violation, and MUST NOT be worked around by having the
|
||||
non-primary surface silently start a second Core.
|
||||
- In either case, this is a same-origin, exhibit-internal dependency
|
||||
between the exhibit's own documents — never a dependency on NGN. A
|
||||
non-primary surface opened without NGN and without its dependency met
|
||||
SHOULD degrade to a clear "waiting for exhibit" state rather than
|
||||
silently diverging into independent state.
|
||||
- Nothing about surface discovery itself requires network access, a host, or
|
||||
any NGN-specific code inside the exhibit. `surfaces` is just more
|
||||
`describe` metadata; an exhibit run standalone never calls `describe` on
|
||||
itself.
|
||||
|
||||
---
|
||||
|
||||
## 11. Host (NGN) Behavior
|
||||
|
||||
- On `describe`, NGN parses `surfaces` alongside `targets`/`capabilities`
|
||||
using the validation discipline in Sections 6.1–6.2: tolerate individually
|
||||
malformed entries, but reject the whole array (falling back to implicit
|
||||
single-primary behavior) on a `primary` structural violation.
|
||||
- NGN's admin/control UI (`src/ui.js`) gains a generic, descriptor-driven
|
||||
surface list — rendered from `label`/`role`/`primary`, never from any
|
||||
hard-coded surface name. This is the direct analog of the existing generic
|
||||
target-rendering rule in the NGN Implementation Plan §8; no exhibit
|
||||
vocabulary may leak into `src/`.
|
||||
- Opening a surface (Phase 6.4) means NGN loads that surface's resolved `url`
|
||||
(Section 6.3) into a new local browser window/view, same-origin, using the
|
||||
same hosting mechanism `server/serve.js` already provides for the primary
|
||||
exhibit frame today.
|
||||
- NGN's control UI and any number of open surface windows coexist; closing
|
||||
or reloading a surface window never tears down the underlying exhibit
|
||||
session, and disconnecting the exhibit session (existing Disconnect
|
||||
behavior) SHOULD close any surface windows NGN opened, since they have
|
||||
nothing left to observe.
|
||||
- A single-surface exhibit (ordinary case, including every Step 5 reference
|
||||
fixture, and every exhibit using discovery form 1 or 2 from Section 6.2)
|
||||
continues to work with **zero NGN behavior change**: one implicit or
|
||||
declared primary surface, opened automatically into the one frame NGN
|
||||
already loads today, with no surface picker UI shown when there is nothing
|
||||
to pick between.
|
||||
|
||||
---
|
||||
|
||||
## 12. Non-Goals (Explicitly Out of Scope for Step 6)
|
||||
|
||||
Restating the brief's guardrails as binding scope boundaries for this
|
||||
document and everything built from it:
|
||||
|
||||
- Casting, Chromecast, Google TV, Android clients, remote/network display
|
||||
endpoints — all Step 7.
|
||||
- Any encoding of a physical or network display target inside a surface
|
||||
descriptor.
|
||||
- MIDI, MCP, webhooks, scenarios, recording, telemetry acquisition,
|
||||
multi-exhibit orchestration, databases, cloud services, user accounts.
|
||||
- Any redesign of Contract 5.2 behavior unrelated to surfaces.
|
||||
- Any surface concept that requires per-surface authentication/authorization
|
||||
beyond the existing same-origin session trust boundary (§25) — internet-
|
||||
facing auth remains NGN's problem generally, unchanged by this feature.
|
||||
- Standardizing the cross-document attachment transport (`BroadcastChannel`
|
||||
vs. `SharedWorker` vs. `postMessage`) as a contract-level choice — Section
|
||||
7.2 deliberately leaves this to the exhibit.
|
||||
|
||||
---
|
||||
|
||||
## 13. Security / Origin Considerations
|
||||
|
||||
- Every surface `url` resolves same-origin per the algorithm in Section 6.3,
|
||||
a direct extension of Contract §25's same-origin `postMessage` validation
|
||||
requirement (`event.origin` and `event.source` checks) to the new case of
|
||||
multiple documents. A surface document is exactly as trusted, and exactly
|
||||
as constrained, as the exhibit's primary document already is.
|
||||
- The exhibit-internal attachment channel (Section 7.2) — `BroadcastChannel`,
|
||||
`SharedWorker`, or same-origin `postMessage` — inherits the same
|
||||
same-origin trust boundary. It carries no contract envelope and MUST NOT be
|
||||
reachable from any other origin; this is ordinary same-origin browser
|
||||
isolation, not a new security mechanism.
|
||||
- No surface descriptor field may contain executable markup, a remote origin,
|
||||
or a URL scheme other than a relative path/fragment into the served
|
||||
exhibit. This mirrors the existing safe-text-injection rule (§20) applied
|
||||
to the new descriptor type.
|
||||
- NGN's server (`server/serve.js`) already restricts served paths to
|
||||
`public/`, `src/`, and `test-fixtures/` and denies path escapes; surface
|
||||
`url` values are subject to the same path-containment validation (Section
|
||||
6.3, step 4) before the host will load them — a malformed or escaping
|
||||
`url` is a rejected individual entry (Section 6.1), not a followed one.
|
||||
- Multiple surface documents observing one session is a same-origin,
|
||||
same-trust-boundary fan-out, not a new external trust relationship. No new
|
||||
authentication concept is introduced in Step 6.
|
||||
|
||||
---
|
||||
|
||||
## 14. Contract-Version Decision — APPROVED, NOT YET MINTED
|
||||
|
||||
### 14.1 Decision
|
||||
|
||||
**The owner has approved Contract 5.3 as the intended version for
|
||||
multi-surface presentation.** This is no longer a pending recommendation; it
|
||||
is the settled architectural direction this document and Phase 6.2 onward
|
||||
are written against.
|
||||
|
||||
**What remains explicitly deferred, per this instruction, until separately
|
||||
authorized:**
|
||||
|
||||
- minting the actual versioned file
|
||||
(`docs/contract/XZBT-Exhibit-Contract-Specification-v5.3.md`);
|
||||
- any change to `contract.major`/`contract.minor` reported by real
|
||||
`describe.result` responses;
|
||||
- any edit to the existing, currently-normative
|
||||
`XZBT-Exhibit-Contract-Specification-v5.2.md` document.
|
||||
|
||||
This document (6.1) continues to illustrate `describe.result` examples as
|
||||
`"xzbt": "5.3"` / `contract: {major: 5, minor: 3}` (Section 6) purely as the
|
||||
**target shape** the eventual 5.3 document will formalize — this is
|
||||
illustrative design content, not a claim that the file or the reported
|
||||
version exists yet.
|
||||
|
||||
### 14.2 Why 5.3 (retained from Revision 1, unchanged)
|
||||
|
||||
`describe.result` gains a new top-level array, `surfaces`. This is additive
|
||||
in the sense that an unaware consumer can ignore an unknown field — but
|
||||
Contract §28.2 is explicit: *"Backward-compatible additions may increase the
|
||||
minor version."* A new, normatively defined top-level response field with its
|
||||
own validation rules (Section 6), its own descriptor grammar (Section 5), and
|
||||
its own required/optional field contract is exactly the category of change
|
||||
§28.2 describes, not a same-version clarification. Step 5's `arguments` field
|
||||
addition (Contract §9.5) was explicitly scoped as *"clarifies Contract 5.2;
|
||||
it does not introduce Contract 5.3"* — because it formalized behavior that
|
||||
was already implicitly required (impulse arguments had to be validated
|
||||
somehow) without adding a new discoverable construct. Surfaces are different:
|
||||
nothing in 5.2 implies surfaces exist, and no 5.2-conformant exhibit or host
|
||||
is required to know what `surfaces` means.
|
||||
|
||||
Contract major stays 5 — this is additive, not breaking. An exhibit that
|
||||
only implements 5.2 behavior remains fully conformant; it simply never
|
||||
emits `surfaces` (Section 6.2, form 1), and a 5.3-aware host tolerates that
|
||||
identically to an exhibit that emits `surfaces: []` (form 2) or a single
|
||||
`primary` entry (form 3). A 5.3-aware host MUST remain fully capable of
|
||||
driving a 5.2-only exhibit exactly as it does today.
|
||||
|
||||
### 14.3 What still requires owner sign-off before the file is minted
|
||||
|
||||
- Timing: whether to mint the versioned file once this 6.1 design is
|
||||
approved, or defer the formal document bump until the surface model has
|
||||
additionally been proven out end-to-end in the reference exhibit (Phases
|
||||
6.2–6.6).
|
||||
- Any wording changes to the *existing* 5.2 document (this proposal
|
||||
recommends none — 5.3 is additive-only — but that remains a decision to
|
||||
reconfirm at minting time).
|
||||
|
||||
---
|
||||
|
||||
## 15. Open Questions
|
||||
|
||||
Revision 1's Open Question 1 (empty vs. absent `surfaces`) is now resolved by
|
||||
Section 6.2 and removed from this list.
|
||||
|
||||
1. Should `role` (Section 5.2) start as free text or a small closed
|
||||
enumeration? This proposal leaves it free text for 6.1–6.6 and defers
|
||||
tightening it until real reference-exhibit and SciFi-XZBT usage exists to
|
||||
generalize from.
|
||||
2. Exact behavior when a currently-open surface's descriptor disappears from
|
||||
a new `describe` result (e.g. `registryRevision` bump removes a surface
|
||||
that is currently open in a window) — recommend NGN closes the window and
|
||||
surfaces a diagnostic, but this needs to be proven in Phase 6.5/6.6, not
|
||||
just asserted here.
|
||||
3. Whether `requires` on a surface (Section 5.2) should block *opening* the
|
||||
surface outright, or only annotate it as degraded while still allowing
|
||||
open (consistent with how a target with an unavailable capability remains
|
||||
discoverable per §9). This proposal recommends the latter, for
|
||||
consistency, but flags it for confirmation during Phase 6.3.
|
||||
4. Whether the generic attachment mechanism (Section 7.2) should eventually
|
||||
be *documented* (not contractually mandated) as a recommended pattern in
|
||||
the Authoring Guide, once the reference exhibit and SciFi-XZBT adaptation
|
||||
have both proven it out — so future exhibit authors aren't reinventing
|
||||
attachment transport choices from scratch. Recommend deferring this
|
||||
documentation question to after Phase 6.8.
|
||||
|
||||
---
|
||||
|
||||
## 16. Acceptance Criteria for 6.1 (Revision 2)
|
||||
|
||||
This document satisfies Phase 6.1 when:
|
||||
|
||||
- [x] It defines what a presentation surface is and is not (Sections 2–3).
|
||||
- [x] It defines the surface descriptor, required and optional fields
|
||||
(Section 5), with surface IDs conforming exactly to the existing
|
||||
canonical target-ID grammar (Section 5.1) — no carve-out.
|
||||
- [x] It defines discovery as an extension to `describe` governed by
|
||||
`registryRevision` (Sections 6, 9), with a tightened, unambiguous
|
||||
`primary`/backward-compatibility invariant covering exactly three
|
||||
recognized forms of `surfaces` (Section 6.2).
|
||||
- [x] It defines an explicit, generic surface-attachment mechanism — required
|
||||
properties, a required attachment sequence, and two permitted
|
||||
transport shapes — that any exhibit implementation can follow without
|
||||
inventing its own protocol (Section 7.2).
|
||||
- [x] It defines an explicit URL-resolution algorithm for the surface `url`
|
||||
field, including same-origin and path-containment enforcement (Section
|
||||
6.3).
|
||||
- [x] It defines a state-synchronization model with a single authoritative
|
||||
state (the Exhibit State Core) shared by all surfaces, and explicitly
|
||||
rules out per-surface duplicated state (Section 7).
|
||||
- [x] It defines the surface lifecycle (Section 8).
|
||||
- [x] It defines ownership across exhibit / NGN / transport / display
|
||||
endpoint (Section 4), explicitly deferring display endpoints to Step 7.
|
||||
- [x] It defines standalone-exhibit compatibility with a precise,
|
||||
non-conflicting requirement split between the primary surface
|
||||
(unconditional) and non-primary surfaces (best-effort, attachment-
|
||||
mechanism-dependent) (Section 10).
|
||||
- [x] It defines security/origin assumptions, extended to the attachment
|
||||
channel (Section 13).
|
||||
- [x] It records the owner-approved Contract 5.3 decision, with the file
|
||||
mint and contract-document edits still explicitly deferred (Section
|
||||
14).
|
||||
- [x] It lists genuine remaining open questions rather than inventing answers
|
||||
where none are warranted (Section 15).
|
||||
- [x] It does not begin implementation, does not create the Contract 5.3
|
||||
file, and does not modify existing source or contract documents.
|
||||
|
||||
**Next step, upon final owner approval of this revision:** mint the
|
||||
versioned Contract 5.3 file per Section 14.3's timing decision, then begin
|
||||
Phase 6.2 — build the Museum Gallery reference exhibit proving this model.
|
||||
Neither has begun as part of this document.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,395 @@
|
||||
# Museum Gallery — Step 6.2 Verification
|
||||
|
||||
**Status: STEP 6.2 COMPLETE — REFERENCE MULTI-SURFACE MODEL PROVEN.** The
|
||||
real-browser harness (Section K) has now been executed and passed 13/13
|
||||
required proof points (16/16 including 3 additional harness self-checks),
|
||||
repeated across 6 consecutive runs with zero flakes.
|
||||
|
||||
## A. Purpose
|
||||
|
||||
This document records the verification evidence for Phase 6.2 of Step 6:
|
||||
the Museum Gallery multi-surface reference exhibit, built against Contract
|
||||
5.3 (`docs/contract/XZBT-Exhibit-Contract-Specification-v5.3.md`) and the
|
||||
Step 6.1 architecture (`docs/architecture/XZBT-Multi-Surface-Model-Step6.1.md`,
|
||||
Revision 2).
|
||||
|
||||
## B. Architecture summary
|
||||
|
||||
One Museum Gallery exhibit instance exposes three presentation surfaces —
|
||||
`surface.control` (primary), `surface.artifact`, `surface.info-wall` — all
|
||||
views onto one Exhibit State Core, constructed exactly once, only by the
|
||||
primary surface's document (`control.html`). Non-primary surfaces
|
||||
(`artifact.html`, `info-wall.html`) never load the Core-constructing files
|
||||
(`contract-core.js`, `exhibit.js`, `contract-adapter.js`) at all; they load
|
||||
only the exhibit-internal attachment bus (`surface-bus.js`) and attach to the
|
||||
Core that `control.html` owns. This is structural, not just documented
|
||||
convention: those two documents are incapable of constructing a second Core
|
||||
because the constructor (`window.MuseumGalleryContract.create`) is simply
|
||||
undefined in their execution context — proven directly in
|
||||
`tests/museum-gallery.test.js` ("Exactly one Exhibit State Core exists").
|
||||
|
||||
## C. Chosen internal attachment transport
|
||||
|
||||
Same-origin `BroadcastChannel`, channel name `xzbt-museum-gallery-core-v1`,
|
||||
with `control.html` as the sole identified owner. Full rationale and message
|
||||
shapes are documented in
|
||||
`test-fixtures/reference-exhibits/museum-gallery/README.md`. This transport
|
||||
is exhibit-internal per Contract 5.3 §31.7/§31.9 — it is not part of the
|
||||
XZBT wire protocol, carries no contract envelope, and Contract 5.3 was not
|
||||
modified to encode it.
|
||||
|
||||
## D. Surface list
|
||||
|
||||
| id | label | primary | url |
|
||||
| --- | --- | --- | --- |
|
||||
| `surface.control` | Control Room | `true` | `control.html` |
|
||||
| `surface.artifact` | Artifact Display | `false` | `artifact.html` |
|
||||
| `surface.info-wall` | Information Wall | `false` | `info-wall.html` |
|
||||
|
||||
All three are discovered from a live `core.describe().surfaces` call in
|
||||
`tests/museum-gallery.test.js`'s first test, which also asserts the
|
||||
canonical dotted-ID grammar, the single-`primary` invariant, and
|
||||
relative-only `url` values.
|
||||
|
||||
## E. Shared state
|
||||
|
||||
`artifact.selected` (selection: `the-orrery` / `star-map` / `meteorite`),
|
||||
`lighting.level` (range 0–1), `rotation.speed` (range 0–2),
|
||||
`labels.enabled` (boolean state), plus one impulse, `action.spotlight-flash`,
|
||||
used to prove `action.executed` propagation across surfaces.
|
||||
|
||||
## F. Proof of one authoritative Core
|
||||
|
||||
Three lines of evidence:
|
||||
|
||||
1. **Structural**: non-primary surface test contexts never load the files
|
||||
that could construct a Core (`tests/museum-gallery.test.js`, "Exactly one
|
||||
Exhibit State Core exists" and the artifact/info-wall boot scripts
|
||||
themselves — see their file headers in
|
||||
`test-fixtures/reference-exhibits/museum-gallery/`).
|
||||
2. **Behavioral**: every synchronization test drives state changes from
|
||||
both the "primary" (direct `core.applyMutation`/`invokeAction` calls,
|
||||
standing in for the Control Room's own controls) and from a non-primary
|
||||
surface's `link.mutate(...)` call, and asserts the *other* surface(s)
|
||||
converge on the identical value with the identical `stateRevision` —
|
||||
there is no code path in this exhibit that could produce disagreement,
|
||||
because there is only one place state is ever written.
|
||||
3. **Sequence**: `core.eventLog()` sequence numbers are asserted strictly
|
||||
monotonic and non-duplicated across mutations that originated from
|
||||
different "surfaces" in the same test run — one stream, not one per
|
||||
surface.
|
||||
|
||||
## G. Reference exhibit implementation
|
||||
|
||||
Files added under `test-fixtures/reference-exhibits/museum-gallery/`:
|
||||
`exhibit.js` (domain model), `contract-adapter.js` (Contract 5.3 wiring,
|
||||
including the `SurfaceCatalog`), `surface-bus.js` (attachment transport),
|
||||
`control.html`/`control.boot.js` (primary), `artifact.html`/
|
||||
`artifact.boot.js` and `info-wall.html`/`info-wall.boot.js` (non-primary),
|
||||
`style.css`, `index.html` (redirect to `control.html`), `README.md`,
|
||||
`verification.html` (browser harness — see Section K).
|
||||
|
||||
## H. NGN discovery implementation
|
||||
|
||||
**Not built in Phase 6.2, by design.** The Step 6.2 brief explicitly
|
||||
excludes NGN generic surface discovery, a surface picker, and open/close
|
||||
window controls from this phase (those are Phase 6.3). Nothing in
|
||||
`src/` was touched.
|
||||
|
||||
## I. Local rendering implementation
|
||||
|
||||
Also explicitly out of scope for 6.2 (Phase 6.4). `verification.html` opens
|
||||
the three surfaces as iframes purely as a **test harness** for this
|
||||
document's own evidence gathering, not as NGN product behavior.
|
||||
|
||||
## J. Shared-state synchronization — automated test results
|
||||
|
||||
`tests/museum-gallery.test.js`, run via `node --test tests/museum-gallery.test.js`:
|
||||
|
||||
```
|
||||
1..16
|
||||
# tests 16
|
||||
# pass 16
|
||||
# fail 0
|
||||
```
|
||||
|
||||
Full suite together with the pre-existing tests (`node --test 'tests/*.test.js'`):
|
||||
|
||||
```
|
||||
1..18
|
||||
# tests 18
|
||||
# pass 18
|
||||
# fail 0
|
||||
```
|
||||
|
||||
The 16 Museum Gallery tests cover proof points 1–13 from the Step 6.2 brief
|
||||
directly:
|
||||
|
||||
1. Contract 5.3 `describe` includes `surfaces` — covered.
|
||||
2. exactly one primary surface — covered.
|
||||
3. surface IDs validate — covered (both the describe-level test and four
|
||||
dedicated `SurfaceCatalog` unit tests covering the Part A2 validation
|
||||
order: an individually-invalid entry discarded before the primary
|
||||
invariant is evaluated, zero-primary rejection, multiple-primary
|
||||
rejection, and all-entries-discarded-is-absent-not-malformed).
|
||||
4. surface URLs validate — covered, including a dedicated test for
|
||||
absolute/protocol-relative URLs being discarded.
|
||||
5. one authoritative Core — covered (Section F).
|
||||
6. primary → non-primary synchronization — covered.
|
||||
7. non-primary → primary and non-primary → other non-primary — covered in
|
||||
one test, asserting both directions from a single surface-originated
|
||||
mutation.
|
||||
8. `stateRevision` increments correctly — covered, including the "increments
|
||||
exactly once" assertion on the surface-originated mutation test.
|
||||
9. one event-sequence stream — covered.
|
||||
10. close/detach does not mutate state — covered (asserts `stateRevision`
|
||||
and `eventLog().length` are unchanged immediately after detach).
|
||||
11. reopen/reattach gets current state — covered, both as its own test and
|
||||
as part of the detach test (attach → mutate → detach → mutate again →
|
||||
reattach → assert the reattached surface sees the latest value, not a
|
||||
stale one).
|
||||
12. no independent per-surface state — covered directly (two surfaces
|
||||
attached simultaneously are asserted to receive byte-identical event
|
||||
sequences) and structurally (Section F).
|
||||
13. primary still works standalone — covered: a dedicated test drives the
|
||||
Core through `stateSnapshot`/`applyMutation`/`invokeAction` with zero
|
||||
surface-bus or NGN involvement at all.
|
||||
|
||||
**What this test suite genuinely proves, precisely stated**: these tests use
|
||||
Node's own global `BroadcastChannel` implementation (the same message-passing
|
||||
spec surface the DOM implementation exposes, keyed by channel name,
|
||||
process-wide) across independent `vm` contexts that do not share JavaScript
|
||||
realm state — this is a real, if not browser-hosted, proof of the
|
||||
cross-document attachment sequence, not a mock of it. What it does **not**
|
||||
prove is browser-specific behavior: real separate windows/tabs, real
|
||||
same-origin-over-HTTP enforcement, real page-unload `detach()` firing, or
|
||||
visual rendering. That gap is Section K/L below.
|
||||
|
||||
## K. Reference exhibit browser verification
|
||||
|
||||
**Executed this session — real browser, real HTTP origin, real
|
||||
cross-document `BroadcastChannel`. Result: 13/13 required proof points
|
||||
passing (16/16 including 3 additional harness self-checks), on every one of
|
||||
6 consecutive runs.**
|
||||
|
||||
`test-fixtures/reference-exhibits/museum-gallery/verification.html` opens
|
||||
all three surfaces (`control.html`, `artifact.html`, `info-wall.html`) as
|
||||
three separate same-origin iframe documents and drives the 13 proof points
|
||||
listed in Section J directly against real DOM state and a real,
|
||||
non-mocked `BroadcastChannel`, logging PASS/FAIL per check plus a
|
||||
pass/total summary.
|
||||
|
||||
**Why this session, not the user's machine.** The device bridge's file
|
||||
mount (`device_bash`) has remained unreachable all session ("A Windows
|
||||
update released September 8 prevents Claude's workspace from reaching your
|
||||
files"), so `npm start` could not be run on the user's machine as
|
||||
originally planned. Rather than leave the browser-verification requirement
|
||||
unsatisfied, the exhibit's already-staged files (present in this session's
|
||||
own working copy from building Phase 6.2) were served over a real HTTP
|
||||
origin (`http://127.0.0.1:4173/...`) by a throwaway static file server
|
||||
started in this session's cloud container, and driven with a genuine
|
||||
Chromium instance (Playwright, headless) — not a headless DOM shim, not
|
||||
`file://`, and not Node's `vm`-context proof from Section J. This satisfies
|
||||
the same same-origin requirement `npm start` would have: a real HTTP
|
||||
origin, not an opaque per-navigation `file://` origin. The static server
|
||||
and the Playwright driver script are test infrastructure only; neither is
|
||||
part of the Museum Gallery deliverable and neither was added to the repo.
|
||||
|
||||
**Harness hardening applied this pass.** The harness previously used fixed
|
||||
sleeps (`await wait(300)`, `wait(200)`, `wait(250)`, …) standing in for
|
||||
attachment and propagation delays. Every one of those was replaced with
|
||||
condition-based polling against the actual signal being waited on:
|
||||
`isAttached()` on both non-primary surface links before proceeding, DOM
|
||||
content matching the expected post-mutation value (for the primary→non-primary
|
||||
and non-primary→primary/non-primary checks), and `isAttached()` again after
|
||||
the Information Wall's reopen. A failed poll now dumps diagnostics (the
|
||||
Core's current value for the target, the surface's DOM value, the current
|
||||
`stateRevision`, and the 5 most recent events) instead of a bare FAIL line.
|
||||
Only `verification.html` (the test harness) was changed — no change was
|
||||
made to `surface-bus.js`, `contract-core.js`, or any Museum Gallery boot
|
||||
script, since no run of the hardened harness ever demonstrated a defect in
|
||||
them (see below).
|
||||
|
||||
**One flaky result during hardening, and what it showed.** The very first
|
||||
run against the newly-hardened harness (before any polling logic existed)
|
||||
returned 12/16, with `artifactLink.isAttached()` still `false` at the
|
||||
3-second poll deadline and two downstream checks failing in consequence;
|
||||
the diagnostic dump showed the Core's own state (`rotation.speed: 1.7`,
|
||||
`stateRevision: 1`) was already correct at that moment — only the
|
||||
`artifact.html` iframe's attachment hadn't yet been observed by the
|
||||
poll. Every subsequent run (6 consecutive, described above) passed 16/16
|
||||
with the identical unmodified exhibit code, which is inconsistent with a
|
||||
code defect and consistent with a one-time cold-start artifact (first
|
||||
Chromium launch, first page load, first `BroadcastChannel` construction in
|
||||
that process). No exhibit or surface-bus code was changed in response,
|
||||
per the instruction to leave the state architecture and surface bus alone
|
||||
absent a deterministic proof of a defect — six identical, clean runs is
|
||||
that determinism check, and it did not implicate the exhibit.
|
||||
|
||||
**One benign console error observed, not a defect.** On every run, the
|
||||
Information Wall's reopen step (proof point 11: the iframe's `src` is set
|
||||
back to `info-wall.html`, forcing a real navigation) produces a harmless
|
||||
`pageerror` from the *previous* document's `beforeunload` handler:
|
||||
`info-wall.boot.js` calls `link.detach()`, which calls
|
||||
`channel.postMessage(...)`, but the browser has already begun invalidating
|
||||
that document's `BroadcastChannel` by the time `beforeunload` runs, so the
|
||||
`postMessage` throws `InvalidStateError: Channel is closed`. This is a
|
||||
teardown-time exception in code that no longer affects any observable
|
||||
state (the document is already being discarded) — it does not fail any of
|
||||
the 13 proof points in any of the 6 runs, and `surface-bus.js`'s own
|
||||
comment already documents that detach is bookkeeping-only ("no state is
|
||||
held by the surface, so nothing reconciles"). It is recorded here as an
|
||||
observed cosmetic rough edge, not fixed, because it does not meet the bar
|
||||
of a proven implementation defect and touching `surface-bus.js` is outside
|
||||
this pass's narrow scope.
|
||||
|
||||
See Section J for exactly what the Node-level `vm`-context tests prove on
|
||||
their own, and Section R for the residual limitation this run does still
|
||||
leave open (real separate top-level windows vs. iframes).
|
||||
|
||||
## L. SciFi-XZBT Observation integration
|
||||
|
||||
Not applicable to Phase 6.2 — that is Phase 6.7/6.8.
|
||||
|
||||
## M. SciFi-XZBT interoperability results
|
||||
|
||||
Not applicable to Phase 6.2.
|
||||
|
||||
## N. Automated tests
|
||||
|
||||
See Section J. `node --test 'tests/*.test.js'`: 18/18 passing, including the
|
||||
2 pre-existing Haunted House tests, confirmed unaffected by the shared
|
||||
`contract-core.js` changes (see Section P).
|
||||
|
||||
## O. Browser/runtime verification
|
||||
|
||||
See Section K. 13/13 required proof points passing in a real browser over a
|
||||
real HTTP origin, 6/6 consecutive clean runs.
|
||||
|
||||
## P. Genericity verification
|
||||
|
||||
- `test-fixtures/reference-exhibits/shared/contract-core.js` was extended
|
||||
additively: every new field (`options.surfaces`, `options.contractMinor`,
|
||||
`options.xzbtVersion`) defaults to the exact pre-5.3 behavior when
|
||||
omitted. `describe()` omits the `surfaces` key entirely unless a
|
||||
`SurfaceCatalog` was supplied — verified directly by the "Non-surface-aware
|
||||
exhibits are unaffected" test.
|
||||
- Aquarium, Planetarium, and Haunted House were **not modified**. Haunted
|
||||
House's existing test suite (`tests/haunted-house.test.js`) was rerun
|
||||
unchanged against the modified shared core and still passes 2/2.
|
||||
- Nothing under `src/` (the NGN host itself) was touched in Phase 6.2.
|
||||
- Museum Gallery's own files contain no SciFi-XZBT vocabulary, and nothing
|
||||
in `contract-core.js`'s new `SurfaceCatalog` logic references any
|
||||
exhibit's domain content — it validates only the generic Contract 5.3
|
||||
§31.2/§31.3 shape (id grammar, required fields, the primary invariant, URL
|
||||
relative-safety).
|
||||
|
||||
## Q. Files changed
|
||||
|
||||
New:
|
||||
|
||||
```
|
||||
test-fixtures/reference-exhibits/museum-gallery/exhibit.js
|
||||
test-fixtures/reference-exhibits/museum-gallery/contract-adapter.js
|
||||
test-fixtures/reference-exhibits/museum-gallery/surface-bus.js
|
||||
test-fixtures/reference-exhibits/museum-gallery/control.html
|
||||
test-fixtures/reference-exhibits/museum-gallery/control.boot.js
|
||||
test-fixtures/reference-exhibits/museum-gallery/artifact.html
|
||||
test-fixtures/reference-exhibits/museum-gallery/artifact.boot.js
|
||||
test-fixtures/reference-exhibits/museum-gallery/info-wall.html
|
||||
test-fixtures/reference-exhibits/museum-gallery/info-wall.boot.js
|
||||
test-fixtures/reference-exhibits/museum-gallery/index.html
|
||||
test-fixtures/reference-exhibits/museum-gallery/style.css
|
||||
test-fixtures/reference-exhibits/museum-gallery/README.md
|
||||
test-fixtures/reference-exhibits/museum-gallery/verification.html
|
||||
tests/museum-gallery.test.js
|
||||
docs/reference/Museum-Gallery-Step6.2-Verification.md (this file)
|
||||
```
|
||||
|
||||
Modified (additive only — see Section P):
|
||||
|
||||
```
|
||||
test-fixtures/reference-exhibits/shared/contract-core.js
|
||||
```
|
||||
|
||||
Not modified: `src/`, `public/`, `server/`, Aquarium, Planetarium, Haunted
|
||||
House, Contract 5.2, Contract 5.3, the Step 6.1 architecture document.
|
||||
|
||||
## R. Known limitations
|
||||
|
||||
1. Live browser verification (`verification.html`) has been executed and
|
||||
passed 13/13 (Section K), but only as three iframes under one top-level
|
||||
page, not as three genuinely separate browser windows/tabs opened
|
||||
independently on the user's machine via `npm start` — see item 4 below
|
||||
for what that gap does and doesn't matter for.
|
||||
2. NGN attachment interoperability is not exercised: Museum Gallery declares
|
||||
`xzbt: '5.3'`; today's pre-6.3 NGN host always sends `xzbt: '5.2'` on
|
||||
every envelope, so an actual attach attempt from the current NGN would
|
||||
currently be rejected at the envelope-version check in
|
||||
`ContractCore.handleRequest`, never reaching contract-major negotiation.
|
||||
This is expected — NGN's own Contract 5.3 awareness is Phase 6.3+ work —
|
||||
but is recorded here explicitly rather than left implicit.
|
||||
3. The `role` field on surface descriptors (`control` / `ambient` /
|
||||
`information`) is illustrative only, per Contract 5.3 §31.2; no host
|
||||
behavior depends on its value yet.
|
||||
4. `verification.html`'s iframe-based harness proves same-origin,
|
||||
same-tab-group multi-document behavior (now executed, Section K); it
|
||||
does not by itself prove behavior across genuinely separate top-level
|
||||
browser windows (e.g. window-close/`beforeunload` timing can differ
|
||||
slightly between an iframe removal and a real window close — Section K
|
||||
already surfaced one such teardown-time quirk, observed as benign). A
|
||||
follow-up pass opening the three surfaces as real separate windows on
|
||||
the user's own machine via `npm start` is recommended before Phase
|
||||
6.6's broader reference-exhibit closure, though Phase 6.2 itself only
|
||||
requires proving the architecture, which the iframe harness and the
|
||||
Node-level tests both now do.
|
||||
|
||||
## S. Deferred Step 7 concerns
|
||||
|
||||
Casting, remote display endpoints, Chromecast/Google TV, network display
|
||||
discovery — untouched, as required. Nothing in Museum Gallery's surface
|
||||
descriptors or attachment transport names a display endpoint of any kind.
|
||||
|
||||
## T. Architecture review findings
|
||||
|
||||
- No hidden duplicate state machine: confirmed structurally (Section F) —
|
||||
non-primary documents cannot construct a Core even accidentally, since the
|
||||
constructing function is undefined in their scope.
|
||||
- No host/exhibit ownership confusion: NGN was not touched; every line of
|
||||
new code lives in the exhibit or its tests.
|
||||
- No implicit hard-coded surfaces: `surfaces` are discovered from
|
||||
`core.describe()`'s live `SurfaceCatalog` output in every test; nothing
|
||||
hard-codes surface names outside the exhibit's own descriptor list.
|
||||
- No transport coupling: Contract 5.3 was not touched by this phase, and the
|
||||
BroadcastChannel choice lives entirely in `surface-bus.js` and this
|
||||
document/README, never in the contract.
|
||||
- No display-endpoint assumptions: confirmed (Section S).
|
||||
- No revision inconsistencies: a dedicated test asserts `stateRevision`
|
||||
increments exactly once per transaction regardless of originating surface,
|
||||
and `registryRevision` is read directly from `core.registryRevision` with
|
||||
no separate surface-registry counter anywhere in the implementation.
|
||||
- Reconnect/stale-window behavior: not applicable in Phase 6.2 (no NGN
|
||||
session, no window lifecycle beyond the exhibit's own attach/detach) —
|
||||
deferred to Phase 6.5/6.6 as the Step 6.1 document already anticipated.
|
||||
|
||||
## U. Final verdict
|
||||
|
||||
**STEP 6.2 COMPLETE — REFERENCE MULTI-SURFACE MODEL PROVEN**
|
||||
|
||||
The multi-surface architecture is implemented and proven by two
|
||||
independent lines of evidence: 16 passing Node-level tests exercising a
|
||||
real cross-context `BroadcastChannel` attachment sequence (Section J), and
|
||||
a real-browser run of `verification.html` — three separate same-origin
|
||||
documents served over a genuine HTTP origin and driven with headless
|
||||
Chromium — passing all 13 required proof points (16/16 including harness
|
||||
self-checks) across 6 consecutive runs with no flakes and no code changes
|
||||
to the exhibit or its attachment transport (Section K). 2 pre-existing
|
||||
tests confirm no regression to the other reference exhibits. The residual
|
||||
item (Section R.4/R.1) — real separate top-level windows on the user's own
|
||||
machine via `npm start`, rather than same-page iframes — is a
|
||||
lower-confidence gap the brief's 13 proof points do not require closed for
|
||||
Phase 6.2, and is recommended before Phase 6.6 rather than blocking this
|
||||
verdict.
|
||||
|
||||
Per the Step 6 roadmap: **NGN Phase 6.3 (generic surface discovery in the
|
||||
host) has not begun.** No file under `src/` was touched.
|
||||
Reference in New Issue
Block a user