Step 5 Complete

This commit is contained in:
2026-09-14 13:01:45 -07:00
parent 76928f7870
commit 961919e017
34 changed files with 3309 additions and 273 deletions
+3 -1
View File
@@ -20,4 +20,6 @@ Thumbs.db
# Temporary and editor files
*.swp
*~
*~
# Locally supplied external exhibits; never vendor them into NGN.
/test-fixtures/local/
@@ -25,4 +25,3 @@ body: |
- Comprehensive test fixtures and test suites under `test‑fixtures/` and `tests/`.
All modified files (`AGENTS.md`, `README.md`) were updated to reflect the latest project state. The commit was authored as part of the Step 4 milestone and is tagged accordingly.
---
+7 -1
View File
@@ -49,10 +49,16 @@ scoped `AGENTS.md` files before making changes.
## Authoritative project documents, in priority order:
This repository is the canonical home of the shared Contract Specification and
Authoring Guide, and owns the NGN Implementation Plan. Documents in
`docs/reference/` describe external implementations, not NGN architecture.
Reference exhibits under `test-fixtures/` are test/reference fixtures, not product
code. Keep exhibit-specific names and paths in test configuration or documentation.
- XZBT Exhibit Contract Specification v5.2
- XZBT-NGN Exhibit Engine Implementation Plan v5.2
- XZBT Exhibit Authoring Guide v5.2
- Step 3.7 reference exhibit report
- Do not infer XZBT-NGN behavior from SciFi-XZBT source code.
- SciFi-XZBT is an external exhibit implementation, not the NGN architecture.
- SciFi-XZBT is an external exhibit implementation, not the NGN architecture.
+72 -241
View File
@@ -1,24 +1,41 @@
# XZBT-NGN Exhibit Engine
A minimal, single-exhibit engineering host for XZBT Exhibit Contract 5.2.
It discovers controls from `describe`, tracks the exhibit's reported state,
and exposes set/invoke operations, events, sessions, and protocol errors.
A single-exhibit host for XZBT Exhibit Contract 5.2. It negotiates sessions,
discovers descriptor-driven controls, inspects reported state, sends set/invoke
operations, and displays capabilities, events, revisions, and protocol errors.
Step 5 implementation and verification are recorded in
[the completion report](STEP5-MVP-COMPLETION-REPORT.md). The SciFi-XZBT argument
descriptors are corrected and both text invocations pass through the operator UI.
Step 5 is complete: 27 automated tests and all three browser reference checks
pass. Haunted House reports conformant values, and publishing validation passes.
Step 6 is planned but has not begun.
## Run locally
Requires Node.js 22 or later. No npm dependencies or build step are needed.
Requires Node.js 22 or later. No npm dependencies or build step.
```powershell
npm start
```
Open `http://127.0.0.1:4173`. Enter a same-origin exhibit path and select
**Load exhibit**. The host negotiates a real session, calls `describe` and
`state.get`, then renders the discovered catalog. **Reconnect** establishes
a new session without reloading the exhibit; **Refresh state** requests a
new authoritative snapshot. **Disconnect** removes the exhibit frame.
**Load exhibit**. The host sends `hello`, `describe`, and `state.get` and renders
the discovered controls. Enter another path and select **Load exhibit** to switch.
Included test fixture paths:
- **Reconnect** negotiates a new session with the current frame, preserving the
exhibit instance. After **Disconnect**, it reloads the remembered URL; the new
exhibit instance starts with its own initial state.
- **Disconnect** removes the frame and clears session, catalog, state and events.
- **Refresh state** reads a new authoritative snapshot.
- Controls are grouped by the exhibit's category (or Other when omitted).
Reported values and draft inputs are distinct; select **Set** or **Invoke**
to submit. Rediscovery resets drafts.
- Expand **Capabilities**, **State and metadata**, or **Protocol diagnostics**
details to inspect engineering information. The latest diagnostic is also
shown near the connection panel. **Clear log** dismisses historical diagnostics.
Included reference fixture paths:
```text
/test-fixtures/reference-exhibits/aquarium/index.html
@@ -26,12 +43,13 @@ Included test fixture paths:
/test-fixtures/reference-exhibits/haunted-house/index.html
```
These copied fixtures have documented compatibility corrections; see
[fixture provenance](test-fixtures/PROVENANCE.md). They are not NGN product
logic. Additional trusted HTML exhibits can be placed in a directory under
`public/` and loaded by their same-origin URL. The server exposes only
`public/`, `src/`, and `test-fixtures/`, and binds to `127.0.0.1`.
Set the `PORT` environment variable to change the default port.
These are locally corrected test/reference fixtures, not NGN product code or
pristine upstream conformance evidence. See [provenance](test-fixtures/PROVENANCE.md).
Place additional trusted HTML exhibits and their relative assets under `public/`
and enter their served URL. A filesystem path or a different server origin cannot
be attached directly. The server exposes only `public/`, `src/`, and
`test-fixtures/`, binds to `127.0.0.1`, and denies paths escaping those directories.
For a port conflict, set `$env:PORT='4175'` before `npm start` and use that origin.
## Verification
@@ -41,240 +59,53 @@ python devlog_editor.py --validate
git diff --check
```
With the server running, open
`http://127.0.0.1:4173/test-fixtures/host-verification.html` and select
**Run verification** for repeatable real-browser transport tests against all
three exhibits. Manual admin UI and native exhibit gesture checks are recorded
in [the Step 4 report](STEP4-MVP-REPORT.md).
Run the browser suite at `/test-fixtures/host-verification.html`; its timestamped
results appear on the page and in the browser console. The normal operator UI
checks and external SciFi-XZBT preparation are documented in
[Step 5 verification](test-fixtures/STEP5-VERIFICATION.md). Saved browser evidence
is under `test-fixtures/evidence/`. No source edits or dedicated harness are
needed for ordinary operation.
## Implementation boundaries
## Canonical documentation
- `src/host.js`: session/request correlation, catalog, state cache, events,
synchronization, and control operations.
- `src/validation.js`: descriptor, value, and clarified argument validation.
- `src/transport/post-message.js`: same-origin frame transport only.
- `src/ui.js` and `public/`: descriptor-driven engineering interface.
- `server/serve.js`: loopback static server with explicit served directories.
- `tests/`: focused automated host and server tests.
- `test-fixtures/`: copied reference exhibits and real-browser verification.
This repository is the canonical home for the shared
[XZBT Exhibit Contract Specification 5.2](docs/contract/XZBT-Exhibit-Contract-Specification-v5.2.md)
and [XZBT Exhibit Authoring Guide 5.2](docs/authoring/XZBT-Exhibit-Authoring-Guide-v5.2-Step3.7.md).
The [NGN Implementation Plan 5.2](docs/architecture/XZBT-NGN-Exhibit-Engine-Implementation-Plan-v5.2.md)
also belongs here. Its historical phase numbering describes the longer roadmap;
the current Step 5 completion pass does not authorize those later features.
SciFi-XZBT is an external exhibit implementation, not the NGN architecture.
`docs/reference/` contains external implementation context, not host requirements.
The state display is a cache of exhibit reports. Events update reported values;
editable fields are drafts and are submitted only when Set/Invoke is selected.
Draft fields start from the first snapshot and do not overwrite themselves on
each incoming event. Registry/capability rediscovery rebuilds the controls.
Logs retain the latest 200 entries in memory. The interface intentionally has
no persistence, accounts, MIDI, scenarios, webhooks, packaging, or orchestration.
## Implementation and MVP boundary
## Template and publishing records
- `src/host.js`: negotiation, requests, discovery, state cache, events and recovery.
- `src/connection.js`: URL validation and frame load/disconnect/reconnect lifecycle.
- `src/validation.js`: descriptor, state value and argument validation.
- `src/transport/post-message.js`: same-origin frame transport.
- `src/ui.js`, `public/`: generic operator interface.
- `server/serve.js`: loopback static server.
- `tests/`, `test-fixtures/`: automated checks, reference fixtures and browser evidence.
A starter template and baseline structure for projects compatible with
[Labyricorn](https://www.labyricorn.com).
The MVP supports one exhibit, Contract 5.2 discovery, generic target rendering,
state inspection, set/invoke, capabilities, events, session/revision/sequence
tracking, reconnect and synchronization recovery. It has in-memory state only;
logs retain 200 entries. Idle polling is not enabled. The last connection URL is
remembered only until the page is reloaded.
This template provides the required `.labyricorn/` publishing records, assistant
guidance files (`AGENTS.md`), devlog structure, and the standalone
`devlog_editor.py` management utility for local validation and GUI editing.
Not implemented: multi-surface presentation, casting/display endpoints, MIDI,
MCP/webhooks, scenarios/recording, telemetry acquisition, multi-exhibit
orchestration, persisted operator configurations, remote deployment, accounts,
databases, cloud services, plugins, commercial licensing or production branding.
---
## Publishing records
## Overview
Projects published on Labyricorn maintain their own exhibition pages and
development logs directly inside their Git repositories under the `.labyricorn/`
directory. The Labyricorn site importer periodically synchronizes these records
as read-only content.
This template includes:
- **`.labyricorn/` publishing structure**: Validated Lektor-format records for
project identity and development logs.
- **`devlog_editor.py`**: Standalone GUI and CLI tool for managing devlog entries,
validating publishing contracts, and safely publishing changes.
- **`AGENTS.md` instructions**: Scoped assistant instructions at repository root
and publishing directories to ensure AI assistants adhere to the publishing
contract.
- **`.gitignore`**: Configured exclusions for Python environments, caches, and build
artifacts.
---
## Directory Structure
```text
.
├── .gitignore
├── .labyricorn/
│ ├── AGENTS.md # Assistant instructions for publishing content
│ ├── README.md # Publishing structure documentation
│ ├── project/
│ │ ├── AGENTS.md # Instructions for the project exhibition record
│ │ ├── contents.lr # Project identity and exhibition narrative
│ │ └── <optional logo> # Project logo (PNG, JPEG, WebP)
│ └── devlog/
│ ├── AGENTS.md # Instructions for devlog records
│ ├── contents.lr # Devlog index record
│ └── <entry-slug>/ # (Optional) Devlog entry directories
│ ├── contents.lr # Devlog entry record
│ └── <optional img> # Entry attachments (PNG, JPEG, WebP)
├── AGENTS.md # Assistant instructions for the repository
├── devlog_editor.py # Standalone devlog editor & validator
├── LICENSE # License file
└── README.md # Project documentation
```
---
## Getting Started
### 1. Configure the Project Identity
Edit `.labyricorn/project/contents.lr` to set your project details:
```ini
_model: project
---
schema_version: 1
---
project_id: my-project-id
---
title: My Project Title
---
summary:
A concise summary of the project for listings and cards.
---
status: active
---
started: 2026-08-23
---
author: Author Name
---
repository_url: https://git.labyricorn.com/Owner/my-project
---
default_branch: main
---
tags: python, web, tool
---
body:
An exhibition narrative describing the project background, goals, and features.
```
#### Project Fields:
- `project_id`: Stable lowercase hyphenated slug (e.g. `my-project`).
- `title`: Display title for the project.
- `summary`: Short multi-line summary.
- `status`: One of `active`, `released`, `maintained`, or `archived`.
- `started`: Start date in `YYYY-MM-DD` format.
- `author`: Author or organization name.
- `repository_url`: Public credential-free HTTPS Git URL.
- `default_branch`: Target branch name (e.g. `main`).
- `logo`: (Optional) Local filename of an image inside `.labyricorn/project/`.
- `tags`: Comma-separated technology or topic labels.
- `body`: Markdown narrative of the project.
### 2. Configure the Devlog Index
Edit `.labyricorn/devlog/contents.lr` to customize the devlog title and summary:
```ini
_model: devlog
---
schema_version: 1
---
title: My Project development log
---
summary: Milestones and development updates for My Project.
```
### 3. Add Devlog Entries
To add an entry manually, create a directory under `.labyricorn/devlog/<entry-slug>/`
containing `contents.lr`:
```ini
_model: devlog-entry
---
schema_version: 1
---
title: Initial Project Milestone
---
date: 2026-08-23
---
author: Author Name
---
summary:
Overview of the milestone accomplishments.
---
tags: release, setup
---
source_commit: <40-character-commit-hash>
---
body:
Detailed Markdown description of the milestone, technical decisions, and progress.
```
Alternatively, use `devlog_editor.py` to create and edit entries interactively.
---
## Devlog Editor
The included `devlog_editor.py` script is a standalone utility with zero external
dependencies (uses Python standard library and Tkinter).
### CLI Validation
Validate all project and devlog records without opening a GUI:
```bash
python devlog_editor.py --validate
```
### Graphical Editor
Launch the Tkinter management interface to view repository status, draft entries,
and publish updates:
```bash
python devlog_editor.py
```
Features of the GUI editor:
- **Repository status**: Displays current branch, remote, and ahead/behind counts.
- **Entry manager**: Create, edit, and delete devlog entries with automatic slug and date handling.
- **Publishing safety**: Stages and commits only `.labyricorn/` publishing files, leaving application source files untouched.
---
## Publishing Rules & Constraints
The Labyricorn site importer enforces strict validation rules on imported repositories:
1. **Native Lektor Records**: Records must be named `contents.lr` and declare `schema_version: 1`.
2. **No Raw HTML**: HTML tags (`<script>`, `<style>`, `<div>`, etc.) are prohibited in text fields and Markdown bodies.
3. **Approved Images**: Attachments must be PNG, JPEG, or WebP files located within the record directory. Files must match their magic-byte signatures.
4. **Limits**: Up to 100 imported files, maximum 5 MiB per file, and maximum 20 MiB total per project snapshot.
5. **Source Commits**: Devlog `source_commit` values must be valid 40-character commit hashes existing on the branch history.
6. **No Symlinks or Executables**: Linked paths and executable scripts under `.labyricorn/` are rejected.
---
## Assistant Guidance (`AGENTS.md`)
This repository uses hierarchical `AGENTS.md` files to guide coding assistants:
- **`AGENTS.md` (Root)**: General project guidelines, authorization boundaries, and devlog practices.
- **`.labyricorn/AGENTS.md`**: Publishing boundary contract and security rules.
- **`.labyricorn/project/AGENTS.md`**: Rules for editing project exhibition metadata.
- **`.labyricorn/devlog/AGENTS.md`**: Standards for crafting verifiable devlog entries.
Coding assistants working in this repository should always review the nearest
applicable `AGENTS.md` before making changes.
---
`.labyricorn/` holds this project's Lektor exhibition and development records.
See [publishing guidance](.labyricorn/README.md) and scoped `AGENTS.md` files.
`python devlog_editor.py` opens the standalone publishing editor;
`python devlog_editor.py --validate` validates records without opening the GUI.
Application code lives outside `.labyricorn/`.
## License
This template is released into the public domain under the [Unlicense](LICENSE).
See [LICENSE](LICENSE).
+394
View File
@@ -0,0 +1,394 @@
# A. Executive result
STEP 5 COMPLETE — NGN MVP COMPLETE — READY FOR STEP 6
Final closure on 2026-09-14: **27/27 automated tests**, **3/3 browser reference
checks**, Haunted House normal operator checks, both SciFi text invocations,
publishing validation, diff check, and genericity checks pass. The original SciFi
argument defect, Haunted House reporting defect, and malformed devlog structure
are resolved. Section R records the final results and evidence.
The existing NGN architecture and validation are unchanged by this closure pass.
No exhibit-specific host logic or Step 6 implementation was added. Earlier
failures and their evidence remain below as historical context, not current gates.
# B. Pre-Step-5 MVP state
Baseline: NGN HEAD `76928f7`, clean at initial inspection. Read the Contract 5.2,
Implementation Plan 5.2, Authoring Guide 5.2, Step 4 report/review, README/AGENTS,
source, fixtures and completion devlog. The owner supplied `docs/implementation_plan.md`
and `docs/walkthrough.md` during the run; these accepted triage/completion notes
were read and preserved. Their permissive metadata interpretation matches this
implementation. Their existing fixture copy notice was confirmed present.
The Step 4 UI already accepted same-origin paths without source edits. Its gaps
were rough presentation, absent loading timeout/status, disabled reconnect after
disconnect, and empty catalog text lost during initial rendering. Capabilities
and diagnostics were raw JSON beside the main workflow. README still contained
extensive starter-template identity/setup instructions.
Baseline `npm test`: 20/20. Initial sandbox execution failed with spawn EPERM;
execution with the required process permission passed. Baseline real browser:
Aquarium loaded through normal UI; the existing transport suite passed 3/3.
# C. Operator connection workflow
The normal URL/path form remains the single entry point. Load exhibit connects
or switches; invalid/off-origin/credential-bearing URLs are rejected before
removing the active frame. Loading is explicit and bounded by a 15-second document
load timer. A completed document without a responding contract uses the existing
5-second request timeout. The URL is remembered in memory for reconnect.
Disconnect cancels pending host work, removes the frame and clears session view.
Reconnect while attached negotiates a new session with the same exhibit instance.
Reconnect after disconnect reloads the remembered location as a new instance;
its initial values/revision need not equal those of the removed instance.
Stale frame load callbacks are ignored after switch/disconnect. No filesystem
picker, mount, remote transport, persistence, or exhibit-specific selector exists.
# D. Operator usability changes
Connection status, identity, version and negotiated contract are prominent.
Controls are grouped by discovered category, with Other as the missing/empty
category fallback. Capabilities are readable ID/state items. State/session data,
full descriptors, events and protocol logs remain available in expandable areas.
Disconnected states explain how to begin. Draft-versus-reported-value behavior is
explained. The latest diagnostic is visible near connection with its timestamp;
Clear log dismisses history. Existing colors and simple layout were retained.
# E. Canonical documentation ownership
README now describes NGN as the project, links canonical Contract/Authoring Guide/
Implementation Plan documents, identifies external SciFi-XZBT and reference
fixtures, and documents startup, workflow, verification and MVP limits. The root
AGENTS ownership paragraph matches the owner's updated instructions. Publishing
management is linked rather than presented as the identity of the application.
Authoritative specifications and external repository documentation were not edited.
The plan's historical roadmap numbering is distinguished from this Step 5 request.
# F. Descriptor validation changes
`validateCatalog` now checks supplied `restorable` values are boolean and supplied
`category` values are strings. Omission remains accepted, as required by the
accepted triage interpretation of Contract section 9's illustrative metadata.
Empty category strings are accepted and use the UI fallback. No new mandatory
fields, category vocabulary, restore behavior, or protocol semantics were added.
Tests cover omission, valid false/true/empty/custom values, and malformed types.
# G. SciFi-XZBT interoperability
Tested the real external working copy at `G:/.vibe/SciFi-XZBT`, HEAD
`179b9db715aaa92999c5bfef451b6877f30079fe`, including its pre-existing local changes
and untracked contract files. Copied only index.html, js/ and css/ unchanged into
ignored `test-fixtures/local/scifi/`. All 12 file hashes matched the source;
[evidence manifest](test-fixtures/evidence/step5-scifi-files.json) identifies the
actual tested contents. This is not a test of HEAD alone or a dist release.
All operations used the normal NGN URL form and descriptor-generated controls.
| Check | Observed result |
| --- | --- |
| Connect, hello/session, describe, state.get | Pass: SciFi-XZBT 5.2.0 / Contract 5.2, 61 targets, registry revision 1 |
| Range | Master Volume 0.75 to 0.42; reported value and event matched |
| Boolean/state | Master Mute false to true; matched |
| Selection/preset | Main Engineering selected; related state events shared revision 5 |
| Observation | true then false; matching state reports and events |
| Argumentless impulse | Comm Badge Chime accepted, action.executed received |
| Contextual unavailability | Demat in Starfleet returned CAPABILITY_UNAVAILABLE without deleting target |
| Universe | Whataverse selected; Demat then emitted action.executed |
| Text argument invoke | Initially failed with legacy args.text. Follow-up PASS: both expose arguments arrays and emit action.executed with exact submitted text; see section Q. |
| Incoming events | state.changed, selection.changed, capability.changed, action.executed observed, with source/correlation where supplied |
| stateRevision / sequence | Separate counters tracked; saved session ended at revision 6 / sequence 31 |
| Reconnect attached | New session, revision 6 preserved, sequence baseline null |
| Disconnect / reconnect again | Frame removed and URL reloaded; new session and fresh instance state, revision 0 |
[saved session evidence](test-fixtures/evidence/step5-scifi-session.json) contains
actual descriptors, events, metadata and errors. Per Contract section 9.5, missing
arguments metadata is tolerated as empty; NGN must not infer undocumented
arguments from the legacy field. The authorized correction and narrow normal-UI
rerun are now complete (section Q). Context-gated actions and a large fixed preset
list are conformant but can be awkward; no domain-specific UI logic was added.
Audible fidelity, optional AI model preparation, and all SciFi actions are not
certified by these representative checks.
# H. Reference exhibit regression (initial run; closure results in R)
| Exhibit | Normal operator checks | Result |
| --- | --- | --- |
| Aquarium (12 targets) | Population 16, Paused true, Blue Tang selection, Feed, reconnect, disconnect/reload | Pass; events/readback and new session observed |
| Planetarium (14 targets) | Azimuth 0.75, Sky frozen true, Cygnus, Advance time with minutes 30, reconnect | Pass; declared argument delivered and action event captured |
| Haunted House (13 targets) | Haunt level 0.5, Held true, Library, Manifest, reconnect | Writes/session pass; Manifest event received, but transient snapshot violates declared 0.01 step |
The earlier independent transport-style browser suite passed 3/3. The latest
rerun passes 2/3, failing Haunted House's haunt-level step validation. Source
inspection also corrects the earlier claim that the suite avoids Manifest: its
first impulse without capability requirements is `action.manifest`. The fixture
also drifts haunt level continuously before it is paused, so off-step snapshots
are not confined to that action. The earlier additional Manifest case exposed:
NGN reports INVALID_MESSAGE for room.haunt-level and preserves its previous cache
instead of accepting a value outside the descriptor's step. This is not a Step 5
host regression or a reason to weaken validation. No fixture source changed.
These are the previously corrected AbacusAI fixtures, not pristine upstream
implementations. Corrections remain documented in `test-fixtures/PROVENANCE.md`.
Neither 3/3 nor the operator checks certify every fixture action/state transition.
# I. Initial automated test results (final count in R)
`npm test`: **25 passed, 0 failed**, including all 20 existing tests and five
new connection/metadata tests. Coverage added: accepted/rejected URL forms,
invalid URL preserving a frame, stale loads after switching/disconnect,
reconnect with and without a frame, load timeout/recovery, optional metadata types.
Existing host tests cover invalid sessions, sequence/revision gaps, pending
snapshot races, stale sessions, transport origin/source and server boundaries.
`git diff --check`: passed after line-ending normalization of authored files.
`python devlog_editor.py --validate`: **failed on pre-existing content**:
`.labyricorn/devlog/post-step-4-completion/contents.lr: empty or malformed record block`.
The trailing separator is present in HEAD and that file is unchanged. Publishing
record repair was not included in this application milestone. Do not read the
historical Step 4 validation claim as a successful validation of the current tree.
# J. Browser/runtime verification
Local Codex in-app Chromium, 2026-09-14. Normal UI verified on loopback port 4173.
A new `npm start` process succeeded on 4175 (4173 was already occupied); final
reference suite ran against that fresh process and printed timestamped 3/3 output.
See [browser results](test-fixtures/evidence/step5-reference-suite.txt), the three
reference JSON snapshots and [operator screenshot](test-fixtures/evidence/step5-operator.jpg).
The missing-exhibit URL hit LOAD_TIMEOUT in this browser and a valid URL recovered
to connected/synchronized. Initial and disconnected views, loading, switching,
attached reconnect, disconnect/reload, discovery and real controls were checked.
A browser tab disappeared once during verification; a new tab resumed the remaining
checks, including a repeated successful SciFi disconnect/reload.
No separate pristine checkout was created. Startup and tests run directly from
current source, with no dependencies/build products or hidden source edits.
External SciFi assets must be supplied separately using the documented commands.
This is local Chromium evidence, not cross-browser or clean-checkout certification.
# K. Genericity verification
Case-insensitive scan of `src/`, `public/`, `server/` for
`scifi|aquarium|planetarium|haunted|tank\.|sky\.|room\.|universe|preset|speech|warp`
returned no matches. Diff review confirms controls depend on descriptors and
categories only. Exhibit names/paths are confined to documentation, tests and
ignored runtime copies. No transport architecture changes or Step 6 features
were introduced. No SciFi internals were used to infer NGN behavior.
# L. Files changed
- `.gitignore`: ignore locally supplied external exhibit copies.
- `AGENTS.md`, `README.md`: canonical ownership, operator instructions and boundaries.
- `src/connection.js` (new): URL/frame lifecycle helper.
- `src/ui.js`, `public/index.html`, `public/style.css`: connection and usability pass.
- `src/validation.js`: supplied optional metadata type checks.
- `tests/connection.test.js` (new): five focused tests.
- `test-fixtures/host-verification.js`: timestamped page/console output.
- `test-fixtures/STEP5-VERIFICATION.md` (new): repeatable operator and external setup checks.
- `test-fixtures/evidence/` (new): six JSON/text evidence files and one screenshot.
- `STEP5-MVP-COMPLETION-REPORT.md` (new): this report.
The owner's newly supplied `docs/implementation_plan.md` and `docs/walkthrough.md`
remain unmodified. Production host/session implementation, transport and server
were not changed during closure. The earlier follow-up changed only SciFi's
contract adapter externally; closure makes no external source changes. Section R
lists the final fixture, devlog, test, and evidence changes. Nothing was staged,
committed or pushed.
# M. Known limitations
The SciFi descriptor, Haunted House reporting, and publishing validation defects
are resolved. External source provenance is a working-copy hash manifest,
not a reproducible released commit. A fresh checkout was not independently tested.
One trusted same-origin exhibit only; last URL, cache and logs are in memory.
Disconnect destroys the exhibit instance. Drafts reset on rediscovery. Large
catalogs require scrolling; controls use numeric inputs. No idle polling or
complete cross-browser/audio certification. These limits are visible/documented,
not hidden by a host-specific workaround.
# N. Deferred roadmap features
No multi-surface presentation, casting/display endpoints, second-monitor output,
Google TV, MIDI, MCP, webhooks, scenarios, recording, telemetry acquisition,
multi-exhibit orchestration, remote automation, persisted operator configuration,
plugins, databases, accounts, cloud services, commercial licensing or production
branding. Nothing in the broader Implementation Plan makes these MVP features.
# O. Step 6 prerequisites
The Step 5 gate has passed with all three defects resolved, without waivers.
The next planned work is listed below. This closure pass stops here and does not
begin Step 6 implementation.
The owner's planned Step 6 sequence remains unimplemented:
6.1 Define the Multi-Surface Model
6.2 Build a Minimal Multi-Surface Reference Exhibit
6.3 Add Generic Surface Discovery to XZBT-NGN
6.4 Implement Local Multi-Surface Rendering
6.5 Prove Shared-State Synchronization
6.6 Validate the Reference Exhibit End-to-End
6.7 Adapt SciFi-XZBT Observation to the Surface Model
6.8 Test Multi-Surface Behavior Against SciFi-XZBT
6.9 Step 6 Conformance and Architecture Review
6.10 Step 6 Completion Gate
# P. Final verdict
STEP 5 COMPLETE — NGN MVP COMPLETE — READY FOR STEP 6
# Q. Historical descriptor follow-up and unsuccessful gate rerun
Date: 2026-09-14. This section records the resolved SciFi failure and the subsequent
2/3 gate failure; final closure in R supersedes its gate status. The broader operator checks in G/H/J are prior evidence;
this follow-up reran only the requested two SciFi targets and the acceptance checks.
## Patch and provenance
External file: `G:/.vibe/SciFi-XZBT/js/contract-adapter.js`. Both descriptors now
publish `arguments: [{ name: 'text', type: 'string', required: true, maxLength }]`,
with limits 2000 for speech and 512 for ticker. The validator consumes that array
for required, type, length and unknown-key checks. Invoke payloads correctly keep
the Contract's `args` object. No host fallback for legacy descriptors was added.
The source HEAD remains `179b9db715aaa92999c5bfef451b6877f30079fe`; the adapter is
an existing untracked working-copy file. A fresh ignored copy was created at
`test-fixtures/local/scifi-arguments/`. All 12 runtime files match source SHA-256
hashes in [the new manifest](test-fixtures/evidence/step5-scifi-arguments-files.json).
The original manifest and failing session evidence are retained unchanged.
The external packaging script also successfully generated `standalone.html` in
that ignored directory. It is a local working-copy build, not a released artifact;
offline packaged-runtime behavior was not separately certified in this narrow run.
## Normal operator UI results
A fresh `npm start` served port 4173. Local Chromium loaded the corrected source
copy through the URL form, negotiated Contract 5.2, and discovered 61 targets.
Both descriptor-generated text fields appeared. Clicking their Invoke buttons
produced the following incoming events in session `sess-inf3scqz`:
| Target | Submitted text | Result |
| --- | --- | --- |
| speech.say | NGN Step 5 speech interoperability check. | action.executed, sequence 1, correlationId ngn-2-4, source host |
| display.ticker | NGN Step 5 ticker interoperability check. | action.executed, sequence 2, correlationId ngn-2-6, source host |
Both events contain the exact submitted `args.text`. State revision remained 0,
as appropriate for these transient impulses. Speech then reported capability
ready at sequence 3. The protocol log contained only session establishment; the
browser reported no warning/error console entries at capture time. This verifies
invocation acceptance and events, not audible fidelity or AI preparation.
See [captured UI and descriptors](test-fixtures/evidence/step5-scifi-arguments-ui.json).
After preserving the adapter's original LF line endings, the fresh source copy
was reloaded and both operations were repeated. The manifest identifies this
final copy. In session `sess-igidi1h9`, speech succeeded at sequence 1 with
correlation `ngn-4-13`; ticker succeeded with the exact text at sequence 5 with
correlation `ngn-4-19`. Speech capability rediscovery cleared a draft during this
repeat, so an intervening ticker event contains an empty string; re-entering the
text and invoking again succeeded. This is the documented draft-reset behavior.
The later capture also includes a native `transport.playing` event with source
`ui`, revision 1, and speech capability transitions. Those are retained without
attributing the native state change to the transient text operation. See the
[final-source UI capture](test-fixtures/evidence/step5-scifi-arguments-ui-final.json).
## Acceptance results at the descriptor follow-up
| Check | Latest result |
| --- | --- |
| NGN npm test | PASS: 25/25 after permitting Node worker spawning (sandbox first returned EPERM) |
| External adapter syntax | PASS: node --check |
| Focused external adapter checks | PASS: metadata; missing/null/wrong-type/unknown/overlength rejection before execution; exact maximum length accepted; argumentless invocation preserved |
| Normal UI: speech.say and display.ticker | PASS: 2/2 |
| Reference browser suite | FAIL: 2/3; Aquarium and Planetarium pass, Haunted House reports off-step room.haunt-level |
| Publishing validation | FAIL: unchanged post-step-4-completion record has an empty/malformed trailing block |
| NGN git diff --check | PASS |
| Genericity scan of src/public/server | PASS: no exhibit-specific vocabulary matches |
The browser gate ran from `2026-09-14T19:41:45.445Z` to
`2026-09-14T19:41:45.575Z`; [actual output](test-fixtures/evidence/step5-acceptance-rerun-browser.txt)
is retained without replacing it with the earlier 3/3 result. The Haunted House
fixture files are unchanged. Its continuous drift and Manifest can produce
values that conflict with the descriptor's 0.01 step; NGN validation was not weakened.
Follow-up NGN changes: this report, README, STEP5-VERIFICATION, the optional
`test-fixtures/check-scifi-arguments.cjs` regression script, and new evidence
files. Run the external check with:
```powershell
node test-fixtures/check-scifi-arguments.cjs G:/.vibe/SciFi-XZBT/js/contract-adapter.js
```
At that point the SciFi fix was complete but Step 5 completion was withheld for
two remaining failures. The final authorized closure below resolves both.
# R. Final Step 5 closure
Date: 2026-09-14. Final browser suite ran at **19:55:22.193–19:55:22.342 UTC**;
operator verification continued through the SciFi text events at **19:57:23 UTC**.
These are new closure results, not historical counts copied forward.
## Minimal repairs
- `test-fixtures/reference-exhibits/haunted-house/exhibit.js`: its shared reader
now reports `Math.round(clamp(h.hauntLevel, 0, 1) * 100) / 100`. This uses integer
hundredths without cumulative rounding, conforms to the existing 0–1/0.01
descriptor, and leaves the internal continuous simulation untouched. Both
snapshots and mutation events use this reader. Manifest still reaches 1 and
the model resumes drifting when released.
- `.labyricorn/devlog/post-step-4-completion/contents.lr`: removed the final `---`
that created an empty record block. All meaningful content and fields remain
unchanged. Its source commit exists and retains the recorded 2026-09-14 date.
- `tests/haunted-house.test.js`: two focused regressions use the actual exhibit
and contract core, with canvas construction stubbed. They test bounds and
rounding, 600 simulation steps for each of 20 room/spirit combinations, full
snapshots during post-Manifest drift, mutation events, and Calm. They use
unchanged NGN reported-value validation and assert that reading does not
quantize the internal simulation.
## Final acceptance results
| Acceptance check | Final result | Evidence |
| --- | --- | --- |
| Automated tests | PASS: 27 tests, 0 failures (25 existing + 2 fixture regressions) | [Command output](test-fixtures/evidence/step5-closure-checks.json) |
| Browser reference suite | PASS: 3/3, Aquarium / Planetarium / Haunted House | [Timestamped output](test-fixtures/evidence/step5-closure-browser-suite.txt) |
| Haunted House normal UI | PASS: held true, haunt level 0.5, Library selection, Manifest reports 1, release and refresh report 0.93 then 0.26, reconnect synchronized | [Snapshots and events](test-fixtures/evidence/step5-closure-haunted-house.json) |
| Corrected SciFi text targets | PASS: both arrays/fields present; exact text accepted in action.executed | [Final UI capture](test-fixtures/evidence/step5-closure-scifi.json) |
| Publishing validation | PASS: `devlog-editor: devlog is valid` | [Command output](test-fixtures/evidence/step5-closure-checks.json) |
| git diff --check | PASS | Final review |
| Genericity scan | PASS: no matches in src/public/server for the vocabulary in K | Final review |
| Architecture and validation | Unchanged in this closure pass; no host workaround | Diff review |
| Step 6 features | None implemented | Diff review |
Haunted House session `sess-12844` recorded valid writes and Manifest, with no
protocol errors. Attached reconnect established `sess-29bfc`, preserving revision
4 and resetting the sequence baseline. The continuous visual model remains active;
refreshed reports conform to the descriptor. No additional invalid state was
observed in the focused model tests or normal operator checks.
SciFi session `sess-319js95v` accepted `Step 5 final closure ticker check.` at
sequence 1 / correlation `ngn-5-23` and `Step 5 final closure speech check.` at
sequence 2 / correlation `ngn-5-25`, both with source `host`. All 12 source and
served-file hashes still match the corrected manifest from Q. No external SciFi
file was changed in this pass. Audio fidelity and optional AI preparation remain
outside this narrow interoperability claim.
The browser used the running normal loopback server on port 4173. Automated
execution required the existing permission for Node worker spawning after a
sandbox EPERM. This is local Chromium and current-working-tree evidence; it does
not expand the cross-browser or released-artifact claims.
## Closure files and handoff
In addition to the two repairs and focused test file, closure updates
`test-fixtures/PROVENANCE.md`, `test-fixtures/STEP5-VERIFICATION.md`, `README.md`,
this report, and the four new `step5-closure-*` evidence files. Earlier failed and
corrected-follow-up evidence is preserved. Product code and the browser harness
remain unchanged during closure. Existing unrelated edits are preserved.
All nine acceptance criteria pass. The planned next work is the ten-item Step 6
sequence in O, starting with **6.1 Define the Multi-Surface Model**. No Step 6 work
has begun. No staging, commit, push, or publication was performed; public-site
synchronization was not checked.
+255
View File
@@ -0,0 +1,255 @@
# Step 4 MVP — Triage of Independent Review Findings
## A. Executive Triage Verdict
The Step 4 MVP is sound. The independent review confirmed 20/20 tests, generic host implementation, and no domain vocabulary in production source. None of the five findings invalidate the MVP.
After triage against the actual Contract 5.2 text, the current implementation, and the authoring guide:
- **One finding (F-1) is NOT ACTUALLY A DEFECT** — the fixture behavior matches the normative Contract text.
- **One finding (F-3) SHOULD BE FIXED NOW** — a small README header note to prevent future re-copy confusion.
- **One finding (F-2) is SAFE TO DEFER** — permissive but conformant; future restore work will need it.
- **One finding (F-4) is INFORMATIONAL ONLY** — the current evidence model is sufficient for an MVP.
- **One finding (F-5) is INFORMATIONAL ONLY** — already documented in the Authoring Guide.
**No contract blockers exist. No owner decisions are required.**
The milestone can be closed after one small documentation patch (F-3 README header).
---
## B. F-1 — `INVALID_ARGUMENTS` vs `INVALID_VALUE`
### Contract Basis
The review claims that Contract §24 and Authoring Guide Appendix A line 522 require `INVALID_ARGUMENTS` as a distinct error code for argument failures.
**Actual Contract text (§24):**
> Implementations **SHOULD** use stable machine-readable error codes.
> **Recommended** base codes include: [...] `INVALID_ARGUMENTS` [...]
This is SHOULD + "recommended", not MUST. The codes are a recommended palette, not mandatory assignments.
**Actual Contract text (§9.5) — the normative argument validation section:**
> Invoke `args` MUST be an object. Every argument with `required: true` MUST be present. **Undeclared keys MUST be rejected with `INVALID_VALUE`.** Values MUST match the declared type, and all declared enum, bounds, step, and length constraints MUST be enforced.
This is the only normative text that specifies which error code to use for argument failures, and it **explicitly mandates `INVALID_VALUE`** — including for undeclared keys, which are the most "shape-like" argument failure.
**Authoring Guide Appendix A line 522:**
> Unknown target, invalid value, invalid session, and invalid arguments each return the correct distinct error code.
This checklist item says "the correct distinct error code" — it does not say which code that is. Reading it together with §9.5, the "correct" code for argument failures *is* `INVALID_VALUE`, because that is what §9.5 normatively requires.
`INVALID_ARGUMENTS` is listed in §24 as an available code but has **no normative triggering condition anywhere in the Contract**. It exists as a palette option for exhibits that choose to distinguish argument-shape failures from value-constraint failures, but nothing requires it.
### Classification: **NOT ACTUALLY A DEFECT**
The fixture core returns `INVALID_VALUE` for all argument failures. This is exactly what Contract §9.5 mandates. The fixture is conformant.
The host already handles any error code an exhibit returns — if a future exhibit emits `INVALID_ARGUMENTS`, the host will surface it correctly. No host change is needed.
### Recommended Resolution
**No code change required.** The fixture is conformant with the normative Contract text.
Optionally, add one clarifying sentence to `PROVENANCE.md` noting that `INVALID_VALUE` is used for argument failures per §9.5 and that `INVALID_ARGUMENTS` remains available but has no normative trigger in Contract 5.2. This is strictly optional.
### Blocks MVP Closure: **No**
---
## C. F-2 — Descriptor Validation Strictness
### Field-by-Field Requirement Analysis
| Field | Contract §9 Base Descriptor | Normative Strength | Currently Validated | Classification |
| --- | --- | --- | --- | --- |
| `id` | Required, canonical grammar in §8 | MUST (§8.1) | ✅ Yes | — |
| `kind` | Required, four kinds in §10 | MUST (§10) | ✅ Yes | — |
| `readable` | Required boolean | MUST (§9, §13) | ✅ Yes | — |
| `writable` | Required boolean | MUST (§9, §12) | ✅ Yes | — |
| `requires` | Required array of capability IDs | MUST (§9, §17) | ✅ Yes | — |
| `restorable` | Present in base descriptor example | Illustrative example, not MUST | ❌ No | SHOULD validate — future restore logic needs it |
| `category` | Present in base descriptor example | Illustrative example, not MUST | ❌ No | Optional for MVP — no host feature uses it |
| `label` | Present in base descriptor example | Illustrative example, not MUST | ❌ No | Optional — host can default to `id` |
| `unit` | Present in base descriptor example as `null` | Illustrative, OPTIONAL | ❌ No | Optional |
**Key analysis:** Contract §9 says *"Each target returned by `describe` MUST include enough metadata for a generic host to inspect and operate it"* and then shows a "Base descriptor" example. The section does **not** use RFC 2119 MUST/REQUIRED language for `restorable`, `category`, `label`, or `unit` individually. The normative MUST statements in §9 are about the descriptor being sufficient for generic operation — which `id`, `kind`, `readable`, `writable`, and `requires` satisfy.
The Authoring Guide line 94 lists these fields together, but as an authoring recommendation, not a host validation mandate.
### Classification: **SAFE TO DEFER**
Current `validateCatalog` is permissive but not nonconformant. It checks everything needed for the host to operate: `id`, `kind`, `readable`, `writable`, `requires`, and kind-specific metadata (`min`/`max`/`step` for ranges, `options` for selections). The host does not currently use `restorable` or `category` for any behavior.
### Recommended Resolution
Defer `restorable` and `category` validation to the scenario/restore milestone (future Step 4 or Step 5 work), where `restorable` becomes load-bearing. Adding it now is harmless but not required.
`label` validation is genuinely optional — the host can and does fall back to `id`.
### Blocks MVP Closure: **No**
---
## D. F-3 — Fixture README / Genericity Framing
### Classification: **SHOULD FIX NOW** — documentation/evidence-integrity issue with concrete future risk
This is not a product defect but a real risk to future development:
1. The copied [README.md](file:///g:/.vibe/XZBT-NGN/test-fixtures/reference-exhibits/README.md) is an upstream document that describes upstream verification and lists bugs as "found and fixed during verification" — but they are **not fixed upstream**. The upstream repository at `G:\.vibe\SciFi-XZBT` still has all three defects.
2. A future developer or agent re-copying from the stated upstream source will silently reintroduce all defects.
3. [PROVENANCE.md](file:///g:/.vibe/XZBT-NGN/test-fixtures/PROVENANCE.md) is accurate, but the README is longer, more prominent, and contradicts it.
### Exact Recommended Wording
Add the following header note to the **top** of `test-fixtures/reference-exhibits/README.md`, before the existing content:
```markdown
> **Local copy notice.** This directory contains locally corrected copies of
> the upstream reference exhibits. The corrections are documented in
> `../PROVENANCE.md`. The text below is the original upstream README; its
> verification claims describe the upstream state, not this corrected copy.
> Re-copying from the upstream source without reapplying corrections will
> reintroduce known defects.
```
Do **not** rewrite the upstream README body. One blockquote header is sufficient.
### Blocks MVP Closure: **Should be done before closure, but is not a contract blocker**
---
## E. F-4 — Browser Evidence
### Sufficiency Assessment
The current evidence model is:
- **Reproducible baseline:** 20/20 automated Node.js tests (`npm test`), fully reproducible from the repository
- **Supporting evidence:** 3/3 real-browser verification via `/test-fixtures/host-verification.html`, manually triggered, with results described in the report
This is appropriate for an engineering MVP. The report correctly hedges: *"This is local Chromium evidence, not a cross-browser certification"* and *"No success claim relies on mocks alone."*
### Must the browser suite produce a machine-captured artifact?
**No.** The automated suite is the reproducible baseline. The browser suite supplements it with end-to-end evidence that cannot be captured without a real browser (rendering, audio context lifecycle, iframe postMessage). Its manual nature is inherent to what it tests.
### Should automated browser execution be deferred?
**Yes.** Automated browser execution (Playwright, Puppeteer, etc.) is CI/infrastructure work, not Step 4 MVP scope.
### Classification: **INFORMATIONAL ONLY**
### Blocks MVP Closure: **No**
---
## F. F-5 — `stateRevision` Across Session Reconnect
### Semantic Assessment
The current implementation preserves `stateRevision` across sessions while resetting `sequence`. The review notes this is an "unwritten" cross-component invariant.
**Actual document coverage:**
1. **Contract §14.4:** *"`stateRevision` tracks committed persistent-state history."* — History is not session-scoped.
2. **Contract §16.1:** *"Event `sequence` [...] is session-scoped and resets when a new `sessionId` is issued."* — Only `sequence` is declared session-scoped. `stateRevision` is not.
3. **Authoring Guide §H (line 200):** *"`stateRevision` tracks committed persistent-state history and **does not reset with a new session**"* — **This is already explicitly stated.**
4. **Authoring Guide Appendix A (line 519):** *"Events carry monotonic `sequence`, reset on new session; `stateRevision` does not reset with session."* — **Also already explicit.**
The invariant **is** written down — in the Authoring Guide, which is an authoritative project document. The Contract itself implies it through §14.4 (state *history* is not session-scoped) and by the asymmetry with §16.1 (only `sequence` is called session-scoped).
### Is the current host behavior reasonable?
**Yes.** A host that detects `stateRevision` regression on reconnect is correctly enforcing the monotonicity requirement of §14.
### Would an exhibit that resets `stateRevision` on every session violate the contract?
**Yes** — it would violate §14's monotonicity requirement and contradict the Authoring Guide's explicit statement.
### Classification: **INFORMATIONAL ONLY**
### Recommended Clarification Level: **No action needed**
The Authoring Guide already states this explicitly. A future Contract §14 revision could add one sentence for completeness, but this is not a gap that needs closing before the MVP.
### Blocks MVP Closure: **No**
---
## G. Required Owner Decisions
**NO OWNER DECISIONS REQUIRED**
---
## H. Minimal Completion Patch List
### Patch 1 (the only patch): README copy-notice header
| Attribute | Detail |
| --- | --- |
| **File** | `test-fixtures/reference-exhibits/README.md` |
| **Change** | Add a blockquote header note (5 lines) at the top of the file, before existing content |
| **Why** | Prevents future re-copy confusion; distinguishes local corrected copy from upstream claims |
| **Verification** | Visual review of the added header; confirm existing README body is unchanged; `git diff --check` |
**Exact content:**
```markdown
> **Local copy notice.** This directory contains locally corrected copies of
> the upstream reference exhibits. The corrections are documented in
> `../PROVENANCE.md`. The text below is the original upstream README; its
> verification claims describe the upstream state, not this corrected copy.
> Re-copying from the upstream source without reapplying corrections will
> reintroduce known defects.
```
No other patches are required for MVP closure.
---
## I. Safe Deferrals
| Item | Defer To | Reason |
| --- | --- | --- |
| `validateCatalog` adding `restorable`/`category` checks (F-2) | Scenario/restore milestone | Not needed until restore logic depends on `restorable`; current behavior is permissive but conformant |
| Automated browser test execution (F-4) | CI/testing infrastructure work | Infrastructure concern, not an MVP requirement |
| Contract §14 explicit `stateRevision` session-persistence sentence (F-5) | Future Contract revision | Already stated in the Authoring Guide; not blocking |
| Optional `PROVENANCE.md` note about `INVALID_VALUE` per §9.5 (F-1) | Any convenient time | Purely informational; the fixture is already conformant |
| Report framing refinements (review §5 recommendations) | Owner's discretion | Report accuracy improvements; do not block the MVP |
---
## J. Recommended Final Report Wording
The following paragraph should be used in future reports to describe the genericity result:
> A single unmodified XZBT-NGN host drove three reference exhibits — Aquarium (12 targets), Planetarium (14 targets), and Haunted House (13 targets) — copied from the upstream AbacusAI reference set and locally corrected to conform to Contract 5.2. The corrections addressed non-standard descriptor kinds, a misnamed metadata field, missing argument validation, and a stale internal ID mapping; they are documented in `test-fixtures/PROVENANCE.md`. The host contains no exhibit-domain vocabulary and required no fixture-specific logic. The upstream reference source was not modified.
This wording makes clear that:
- ✅ The NGN host remained generic and unmodified
- ✅ Fixture corrections were necessary and load-bearing
- ✅ Corrections were Contract-oriented (conformance repairs), not host-specific exceptions
- ✅ Upstream reference sources were not modified
- ✅ The corrections are documented and traceable
---
## K. Final Triage Verdict
**STEP 4 MVP REQUIRES MINOR COMPLETION PATCHES**
One documentation patch (F-3 README header) should be applied before changing the milestone verdict. All other findings are either not defects, informational, or safely deferred.
After that patch, the milestone verdict should become:
**STEP 4 MVP COMPLETE — READY FOR NEXT STEP 4 PHASE**
+34
View File
@@ -0,0 +1,34 @@
# Step 4 MVP Triage — Completion Walkthrough
## Change Made
**One file modified:**
- [README.md](file:///g:/.vibe/XZBT-NGN/test-fixtures/reference-exhibits/README.md) — Added a 6-line blockquote copy-notice header at the top of the file, before the existing upstream README content. The header:
- Identifies this as a locally corrected copy
- Points to `../PROVENANCE.md` for correction details
- Warns that re-copying from upstream without reapplying corrections will reintroduce known defects
- Does not modify any of the original upstream README body
## Validation
| Check | Result |
| --- | --- |
| `git diff --check` | ✅ Clean (exit 0) |
| `python devlog_editor.py --validate` | ✅ "devlog is valid" |
## Triage Summary
| Finding | Classification | Action Taken |
| --- | --- | --- |
| F-1 `INVALID_ARGUMENTS` | Not actually a defect — §9.5 mandates `INVALID_VALUE` | None needed |
| F-2 Descriptor validation | Safe to defer — permissive but conformant | Deferred to restore milestone |
| F-3 README framing | Should fix now — evidence-integrity risk | **Applied** |
| F-4 Browser evidence | Informational only | None needed |
| F-5 `stateRevision` session persistence | Informational only — already in Authoring Guide | None needed |
## Next Steps
- No commit or push was performed (per AGENTS.md rules)
- The milestone verdict can now be changed to: **STEP 4 MVP COMPLETE — READY FOR NEXT STEP 4 PHASE**
- New application files remain untracked until the owner requests staging/commit
+18 -9
View File
@@ -2,19 +2,28 @@
<html lang="en">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>XZBT-NGN Exhibit Engine</title><link rel="stylesheet" href="/public/style.css"></head>
<body>
<header><h1>XZBT-NGN Exhibit Engine</h1><p>Contract 5.2 · Single-exhibit engineering console</p></header>
<header><h1>XZBT-NGN Exhibit Engine</h1><p>Contract 5.2 · One exhibit at a time</p></header>
<main>
<section><h2>Connection</h2>
<form id="connection"><label for="exhibit-url">Exhibit URL (same origin)</label><div class="connection-row"><input id="exhibit-url" placeholder="/test-fixtures/reference-exhibits/…/index.html" required><button>Load exhibit</button><button id="reconnect" type="button" disabled>Reconnect</button><button id="disconnect" type="button">Disconnect</button></div></form>
<p id="status" role="status">Disconnected</p><button id="refresh" disabled>Refresh state</button>
<p>Only load trusted local exhibits. Required capabilities in available/loading states may accept requests through an exhibit fallback; the exhibit decides.</p>
<form id="connection"><label for="exhibit-url">Exhibit URL (same origin)</label><div class="connection-row"><input id="exhibit-url" placeholder="/public/exhibits/example/index.html" required><button>Load exhibit</button><button id="reconnect" type="button" disabled>Reconnect</button><button id="disconnect" type="button" disabled>Disconnect</button></div></form>
<p>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.</p>
<p id="status" role="status">Disconnected</p><p id="identity"></p><p id="loaded-url"></p>
<p id="latest-error" role="alert" hidden></p>
<div id="frame-container"></div>
</section>
<section><h2>Session and exhibit</h2><pre id="metadata">No negotiated session.</pre><h3>Capabilities</h3><pre id="capabilities">[]</pre></section>
<section><h2>Discovered targets <span id="target-count"></span></h2><div id="catalog">Connect to discover the catalog.</div></section>
<section><h2>Current persistent state</h2><p>Last reported exhibit values. This host view may be uncertain while resynchronizing.</p><pre id="state">{}</pre></section>
<section><h2>Incoming events</h2><p>Latest 200 events, including source, sequence, revision, and correlation when supplied.</p><pre id="events" aria-label="Incoming events">[]</pre></section>
<section><h2>Protocol and errors</h2><pre id="errors" role="log">[]</pre></section>
<section><h2>Exhibit controls <span id="target-count"></span></h2>
<details><summary>Capabilities</summary><ul id="capabilities"></ul><p>Unavailable requirements disable affected controls. Available/loading capabilities may use an exhibit fallback; the exhibit decides whether to accept a request.</p></details>
<p>Reported values are authoritative. New values are drafts; select Set or Invoke to submit. Rediscovery resets drafts.</p>
<div id="catalog">Connect an exhibit to discover its controls.</div>
</section>
<section><h2>State and metadata</h2><button id="refresh" disabled>Refresh state</button>
<details><summary>Session and exhibit metadata</summary><pre id="metadata">No negotiated session.</pre></details>
<details><summary>Current persistent state</summary><p>Last reported exhibit values. This host view may be uncertain while resynchronizing.</p><pre id="state">{}</pre></details>
</section>
<section><h2>Protocol diagnostics</h2>
<details><summary>Incoming events</summary><p>Latest 200 session events, including source, sequence, revision, and correlation when supplied.</p><pre id="events" aria-label="Incoming events">[]</pre></details>
<details><summary>Protocol and errors</summary><p>Latest 200 entries across connections in this page. Timestamps identify earlier failures.</p><button id="clear-errors">Clear log</button><pre id="errors" role="log">[]</pre></details>
</section>
</main>
<script type="module" src="/src/ui.js"></script>
</body></html>
+11
View File
@@ -13,3 +13,14 @@ pre { white-space: pre-wrap; overflow-wrap: anywhere; background: #111d28; paddi
.controls { display: flex; align-items: end; flex-wrap: wrap; gap: 12px; } .controls label { min-width: 150px; }
.notice { color: #ffcd7a; } details { margin: 8px 0; } summary { cursor: pointer; } output { display: block; margin: 8px 0; }
button:focus-visible, input:focus-visible, select:focus-visible { outline: 2px solid #7cceff; outline-offset: 2px; }
#status { font-weight: 650; padding: 8px 12px; border-left: 4px solid #ffcd7a; }
#status[data-state=connected] { border-color: #80d6ad; }
#status[data-state=error], #latest-error { color: #ffcd7a; }
#loaded-url { overflow-wrap: anywhere; font-size: 13px; }
#identity { font-weight: 600; }
#capabilities { display: flex; flex-wrap: wrap; gap: 8px 24px; padding-left: 20px; }
.target-group { margin-top: 24px; }
.target-group > h3 { background: #233444; padding: 8px 12px; }
.target { padding: 16px 12px; }
.target code { overflow-wrap: anywhere; }
@media (max-width: 600px) { header, main { padding: 12px; } section { padding: 12px; } .connection-row input { flex-basis: 100%; } }
+49
View File
@@ -0,0 +1,49 @@
import { ProtocolError } from './validation.js';
export function exhibitURL(value, base) {
if (!value.trim()) throw new Error('Enter an exhibit URL or served path.');
const url = new URL(value.trim(), base);
if (!['http:', 'https:'].includes(url.protocol) || url.origin !== new URL(base).origin || url.username || url.password) {
throw new Error('Enter a same-origin HTTP exhibit URL or served path.');
}
return url.href;
}
// Frame lifecycle only; protocol negotiation remains in ExhibitHost.
export class ExhibitConnection {
constructor({ host, createFrame, transport, base, changed = () => {}, loadTimeoutMs = 15000 }) {
Object.assign(this, { host, createFrame, transport, base, changed, loadTimeoutMs });
this.frame = null; this.url = ''; this.loading = false; this.generation = 0;
}
disconnect() {
this.generation++; clearTimeout(this.timer); this.loading = false;
this.frame?.remove(); this.frame = null; this.host.disconnect(); this.changed();
}
load(value) {
const url = exhibitURL(value, this.base); // Invalid input preserves the active exhibit.
this.disconnect(); this.url = url; this.loading = true;
const generation = this.generation;
const frame = this.createFrame(); this.frame = frame;
frame.addEventListener('load', () => {
if (generation !== this.generation) return;
clearTimeout(this.timer); this.loading = false; this.attach();
});
this.timer = setTimeout(() => {
if (generation !== this.generation) return;
this.disconnect();
this.host.report(new ProtocolError('LOAD_TIMEOUT', 'Exhibit did not finish loading. Check the served path and reconnect.'));
}, this.loadTimeoutMs);
frame.src = url; this.changed();
}
async attach() {
if (!this.frame || this.loading) return;
const generation = this.generation;
try { await this.host.connect(this.transport(this.frame)); }
catch (error) { if (generation === this.generation && !error.code) this.host.report(error); }
this.changed();
}
reconnect() {
if (this.frame && !this.loading) return this.attach();
if (this.url) this.load(this.url);
}
}
+36 -17
View File
@@ -1,12 +1,21 @@
import { ExhibitHost } from './host.js';
import { argumentSchema } from './validation.js';
import { postMessageTransport } from './transport/post-message.js';
import { ExhibitConnection } from './connection.js';
const host = new ExhibitHost();
const byId = id => document.getElementById(id);
const json = value => JSON.stringify(value, null, 2);
const rows = new Map();
let frame = null, renderedCatalog = null;
let renderedCatalog = null;
const connection = new ExhibitConnection({ host, base: location.href, transport: postMessageTransport, changed: render,
createFrame() {
const frame = document.createElement('iframe'); frame.title = 'Connected exhibit';
// Append only after src is assigned to avoid negotiating with the blank document.
queueMicrotask(() => { if (connection.frame === frame) byId('frame-container').append(frame); });
return frame;
}
});
function element(tag, text, parent) {
const node = document.createElement(tag); if (text !== undefined) node.textContent = text;
parent?.append(node); return node;
@@ -38,8 +47,15 @@ function editor(spec, label, parent) {
}
function buildCatalog() {
rows.clear(); byId('catalog').replaceChildren();
if (!host.catalog.length) element('p', 'Connect an exhibit to discover its controls.', byId('catalog'));
const groups = new Map();
for (const target of host.catalog) {
const section = element('article', undefined, byId('catalog')); section.className = 'target';
const category = target.category || 'Other';
if (!groups.has(category)) {
const group = element('div', undefined, byId('catalog')); group.className = 'target-group';
element('h3', category, group); groups.set(category, group);
}
const section = element('article', undefined, groups.get(category)); section.className = 'target';
element('h3', target.label ?? target.id, section); element('code', target.id, section);
element('p', `${target.kind} · ${target.readable ? 'readable' : 'not readable'} · ${target.writable ? 'writable' : 'not writable'}`, section);
const current = element('output', '', section);
@@ -85,16 +101,27 @@ function buildCatalog() {
}
}
function render() {
byId('status').textContent = `${host.status} · ${host.sync}`;
byId('status').textContent = connection.loading ? 'Loading exhibit…' : `${host.status} · ${host.sync}`;
byId('status').dataset.state = host.status;
byId('identity').textContent = host.exhibit
? `${host.exhibit.product} · ${host.exhibit.version ?? ''} · Contract ${host.contract.major}.${host.contract.minor}`
: 'No exhibit connected. Enter a served path to begin.';
byId('loaded-url').textContent = connection.url ? `Exhibit location: ${connection.url}` : '';
byId('metadata').textContent = json({ sessionId: host.sessionId, contract: host.contract, exhibit: host.exhibit,
registryRevision: host.registryRevision, stateRevision: host.stateRevision, sequence: host.sequence });
byId('capabilities').textContent = json(host.capabilities);
byId('capabilities').replaceChildren();
if (!host.capabilities.length) element('p', host.sessionId ? 'No capabilities declared.' : 'Capabilities appear after discovery.', byId('capabilities'));
for (const capability of host.capabilities) element('li', `${capability.id}: ${capability.state}`, byId('capabilities'));
byId('state').textContent = json(Object.fromEntries(host.values));
byId('events').textContent = json(host.eventLog);
byId('errors').textContent = json(host.logs);
const latest = host.logs.findLast(entry => entry.code !== 'SESSION');
byId('latest-error').textContent = latest ? `Latest diagnostic (${latest.time}): ${latest.code}: ${latest.message}` : '';
byId('latest-error').hidden = !latest;
byId('target-count').textContent = `(${host.catalog.length})`;
byId('refresh').disabled = host.status !== 'connected';
byId('reconnect').disabled = !frame;
byId('reconnect').disabled = !connection.url || connection.loading || host.status === 'negotiating';
byId('disconnect').disabled = !connection.frame;
if (host.catalog !== renderedCatalog) { renderedCatalog = host.catalog; buildCatalog(); }
for (const row of rows.values()) {
row.current.textContent = host.values.has(row.target.id) ? `Reported value: ${json(host.values.get(row.target.id))}` : 'No persistent value reported.';
@@ -106,22 +133,14 @@ function render() {
if (row.submit) row.submit.disabled = row.busy || host.status !== 'connected' || !!row.schemaError || unavailable.length > 0;
}
}
async function attach() {
if (!frame) return;
try { await host.connect(postMessageTransport(frame)); } catch (error) { if (!error.code) host.report(error); }
}
byId('connection').addEventListener('submit', event => {
event.preventDefault();
try {
const url = new URL(byId('exhibit-url').value, location.href);
if (url.origin !== location.origin || !['http:', 'https:'].includes(url.protocol)) throw new Error('Enter a same-origin HTTP exhibit URL.');
host.disconnect(); frame?.remove();
frame = document.createElement('iframe'); frame.title = 'Connected exhibit';
frame.addEventListener('load', attach); frame.src = url.href;
byId('frame-container').append(frame); render();
connection.load(byId('exhibit-url').value);
} catch (error) { host.report(error); }
});
byId('reconnect').addEventListener('click', attach);
byId('disconnect').addEventListener('click', () => { host.disconnect(); frame?.remove(); frame = null; render(); });
byId('reconnect').addEventListener('click', () => connection.reconnect());
byId('disconnect').addEventListener('click', () => connection.disconnect());
byId('refresh').addEventListener('click', () => host.refresh().catch(error => host.report(error)));
byId('clear-errors').addEventListener('click', () => { host.logs = []; render(); });
host.subscribe(render); render();
+2
View File
@@ -92,6 +92,8 @@ export function validateCatalog(message) {
&& typeof t.readable === 'boolean' && typeof t.writable === 'boolean'
&& Array.isArray(t.requires) && t.requires.every(id => typeof id === 'string'), 'Invalid target descriptor.');
ids.add(t.id);
check(t.restorable === undefined || typeof t.restorable === 'boolean', 'Invalid target restorable metadata.');
check(t.category === undefined || typeof t.category === 'string', 'Invalid target category metadata.');
if (t.kind === 'range') check(Number.isFinite(t.min) && Number.isFinite(t.max) && t.min <= t.max
&& Number.isFinite(t.step) && t.step > 0, 'Invalid range descriptor.');
if (t.kind === 'selection') check(Array.isArray(t.options)
+5
View File
@@ -20,6 +20,11 @@ Local compatibility corrections:
- Planetarium's magnitude reader and native dispatch still used the old
camelCase ID. They now match its existing canonical `sky.magnitude-limit`
descriptor, fixing a null snapshot value and failed native writes.
- Final Step 5 closure: Haunted House's haunt-level reader now clamps to 0–1
and rounds to integer hundredths, matching its unchanged 0.01 descriptor step.
The internal drifting simulation and Manifest behavior remain continuous;
snapshots and mutation events share this normalized reader. Previously the
reader rounded to thousandths and could report off-step values.
NGN has no fixture-specific target logic. These corrections apply only to
the local fixture copies. No upstream fixes or independent conformance
+147
View File
@@ -0,0 +1,147 @@
# Step 5 repeatable verification
Latest rerun (2026-09-14): the corrected SciFi text targets pass through the normal
UI; NGN tests pass 25/25. The browser gate currently fails at 2/3 on Haunted House's
haunt-level step, and publishing validation still fails. See completion report Q.
Run from the NGN repository root with Node.js 22+. Use `npm start` and the normal
UI at `http://127.0.0.1:4173/`. No npm install, build, source edit, external mount,
or test harness is required for operator use. Use the same origin/port for every
exhibit. `npm test` runs the automated tests.
## External SciFi-XZBT preparation
SciFi-XZBT is supplied separately. The run recorded here used the actual working
copy at `G:/.vibe/SciFi-XZBT`, whose HEAD was
`179b9db715aaa92999c5bfef451b6877f30079fe` with local changes and untracked contract
files. This is NOT evidence for that commit alone or for a released dist build.
The original SHA-256 manifest `evidence/step5-scifi-files.json` identifies the
initial 12 files tested. The authorized follow-up corrected the external adapter's
two argument descriptors and matching validator. Its fresh copy is identified by
`evidence/step5-scifi-arguments-files.json`; all 12 source/copy hashes match.
For this source layout, copy the runtime entry point and its relative assets
without modifying them. Use a fresh destination name when repeating with a newer
copy, to avoid mixing versions:
```powershell
$exhibitSource = 'G:/.vibe/SciFi-XZBT'
$exhibitDestination = 'test-fixtures/local/scifi-arguments'
New-Item -ItemType Directory -Path $exhibitDestination
Copy-Item -LiteralPath (Join-Path $exhibitSource 'index.html') -Destination $exhibitDestination
Copy-Item -LiteralPath (Join-Path $exhibitSource 'js') -Destination $exhibitDestination -Recurse
Copy-Item -LiteralPath (Join-Path $exhibitSource 'css') -Destination $exhibitDestination -Recurse
```
`test-fixtures/local/` is ignored by Git. Do not copy the external repository's
credentials, .git, or development tooling into a served directory. A future
self-contained distributable can instead be placed under `public/` unchanged.
The tested source copy uses only index.html, js/ and css/ for this workflow.
Enter `/test-fixtures/local/scifi-arguments/index.html` in the **normal NGN UI**, then
select **Load exhibit**. Expected identity: SciFi-XZBT 5.2.0, Contract 5.2,
61 discovered targets. Inspect Session and exhibit metadata and Current
persistent state. Controls below refer to labels discovered from this copy;
they are test instructions, not product configuration.
1. Set Master Volume to 0.42; verify reported value and state.changed.
2. Set Master Mute true; verify reported value.
3. Set Observation Viewport Active true, then false; verify reported values
and native exhibit behavior within its single frame.
4. Select Active Audio Preset / Interprise-G: Main Engineering; verify
preset.selected and related changes share one resulting revision.
5. Invoke Comm Badge Chime; verify action.executed (acceptance, not audio fidelity).
6. Invoke Demat Cycle Impulse while in Starfleet; expect CAPABILITY_UNAVAILABLE.
7. Select Active Sci-Fi Universe / Whataverse / Tardix, then invoke Demat Cycle
Impulse; verify action.executed in that context.
8. Inspect Speak Text Line and Display Ticker Message descriptors. A conformant
version declares an `arguments` array and NGN shows its text field. Enter text
and invoke; verify acceptance/event. **The corrected copy passes this check:**
both fields appear and action.executed contains the exact submitted args.text.
Speech declares maxLength 2000, ticker 512, both required strings. Correct the
external source before copying; do not add a host workaround or patch only the
served copy. The original copy's legacy args.text failure remains in old evidence.
9. Inspect Incoming events: source, correlationId, monotonically increasing
sequence and stateRevision. Reconnect while attached: new session, cleared
event log/sequence baseline, same exhibit state and revision.
10. Disconnect: no frame/catalog/cache remains. Reconnect: URL reloads, a new
instance and session are discovered. Initial state/revision may differ from
the removed instance. No source changes required.
Classify failures before changing NGN: host defect, external exhibit contract
defect, conformant but awkward behavior, or environment limitation. Sound
acceptance/events are covered; audible output and optional AI downloads are not
certified. Text-argument failure blocks Step 5 acceptance.
## Reference regression through the normal UI
Use these paths, changing only the URL field:
| Exhibit | URL | Representative operations |
| --- | --- | --- |
| Aquarium | /test-fixtures/reference-exhibits/aquarium/index.html | Population 16, Paused true, Species Blue Tang, Feed |
| Planetarium | /test-fixtures/reference-exhibits/planetarium/index.html | Azimuth 0.75, Sky frozen true, Constellation Cygnus, Advance time with Include minutes checked and minutes 30 |
| Haunted House | /test-fixtures/reference-exhibits/haunted-house/index.html | Haunt level 0.5, Held true, Room Library, Manifest |
Inspect reports and events, reconnect and confirm a new session. Disconnect and
reload the remembered URL. Aquarium and Planetarium pass the representative
flows. Haunted House emits Manifest's action event, but during the effect a
snapshot can contain a haunt-level value off its declared 0.01 step; NGN reports
INVALID_MESSAGE and preserves its previous cache. This is a pre-existing fixture
limitation, not a new host regression. No fixture repair was made in Step 5.
The existing transport suite at `/test-fixtures/host-verification.html` remains a
separate regression check: select Run verification, expect 3/3, and save the
visible timestamped text or browser console output. The latest rerun produced
2/3, failing Haunted House's step validation. Its first impulse with no capability
requirements is Manifest, and the fixture's continuous drift can also produce
off-step snapshots. The earlier statement that this suite avoids Manifest was
incorrect. Do not reinterpret a historical 3/3 as the current result or as complete
fixture conformance certification.
## Failure and cancellation checks
- Start with no URL: useful disconnected controls/empty catalog.
- Enter an off-origin URL: reject and preserve the current exhibit.
- Enter a missing local URL: document-load timeout (15 seconds in the checked
browser), or hello timeout (5 seconds if an error page finishes loading).
Enter a valid path to recover.
- Disconnect during loading or switch rapidly: old frame load callbacks must
not attach to the new exhibit. Focused automated lifecycle tests cover this.
- Refresh state reads current values. Logs can be cleared; prior diagnostics
are timestamped and do not imply the current connection is failed.
## Evidence and repeatability limits
`evidence/step5-reference-suite.txt` is the actual timestamped browser suite output.
`step5-scifi-session.json` contains the actual discovered catalog, session metadata,
events and failures. The three reference JSON files contain DOM-rendered runtime
metadata/events; Haunted House also includes its errors. `step5-operator.jpg`
shows the operator panel. Evidence was captured in local Chromium on 2026-09-14.
A fresh `npm start` succeeded on port 4175 because an existing service occupied
4173; the final browser transport suite ran on that fresh process. The normal UI
ran on 4173. No separate pristine checkout was created; tests/startup use the
current source tree without build products. SciFi repeatability requires the
external files described above. Repository publishing validation has an existing
malformed trailing record block in the Post Step 4 Completion devlog; see report.
Follow-up evidence is separate from those historical artifacts:
`step5-scifi-arguments-ui.json` captures the corrected descriptors, text fields,
and successful events; `step5-scifi-arguments-files.json` identifies the tested
working copy; `step5-acceptance-rerun-browser.txt` records the current 2/3 failure.
`step5-scifi-arguments-ui-final.json` records the repeated checks after line-ending
cleanup, including a draft reset on speech capability rediscovery and successful
ticker resubmission with the exact intended text.
The follow-up started a fresh server on 4173 and performed the requested narrow
SciFi checks, rather than repeating every earlier operator case above.
The optional external validator regression check is:
```powershell
node test-fixtures/check-scifi-arguments.cjs G:/.vibe/SciFi-XZBT/js/contract-adapter.js
```
It checks descriptor arrays, required/type/unknown-key/length rejection before
execution, maximum-length acceptance, and unchanged argumentless behavior. It
supplements the real operator UI checks; it does not replace them.
+38
View File
@@ -0,0 +1,38 @@
// Optional external-exhibit regression check; not NGN product configuration.
// Usage: node test-fixtures/check-scifi-arguments.cjs <contract-adapter.js>
const fs = require('node:fs');
const vm = require('node:vm');
const assert = require('node:assert/strict');
async function main() {
const context = { window: {}, console };
vm.createContext(context);
vm.runInContext(fs.readFileSync(process.argv[2], 'utf8'), context);
const calls = [];
const adapter = new context.window.XZBTContractAdapter({ bindings: { invokers: {
'speech.say': args => calls.push(args),
'display.ticker': args => calls.push(args),
'speech.generate-announcement': args => calls.push(args)
} } });
for (const [id, maxLength] of [['speech.say', 2000], ['display.ticker', 512]]) {
const descriptor = adapter.targets.get(id);
assert.equal(Object.hasOwn(descriptor, 'args'), false);
assert.deepEqual(JSON.parse(JSON.stringify(descriptor.arguments)), [
{ name: 'text', type: 'string', required: true, maxLength }
]);
for (const args of [{}, { text: null }, { text: 42 }, { text: 'x', extra: true }, { text: 'x'.repeat(maxLength + 1) }]) {
const before = calls.length;
const result = await adapter.invokeAction(id, args);
assert.equal(result.code, 'INVALID_VALUE');
assert.equal(calls.length, before, 'invalid arguments must not execute');
}
const text = 'x'.repeat(maxLength);
assert.equal((await adapter.invokeAction(id, { text })).ok, true);
assert.equal(calls.at(-1).text, text);
console.log(`PASS ${id}: arguments metadata, required/type/unknown/length rejection, exact length accepted`);
}
assert.equal((await adapter.invokeAction('speech.generate-announcement', {})).ok, true);
assert.equal((await adapter.invokeAction('speech.generate-announcement', { text: 'x' })).code, 'INVALID_VALUE');
console.log('PASS argumentless impulse: empty args accepted, unknown key rejected');
}
main().catch(error => { console.error(error); process.exitCode = 1; });
@@ -0,0 +1,7 @@
Started: 2026-09-14T19:41:45.445Z
PASS aquarium: 12 targets; negotiation, discovery, state/range/selection, invoke, errors, events, no-op, reconnect, session invalidation/recovery.
PASS planetarium: 14 targets; negotiation, discovery, state/range/selection, invoke, errors, events, no-op, reconnect, session invalidation/recovery.
FAIL haunted-house: room.haunt-level: Value must follow step 0.01.
2/3 real exhibits passed.
Finished: 2026-09-14T19:41:45.575Z
@@ -0,0 +1,66 @@
{
"events": [
{
"correlationId": "ngn-2-4",
"sequence": 1,
"sessionId": "sess-1a192",
"source": "host",
"stateRevision": 1,
"target": "tank.population",
"timestamp": 1789398588359,
"type": "state.changed",
"value": 16,
"xzbt": "5.2"
},
{
"correlationId": "ngn-2-6",
"sequence": 2,
"sessionId": "sess-1a192",
"source": "host",
"stateRevision": 2,
"target": "tank.paused",
"timestamp": 1789398588908,
"type": "state.changed",
"value": true,
"xzbt": "5.2"
},
{
"correlationId": "ngn-2-8",
"sequence": 3,
"sessionId": "sess-1a192",
"source": "host",
"stateRevision": 3,
"target": "tank.species",
"timestamp": 1789398589178,
"type": "selection.changed",
"value": "blue-tang",
"xzbt": "5.2"
},
{
"args": {},
"correlationId": "ngn-2-10",
"sequence": 4,
"sessionId": "sess-1a192",
"source": "host",
"target": "action.feed",
"timestamp": 1789398589436,
"type": "action.executed",
"xzbt": "5.2"
}
],
"metadata": {
"contract": {
"major": 5,
"minor": 2
},
"exhibit": {
"build": "reference-exhibit",
"product": "Aquarium",
"version": "1.0.0"
},
"registryRevision": 1,
"sequence": 4,
"sessionId": "sess-1a192",
"stateRevision": 3
}
}
@@ -0,0 +1,7 @@
Started: 2026-09-14T19:55:22.193Z
PASS aquarium: 12 targets; negotiation, discovery, state/range/selection, invoke, errors, events, no-op, reconnect, session invalidation/recovery.
PASS planetarium: 14 targets; negotiation, discovery, state/range/selection, invoke, errors, events, no-op, reconnect, session invalidation/recovery.
PASS haunted-house: 13 targets; negotiation, discovery, state/range/selection, invoke, errors, events, no-op, reconnect, session invalidation/recovery.
3/3 real exhibits passed.
Finished: 2026-09-14T19:55:22.342Z
@@ -0,0 +1,25 @@
{
"capturedAt": "2026-09-14T19:56:36.172Z",
"checks": [
{
"status": "fulfilled",
"value": {
"chunk_id": "82977d",
"wall_time_seconds": 1.3615515999999999,
"exit_code": 0,
"original_token_count": 585,
"output": "\n> [email protected] test\n> node --test\n\n✔ connection accepts served paths and same-origin URLs, rejects unsupported locations (1.7406ms)\n✔ switch and disconnect cancel old frame loads; invalid URL preserves active frame (1.1493ms)\n✔ reconnect negotiates existing frame and reloads remembered URL after disconnect (0.4236ms)\n✔ stalled document load ends with recoverable error (20.0644ms)\n✔ optional descriptor metadata is permissive when omitted and typed when supplied (1.4027ms)\n✔ Haunted House reports bounded hundredths while preserving continuous drift (28.2601ms)\n✔ Haunted House snapshots and mutation events stay valid through Manifest, drift and Calm (11.0154ms)\n✔ negotiates, discovers and reads real session fields (4.696ms)\n✔ sets all persistent kinds, invokes arguments, preserves no-op revisions (1.6233ms)\n✔ invalid values and required/unknown arguments never reach transport (1.4478ms)\n✔ argument type/enum/bounds/step/length rules and unsupported future types (0.5322ms)\n✔ surfaces exhibit errors and invalid session requires reconnect (0.9883ms)\n✔ unsupported version and timeout are explicit failures (11.4367ms)\n✔ native events update cache; shared revision is not a gap (0.8535ms)\n✔ sequence and revision gaps resync and remain visible (26.0243ms)\n✔ capability and registry changes debounce discovery; normal state does not (52.8678ms)\n✔ events arriving during snapshot are replayed without lost updates (0.6791ms)\n✔ old session traffic does not mutate new cache (0.4186ms)\n✔ malformed persistent events trigger recovery (25.6604ms)\n✔ postMessage verifies origin and source and cleans up (0.4321ms)\n✔ local server serves host and denies repository/private paths (41.9438ms)\n✔ invalid reported types do not contaminate the cache (26.2967ms)\n✔ invalid snapshot values preserve previous cache and mark uncertainty (0.6139ms)\n✔ snapshot revision cannot roll back a previously observed transaction (0.322ms)\n✔ registry rediscovery adds descriptors and removes obsolete cached targets (25.7427ms)\n✔ unsupported future impulse arguments do not hide the catalog (0.7149ms)\n✔ capability requirements block unusable services without deleting targets (0.4759ms)\nℹ tests 27\nℹ suites 0\nℹ pass 27\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 388.0334\n"
}
},
{
"status": "fulfilled",
"value": {
"chunk_id": "9c0253",
"wall_time_seconds": 31.5457821,
"exit_code": 0,
"original_token_count": 8,
"output": "devlog-editor: devlog is valid\r\n"
}
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,128 @@
{
"events": [
{
"correlationId": "ngn-11-35",
"sequence": 1,
"sessionId": "sess-1ce04",
"source": "host",
"stateRevision": 1,
"target": "room.haunt-level",
"timestamp": 1789398645709,
"type": "state.changed",
"value": 0.5,
"xzbt": "5.2"
},
{
"correlationId": "ngn-11-37",
"sequence": 2,
"sessionId": "sess-1ce04",
"source": "host",
"stateRevision": 2,
"target": "room.paused",
"timestamp": 1789398646251,
"type": "state.changed",
"value": true,
"xzbt": "5.2"
},
{
"correlationId": "ngn-11-39",
"sequence": 3,
"sessionId": "sess-1ce04",
"source": "host",
"stateRevision": 3,
"target": "room.current",
"timestamp": 1789398646528,
"type": "selection.changed",
"value": "library",
"xzbt": "5.2"
},
{
"args": {},
"correlationId": "ngn-11-41",
"sequence": 4,
"sessionId": "sess-1ce04",
"source": "host",
"target": "action.manifest",
"timestamp": 1789398646792,
"type": "action.executed",
"xzbt": "5.2"
}
],
"logs": [
{
"code": "SESSION",
"detail": {
"sessionId": "sess-1a192"
},
"message": "Exhibit session established.",
"time": "2026-09-14T15:09:20.864Z"
},
{
"code": "SESSION",
"detail": {
"sessionId": "sess-21bcb"
},
"message": "Exhibit session established.",
"time": "2026-09-14T15:09:49.720Z"
},
{
"code": "SESSION",
"detail": {
"sessionId": "sess-14131"
},
"message": "Exhibit session established.",
"time": "2026-09-14T15:09:50.283Z"
},
{
"code": "SESSION",
"detail": {
"sessionId": "sess-194b7"
},
"message": "Exhibit session established.",
"time": "2026-09-14T15:10:00.111Z"
},
{
"code": "SESSION",
"detail": {
"sessionId": "sess-22e"
},
"message": "Exhibit session established.",
"time": "2026-09-14T15:10:24.934Z"
},
{
"code": "SESSION",
"detail": {
"sessionId": "sess-1ce04"
},
"message": "Exhibit session established.",
"time": "2026-09-14T15:10:25.249Z"
},
{
"code": "INVALID_MESSAGE",
"detail": null,
"message": "room.haunt-level: Value must follow step 0.01.",
"time": "2026-09-14T15:10:46.252Z"
},
{
"code": "INVALID_MESSAGE",
"detail": null,
"message": "room.haunt-level: Value must follow step 0.01.",
"time": "2026-09-14T15:10:46.530Z"
}
],
"metadata": {
"contract": {
"major": 5,
"minor": 2
},
"exhibit": {
"build": "reference-exhibit",
"product": "Haunted House",
"version": "1.0.0"
},
"registryRevision": 1,
"sequence": 4,
"sessionId": "sess-1ce04",
"stateRevision": 3
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

@@ -0,0 +1,68 @@
{
"events": [
{
"correlationId": "ngn-8-21",
"sequence": 1,
"sessionId": "sess-194b7",
"source": "host",
"stateRevision": 1,
"target": "sky.azimuth",
"timestamp": 1789398623275,
"type": "state.changed",
"value": 0.75,
"xzbt": "5.2"
},
{
"correlationId": "ngn-8-23",
"sequence": 2,
"sessionId": "sess-194b7",
"source": "host",
"stateRevision": 2,
"target": "sky.paused",
"timestamp": 1789398623816,
"type": "state.changed",
"value": true,
"xzbt": "5.2"
},
{
"correlationId": "ngn-8-25",
"sequence": 3,
"sessionId": "sess-194b7",
"source": "host",
"stateRevision": 3,
"target": "sky.constellation",
"timestamp": 1789398624094,
"type": "selection.changed",
"value": "cygnus",
"xzbt": "5.2"
},
{
"args": {
"minutes": 30
},
"correlationId": "ngn-8-27",
"sequence": 4,
"sessionId": "sess-194b7",
"source": "host",
"target": "action.advance-time",
"timestamp": 1789398624643,
"type": "action.executed",
"xzbt": "5.2"
}
],
"metadata": {
"contract": {
"major": 5,
"minor": 2
},
"exhibit": {
"build": "reference-exhibit",
"product": "Planetarium",
"version": "1.0.0"
},
"registryRevision": 1,
"sequence": 4,
"sessionId": "sess-194b7",
"stateRevision": 3
}
}
@@ -0,0 +1,6 @@
Started: 2026-09-14T15:11:53.141Z
PASS aquarium: 12 targets; negotiation, discovery, state/range/selection, invoke, errors, events, no-op, reconnect, session invalidation/recovery.
PASS planetarium: 14 targets; negotiation, discovery, state/range/selection, invoke, errors, events, no-op, reconnect, session invalidation/recovery.
PASS haunted-house: 13 targets; negotiation, discovery, state/range/selection, invoke, errors, events, no-op, reconnect, session invalidation/recovery.
3/3 real exhibits passed.
Finished: 2026-09-14T15:11:53.273Z
@@ -0,0 +1,68 @@
{
"capturedAt": "2026-09-14T19:45:18.3670019Z",
"source": "G:\\.vibe\\SciFi-XZBT",
"sourceCommit": "179b9db715aaa92999c5bfef451b6877f30079fe",
"destination": "G:\\.vibe\\XZBT-NGN\\test-fixtures\\local\\scifi-arguments",
"files": [
{
"path": "index.html",
"sourceSha256": "9A851185BB5CB78AD034E25709253222BB35CE061716F611750CB758A55BC44C",
"copySha256": "9A851185BB5CB78AD034E25709253222BB35CE061716F611750CB758A55BC44C"
},
{
"path": "css\\style.css",
"sourceSha256": "93032672388E757EFB6A52C5059344C378A7CED9F738D2C907EF4D969BB87CEE",
"copySha256": "93032672388E757EFB6A52C5059344C378A7CED9F738D2C907EF4D969BB87CEE"
},
{
"path": "js\\app.js",
"sourceSha256": "499280FE19BAF4D4429AD915958C2BB54247018CDAC1195EB17E906DDC6370FE",
"copySha256": "499280FE19BAF4D4429AD915958C2BB54247018CDAC1195EB17E906DDC6370FE"
},
{
"path": "js\\audio.js",
"sourceSha256": "0E8C4D8C09B7C53F0C2F402C259AACE95656AA4181709CB9F4D8F30F0D451A64",
"copySha256": "0E8C4D8C09B7C53F0C2F402C259AACE95656AA4181709CB9F4D8F30F0D451A64"
},
{
"path": "js\\config.js",
"sourceSha256": "8ED99BDABD688C5D738DF6515BADCED90CD2EC73C81CAFC07376E62BFCF15CE5",
"copySha256": "8ED99BDABD688C5D738DF6515BADCED90CD2EC73C81CAFC07376E62BFCF15CE5"
},
{
"path": "js\\contract-adapter.js",
"sourceSha256": "71C74BECA55046D7899653C0C8FA420A169BB6A68BAA43E1D02443AA6AAE7747",
"copySha256": "71C74BECA55046D7899653C0C8FA420A169BB6A68BAA43E1D02443AA6AAE7747"
},
{
"path": "js\\control-bus.js",
"sourceSha256": "47160558AF54F8F7E6408B6E2E52E726568EBA02058C4518CC221B86AC8F2234",
"copySha256": "47160558AF54F8F7E6408B6E2E52E726568EBA02058C4518CC221B86AC8F2234"
},
{
"path": "js\\core-animations.js",
"sourceSha256": "0CA430E307090F2D391FD92C074B00B7193259DD82FC656612DBF7397D945E7D",
"copySha256": "0CA430E307090F2D391FD92C074B00B7193259DD82FC656612DBF7397D945E7D"
},
{
"path": "js\\generative-experience.js",
"sourceSha256": "371FF099E9529DA9D93F20B943F46A40466248AAEF91955D11BF005DC29817DB",
"copySha256": "371FF099E9529DA9D93F20B943F46A40466248AAEF91955D11BF005DC29817DB"
},
{
"path": "js\\observation-bezels.js",
"sourceSha256": "18B853477F66C39CA6BCC7FE08C9C2BF868E2A909D3E18C38681D589205108FA",
"copySha256": "18B853477F66C39CA6BCC7FE08C9C2BF868E2A909D3E18C38681D589205108FA"
},
{
"path": "js\\observation-engine.js",
"sourceSha256": "A5FEE90BDE61F733D28FD9F5EA58A81BCA911F9B3D0BBE41AE65CB06C5188376",
"copySha256": "A5FEE90BDE61F733D28FD9F5EA58A81BCA911F9B3D0BBE41AE65CB06C5188376"
},
{
"path": "js\\visualizer.js",
"sourceSha256": "ED33A96E62674D0F7F30DDDC9FE83E94CA488BFCA45F32585E7AE51F0B35B89A",
"copySha256": "ED33A96E62674D0F7F30DDDC9FE83E94CA488BFCA45F32585E7AE51F0B35B89A"
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,50 @@
[
{
"path": "index.html",
"sha256": "9A851185BB5CB78AD034E25709253222BB35CE061716F611750CB758A55BC44C"
},
{
"path": "css\\style.css",
"sha256": "93032672388E757EFB6A52C5059344C378A7CED9F738D2C907EF4D969BB87CEE"
},
{
"path": "js\\app.js",
"sha256": "499280FE19BAF4D4429AD915958C2BB54247018CDAC1195EB17E906DDC6370FE"
},
{
"path": "js\\audio.js",
"sha256": "0E8C4D8C09B7C53F0C2F402C259AACE95656AA4181709CB9F4D8F30F0D451A64"
},
{
"path": "js\\config.js",
"sha256": "8ED99BDABD688C5D738DF6515BADCED90CD2EC73C81CAFC07376E62BFCF15CE5"
},
{
"path": "js\\contract-adapter.js",
"sha256": "0ED3234477EE21C50F1E872313B6259DF37E269D06C523DA60E41B484DA5236F"
},
{
"path": "js\\control-bus.js",
"sha256": "47160558AF54F8F7E6408B6E2E52E726568EBA02058C4518CC221B86AC8F2234"
},
{
"path": "js\\core-animations.js",
"sha256": "0CA430E307090F2D391FD92C074B00B7193259DD82FC656612DBF7397D945E7D"
},
{
"path": "js\\generative-experience.js",
"sha256": "371FF099E9529DA9D93F20B943F46A40466248AAEF91955D11BF005DC29817DB"
},
{
"path": "js\\observation-bezels.js",
"sha256": "18B853477F66C39CA6BCC7FE08C9C2BF868E2A909D3E18C38681D589205108FA"
},
{
"path": "js\\observation-engine.js",
"sha256": "A5FEE90BDE61F733D28FD9F5EA58A81BCA911F9B3D0BBE41AE65CB06C5188376"
},
{
"path": "js\\visualizer.js",
"sha256": "ED33A96E62674D0F7F30DDDC9FE83E94CA488BFCA45F32585E7AE51F0B35B89A"
}
]
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -19,7 +19,7 @@ async function raw(transport, message) {
});
}
run.addEventListener('click', async () => {
run.disabled = true; results.textContent = ''; document.querySelector('#fixtures').replaceChildren();
run.disabled = true; results.textContent = `Started: ${new Date().toISOString()}\n`; document.querySelector('#fixtures').replaceChildren();
let passed = 0;
for (const name of ['aquarium', 'planetarium', 'haunted-house']) {
const host = new ExhibitHost();
@@ -74,5 +74,6 @@ run.addEventListener('click', async () => {
} catch (error) { results.textContent += `FAIL ${name}: ${error.message}\n`; }
finally { host.disconnect(); frame.remove(); }
}
results.textContent += `${passed}/3 real exhibits passed.\n`; run.disabled = false;
results.textContent += `${passed}/3 real exhibits passed.\nFinished: ${new Date().toISOString()}\n`; run.disabled = false;
console.info(results.textContent);
});
@@ -582,7 +582,8 @@
switch (targetId) {
case 'room.current': return h.room;
case 'room.spirit': return h.spirit;
case 'room.haunt-level': return Math.round(h.hauntLevel * 1000) / 1000;
// Keep the simulation continuous; report bounded, integer hundredths.
case 'room.haunt-level': return Math.round(clamp(h.hauntLevel, 0, 1) * 100) / 100;
case 'room.fog-density': return h.fogDensity;
case 'room.candlelight': return h.candlelight;
case 'room.audio-enabled': return h.audioEnabled;
+63
View File
@@ -0,0 +1,63 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { ExhibitConnection, exhibitURL } from '../src/connection.js';
import { validateCatalog } from '../src/validation.js';
const base = 'http://127.0.0.1:4173/';
test('connection accepts served paths and same-origin URLs, rejects unsupported locations', () => {
assert.equal(exhibitURL('/public/example/index.html', base), base + 'public/example/index.html');
assert.equal(exhibitURL(' public/example/ ', base), base + 'public/example/');
for (const value of ['', ' ', 'file:///C:/exhibit.html', 'https://other.example/', 'http://127.0.0.1:4174/', 'javascript:alert(1)', 'http://user:[email protected]:4173/']) {
assert.throws(() => exhibitURL(value, base));
}
});
function setup(t, loadTimeoutMs = 1000) {
const frames = [], connections = [], errors = [];
const host = { disconnect() {}, async connect(frame) { connections.push(frame); }, report(e) { errors.push(e); } };
const connection = new ExhibitConnection({ host, base, loadTimeoutMs, transport: frame => frame,
createFrame() {
const frame = { handlers: {}, addEventListener(type, fn) { this.handlers[type] = fn; }, remove() { this.removed = true; } };
frames.push(frame); return frame;
}
});
t.after(() => connection.disconnect());
return { connection, frames, connections, errors };
}
test('switch and disconnect cancel old frame loads; invalid URL preserves active frame', async t => {
const { connection, frames, connections } = setup(t);
connection.load('/public/one/'); connection.load('/public/two/');
frames[0].handlers.load(); assert.equal(connections.length, 0);
frames[1].handlers.load(); assert.equal(connections.length, 1);
assert.throws(() => connection.load('https://other.example/'));
assert.equal(connection.frame, frames[1]);
connection.disconnect(); frames[1].handlers.load();
assert.equal(connections.length, 1); assert.equal(connection.frame, null);
assert.ok(frames.every(f => f.removed));
});
test('reconnect negotiates existing frame and reloads remembered URL after disconnect', async t => {
const { connection, frames, connections } = setup(t);
connection.load('/public/one/'); frames[0].handlers.load();
await connection.reconnect(); assert.equal(connections.length, 2); assert.equal(frames.length, 1);
connection.disconnect(); connection.reconnect();
assert.equal(frames.length, 2); assert.equal(frames[1].src, base + 'public/one/');
frames[1].handlers.load(); assert.equal(connections.length, 3);
});
test('stalled document load ends with recoverable error', async t => {
const { connection, frames, errors } = setup(t, 5);
connection.load('/public/stalled/');
await new Promise(resolve => setTimeout(resolve, 20));
assert.equal(connection.loading, false); assert.equal(connection.frame, null);
assert.equal(errors[0].code, 'LOAD_TIMEOUT');
connection.reconnect(); assert.equal(frames.length, 2);
});
test('optional descriptor metadata is permissive when omitted and typed when supplied', () => {
const message = { exhibit: {}, contract: { major: 5 }, registryRevision: 0, stateRevision: 0, capabilities: [],
targets: [{ id: 'sample.enabled', kind: 'state', readable: true, writable: true, valueType: 'boolean', requires: [] }] };
validateCatalog(message);
for (const metadata of [{ restorable: false, category: '' }, { restorable: true, category: 'Custom category' }]) {
validateCatalog({ ...message, targets: [{ ...message.targets[0], ...metadata }] });
}
for (const metadata of [{ restorable: 'false' }, { restorable: null }, { restorable: 1 }, { category: null }, { category: [] }, { category: false }]) {
assert.throws(() => validateCatalog({ ...message, targets: [{ ...message.targets[0], ...metadata }] }), { code: 'INVALID_MESSAGE' });
}
});
+63
View File
@@ -0,0 +1,63 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import vm from 'node:vm';
import { validateReportedValue } from '../src/validation.js';
function setup() {
const context = vm.createContext({ window: {} });
for (const file of ['shared/contract-core.js', 'haunted-house/exhibit.js', 'haunted-house/contract-adapter.js']) {
vm.runInContext(readFileSync(new URL(`../test-fixtures/reference-exhibits/${file}`, import.meta.url), 'utf8'), context);
}
const exhibit = new context.window.HauntedHouseExhibit({
canvas: { getContext: () => ({ setTransform() {} }), getBoundingClientRect: () => ({ width: 100, height: 100 }) },
announcer: { flash() {} }
});
const core = context.window.HauntedHouseContract.create(exhibit, { sync() {} });
const descriptor = core.catalog.get('room.haunt-level');
assert.deepEqual([descriptor.min, descriptor.max, descriptor.step], [0, 1, 0.01]);
return { exhibit, core, descriptor };
}
test('Haunted House reports bounded hundredths while preserving continuous drift', () => {
const { exhibit, core, descriptor } = setup();
for (const [raw, expected] of [[-0.1, 0], [0, 0], [0.0049, 0], [0.0051, 0.01], [0.303, 0.3], [0.999, 1], [1, 1], [1.1, 1]]) {
exhibit.house.hauntLevel = raw;
assert.equal(core.readValue(descriptor.id), expected);
assert.equal(exhibit.house.hauntLevel, raw, 'reading must not quantize the simulation');
}
for (const room of ['foyer', 'library', 'cellar', 'attic', 'nursery']) {
exhibit.setRoom(room);
for (const spirit of ['restless', 'vengeful', 'mourning', 'mischievous']) {
exhibit.setSpirit(spirit);
exhibit.house.hauntLevel = 0.303;
for (let i = 0; i < 600; i++) {
exhibit.house.step(1 / 60);
validateReportedValue(descriptor, core.readValue(descriptor.id));
}
assert.notEqual(exhibit.house.hauntLevel, 0.303, 'simulation must keep drifting');
}
}
});
test('Haunted House snapshots and mutation events stay valid through Manifest, drift and Calm', () => {
const { exhibit, core, descriptor } = setup();
core.handleHello({ supportedContractMajors: [5] });
const checkSnapshot = () => {
const snapshot = core.stateSnapshot();
for (const target of core.catalog.descriptors().filter(t => t.readable)) validateReportedValue(target, snapshot.values[target.id]);
return snapshot.values[descriptor.id];
};
assert.equal(core.invokeAction('action.manifest', {}, 'host').ok, true);
assert.equal(exhibit.house.hauntLevel, 1);
assert.equal(checkSnapshot(), 1);
for (let i = 0; i < 600; i++) { exhibit.house.step(1 / 60); checkSnapshot(); }
for (const value of [0, 0.01, 0.29, 0.5, 0.99, 1]) {
assert.equal(core.applyMutation(descriptor.id, value, 'ui').ok, true);
assert.equal(checkSnapshot(), value);
}
for (const event of core.eventLog().filter(e => e.type === 'state.changed')) validateReportedValue(core.catalog.get(event.target), event.value);
assert.ok(core.eventLog().some(e => e.type === 'action.executed' && e.target === 'action.manifest'));
assert.equal(core.invokeAction('action.calm', {}, 'host').ok, true);
assert.equal(checkSnapshot(), 0);
});