generated from Labyricorn/labyricorn-project-template
Steps 6.4-6.7B — Local surfaces, reference-exhibit validation, SciFi Observation surface
One commit for the work accumulated in the working tree since Step 6.3, which had never been split into per-step commits: - src/local-surfaces.js + src/surface-url.js (new); src/ui.js, src/validation.js, src/connection.js and public/index.html updated for local-surface hosting and generic surface rendering - tests: local-surfaces (20), scifi-surfaces (24) and postmessage-interop (7) new; connection/museum-gallery/surface-validation suites updated - reference exhibits: shared/contract-core.js defaults to Contract 5.3 (major 5, minor 3, xzbt 5.3); museum-gallery advertises its surface catalog; aquarium/haunted-house/planetarium adapters updated - SciFi-XZBT (Step 6.7A/6.7B): surface-mode.js + surface-bus.js, Observation-surface boot branch, local-change hooks, view.pillars / view.warp-flight targets; fixture byte-identical to G:/.vibe/SciFi-XZBT - SciFi-XZBT contract adapter handshake fix: the inbound bridge filter no longer gates on an exact advisory xzbt value (Contract 5.3 §6.5), only on its presence/type, matching the host's own envelope validation; the adapter now advertises contract minor 3 / version 5.3.0, which it already implemented via the 5.3 surfaces field. Root cause of the five failing postmessage-interop tests (host hello was silently dropped). - docs: architecture 6.4 and 6.7A, reference 6.6 and 6.7; evidence logs; test-fixtures/PROVENANCE.md resync record Test results: NGN 154/154 (was 149/154); postmessage-interop 7/7 (was 2/7); SciFi contract harness 21/21, real-adapter suite 32/32. git diff --check clean for changed files; two pre-existing trailing-whitespace lines remain in test-fixtures/reference-exhibits/scifi/index.html, copied verbatim from the authoritative SciFi source. Step 6.7 live verification (browser Observation, packaged standalone) is still pending and is not claimed here.
This commit is contained in:
@@ -0,0 +1,945 @@
|
||||
# Step 6.7A — SciFi Observation Surface Architecture
|
||||
|
||||
**Status:** design only. No implementation performed. No source files modified.
|
||||
**Scope:** adapting SciFi-XZBT Observation to the already-proven Contract 5.3
|
||||
local presentation-surface model (Steps 6.3–6.6).
|
||||
**Audience:** the implementation model executing Step 6.7B.
|
||||
|
||||
### Repositories inspected
|
||||
|
||||
| Repo | Path | Role |
|
||||
| --- | --- | --- |
|
||||
| XZBT-NGN | `G:/.vibe/XZBT-NGN` | Host engine, contract spec, test fixtures |
|
||||
| SciFi-XZBT | `G:/.vibe/SciFi-XZBT` | **The real exhibit source.** Step 6.7B edits here. |
|
||||
|
||||
`test-fixtures/reference-exhibits/scifi/` inside XZBT-NGN is a *copy* of
|
||||
SciFi-XZBT, kept for NGN's own tests (`test-fixtures/PROVENANCE.md`). Step 6.7B
|
||||
must change SciFi-XZBT and then resync the fixture. Both working trees carry
|
||||
uncommitted work; nothing was modified by this task.
|
||||
|
||||
---
|
||||
|
||||
## 1. Recommendation
|
||||
|
||||
**Adopt Candidate A in its narrowest form: one document, one script set, two boot
|
||||
modes.** SciFi advertises exactly two surfaces. The primary is the existing
|
||||
`index.html` (unchanged, the full console). The Observation surface is
|
||||
*the same document* re-served with a query string — `?surface=observation&xi=<instanceId>`
|
||||
— which Contract 5.3 §31.4 explicitly provides for ("a bare query string and/or
|
||||
fragment, resolving against the exhibit's own base document, for a single-page
|
||||
exhibit whose surfaces are views within one already-served document"). A boot-mode
|
||||
resolver read at the top of `app.js` decides which mode the instance runs in. In
|
||||
Observation mode the instance constructs no audio graph, no generative engine, no
|
||||
contract host bridge, and no ambient event scheduler; it renders the existing
|
||||
`#observation-overlay` and mirrors state from the authoritative console instance
|
||||
over an exhibit-internal, instance-scoped `BroadcastChannel`.
|
||||
|
||||
This is the only candidate that survives contact with the actual code. Roughly
|
||||
3,230 of `app.js`'s 4,703 lines (lines ~1261–4494) are the Observation renderer,
|
||||
and they live inside `app.js`'s single IIFE closure, reading `activeUniverseId`,
|
||||
`activePresetId`, `isPlaying`, `observationActivity`, `UniverseRegistry`,
|
||||
`observationEngine`, `alerts` and `audioManager` as free variables.
|
||||
`observation-engine.js` additionally reads five *console-panel* element IDs
|
||||
(`toggle-observation-viewport`, `val-observation-viewport`,
|
||||
`toggle-observation-pillars`, `val-observation-pillars`, and the preset select)
|
||||
that do not exist outside the console document. Keeping one document means **zero
|
||||
renderer extraction and zero DOM decoupling**: every element the renderer reaches
|
||||
for is still present, merely hidden by a body class. A dedicated lightweight page
|
||||
(Candidate B) would require lifting that entire closure into a shared module —
|
||||
the single largest and riskiest refactor available in this project — and would
|
||||
still not fit the distribution, because `tools/package.ps1` inlines exactly one
|
||||
input document and emits exactly one output file. A second HTML document simply
|
||||
cannot exist in the standalone single-file build.
|
||||
|
||||
The second load-bearing decision: **surface existence is not `view.observation`.**
|
||||
`view.observation` keeps its current meaning exactly — the console page's own
|
||||
full-screen overlay — and opening or closing the NGN Observation pane never
|
||||
touches it. Conflating them would black out the operator's console the moment
|
||||
they opened the presentation pane, which is precisely the simultaneity failure
|
||||
Candidate C is rejected for. The Observation surface renders the scene
|
||||
unconditionally while it is open; that is its whole purpose and it needs no state
|
||||
flag to say so.
|
||||
|
||||
---
|
||||
|
||||
## 2. Current SciFi state architecture
|
||||
|
||||
### 2.1 Where state actually lives
|
||||
|
||||
`app.js` is one `DOMContentLoaded` IIFE. There is no state object. Authority is
|
||||
distributed across subsystem instances and three closure variables, and
|
||||
`getContractStateSnapshot()` (app.js:4666) is the one function that composes them
|
||||
into the contract-visible view.
|
||||
|
||||
| Contract state | Authoritative owner | Read path |
|
||||
| --- | --- | --- |
|
||||
| `universe.selected` | `let activeUniverseId` (app.js:29) | closure var |
|
||||
| `preset.selected` | `let activePresetId` (app.js:30) | closure var |
|
||||
| `transport.playing` | `let isPlaying` (app.js:28) | closure var |
|
||||
| `transport.muted`, `mix.master` | `AudioManager` | `audioManager.isMuted` / `.getMasterVolume()` |
|
||||
| `mix.hull.*` | `HullDroneSynth.params` | `hullDrone.params.*` |
|
||||
| `mix.drive.*` | `WarpCoreSynth.params` | `warpCore.params.*` |
|
||||
| `mix.environment.*` | `LifeSupportSynth.params` | `lifeSupport.params.*` |
|
||||
| `mix.telemetry.*` | `TelemetrySynth.params` | `telemetry.params.*` |
|
||||
| `fnc.enabled`, `fnc.level` | `FuturisticNoiceCancellation` | `fncSystem.enabled` / `.level` |
|
||||
| `speech.robot-amount` | `XZBTGenerativeExperience` | `generativeExperience.roboticAmount()` |
|
||||
| `alert.active` | `AlertSynth.activeAlert` | `alerts.activeAlert` |
|
||||
| `view.observation` | `let observationActive` (app.js:1281) | closure var |
|
||||
| `view.activity` | `let observationActivity` (app.js:1282) | closure var |
|
||||
| `view.viewport-frame` | `ObservationEngine.showViewport` | `observationEngine.showViewport` |
|
||||
| **`showPillars`** *(no target)* | `ObservationEngine.showPillars` | — |
|
||||
| **warp flight** *(no target)* | `ObservationEngine` internal | — |
|
||||
| **HUD hold** *(no target)* | console checkbox DOM only | — |
|
||||
|
||||
**Canonical vs reflected.** The subsystem/closure values above are canonical. All
|
||||
DOM is *reflected*: every setter in the adapter's `bindings.setters` writes the
|
||||
subsystem first and then assigns `slider.value` / `label.textContent`. There is
|
||||
exactly one genuine exception — **HUD hold** is stored nowhere but
|
||||
`#toggle-observation-hud.checked`, which is why it is treated as surface-local in
|
||||
§7 rather than promoted to a target.
|
||||
|
||||
**Observation does not own its own mutation path.** `observationEngine.onMutation`
|
||||
(app.js:259) routes every dock interaction straight into
|
||||
`contractAdapter.applyMutation(...)` with source `'ui'` — except `togglePillars()`
|
||||
and `toggleWarpFlight()`, which mutate engine-local fields directly because no
|
||||
canonical target exists for them (see §11).
|
||||
|
||||
**The adapter reads the same authority.** `bindings.getState` is literally
|
||||
`() => getContractStateSnapshot()`. There is no second state computation anywhere.
|
||||
|
||||
### 2.2 Flow diagram (actual)
|
||||
|
||||
```text
|
||||
NGN (postMessage) console UI hotkeys Observation dock
|
||||
| | | |
|
||||
| | | | observationEngine.onMutation
|
||||
v v v v
|
||||
adapter.handleMessage +------------+---------------+
|
||||
| |
|
||||
v v
|
||||
XZBTControlBus (intercepted) --> contractAdapter.applyMutation / invokeAction
|
||||
| [ THE one chokepoint ]
|
||||
v
|
||||
bindings.setters[id] / invokers[id]
|
||||
|
|
||||
+-------------------+-----------+-----------+-------------------+
|
||||
v v v v
|
||||
audio subsystems closure vars ObservationEngine reflected DOM
|
||||
(AudioManager, (activeUniverseId, (showViewport, canvas (sliders, labels,
|
||||
synths, FNC, activePresetId, scene, bezels) dock button text)
|
||||
alerts) isPlaying,
|
||||
observationActive,
|
||||
observationActivity)
|
||||
| | |
|
||||
+---------+---------+-----------------------+
|
||||
v
|
||||
_commitChanges() -> getContractStateSnapshot() diff
|
||||
|
|
||||
+--> stateRevision++ , state.changed / selection.changed
|
||||
+--> _emitEvent(...) [ GATED on sessionActive ]
|
||||
|
|
||||
v
|
||||
NGN host session
|
||||
```
|
||||
|
||||
**The gate marked above is critical for 6.7B.** `_emitEvent` begins
|
||||
`if (!this.sessionActive) return;` (contract-adapter.js:596). With no NGN attached,
|
||||
**no events are emitted at all.** A mirror built on contract events would therefore
|
||||
go dead in standalone multi-window use. §6.4 specifies the fix.
|
||||
|
||||
### 2.3 Observation entry/exit today
|
||||
|
||||
- `enterObservation()` (app.js:4443) — calls
|
||||
`generativeExperience.prepareExperience()` **(the WebLLM/Kokoro loader)**, sets
|
||||
`observationActive = true`, adds `.active` to `#observation-overlay`, calls
|
||||
`refreshObservation()`, starts the HUD/return fade timers, and calls
|
||||
`scheduleObservationAmbientActivity()`.
|
||||
- `exitObservation()` (app.js:4460) — stops the engine, the ambient timer, live
|
||||
instrumentation, spline morphs and the scene loop, then clears the stage markup.
|
||||
- `refreshObservation()` (app.js:4422) — injects `getObservationSvg(activeUniverseId)`,
|
||||
starts the scene and instrumentation, and calls `observationEngine.start(activeUniverseId)`.
|
||||
- Every entry/exit path (button app.js:4502, background click 4508, return pill 4517,
|
||||
Escape 4574) goes through `applyMutation('view.observation', …)`. Nothing toggles
|
||||
`observationActive` directly. This is already clean.
|
||||
|
||||
### 2.4 Assets, packaging and existing surface concepts
|
||||
|
||||
- `index.html` loads one stylesheet and ten scripts in a **fixed order**, all
|
||||
classic globals. `agents.md` forbids ES modules, dependencies and network access
|
||||
outside the one bounded `generative-experience.js` exception.
|
||||
- `tools/package.ps1` walks `index.html` line by line and inlines any
|
||||
`<script src="…"></script>` or `<link rel="stylesheet" href="…">` that matches
|
||||
its exact regex, emitting **one** file, `dist/SciFiAmbientDisplay_V<commit>.html`.
|
||||
One input document, one output document. There is no other build step.
|
||||
- **No existing surface concept.** `describe()` (contract-adapter.js:854) returns
|
||||
`exhibit / contract / registryRevision / stateRevision / capabilities / targets`
|
||||
and no `surfaces` field. SciFi is currently a conformant Contract 5.3 exhibit in
|
||||
§31.3 form 1 ("absent").
|
||||
- The nearest adjacent concept is the **TV PRESENTATION** card (16:9 aspect lock,
|
||||
render density). It is console-local presentation tuning, not a surface, and
|
||||
Step 6.7 should not disturb it.
|
||||
- **No Step 6.7 planning notes exist** in either repo. The only forward references
|
||||
are `STEP5-MVP-COMPLETION-REPORT.md:234` (the step list) and
|
||||
`docs/architecture/XZBT-Multi-Surface-Model-Step6.1.md:393`, which names §7.2/§8
|
||||
as "the reusable template Phase 6.7 adapts" — that template is the attachment
|
||||
sequence this design follows.
|
||||
|
||||
---
|
||||
|
||||
## 3. Candidate comparison
|
||||
|
||||
| | **A — same document, surface mode** | B — dedicated lightweight page | C — reuse control page/overlay |
|
||||
| --- | --- | --- | --- |
|
||||
| Architectural cleanliness | Good. One app, one code path, mode is a boot input. | Cleanest *in theory*; unreachable in practice. | Poor. Conflates host UI with exhibit presentation. |
|
||||
| Renderer refactoring | **None.** All ~3,230 renderer lines and all DOM stay put. | Extract the renderer from `app.js`'s closure; decouple 5 console-panel element reads in `observation-engine.js`. Very large. | None. |
|
||||
| Standalone behavior | Untouched. `index.html` with no query is byte-for-byte today's app. | Untouched, but the new page has no place in the single-file build. | Untouched. |
|
||||
| Duplicate initialization | Controlled: mode guard skips audio, AI, adapter bridge, visualizer, hotkeys, timers. | Structurally impossible to duplicate. | N/A. |
|
||||
| State authority | Single. Presentation instance never calls `applyMutation` locally. | Single. | Single, but only one view can exist. |
|
||||
| Sync complexity | One instance-scoped BroadcastChannel; Museum's proven shape. | Same. | None. |
|
||||
| Contract purity | Exactly the §31.4 query-string form the spec provides for. | Fine. | Fine, but defeats §31.1's "independently viewable". |
|
||||
| NGN genericity | No NGN change. | No NGN change. | Would tempt NGN into exhibit-specific pane handling. |
|
||||
| Future cast suitability | Good — the surface is already a standalone URL with its own viewport. | Best (smallest payload), marginally. | Bad. |
|
||||
| Packaging | `package.ps1` unchanged; two new inlined scripts. | **Breaks.** Second document cannot be packaged. | Unchanged. |
|
||||
| Implementation risk | Moderate, concentrated in `app.js` mode branching. | High. | Low, but wrong. |
|
||||
|
||||
### Rejections
|
||||
|
||||
- **B — Dedicated lightweight Observation surface.** Rejected on two independent
|
||||
grounds, either sufficient. (i) *Packaging:* `tools/package.ps1` produces a single
|
||||
self-contained document; a second HTML page has nowhere to go in the offline
|
||||
deliverable, so SciFi would fork into two maintained applications — explicitly
|
||||
forbidden by the brief. (ii) *Cost and risk:* it requires lifting ~3,230 lines out
|
||||
of a shared closure that `agents.md` marks "surgical, scoped edits — do not
|
||||
rewrite", plus decoupling `observation-engine.js` from five console-panel element
|
||||
IDs. The payoff is a lighter payload; but under Candidate A the presentation
|
||||
instance already parses the same bytes the browser has cached and *runs* almost
|
||||
none of the heavy subsystems, so the real saving is parse time, not runtime. Not
|
||||
worth the refactor. Revisit only if remote casting later demands a minimal payload.
|
||||
- **C — Reuse the existing page / overlay.** Rejected: it makes simultaneous
|
||||
control and observation impossible, which is the entire point of the multi-surface
|
||||
model (§31.1 "independently viewable"); NGN would end up manipulating exhibit DOM,
|
||||
breaking the generic boundary Step 6.4 established; and it has no path to casting.
|
||||
- **D — Better architecture found in the repository.** None found. The repository
|
||||
does, however, supply two refinements adopted into A: the §31.4 query-string
|
||||
surface form, and Museum Gallery's attach/snapshot/mutate/detach bus — improved
|
||||
here with an instance-scoped channel name (§6.2).
|
||||
|
||||
---
|
||||
|
||||
## 4. Surface catalog
|
||||
|
||||
Two surfaces. No more are justified: the console is one coherent operator view,
|
||||
and Observation is the one thing an audience is meant to look at. Adding
|
||||
per-universe or per-panel surfaces now would be speculative.
|
||||
|
||||
```js
|
||||
// js/surface-mode.js — instanceId is the live per-document id (see §6.2)
|
||||
function SURFACES(instanceId) {
|
||||
return [
|
||||
{
|
||||
id: 'surface.console',
|
||||
label: 'Main Console',
|
||||
kind: 'surface',
|
||||
primary: true,
|
||||
url: 'index.html',
|
||||
role: 'control',
|
||||
category: 'console',
|
||||
description: 'The full SciFi-XZBT operator console.'
|
||||
},
|
||||
{
|
||||
id: 'surface.observation',
|
||||
label: 'Observation',
|
||||
kind: 'surface',
|
||||
primary: false,
|
||||
url: '?surface=observation&xi=' + encodeURIComponent(instanceId),
|
||||
role: 'ambient',
|
||||
category: 'observation',
|
||||
aspectRatio: '16:9',
|
||||
requires: ['observation'],
|
||||
description: 'Full-screen procedural viewscreen mirroring the console.'
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
Schema notes, all verified against §31.2 and `src/validation.js`:
|
||||
|
||||
- `id` uses the canonical target-ID grammar (§8.1) and matches Museum's
|
||||
`surface.control` / `surface.info-wall` convention.
|
||||
- `kind: 'surface'` is the required constant. Exactly one `primary: true`.
|
||||
- Only documented optional fields are used: `description`, `role`, `aspectRatio`,
|
||||
`category`, `requires`. **No invented fields.** No field names a transport,
|
||||
endpoint or device class (§31.9).
|
||||
- `requires: ['observation']` reuses the adapter's already-declared `observation`
|
||||
capability (contract-adapter.js:52).
|
||||
|
||||
### Why `url: 'index.html'` for the primary
|
||||
|
||||
`src/local-surfaces.js:35` marks a descriptor as the already-open control pane
|
||||
with **full URL equality** against `host.exhibitBaseUrl`, and
|
||||
`src/connection.js` sets that base from `frame.contentWindow.location.href` after
|
||||
load. Connecting NGN to `…/scifi/index.html` — the URL already used by
|
||||
`README.md:54` and the Step 6.6 procedure — makes `index.html` resolve to exactly
|
||||
the base, so the primary reuses the existing session frame and is never opened a
|
||||
second time. See risk **R1** for the trailing-slash case.
|
||||
|
||||
### Why the Observation `url` is a bare query
|
||||
|
||||
§31.4 sanctions it for single-page exhibits, `resolveSurfaceURL` accepts it
|
||||
(relative, same-origin, no scheme), and `server/serve.js` routes on `url.pathname`
|
||||
only, so the `HEAD` resource check in `checkSurfaceResource` returns 200. No new
|
||||
file is served and `package.ps1` never sees a second document.
|
||||
|
||||
---
|
||||
|
||||
## 5. Authority and synchronization
|
||||
|
||||
### 5.1 Ownership rule
|
||||
|
||||
> The instance whose resolved mode is `console` is the sole authority. It owns
|
||||
> every subsystem, the one contract session, and the one mutation chokepoint. An
|
||||
> instance whose mode is `observation` owns **nothing**: it holds a read-only
|
||||
> mirror and may only *request* mutations.
|
||||
|
||||
The presentation instance never calls `contractAdapter.applyMutation`,
|
||||
`invokeAction`, or any `bindings.setter`. It never constructs a contract session:
|
||||
it does not install the window message bridge, so a stray `hello` cannot reach it,
|
||||
and NGN never offers one — `LocalSurfaces` attaches no transport to surface frames
|
||||
(`src/local-surfaces.js`, and `tests/local-surfaces.test.js:232` asserts a
|
||||
presentation window cannot impersonate an authoritative peer).
|
||||
|
||||
### 5.2 Mechanism: instance-scoped BroadcastChannel
|
||||
|
||||
Chosen mechanism: `BroadcastChannel`, named
|
||||
`xzbt-scifi-surface-v1:<instanceId>`, with the console document as owner —
|
||||
Museum Gallery's proven shape (`surface-bus.js`), with one deliberate improvement.
|
||||
|
||||
Evaluated and rejected:
|
||||
|
||||
- **Explicit parent/child `postMessage`.** *Structurally impossible here.* Under
|
||||
NGN the console document and the Observation iframe are **siblings** inside the
|
||||
NGN page, not parent and child. Neither holds a window handle to the other.
|
||||
This alone decides the mechanism.
|
||||
- **`storage` events / same-origin storage.** No request/response, so late join
|
||||
needs a polled or mirrored snapshot key; fires only in *other* documents;
|
||||
serializes on every change. Strictly worse.
|
||||
- **`SharedWorker`.** Extra lifecycle and an unsupported-context risk, for no
|
||||
benefit over BroadcastChannel.
|
||||
- **Extending `XZBTControlBus`.** It is an in-document registry with no transport;
|
||||
extending it means writing one of the above anyway. Keep it as-is.
|
||||
|
||||
**The improvement over Museum: the channel name carries an instance id.** Museum
|
||||
uses one fixed channel name, which means two independent Museum tabs on one origin
|
||||
would cross-talk. For SciFi that matters more: two console tabs is a normal thing
|
||||
to do. The console mints `instanceId` once per document load, publishes it inside
|
||||
the Observation surface URL (which `describe()` produces at call time, so it is
|
||||
always current), and the presentation instance reads it from its own query string.
|
||||
A surface with an unknown or stale id finds no owner and lands in the waiting
|
||||
state rather than attaching to the wrong console.
|
||||
|
||||
### 5.3 Message shapes (exhibit-internal; never seen by a host)
|
||||
|
||||
```
|
||||
surface -> owner : { type:'attach', requestId, participantId }
|
||||
owner -> surface : { type:'attach.snapshot', inReplyTo, values, stateRevision,
|
||||
registryRevision, presentation:{ tickerText } }
|
||||
owner -> all : { type:'state', target, value, stateRevision }
|
||||
owner -> all : { type:'action', target, args }
|
||||
owner -> all : { type:'presentation', kind:'ticker'|'obs-activity', … }
|
||||
surface -> owner : { type:'mutate', kind:'set'|'invoke', target, value|args }
|
||||
surface -> owner : { type:'detach', participantId }
|
||||
```
|
||||
|
||||
This carries no XZBT envelope, no `xzbt` field, no `sessionId`, and is never
|
||||
observed by NGN — §31.7's explicit allowance. Participant bookkeeping is a
|
||||
`Set` of `participantId`s, not a counter, so duplicate attach or late detach
|
||||
cannot ratchet the count (Museum's lesson; see its participant-lifecycle tests).
|
||||
|
||||
### 5.4 The event-gate correction (mandatory)
|
||||
|
||||
Because `_emitEvent` early-returns unless `sessionActive`, the mirror **must not**
|
||||
be fed from contract events. Add one exhibit-internal hook to the adapter,
|
||||
independent of session state:
|
||||
|
||||
```js
|
||||
// contract-adapter.js, inside _commitChanges(), after the event loop:
|
||||
if (this.onLocalChange) {
|
||||
for (const target of changes) {
|
||||
this.onLocalChange(target.id, after[target.id], this.stateRevision);
|
||||
}
|
||||
}
|
||||
// and in _invokeAction(), after _commitChanges:
|
||||
if (this.onLocalAction) this.onLocalAction(canonicalId, sanitizedArgs);
|
||||
```
|
||||
|
||||
Contract event semantics are untouched — a host still only sees events inside a
|
||||
session. The mirror gets a session-independent feed, so Observation works with or
|
||||
without NGN attached.
|
||||
|
||||
### 5.5 Guarantees
|
||||
|
||||
- **One authority:** enforced structurally — the presentation branch never
|
||||
constructs the mutation path's callers.
|
||||
- **Late join:** `attach` → owner replies with the live
|
||||
`contractAdapter.getContractState()` plus transient presentation extras.
|
||||
- **Reload:** a reload is a fresh `attach` with a fresh `participantId`; identical
|
||||
path to first join.
|
||||
- **Prompt propagation:** `onLocalChange` fires inside `_commitChanges`, i.e. in
|
||||
the same turn as the mutation.
|
||||
- **Surface interactions:** routed as `mutate` and executed by the owner through
|
||||
`contractAdapter.applyMutation(..., 'ui')` — the same chokepoint NGN's `set`
|
||||
uses, satisfying §31.7's mutation/revision/source/event requirement.
|
||||
- **No NGN-specific logic:** the bus is unaware NGN exists.
|
||||
|
||||
---
|
||||
|
||||
## 6. Audio / AI / procedural subsystem ownership
|
||||
|
||||
Audio is already safe by construction: `AudioManager`'s constructor
|
||||
(`audio.js`) sets `this.ctx = null` and creates nothing; the `AudioContext` and
|
||||
every node appear only in `init()`, reached only via `resume()`. So a presentation
|
||||
instance that never engages transport never creates an audio graph. The dangerous
|
||||
paths are the ones that *call* `resume()` or `prepareExperience()`.
|
||||
|
||||
| Subsystem | Authoritative console | Observation surface | Note |
|
||||
| --- | --- | --- | --- |
|
||||
| `AudioManager` / `AudioContext` | **Yes** | Constructed, **never `init()`/`resume()`** | Needed only to satisfy `ObservationEngine`'s constructor arity. |
|
||||
| Hull / Warp / LifeSupport / Telemetry / Alert / Whataverse / Expanded / EngineTransition synths | **Yes** | Constructed, silent | Same reason; they create nodes only on play. |
|
||||
| `FuturisticNoiceCancellation` | **Yes** | Constructed, inert | Do not call `setEnabled`/`setLevel` locally. |
|
||||
| `XZBTGenerativeExperience` (WebLLM + Kokoro) | **Yes** | **Not constructed. Not bound. Never prepared.** | `enterObservation()` calls `prepareExperience()` — **this call must be skipped in presentation mode**, or every opened pane downloads a 1.7B model. Highest-value guard in this design. |
|
||||
| `XZBTContractAdapter` + window bridge | **Yes — the one session** | **Not constructed** | Guarantees no second contract session. |
|
||||
| `XZBTControlBus` | **Yes** | Not constructed | |
|
||||
| `StarshipVisualizer` (spectrum, warp core) | **Yes** | Not constructed | Its canvases are console-panel only. |
|
||||
| `CoreAnimations` | **Yes** | Not constructed | |
|
||||
| Sleep timer | **Yes** | Not constructed | |
|
||||
| Hotkey handler (`window keydown`) | **Yes** | **Not installed** | A keystroke on the pane must not mutate state; see §7. |
|
||||
| Ambient activity scheduler (`scheduleObservationAmbientActivity`) | **Yes — authority only** | **Disabled**; renders broadcast events | Prevents duplicate random event generation. |
|
||||
| `triggerObservationActivity` | Decides + broadcasts | Renders on receipt | |
|
||||
| AI announcement / ticker text | **Yes** (`onGenerated`) | Renders relayed text | Relayed as `presentation/ticker`; announcement text is not contract state. |
|
||||
| `ObservationEngine` (canvas, bezels, waveform) | Yes (when overlay active) | **Yes — renderer only** | `am.analyser` guards already exist (`observation-engine.js:2262, 3097`). |
|
||||
| Observation SVG scene, spline morphs, scene entity loop | Yes (when overlay active) | **Yes — renderer only** | Deterministic from universe + preset + activity. |
|
||||
| Live instrumentation digit churn (`observationInstrumentTick`) | Yes | **Yes — local, deliberately** | See below. |
|
||||
| `ObservationBezels` | Yes | Yes | Pure SVG generation. |
|
||||
| Web3D / external visual deps | None exist | None | SciFi is canvas + SVG only. |
|
||||
|
||||
### Two judgment calls, stated explicitly
|
||||
|
||||
**1. Instrumentation digit churn stays local.** `observationInstrumentTick` mutates
|
||||
cosmetic numerals in the HUD text. It is high-frequency, carries no narrative or
|
||||
contract meaning, and mirroring it would need a continuous high-rate channel for
|
||||
nothing. Two screens showing different filler digits is invisible. This is a
|
||||
deliberate, bounded divergence — and it is the *only* one.
|
||||
|
||||
**2. Transient activity events are mirrored, not regenerated.** By contrast,
|
||||
`triggerObservationActivity` spawns visible narrative beats (contacts, expanding
|
||||
rings, text cards). Regenerating them independently would give two screens
|
||||
different events and would violate the brief's "no duplicate random event
|
||||
generation". The authority decides; the surface renders.
|
||||
|
||||
### The audience-scheduler correction (mandatory)
|
||||
|
||||
`triggerObservationActivity` begins `if (!observationActive …) return;` and
|
||||
`scheduleObservationAmbientActivity` refuses to schedule unless `observationActive`.
|
||||
So with the console in normal mode and only the NGN Observation pane open,
|
||||
**nothing would ever be generated** — the surface would render a static scene.
|
||||
6.7B must decouple the scheduler from local overlay visibility:
|
||||
|
||||
```js
|
||||
const observationAudienceActive = () => observationActive || surfaceOwner.attachedCount() > 0;
|
||||
```
|
||||
|
||||
Use that predicate for *scheduling and deciding* activity; keep `observationActive`
|
||||
for *local rendering* only. The activity decision is then broadcast, and the
|
||||
console renders it locally only when its own overlay is up.
|
||||
|
||||
### Audio-reactive fidelity (accepted limitation)
|
||||
|
||||
With no audio graph, `am.analyser` is null on the surface, so the waveform sill
|
||||
and the audio-reactive modulation read zero. `ObservationEngine` already guards
|
||||
this and degrades to a flat trace; the scene itself is procedural and unaffected.
|
||||
Mirroring a scalar audio level is a reasonable future refinement and is **out of
|
||||
scope for 6.7**.
|
||||
|
||||
---
|
||||
|
||||
## 7. Observation interaction policy
|
||||
|
||||
**Policy: minimally interactive — every retained control is a mutation request to
|
||||
the authority; nothing is applied locally.**
|
||||
|
||||
Strict read-only would be a regression (the dock already exists and is genuinely
|
||||
useful on a second screen). Fully interactive would mean installing hotkeys and
|
||||
local setters on a non-authoritative document. The middle is both correct and
|
||||
already half-built: four of the six dock controls *already* route through
|
||||
`observationEngine.onMutation`.
|
||||
|
||||
| Control (`index.html`) | Decision | Mechanism |
|
||||
| --- | --- | --- |
|
||||
| `#obs-btn-warp` (WARP) | **Route** | Needs a new target — `view.warp-flight`. Today `toggleWarpFlight()` mutates engine-local state and would diverge. |
|
||||
| `#obs-btn-frame` (VIEWPORT) | **Route** | Existing `view.viewport-frame`; already routed. |
|
||||
| `#obs-btn-pillars` (PILLARS) | **Route** | Needs a new target — `view.pillars`. Today `togglePillars()` is engine-local and would diverge. |
|
||||
| `#obs-btn-alert` (RED ALERT) | **Route** | Existing `alert.active`; already routed. |
|
||||
| `#obs-select-preset` (PRESET) | **Route** | Existing `preset.selected`; already routed. |
|
||||
| `#obs-btn-exit` (RETURN ✕) | **Remove** on the surface | Hidden by CSS. Closing a pane is NGN's job; posting `view.observation=false` from the pane would toggle the *console's* overlay — a confusing cross-surface side effect. |
|
||||
| Background click-to-exit (app.js:4508) | **Remove** on the surface | Same reason; also makes the pane fragile to stray clicks. |
|
||||
| `#observation-return-pill` | **Remove** on the surface | Same. |
|
||||
| HUD HOLD (`#toggle-observation-hud`) | **Surface-local** | A per-screen fade preference, stored only in the checkbox. §31.7 explicitly leaves purely presentational surface-local interaction outside the contract. |
|
||||
| ACTIVITY slider (console panel) | Console-only | Existing `view.activity`; the surface follows it. |
|
||||
| Hotkeys | **Not installed** on the surface | Would be a local mutation path. |
|
||||
|
||||
Removal is by CSS on the body class, so `observation-engine.js` needs no change —
|
||||
it may keep binding listeners to elements the user can never reach.
|
||||
|
||||
---
|
||||
|
||||
## 8. Observation lifecycle semantics
|
||||
|
||||
Four concepts, deliberately kept distinct:
|
||||
|
||||
| Concept | Owner | Meaning | Changed by |
|
||||
| --- | --- | --- | --- |
|
||||
| **Surface lifecycle** | NGN (`LocalSurfaces`) | Whether an Observation pane exists and is loaded | Operator clicking Open/Close/Reload in NGN |
|
||||
| **`view.observation`** | SciFi console instance | Whether the **console page's own** full-screen overlay is up | WATCH EXPERIENCE, background click, Escape, contract `set` |
|
||||
| **Fullscreen state** | Browser | Whether a document occupies the screen | User/OS; SciFi does not drive it |
|
||||
| **Local overlay CSS state** | Each document | `.active` on `#observation-overlay` | Derived from the two above, per document |
|
||||
|
||||
### The explicit answer
|
||||
|
||||
> **Opening the secondary surface does NOT imply `view.observation = true`.**
|
||||
> "A surface exists" and "Observation mode is active" are separate facts.
|
||||
|
||||
Rationale: `view.observation` is declared `restorable: false` and is scoped to the
|
||||
console's own presentation. If opening the pane set it true, the operator's console
|
||||
would black out into its overlay the instant they opened the pane — destroying
|
||||
simultaneous control and observation, the whole point of §31.1. Conversely,
|
||||
closing the pane must not set it false, or an operator using the console overlay
|
||||
would lose it when tidying up NGN panes. Contract 5.3 is additive; no existing
|
||||
target may be reinterpreted (`AGENTS.md`), and this keeps `view.observation`'s
|
||||
meaning exactly as it is today.
|
||||
|
||||
Consequences, all of which 6.7B must honor:
|
||||
|
||||
- The Observation **surface renders the scene whenever it is open**, with no state
|
||||
flag gating it. It is an Observation surface; that is what it is for.
|
||||
- **WATCH EXPERIENCE is unchanged** — same button, same target, same local overlay.
|
||||
- A host `set view.observation true` still raises the *console's* overlay and is
|
||||
reflected on the surface only as a state readout. It never opens or closes a pane;
|
||||
NGN owns pane lifecycle and Contract 5.3 gives an exhibit no way to ask for one.
|
||||
- Closing the pane detaches cleanly: `window.__xzbtSurfaceDispose` (called
|
||||
synchronously by `local-surfaces.js` before frame removal) plus a `beforeunload`
|
||||
fallback, both idempotent. The owner removes the participant; **no state mutates.**
|
||||
- When the last participant detaches and the console overlay is down,
|
||||
`observationAudienceActive()` goes false and the ambient scheduler stops — the
|
||||
symmetric counterpart of §6's correction.
|
||||
|
||||
---
|
||||
|
||||
## 9. Standalone behavior
|
||||
|
||||
Unconditionally preserved, and mostly by construction.
|
||||
|
||||
- **`index.html` opened with no query string is byte-for-byte today's application.**
|
||||
The mode resolver returns `console` for the absence of `?surface=`, and the
|
||||
console path is the existing code path. No behavior is gated on NGN.
|
||||
- **Host attachment is not part of startup.** It is not today (the adapter only
|
||||
installs a passive `message` listener) and must not become so. No `hello` is
|
||||
ever initiated by SciFi.
|
||||
- **In-page Observation is retained exactly.** WATCH EXPERIENCE, Escape,
|
||||
background click, the dock, and the hotkeys all keep working with no NGN, no
|
||||
surface, and no bus participant.
|
||||
- **The renderer is shared, not duplicated.** Both modes run the same
|
||||
`app.js`/`observation-engine.js` code; only the boot mode differs. There is no
|
||||
second implementation to drift.
|
||||
- **`?surface=observation` is directly usable without NGN** (§31.8's SHOULD): open
|
||||
it in a second browser window over `http(s)` and it attaches to the console tab
|
||||
on the same origin. With no console open it shows a waiting state and never
|
||||
invents its own authority — Museum's proven behavior.
|
||||
- **Single-file build:** opening `SciFiAmbientDisplay_V<commit>.html` normally is
|
||||
unchanged. Under `file://`, `BroadcastChannel` is scoped to an opaque origin, so
|
||||
the two-window pairing is **not** promised there; the single-file build's
|
||||
Observation remains the in-page overlay. That is the correct guarantee, and it is
|
||||
exactly what §31.8 requires (primary unconditional, non-primary best-effort).
|
||||
|
||||
---
|
||||
|
||||
## 10. Packaging approach
|
||||
|
||||
**No second application, no second document, no build change.**
|
||||
|
||||
| Concern | Impact |
|
||||
| --- | --- |
|
||||
| Source files | +2 small JS files (`js/surface-mode.js`, `js/surface-bus.js`); edits to `index.html`, `js/app.js`, `js/contract-adapter.js`, `css/style.css`. |
|
||||
| Generated HTML | Still one file, `dist/SciFiAmbientDisplay_V<commit>.html`. |
|
||||
| Optional secondary page | **None. Deliberately.** The Observation surface is a query-string view of the same document (§31.4). |
|
||||
| Shared scripts | 100% shared — the surface *is* the same script set. Divergence is structurally impossible. |
|
||||
| Build process | `tools/package.ps1` unchanged. The two new tags are inlined automatically **only if written in the exact form** `<script src="js/surface-mode.js"></script>` — the regex is whitespace- and quote-sensitive and fails silently otherwise (`agents.md`, "Adding a new JS file"). |
|
||||
| Offline behavior | Unchanged. Nothing new touches the network. `BroadcastChannel` is a browser primitive with no dependency. |
|
||||
| Future embedded offline package | Unaffected — still one artifact. |
|
||||
| Resource duplication | The surface re-parses the same cached bytes and constructs the inert subsystem shells; it downloads nothing new and runs no second engine. |
|
||||
|
||||
Script order in `index.html` — insert **after `control-bus.js`, before
|
||||
`generative-experience.js`**, so the adapter and `app.js` can see both globals:
|
||||
|
||||
```
|
||||
audio.js → config.js → core-animations.js → visualizer.js →
|
||||
observation-bezels.js → observation-engine.js → control-bus.js →
|
||||
surface-mode.js → surface-bus.js →
|
||||
generative-experience.js → contract-adapter.js → app.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Contract adapter changes
|
||||
|
||||
All in `js/contract-adapter.js`. Additive only. No change to the handshake, the
|
||||
envelope, message types, or generic semantics.
|
||||
|
||||
**1. Surface catalog.** Accept `options.surfaces` (an array) and
|
||||
`options.instanceId`; store them. Emit `surfaces` from `describe()` **only when
|
||||
non-empty**, so absence stays §31.3 form 1:
|
||||
|
||||
```js
|
||||
describe() {
|
||||
const description = { exhibit: {...}, contract: {...}, registryRevision, stateRevision,
|
||||
capabilities: [...], targets: [...] };
|
||||
if (this.surfaces && this.surfaces.length) description.surfaces = this.surfaces;
|
||||
return description;
|
||||
}
|
||||
```
|
||||
|
||||
The array is a plain static declaration with exactly one `primary: true`.
|
||||
Host-side §31.3 validation is NGN's job, not the exhibit's; do not reimplement it.
|
||||
|
||||
**2. Two missing absolute state targets.** Required for deterministic presentation
|
||||
sync — without them, PILLARS and WARP diverge between console and surface, and a
|
||||
host cannot read or restore them at all:
|
||||
|
||||
```js
|
||||
reg({ id: 'view.pillars', legacyId: 'observation-pillars',
|
||||
label: 'Window Pillars / Mullions', kind: 'state', valueType: 'boolean',
|
||||
readable: true, writable: true, restorable: true,
|
||||
category: 'observation', requires: ['observation'] });
|
||||
|
||||
reg({ id: 'view.warp-flight',
|
||||
label: 'Warp Flight Mode', kind: 'state', valueType: 'boolean',
|
||||
readable: true, writable: true, restorable: true,
|
||||
category: 'observation', requires: ['observation'] });
|
||||
```
|
||||
|
||||
Permitted by §28.7 (additive targets). Wire matching entries in
|
||||
`bindings.setters` (→ `observationEngine.setPillars` / `setWarpFlight`) and in
|
||||
`getContractStateSnapshot()` (→ `observationEngine.showPillars` and the engine's
|
||||
warp-flight field — confirm its exact name when implementing). `registryRevision`
|
||||
stays `1`; this is a new build, not a runtime registry change, so no
|
||||
`registry.changed` is emitted.
|
||||
|
||||
**3. Session-independent local hooks.** Add `onLocalChange` / `onLocalAction` as
|
||||
described in §5.4. This is the only structural addition and it changes no
|
||||
contract-visible behavior.
|
||||
|
||||
**4. Identity.** `app.js:126` passes `version: '5.2.0'`; bump to `'5.3.0'` to
|
||||
match the adapter's declared `contractMinor = 3` and this build's surface support.
|
||||
Advisory metadata only.
|
||||
|
||||
**No second command protocol.** Surface-originated interactions reuse `set` /
|
||||
`invoke` semantics through `applyMutation` / `invokeAction`. **No capability
|
||||
change** — `observation` is already declared `ready`.
|
||||
|
||||
---
|
||||
|
||||
## 12. NGN changes
|
||||
|
||||
### None.
|
||||
|
||||
Verified against the current source rather than assumed:
|
||||
|
||||
- **Query-string surface URLs already work.** `src/surface-url.js` accepts a
|
||||
relative reference with a query and rejects only schemes, protocol-relative and
|
||||
cross-origin values. `tests/local-surfaces.test.js:153` is literally
|
||||
*"a separate primary page and query/fragment views are rendered from generic
|
||||
descriptors"* — the case SciFi needs is already covered.
|
||||
- **The resource check passes.** `server/serve.js` routes on `url.pathname` and
|
||||
ignores the query, so `HEAD index.html?surface=observation` returns 200 and
|
||||
`checkSurfaceResource` is satisfied.
|
||||
- **Primary reuse already works.** `src/local-surfaces.js:35` compares full URLs
|
||||
and marks the matching primary as the control pane, so it is never re-opened.
|
||||
`tests/local-surfaces.test.js:54` covers it with Museum's real descriptors.
|
||||
- **Lifecycle already works.** `release()` calls `__xzbtSurfaceDispose` synchronously
|
||||
before removing a frame (`src/local-surfaces.js:51`); SciFi implements the hook.
|
||||
- **Catalog validation already works.** `host.js:154` runs `validateSurfaceCatalog`
|
||||
against §31.3 ordering, exercised by `tests/museum-gallery.test.js:172–253`.
|
||||
|
||||
If a genuine NGN gap emerges during 6.7B, it must be fixed **generically** and
|
||||
justified against Museum Gallery; a SciFi-specific branch is not an acceptable
|
||||
outcome (`AGENTS.md`: "Do not infer XZBT-NGN behavior from SciFi-XZBT source code").
|
||||
|
||||
---
|
||||
|
||||
## 13. File-by-file implementation plan
|
||||
|
||||
### A. SciFi-XZBT — `G:/.vibe/SciFi-XZBT` (the real exhibit)
|
||||
|
||||
| # | File | Why it changes | Responsibility added | Must not change |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| A1 | `js/surface-mode.js` **(new, ~60 lines)** | A pure, testable seam for mode resolution and the catalog | `XZBTSurfaceMode.resolve(locationLike) → { mode, instanceId }`; `XZBTSurfaceMode.newInstanceId()`; `XZBTSurfaceMode.SURFACES(instanceId)`; `XZBTSurfaceMode.channelName(instanceId)` | No DOM, no globals beyond `window.XZBTSurfaceMode`, no imports. Unknown `?surface=` values resolve to `console`. |
|
||||
| A2 | `js/surface-bus.js` **(new, ~180 lines)** | Exhibit-internal state mirror | `createOwner(adapter, { channelName, onParticipantsChanged })` and `attach({ channelName, timeoutMs, onSnapshot, onChange, onAction, onPresentation, onTimeout })`, plus idempotent `detach()` and `mutate(kind, target, valueOrArgs)` | Carries no XZBT envelope. `Set`-based participant bookkeeping (not a counter). Owner routes every `mutate` through `adapter.applyMutation/invokeAction` with source `'ui'` — never a second computation of state. |
|
||||
| A3 | `index.html` | Load the two new files | Two `<script src>` tags in the exact `package.ps1` form, positioned per §10 | **No DOM changes.** The overlay markup, dock and all IDs stay exactly as they are — the design depends on them existing in both modes. |
|
||||
| A4 | `js/contract-adapter.js` | Advertise surfaces; close the two target gaps; expose session-independent hooks | §11 items 1–4 | Handshake, envelope, `xzbt` advisory handling, inbound gating, error codes, `_emitEvent`'s session gate, existing target IDs and semantics. |
|
||||
| A5 | `js/app.js` | Mode branch, owner/mirror wiring, audience scheduler | (a) resolve mode at the top; add `body.surface-observation` in presentation mode. (b) **Presentation branch skips:** `XZBTGenerativeExperience` construction/bind, `StarshipVisualizer`, `CoreAnimations`, `XZBTControlBus`, `XZBTContractAdapter`, the `keydown` handler, the sleep timer, and all console-panel listeners. (c) **`enterObservation()` must not call `prepareExperience()` in presentation mode.** (d) Presentation enters the scene on snapshot and re-runs `refreshObservation()` on universe/preset/activity change. (e) Replace `observationActive` with `observationAudienceActive()` for **scheduling/deciding** activity only. (f) Broadcast activity decisions and ticker text; presentation renders them and runs no scheduler. (g) Presentation dock routes through `bus.mutate`. (h) `window.__xzbtSurfaceDispose = () => link.detach()` + `beforeunload`. (i) Console mode constructs the bus owner and feeds it from `onLocalChange`/`onLocalAction`. | The console code path's behavior. `getContractStateSnapshot()`'s shape (extend with the two new keys only). The single mutation chokepoint. `agents.md`: surgical scoped edits, no reformatting. |
|
||||
| A6 | `css/style.css` | Present the surface mode | `body.surface-observation`: hide `header/main/footer`, show the overlay full-bleed, hide `#obs-btn-exit` and `#observation-return-pill` | Existing theme, overlay and CRT rules. Additive block only. |
|
||||
| A7 | `agents.md` | Keep the agent contract honest | Document the one-document/two-mode rule, the two new files, the bus, and the "never construct audio/AI in presentation mode" invariant | The hard constraints; they are all still satisfied. |
|
||||
| A8 | `README.md` | User-facing | One short paragraph on the Observation surface | — |
|
||||
|
||||
**Deliberately unchanged:** `js/observation-engine.js`, `js/observation-bezels.js`,
|
||||
`js/audio.js`, `js/config.js`, `js/visualizer.js`, `js/core-animations.js`,
|
||||
`js/generative-experience.js`, `js/control-bus.js`, `tools/package.ps1`, `dist/**`
|
||||
(regenerate, never edit).
|
||||
|
||||
### B. XZBT-NGN — `G:/.vibe/XZBT-NGN`
|
||||
|
||||
| # | File | Why it changes | Responsibility added | Must not change |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| B1 | `test-fixtures/reference-exhibits/scifi/**` | Fixture must match the new exhibit | Mechanical resync of the changed files + the two new ones | Nothing hand-edited; keep it a faithful copy. |
|
||||
| B2 | `test-fixtures/PROVENANCE.md` | Provenance discipline | Note the resync, its source commit, and that no fixture-local corrections were added | The existing correction record. |
|
||||
| B3 | `tests/scifi-surfaces.test.js` **(new)** | Cover the new seams | §14 tests, in the existing `node:test` + `vm.createContext` + real `BroadcastChannel` style (`tests/museum-gallery.test.js`) | — |
|
||||
| B4 | `tests/local-surfaces.test.js` | Prove the real SciFi descriptors render correctly | One case using SciFi's actual catalog, mirroring the Museum cases at :54 and :153 | Existing cases. |
|
||||
| B5 | `docs/reference/XZBT-NGN-Step6.7-SciFi-Surface-Adaptation.md` **(new)** | Verification writeup | Evidence for 6.7B, matching the Step 6.6 document's shape | — |
|
||||
| B6 | `README.md` | Operator accuracy | Note that NGN should be pointed at the explicit `…/scifi/index.html` URL (risk R1) | — |
|
||||
|
||||
**`src/**` — no changes.** If a diff appears there in 6.7B, it is a design
|
||||
deviation and needs review before merge.
|
||||
|
||||
---
|
||||
|
||||
## 14. Automated test plan for Step 6.7B
|
||||
|
||||
`node --test`, matching the existing style: `vm.createContext` per simulated
|
||||
document, Node's real `BroadcastChannel`, `plain()` for cross-realm comparison,
|
||||
and every channel closed in teardown. `app.js` is far too DOM-bound for `vm`;
|
||||
that is why A1/A2 are deliberately small, pure and separately loadable, and why
|
||||
the app-level invariants below are asserted as **source-structure tests** — the
|
||||
same technique as Museum's "Exactly one Exhibit State Core exists".
|
||||
|
||||
**Catalog and contract (`tests/scifi-surfaces.test.js`)**
|
||||
|
||||
1. `describe()` includes a `surfaces` array that passes `validateSurfaceCatalog`
|
||||
against a representative base URL.
|
||||
2. Exactly one entry has `primary: true`.
|
||||
3. Every entry carries only §31.2 fields; no invented fields; `kind === 'surface'`.
|
||||
4. Constructing the adapter with no `surfaces` option omits the field entirely
|
||||
(§31.3 form 1 preserved — the Museum analogue at `museum-gallery.test.js:148`).
|
||||
5. `resolveSurfaceURL('?surface=observation&xi=…', base)` resolves same-origin to
|
||||
`base?…`; absolute/protocol-relative variants are rejected.
|
||||
6. The primary's `url` resolves to exactly a base of `…/scifi/index.html`, so
|
||||
`LocalSurfaces` marks it `control` and opens no second frame (extend
|
||||
`tests/local-surfaces.test.js`).
|
||||
7. `view.pillars` and `view.warp-flight` exist, are `state`/`boolean`, readable,
|
||||
writable, and appear in the snapshot key set.
|
||||
|
||||
**Mode resolution (pure)**
|
||||
|
||||
8. No query → `console`. `?surface=observation` → `observation`.
|
||||
`?surface=bogus` → `console`. Query order and extra params are tolerated.
|
||||
9. `xi` round-trips through `SURFACES()` → `resolve()` and `channelName()` is
|
||||
instance-scoped (two ids yield different names).
|
||||
|
||||
**Bus / synchronization**
|
||||
|
||||
10. **Late join:** mutate first, then attach — the snapshot carries current
|
||||
universe, preset and view state, plus the current `stateRevision`.
|
||||
11. **Propagation:** an authority mutation reaches an attached surface as one
|
||||
`state`/`selection` message with the post-mutation value.
|
||||
12. **Mutation routing:** `link.mutate('set','view.pillars',true)` results in
|
||||
exactly one `adapter.applyMutation` call with source `'ui'`, and the value
|
||||
converges on the authority and on a *second* attached surface.
|
||||
13. **No second authority:** a surface-side `mutate` performs no local state write;
|
||||
with no owner present it is a no-op and the surface enters the waiting state
|
||||
(the analogue of `museum-gallery.test.js:469`).
|
||||
14. **One event stream:** `stateRevision` advances monotonically regardless of
|
||||
which surface originated the interaction; two attached surfaces never disagree.
|
||||
15. **Reload:** re-attaching with a fresh `participantId` yields the current
|
||||
snapshot; repeated reloads leave the participant count stable (no ratchet).
|
||||
16. **Detach:** idempotent; never mutates state; count decrements exactly once;
|
||||
duplicate/late `detach` cannot underflow.
|
||||
17. **Session independence:** with `sessionActive === false`, `onLocalChange` still
|
||||
fires on `_commitChanges` — the correction in §5.4 — while `_emitEvent` still
|
||||
emits nothing.
|
||||
|
||||
**Duplicate-subsystem prevention (construction spies + source structure)**
|
||||
|
||||
18. *Spy test:* load `surface-bus.js` in a `vm` context with stub globals and
|
||||
assert the surface side never touches `AudioContext`, `fetch` or `import` —
|
||||
a counting stub for `globalThis.AudioContext` must record zero constructions.
|
||||
19. *Source-structure test:* assert `js/app.js` contains no call to
|
||||
`prepareExperience()` that is not inside the console-mode branch — pin it by
|
||||
asserting the presentation branch is entered before the
|
||||
`new XZBTGenerativeExperience` / `new XZBTContractAdapter` /
|
||||
`new StarshipVisualizer` construction sites, in the manner of Museum's
|
||||
"structurally incapable" assertion. Prefer a resilient marker (e.g. a named
|
||||
`bootConsole()` function containing those constructions) over brittle regex —
|
||||
**A5 should be implemented so this test is easy to write.**
|
||||
20. *Timer test:* with only a presentation instance simulated, no ambient-activity
|
||||
timer is scheduled.
|
||||
21. **One contract session:** the presentation branch never installs a `message`
|
||||
listener; a simulated `hello` posted at it produces no `hello.result`.
|
||||
|
||||
**Regression**
|
||||
|
||||
22. `tests/museum-gallery.test.js`, `tests/local-surfaces.test.js`,
|
||||
`tests/surface-validation.test.js`, `tests/host.test.js`,
|
||||
`tests/connection.test.js`, `tests/postmessage-interop.test.js`,
|
||||
`tests/haunted-house.test.js` all remain green (`npm test`).
|
||||
23. `tests/postmessage-interop.test.js:118` still negotiates a SciFi session
|
||||
against the resynced fixture.
|
||||
|
||||
---
|
||||
|
||||
## 15. Live acceptance procedure
|
||||
|
||||
Serve with `npm start` (`http://127.0.0.1:4173`).
|
||||
|
||||
1. Open NGN at `http://127.0.0.1:4173/` and connect to
|
||||
**`http://127.0.0.1:4173/test-fixtures/reference-exhibits/scifi/index.html`**
|
||||
(the explicit filename — see R1).
|
||||
2. Confirm `connected`, Contract major 5 / minor 3, target catalog populated.
|
||||
3. In the Surfaces panel: exactly two entries. `surface.console` is badged
|
||||
**Primary** and offers **no Open button** (it is the control pane).
|
||||
`surface.observation` is non-primary with `URL: ?surface=observation&xi=…`.
|
||||
4. Open `surface.observation`. It loads in its own pane and renders the current
|
||||
universe and preset within ~1s.
|
||||
5. Confirm the pane shows **no RETURN ✕ and no return pill**, and that clicking
|
||||
its background does nothing.
|
||||
6. Confirm the console pane is **still the full console** — `view.observation`
|
||||
remains `false` and the console did not black out. *(This is the §8 invariant.)*
|
||||
7. From NGN, `set universe.selected` to another universe. Both the console and the
|
||||
Observation pane change scene. Repeat for `preset.selected`, `alert.active`,
|
||||
`view.viewport-frame`, `view.activity`.
|
||||
8. On the Observation pane, click **PILLARS** and **WARP**. The value changes on
|
||||
the pane *and* on the console's own controls, and NGN's state readout advances
|
||||
`stateRevision` — proving the mutation went through the authority, not locally.
|
||||
9. Click **RED ALERT** and change **PRESET** on the pane; confirm the same
|
||||
convergence across console, pane and NGN.
|
||||
10. Reload the Observation pane from NGN. It returns to the *current* state, not a
|
||||
default one.
|
||||
11. Close the pane, then reopen it. Same result; the participant count in the
|
||||
console does not ratchet.
|
||||
12. On the console, press **WATCH EXPERIENCE**. The console overlay comes up,
|
||||
`view.observation` becomes `true`, and the Observation pane is **unaffected**
|
||||
(it was already rendering).
|
||||
13. Exit the console overlay. `view.observation` returns to `false`; the pane is
|
||||
still rendering. Repeat once.
|
||||
14. **Duplication checks, with the pane open and the console engaged (audio on):**
|
||||
- DevTools → the pane's frame → console shows no WebLLM/Kokoro network
|
||||
activity, and `window.generativeExperience` is `undefined`.
|
||||
- The pane's frame has no `AudioContext` (audio comes only from the console);
|
||||
mute the console and the room goes fully silent.
|
||||
- Transient activity events (contacts, rings, cards) appear on console overlay
|
||||
and pane **at the same moment and as the same events**, not independently.
|
||||
- An AI announcement appears once, on both tickers, with identical text.
|
||||
15. Disconnect NGN. The pane is released and the console continues standalone.
|
||||
Reconnect; surfaces rediscover and the pane reopens cleanly.
|
||||
16. **Standalone, no NGN:** open
|
||||
`…/test-fixtures/reference-exhibits/scifi/index.html` directly. Full console,
|
||||
clean browser console, WATCH EXPERIENCE works, hotkeys work, audio engages.
|
||||
17. **Standalone two-window:** with that tab open, open
|
||||
`…/index.html?surface=observation&xi=<id from the console's describe()>` in a
|
||||
second window. It attaches and mirrors with no NGN present.
|
||||
18. Open `…/index.html?surface=observation&xi=nonexistent` alone. It shows a
|
||||
waiting state and does **not** invent its own state or start audio.
|
||||
19. Re-run `tools/package.ps1`; open the new `dist/` file **with the network
|
||||
disabled**. Console, Observation overlay, audio and hotkeys all work; the
|
||||
Generative Experience panel degrades quietly. *(The packaged build's
|
||||
Observation is the in-page overlay; two-window pairing is not promised under
|
||||
`file://` — §9.)*
|
||||
|
||||
---
|
||||
|
||||
## 16. Risks and mitigations
|
||||
|
||||
**R1 — A directory-style base URL causes the primary to open a second full
|
||||
instance.** `LocalSurfaces` uses full-URL equality. If the operator connects to
|
||||
`…/scifi/` rather than `…/scifi/index.html`, `exhibitBaseUrl` ends in `/`,
|
||||
`index.html` no longer equals it, and NGN offers to open the primary as a pane —
|
||||
which would load a second complete SciFi app.
|
||||
*Impact:* bounded. That instance gets no transport from NGN and its window bridge
|
||||
only answers parent/opener/self, so **no second contract session** is created, and
|
||||
it creates no `AudioContext` until someone engages it. It is wasted, not wrong.
|
||||
*Mitigation:* document the explicit-filename URL in `README.md` (B6) and in the
|
||||
6.7B verification doc; it is already the documented URL. Do **not** "fix" this in
|
||||
NGN — full-URL equality is deliberate (queries and fragments designate other
|
||||
views), and Museum has the same property. Revisit generically in a later step if
|
||||
it proves to bite operators.
|
||||
|
||||
**R2 — The `prepareExperience()` guard is missed.** `enterObservation()` calls it
|
||||
unconditionally today; a presentation instance that reaches that line downloads a
|
||||
1.7B model per pane.
|
||||
*Mitigation:* the single most important guard in A5(c); covered by tests 18–19 and
|
||||
acceptance step 14. Structure A5 so the construction sites sit in a named
|
||||
console-only function, making the assertion cheap and durable.
|
||||
|
||||
**R3 — The ambient scheduler silently never runs.** Both
|
||||
`triggerObservationActivity` and `scheduleObservationAmbientActivity` early-return
|
||||
on `!observationActive`; with the console in normal mode, an open pane would show
|
||||
a static scene and the defect would look like "the surface is just quiet".
|
||||
*Mitigation:* the `observationAudienceActive()` correction (§6), test 20, and
|
||||
acceptance step 14's "same moment, same events" check, which fails loudly if
|
||||
nothing is generated.
|
||||
|
||||
**R4 — Mode branching damages the console path.** `app.js` is 4,703 lines of
|
||||
tightly coupled closure; a badly placed branch could disable console features.
|
||||
*Mitigation:* branch by *skipping construction* in presentation mode rather than
|
||||
by adding conditionals through the console path; `agents.md`'s surgical-edit rule;
|
||||
acceptance steps 16–19 exercise standalone fully; keep the diff reviewable.
|
||||
|
||||
**R5 — BroadcastChannel cross-talk between instances.** Two console tabs on one
|
||||
origin would share a fixed channel name.
|
||||
*Mitigation:* the instance-scoped channel name (§5.2), tests 9 and 18.
|
||||
|
||||
**R6 — Fixture drift between SciFi-XZBT and the NGN copy.** Two trees, one
|
||||
mechanical copy, and both currently carry uncommitted work.
|
||||
*Mitigation:* resync as one discrete step (B1), record it in `PROVENANCE.md` (B2),
|
||||
and hash-verify as Step 5 did (`test-fixtures/evidence/step5-scifi-files.json`).
|
||||
|
||||
**R7 — `package.ps1`'s silent inlining failure.** A script tag with different
|
||||
quoting, extra attributes or self-closing form is left as an external reference and
|
||||
the standalone build breaks with no error.
|
||||
*Mitigation:* A3 specifies the exact form; acceptance step 19 opens the packaged
|
||||
build offline, which is the only test that catches it.
|
||||
|
||||
**R8 — Audio-reactive visuals are flat on the surface.** No analyser without an
|
||||
audio graph.
|
||||
*Mitigation:* accepted and documented (§6). `ObservationEngine` already guards it.
|
||||
Mirroring a scalar level is deferred; do not solve it in 6.7B.
|
||||
|
||||
**Explicitly not risks:** a second contract session (structurally prevented twice
|
||||
over); NGN genericity (no NGN change); loss of standalone behavior (the console
|
||||
path is untouched); packaging fork (one document by construction).
|
||||
|
||||
---
|
||||
|
||||
## 17. Step 6.7B scope estimate
|
||||
|
||||
### Moderate.
|
||||
|
||||
Not *small*: the work spans two repositories, touches the largest and most
|
||||
sensitive file in SciFi-XZBT (`app.js`), adds two contract targets and a catalog,
|
||||
introduces two new source files and a new synchronization mechanism, and needs a
|
||||
fixture resync plus two test files and a verification document. Three of the
|
||||
corrections it depends on — the `_emitEvent` session gate, the
|
||||
`prepareExperience()` call inside `enterObservation()`, and the
|
||||
`observationActive` gate on the ambient scheduler — are non-obvious and would each
|
||||
produce a plausible-looking but wrong implementation if missed.
|
||||
|
||||
Not *large*: the decisive cost driver was avoided. There is **no renderer
|
||||
extraction, no DOM decoupling, no second document, no build change, and no NGN
|
||||
change.** `observation-engine.js` (3,595 lines) and `audio.js` (6,889 lines) are
|
||||
untouched. The synchronization mechanism is a direct adaptation of Museum
|
||||
Gallery's already-validated `surface-bus.js`, and the NGN side was proven in Steps
|
||||
6.3–6.6.
|
||||
|
||||
Expected diff: roughly 240 new lines across two new SciFi files, ~120 modified
|
||||
lines in `app.js`, ~40 in `contract-adapter.js`, ~25 in `css/style.css`, 2 lines
|
||||
in `index.html`, plus ~350 lines of new tests and the fixture resync.
|
||||
|
||||
The risk concentrates almost entirely in A5. Implement A1 and A2 first, test them
|
||||
in isolation, and only then branch `app.js`.
|
||||
Reference in New Issue
Block a user