generated from Labyricorn/labyricorn-project-template
Step 5 Complete
This commit is contained in:
@@ -21,3 +21,5 @@ Thumbs.db
|
|||||||
# Temporary and editor files
|
# Temporary and editor files
|
||||||
*.swp
|
*.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/`.
|
- 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.
|
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.
|
||||||
---
|
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ scoped `AGENTS.md` files before making changes.
|
|||||||
|
|
||||||
## Authoritative project documents, in priority order:
|
## 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 Exhibit Contract Specification v5.2
|
||||||
- XZBT-NGN Exhibit Engine Implementation Plan v5.2
|
- XZBT-NGN Exhibit Engine Implementation Plan v5.2
|
||||||
- XZBT Exhibit Authoring Guide v5.2
|
- XZBT Exhibit Authoring Guide v5.2
|
||||||
|
|||||||
@@ -1,24 +1,41 @@
|
|||||||
# XZBT-NGN Exhibit Engine
|
# XZBT-NGN Exhibit Engine
|
||||||
|
|
||||||
A minimal, single-exhibit engineering host for XZBT Exhibit Contract 5.2.
|
A single-exhibit host for XZBT Exhibit Contract 5.2. It negotiates sessions,
|
||||||
It discovers controls from `describe`, tracks the exhibit's reported state,
|
discovers descriptor-driven controls, inspects reported state, sends set/invoke
|
||||||
and exposes set/invoke operations, events, sessions, and protocol errors.
|
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
|
## 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
|
```powershell
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
Open `http://127.0.0.1:4173`. Enter a same-origin exhibit path and select
|
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
|
**Load exhibit**. The host sends `hello`, `describe`, and `state.get` and renders
|
||||||
`state.get`, then renders the discovered catalog. **Reconnect** establishes
|
the discovered controls. Enter another path and select **Load exhibit** to switch.
|
||||||
a new session without reloading the exhibit; **Refresh state** requests a
|
|
||||||
new authoritative snapshot. **Disconnect** removes the exhibit frame.
|
|
||||||
|
|
||||||
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
|
```text
|
||||||
/test-fixtures/reference-exhibits/aquarium/index.html
|
/test-fixtures/reference-exhibits/aquarium/index.html
|
||||||
@@ -26,12 +43,13 @@ Included test fixture paths:
|
|||||||
/test-fixtures/reference-exhibits/haunted-house/index.html
|
/test-fixtures/reference-exhibits/haunted-house/index.html
|
||||||
```
|
```
|
||||||
|
|
||||||
These copied fixtures have documented compatibility corrections; see
|
These are locally corrected test/reference fixtures, not NGN product code or
|
||||||
[fixture provenance](test-fixtures/PROVENANCE.md). They are not NGN product
|
pristine upstream conformance evidence. See [provenance](test-fixtures/PROVENANCE.md).
|
||||||
logic. Additional trusted HTML exhibits can be placed in a directory under
|
Place additional trusted HTML exhibits and their relative assets under `public/`
|
||||||
`public/` and loaded by their same-origin URL. The server exposes only
|
and enter their served URL. A filesystem path or a different server origin cannot
|
||||||
`public/`, `src/`, and `test-fixtures/`, and binds to `127.0.0.1`.
|
be attached directly. The server exposes only `public/`, `src/`, and
|
||||||
Set the `PORT` environment variable to change the default port.
|
`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
|
## Verification
|
||||||
|
|
||||||
@@ -41,240 +59,53 @@ python devlog_editor.py --validate
|
|||||||
git diff --check
|
git diff --check
|
||||||
```
|
```
|
||||||
|
|
||||||
With the server running, open
|
Run the browser suite at `/test-fixtures/host-verification.html`; its timestamped
|
||||||
`http://127.0.0.1:4173/test-fixtures/host-verification.html` and select
|
results appear on the page and in the browser console. The normal operator UI
|
||||||
**Run verification** for repeatable real-browser transport tests against all
|
checks and external SciFi-XZBT preparation are documented in
|
||||||
three exhibits. Manual admin UI and native exhibit gesture checks are recorded
|
[Step 5 verification](test-fixtures/STEP5-VERIFICATION.md). Saved browser evidence
|
||||||
in [the Step 4 report](STEP4-MVP-REPORT.md).
|
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,
|
This repository is the canonical home for the shared
|
||||||
synchronization, and control operations.
|
[XZBT Exhibit Contract Specification 5.2](docs/contract/XZBT-Exhibit-Contract-Specification-v5.2.md)
|
||||||
- `src/validation.js`: descriptor, value, and clarified argument validation.
|
and [XZBT Exhibit Authoring Guide 5.2](docs/authoring/XZBT-Exhibit-Authoring-Guide-v5.2-Step3.7.md).
|
||||||
- `src/transport/post-message.js`: same-origin frame transport only.
|
The [NGN Implementation Plan 5.2](docs/architecture/XZBT-NGN-Exhibit-Engine-Implementation-Plan-v5.2.md)
|
||||||
- `src/ui.js` and `public/`: descriptor-driven engineering interface.
|
also belongs here. Its historical phase numbering describes the longer roadmap;
|
||||||
- `server/serve.js`: loopback static server with explicit served directories.
|
the current Step 5 completion pass does not authorize those later features.
|
||||||
- `tests/`: focused automated host and server tests.
|
SciFi-XZBT is an external exhibit implementation, not the NGN architecture.
|
||||||
- `test-fixtures/`: copied reference exhibits and real-browser verification.
|
`docs/reference/` contains external implementation context, not host requirements.
|
||||||
|
|
||||||
The state display is a cache of exhibit reports. Events update reported values;
|
## Implementation and MVP boundary
|
||||||
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.
|
|
||||||
|
|
||||||
## 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
|
The MVP supports one exhibit, Contract 5.2 discovery, generic target rendering,
|
||||||
[Labyricorn](https://www.labyricorn.com).
|
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
|
Not implemented: multi-surface presentation, casting/display endpoints, MIDI,
|
||||||
guidance files (`AGENTS.md`), devlog structure, and the standalone
|
MCP/webhooks, scenarios/recording, telemetry acquisition, multi-exhibit
|
||||||
`devlog_editor.py` management utility for local validation and GUI editing.
|
orchestration, persisted operator configurations, remote deployment, accounts,
|
||||||
|
databases, cloud services, plugins, commercial licensing or production branding.
|
||||||
|
|
||||||
---
|
## Publishing records
|
||||||
|
|
||||||
## Overview
|
`.labyricorn/` holds this project's Lektor exhibition and development records.
|
||||||
|
See [publishing guidance](.labyricorn/README.md) and scoped `AGENTS.md` files.
|
||||||
Projects published on Labyricorn maintain their own exhibition pages and
|
`python devlog_editor.py` opens the standalone publishing editor;
|
||||||
development logs directly inside their Git repositories under the `.labyricorn/`
|
`python devlog_editor.py --validate` validates records without opening the GUI.
|
||||||
directory. The Labyricorn site importer periodically synchronizes these records
|
Application code lives outside `.labyricorn/`.
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This template is released into the public domain under the [Unlicense](LICENSE).
|
See [LICENSE](LICENSE).
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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**
|
||||||
@@ -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
@@ -2,19 +2,28 @@
|
|||||||
<html lang="en">
|
<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>
|
<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>
|
<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>
|
<main>
|
||||||
<section><h2>Connection</h2>
|
<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>
|
<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 id="status" role="status">Disconnected</p><button id="refresh" disabled>Refresh state</button>
|
<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>Only load trusted local exhibits. Required capabilities in available/loading states may accept requests through an exhibit fallback; the exhibit decides.</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>
|
<div id="frame-container"></div>
|
||||||
</section>
|
</section>
|
||||||
<section><h2>Session and exhibit</h2><pre id="metadata">No negotiated session.</pre><h3>Capabilities</h3><pre id="capabilities">[]</pre></section>
|
<section><h2>Exhibit controls <span id="target-count"></span></h2>
|
||||||
<section><h2>Discovered targets <span id="target-count"></span></h2><div id="catalog">Connect to discover the catalog.</div></section>
|
<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>
|
||||||
<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>
|
<p>Reported values are authoritative. New values are drafts; select Set or Invoke to submit. Rediscovery resets drafts.</p>
|
||||||
<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>
|
<div id="catalog">Connect an exhibit to discover its controls.</div>
|
||||||
<section><h2>Protocol and errors</h2><pre id="errors" role="log">[]</pre></section>
|
</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>
|
</main>
|
||||||
<script type="module" src="/src/ui.js"></script>
|
<script type="module" src="/src/ui.js"></script>
|
||||||
</body></html>
|
</body></html>
|
||||||
|
|||||||
@@ -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; }
|
.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; }
|
.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; }
|
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%; } }
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,21 @@
|
|||||||
import { ExhibitHost } from './host.js';
|
import { ExhibitHost } from './host.js';
|
||||||
import { argumentSchema } from './validation.js';
|
import { argumentSchema } from './validation.js';
|
||||||
import { postMessageTransport } from './transport/post-message.js';
|
import { postMessageTransport } from './transport/post-message.js';
|
||||||
|
import { ExhibitConnection } from './connection.js';
|
||||||
|
|
||||||
const host = new ExhibitHost();
|
const host = new ExhibitHost();
|
||||||
const byId = id => document.getElementById(id);
|
const byId = id => document.getElementById(id);
|
||||||
const json = value => JSON.stringify(value, null, 2);
|
const json = value => JSON.stringify(value, null, 2);
|
||||||
const rows = new Map();
|
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) {
|
function element(tag, text, parent) {
|
||||||
const node = document.createElement(tag); if (text !== undefined) node.textContent = text;
|
const node = document.createElement(tag); if (text !== undefined) node.textContent = text;
|
||||||
parent?.append(node); return node;
|
parent?.append(node); return node;
|
||||||
@@ -38,8 +47,15 @@ function editor(spec, label, parent) {
|
|||||||
}
|
}
|
||||||
function buildCatalog() {
|
function buildCatalog() {
|
||||||
rows.clear(); byId('catalog').replaceChildren();
|
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) {
|
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('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);
|
element('p', `${target.kind} · ${target.readable ? 'readable' : 'not readable'} · ${target.writable ? 'writable' : 'not writable'}`, section);
|
||||||
const current = element('output', '', section);
|
const current = element('output', '', section);
|
||||||
@@ -85,16 +101,27 @@ function buildCatalog() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function render() {
|
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,
|
byId('metadata').textContent = json({ sessionId: host.sessionId, contract: host.contract, exhibit: host.exhibit,
|
||||||
registryRevision: host.registryRevision, stateRevision: host.stateRevision, sequence: host.sequence });
|
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('state').textContent = json(Object.fromEntries(host.values));
|
||||||
byId('events').textContent = json(host.eventLog);
|
byId('events').textContent = json(host.eventLog);
|
||||||
byId('errors').textContent = json(host.logs);
|
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('target-count').textContent = `(${host.catalog.length})`;
|
||||||
byId('refresh').disabled = host.status !== 'connected';
|
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(); }
|
if (host.catalog !== renderedCatalog) { renderedCatalog = host.catalog; buildCatalog(); }
|
||||||
for (const row of rows.values()) {
|
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.';
|
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;
|
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 => {
|
byId('connection').addEventListener('submit', event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
try {
|
try {
|
||||||
const url = new URL(byId('exhibit-url').value, location.href);
|
connection.load(byId('exhibit-url').value);
|
||||||
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();
|
|
||||||
} catch (error) { host.report(error); }
|
} catch (error) { host.report(error); }
|
||||||
});
|
});
|
||||||
byId('reconnect').addEventListener('click', attach);
|
byId('reconnect').addEventListener('click', () => connection.reconnect());
|
||||||
byId('disconnect').addEventListener('click', () => { host.disconnect(); frame?.remove(); frame = null; render(); });
|
byId('disconnect').addEventListener('click', () => connection.disconnect());
|
||||||
byId('refresh').addEventListener('click', () => host.refresh().catch(error => host.report(error)));
|
byId('refresh').addEventListener('click', () => host.refresh().catch(error => host.report(error)));
|
||||||
|
byId('clear-errors').addEventListener('click', () => { host.logs = []; render(); });
|
||||||
host.subscribe(render); render();
|
host.subscribe(render); render();
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ export function validateCatalog(message) {
|
|||||||
&& typeof t.readable === 'boolean' && typeof t.writable === 'boolean'
|
&& typeof t.readable === 'boolean' && typeof t.writable === 'boolean'
|
||||||
&& Array.isArray(t.requires) && t.requires.every(id => typeof id === 'string'), 'Invalid target descriptor.');
|
&& Array.isArray(t.requires) && t.requires.every(id => typeof id === 'string'), 'Invalid target descriptor.');
|
||||||
ids.add(t.id);
|
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
|
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.');
|
&& Number.isFinite(t.step) && t.step > 0, 'Invalid range descriptor.');
|
||||||
if (t.kind === 'selection') check(Array.isArray(t.options)
|
if (t.kind === 'selection') check(Array.isArray(t.options)
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ Local compatibility corrections:
|
|||||||
- Planetarium's magnitude reader and native dispatch still used the old
|
- Planetarium's magnitude reader and native dispatch still used the old
|
||||||
camelCase ID. They now match its existing canonical `sky.magnitude-limit`
|
camelCase ID. They now match its existing canonical `sky.magnitude-limit`
|
||||||
descriptor, fixing a null snapshot value and failed native writes.
|
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
|
NGN has no fixture-specific target logic. These corrections apply only to
|
||||||
the local fixture copies. No upstream fixes or independent conformance
|
the local fixture copies. No upstream fixes or independent conformance
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
@@ -19,7 +19,7 @@ async function raw(transport, message) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
run.addEventListener('click', async () => {
|
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;
|
let passed = 0;
|
||||||
for (const name of ['aquarium', 'planetarium', 'haunted-house']) {
|
for (const name of ['aquarium', 'planetarium', 'haunted-house']) {
|
||||||
const host = new ExhibitHost();
|
const host = new ExhibitHost();
|
||||||
@@ -74,5 +74,6 @@ run.addEventListener('click', async () => {
|
|||||||
} catch (error) { results.textContent += `FAIL ${name}: ${error.message}\n`; }
|
} catch (error) { results.textContent += `FAIL ${name}: ${error.message}\n`; }
|
||||||
finally { host.disconnect(); frame.remove(); }
|
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) {
|
switch (targetId) {
|
||||||
case 'room.current': return h.room;
|
case 'room.current': return h.room;
|
||||||
case 'room.spirit': return h.spirit;
|
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.fog-density': return h.fogDensity;
|
||||||
case 'room.candlelight': return h.candlelight;
|
case 'room.candlelight': return h.candlelight;
|
||||||
case 'room.audio-enabled': return h.audioEnabled;
|
case 'room.audio-enabled': return h.audioEnabled;
|
||||||
|
|||||||
@@ -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' });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user