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:
@@ -29,3 +29,76 @@ Local compatibility corrections:
|
||||
NGN has no fixture-specific target logic. These corrections apply only to
|
||||
the local fixture copies. No upstream fixes or independent conformance
|
||||
certification are claimed.
|
||||
|
||||
## Step 6.7B resync (2026-09-15)
|
||||
|
||||
The `scifi/` fixture was resynced from the real SciFi-XZBT exhibit source at
|
||||
`G:/.vibe/SciFi-XZBT/` after that repo's Step 6.7B implementation (SciFi
|
||||
Observation Surface Integration, `docs/architecture/XZBT-NGN-Step6.7A-SciFi-Observation-Surface.md`).
|
||||
|
||||
Copied files (byte-for-byte from source, no fixture-local corrections):
|
||||
|
||||
- `index.html` — two new `<script>` tags for `surface-mode.js`/`surface-bus.js`
|
||||
- `js/app.js` — boot-mode resolution, console/observation branching, the
|
||||
surface-attachment bus wiring, `view.pillars`/`view.warp-flight` routing
|
||||
- `js/contract-adapter.js` — `surfaces`/`instanceId` constructor options,
|
||||
the `view.pillars`/`view.warp-flight` targets, `onLocalChange`/`onLocalAction`
|
||||
hooks, `describe()`'s conditional `surfaces` field, version bump to 5.3.0
|
||||
- `js/observation-engine.js` — WARP/PILLARS dock buttons now route through
|
||||
`onMutation` (matching the existing VIEWPORT/RED ALERT/PRESET pattern)
|
||||
instead of mutating engine state directly
|
||||
- `css/style.css` — additive `body.surface-observation` rules
|
||||
- `js/surface-mode.js` (new) — boot-mode resolver + surface catalog
|
||||
- `js/surface-bus.js` (new) — instance-scoped BroadcastChannel sync
|
||||
|
||||
No fixture-local corrections were added on top of this resync; the fixture
|
||||
is a faithful mechanical copy of the source as it stood after Step 6.7B.
|
||||
`js/audio.js`, `js/config.js`, `js/core-animations.js`, `js/visualizer.js`,
|
||||
`js/observation-bezels.js`, `js/generative-experience.js`, and `js/control-bus.js`
|
||||
were not touched by Step 6.7B and were not recopied. `dist/` was not
|
||||
regenerated (no PowerShell available in the environment that performed this
|
||||
resync); it still reflects a pre-6.7B build and should be rebuilt from
|
||||
source before any test or procedure that opens the packaged file.
|
||||
|
||||
## SciFi adapter handshake correction (2026-09-15)
|
||||
|
||||
Correction to the record above: the Step 6.7B resync note listed a "version
|
||||
bump to 5.3.0" for `js/contract-adapter.js`, but no such bump was present in
|
||||
the file as resynced — it still read `version = '5.2.0'` / `contractMinor = 2`.
|
||||
The adapter also still carried a pre-5.3 inbound gate in `_setupWindowBridge`:
|
||||
|
||||
```js
|
||||
if (!e.data || typeof e.data !== 'object' || e.data.xzbt !== '5.2') return;
|
||||
```
|
||||
|
||||
That is an exact-string comparison against the advisory `xzbt` field, which
|
||||
Contract 5.3 §6.5 designates as diagnostics-only metadata ("The negotiated
|
||||
session contract major/minor is authoritative after handshake"). Because
|
||||
XZBT-NGN's host stamps outgoing envelopes `xzbt: '5.3'` (Step 6.3), the gate
|
||||
silently dropped every host message — including `hello` — and NGN's handshake
|
||||
timed out against this exhibit (`tests/postmessage-interop.test.js`, four
|
||||
tests).
|
||||
|
||||
Two source-side corrections were made in `G:/.vibe/SciFi-XZBT/js/contract-adapter.js`
|
||||
and then copied here verbatim, keeping the one-directional source -> fixture
|
||||
flow this file documents:
|
||||
|
||||
1. The inbound filter now checks the advisory field's *presence and type*
|
||||
(`typeof e.data.xzbt !== 'string'`), matching how XZBT-NGN's host validates
|
||||
the same field. It no longer branches on any particular value, so the
|
||||
field cannot act as a compatibility gate for any sender.
|
||||
2. The adapter advertises the contract it actually implements: `contractMinor`
|
||||
`2` -> `3` and the exhibit product version `'5.2.0'` -> `'5.3.0'` (its
|
||||
`describe()` already emitted the Contract 5.3 `surfaces` field added by
|
||||
Step 6.7B). The file header now reads "Specification v5.3" to match.
|
||||
|
||||
The outbound advisory `xzbt: '5.2'` stamps on this adapter's own envelopes were
|
||||
deliberately left unchanged: §6.5 makes the value non-authoritative, Step 6.7B
|
||||
documented the envelope as unchanged, and no receiver — NGN included — reads
|
||||
that value for admission. `dist/` remains a stale generated artifact and was
|
||||
not regenerated.
|
||||
|
||||
Resynced file (source and fixture byte-identical,
|
||||
SHA-256 `10ED30527153D5B30F23557BA6EC427884D470AB6561AC33C47E7F17AFA28DD9`):
|
||||
|
||||
- `js/contract-adapter.js`
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
- banner:
|
||||
- heading "XZBT-NGN Exhibit Engine" [level=1]
|
||||
- paragraph: Contract 5.2/5.3 · One exhibit at a time
|
||||
- main:
|
||||
- heading "Connection" [level=2]
|
||||
- generic: Exhibit URL (same origin)
|
||||
- textbox "Exhibit URL (same origin)":
|
||||
- /placeholder: /public/exhibits/example/index.html
|
||||
- text: http://127.0.0.1:4173/test-fixtures/reference-exhibits/museum-gallery/control.html
|
||||
- button "Load exhibit"
|
||||
- button "Reconnect"
|
||||
- button "Disconnect"
|
||||
- paragraph: Enter a trusted exhibit's local served path or a URL on this origin. Load exhibit also switches exhibits. Reconnect starts a new session, or reloads the last exhibit after disconnect.
|
||||
- status: connected · synchronized
|
||||
- paragraph: Museum Gallery · 0.1.0 · Contract 5.3
|
||||
- paragraph: "Exhibit location: http://127.0.0.1:4173/test-fixtures/reference-exhibits/museum-gallery/control.html"
|
||||
- iframe:
|
||||
- banner:
|
||||
- heading "Museum Gallery — Control Room" [level=1]
|
||||
- generic: Primary surface · Contract 5.3 · standalone-capable
|
||||
- generic: host attached
|
||||
- main:
|
||||
- paragraph: The Orrery
|
||||
- paragraph: "Lighting: 60%"
|
||||
- paragraph: "Rotation: 0.4x"
|
||||
- paragraph: "Labels: ON"
|
||||
- paragraph: "Attached non-primary surfaces: 0"
|
||||
- complementary:
|
||||
- heading "Artifact" [level=2]
|
||||
- generic: Selected artifact
|
||||
- combobox "Selected artifact":
|
||||
- option "The Orrery" [selected]
|
||||
- option "Star Map"
|
||||
- option "Meteorite Fragment"
|
||||
- heading "Environment" [level=2]
|
||||
- generic: Lighting level
|
||||
- generic: 60%
|
||||
- slider "Lighting level 60%"
|
||||
- generic: Rotation speed
|
||||
- generic: 0.4x
|
||||
- slider "Rotation speed 0.4x"
|
||||
- generic: Labels enabled
|
||||
- button "Toggle" [pressed]
|
||||
- heading "Actions" [level=2]
|
||||
- button "Flash spotlight"
|
||||
- contentinfo:
|
||||
- generic: Control Room
|
||||
- generic: Control Room standing by.
|
||||
- heading "Exhibit controls (5)" [level=2]
|
||||
- generic "Capabilities"
|
||||
- paragraph: Reported values are authoritative. New values are drafts; select Set or Invoke to submit. Rediscovery resets drafts.
|
||||
- heading "gallery" [level=3]
|
||||
- article:
|
||||
- heading "Selected Artifact" [level=3]
|
||||
- code: artifact.selected
|
||||
- paragraph: selection · readable · writable
|
||||
- status: "Reported value: \"the-orrery\""
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- combobox "New value":
|
||||
- option "The Orrery" [selected]
|
||||
- option "Star Map"
|
||||
- option "Meteorite Fragment"
|
||||
- button "Set"
|
||||
- article:
|
||||
- heading "Lighting Level" [level=3]
|
||||
- code: lighting.level
|
||||
- paragraph: range · readable · writable
|
||||
- status: "Reported value: 0.6"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- spinbutton "New value": "0.6"
|
||||
- button "Set"
|
||||
- article:
|
||||
- heading "Rotation Speed" [level=3]
|
||||
- code: rotation.speed
|
||||
- paragraph: range · readable · writable
|
||||
- status: "Reported value: 0.4"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- spinbutton "New value": "0.4"
|
||||
- button "Set"
|
||||
- article:
|
||||
- heading "Labels Enabled" [level=3]
|
||||
- code: labels.enabled
|
||||
- paragraph: state · readable · writable
|
||||
- status: "Reported value: true"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- checkbox "New value" [checked]
|
||||
- button "Set"
|
||||
- heading "action" [level=3]
|
||||
- article:
|
||||
- heading "Spotlight Flash" [level=3]
|
||||
- code: action.spotlight-flash
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- heading "Presentation surfaces (3)" [level=2]
|
||||
- paragraph: Open Contract 5.3 presentation surfaces locally below. URLs resolve against the supplying exhibit. Disconnecting or switching exhibits closes these views. Remote/cast displays remain deferred.
|
||||
- article:
|
||||
- heading "Control Room" [level=3]
|
||||
- code: surface.control
|
||||
- generic: Primary
|
||||
- generic: "Role: control"
|
||||
- paragraph: "URL: control.html"
|
||||
- status: Open in the authoritative control frame. Use Connection controls to reload or disconnect.
|
||||
- button "Open" [disabled]
|
||||
- button "Reload" [disabled]
|
||||
- button "Close" [disabled]
|
||||
- article:
|
||||
- heading "Artifact Display" [level=3]
|
||||
- code: surface.artifact
|
||||
- generic: Non-primary
|
||||
- generic: "Role: ambient"
|
||||
- paragraph: "URL: artifact.html"
|
||||
- status: open
|
||||
- button "Open" [disabled]
|
||||
- button "Reload"
|
||||
- button "Close"
|
||||
- iframe:
|
||||
- banner:
|
||||
- heading "Museum Gallery — Artifact Display" [level=1]
|
||||
- generic: Non-primary surface · attaches to the Control Room
|
||||
- generic: surface
|
||||
- main:
|
||||
- paragraph: The Orrery
|
||||
- paragraph: "Lighting: 60%"
|
||||
- paragraph: "Rotation: 0.4x"
|
||||
- paragraph:
|
||||
- button "Cycle artifact"
|
||||
- paragraph: Attached to Control Room.
|
||||
- contentinfo:
|
||||
- generic: Artifact Display
|
||||
- generic: Waiting for shared state.
|
||||
- article:
|
||||
- heading "Information Wall" [level=3]
|
||||
- code: surface.info-wall
|
||||
- generic: Non-primary
|
||||
- generic: "Role: information"
|
||||
- paragraph: "URL: info-wall.html"
|
||||
- status: open
|
||||
- button "Open" [disabled]
|
||||
- button "Reload"
|
||||
- button "Close"
|
||||
- iframe:
|
||||
- banner:
|
||||
- heading "Museum Gallery — Information Wall" [level=1]
|
||||
- generic: Non-primary surface · attaches to the Control Room
|
||||
- generic: surface
|
||||
- main:
|
||||
- paragraph: The Orrery
|
||||
- paragraph: "Labels: ON"
|
||||
- paragraph:
|
||||
- button "Toggle labels"
|
||||
- paragraph: Attached to Control Room.
|
||||
- contentinfo:
|
||||
- generic: Information Wall
|
||||
- generic: Waiting for shared state.
|
||||
- heading "State and metadata" [level=2]
|
||||
- button "Refresh state"
|
||||
- generic "Session and exhibit metadata"
|
||||
- generic "Current persistent state"
|
||||
- heading "Protocol diagnostics" [level=2]
|
||||
- generic "Incoming events"
|
||||
- generic "Protocol and errors"
|
||||
@@ -0,0 +1,846 @@
|
||||
- banner:
|
||||
- heading "XZBT-NGN Exhibit Engine" [level=1]
|
||||
- paragraph: Contract 5.2/5.3 · One exhibit at a time
|
||||
- main:
|
||||
- heading "Connection" [level=2]
|
||||
- generic: Exhibit URL (same origin)
|
||||
- textbox "Exhibit URL (same origin)":
|
||||
- /placeholder: /public/exhibits/example/index.html
|
||||
- text: /test-fixtures/reference-exhibits/scifi/index.html
|
||||
- button "Load exhibit" [active]
|
||||
- button "Reconnect"
|
||||
- button "Disconnect"
|
||||
- paragraph: Enter a trusted exhibit's local served path or a URL on this origin. Load exhibit also switches exhibits. Reconnect starts a new session, or reloads the last exhibit after disconnect.
|
||||
- status: connected · synchronized
|
||||
- paragraph: SciFi-XZBT · 5.2.0 · Contract 5.3
|
||||
- paragraph: "Exhibit location: http://127.0.0.1:4173/test-fixtures/reference-exhibits/scifi/index.html"
|
||||
- iframe:
|
||||
- banner:
|
||||
- generic "Click to Change Sci-Fi Universe":
|
||||
- generic: STARFLIGHT
|
||||
- generic: ▼
|
||||
- generic: "INTERPRISE-G: MAIN BRIDGE"
|
||||
- button "MUTE"
|
||||
- generic: 75%
|
||||
- generic: STARFLIGHT SOUND MATRIX v2.9
|
||||
- main:
|
||||
- complementary:
|
||||
- generic: STARSHIP PRESETS
|
||||
- 'button "Interprise-G: Main Bridge STARFLIGHT COMMAND"':
|
||||
- generic: "Interprise-G: Main Bridge"
|
||||
- generic: STARFLIGHT COMMAND
|
||||
- 'button "Interprise-G: Main Engineering STARFLIGHT COMMAND"':
|
||||
- generic: "Interprise-G: Main Engineering"
|
||||
- generic: STARFLIGHT COMMAND
|
||||
- 'button "Interprise-G: Crew Quarters STARFLIGHT COMMAND"':
|
||||
- generic: "Interprise-G: Crew Quarters"
|
||||
- generic: STARFLIGHT COMMAND
|
||||
- 'button "USS Wayfarer: Bridge STARFLIGHT COMMAND"':
|
||||
- generic: "USS Wayfarer: Bridge"
|
||||
- generic: STARFLIGHT COMMAND
|
||||
- 'button "USS Wayfarer: Class-N Worp Core STARFLIGHT COMMAND"':
|
||||
- generic: "USS Wayfarer: Class-N Worp Core"
|
||||
- generic: STARFLIGHT COMMAND
|
||||
- 'button "USS Defiance: Tactical Bridge STARFLIGHT COMMAND"':
|
||||
- generic: "USS Defiance: Tactical Bridge"
|
||||
- generic: STARFLIGHT COMMAND
|
||||
- 'button "Deep Space 8: Ops Center STARFLIGHT COMMAND"':
|
||||
- generic: "Deep Space 8: Ops Center"
|
||||
- generic: STARFLIGHT COMMAND
|
||||
- 'button "Interprise NCC-1701.5: Bridge (TOS) STARFLIGHT COMMAND"':
|
||||
- generic: "Interprise NCC-1701.5: Bridge (TOS)"
|
||||
- generic: STARFLIGHT COMMAND
|
||||
- 'button "Interprise NCC-1701.5: Engineering (TOS) STARFLIGHT COMMAND"':
|
||||
- generic: "Interprise NCC-1701.5: Engineering (TOS)"
|
||||
- generic: STARFLIGHT COMMAND
|
||||
- 'button "Interprise NS-01: Command Bridge STARFLIGHT COMMAND"':
|
||||
- generic: "Interprise NS-01: Command Bridge"
|
||||
- generic: STARFLIGHT COMMAND
|
||||
- generic: "INTERPRISE-G: MAIN BRIDGE"
|
||||
- generic: Warm, low-frequency 50Hz hull tone with gentle ventilation and soft LCARD computer chirps.
|
||||
- generic: AUDIO ENGINE
|
||||
- generic: STANDBY
|
||||
- generic: CORE FREQ
|
||||
- generic: 58.0 Hz
|
||||
- button "▶ ENGAGE":
|
||||
- generic: ▶
|
||||
- generic: ENGAGE
|
||||
- button "WARP JUMP"
|
||||
- button "RED ALERT"
|
||||
- button "YELLOW ALERT"
|
||||
- button "WATCH EXPERIENCE"
|
||||
- generic: Full-screen procedural visuals, soundscape, telemetry, events, and optional local AI announcements.
|
||||
- 'generic "Toggle starship viewport window frame (ON: framed starship lounge / OFF: pure deep space)"':
|
||||
- generic: VIEWPORT
|
||||
- checkbox "Toggle Observation Viewport Frame" [checked]
|
||||
- generic: "ON"
|
||||
- 'generic "Toggle vertical structural window pillars / mullions (OFF: unobstructed panoramic view / ON: structural posts)"':
|
||||
- generic: PILLARS
|
||||
- checkbox "Toggle Window Pillars"
|
||||
- generic: "OFF"
|
||||
- generic "Controls how frequently and intensely Observation generates transient visual activity":
|
||||
- generic: ACTIVITY
|
||||
- slider "Observation activity level"
|
||||
- generic: 60%
|
||||
- generic "Keep OBSERVATION profile/status information visible. Turn off to fade it out over 30 seconds.":
|
||||
- generic: HUD HOLD
|
||||
- checkbox "Keep Observation information visible" [checked]
|
||||
- generic: "ON"
|
||||
- generic: MAIN MASTER VOLUME
|
||||
- generic: 75%
|
||||
- generic: "-2.5 dB"
|
||||
- button "MUTE"
|
||||
- button "25%"
|
||||
- button "50%"
|
||||
- button "75%"
|
||||
- button "MAX"
|
||||
- button "−"
|
||||
- slider "Main Volume"
|
||||
- button "+"
|
||||
- generic: FNC // FUTURISTIC NOICE CANCELLATION
|
||||
- generic: ACTIVE
|
||||
- button "FNC ENABLED" [pressed]
|
||||
- generic: FNC LEVEL
|
||||
- slider "Futuristic Noice Cancellation level"
|
||||
- generic: 65%
|
||||
- generic: "FNC comfort filter remains applied. Adaptive watchdog memory reset: preset change."
|
||||
- generic: ACOUSTIC FREQUENCY SPECTRUM
|
||||
- generic: PROCEDURAL HARMONIC ANALYSIS
|
||||
- generic: "SPECTRUM BIN: 32 CH // REAL-TIME FFT"
|
||||
- generic: PROCEDURAL SOUND ENGINE MIXER
|
||||
- generic: LIVE PARAMETER MODULATION
|
||||
- generic: 1. HULL DRONE
|
||||
- generic: 65%
|
||||
- generic: LEVEL
|
||||
- slider
|
||||
- generic: BASE TONE
|
||||
- generic: 50 Hz
|
||||
- slider
|
||||
- generic: DAMPING CUTOFF
|
||||
- generic: 105 Hz
|
||||
- slider
|
||||
- generic: 2. WORP CORE
|
||||
- generic: 35%
|
||||
- generic: LEVEL
|
||||
- slider
|
||||
- generic: PULSE RATE (BPM)
|
||||
- generic: 48 BPM
|
||||
- slider
|
||||
- generic: REACTOR PITCH
|
||||
- generic: 58 Hz
|
||||
- slider
|
||||
- generic: 3. LIFE SUPPORT
|
||||
- generic: 55%
|
||||
- generic: LEVEL
|
||||
- slider
|
||||
- generic: AIRFLOW AIR FILTER
|
||||
- generic: 1600 Hz
|
||||
- slider
|
||||
- generic: 4. TELEMETRY
|
||||
- generic: 45%
|
||||
- generic: LEVEL
|
||||
- slider
|
||||
- generic: BACKGROUND CHIRP DENSITY
|
||||
- generic: 65%
|
||||
- slider
|
||||
- complementary:
|
||||
- generic: INTERMIX CASCADE
|
||||
- generic: INTERMIX CHAMBER // ACTIVE
|
||||
- generic: GENERATIVE EXPERIENCE
|
||||
- generic: LOCAL WEBLLM + KOKORO TTS
|
||||
- generic: LOCAL GENERATIVE SYSTEM
|
||||
- generic: STANDBY
|
||||
- text: LANGUAGE MODEL
|
||||
- text: Qwen3-1.7B-q4f16_1-MLC
|
||||
- text: Automatically prepared when the experience starts. No model selection required.
|
||||
- text: WEBLLM
|
||||
- generic: "OFF"
|
||||
- text: KOKORO
|
||||
- generic: "OFF"
|
||||
- button "PREPARE AI"
|
||||
- button "GENERATE NOW" [disabled]
|
||||
- checkbox "AUTO UPDATES" [checked]
|
||||
- text: AUTO UPDATES
|
||||
- checkbox "SPEAK" [checked]
|
||||
- text: SPEAK
|
||||
- combobox "Kokoro voice":
|
||||
- option "Kokoro voice loads on preparation" [selected]
|
||||
- generic: VOICE AUTOMATION
|
||||
- generic: 84%
|
||||
- slider "Robotic voice effect intensity"
|
||||
- generic: AUTO CADENCE
|
||||
- generic: 90-180 SEC
|
||||
- slider
|
||||
- generic: AI and neural voice models load only when needed and are cached by the browser when supported.
|
||||
- generic: GENERATIVE CHANNEL STANDBY
|
||||
- generic: TV PRESENTATION
|
||||
- generic: CAST-FRIENDLY 16:9
|
||||
- checkbox "LOCK EXPERIENCE TO 16:9" [checked]
|
||||
- text: LOCK EXPERIENCE TO 16:9
|
||||
- generic: RENDER DENSITY
|
||||
- generic: 1080P TARGET
|
||||
- combobox:
|
||||
- option "720P PERFORMANCE"
|
||||
- option "1080P STANDARD" [selected]
|
||||
- option "1440P HIGH"
|
||||
- option "2160P ULTRA"
|
||||
- generic: The scene keeps a canonical 16:9 layout on phones, PCs, and cast displays. Chrome still negotiates the final Cast stream resolution.
|
||||
- generic: SLEEP TIMER
|
||||
- generic: TIMER INACTIVE (CONTINUOUS)
|
||||
- button "15M"
|
||||
- button "30M"
|
||||
- button "60M"
|
||||
- button "OFF"
|
||||
- generic: LCARD KEYPAD
|
||||
- button "COM BADGE CHIRP"
|
||||
- button "DOOR PNEUMATIC SWISH"
|
||||
- button "BOSUN'S PIPE WHISTLE"
|
||||
- button "MEDBAY ECG MONITOR"
|
||||
- button "CARDASSIAN BULKHEAD DOOR"
|
||||
- button "TURBOCHUTE WHOOSH"
|
||||
- button "FABRICATOR SHIMMER"
|
||||
- button "TRANSLOCATOR CYCLE"
|
||||
- button "LCARD SINGLE CHIRP"
|
||||
- contentinfo:
|
||||
- generic: "SYSTEM STATUS: NORMAL"
|
||||
- link "VIBE ENGINEERED BY LABYRICORN STUDIOS":
|
||||
- /url: https://www.labyricorn.com/
|
||||
- generic: "SPACE: PLAY/PAUSE | M: MUTE | ↑/↓: VOLUME"
|
||||
- heading "Exhibit controls (61)" [level=2]
|
||||
- generic "Capabilities"
|
||||
- paragraph: Reported values are authoritative. New values are drafts; select Set or Invoke to submit. Rediscovery resets drafts.
|
||||
- heading "audio" [level=3]
|
||||
- article:
|
||||
- heading "Master Volume" [level=3]
|
||||
- code: mix.master
|
||||
- paragraph: range · readable · writable
|
||||
- status: "Reported value: 0.75"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- spinbutton "New value": "0.75"
|
||||
- button "Set"
|
||||
- article:
|
||||
- heading "Hull Drone Level" [level=3]
|
||||
- code: mix.hull.level
|
||||
- paragraph: range · readable · writable
|
||||
- status: "Reported value: 0.65"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- spinbutton "New value": "0.65"
|
||||
- button "Set"
|
||||
- article:
|
||||
- heading "Hull Drone Frequency" [level=3]
|
||||
- code: mix.hull.frequency
|
||||
- paragraph: range · readable · writable
|
||||
- status: "Reported value: 50"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- spinbutton "New value": "50"
|
||||
- button "Set"
|
||||
- article:
|
||||
- heading "Hull Drone Cutoff" [level=3]
|
||||
- code: mix.hull.cutoff
|
||||
- paragraph: range · readable · writable
|
||||
- status: "Reported value: 105"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- spinbutton "New value": "105"
|
||||
- button "Set"
|
||||
- article:
|
||||
- heading "Drive / Core Level" [level=3]
|
||||
- code: mix.drive.level
|
||||
- paragraph: range · readable · writable
|
||||
- status: "Reported value: 0.35"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- spinbutton "New value": "0.35"
|
||||
- button "Set"
|
||||
- article:
|
||||
- heading "Drive Pulse Rate" [level=3]
|
||||
- code: mix.drive.pulse-rate
|
||||
- paragraph: range · readable · writable
|
||||
- status: "Reported value: 48"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- spinbutton "New value": "48"
|
||||
- button "Set"
|
||||
- article:
|
||||
- heading "Drive Carrier Frequency" [level=3]
|
||||
- code: mix.drive.carrier
|
||||
- paragraph: range · readable · writable
|
||||
- status: "Reported value: 58"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- spinbutton "New value": "58"
|
||||
- button "Set"
|
||||
- article:
|
||||
- heading "Environmental Airflow Level" [level=3]
|
||||
- code: mix.environment.level
|
||||
- paragraph: range · readable · writable
|
||||
- status: "Reported value: 0.55"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- spinbutton "New value": "0.55"
|
||||
- button "Set"
|
||||
- article:
|
||||
- heading "Environmental Filter Cutoff" [level=3]
|
||||
- code: mix.environment.cutoff
|
||||
- paragraph: range · readable · writable
|
||||
- status: "Reported value: 1600"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- spinbutton "New value": "1600"
|
||||
- button "Set"
|
||||
- article:
|
||||
- heading "Telemetry Level" [level=3]
|
||||
- code: mix.telemetry.level
|
||||
- paragraph: range · readable · writable
|
||||
- status: "Reported value: 0.45"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- spinbutton "New value": "0.45"
|
||||
- button "Set"
|
||||
- article:
|
||||
- heading "Telemetry Density" [level=3]
|
||||
- code: mix.telemetry.density
|
||||
- paragraph: range · readable · writable
|
||||
- status: "Reported value: 0.65"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- spinbutton "New value": "0.65"
|
||||
- button "Set"
|
||||
- article:
|
||||
- heading "Futuristic Noise Cancellation Level" [level=3]
|
||||
- code: fnc.level
|
||||
- paragraph: range · readable · writable
|
||||
- status: "Reported value: 0.65"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- spinbutton "New value": "0.65"
|
||||
- button "Set"
|
||||
- article:
|
||||
- heading "Futuristic Noise Cancellation Active" [level=3]
|
||||
- code: fnc.enabled
|
||||
- paragraph: state · readable · writable
|
||||
- status: "Reported value: true"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- checkbox "New value" [checked]
|
||||
- button "Set"
|
||||
- heading "speech" [level=3]
|
||||
- article:
|
||||
- heading "Speech Robotic Modulation" [level=3]
|
||||
- code: speech.robot-amount
|
||||
- paragraph: range · readable · writable
|
||||
- status: "Reported value: 0.84"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- spinbutton "New value": "0.84"
|
||||
- button "Set"
|
||||
- article:
|
||||
- heading "Speak Text Line" [level=3]
|
||||
- code: speech.say
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Generate Ambient AI Announcement" [level=3]
|
||||
- code: speech.generate-announcement
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- heading "observation" [level=3]
|
||||
- article:
|
||||
- heading "Observation Visual Activity" [level=3]
|
||||
- code: view.activity
|
||||
- paragraph: range · readable · writable
|
||||
- status: "Reported value: 0.6"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- spinbutton "New value": "0.6"
|
||||
- button "Set"
|
||||
- article:
|
||||
- heading "Observation Viewport Active" [level=3]
|
||||
- code: view.observation
|
||||
- paragraph: state · readable · writable
|
||||
- status: "Reported value: false"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- checkbox "New value"
|
||||
- button "Set"
|
||||
- article:
|
||||
- heading "Starship Bezel Frame Framing" [level=3]
|
||||
- code: view.viewport-frame
|
||||
- paragraph: state · readable · writable
|
||||
- status: "Reported value: true"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- checkbox "New value" [checked]
|
||||
- button "Set"
|
||||
- heading "transport" [level=3]
|
||||
- article:
|
||||
- heading "Audio Engine Online" [level=3]
|
||||
- code: transport.playing
|
||||
- paragraph: state · readable · writable
|
||||
- status: "Reported value: false"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- checkbox "New value"
|
||||
- button "Set"
|
||||
- article:
|
||||
- heading "Master Mute" [level=3]
|
||||
- code: transport.muted
|
||||
- paragraph: state · readable · writable
|
||||
- status: "Reported value: false"
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- checkbox "New value"
|
||||
- button "Set"
|
||||
- heading "alert" [level=3]
|
||||
- article:
|
||||
- heading "Active Tactical Alert Status" [level=3]
|
||||
- code: alert.active
|
||||
- paragraph: selection · readable · writable
|
||||
- status: "Reported value: \"none\""
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- combobox "New value":
|
||||
- option "Condition Green (Normal)" [selected]
|
||||
- option "Yellow Alert"
|
||||
- option "Red Alert"
|
||||
- button "Set"
|
||||
- heading "universe" [level=3]
|
||||
- article:
|
||||
- heading "Active Sci-Fi Universe" [level=3]
|
||||
- code: universe.selected
|
||||
- paragraph: selection · readable · writable
|
||||
- status: "Reported value: \"starfleet\""
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- combobox "New value":
|
||||
- option "Starfleet / Galaxy" [selected]
|
||||
- option "Whataverse / Tardix"
|
||||
- option "Industrial / Weyland"
|
||||
- option "Bioships / Leviathan"
|
||||
- option "Retro Future / 1960s"
|
||||
- option "Colonial Military"
|
||||
- option "Deep Space Generation"
|
||||
- option "Frontier & Salvage"
|
||||
- option "Orbital Stations & Docks"
|
||||
- option "Whimsical Fleet"
|
||||
- button "Set"
|
||||
- heading "preset" [level=3]
|
||||
- article:
|
||||
- heading "Active Audio Preset" [level=3]
|
||||
- code: preset.selected
|
||||
- paragraph: selection · readable · writable
|
||||
- status: "Reported value: \"tng-bridge\""
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- text: New value
|
||||
- combobox "New value":
|
||||
- 'option "Interprise-G: Main Bridge" [selected]'
|
||||
- 'option "Interprise-G: Main Engineering"'
|
||||
- 'option "Interprise-G: Crew Quarters"'
|
||||
- 'option "USS Wayfarer: Bridge"'
|
||||
- 'option "USS Wayfarer: Class-N Worp Core"'
|
||||
- 'option "USS Defiance: Tactical Bridge"'
|
||||
- 'option "Deep Space 8: Ops Center"'
|
||||
- 'option "Interprise NCC-1701.5: Bridge (TOS)"'
|
||||
- 'option "Interprise NCC-1701.5: Engineering (TOS)"'
|
||||
- 'option "Interprise NS-01: Command Bridge"'
|
||||
- option "1962 Classic Console (1st / 2nd Doctors)"
|
||||
- option "Edwardian Secondary Console (4th Doctor)"
|
||||
- option "The Coral Living TARDIX (9th / 10th Doctors)"
|
||||
- option "The Bronze Workshop (11th Doctor)"
|
||||
- option "The Cool Machine (12th Doctor)"
|
||||
- option "The Humming Crystal Console (13th Doctor)"
|
||||
- option "The Endless White (14th / 15th Doctors)"
|
||||
- 'option "USCSS Nostramo: Ore Refinery Bridge"'
|
||||
- 'option "Serenety: Firewing-Class Cargo Hold"'
|
||||
- 'option "Red Dwarve: Main Drive Corridor"'
|
||||
- 'option "Starbug Z: Cockpit Environment"'
|
||||
- 'option "The Rasa: Dark Matter Bridge"'
|
||||
- 'option "Rocinanta: Combat Ops & Epsilon Drive"'
|
||||
- 'option "Hawk Translocator: Command Module"'
|
||||
- 'option "Valley Foundry: Agro-Dome Forest Hub"'
|
||||
- 'option "Meya: Leviathon Central Nexus"'
|
||||
- 'option "Talym: Gunship Neural Bridge"'
|
||||
- 'option "The Lexxi: Primary Organ Bridge"'
|
||||
- 'option "Vorlan Cruiser: Sentient Core"'
|
||||
- 'option "Wraithe Hive Ship: Throne Chamber"'
|
||||
- 'option "Species 8675309: Fluid Bioship"'
|
||||
- 'option "Jupiter Duece: Upper Deck Astrogator"'
|
||||
- 'option "The Liberation: Zen Flight Bridge"'
|
||||
- 'option "USS Cygnis: Victorian Engine Hall"'
|
||||
- 'option "USS Palomina: Deep Research Pod"'
|
||||
- 'option "Discovery Zero: Habitation Centrifuge"'
|
||||
- 'option "Dim Star: Bomb Bay & Quarters"'
|
||||
- 'option "Gunstarr: Tactical Combat Cockpit"'
|
||||
- 'option "The Seeker: 25th Century Flagship"'
|
||||
- 'option "USS Solaco: Conestega Hangar Deck"'
|
||||
- 'option "Battlestar Galactica: Combat Information Center (CIC)"'
|
||||
- 'option "Pale Star: Minbara/Vorlan Hybrid Bridge"'
|
||||
- 'option "EAS Agamemnon II: Omega Destroyer Bridge"'
|
||||
- 'option "Andromeda Ascending: Command Deck"'
|
||||
- 'option "Excaliber: Triumph-Class Heavy Combat Core"'
|
||||
- 'option "Colonial Adder Mk II: Cockpit Atmosphere"'
|
||||
- 'option "The Avalin: Interstellar Cruise Concourse"'
|
||||
- 'option "The Nightflier: Telepathic Corridor"'
|
||||
- 'option "USS Ascention: Generation Ship Promenade"'
|
||||
- 'option "Ark Zero: Evacuation Ark Bridge"'
|
||||
- 'option "Icarus I: Solar Shield Core"'
|
||||
- 'option "Event Boundary: Gravity Singularity Core"'
|
||||
- 'option "Lewis & Clarke: Rescue Cutter Bridge"'
|
||||
- 'option "The Betsy: Salvage Freighter Mess"'
|
||||
- 'option "BeBoppa: Living Quarters & Hangar"'
|
||||
- 'option "Outlaw Sun: Grapple Bridge"'
|
||||
- 'option "Scorpius: Wanderer Salvage Vessel"'
|
||||
- 'option "The Raider: Havoc Shuttle Bridge"'
|
||||
- 'option "The Milan: M-Ship Cockpit Lounge"'
|
||||
- 'option "Babylon 4: Core Control & Zocolo"'
|
||||
- 'option "Moonbase Beta: Main Mission Control"'
|
||||
- 'option "Sebastopol Station: Habitation Deck"'
|
||||
- 'option "Tyco Station: Asteroid Construction Bay"'
|
||||
- 'option "Ceres Minor Station: Sub-Crustal Tunnels"'
|
||||
- 'option "Gatehouse Station: Quarantine Transfer Deck"'
|
||||
- 'option "USS Norval: Command Bridge"'
|
||||
- 'option "NSEA Protractor: Beryllium Orb Engine Room"'
|
||||
- 'option "Heart of Silver: Unlikelihood Bridge"'
|
||||
- 'option "HMS Camden Key: Flight Deck & Mess"'
|
||||
- 'option "Spaceorb One: Mega-Warship Bridge"'
|
||||
- button "Set"
|
||||
- heading "display" [level=3]
|
||||
- article:
|
||||
- heading "Display Ticker Message (Transient)" [level=3]
|
||||
- code: display.ticker
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- heading "event" [level=3]
|
||||
- article:
|
||||
- heading "Red Alert Impulse" [level=3]
|
||||
- code: event.alert-red
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Warp Jump Impulse" [level=3]
|
||||
- code: event.warp-jump
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Demat Cycle Impulse" [level=3]
|
||||
- code: event.demat
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Time Vortex Impulse" [level=3]
|
||||
- code: event.vortex
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Cloister Bell Impulse" [level=3]
|
||||
- code: event.cloister
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Flak Barrage Impulse" [level=3]
|
||||
- code: event.flak-barrage
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "FTL Jump Impulse" [level=3]
|
||||
- code: event.ftl-jump
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Singularity Engage Impulse" [level=3]
|
||||
- code: event.singularity
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Solar Flare Roar Impulse" [level=3]
|
||||
- code: event.solar-roar
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Afterburner Ignite Impulse" [level=3]
|
||||
- code: event.afterburner
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Ludicrous Speed Impulse" [level=3]
|
||||
- code: event.ludicrous-speed
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Improbability Drive Flip" [level=3]
|
||||
- code: event.improbability-flip
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- heading "sfx" [level=3]
|
||||
- article:
|
||||
- heading "Comm Badge Chime" [level=3]
|
||||
- code: sfx.comm-badge
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Pneumatic Door Swish" [level=3]
|
||||
- code: sfx.door-swish
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Bosun Pipe Call" [level=3]
|
||||
- code: sfx.bosun-whistle
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Medical Vital Monitor" [level=3]
|
||||
- code: sfx.medical-monitor
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Heavy Bulkhead Door" [level=3]
|
||||
- code: sfx.cardassian-door
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Turbochute Whoosh" [level=3]
|
||||
- code: sfx.turbolift
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Fabricator Materialize" [level=3]
|
||||
- code: sfx.replicator
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Translocator Beam" [level=3]
|
||||
- code: sfx.transporter
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Console Single Chirp" [level=3]
|
||||
- code: sfx.console-chirp
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Sonic Acoustic Screwdriver" [level=3]
|
||||
- code: sfx.sonic
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Console Mechanical Lever Clunk" [level=3]
|
||||
- code: sfx.lever-clunk
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Tardix Wood & Lock Door" [level=3]
|
||||
- code: sfx.tardix-door
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Demat Switch Rotor" [level=3]
|
||||
- code: sfx.demat-switch
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Telepathic Circuit Chime" [level=3]
|
||||
- code: sfx.telepathic-chime
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Air Handler Valve Thud" [level=3]
|
||||
- code: sfx.air-handler
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Mag-Boot Solenoid Latch" [level=3]
|
||||
- code: sfx.mag-boot
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Comlock Electronic Tone" [level=3]
|
||||
- code: sfx.comlock
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Solar Radiation Wind" [level=3]
|
||||
- code: sfx.solar-roar
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Cassette Magnetic Transport Clunk" [level=3]
|
||||
- code: sfx.tape-clunk
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Thruster Burner Pulse" [level=3]
|
||||
- code: sfx.afterburner
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Radiation Geiger Counter" [level=3]
|
||||
- code: sfx.geiger
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Air Valve & Dock Clamp" [level=3]
|
||||
- code: sfx.dock-clamp
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "Dradis Combat Sonar Ping" [level=3]
|
||||
- code: sfx.dradis-ping
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- article:
|
||||
- heading "HAL Logic Melodic Chime" [level=3]
|
||||
- code: sfx.hal-chime
|
||||
- paragraph: impulse · not readable · not writable
|
||||
- status: No persistent value reported.
|
||||
- generic "Target descriptor"
|
||||
- paragraph
|
||||
- button "Invoke"
|
||||
- heading "Presentation surfaces" [level=2]: Presentation surfaces
|
||||
- paragraph: Open Contract 5.3 presentation surfaces locally below. URLs resolve against the supplying exhibit. Disconnecting or switching exhibits closes these views. Remote/cast displays remain deferred.
|
||||
- paragraph: No presentation surfaces advertised.
|
||||
- heading "State and metadata" [level=2]
|
||||
- button "Refresh state"
|
||||
- generic "Session and exhibit metadata"
|
||||
- generic "Current persistent state"
|
||||
- heading "Protocol diagnostics" [level=2]
|
||||
- generic "Incoming events"
|
||||
- generic "Protocol and errors"
|
||||
@@ -0,0 +1,180 @@
|
||||
Step 6.6 — Museum Gallery end-to-end validation through the NGN operator UI
|
||||
Server: pre-existing `npm start` server (node server/serve.js) on http://127.0.0.1:4173,
|
||||
started before this session and left running. A second instance started for
|
||||
validation failed with EADDRINUSE and exited; the served state was unchanged.
|
||||
NGN UI: http://127.0.0.1:4173/
|
||||
Exhibit: http://127.0.0.1:4173/test-fixtures/reference-exhibits/museum-gallery/control.html
|
||||
Replacement exhibit: http://127.0.0.1:4173/test-fixtures/reference-exhibits/scifi/index.html
|
||||
Method: live browser session against the served page; values read from the live DOM, the
|
||||
exhibit frames, and NGN's own diagnostics panes. No test doubles.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
1. Initial connection (Load exhibit)
|
||||
--------------------------------------------------------------------
|
||||
status: connected · synchronized
|
||||
identity: Museum Gallery · 0.1.0 · Contract 5.3
|
||||
metadata: { sessionId: "sess-1f54f",
|
||||
contract: { major: 5, minor: 3 },
|
||||
exhibit: { product: "Museum Gallery", version: "0.1.0", build: "reference-exhibit" },
|
||||
registryRevision: 1, stateRevision: 0, sequence: null }
|
||||
capabilities: render: ready
|
||||
targets (5): artifact.selected (selection, r/w), lighting.level (range, r/w),
|
||||
rotation.speed (range, r/w), labels.enabled (state, r/w),
|
||||
action.spotlight-flash (impulse, not readable, not writable)
|
||||
state: { "artifact.selected": "the-orrery", "lighting.level": 0.6,
|
||||
"rotation.speed": 0.4, "labels.enabled": true }
|
||||
error log: one SESSION entry, no errors
|
||||
|
||||
--------------------------------------------------------------------
|
||||
2. Surface discovery and primary handling
|
||||
--------------------------------------------------------------------
|
||||
Presentation surfaces (3):
|
||||
surface.control Control Room Primary Role: control URL: control.html
|
||||
surface.artifact Artifact Display Non-primary Role: ambient URL: artifact.html
|
||||
surface.info-wall Information Wall Non-primary Role: information URL: info-wall.html
|
||||
Primary card buttons: Open disabled, Reload disabled, Close disabled
|
||||
Primary card status: "Open in the authoritative control frame. Use Connection controls to
|
||||
reload or disconnect."
|
||||
Non-primary panes rendered and each reported: "Attached to Control Room."
|
||||
Control Room readout "Attached non-primary surfaces": 2
|
||||
|
||||
--------------------------------------------------------------------
|
||||
3. Lifecycle (participant count in the authoritative Control Room)
|
||||
--------------------------------------------------------------------
|
||||
open both -> 2
|
||||
reload -> 2
|
||||
reload again -> 2
|
||||
close one -> 1
|
||||
reopen -> 2
|
||||
Reload freshness probe: a marker written to the secondary frame contentWindow was absent
|
||||
after reload (fresh document), and the document re-attached and re-rendered current state.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
4. Shared-state synchronization
|
||||
--------------------------------------------------------------------
|
||||
Control -> secondaries
|
||||
Control Room combobox -> "star-map":
|
||||
event { type: "selection.changed", sequence: 1, stateRevision: 1,
|
||||
target: "artifact.selected", value: "star-map", source: "ui" }
|
||||
Control Room "Star Map"; Artifact Display "Star Map"; Information Wall "Star Map"
|
||||
Control Room lighting range -> 0.25:
|
||||
event { type: "state.changed", sequence: 2, stateRevision: 2,
|
||||
target: "lighting.level", value: 0.25, source: "ui" }
|
||||
Control Room "25%"; Artifact Display "Lighting: 25%"
|
||||
|
||||
Secondary -> primary -> secondaries
|
||||
Real mouse click on Artifact Display "Cycle artifact" (authoritative value "star-map"):
|
||||
Control Room combobox became "meteorite"; Control Room label "Meteorite Fragment"
|
||||
event { type: "selection.changed", sequence: 3, stateRevision: 3,
|
||||
target: "artifact.selected", value: "meteorite", source: "ui" }
|
||||
Information Wall -> "Meteorite Fragment"; Artifact Display re-rendered authoritative result
|
||||
|
||||
Sustained convergence (both secondaries open, alternating origins)
|
||||
step 1 control select the-orrery -> all views "The Orrery" revision 7 count 2
|
||||
step 2 info-wall toggle labels -> labels.enabled true revision 8 count 2
|
||||
step 3 control rotation 0.6 -> rotation.speed 0.6 revision 9 count 2
|
||||
step 4 artifact-display cycle -> artifact.selected star-map revision 10 count 2
|
||||
Control Room, Artifact Display, Information Wall and NGN reported state all agreed
|
||||
after every step.
|
||||
|
||||
Late join
|
||||
close Information Wall -> count 1
|
||||
mutate: rotation 1.7, labels OFF -> stateRevision 5
|
||||
reopen Information Wall -> immediately rendered "Meteorite Fragment" / "Labels: OFF",
|
||||
count back to 2
|
||||
|
||||
Reload recovery
|
||||
mutate: lighting 0.9 -> stateRevision 6
|
||||
reload Artifact Display -> fresh document returned with "Meteorite Fragment",
|
||||
"Lighting: 90%", "Rotation: 1.7x" (current state, not defaults); count still 2
|
||||
|
||||
--------------------------------------------------------------------
|
||||
5. NGN host commands
|
||||
--------------------------------------------------------------------
|
||||
Refresh state (state.get): succeeded; stateRevision 10, status connected · synchronized,
|
||||
no error
|
||||
set lighting.level = 0.35: succeeded; Control Room "35%" (range 0.35),
|
||||
Artifact Display "Lighting: 35%";
|
||||
event { type: "state.changed", sequence: 11, stateRevision: 11,
|
||||
target: "lighting.level", value: 0.35,
|
||||
source: "host", correlationId: "ngn-2-4" }
|
||||
set artifact.selected = meteorite: succeeded; Control Room / Artifact Display /
|
||||
Information Wall all "Meteorite Fragment";
|
||||
event { type: "selection.changed", sequence: 12, stateRevision: 12,
|
||||
target: "artifact.selected", value: "meteorite",
|
||||
source: "host", correlationId: "ngn-2-6" }
|
||||
invoke action.spotlight-flash: succeeded;
|
||||
event { type: "action.executed", sequence: 13,
|
||||
target: "action.spotlight-flash", args: {},
|
||||
source: "host", correlationId: "ngn-2-8" };
|
||||
exhibit gallery.spotlightFlashCount incremented;
|
||||
Control Room and both secondaries announcement nodes flashed
|
||||
"Spotlight flashed." (captured by a DOM mutation observer on the
|
||||
Artifact Display announcement node: seen transitions
|
||||
["Spotlight flashed.", "Waiting for shared state."])
|
||||
|
||||
--------------------------------------------------------------------
|
||||
6. Reconnect and exhibit replacement
|
||||
--------------------------------------------------------------------
|
||||
Disconnect:
|
||||
status: disconnected · not synchronized
|
||||
catalog: "Connect an exhibit to discover its controls."
|
||||
surfaces: "No presentation surfaces advertised."; surface cards 0; surface iframes 0
|
||||
control frame removed; state {}; metadata all null; no error entries
|
||||
Reconnect:
|
||||
status: connected · synchronized; session sess-15cf1; contract { major: 5, minor: 3 }
|
||||
targets (5); surfaces (3) with surface.control primary; participant count 0
|
||||
no stale panes; both secondaries reopened -> "Attached to Control Room.", count 2
|
||||
Switch to SciFi:
|
||||
identity: SciFi-XZBT · 5.2.0 · Contract 5.3
|
||||
metadata: sessionId "sess-2akzu3a7", contract { major: 5, minor: 3 },
|
||||
registryRevision 1, stateRevision 0
|
||||
status: connected · synchronized; targets (61); surfaces: none advertised
|
||||
iframes referencing museum-gallery: 0
|
||||
Switch back to Museum Gallery:
|
||||
session sess-126c6; contract { major: 5, minor: 3 }; targets (5); surfaces (3) with
|
||||
correct primary; participant count 0; iframes referencing scifi: 0
|
||||
state: fresh exhibit defaults, stateRevision 0
|
||||
|
||||
--------------------------------------------------------------------
|
||||
7. Failure isolation (live)
|
||||
--------------------------------------------------------------------
|
||||
Artifact Display document globals:
|
||||
XZBTContractCore: undefined
|
||||
MuseumGalleryContract: undefined
|
||||
MuseumGalleryExhibit: undefined
|
||||
XZBTHostTransport: undefined
|
||||
MuseumGallerySurfaceBus: present
|
||||
__museumGalleryDebug.core: absent
|
||||
Forged protocol messages posted directly to the secondary window:
|
||||
{ xzbt: "5.3", type: "hello", requestId: "probe-secondary", supportedContractMajors: [5] }
|
||||
{ xzbt: "5.3", type: "set", requestId: "probe-set", sessionId: "x",
|
||||
target: "artifact.selected", value: "meteorite" }
|
||||
-> responses received: none; authoritative selection unchanged ("The Orrery")
|
||||
Surface-local failure: Artifact Display frame navigated to
|
||||
/test-fixtures/reference-exhibits/museum-gallery/does-not-exist.html (served "Not found")
|
||||
-> NGN status stayed "connected · synchronized" on session sess-126c6;
|
||||
control frame still control.html; registry/state unaffected; no error entry;
|
||||
participant count 2 -> 0 with no stale entry
|
||||
NGN per-surface Reload on the failed pane:
|
||||
-> frame back at artifact.html, "Attached to Control Room.", participant count 1,
|
||||
session still synchronized, no error entry
|
||||
|
||||
Automated-only isolation evidence (not re-injected live): malformed/invalid descriptor
|
||||
catalogs, isolated HTTP/frame/reload failures, external frame removal detection, stalled
|
||||
surface resource, redirect refusal on resource check, presentation-window impersonation
|
||||
refusal, transport source/origin validation.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
8. Automated verification
|
||||
--------------------------------------------------------------------
|
||||
Focused: node --test tests/museum-gallery.test.js tests/local-surfaces.test.js
|
||||
tests/surface-validation.test.js tests/postmessage-interop.test.js
|
||||
tests/connection.test.js
|
||||
tests 107 / pass 107 / fail 0
|
||||
|
||||
Full: node --test
|
||||
tests 129 / pass 129 / fail 0
|
||||
|
||||
git diff --check -> clean (exit 0)
|
||||
python devlog_editor.py --validate -> devlog-editor: devlog is valid (exit 0)
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Aquarium — XZBT Exhibit Contract 5.2 adapter.
|
||||
* Aquarium — XZBT Exhibit Contract 5.3 adapter.
|
||||
*
|
||||
* This file is the only place in the exhibit that knows the contract exists.
|
||||
* It does three things and nothing else:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Haunted House — XZBT Exhibit Contract 5.2 adapter.
|
||||
* Haunted House — XZBT Exhibit Contract 5.3 adapter.
|
||||
*
|
||||
* This adapter is the one that has to be honest about capabilities. The
|
||||
* exhibit's audio engine cannot start until the browser has seen a user
|
||||
|
||||
@@ -71,6 +71,16 @@
|
||||
link.mutate('set', 'artifact.selected', next);
|
||||
});
|
||||
|
||||
/* `beforeunload` is best-effort only -- it does not reliably fire when
|
||||
* NGN removes/replaces this frame's iframe element directly (as opposed
|
||||
* to a real navigation), so it is not the correctness mechanism here.
|
||||
* `__xzbtSurfaceDispose` is a well-known, optional hook: local-surfaces.js
|
||||
* calls it synchronously, if present, on this frame's contentWindow right
|
||||
* before removing the iframe -- deterministic because it is a direct
|
||||
* same-origin call, not a queued postMessage that a doomed frame might
|
||||
* never get to process. detach() is idempotent, so it is safe for both
|
||||
* this hook and beforeunload to fire (or neither, or either alone). */
|
||||
window.__xzbtSurfaceDispose = function () { link.detach(); };
|
||||
window.addEventListener('beforeunload', function () { link.detach(); });
|
||||
window.__museumGalleryDebug = { link: link };
|
||||
})();
|
||||
|
||||
@@ -126,11 +126,12 @@
|
||||
setters: setters,
|
||||
readers: readers,
|
||||
actions: actions,
|
||||
surfaces: surfaces,
|
||||
/* Contract 5.3 adoption is per-instance; every other reference exhibit
|
||||
* keeps the shared core's Contract 5.2 defaults untouched. */
|
||||
contractMinor: 3,
|
||||
xzbtVersion: '5.3'
|
||||
/* contractMinor/xzbtVersion are intentionally omitted: the shared
|
||||
* contract-core.js now defaults every exhibit to Contract 5.3, and
|
||||
* Museum Gallery has no reason to override that default. `surfaces`
|
||||
* is the one thing that makes this exhibit's contract usage different
|
||||
* from the others -- Contract 5.3 §31 presentation surfaces. */
|
||||
surfaces: surfaces
|
||||
});
|
||||
|
||||
return core;
|
||||
|
||||
@@ -81,12 +81,17 @@
|
||||
* surface) to attached surfaces, and answers their attach requests with a
|
||||
* fresh snapshot read straight from this Core -- never a second
|
||||
* computation. */
|
||||
var bus = window.MuseumGallerySurfaceBus.createOwner(core);
|
||||
function updateSubscriberCount(count) {
|
||||
Shell.setText(subscriberCountEl, String(count));
|
||||
}
|
||||
var bus = window.MuseumGallerySurfaceBus.createOwner(core, {
|
||||
onConnectionChange: updateSubscriberCount
|
||||
});
|
||||
updateSubscriberCount(bus.attachedCount());
|
||||
var priorOnEvent = core.onEvent;
|
||||
core.onEvent = function (event) {
|
||||
if (typeof priorOnEvent === 'function') priorOnEvent(event);
|
||||
sync();
|
||||
Shell.setText(subscriberCountEl, String(bus.attachedCount()));
|
||||
};
|
||||
|
||||
/* Optional NGN attachment -- same optional transport every reference
|
||||
|
||||
@@ -56,6 +56,12 @@
|
||||
link.mutate('set', 'labels.enabled', !latest['labels.enabled']);
|
||||
});
|
||||
|
||||
/* Same rationale as artifact.boot.js: `__xzbtSurfaceDispose` is the
|
||||
* deterministic path (called synchronously by local-surfaces.js right
|
||||
* before it removes this frame); `beforeunload` remains only as a
|
||||
* best-effort fallback for the cases NGN can't foresee (e.g. the tab
|
||||
* closing on its own). detach() is idempotent either way. */
|
||||
window.__xzbtSurfaceDispose = function () { link.detach(); };
|
||||
window.addEventListener('beforeunload', function () { link.detach(); });
|
||||
window.__museumGalleryDebug = { link: link };
|
||||
})();
|
||||
|
||||
@@ -39,23 +39,42 @@
|
||||
* second computation of state.
|
||||
*
|
||||
* @param {object} core an XZBTContractCore.ContractCore instance
|
||||
* @param {object} [options]
|
||||
* @param {function} [options.onConnectionChange] (attachedCount)
|
||||
*/
|
||||
function createOwner(core) {
|
||||
function createOwner(core, options) {
|
||||
options = options || {};
|
||||
var channel = new BroadcastChannel(CHANNEL_NAME);
|
||||
var attachedCount = 0;
|
||||
/* Participant bookkeeping is identity-based (a Set of live participant
|
||||
* ids), not a bare counter. This is what makes it idempotent: a
|
||||
* duplicate `attach` from a still-live participant cannot inflate the
|
||||
* count, and a `detach` for an id that is not (or no longer) present
|
||||
* cannot underflow it -- both matter because iframe reload/removal can
|
||||
* make attach/detach delivery imperfect (see attach()'s participantId
|
||||
* and local-surfaces.js's pre-removal dispose call). */
|
||||
var participants = new Set();
|
||||
|
||||
function notifyConnectionChange() {
|
||||
if (typeof options.onConnectionChange === 'function') {
|
||||
options.onConnectionChange(participants.size);
|
||||
}
|
||||
}
|
||||
|
||||
channel.onmessage = function (ev) {
|
||||
var msg = ev.data;
|
||||
if (!msg || typeof msg !== 'object') return;
|
||||
|
||||
if (msg.type === 'attach') {
|
||||
attachedCount += 1;
|
||||
if (typeof msg.participantId === 'string') {
|
||||
participants.add(msg.participantId);
|
||||
}
|
||||
channel.postMessage({
|
||||
type: 'attach.snapshot',
|
||||
inReplyTo: msg.requestId,
|
||||
snapshot: core.stateSnapshot(),
|
||||
registryRevision: core.registryRevision
|
||||
});
|
||||
notifyConnectionChange();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,7 +94,13 @@
|
||||
}
|
||||
|
||||
if (msg.type === 'detach') {
|
||||
attachedCount = Math.max(0, attachedCount - 1);
|
||||
/* A detach with no/unknown participantId (a stray legacy message, a
|
||||
* duplicate, or one that arrives after that id was already removed)
|
||||
* is a no-op -- Set.delete() only reports and fires the callback
|
||||
* when it actually removed something live. */
|
||||
if (typeof msg.participantId === 'string' && participants.delete(msg.participantId)) {
|
||||
notifyConnectionChange();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -87,7 +112,7 @@
|
||||
|
||||
return {
|
||||
channel: channel,
|
||||
attachedCount: function () { return attachedCount; },
|
||||
attachedCount: function () { return participants.size; },
|
||||
close: function () { channel.close(); }
|
||||
};
|
||||
}
|
||||
@@ -109,7 +134,14 @@
|
||||
options = options || {};
|
||||
var channel = new BroadcastChannel(CHANNEL_NAME);
|
||||
var requestId = randomId();
|
||||
/* A stable identity for THIS attachment (this document load, this
|
||||
* BroadcastChannel instance). A reload creates a brand-new attach() call
|
||||
* and therefore a brand-new participantId, which is exactly what lets
|
||||
* the owner's Set-based bookkeeping retire the old identity and adopt
|
||||
* the new one without the two ever being confused for each other. */
|
||||
var participantId = randomId();
|
||||
var attached = false;
|
||||
var detached = false;
|
||||
|
||||
var timer = setTimeout(function () {
|
||||
if (attached) return;
|
||||
@@ -134,7 +166,21 @@
|
||||
}
|
||||
};
|
||||
|
||||
channel.postMessage({ type: 'attach', requestId: requestId });
|
||||
channel.postMessage({ type: 'attach', requestId: requestId, participantId: participantId });
|
||||
|
||||
function detach() {
|
||||
/* Idempotent: local-surfaces.js's deterministic pre-removal dispose
|
||||
* call and this document's own best-effort `beforeunload` handler can
|
||||
* both end up calling detach() for the same attachment. Only the
|
||||
* first actually posts a message; every call still tears down the
|
||||
* timer/channel so repeated calls are always safe. */
|
||||
if (attached && !detached) {
|
||||
detached = true;
|
||||
channel.postMessage({ type: 'detach', participantId: participantId });
|
||||
}
|
||||
clearTimeout(timer);
|
||||
channel.close();
|
||||
}
|
||||
|
||||
return {
|
||||
channel: channel,
|
||||
@@ -148,11 +194,7 @@
|
||||
channel.postMessage(payload);
|
||||
return true;
|
||||
},
|
||||
detach: function () {
|
||||
if (attached) channel.postMessage({ type: 'detach' });
|
||||
clearTimeout(timer);
|
||||
channel.close();
|
||||
}
|
||||
detach: detach
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Planetarium — XZBT Exhibit Contract 5.2 adapter.
|
||||
* Planetarium — XZBT Exhibit Contract 5.3 adapter.
|
||||
*
|
||||
* Declares the catalog, binds targets to real exhibit services, and routes
|
||||
* the native UI through the canonical mutation path. No state of its own.
|
||||
|
||||
@@ -2428,3 +2428,30 @@ body.xzbt-tv-aspect .observation-overlay:not(.active){display:none}
|
||||
@media (max-width:700px){
|
||||
.watch-experience-wrap{min-width:150px}.watch-experience-help{font-size:.54rem}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Step 6.7B -- Observation surface mode (body.surface-observation).
|
||||
Applied when this document is booted as the non-authoritative Observation
|
||||
surface (?surface=observation&xi=...). Presentation-only: additive rules,
|
||||
nothing above this block is touched. The console DOM (header/main) is
|
||||
hidden rather than removed -- observation-engine.js keeps binding to it
|
||||
exactly as it does today, unmodified, per the Step 6.7A design.
|
||||
========================================================================== */
|
||||
body.surface-observation > header,
|
||||
body.surface-observation > main {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.surface-observation .observation-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Exiting/returning to the console is NGN's job (pane lifecycle), not the
|
||||
surface's -- these controls would otherwise mutate the *console's* own
|
||||
view.observation overlay flag from a stray click (report SS7/SS8). */
|
||||
body.surface-observation #obs-btn-exit,
|
||||
body.surface-observation #observation-return-pill {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -440,6 +440,8 @@
|
||||
<script src="js/observation-bezels.js"></script>
|
||||
<script src="js/observation-engine.js"></script>
|
||||
<script src="js/control-bus.js"></script>
|
||||
<script src="js/surface-mode.js"></script>
|
||||
<script src="js/surface-bus.js"></script>
|
||||
<script src="js/generative-experience.js"></script>
|
||||
<script src="js/contract-adapter.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
|
||||
@@ -5,6 +5,20 @@
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Step 6.7B: boot-mode resolver (Contract 5.3 SS31.4 query-string surface
|
||||
// form). 'console' is today's application, byte-for-byte -- the resolver
|
||||
// returns 'console' for no query string and for any unrecognized
|
||||
// ?surface= value. 'observation' renders only the Observation viewscreen
|
||||
// and mirrors an authoritative console instance over an exhibit-internal
|
||||
// BroadcastChannel; it never constructs a second contract session, audio
|
||||
// graph or generative engine (see the isConsoleMode guards below).
|
||||
const xzbtSurface = XZBTSurfaceMode.resolve(window.location);
|
||||
const isConsoleMode = xzbtSurface.mode !== 'observation';
|
||||
const xzbtInstanceId = isConsoleMode ? XZBTSurfaceMode.newInstanceId() : xzbtSurface.instanceId;
|
||||
if (!isConsoleMode) document.body.classList.add('surface-observation');
|
||||
let surfaceOwner = null; // console mode: bus owner, feeds attached surfaces
|
||||
let surfaceLink = null; // observation mode: bus attachment to the console
|
||||
|
||||
// 1. Initialize Audio Subsystems
|
||||
const audioManager = new AudioManager();
|
||||
const fncSystem = new FuturisticNoiceCancellation(audioManager);
|
||||
@@ -21,7 +35,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
window.expandedAudio = expandedAudio;
|
||||
window.whataverseAudio = whataverseAudio;
|
||||
window.engineTransitions = engineTransitions;
|
||||
const visualizer = new StarshipVisualizer(audioManager, warpCore);
|
||||
// Step 6.7B: console-only. Its canvases (spectrum, warp core) are
|
||||
// console-panel elements; never construct it on the Observation surface.
|
||||
const visualizer = isConsoleMode ? new StarshipVisualizer(audioManager, warpCore) : null;
|
||||
const observationEngine = new ObservationEngine(audioManager, warpCore, alerts, hullDrone, lifeSupport);
|
||||
window.observationEngine = observationEngine;
|
||||
|
||||
@@ -97,34 +113,55 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const timerDisplay = document.getElementById('timer-display');
|
||||
const timerButtons = document.querySelectorAll('.btn-timer');
|
||||
|
||||
// XZBT Generative Experience subsystem. Deterministic engine remains authoritative.
|
||||
const generativeExperience = new XZBTGenerativeExperience({
|
||||
getUniverse: () => UniverseRegistry[activeUniverseId],
|
||||
getPreset: () => UniverseRegistry[activeUniverseId]?.presets?.[activePresetId] || null,
|
||||
isPlaying: () => isPlaying,
|
||||
isObservation: () => observationActive,
|
||||
audioManager,
|
||||
onGenerated: (line, state) => {
|
||||
const ticker = document.getElementById('observation-ticker-text');
|
||||
if (ticker && observationActive) ticker.textContent = line.toUpperCase();
|
||||
window.dispatchEvent(new CustomEvent('xzbt:generated-content', { detail: { line, state } }));
|
||||
}
|
||||
});
|
||||
generativeExperience.bindUI();
|
||||
window.generativeExperience = generativeExperience;
|
||||
// XZBT Generative Experience subsystem. Deterministic engine remains
|
||||
// authoritative. Step 6.7B: console-only -- WebLLM/Kokoro must never be
|
||||
// constructed or prepared on the Observation surface (highest-risk guard
|
||||
// in this design; see the isConsoleMode guard on prepareExperience() in
|
||||
// enterObservation() below).
|
||||
let generativeExperience = null;
|
||||
if (isConsoleMode) {
|
||||
generativeExperience = new XZBTGenerativeExperience({
|
||||
getUniverse: () => UniverseRegistry[activeUniverseId],
|
||||
getPreset: () => UniverseRegistry[activeUniverseId]?.presets?.[activePresetId] || null,
|
||||
isPlaying: () => isPlaying,
|
||||
isObservation: () => observationActive,
|
||||
audioManager,
|
||||
onGenerated: (line, state) => {
|
||||
const ticker = document.getElementById('observation-ticker-text');
|
||||
const tickerText = line.toUpperCase();
|
||||
if (ticker && observationActive) ticker.textContent = tickerText;
|
||||
// Mirror the announcement to any attached Observation surface(s):
|
||||
// the AI only verbalizes state the authority already decided, so
|
||||
// this is presentation-only text, not contract state (report SS6).
|
||||
if (surfaceOwner) surfaceOwner.broadcastPresentation('ticker', { tickerText });
|
||||
window.dispatchEvent(new CustomEvent('xzbt:generated-content', { detail: { line, state } }));
|
||||
}
|
||||
});
|
||||
generativeExperience.bindUI();
|
||||
window.generativeExperience = generativeExperience;
|
||||
}
|
||||
|
||||
// Forward-declare absolute state setters & helpers for Contract Adapter
|
||||
let setMasterVolumeDirect, setPlayingDirect, setMutedDirect, setFncEnabledDirect;
|
||||
let setObservationDirect, setViewportFrameDirect, setAlertActiveDirect;
|
||||
let setUniverseDirect, selectPresetDirect, getContractStateSnapshot;
|
||||
// Step 6.7B: console-only. Never constructed on the Observation surface --
|
||||
// this is what makes "no second contract session" a structural fact.
|
||||
let controlBus = null;
|
||||
let contractAdapter = null;
|
||||
|
||||
// Semantic control bus & XZBT Contract Adapter
|
||||
const controlBus = new XZBTControlBus();
|
||||
// Semantic control bus & XZBT Contract Adapter. Step 6.7B: constructed
|
||||
// only in console mode -- see the isConsoleMode guard closing after
|
||||
// generativeExperience.onSpeechStateChanged below.
|
||||
if (isConsoleMode) {
|
||||
controlBus = new XZBTControlBus();
|
||||
window.xzbtControlBus = controlBus;
|
||||
|
||||
const contractAdapter = new XZBTContractAdapter({
|
||||
contractAdapter = new XZBTContractAdapter({
|
||||
product: 'SciFi-XZBT',
|
||||
version: '5.2.0',
|
||||
version: '5.3.0',
|
||||
surfaces: XZBTSurfaceMode.SURFACES(xzbtInstanceId),
|
||||
instanceId: xzbtInstanceId,
|
||||
bindings: {
|
||||
getUniverse: () => activeUniverseId,
|
||||
getPreset: () => activePresetId,
|
||||
@@ -203,7 +240,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
'fnc.enabled': (v, src) => setFncEnabledDirect(v, src),
|
||||
'view.observation': (v, src) => setObservationDirect(v, src),
|
||||
'view.viewport-frame': (v, src) => setViewportFrameDirect(v, src),
|
||||
'alert.active': (v, src) => setAlertActiveDirect(v, src)
|
||||
'alert.active': (v, src) => setAlertActiveDirect(v, src),
|
||||
// Step 6.7B: newly-canonical targets. observation-engine.js keeps
|
||||
// setPillars(); warp-flight has no engine-side setter (only
|
||||
// toggleWarpFlight()), so the desired boolean is compared against
|
||||
// the engine's current flightMode before toggling -- this keeps
|
||||
// observation-engine.js's own simulation code untouched.
|
||||
'view.pillars': v => {
|
||||
if (observationEngine) observationEngine.setPillars(!!v);
|
||||
},
|
||||
'view.warp-flight': v => {
|
||||
if (!observationEngine) return;
|
||||
const want = !!v;
|
||||
const isWarp = observationEngine.flightMode === 'warp';
|
||||
if (want !== isWarp) observationEngine.toggleWarpFlight();
|
||||
}
|
||||
},
|
||||
invokers: {
|
||||
'speech.say': async args => {
|
||||
@@ -252,43 +303,130 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
window.xzbtContractAdapter = contractAdapter;
|
||||
fncSystem.onMutation = (id, value) => contractAdapter.applyMutation(id, value, 'ui');
|
||||
generativeExperience.onMutation = (id, value) => contractAdapter.applyMutation(id, value, 'ui');
|
||||
generativeExperience.onInvoke = id => contractAdapter.invokeAction(id, {}, 'ui');
|
||||
generativeExperience.onSpeechStateChanged = state => contractAdapter.updateCapability('speech', state);
|
||||
observationEngine.onMutation = (id, value) => contractAdapter.applyMutation(id, value, 'ui');
|
||||
fncSystem.onMutation = (id, value) => contractAdapter?.applyMutation(id, value, 'ui');
|
||||
generativeExperience.onMutation = (id, value) => contractAdapter?.applyMutation(id, value, 'ui');
|
||||
generativeExperience.onInvoke = id => contractAdapter?.invokeAction(id, {}, 'ui');
|
||||
generativeExperience.onSpeechStateChanged = state => contractAdapter?.updateCapability('speech', state);
|
||||
|
||||
// Step 6.7B: exhibit-internal surface-attachment bus, owner side. Fed
|
||||
// from the adapter's session-independent onLocalChange/onLocalAction
|
||||
// hooks (contract-adapter.js SS5.4) rather than contract events, which are
|
||||
// gated on an NGN session and would go silent standalone.
|
||||
surfaceOwner = XZBTSurfaceBus.createOwner(contractAdapter, {
|
||||
channelName: XZBTSurfaceMode.channelName(xzbtInstanceId),
|
||||
// A surface attaching/detaching can flip observationAudienceActive();
|
||||
// re-evaluate the scheduler so a pane opened with the console overlay
|
||||
// off (or the last pane closing with it still off) starts/stops the
|
||||
// ambient generator correctly (report SS6, SS8's symmetric detach case).
|
||||
onParticipantsChanged: () => scheduleObservationAmbientActivity()
|
||||
});
|
||||
contractAdapter.onLocalChange = (target, value, stateRevision) => {
|
||||
surfaceOwner.broadcastState(target, value, stateRevision);
|
||||
};
|
||||
contractAdapter.onLocalAction = (target, args) => {
|
||||
surfaceOwner.broadcastAction(target, args);
|
||||
};
|
||||
} else {
|
||||
// Step 6.7B: Observation surface (non-authoritative). Never constructs
|
||||
// a contract session, control bus, or generative engine; attaches to
|
||||
// the console instance named by ?xi= and mirrors its state over the
|
||||
// exhibit-internal bus. `applyPresentationState` is defined further
|
||||
// down (function declarations hoist; these callbacks only actually run
|
||||
// once a message arrives, long after the rest of this script has run).
|
||||
surfaceLink = XZBTSurfaceBus.attach({
|
||||
channelName: XZBTSurfaceMode.channelName(xzbtInstanceId),
|
||||
timeoutMs: 1500,
|
||||
onSnapshot: (values, stateRevision, registryRevision, presentation) => {
|
||||
Object.keys(values || {}).forEach(target => applyPresentationState(target, values[target]));
|
||||
if (presentation && typeof presentation.tickerText === 'string' && presentation.tickerText) {
|
||||
const ticker = document.getElementById('observation-ticker-text');
|
||||
if (ticker) ticker.textContent = presentation.tickerText;
|
||||
}
|
||||
// The surface renders the scene unconditionally while open -- no
|
||||
// view.observation gating (report SS8: surface existence and the
|
||||
// console's own overlay flag are deliberately separate concepts).
|
||||
enterObservation();
|
||||
},
|
||||
onChange: (target, value) => applyPresentationState(target, value),
|
||||
onAction: (target, args) => {
|
||||
if (target === 'display.ticker' && args && typeof args.text === 'string') {
|
||||
const ticker = document.getElementById('observation-ticker-text');
|
||||
if (ticker) ticker.textContent = args.text;
|
||||
}
|
||||
},
|
||||
onPresentation: (kind, payload) => {
|
||||
if (kind === 'ticker' && typeof payload.tickerText === 'string') {
|
||||
const ticker = document.getElementById('observation-ticker-text');
|
||||
if (ticker) ticker.textContent = payload.tickerText;
|
||||
} else if (kind === 'obs-activity') {
|
||||
// Render the authority's decision; never independently generate
|
||||
// one (report SS6's "authority decides, surface renders").
|
||||
triggerObservationActivity(payload.source || 'ambient');
|
||||
}
|
||||
},
|
||||
onTimeout: () => {
|
||||
// No console instance open on this channel (or a stale/unknown
|
||||
// ?xi=). Degrade to a clear waiting state rather than inventing
|
||||
// local authority (report SS9, Museum's proven behavior).
|
||||
console.warn('[SciFi-XZBT] Observation surface: no console instance found for xi=' + xzbtInstanceId);
|
||||
}
|
||||
});
|
||||
|
||||
window.__xzbtSurfaceDispose = () => { if (surfaceLink) surfaceLink.detach(); };
|
||||
window.addEventListener('beforeunload', () => { if (surfaceLink) surfaceLink.detach(); });
|
||||
} // end isConsoleMode / else observation-surface attach (control bus, contract adapter, surface bus)
|
||||
|
||||
// Step 6.7B: the one dock-interaction chokepoint (already true before this
|
||||
// step for every control except PILLARS/WARP, now including them). Console
|
||||
// mode routes into the one mutation chokepoint as before; the Observation
|
||||
// surface routes the same five interactive controls to the console over the
|
||||
// bus instead, and drops view.observation (RETURN) locally -- posting it
|
||||
// would toggle the *console's* own overlay, a cross-surface side effect
|
||||
// the design explicitly forbids (report SS8). RETURN/exit are additionally
|
||||
// hidden by CSS (body.surface-observation) as the primary defense.
|
||||
const XZBT_PRESENTATION_ROUTED_TARGETS = new Set([
|
||||
'view.warp-flight', 'view.viewport-frame', 'view.pillars', 'alert.active', 'preset.selected'
|
||||
]);
|
||||
observationEngine.onMutation = isConsoleMode
|
||||
? (id, value) => contractAdapter?.applyMutation(id, value, 'ui')
|
||||
: (id, value) => {
|
||||
if (!XZBT_PRESENTATION_ROUTED_TARGETS.has(id)) return;
|
||||
if (surfaceLink) surfaceLink.mutate('set', id, value);
|
||||
};
|
||||
|
||||
// Step 6.7B: console-only (controlBus/contractAdapter don't exist otherwise).
|
||||
if (isConsoleMode) {
|
||||
// Intercept XZBTControlBus calls to route through contractAdapter
|
||||
const origBusSet = controlBus.set.bind(controlBus);
|
||||
const origBusTrigger = controlBus.trigger.bind(controlBus);
|
||||
controlBus.set = function (id, value, source = 'external') {
|
||||
if (contractAdapter.legacyIdMap.has(id) || contractAdapter.targets.has(id)) {
|
||||
const canonicalId = contractAdapter.legacyIdMap.get(id) || id;
|
||||
const target = contractAdapter.targets.get(canonicalId);
|
||||
if (contractAdapter?.legacyIdMap.has(id) || contractAdapter?.targets.has(id)) {
|
||||
const canonicalId = contractAdapter?.legacyIdMap.get(id) || id;
|
||||
const target = contractAdapter?.targets.get(canonicalId);
|
||||
let normVal = value;
|
||||
if (target && target.kind === 'range') {
|
||||
if (contractAdapter.legacyIdMap.has(id)) {
|
||||
if (contractAdapter?.legacyIdMap.has(id)) {
|
||||
const legacy = controlBus.targets.get(id);
|
||||
if (legacy?.step) normVal = Math.round(Number(value) / legacy.step) * legacy.step;
|
||||
if (canonicalId === 'view.activity') normVal = Number(normVal) / 100;
|
||||
}
|
||||
}
|
||||
contractAdapter.applyMutation(canonicalId, normVal, source);
|
||||
contractAdapter?.applyMutation(canonicalId, normVal, source);
|
||||
return true;
|
||||
}
|
||||
return origBusSet(id, value, source);
|
||||
};
|
||||
controlBus.trigger = function (id, source = 'external') {
|
||||
if (contractAdapter.legacyIdMap.has(id) || contractAdapter.targets.has(id)) {
|
||||
const canonicalId = contractAdapter.legacyIdMap.get(id) || id;
|
||||
const target = contractAdapter.targets.get(canonicalId);
|
||||
if (contractAdapter?.legacyIdMap.has(id) || contractAdapter?.targets.has(id)) {
|
||||
const canonicalId = contractAdapter?.legacyIdMap.get(id) || id;
|
||||
const target = contractAdapter?.targets.get(canonicalId);
|
||||
if (target && target.kind === 'state') {
|
||||
// Toggle state if boolean
|
||||
const curr = contractAdapter.getContractState()[canonicalId];
|
||||
contractAdapter.applyMutation(canonicalId, !curr, source);
|
||||
const curr = contractAdapter?.getContractState()[canonicalId];
|
||||
contractAdapter?.applyMutation(canonicalId, !curr, source);
|
||||
return true;
|
||||
}
|
||||
contractAdapter.invokeAction(canonicalId, {}, source);
|
||||
contractAdapter?.invokeAction(canonicalId, {}, source);
|
||||
return true;
|
||||
}
|
||||
return origBusTrigger(id, source);
|
||||
@@ -331,6 +469,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
controlBus.register('red-alert', { label: 'Red Alert Toggle', type: 'action' });
|
||||
controlBus.register('generate-announcement', { label: 'Generate AI Announcement', type: 'action' });
|
||||
controlBus.register('mute', { label: 'Master Mute', type: 'action' });
|
||||
} // end isConsoleMode (control bus semantic registration)
|
||||
|
||||
|
||||
// TV presentation controls. The final Cast stream quality remains Chrome-controlled.
|
||||
@@ -365,7 +504,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
btn.innerHTML = `<span class="u-icon">${u.icon}</span> <span>${u.name}</span>`;
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
contractAdapter.applyMutation('universe.selected', u.id, 'ui');
|
||||
contractAdapter?.applyMutation('universe.selected', u.id, 'ui');
|
||||
});
|
||||
universeDropdown.appendChild(btn);
|
||||
});
|
||||
@@ -399,6 +538,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
document.body.className = universe.themeClass;
|
||||
// Assigning className wholesale drops the TV presentation classes, so re-apply them.
|
||||
applyTV();
|
||||
// ...and the Step 6.7B surface-mode class, for the same reason.
|
||||
if (!isConsoleMode) document.body.classList.add('surface-observation');
|
||||
headerMatrixLabel.textContent = universe.headerTitle;
|
||||
|
||||
// Custom titles per universe
|
||||
@@ -472,7 +613,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
soundboardTitle.textContent = conf.soundboard;
|
||||
|
||||
// Configure Visualizer mode
|
||||
visualizer.setMode(universe.visualizer || 'warp-core');
|
||||
visualizer?.setMode(universe.visualizer || 'warp-core');
|
||||
|
||||
// Render Event Buttons & Soundboard
|
||||
renderUniverseEvents(universe);
|
||||
@@ -509,10 +650,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
async function invokeConsoleEvent(evt, btn, source) {
|
||||
if (evt.type === 'alert-red' || evt.type === 'alert-yellow') {
|
||||
const value = evt.type === 'alert-red' ? 'red' : 'yellow';
|
||||
return contractAdapter.applyMutation('alert.active', alerts.activeAlert === value ? 'none' : value, source);
|
||||
return contractAdapter?.applyMutation('alert.active', alerts.activeAlert === value ? 'none' : value, source);
|
||||
}
|
||||
const canonical = [...contractAdapter.targets.values()].find(t => t.kind === 'impulse' && t.id.startsWith('event.') && (t.id === `event.${evt.type}` || `btn-${t.id.slice(6)}` === evt.id));
|
||||
if (canonical) return contractAdapter.invokeAction(canonical.id, {}, source);
|
||||
const canonical = [...contractAdapter?.targets.values()].find(t => t.kind === 'impulse' && t.id.startsWith('event.') && (t.id === `event.${evt.type}` || `btn-${t.id.slice(6)}` === evt.id));
|
||||
if (canonical) return contractAdapter?.invokeAction(canonical.id, {}, source);
|
||||
await audioManager.resume();
|
||||
handleUniverseEvent(evt.type, btn);
|
||||
}
|
||||
@@ -531,7 +672,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const aliases = { 'btn-chirp-single': 'sfx.console-chirp', 'btn-telepathic': 'sfx.telepathic-chime', 'btn-commlock': 'sfx.comlock', 'btn-afterburner-snd': 'sfx.afterburner' };
|
||||
const canonical = aliases[key.id] || `sfx.${key.id.replace(/^btn-/, '')}`;
|
||||
if (contractAdapter.targets.has(canonical)) return contractAdapter.invokeAction(canonical, {}, 'ui');
|
||||
if (contractAdapter?.targets.has(canonical)) return contractAdapter?.invokeAction(canonical, {}, 'ui');
|
||||
await audioManager.resume();
|
||||
handleSoundboardTrigger(key.id);
|
||||
});
|
||||
@@ -553,7 +694,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
`;
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
contractAdapter.applyMutation('preset.selected', preset.id, 'ui');
|
||||
contractAdapter?.applyMutation('preset.selected', preset.id, 'ui');
|
||||
});
|
||||
|
||||
presetContainer.appendChild(btn);
|
||||
@@ -763,7 +904,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
btnPlay.addEventListener('click', () => {
|
||||
contractAdapter.applyMutation('transport.playing', !isPlaying, 'ui');
|
||||
contractAdapter?.applyMutation('transport.playing', !isPlaying, 'ui');
|
||||
});
|
||||
|
||||
// 6. Master Volume Control System
|
||||
@@ -800,65 +941,65 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
function setMasterVolume(val) {
|
||||
contractAdapter.applyMutation('mix.master', val, 'ui');
|
||||
contractAdapter?.applyMutation('mix.master', val, 'ui');
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
contractAdapter.applyMutation('transport.muted', !audioManager.isMuted, 'ui');
|
||||
contractAdapter?.applyMutation('transport.muted', !audioManager.isMuted, 'ui');
|
||||
}
|
||||
|
||||
sliderMasterVol.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.master', parseFloat(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.master', parseFloat(e.target.value), 'ui');
|
||||
});
|
||||
btnMasterMute.addEventListener('click', toggleMute);
|
||||
btnHeaderMute.addEventListener('click', toggleMute);
|
||||
btnVolDown.addEventListener('click', () => {
|
||||
contractAdapter.applyMutation('mix.master', Math.max(0, audioManager.getMasterVolume() - 0.05), 'ui');
|
||||
contractAdapter?.applyMutation('mix.master', Math.max(0, audioManager.getMasterVolume() - 0.05), 'ui');
|
||||
});
|
||||
btnVolUp.addEventListener('click', () => {
|
||||
contractAdapter.applyMutation('mix.master', Math.min(1, audioManager.getMasterVolume() + 0.05), 'ui');
|
||||
contractAdapter?.applyMutation('mix.master', Math.min(1, audioManager.getMasterVolume() + 0.05), 'ui');
|
||||
});
|
||||
|
||||
volStepButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const stepVal = parseFloat(btn.getAttribute('data-vol'));
|
||||
contractAdapter.applyMutation('mix.master', stepVal, 'ui');
|
||||
contractAdapter?.applyMutation('mix.master', stepVal, 'ui');
|
||||
});
|
||||
});
|
||||
|
||||
// 7. Channel Synthesizer Sliders
|
||||
sliderHullVol.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.hull.level', parseFloat(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.hull.level', parseFloat(e.target.value), 'ui');
|
||||
});
|
||||
sliderHullFreq.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.hull.frequency', parseFloat(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.hull.frequency', parseFloat(e.target.value), 'ui');
|
||||
});
|
||||
sliderHullCutoff.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.hull.cutoff', parseFloat(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.hull.cutoff', parseFloat(e.target.value), 'ui');
|
||||
});
|
||||
|
||||
sliderWarpVol.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.drive.level', parseFloat(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.drive.level', parseFloat(e.target.value), 'ui');
|
||||
});
|
||||
sliderWarpBpm.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.drive.pulse-rate', parseInt(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.drive.pulse-rate', parseInt(e.target.value), 'ui');
|
||||
});
|
||||
sliderWarpCarrier.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.drive.carrier', parseFloat(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.drive.carrier', parseFloat(e.target.value), 'ui');
|
||||
});
|
||||
|
||||
sliderAirVol.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.environment.level', parseFloat(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.environment.level', parseFloat(e.target.value), 'ui');
|
||||
});
|
||||
sliderAirCutoff.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.environment.cutoff', parseFloat(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.environment.cutoff', parseFloat(e.target.value), 'ui');
|
||||
});
|
||||
|
||||
sliderTelemetryVol.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.telemetry.level', parseFloat(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.telemetry.level', parseFloat(e.target.value), 'ui');
|
||||
});
|
||||
sliderTelemetryDensity.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.telemetry.density', parseFloat(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.telemetry.density', parseFloat(e.target.value), 'ui');
|
||||
});
|
||||
|
||||
// 8. Universe Event Handlers
|
||||
@@ -872,7 +1013,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
case 'warp':
|
||||
case 'quantum':
|
||||
alerts.synthesizeWarpJump();
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'alert-red':
|
||||
case 'action-stations':
|
||||
@@ -902,11 +1043,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
break;
|
||||
case 'demat':
|
||||
whataverseAudio.synthesizeDematCycle(4);
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'vortex':
|
||||
whataverseAudio.synthesizeDematCycle(1);
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'cloister':
|
||||
if (whataverseAudio.activeCloister) {
|
||||
@@ -923,7 +1064,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
case 'epstein':
|
||||
case 'subdrive':
|
||||
expandedAudio.synthesizeEpsteinBurn();
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'purge':
|
||||
case 'decompress':
|
||||
@@ -936,11 +1077,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
case 'starburst':
|
||||
case 'biodefense':
|
||||
expandedAudio.synthesizeStarburst();
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'neural':
|
||||
expandedAudio.synthesizeNeuralBondSwell();
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'centrifuge':
|
||||
case 'astrogator':
|
||||
@@ -951,7 +1092,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
break;
|
||||
case 'ftl-jump':
|
||||
expandedAudio.synthesizeFtlJump();
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'flak':
|
||||
expandedAudio.synthesizeFlakBarrageBurst();
|
||||
@@ -960,29 +1101,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
case 'gravity':
|
||||
case 'rotation':
|
||||
expandedAudio.synthesizeSingularityEngage();
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'solar':
|
||||
expandedAudio.synthesizeSolarRoar();
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'afterburner':
|
||||
expandedAudio.synthesizeAfterburner();
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'ludicrous':
|
||||
expandedAudio.synthesizeLudicrousSpeed();
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'improbability':
|
||||
expandedAudio.synthesizeImprobabilityFlip();
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
default:
|
||||
telemetry.synthesizeLCARDSingleChirp();
|
||||
break;
|
||||
}
|
||||
generativeExperience.describeExistingEvent(type);
|
||||
generativeExperience?.describeExistingEvent(type);
|
||||
}
|
||||
|
||||
function handleSoundboardTrigger(id) {
|
||||
@@ -2675,8 +2816,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return spawnObservationTransient(obsExpandingRing(obsInt(450,1150),obsInt(250,650),obsPick(['#06b6d4','#f43f5e','#fbbf24']),obsInt(45,100)),3500);
|
||||
}
|
||||
|
||||
// Step 6.7B: an "audience" is the console's own overlay OR at least one
|
||||
// attached Observation surface. Scheduling/deciding activity uses this;
|
||||
// *local rendering* still gates on observationActive alone (report SS6's
|
||||
// audience-scheduler correction -- without it, an audience watching only
|
||||
// the surface would see a static scene forever).
|
||||
const observationAudienceActive = () => observationActive || !!(surfaceOwner && surfaceOwner.attachedCount() > 0);
|
||||
|
||||
function triggerObservationActivity(source = 'ambient') {
|
||||
if (!observationActive || observationActivity <= 0.001) return;
|
||||
if (!observationAudienceActive() || observationActivity <= 0.001) return;
|
||||
// The authority decides; attached surfaces render the same decision
|
||||
// rather than independently rolling their own random event (report SS6,
|
||||
// "Transient activity events are mirrored, not regenerated"). Exact
|
||||
// visual parameters (which contact, which ring) are NOT byte-identical
|
||||
// across documents -- see the accepted-limitations note in README.md /
|
||||
// agents.md; only the "an event happens now" decision is synchronized.
|
||||
if (isConsoleMode && surfaceOwner) surfaceOwner.broadcastPresentation('obs-activity', { source });
|
||||
|
||||
// Telemetry is a synchronization pulse, but the slider controls whether
|
||||
// that pulse becomes visible. At maximum every telemetry pulse produces
|
||||
@@ -2713,7 +2868,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
clearTimeout(observationAmbientTimer);
|
||||
observationAmbientTimer = null;
|
||||
}
|
||||
if (!observationActive || observationActivity <= .01) return;
|
||||
// The Observation surface never runs its own scheduler -- it only
|
||||
// renders 'obs-activity' decisions relayed from the authority. Without
|
||||
// this guard two documents would each roll independent random timers,
|
||||
// violating "no duplicate random event generation" (report SS6/R3).
|
||||
if (!isConsoleMode) return;
|
||||
if (!observationAudienceActive() || observationActivity <= .01) return;
|
||||
|
||||
// Quiet = occasional subtle life. Active = a busy generative display.
|
||||
const slow = 12500;
|
||||
@@ -2798,7 +2958,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
updateObservationHudMode();
|
||||
|
||||
sliderObservationActivity.addEventListener('input', () => {
|
||||
contractAdapter.applyMutation('view.activity', Number(sliderObservationActivity.value) / 100, 'ui');
|
||||
contractAdapter?.applyMutation('view.activity', Number(sliderObservationActivity.value) / 100, 'ui');
|
||||
});
|
||||
|
||||
window.addEventListener('scifi-telemetry-activity', (event) => {
|
||||
@@ -4419,6 +4579,48 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6.7B: applies one mirrored contract-state value on the
|
||||
// Observation surface. Reuses the existing local setters/functions
|
||||
// directly -- never contractAdapter.applyMutation (there is none here) --
|
||||
// so this is local *rendering*, not a second computation of authority.
|
||||
// Targets the renderer doesn't care about (mix.*, fnc.*, transport.muted,
|
||||
// speech.*) are intentionally ignored.
|
||||
function applyPresentationState(target, value) {
|
||||
switch (target) {
|
||||
case 'universe.selected':
|
||||
if (value && value !== activeUniverseId) setUniverse(value);
|
||||
break;
|
||||
case 'preset.selected':
|
||||
if (value && value !== activePresetId) selectPreset(value);
|
||||
break;
|
||||
case 'view.viewport-frame':
|
||||
if (observationEngine) observationEngine.setViewport(!!value);
|
||||
break;
|
||||
case 'view.pillars':
|
||||
if (observationEngine) observationEngine.setPillars(!!value);
|
||||
break;
|
||||
case 'view.warp-flight': {
|
||||
if (!observationEngine) break;
|
||||
const isWarp = observationEngine.flightMode === 'warp';
|
||||
if (!!value !== isWarp) observationEngine.toggleWarpFlight();
|
||||
break;
|
||||
}
|
||||
case 'alert.active':
|
||||
if (typeof setAlertActiveDirect === 'function') setAlertActiveDirect(value);
|
||||
break;
|
||||
case 'view.activity':
|
||||
setObservationActivity(Number(value) * 100);
|
||||
break;
|
||||
case 'transport.playing':
|
||||
// Cosmetic only (the "PROCEDURAL AUDIO LINK" status line) -- never
|
||||
// engages audio on the surface.
|
||||
isPlaying = !!value;
|
||||
break;
|
||||
default:
|
||||
break; // view.observation, mix.*, fnc.*, speech.*, transport.muted: not rendered here.
|
||||
}
|
||||
}
|
||||
|
||||
function refreshObservation() {
|
||||
const universe = UniverseRegistry[activeUniverseId];
|
||||
const preset = universe && universe.presets ? universe.presets[activePresetId] : null;
|
||||
@@ -4442,18 +4644,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
function enterObservation() {
|
||||
if (observationActive) return;
|
||||
generativeExperience.prepareExperience().catch(err => console.warn('Generative preload failed', err));
|
||||
// Step 6.7B / R2: the single most important guard in this design.
|
||||
// prepareExperience() lazily loads WebLLM + Kokoro (agents.md's one
|
||||
// documented network/import exception); calling it unconditionally
|
||||
// here would make every opened Observation surface download a
|
||||
// multi-gigabyte model. Console-only.
|
||||
if (isConsoleMode) {
|
||||
generativeExperience.prepareExperience().catch(err => console.warn('Generative preload failed', err));
|
||||
}
|
||||
observationActive = true;
|
||||
observationOverlay.classList.add('active');
|
||||
observationOverlay.setAttribute('aria-hidden', 'false');
|
||||
refreshObservation();
|
||||
resetObservationHudFade();
|
||||
resetObservationReturnFade();
|
||||
scheduleObservationAmbientActivity();
|
||||
if (isConsoleMode) {
|
||||
// The Observation surface never runs its own scheduler -- it renders
|
||||
// activity decisions broadcast by the authority (report SS6).
|
||||
scheduleObservationAmbientActivity();
|
||||
|
||||
// Give the display an immediate but restrained sign of life.
|
||||
if (observationActivity > .08) {
|
||||
window.setTimeout(() => triggerObservationActivity('ambient'), 700);
|
||||
// Give the display an immediate but restrained sign of life.
|
||||
if (observationActivity > .08) {
|
||||
window.setTimeout(() => triggerObservationActivity('ambient'), 700);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4502,7 +4715,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
btnObservation.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
contractAdapter.applyMutation('view.observation', !observationActive, 'ui');
|
||||
contractAdapter?.applyMutation('view.observation', !observationActive, 'ui');
|
||||
});
|
||||
|
||||
// Click background to exit, but ignore clicks on the control dock or its buttons
|
||||
@@ -4510,7 +4723,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (e.target.closest('#observation-control-dock')) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
contractAdapter.applyMutation('view.observation', false, 'ui');
|
||||
contractAdapter?.applyMutation('view.observation', false, 'ui');
|
||||
});
|
||||
|
||||
const observationReturnPill = document.getElementById('observation-return-pill');
|
||||
@@ -4518,19 +4731,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
observationReturnPill.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
contractAdapter.applyMutation('view.observation', false, 'ui');
|
||||
contractAdapter?.applyMutation('view.observation', false, 'ui');
|
||||
});
|
||||
}
|
||||
|
||||
// 10. Keyboard Shortcuts
|
||||
// 10. Keyboard Shortcuts. Step 6.7B: console-only -- a keystroke on the
|
||||
// Observation surface must not mutate state (report SS7).
|
||||
if (isConsoleMode) {
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;
|
||||
|
||||
if (e.code === 'Space') {
|
||||
e.preventDefault();
|
||||
contractAdapter.applyMutation('transport.playing', !isPlaying, 'hotkey');
|
||||
contractAdapter?.applyMutation('transport.playing', !isPlaying, 'hotkey');
|
||||
} else if (e.code === 'KeyM') {
|
||||
contractAdapter.applyMutation('transport.muted', !audioManager.isMuted, 'hotkey');
|
||||
contractAdapter?.applyMutation('transport.muted', !audioManager.isMuted, 'hotkey');
|
||||
} else if (e.code === 'KeyW') {
|
||||
if (observationActive && observationEngine) {
|
||||
observationEngine.toggleWarpFlight();
|
||||
@@ -4541,7 +4756,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (primaryEvent) invokeConsoleEvent(primaryEvent, primaryActionBtn, 'hotkey');
|
||||
} else if (e.code === 'KeyF') {
|
||||
if (observationActive && observationEngine) {
|
||||
contractAdapter.applyMutation('view.viewport-frame', !observationEngine.showViewport, 'hotkey');
|
||||
contractAdapter?.applyMutation('view.viewport-frame', !observationEngine.showViewport, 'hotkey');
|
||||
return;
|
||||
}
|
||||
} else if (e.code === 'KeyP') {
|
||||
@@ -4552,7 +4767,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
} else if (e.code === 'KeyR') {
|
||||
if (observationActive && observationEngine) {
|
||||
const nextAlert = alerts.activeAlert === 'red' ? 'none' : 'red';
|
||||
contractAdapter.applyMutation('alert.active', nextAlert, 'hotkey');
|
||||
contractAdapter?.applyMutation('alert.active', nextAlert, 'hotkey');
|
||||
return;
|
||||
}
|
||||
const firstAlertBtn = universeEventsContainer.children[1] || universeEventsContainer.children[0];
|
||||
@@ -4561,23 +4776,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (alertEvent) invokeConsoleEvent(alertEvent, firstAlertBtn, 'hotkey');
|
||||
} else if (e.code === 'ArrowUp' || e.code === 'Equal' || e.code === 'NumpadAdd') {
|
||||
e.preventDefault();
|
||||
contractAdapter.applyMutation('mix.master', Math.min(1, audioManager.getMasterVolume() + 0.05), 'hotkey');
|
||||
contractAdapter?.applyMutation('mix.master', Math.min(1, audioManager.getMasterVolume() + 0.05), 'hotkey');
|
||||
} else if (e.code === 'ArrowDown' || e.code === 'Minus' || e.code === 'NumpadSubtract') {
|
||||
e.preventDefault();
|
||||
contractAdapter.applyMutation('mix.master', Math.max(0, audioManager.getMasterVolume() - 0.05), 'hotkey');
|
||||
contractAdapter?.applyMutation('mix.master', Math.max(0, audioManager.getMasterVolume() - 0.05), 'hotkey');
|
||||
} else if (e.code >= 'Digit1' && e.code <= 'Digit9') {
|
||||
const num = parseInt(e.code.replace('Digit', ''));
|
||||
contractAdapter.applyMutation('mix.master', num * 0.1, 'hotkey');
|
||||
contractAdapter?.applyMutation('mix.master', num * 0.1, 'hotkey');
|
||||
} else if (e.code === 'Digit0') {
|
||||
contractAdapter.applyMutation('mix.master', 1.0, 'hotkey');
|
||||
contractAdapter?.applyMutation('mix.master', 1.0, 'hotkey');
|
||||
} else if (e.code === 'Escape') {
|
||||
if (observationActive) {
|
||||
contractAdapter.applyMutation('view.observation', false, 'hotkey');
|
||||
contractAdapter?.applyMutation('view.observation', false, 'hotkey');
|
||||
return;
|
||||
}
|
||||
contractAdapter.applyMutation('alert.active', 'none', 'hotkey');
|
||||
contractAdapter?.applyMutation('alert.active', 'none', 'hotkey');
|
||||
}
|
||||
});
|
||||
} // end isConsoleMode (keyboard shortcuts)
|
||||
|
||||
// --- Implementation of Absolute Setters and Contract Helpers ---
|
||||
setMasterVolumeDirect = function (val, isMuted = false) {
|
||||
@@ -4688,6 +4904,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
'fnc.level': fncSystem ? fncSystem.level : 0.5,
|
||||
'view.observation': observationActive,
|
||||
'view.viewport-frame': observationEngine ? observationEngine.showViewport : true,
|
||||
'view.pillars': observationEngine ? observationEngine.showPillars : false,
|
||||
'view.warp-flight': observationEngine ? observationEngine.flightMode === 'warp' : false,
|
||||
'view.activity': observationActivity,
|
||||
'alert.active': alertStatus,
|
||||
'universe.selected': activeUniverseId,
|
||||
@@ -4699,5 +4917,5 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
initUniverseDropdown();
|
||||
setUniverse('starfleet');
|
||||
updateMasterVolumeUI(0.75, false);
|
||||
visualizer.init('spectrum-canvas', 'warp-core-canvas');
|
||||
visualizer?.init('spectrum-canvas', 'warp-core-canvas');
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* XZBT Contract Adapter — Specification v5.2
|
||||
* XZBT Contract Adapter — Specification v5.3
|
||||
*
|
||||
* Implements the normalized contract interface for SciFi-XZBT:
|
||||
* - Canonical external target catalog & descriptors
|
||||
@@ -15,10 +15,10 @@
|
||||
class XZBTContractAdapter {
|
||||
constructor(options = {}) {
|
||||
this.product = options.product || 'SciFi-XZBT';
|
||||
this.version = options.version || '5.2.0';
|
||||
this.version = options.version || '5.3.0';
|
||||
this.build = options.build || 'production';
|
||||
this.contractMajor = 5;
|
||||
this.contractMinor = 2;
|
||||
this.contractMinor = 3;
|
||||
|
||||
this.sessionId = null;
|
||||
this.sessionActive = false;
|
||||
@@ -28,6 +28,17 @@
|
||||
this.operationQueue = Promise.resolve();
|
||||
this.eventSink = null;
|
||||
|
||||
// Surface catalog (Contract 5.3 §31.2/§31.3). Advertised in describe()
|
||||
// only when non-empty, so absence stays §31.3 form 1 ("absent").
|
||||
this.surfaces = Array.isArray(options.surfaces) ? options.surfaces : null;
|
||||
this.instanceId = options.instanceId || null;
|
||||
|
||||
// Session-independent local-change hooks (Step 6.7B). Unlike
|
||||
// _emitEvent, these fire regardless of sessionActive so an
|
||||
// exhibit-internal surface mirror keeps working with no NGN attached.
|
||||
this.onLocalChange = typeof options.onLocalChange === 'function' ? options.onLocalChange : null;
|
||||
this.onLocalAction = typeof options.onLocalAction === 'function' ? options.onLocalAction : null;
|
||||
|
||||
// Transport hooks
|
||||
this.onMessage = options.onMessage || (() => {});
|
||||
this.hostOrigin = options.hostOrigin || null; // optional origin filter
|
||||
@@ -355,6 +366,31 @@
|
||||
requires: ['observation']
|
||||
});
|
||||
|
||||
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']
|
||||
});
|
||||
|
||||
reg({
|
||||
id: 'alert.active',
|
||||
label: 'Active Tactical Alert Status',
|
||||
@@ -416,9 +452,9 @@
|
||||
restorable: false,
|
||||
category: 'speech',
|
||||
requires: ['speech'],
|
||||
args: {
|
||||
text: { type: 'string', required: true, maxLength: 2000 }
|
||||
}
|
||||
arguments: [
|
||||
{ name: 'text', type: 'string', required: true, maxLength: 2000 }
|
||||
]
|
||||
});
|
||||
|
||||
reg({
|
||||
@@ -442,9 +478,9 @@
|
||||
restorable: false,
|
||||
category: 'display',
|
||||
requires: ['display-text'],
|
||||
args: {
|
||||
text: { type: 'string', required: true, maxLength: 512 }
|
||||
}
|
||||
arguments: [
|
||||
{ name: 'text', type: 'string', required: true, maxLength: 512 }
|
||||
]
|
||||
});
|
||||
|
||||
// --- 4. Canonical Universe Event Impulses ---
|
||||
@@ -555,7 +591,7 @@
|
||||
return;
|
||||
}
|
||||
// Ignore non-XZBT messages
|
||||
if (!e.data || typeof e.data !== 'object' || e.data.xzbt !== '5.2') {
|
||||
if (!e.data || typeof e.data !== 'object' || typeof e.data.xzbt !== 'string') {
|
||||
return;
|
||||
}
|
||||
if (e.data.sequence || e.data.type === 'error' || /\.result$/.test(e.data.type)) return;
|
||||
@@ -616,6 +652,15 @@
|
||||
stateRevision: this.stateRevision, target: target.id, value: after[target.id]
|
||||
}, correlationId, source);
|
||||
}
|
||||
// Session-independent mirror feed (Step 6.7B). _emitEvent above is
|
||||
// gated on sessionActive and therefore silent with no NGN attached;
|
||||
// onLocalChange is not, so an exhibit-internal surface mirror keeps
|
||||
// working standalone. Fires for every changed target, same as above.
|
||||
if (this.onLocalChange) {
|
||||
for (const target of changes) {
|
||||
this.onLocalChange(target.id, after[target.id], this.stateRevision);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -755,11 +800,13 @@
|
||||
|
||||
// Validate args schema & text length limits if defined
|
||||
const sanitizedArgs = (args && typeof args === 'object' && !Array.isArray(args)) ? { ...args } : {};
|
||||
if (Object.keys(sanitizedArgs).some(key => !target.args || !target.args[key])) {
|
||||
const argumentSpecs = target.arguments || [];
|
||||
if (Object.keys(sanitizedArgs).some(key => !argumentSpecs.some(spec => spec.name === key))) {
|
||||
return { ok: false, code: 'INVALID_VALUE', message: 'Unexpected argument key' };
|
||||
}
|
||||
if (target.args) {
|
||||
for (const [argKey, argSpec] of Object.entries(target.args)) {
|
||||
if (argumentSpecs.length) {
|
||||
for (const argSpec of argumentSpecs) {
|
||||
const argKey = argSpec.name;
|
||||
if (argSpec.required && (sanitizedArgs[argKey] === undefined || sanitizedArgs[argKey] === null)) {
|
||||
return { ok: false, code: 'INVALID_VALUE', message: `Missing required argument '${argKey}'` };
|
||||
}
|
||||
@@ -823,6 +870,11 @@
|
||||
args: sanitizedArgs
|
||||
}, correlationId, source);
|
||||
|
||||
// Session-independent mirror feed (Step 6.7B); see _commitChanges.
|
||||
if (this.onLocalAction) {
|
||||
this.onLocalAction(canonicalId, sanitizedArgs);
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@@ -836,7 +888,7 @@
|
||||
|
||||
// Public API: Protocol Description
|
||||
describe() {
|
||||
return {
|
||||
const description = {
|
||||
exhibit: {
|
||||
product: this.product,
|
||||
version: this.version,
|
||||
@@ -851,6 +903,10 @@
|
||||
capabilities: Array.from(this.capabilities.values()),
|
||||
targets: Array.from(this.targets.values())
|
||||
};
|
||||
if (this.surfaces && this.surfaces.length) {
|
||||
description.surfaces = this.surfaces;
|
||||
}
|
||||
return description;
|
||||
}
|
||||
|
||||
// Update Capability state and emit capability.changed
|
||||
|
||||
@@ -204,7 +204,12 @@ class ObservationEngine {
|
||||
if (this.btnWarp) {
|
||||
this.btnWarp.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.toggleWarpFlight();
|
||||
// Step 6.7B: route through the canonical view.warp-flight target,
|
||||
// like btnFrame's view.viewport-frame, instead of mutating engine
|
||||
// state directly -- this is what lets the Observation surface and
|
||||
// the console converge through the one mutation chokepoint.
|
||||
if (this.onMutation) this.onMutation('view.warp-flight', this.flightMode !== 'warp');
|
||||
else this.toggleWarpFlight();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -219,7 +224,9 @@ class ObservationEngine {
|
||||
if (this.btnPillars) {
|
||||
this.btnPillars.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.togglePillars();
|
||||
// Step 6.7B: route through the canonical view.pillars target.
|
||||
if (this.onMutation) this.onMutation('view.pillars', !this.showPillars);
|
||||
else this.togglePillars();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* XZBT Surface Bus — SciFi-XZBT (Step 6.7B)
|
||||
*
|
||||
* Exhibit-internal state mirror between the authoritative console document
|
||||
* and the non-authoritative Observation surface document. Adapted from
|
||||
* Museum Gallery's proven `surface-bus.js` shape (Steps 6.3-6.6), with one
|
||||
* deliberate improvement: the BroadcastChannel name is scoped to a single
|
||||
* console instance id (XZBTSurfaceMode.channelName), so two console tabs on
|
||||
* one origin cannot cross-talk.
|
||||
*
|
||||
* This carries NO XZBT Exhibit Contract envelope — no `xzbt` field, no
|
||||
* `sessionId` — and is never observed by an NGN host (Contract 5.3 §31.7's
|
||||
* explicit allowance for exhibit-internal transports). Message shapes:
|
||||
*
|
||||
* 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 }
|
||||
*/
|
||||
(function (root) {
|
||||
'use strict';
|
||||
|
||||
function randomId() {
|
||||
return 'req-' + Math.random().toString(16).slice(2) + Date.now().toString(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner side. Call once, in the console (authoritative) document, after
|
||||
* the XZBTContractAdapter has been constructed.
|
||||
*
|
||||
* @param {object} adapter an XZBTContractAdapter instance
|
||||
* @param {object} options
|
||||
* @param {string} options.channelName instance-scoped channel name
|
||||
* @param {function} [options.onParticipantsChanged] (attachedCount)
|
||||
*/
|
||||
function createOwner(adapter, options) {
|
||||
options = options || {};
|
||||
if (!options.channelName) {
|
||||
throw new Error('surface-bus: createOwner requires options.channelName');
|
||||
}
|
||||
var channel = new BroadcastChannel(options.channelName);
|
||||
var lastPresentation = { tickerText: '' };
|
||||
|
||||
// Set-based participant bookkeeping (not a counter): a duplicate/late
|
||||
// attach or detach can never ratchet the count (Museum's lesson).
|
||||
var participants = new Set();
|
||||
var closed = false;
|
||||
|
||||
function notifyParticipantsChanged() {
|
||||
if (typeof options.onParticipantsChanged === 'function') {
|
||||
options.onParticipantsChanged(participants.size);
|
||||
}
|
||||
}
|
||||
|
||||
function currentValues() {
|
||||
var state = adapter.getContractState();
|
||||
var values = {};
|
||||
adapter.targets.forEach(function (target, id) {
|
||||
if (target.readable && target.kind !== 'impulse') {
|
||||
values[id] = state[id];
|
||||
}
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
channel.onmessage = function (ev) {
|
||||
var msg = ev.data;
|
||||
if (!msg || typeof msg !== 'object') return;
|
||||
|
||||
if (msg.type === 'attach') {
|
||||
if (typeof msg.participantId === 'string') {
|
||||
participants.add(msg.participantId);
|
||||
}
|
||||
channel.postMessage({
|
||||
type: 'attach.snapshot',
|
||||
inReplyTo: msg.requestId,
|
||||
values: currentValues(),
|
||||
stateRevision: adapter.stateRevision,
|
||||
registryRevision: adapter.registryRevision,
|
||||
presentation: lastPresentation
|
||||
});
|
||||
notifyParticipantsChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'mutate') {
|
||||
// Every surface-originated interaction goes through exactly the
|
||||
// same applyMutation/invokeAction chokepoint a contract set/invoke
|
||||
// from NGN would use. Source is always 'ui'.
|
||||
if (msg.kind === 'set' && typeof msg.target === 'string') {
|
||||
adapter.applyMutation(msg.target, msg.value, 'ui');
|
||||
} else if (msg.kind === 'invoke' && typeof msg.target === 'string') {
|
||||
adapter.invokeAction(msg.target, msg.args || {}, 'ui');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'detach') {
|
||||
if (typeof msg.participantId === 'string' && participants.delete(msg.participantId)) {
|
||||
notifyParticipantsChanged();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
channel: channel,
|
||||
attachedCount: function () { return participants.size; },
|
||||
/** Broadcast one changed target (fed from adapter.onLocalChange). */
|
||||
broadcastState: function (target, value, stateRevision) {
|
||||
if (closed) return;
|
||||
channel.postMessage({ type: 'state', target: target, value: value, stateRevision: stateRevision });
|
||||
},
|
||||
/** Broadcast an executed impulse (fed from adapter.onLocalAction). */
|
||||
broadcastAction: function (target, args) {
|
||||
if (closed) return;
|
||||
channel.postMessage({ type: 'action', target: target, args: args || {} });
|
||||
},
|
||||
/** Broadcast a transient presentation-only extra (ticker text, an
|
||||
* ambient-activity decision). Not contract state. */
|
||||
broadcastPresentation: function (kind, payload) {
|
||||
if (closed) return;
|
||||
var msg = Object.assign({ type: 'presentation', kind: kind }, payload || {});
|
||||
if (kind === 'ticker' && payload && typeof payload.tickerText === 'string') {
|
||||
lastPresentation = { tickerText: payload.tickerText };
|
||||
}
|
||||
channel.postMessage(msg);
|
||||
},
|
||||
detach: function () {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
channel.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Surface side. Call from the Observation surface document. Requests
|
||||
* attachment and waits `timeoutMs` for a reply; if no owner document is
|
||||
* open on the same channel, `onTimeout` fires and the surface must
|
||||
* degrade to a waiting state rather than inventing its own authority.
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {string} options.channelName instance-scoped channel name
|
||||
* @param {number} [options.timeoutMs]
|
||||
* @param {function} [options.onSnapshot] (values, stateRevision, registryRevision, presentation)
|
||||
* @param {function} [options.onChange] (target, value, stateRevision)
|
||||
* @param {function} [options.onAction] (target, args)
|
||||
* @param {function} [options.onPresentation] (kind, payload)
|
||||
* @param {function} [options.onTimeout]
|
||||
*/
|
||||
function attach(options) {
|
||||
options = options || {};
|
||||
if (!options.channelName) {
|
||||
throw new Error('surface-bus: attach requires options.channelName');
|
||||
}
|
||||
var channel = new BroadcastChannel(options.channelName);
|
||||
var requestId = randomId();
|
||||
// A fresh participantId per attach() call. A reload is a brand-new
|
||||
// attach() and therefore a brand-new id, which is what lets the
|
||||
// owner's Set-based bookkeeping retire the old identity cleanly.
|
||||
var participantId = randomId();
|
||||
var attachedFlag = false;
|
||||
var detachedFlag = false;
|
||||
|
||||
var timer = setTimeout(function () {
|
||||
if (attachedFlag) return;
|
||||
if (typeof options.onTimeout === 'function') options.onTimeout();
|
||||
}, options.timeoutMs || 1500);
|
||||
|
||||
channel.onmessage = function (ev) {
|
||||
var msg = ev.data;
|
||||
if (!msg || typeof msg !== 'object') return;
|
||||
|
||||
if (msg.type === 'attach.snapshot' && msg.inReplyTo === requestId && !attachedFlag) {
|
||||
attachedFlag = true;
|
||||
clearTimeout(timer);
|
||||
if (typeof options.onSnapshot === 'function') {
|
||||
options.onSnapshot(msg.values, msg.stateRevision, msg.registryRevision, msg.presentation);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!attachedFlag) return;
|
||||
|
||||
if (msg.type === 'state') {
|
||||
if (typeof options.onChange === 'function') {
|
||||
options.onChange(msg.target, msg.value, msg.stateRevision);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'action') {
|
||||
if (typeof options.onAction === 'function') {
|
||||
options.onAction(msg.target, msg.args);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'presentation') {
|
||||
if (typeof options.onPresentation === 'function') {
|
||||
options.onPresentation(msg.kind, msg);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
channel.postMessage({ type: 'attach', requestId: requestId, participantId: participantId });
|
||||
|
||||
function detach() {
|
||||
// Idempotent: local-surfaces.js's pre-removal dispose call and this
|
||||
// document's own beforeunload handler can both fire for the same
|
||||
// attachment. Only the first actually posts; every call tears down
|
||||
// the timer/channel so repeated calls are always safe.
|
||||
if (attachedFlag && !detachedFlag) {
|
||||
detachedFlag = true;
|
||||
channel.postMessage({ type: 'detach', participantId: participantId });
|
||||
}
|
||||
clearTimeout(timer);
|
||||
channel.close();
|
||||
}
|
||||
|
||||
return {
|
||||
channel: channel,
|
||||
isAttached: function () { return attachedFlag; },
|
||||
/** Route a native surface interaction through the owner's canonical
|
||||
* mutation path. No-op (returns false) if not yet attached. */
|
||||
mutate: function (kind, target, valueOrArgs) {
|
||||
if (!attachedFlag) return false;
|
||||
var payload = { type: 'mutate', kind: kind, target: target };
|
||||
if (kind === 'set') payload.value = valueOrArgs;
|
||||
else payload.args = valueOrArgs || {};
|
||||
channel.postMessage(payload);
|
||||
return true;
|
||||
},
|
||||
detach: detach
|
||||
};
|
||||
}
|
||||
|
||||
var XZBTSurfaceBus = {
|
||||
createOwner: createOwner,
|
||||
attach: attach
|
||||
};
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = XZBTSurfaceBus;
|
||||
}
|
||||
if (root) {
|
||||
root.XZBTSurfaceBus = XZBTSurfaceBus;
|
||||
}
|
||||
})(typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : this));
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* XZBT Surface Mode — SciFi-XZBT (Step 6.7B)
|
||||
*
|
||||
* Pure, testable seam for resolving which boot mode this document load is in
|
||||
* (Contract 5.3 §31.4 query-string surface form) and for describing the
|
||||
* exhibit's two-entry surface catalog. No DOM access, no imports, no
|
||||
* side effects — safe to load and exercise under Node's `vm` module.
|
||||
*
|
||||
* Two surfaces only:
|
||||
* - surface.console the full operator console (index.html, primary)
|
||||
* - surface.observation the same document, rendering only the
|
||||
* Observation viewscreen, addressed by
|
||||
* ?surface=observation&xi=<instanceId>
|
||||
*/
|
||||
(function (root) {
|
||||
'use strict';
|
||||
|
||||
var CHANNEL_PREFIX = 'xzbt-scifi-surface-v1:';
|
||||
|
||||
/**
|
||||
* Resolve the boot mode + instance id for a location-like object.
|
||||
* @param {object} locationLike anything with a `.search` (or `.href`)
|
||||
* string, e.g. `window.location`.
|
||||
* @returns {{mode: 'console'|'observation', instanceId: string|null}}
|
||||
*/
|
||||
function resolve(locationLike) {
|
||||
var search = '';
|
||||
if (locationLike && typeof locationLike.search === 'string') {
|
||||
search = locationLike.search;
|
||||
} else if (locationLike && typeof locationLike.href === 'string') {
|
||||
var qIdx = locationLike.href.indexOf('?');
|
||||
search = qIdx >= 0 ? locationLike.href.slice(qIdx) : '';
|
||||
}
|
||||
|
||||
var params;
|
||||
try {
|
||||
params = new URLSearchParams(search);
|
||||
} catch (err) {
|
||||
params = null;
|
||||
}
|
||||
|
||||
var surfaceParam = params ? params.get('surface') : null;
|
||||
var instanceId = params ? params.get('xi') : null;
|
||||
|
||||
if (surfaceParam === 'observation') {
|
||||
return { mode: 'observation', instanceId: instanceId || null };
|
||||
}
|
||||
|
||||
// Unknown/absent `?surface=` values resolve to console. `xi` is
|
||||
// meaningless in console mode.
|
||||
return { mode: 'console', instanceId: null };
|
||||
}
|
||||
|
||||
/** Mint a fresh per-document-load instance id. */
|
||||
function newInstanceId() {
|
||||
return 'xi-' + Math.random().toString(16).slice(2) + Date.now().toString(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* The exhibit's two-entry surface catalog (Contract 5.3 §31.2/§31.3).
|
||||
* @param {string} instanceId the live console instance id.
|
||||
*/
|
||||
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.'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/** The instance-scoped BroadcastChannel name for a console instance id. */
|
||||
function channelName(instanceId) {
|
||||
return CHANNEL_PREFIX + instanceId;
|
||||
}
|
||||
|
||||
var XZBTSurfaceMode = {
|
||||
resolve: resolve,
|
||||
newInstanceId: newInstanceId,
|
||||
SURFACES: SURFACES,
|
||||
channelName: channelName
|
||||
};
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = XZBTSurfaceMode;
|
||||
}
|
||||
if (root) {
|
||||
root.XZBTSurfaceMode = XZBTSurfaceMode;
|
||||
}
|
||||
})(typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : this));
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* XZBT Exhibit Contract 5.2 — generic contract core.
|
||||
* XZBT Exhibit Contract 5.3 — generic contract core.
|
||||
*
|
||||
* WHY THIS FILE IS GENERIC
|
||||
* ------------------------
|
||||
@@ -26,25 +26,33 @@
|
||||
'use strict';
|
||||
|
||||
var CONTRACT_MAJOR = 5;
|
||||
var CONTRACT_MINOR = 2;
|
||||
var XZBT_VERSION = '5.2';
|
||||
var CONTRACT_MINOR = 3;
|
||||
var XZBT_VERSION = '5.3';
|
||||
|
||||
/*
|
||||
* Contract 5.3 presentation-surface support (additive, optional).
|
||||
* Contract 5.3 presentation-surface support.
|
||||
*
|
||||
* Every existing exhibit that does not pass `contractMinor`, `xzbtVersion`,
|
||||
* or `surfaces` to ContractCore gets byte-identical behavior to before this
|
||||
* addition: the defaults below equal the pre-5.3 constants exactly, and
|
||||
* `describe()` omits the `surfaces` key entirely unless a SurfaceCatalog
|
||||
* was supplied. This file remains domain-free; it knows the *shape* of
|
||||
* Contract 5.3 Section 31, not any exhibit's surface content.
|
||||
* `surfaces` remains OPTIONAL per-exhibit: an exhibit that does not supply
|
||||
* a SurfaceCatalog simply gets no `surfaces` key in `describe()` at all
|
||||
* (Contract 5.3 §7). This file remains domain-free; it knows the *shape*
|
||||
* of Contract 5.3 Section 31, not any exhibit's surface content.
|
||||
*
|
||||
* `CONTRACT_MINOR`/`XZBT_VERSION` above are this module's defaults, used by
|
||||
* any exhibit that does not pass its own `contractMinor`/`xzbtVersion` to
|
||||
* ContractCore. XZBT-NGN's maintained exhibit set moves forward with the
|
||||
* contract (Contract 5.3 Changelog, §39): there is no standing requirement
|
||||
* to keep a *currently maintained* exhibit frozen on an old minor version
|
||||
* merely because it once shipped against it. An exhibit that has a real,
|
||||
* independently justified reason to stay on an older minor may still pass
|
||||
* an explicit lower `contractMinor`/`xzbtVersion` — nothing here prevents
|
||||
* that — but it is no longer the silent default.
|
||||
*/
|
||||
|
||||
/* Contract 5.2 §15. The exhibit assigns source at its own trusted
|
||||
/* Contract §15. The exhibit assigns source at its own trusted
|
||||
* boundary; a source supplied by a caller is never trusted. */
|
||||
var SOURCES = ['ui', 'midi', 'hotkey', 'host', 'scenario', 'internal', 'system'];
|
||||
|
||||
/* Contract 5.2 §24. */
|
||||
/* Contract §24. */
|
||||
var ERROR_CODES = [
|
||||
'UNSUPPORTED_VERSION',
|
||||
'INVALID_MESSAGE',
|
||||
@@ -59,10 +67,10 @@
|
||||
'INTERNAL_ERROR'
|
||||
];
|
||||
|
||||
/* Contract 5.2 §17. */
|
||||
/* Contract §17. */
|
||||
var CAPABILITY_STATES = ['unsupported', 'available', 'loading', 'ready', 'busy', 'error'];
|
||||
|
||||
/* Contract 5.2 §16. The base event set is closed; exhibits do not invent
|
||||
/* Contract §16. The base event set is closed; exhibits do not invent
|
||||
* new canonical event types. */
|
||||
var EVENT_TYPES = [
|
||||
'state.changed',
|
||||
@@ -746,14 +754,13 @@
|
||||
if (!isPlainObject(message)) {
|
||||
return this._errorEnvelope(null, null, 'INVALID_MESSAGE', 'Message must be an object.');
|
||||
}
|
||||
if (message.xzbt !== this.xzbtVersion) {
|
||||
return this._errorEnvelope(
|
||||
message.requestId || null,
|
||||
message.sessionId || null,
|
||||
'UNSUPPORTED_VERSION',
|
||||
'This exhibit implements contract ' + this.xzbtVersion + '.'
|
||||
);
|
||||
}
|
||||
/* `xzbt` is advisory metadata, not a compatibility gate (Contract §6.5):
|
||||
* a 5.3-aware host must still interoperate with a 5.2 exhibit and vice
|
||||
* versa. Real compatibility is negotiated by handleHello below via
|
||||
* `supportedContractMajors`/`contract.major`/`minor`; rejecting here on
|
||||
* an exact string mismatch pre-empted that negotiation and made it
|
||||
* impossible for a host and exhibit that both implement the contract,
|
||||
* but stamp different advisory xzbt strings, to ever connect. */
|
||||
if (typeof message.type !== 'string') {
|
||||
return this._errorEnvelope(
|
||||
message.requestId || null,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* XZBT Exhibit Contract 5.2 — same-origin postMessage host transport.
|
||||
* XZBT Exhibit Contract 5.3 — same-origin postMessage host transport.
|
||||
*
|
||||
* This is layer 6 of the Authoring Guide's recommended separation, and it is
|
||||
* deliberately the thinnest file in the project. It knows how to move
|
||||
@@ -61,7 +61,15 @@
|
||||
|
||||
var message = event.data;
|
||||
if (!message || typeof message !== 'object') return;
|
||||
if (message.xzbt !== window.XZBTContractCore.VERSION) return;
|
||||
/* `xzbt` is advisory metadata, not a version gate (Contract §6.5): real
|
||||
* compatibility is negotiated by ContractCore.handleRequest/handleHello
|
||||
* via contract major/minor, not by string-matching the caller's
|
||||
* advisory tag against this exhibit's own static module version here.
|
||||
* (This used to compare against window.XZBTContractCore.VERSION, the
|
||||
* shared module's fixed default -- never the per-instance version an
|
||||
* exhibit actually negotiates -- so it silently discarded every
|
||||
* message from any host whose advisory tag didn't happen to equal
|
||||
* that hardcoded default, including well-formed hellos.) */
|
||||
|
||||
/* Source is assigned here, at the trusted receiving boundary. */
|
||||
var response = this.core.handleRequest(message, 'host');
|
||||
|
||||
Reference in New Issue
Block a user