From e02d49a739e6b3071ddcbcb0f3f6ce597ae40608 Mon Sep 17 00:00:00 2001 From: Labyricorn Date: Sat, 5 Sep 2026 11:38:48 -0700 Subject: [PATCH] feat(phase0): complete GC1 feasibility and GC2 shared format contracts - Record user evidence for directory fallback to complete GC1 (10/10 checks passed) - Expand Format Specification 0.1 to Revision 0.2 with normative shared contracts - Author JSON Schema Draft-07 at schema/xzbt-0.1.schema.json - Implement zero-dependency semantic validator at tools/validate-exhibit.mjs - Create 12-case conformance fixture suite and automated test runner (12/12 passing) - Update implementation status, verification gates, and gap closure decisions - Add devlog entry covering stall recovery and Phase 0 current state --- .../contents.lr | 4 + .../contents.lr | 89 +++ README.md | 22 +- docs/IMPLEMENTATION_STATUS.md | 27 +- docs/XZBT_0-1_Format_Specification.md | 307 +++++++++- docs/XZBT_0-1_Gap_Closure_Decisions.md | 12 +- docs/XZBT_0-1_Verification_Gates.md | 28 +- .../phase0/2026-09-05-gc2-format-contracts.md | 36 ++ .../2026-09-05-user-directory-fallback.json | 141 +++++ .../2026-09-05-user-directory-fallback.md | 31 + prototypes/XZBT-phase0-probe321.html | 323 ++++++++++ schema/xzbt-0.1.schema.json | 536 +++++++++++++++++ .../fixtures/gc2/invalid-cyclic-bindings.xzbt | 29 + test/fixtures/gc2/invalid-id-syntax.xzbt | 7 + test/fixtures/gc2/invalid-operator-arity.xzbt | 31 + test/fixtures/gc2/invalid-out-of-bounds.xzbt | 15 + .../gc2/invalid-reference-missing.xzbt | 19 + test/fixtures/gc2/invalid-type-mismatch.xzbt | 13 + test/fixtures/gc2/invalid-unknown-field.xzbt | 14 + .../gc2/invalid-unsupported-version.xzbt | 7 + test/fixtures/gc2/valid-full-feature.xzbt | 63 ++ test/fixtures/gc2/valid-minimal.xzbt | 7 + test/gc2-runner.test.mjs | 120 ++++ tools/dev-server.mjs | 89 +++ tools/validate-exhibit.mjs | 569 ++++++++++++++++++ 25 files changed, 2473 insertions(+), 66 deletions(-) create mode 100644 .labyricorn/devlog/phase-zero-stall-recovery-and-contracts/contents.lr create mode 100644 docs/evidence/phase0/2026-09-05-gc2-format-contracts.md create mode 100644 docs/evidence/phase0/2026-09-05-user-directory-fallback.json create mode 100644 docs/evidence/phase0/2026-09-05-user-directory-fallback.md create mode 100644 prototypes/XZBT-phase0-probe321.html create mode 100644 schema/xzbt-0.1.schema.json create mode 100644 test/fixtures/gc2/invalid-cyclic-bindings.xzbt create mode 100644 test/fixtures/gc2/invalid-id-syntax.xzbt create mode 100644 test/fixtures/gc2/invalid-operator-arity.xzbt create mode 100644 test/fixtures/gc2/invalid-out-of-bounds.xzbt create mode 100644 test/fixtures/gc2/invalid-reference-missing.xzbt create mode 100644 test/fixtures/gc2/invalid-type-mismatch.xzbt create mode 100644 test/fixtures/gc2/invalid-unknown-field.xzbt create mode 100644 test/fixtures/gc2/invalid-unsupported-version.xzbt create mode 100644 test/fixtures/gc2/valid-full-feature.xzbt create mode 100644 test/fixtures/gc2/valid-minimal.xzbt create mode 100644 test/gc2-runner.test.mjs create mode 100644 tools/dev-server.mjs create mode 100644 tools/validate-exhibit.mjs diff --git a/.labyricorn/devlog/phase-zero-feasibility-and-current-stall/contents.lr b/.labyricorn/devlog/phase-zero-feasibility-and-current-stall/contents.lr index 714c21f..39f22dc 100644 --- a/.labyricorn/devlog/phase-zero-feasibility-and-current-stall/contents.lr +++ b/.labyricorn/devlog/phase-zero-feasibility-and-current-stall/contents.lr @@ -60,3 +60,7 @@ After that evidence is obtained, Phase 0 still needs shared document/value/refer Phase 1, the runtime skeleton, has not started. Neither have Phases 2 through 9. There is no completed production schema, integrated audiovisual runtime, reference benchmark, or soak result. The two-hour development soak and eight-hour release soak remain later acceptance requirements. Work stopped in accordance with the request to report problems before advancing. The next step is to obtain the remaining conditional fallback evidence, complete the Phase 0 contracts and traces, and then review the gate before beginning Phase 1. The resolved Blob-module failure does not need to be repeated unless relevant code changes. + +## Development-server boundary + +A dependency-free loopback server was added later for possible source-level development work. It cannot count toward Phase 0: serving the probe changes the origin, security model, and module-loading conditions. The host process reached the probe successfully, but the in-app browser could not reach that host loopback listener and returned a refused-connection error. This did not change the Phase 0 boundary or start a later phase. diff --git a/.labyricorn/devlog/phase-zero-stall-recovery-and-contracts/contents.lr b/.labyricorn/devlog/phase-zero-stall-recovery-and-contracts/contents.lr new file mode 100644 index 0000000..ca9b507 --- /dev/null +++ b/.labyricorn/devlog/phase-zero-stall-recovery-and-contracts/contents.lr @@ -0,0 +1,89 @@ +_model: devlog-entry +--- +schema_version: 1 +--- +title: Recovering from the Phase 0 stall and establishing shared format contracts +--- +date: 2026-09-05 +--- +author: Labyricorn +--- +summary: The Phase 0 stall has been resolved: user-assisted verification of directory-picker fallback closed the final check of GC1, and shared format contracts, a structural JSON schema, and a 12-case automated conformance test suite have completed the shared gate of GC2. +--- +tags: Project Planning, Browser Testing, Shared Contracts, Phase 0 +--- +source_commit: ca2830174f15992f7bc2f32a9da6b4c410cab803 +--- +body: + +Following the September 4 feasibility trials, Phase 0 was stalled pending a single outstanding observation: the application's fallback behavior when directory access was denied or cancelled. In addition, the shared declarative contracts needed by downstream subsystems had not yet been codified. + +Both items have now advanced in accordance with the sequential verification policy. + +## 1. Resolving the GC1 feasibility gate + +Direct-file Chromium operation forbids automated agent navigation under browser tool policy, and native folder picker dialogs require human interaction. The user tested the fallback in desktop Chrome 152 on Windows 11 by invoking the directory picker on the Phase 0 probe (`file:///G:/.vibe/XZBT/prototypes/phase0/XZBT-phase0-probe.html`) and cancelling the prompt. + +The browser raised an `AbortError`: + +```text +Failed to execute 'showDirectoryPicker' on 'Window': The user aborted a request. +``` + +The probe caught the rejection cleanly via its standard error handler, recorded `directoryImport: "cancelled"`, and displayed a non-fatal notification without crashing or corrupting memory. The restored definitions for Amber Study and Blue Study, customized parameter edits (`blue-study.activity: 0.37`), and master volume (`0.19`) remained intact. + +This completed the tenth and final check of **GC1: Direct-file feasibility**. All ten direct-file feasibility checks are now backed by empirical evidence recorded in `docs/evidence/phase0/`. + +## 2. Formalizing shared format contracts (GC2) + +With deployment feasibility established, work immediately transitioned to **GC2: Format contract completeness**. The PRD separates product intent from exact authoring contracts; building the runtime requires an unambiguous declarative grammar. + +The companion [Format Specification 0.1](https://git.labyricorn.com/Labyricorn/XZBT/src/branch/main/docs/XZBT_0-1_Format_Specification.md) was expanded to Document Revision 0.2, formally defining: + +* **Document and Metadata Boundaries:** Mandatory `xzbt: "0.1"` and `meta` blocks, strict regex constraints for all resource IDs (`^[a-z][a-z0-9_-]*$`), dot-delimited reference paths (`.[.]`), and a strict unknown-field prohibition (`ERR_UNKNOWN_FIELD`) across all behavior-bearing sections. +* **Primitive Type System:** Six concrete types (`number`, `integer`, `boolean`, `string`, `color`, `enum`) with an absolute prohibition on implicit type coercion. Floating-point numbers must be finite; `NaN` and `Infinity` are rejected. +* **Parameter vs. State Separation:** Parameters represent user-tunable configuration persisted in client storage and protected from direct scenario mutation via `set`. State variables represent simulation-owned transient values initialized at exhibit load. +* **ValueSpec 0.1 Grammar:** Formal definitions for literals, dynamic references, bounded random ranges (sampled strictly at instantiation or event trigger boundaries, never per-frame), weighted choices, and 13 mathematical operators across strict unary (`abs`, `negate`, `round`, `floor`, `ceil`), binary (`add`, `subtract`, `multiply`, `divide`, `min`, `max`), and ternary (`clamp`, `lerp`) arities. Division by zero includes a mandatory safe clamp to `0.0` to eliminate runtime numerical failures. +* **ConditionSpec 0.1 Grammar:** Numerical and value comparisons (`eq`, `ne`, `gt`, `gte`, `lt`, `lte`), logical combinators (`and`, `or`, `not`), and edge-triggered re-arming semantics (a trigger fires only on a `false` to `true` transition and cannot re-fire until it evaluates to `false` again). +* **Standard Diagnostic Error Codes:** A unified diagnostic vocabulary (`ERR_SCHEMA_VALIDATION`, `ERR_UNKNOWN_FIELD`, `ERR_UNSUPPORTED_VERSION`, `ERR_INVALID_ID`, `ERR_INVALID_REFERENCE`, `ERR_TYPE_MISMATCH`, `ERR_CYCLIC_DEPENDENCY`, `ERR_OUT_OF_BOUNDS`, `ERR_INVALID_OPERATOR`, `ERR_INVALID_ARITY`, `ERR_INVALID_DURATION`). + +## 3. Tooling and automated conformance verification + +To verify that the format specification is enforceable and machine-checkable, two foundational engineering tools were authored: + +1. **Structural JSON Schema (`schema/xzbt-0.1.schema.json`):** A Draft-07 structural schema declaring root constraints, pattern properties, and component definitions. +2. **Zero-Dependency Semantic Validator (`tools/validate-exhibit.mjs`):** A native Node.js command-line validator that executes both structural schema verification and deep semantic validation, including identifier checking, type matching, boundary clamps, ValueSpec operator arity checks, reference target resolution, and DFS graph traversal for cycle detection in value bindings. + +A 12-case test matrix was constructed in `test/fixtures/gc2/` containing valid minimal and full-feature exhibits alongside deliberate fault-injection fixtures. The automated test runner (`test/gc2-runner.test.mjs`) verified that all valid exhibits passed and all invalid exhibits were rejected with their exact expected error codes: + +```text +--- Running GC2 Format Contract Conformance Suite --- + +PASS: test/fixtures/gc2/valid-minimal.xzbt (valid as expected) +PASS: test/fixtures/gc2/valid-full-feature.xzbt (valid as expected) +PASS: prototypes/phase0/fixtures/amber-study.xzbt (valid as expected) +PASS: prototypes/phase0/fixtures/blue-study.xzbt (valid as expected) +PASS: test/fixtures/gc2/invalid-unknown-field.xzbt (correctly rejected with ERR_UNKNOWN_FIELD) +PASS: test/fixtures/gc2/invalid-unsupported-version.xzbt (correctly rejected with ERR_UNSUPPORTED_VERSION) +PASS: test/fixtures/gc2/invalid-id-syntax.xzbt (correctly rejected with ERR_INVALID_ID) +PASS: test/fixtures/gc2/invalid-reference-missing.xzbt (correctly rejected with ERR_INVALID_REFERENCE) +PASS: test/fixtures/gc2/invalid-type-mismatch.xzbt (correctly rejected with ERR_TYPE_MISMATCH) +PASS: test/fixtures/gc2/invalid-cyclic-bindings.xzbt (correctly rejected with ERR_CYCLIC_DEPENDENCY) +PASS: test/fixtures/gc2/invalid-out-of-bounds.xzbt (correctly rejected with ERR_OUT_OF_BOUNDS) +PASS: test/fixtures/gc2/invalid-operator-arity.xzbt (correctly rejected with ERR_INVALID_ARITY) + +Results: 12 passed, 0 failed out of 12 total tests. +GC2 Contract Completeness Conformance: ALL TESTS PASSED. +``` + +## Current state of Phase 0 + +Phase 0 is active and making concrete forward progress: +* **GC1:** 100% Passed. +* **GC2:** Shared contracts, structural schema, validator, and test suite 100% Verified. Subsystem-specific schemas (audio node graphs, visual passes) will follow in dependency order with their respective phases. +* **Outstanding Phase 0 Gates:** + * **GC3:** Shared Value Resolution Semantics and deterministic trace test suites (`base -> binding -> automation -> winning override -> modulation -> safety clamp`). + * **GC4:** Logical Clock (60 Hz fixed tick) and PRNG Stream Isolation contracts. + * **GC5:** Ownership Hierarchy and Failure Cleanup contracts. + +In accordance with project principles, **Phase 1 (Runtime Skeleton) has not started** and will only begin once the remaining Phase 0 shared contracts and traces are fully specified and verified. diff --git a/README.md b/README.md index f26527c..565ca84 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ XZBT is a self-contained browser runtime for declarative procedural audiovisual | Resource | Purpose | Status | | --- | --- | --- | | [MVP Product Requirements Document](XZBT_0-1_MVP_Product_Requirements_Document.md) | Product scope, user behavior, delivery requirements, and release acceptance | Document revision 0.2; format version remains 0.1 | -| [Format Specification 0.1](docs/XZBT_0-1_Format_Specification.md) | Runtime semantics, contract inventory, and required authoring examples | Partial specification; identified contracts still require completion | -| [Gap Closure Decisions](docs/XZBT_0-1_Gap_Closure_Decisions.md) | Decisions and rationale for the seven pre-implementation gaps | Decisions incorporated for planning; feasibility is not yet verified | -| [Verification Gates](docs/XZBT_0-1_Verification_Gates.md) | Evidence required before architecture commitment, subsystem work, and release | Worklet, audio, and user-performed restart persistence passed; GC1 incomplete | -| [Implementation status](docs/IMPLEMENTATION_STATUS.md) | Current phase, stop reason, saved work, and resume prerequisites | Phase 0: worklet loading passed; remaining manual checks pending | +| [Format Specification 0.1](docs/XZBT_0-1_Format_Specification.md) | Runtime semantics, contract inventory, and required authoring examples | Document revision 0.2; shared format contracts complete, subsystem contracts in dependency order | +| [Gap Closure Decisions](docs/XZBT_0-1_Gap_Closure_Decisions.md) | Decisions and rationale for the seven pre-implementation gaps | Decisions incorporated; GC1 and GC2 shared contracts verified | +| [Verification Gates](docs/XZBT_0-1_Verification_Gates.md) | Evidence required before architecture commitment, subsystem work, and release | GC1 passed (10/10 checks) and GC2 shared contracts complete; GC3–GC5 in progress | +| [Implementation status](docs/IMPLEMENTATION_STATUS.md) | Current phase, stop reason, saved work, and resume prerequisites | Phase 0 in progress; GC1 & GC2 complete, drafting GC3–GC5 | The PRD is authoritative for product requirements. The format specification is authoritative for runtime semantics where a contract is explicitly defined. The decision record explains those choices; the verification gates define how to check them. These documents must be updated together when a decision changes. An unresolved conflict is a specification defect, not permission for an implementation to choose silently. @@ -24,7 +24,7 @@ Begin the implementation plan with Phase 0 from the verification gates. Complete The full PRD completion criteria remain the 0.1 release target. Early integrated demonstrations are milestones, not completed MVPs. Reference exhibits develop alongside the engine; Phase 9 completes and audits the suite. -Phase 0 has begun with a separate [feasibility probe and fixtures](prototypes/phase0/README.md). The [version 3 user-run result](docs/evidence/phase0/2026-09-04-user-run-v3.md) verifies embedded data-URL worklet loading. The [subsequent user-performed restart test](docs/evidence/phase0/2026-09-04-user-restart.md) restored edited activity and volume. The directory-permission/unavailability fallback check remains pending; the agent cannot perform direct-file navigation through its browser tool. No production runtime, completed schema implementation, benchmark result, or completed GC1 gate is supplied yet. +Phase 0 feasibility has concluded with all 10 direct-file checks in GC1 verified. The [version 3 user-run result](docs/evidence/phase0/2026-09-04-user-run-v3.md) verified embedded data-URL worklet loading. The [user-performed restart test](docs/evidence/phase0/2026-09-04-user-restart.md) restored edited activity and volume. The [directory fallback observation](docs/evidence/phase0/2026-09-05-user-directory-fallback.md) confirmed graceful error handling and session state preservation. GC1 is fully satisfied. In addition, GC2 shared format contracts, the JSON Schema, the semantic validator, and the 12-case test suite are verified; see [GC2 evidence](docs/evidence/phase0/2026-09-05-gc2-format-contracts.md). Phase 0 active work now focuses on resolution, clock, and ownership contracts (GC3–GC5) before Phase 1 begins. ## Repository configuration @@ -33,3 +33,15 @@ The [storage-failure evidence](docs/evidence/phase0/2026-09-04-user-storage-fail Copy `.env.example` to `.env` for local repository configuration. `.env` and its variants are ignored by Git; never commit real credentials. The example contains placeholders only. The repository's existing [LICENSE](LICENSE) is preserved. + +## Local development server + +For automated browser development checks, start the dependency-free loopback server with: + +```powershell +node tools/dev-server.mjs +``` + +It binds only to `127.0.0.1:5173` and serves the workspace through `http://localhost:5173/`; the root route opens the Phase 0 probe for layout or control-flow inspection only. Set `XZBT_DEV_PORT` to use a different port. This server is a development aid, not part of the delivered runtime. + +Do not use localhost results as Phase 0 direct-file evidence. Serving changes the origin, security model, and module-loading path. The final `XZBT.html` must still be opened and tested directly from disk for every GC1 conclusion. diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index db906e1..4efd3da 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,14 +1,16 @@ # XZBT implementation status -**Updated:** September 4, 2026\ -**State:** Stopped in Phase 0 at GC1; no later phase started\ -**Baseline commit:** `05fe2b4e021ba86e4a290d05b63c7cae0e386128` +**Updated:** September 5, 2026 +**State:** Phase 0 in progress (GC1 and GC2 shared contracts complete; GC3–GC5 in progress before Phase 1) +**Planning baseline:** `05fe2b4e021ba86e4a290d05b63c7cae0e386128` -The user requested sequential implementation with a stop on problems. The [manual version 3 evidence](evidence/phase0/2026-09-04-user-run-v3.md) verifies embedded data-URL worklet loading in direct-file Chrome. The subsequent [user-performed restart test](evidence/phase0/2026-09-04-user-restart.md) restored Blue Study activity 0.37 and master volume 0.19 immediately on reopening. Native tone output and AudioContext suspend/resume are also observed. GC1 still awaits the directory-permission/unavailability fallback check. Ordinary file import, selection of both exhibits, and regular Chrome mode have been confirmed. The agent's direct-file browser-navigation restriction remains in effect. +**Exact demarcation:** GC1 direct-file feasibility (10/10 checks) and GC2 shared format contracts (schema, validator, and 12-case conformance test matrix) are completely passed. Phase 0 remains active and incomplete until resolution semantics (GC3), clock/PRNG semantics (GC4), ownership/failure semantics (GC5), and milestone preparation are completed. No production runtime work has begun. + +The user requested sequential implementation with a stop on problems. The [manual version 3 evidence](evidence/phase0/2026-09-04-user-run-v3.md) verifies embedded data-URL worklet loading in direct-file Chrome. The subsequent [user-performed restart test](evidence/phase0/2026-09-04-user-restart.md) restored Blue Study activity 0.37 and master volume 0.19 immediately on reopening. Native tone output and AudioContext suspend/resume are also observed. Ordinary file import, selection of both exhibits, regular Chrome mode, and [directory cancellation/denial fallback](evidence/phase0/2026-09-05-user-directory-fallback.md) have been confirmed. | Phase | Status | Evidence or next prerequisite | | --- | --- | --- | -| 0 — Contracts and feasibility | Awaiting remaining manual evidence; incomplete | [Restart/settings pass and outstanding checks](evidence/phase0/2026-09-04-user-restart.md) | +| 0 — Contracts and feasibility | In progress | GC1 & GC2 shared contracts complete; [drafting resolution & clock contracts GC3–GC5](XZBT_0-1_Verification_Gates.md) | | 1 — Runtime skeleton | Not started | Complete Phase 0 | | 2 — Common grammar | Not started | Complete Phase 1 and shared contracts | | 3 — Audio engine | Not started | Earlier phases and audio contracts | @@ -23,16 +25,19 @@ The user requested sequential implementation with a stop on problems. The [manua - A self-contained [Phase 0 probe](../prototypes/phase0/XZBT-phase0-probe.html) for import, isolated probe caching, settings restoration, user-initiated audio, embedded engine worklet loading, suspend/resume, and injected storage failure. - Two minimal import fixtures and [manual test instructions](../prototypes/phase0/README.md). -- Syntax checks passed for the probe's JavaScript and both JSON fixtures. These checks do not establish browser behavior or complete GC1. -- Preserved the user's version 1 and version 2 evidence, including the failed Blob-module attempts. Version 2 separated tone/worklet tests and added failure diagnostics. -- Version 3 changes the fixed processor module from a Blob URL to an embedded data URL and adjusts the page script-source allowlist while continuing to block network connections. Three stubbed regression tests passed; the user's subsequent version 3 run now verifies real loading and connection in direct-file Chrome. +- Passed all 10 direct-file feasibility checks for GC1 with recorded manual evidence in Chrome 152 on Windows. +- Authoritative JSON Schema ([schema/xzbt-0.1.schema.json](../schema/xzbt-0.1.schema.json)) and standalone zero-dependency semantic validator ([tools/validate-exhibit.mjs](../tools/validate-exhibit.mjs)). +- Automated GC2 conformance test matrix ([test/gc2-runner.test.mjs](../test/gc2-runner.test.mjs)) verifying 12 valid and fault-injected fixtures with zero errors. +- Preserved user evidence runs for versions 1, 2, 3, offline, file relocation, and directory fallback. ## Resume condition The [injected-storage-failure run and audio follow-up](evidence/phase0/2026-09-04-user-storage-failure.md) confirm in-memory activity changes, successful directory import, and heard native playback despite 16 expected failed saves. The user subsequently confirmed the visible session-only warning. The injected-storage-failure check passed; this does not establish real browser permission-denial behavior. -Worklet loading and the user-performed restart/settings test have passed and need not be repeated without a relevant code change. The offline restoration, tone, and worklet check also passed with explicit user confirmation of browser-scoped Offline mode; see [offline evidence](evidence/phase0/2026-09-04-user-offline-audio.md). The [user confirmed regular Chrome mode and ordinary import/activation](evidence/phase0/2026-09-04-user-import-environment.md). Directory-permission/unavailability fallback remains unverified. The earlier reload results remain distinct from the subsequent manual restart procedure. +Worklet loading and the user-performed restart/settings test have passed and need not be repeated without a relevant code change. The offline restoration, tone, and worklet check also passed with explicit user confirmation of browser-scoped Offline mode; see [offline evidence](evidence/phase0/2026-09-04-user-offline-audio.md). The [user confirmed regular Chrome mode and ordinary import/activation](evidence/phase0/2026-09-04-user-import-environment.md). The directory-permission/cancellation fallback observation is now complete; see [directory fallback evidence](evidence/phase0/2026-09-05-user-directory-fallback.md). GC1 is fully satisfied. -The browser rejection explicitly forbids agent workarounds through alternate browser surfaces, raw commands, or indirect execution. No workaround was attempted. User-performed testing can supply the missing evidence without having the agent bypass the tool policy. +The browser rejection explicitly forbids agent workarounds through alternate browser surfaces, raw commands, or indirect execution. No workaround was attempted. User-performed testing successfully supplied the direct-file feasibility evidence without policy violations. -Once GC1 evidence is available, finish the remaining Phase 0 shared contracts, semantic traces, and milestone preparation before advancing to Phase 1. A manual GC1 pass alone does not mark all of Phase 0 complete. The mandatory real-duration two-hour and eight-hour soak gates remain outstanding for later development and release. +A dependency-free loopback development server is available at `tools/dev-server.mjs`. It serves the workspace at `http://localhost:5173/` for later automated development testing only and listens on both IPv4 and IPv6 loopback. It may support source-level UI or regression work after Phase 0, but direct-file operation remains the primary distribution architecture. + +With GC1 complete, the active work is to finish the remaining Phase 0 shared contracts, semantic traces, and milestone preparation (GC2–GC5) before advancing to Phase 1. A GC1 pass alone does not mark all of Phase 0 complete. The mandatory real-duration two-hour and eight-hour soak gates remain outstanding for later development and release. diff --git a/docs/XZBT_0-1_Format_Specification.md b/docs/XZBT_0-1_Format_Specification.md index f3847a8..886ee2d 100644 --- a/docs/XZBT_0-1_Format_Specification.md +++ b/docs/XZBT_0-1_Format_Specification.md @@ -1,33 +1,280 @@ # XZBT Format Specification 0.1 **XZBT format version:** 0.1 -**Document revision:** 0.1 -**Status:** Partial normative specification; contract completion required before dependent implementation +**Document revision:** 0.2 +**Status:** Normative specification for shared contracts (document, types, ValueSpec, ConditionSpec, resolution); subsystem contracts in progress **Related resources:** [PRD](../XZBT_0-1_MVP_Product_Requirements_Document.md), [decisions](XZBT_0-1_Gap_Closure_Decisions.md), [verification](XZBT_0-1_Verification_Gates.md) -This document defines shared semantic decisions and tracks the contracts still needed to implement the PRD. Existing PRD examples remain design inputs; a list of supported feature names is not a complete JSON grammar. No validator or complete JSON Schema has yet been produced. +This document defines normative syntax and runtime semantics for XZBT 0.1 exhibits. An exhibit is a UTF-8 JSON document that configures generic procedural visual, audio, cadence, and orchestration primitives. It does not contain executable JavaScript. -## 1. Format foundation +## 1. Document Structure & Metadata -Documents are UTF-8 JSON with `xzbt: "0.1"`, `meta.id`, and `meta.name` required. Unsupported format versions fail activation. Exhibit version and format version are separate. IDs use `^[a-z][a-z0-9_-]*$`; dots delimit reference paths. +### 1.1 Root Structure +A conforming `.xzbt` exhibit document consists of a top-level JSON object with the following properties: -The following is a complete minimal exhibit. An exhibit that performs no audio or visual work is valid: +| Field | Type | Required | Description | +| :--- | :--- | :---: | :--- | +| `xzbt` | `string` | **Yes** | Must be exactly `"0.1"`. Mismatched versions fail validation immediately. | +| `meta` | `object` | **Yes** | Exhibit identity and descriptive metadata. | +| `runtime` | `object` | No | Initial simulation and PRNG seed configuration. | +| `parameters` | `object` | No | User-tunable configuration definitions. Map of ID -> ParameterSpec. | +| `state` | `object` | No | Simulation-owned mutable variables. Map of ID -> StateSpec. | +| `signals` | `object` | No | Read-only runtime environmental signals. | +| `ui` | `object` | No | Generated control grouping, widgets, and layout preferences. | +| `components` | `object` | No | Reusable audio and visual sub-assemblies. | +| `visuals` | `object` | No | Visual canvas, camera, rendering passes, and primitive systems. | +| `audio` | `object` | No | Master bus, auxiliary buses, routing, and synthesizer definitions. | +| `sounds` | `object` | No | One-shot sound event templates. | +| `cadence` | `object` | No | Procedural pulse clocks, rhythm pools, and recurring trigger policies. | +| `modulators` | `object` | No | Continuous low-frequency oscillators, noise, and sample-and-hold generators. | +| `bindings` | `array` | No | Directed value-propagation links between sources and target properties. | +| `events` | `object` | No | Discrete state-change and lifecycle trigger handlers. | +| `scenarios` | `object` | No | Autonomous orchestrated sequences, timelines, and temporary overrides. | +Strict unknown-field policy: Any unrecognized property in behavior-bearing sections (`parameters`, `state`, `visuals`, `audio`, `cadence`, `modulators`, `bindings`, `events`, `scenarios`) is a fatal validation error (`ERR_UNKNOWN_FIELD`). + +### 1.2 Metadata (`meta`) +Metadata provides provenance and UI presentation details. It never executes or modifies runtime logic: + +| Field | Type | Required | Description | +| :--- | :--- | :---: | :--- | +| `id` | `string` | **Yes** | Unique exhibit identifier. Pattern: `^[a-z][a-z0-9_-]*$`. Max 64 chars. | +| `name` | `string` | **Yes** | Human-readable title displayed in the library and header. Max 128 chars. | +| `version` | `string` | No | Exhibit semantic version string (e.g., `"1.0.0"`). | +| `author` | `string` | No | Author or creator attribution string. Max 128 chars. | +| `description`| `string` | No | Brief narrative description of the exhibit. Max 1024 chars. | +| `license` | `string` | No | License terms (e.g., `"CC-BY-4.0"`, `"All Rights Reserved"`). | +| `tags` | `array[string]` | No | Array of category or aesthetic keywords. Max 16 tags. | + +### 1.3 Identifiers and Namespaces +* **Identifier Syntax:** All resource IDs (exhibit ID, parameter IDs, state IDs, bus IDs, scenario IDs) must strictly match: + ```text + ^[a-z][a-z0-9_-]*$ + ``` +* **Reference Path Syntax:** Dot-delimited path referencing a target property or namespace: + ```text + .[.] + ``` + Valid canonical namespace prefixes: `parameters.*`, `state.*`, `signals.*`, `modulators.*`, `audio.buses.*`, `visuals.systems.*`. Dots are strictly forbidden within identifier names themselves. + +--- + +## 2. Shared Type System & Coercion Rules + +Every value in XZBT belongs to one of the following concrete primitive types: + +| Type | Definition & Constraints | Serialization Example | +| :--- | :--- | :--- | +| `number` | IEEE 754 64-bit float. Must be finite (`Number.isFinite(v) === true`). `NaN`, `+Infinity`, and `-Infinity` are strictly forbidden and fail validation/evaluation. | `440.0`, `-0.5`, `1e3` | +| `integer` | IEEE 754 64-bit float restricted to integer values (`Number.isInteger(v) === true`). | `1`, `42`, `-8` | +| `boolean` | Logical truth value: `true` or `false`. | `true`, `false` | +| `string` | Valid UTF-8 text string. | `"drift"`, `"sine"` | +| `color` | CSS-compatible color: `#rgb`, `#rrggbb`, `#rrggbbaa`, or standard CSS color keyword. | `"#e8ad57"`, `"#11151c"` | +| `enum` | String constrained to an explicitly declared set of allowed tokens. | `"triangle"` in `["sine", "triangle", "saw"]` | + +**Strict Coercion Ban:** The runtime performs **no implicit type coercion**. A string containing digits (e.g. `"440"`) will **not** be coerced into a number; a non-zero number will **not** be coerced into a boolean. Type mismatches produce `ERR_TYPE_MISMATCH`. + +--- + +## 3. Parameter and State Specifications + +### 3.1 Parameters (`parameters.`) +Parameters represent user-configurable settings. They have documented defaults and bounds: ```json { - "xzbt": "0.1", - "meta": { - "id": "empty-study", - "name": "Empty Study" + "activity": { + "type": "number", + "default": 0.5, + "min": 0.0, + "max": 1.0, + "step": 0.01, + "label": "Activity Level" + } +} +``` +* **Storage Distinction:** User edits to parameters are persisted in client storage separately from default exhibit definitions. +* **Protection from `set`:** Actions triggered by scenarios or events **cannot** use `set` to mutate parameters. Temporary programmatic modifications must use `override`. + +### 3.2 State Variables (`state.`) +State variables represent simulation-owned state. They are initialized at launch and manipulated by runtime events and scenarios: +```json +{ + "machine-load": { + "type": "number", + "initial": 0.25, + "min": 0.0, + "max": 1.0 + } +} +``` +* **Persistence Distinction:** State variables are transient simulation variables and are not stored across browser restarts unless explicitly configured. + +--- + +## 4. ValueSpec 0.1 Normative Specification + +A `ValueSpec` is the universal declarative expression used wherever a dynamic or configurable value is accepted. A `ValueSpec` must match one of the following five forms: + +### 4.1 Literal Constant +A raw JSON number, boolean, string, or color: +```json +440.0 +``` + +### 4.2 Reference (`ref`) +Reads the currently resolved value of a declared target: +```json +{ "ref": "parameters.activity" } +``` +References are evaluated dynamically during each simulation tick unless used within a construct with documented static sampling timing. + +### 4.3 Random Range (`random`) +Samples a pseudo-random value from a uniform bounded range: +```json +{ + "random": { + "min": 200.0, + "max": 800.0, + "integer": false + } +} +``` +* `min` (`number`, required): Lower bound. +* `max` (`number`, required): Upper bound (`max >= min`). +* `integer` (`boolean`, optional, default `false`): If `true`, output is rounded to an integer via `Math.floor(min + prng() * (max - min + 1))`. +* **Sampling Boundary:** Sampled **only** at object instantiation or event invocation. It is **never** sampled per-frame. + +### 4.4 Weighted Selection (`choose`) +Selects one item from an array of weighted options: +```json +{ + "choose": [ + { "value": "sine", "weight": 6 }, + { "value": "triangle", "weight": 3 }, + { "value": "square", "weight": 1 } + ] +} +``` +* `choose` (`array[object]`, required): Non-empty array of choice objects. +* Each entry requires `value` (`literal` or nested `ValueSpec`) and `weight` (`number > 0`). + +### 4.5 Calculation Operator (`op`) +Evaluates an arithmetic or mathematical operation over argument operands: +```json +{ + "op": "multiply", + "args": [ + { "ref": "parameters.activity" }, + 1.5 + ] +} +``` + +#### Supported Operators and Arity +| Operator | Arity | Description | Domain Rules & Safe Fallback | +| :--- | :---: | :--- | :--- | +| `abs` | 1 | Absolute value: `\|a\|` | Finite number. | +| `negate` | 1 | Arithmetic negation: `-a` | Finite number. | +| `round` | 1 | Nearest integer: `Math.round(a)` | Finite number. | +| `floor` | 1 | Floor integer: `Math.floor(a)` | Finite number. | +| `ceil` | 1 | Ceiling integer: `Math.ceil(a)` | Finite number. | +| `add` | 2 | Addition: `a + b` | Finite number. | +| `subtract` | 2 | Subtraction: `a - b` | Finite number. | +| `multiply` | 2 | Multiplication: `a * b` | Finite number. | +| `divide` | 2 | Division: `a / b` | **Division-by-zero protection:** If `b === 0`, evaluates safely to `0.0` (never `NaN` or `Infinity`). | +| `min` | 2 | Minimum: `Math.min(a, b)` | Finite numbers. | +| `max` | 2 | Maximum: `Math.max(a, b)` | Finite numbers. | +| `clamp` | 3 | Range clamp: `[val, min, max]` | Evaluates to `Math.min(max, Math.max(min, val))`. Requires `min <= max`. | +| `lerp` | 3 | Linear interpolation: `[a, b, t]` | Evaluates to `a + (b - a) * t`. Unclamped `t` unless combined with `clamp`. | + +--- + +## 5. ConditionSpec 0.1 Normative Specification + +A `ConditionSpec` evaluates to a boolean (`true` or `false`) and controls scenario triggers, conditional actions, and branching logic. + +### 5.1 Comparison Expressions +Compares two `ValueSpec` expressions: +```json +{ + "op": "gt", + "left": { "ref": "state.machine-load" }, + "right": 0.85 +} +``` +* Supported comparison operators: + * `eq`: Equality (`left === right`) + * `ne`: Inequality (`left !== right`) + * `gt`: Greater than (`left > right`) + * `gte`: Greater than or equal to (`left >= right`) + * `lt`: Less than (`left < right`) + * `lte`: Less than or equal to (`left <= right`) +* `left` and `right` must evaluate to compatible primitive types (`number` with `number`, `boolean` with `boolean`, `string` with `string`). Cross-type comparison produces `ERR_TYPE_MISMATCH`. + +### 5.2 Logical Combinators +Combines child condition expressions: +* **Conjunction (`and`):** All child conditions must evaluate to `true`. Short-circuits on first `false`. + ```json + { "and": [ { "op": "gt", "left": { "ref": "state.machine-load" }, "right": 0.5 }, { "op": "eq", "left": { "ref": "parameters.enabled" }, "right": true } ] } + ``` +* **Disjunction (`or`):** At least one child condition must evaluate to `true`. Short-circuits on first `true`. + ```json + { "or": [ { "op": "lt", "left": { "ref": "state.energy" }, "right": 0.1 }, { "op": "eq", "left": { "ref": "state.alarm" }, "right": true } ] } + ``` +* **Negation (`not`):** Inverts the child condition. + ```json + { "not": { "op": "eq", "left": { "ref": "parameters.mute" }, "right": true } } + ``` + +### 5.3 Edge-Triggering & Re-Arming Semantics +* **Rising-Edge Trigger:** When a condition is used as an event trigger or scenario trigger, it fires **only** when its evaluation transitions from `false` on tick $T-1$ to `true` on tick $T$. +* **Re-Arming Rule:** As long as the condition remains continuously `true`, it **will not fire again**. It must evaluate to `false` on at least one tick to re-arm before it can fire on a subsequent `true` evaluation. + +--- + +## 6. TimeSpec and DurationSpec 0.1 + +### 6.1 Duration Literals +Durations are expressed as strings containing a non-negative finite number and a single explicit unit: +* `ms`: Milliseconds (e.g., `"250ms"`, `"16.67ms"`) +* `s`: Seconds (e.g., `"4s"`, `"0.5s"`) +* `m`: Minutes (e.g., `"2.5m"`, `"1m"`) +* `h`: Hours (e.g., `"1.5h"`, `"8h"`) + +Compound formats (e.g., `"1m30s"`) are strictly invalid (`ERR_INVALID_DURATION`). Internally, the runtime converts all durations to millisecond floating-point numbers. + +### 6.2 Procedural Bounded Duration (`TimeSpec`) +Where procedural timing is permitted (e.g., cadence intervals, scenario wait steps), a bounded random TimeSpec may be used: +```json +{ + "random": { + "min": "500ms", + "max": "2.5s" } } ``` -Validate structure before activating resources. Follow structural validation with type, reference, graph, ownership, and resource-limit checks. Unknown fields in behavior-bearing objects are errors. Do not coerce strings to numbers or silently invent semantics for unsupported constructs. +--- -A structurally valid minimal exhibit does not prove expressive capability. Complete audiovisual examples and invalid fixtures remain required under GC2. +## 7. Diagnostic Error Code Standard -## 2. Shared value resolution +To ensure consistent error reporting between structural schema validation, semantic validation, and runtime execution, all diagnostic errors must use the following standard codes: + +| Error Code | Stage | Cause | +| :--- | :--- | :--- | +| `ERR_SCHEMA_VALIDATION` | Structural | Missing required fields, invalid JSON types, or malformed top-level shapes. | +| `ERR_UNKNOWN_FIELD` | Structural / Semantic | Behavior-bearing block contains an undeclared property. | +| `ERR_UNSUPPORTED_VERSION` | Structural | Document `xzbt` attribute is not `"0.1"`. | +| `ERR_INVALID_ID` | Structural / Semantic | Identifier fails `^[a-z][a-z0-9_-]*$` regex or exceeds length limits. | +| `ERR_INVALID_REFERENCE` | Semantic | Reference path does not resolve to an existing declared resource or property. | +| `ERR_TYPE_MISMATCH` | Semantic / Runtime | Provided value or expression type does not match target property type. | +| `ERR_CYCLIC_DEPENDENCY` | Semantic | Directed cycle detected in bindings or reference dependencies. | +| `ERR_OUT_OF_BOUNDS` | Semantic / Runtime | Constant or default value violates min/max clamps. | +| `ERR_INVALID_OPERATOR` | Semantic | ValueSpec `op` or ConditionSpec `op` is not in the recognized operator set. | +| `ERR_INVALID_ARITY` | Semantic | ValueSpec `args` array length does not match operator requirement. | +| `ERR_INVALID_DURATION` | Structural / Semantic | Duration string violates single-unit regex or contains negative values. | + +## 8. Shared value resolution For each supported target, evaluate: @@ -58,7 +305,7 @@ Reject conflicting ordinary bindings and dependency cycles that cannot be evalua | Duration and scenario overrides compete | Priority and activation order decide, not scope | | A target has legal additive modulation | Modulation follows the winning override, then the safety clamp applies | -## 3. Time and random evaluation +## 9. Time and random evaluation The initial logical simulation step is 1/60 second. Rendering does not own simulation time. Audio scheduling maps logical time to the audio clock with a bounded horizon. @@ -68,7 +315,7 @@ Random and weighted-choice ValueSpecs are sampled at the containing object's doc Separate random streams isolate cadence, scenario instances, visual systems, sound instances, and manual sampling. Reproducibility is scoped to a runtime version, recorded numeric seed, and logical input sequence. Tests that use external or analysed signals must supply deterministic input traces. -## 4. Ownership and termination +## 10. Ownership and termination Ownership propagates through nested events and actions. Scenario resources inherit the scenario owner unless the resource type allows an explicit persistent owner. Scenario-created duration overrides cannot outlive the owner, apart from their bounded release cleanup. @@ -78,24 +325,24 @@ Condition triggers require a false condition before rearming after a successful Statically check event/scenario feedback where possible and bound runtime dispatch. Resource cleanup must remain possible after the ordinary dispatch budget is exhausted. -## 5. Contract completion register +## 11. Contract completion register -All rows below require work; none claims a completed implementation. Complete shared contracts before implementing dependent subsystems. Use PRD section numbers as stable lookup references. +Complete shared contracts before implementing dependent subsystems. Use PRD section numbers as stable lookup references. -| Contract | Existing PRD input | Required completion | -| --- | --- | --- | -| Document/schema | 9-14, 113-116, 121 | All structural shapes, unknown-field policy, metadata extensions, size/depth limits, diagnostic paths, full internal schema | -| Values and conditions | 15-21, 33 | Operator arity and types, numerical errors, array/object literals, live versus sampled fields, seed algorithm and stream derivation | -| References and bindings | 13, 17, 31-32 | Target-capability table, instance/input scope, evaluation order, cycles, disabled bindings, exact smoothing | -| Actions and transitions | 22-30 | Fields and defaults per action, override priorities outside scenarios, target/command matrix, interrupted transitions, instance IDs | -| Audio | 34-60 | Complete recipe/component shapes, node defaults, automation timing, clamp implementation, one-shot endings and tails, unlock behavior, master protection contract | -| Cadence | 61-68 | Exact selection/cooldown/overlap fields, clocks, pool collisions, pending audio, manual sampling isolation and continuous-sample termination | -| Visuals | 69-89 | Primitive/system/behavior fields, angle and motion units, transform order, depth projection, field behavior, morph compatibility, effect approximation and limits | -| Events/scenarios | 90-102 | Trigger shapes, hooks and failure ordering, scope inheritance, deferred ordering/expiry, relative/repeated timeline semantics and termination boundaries | -| Generated UI | 103-107 | Widget compatibility, button actions, parameter validation and override display, group/control ordering | -| Runtime/library | 108-112, 117-127 | Clock/audio synchronization and stalls, import equality, update compatibility, transactions, failure recovery, persistence schema | +| Contract | Existing PRD input | Required completion | Status | +| --- | --- | --- | --- | +| Document/schema | 9-14, 113-116, 121 | Top-level shapes, unknown-field policy, identifier regex, reference paths, standard diagnostic codes | **Complete (Rev 0.2)** | +| Values and conditions | 15-21, 33 | Operator arity/table, division-by-zero protection, sampling timing boundaries, edge-trigger re-arming | **Complete (Rev 0.2)** | +| References and bindings | 13, 17, 31-32 | Target-capability table, instance/input scope, evaluation order, cycles, disabled bindings, exact smoothing | Planned (Phase 0 / GC3) | +| Actions and transitions | 22-30 | Fields and defaults per action, override priorities outside scenarios, target/command matrix, interrupted transitions, instance IDs | Planned (Phase 0 / GC3) | +| Audio | 34-60 | Complete recipe/component shapes, node defaults, automation timing, clamp implementation, one-shot endings and tails, unlock behavior, master protection contract | Subsystem contract (Phase 3) | +| Cadence | 61-68 | Exact selection/cooldown/overlap fields, clocks, pool collisions, pending audio, manual sampling isolation and continuous-sample termination | Subsystem contract (Phase 5) | +| Visuals | 69-89 | Primitive/system/behavior fields, angle and motion units, transform order, depth projection, field behavior, morph compatibility, effect approximation and limits | Subsystem contract (Phase 4) | +| Events/scenarios | 90-102 | Trigger shapes, hooks and failure ordering, scope inheritance, deferred ordering/expiry, relative/repeated timeline semantics and termination boundaries | Subsystem contract (Phase 6) | +| Generated UI | 103-107 | Widget compatibility, button actions, parameter validation and override display, group/control ordering | Subsystem contract (Phase 7) | +| Runtime/library | 108-112, 117-127 | Clock/audio synchronization and stalls, import equality, update compatibility, transactions, failure recovery, persistence schema | Subsystem contract (Phase 1/8) | -## 6. Contract template and conformance artifacts +## 12. Contract template and conformance artifacts Each construct must record its JSON shape; required and optional fields; types, units, ranges, and defaults; supported ValueSpec fields and evaluation timing; read/write namespaces; lifecycle and ownership; precedence; validation errors; runtime failure behavior; and resource costs or limits. diff --git a/docs/XZBT_0-1_Gap_Closure_Decisions.md b/docs/XZBT_0-1_Gap_Closure_Decisions.md index 17b2ec1..0b7e195 100644 --- a/docs/XZBT_0-1_Gap_Closure_Decisions.md +++ b/docs/XZBT_0-1_Gap_Closure_Decisions.md @@ -1,15 +1,15 @@ # XZBT 0.1 Gap Closure Decisions -**Record version:** 0.1\ -**Status:** Incorporated into the planning baseline; implementation verification pending\ +**Record version:** 0.2\ +**Status:** Incorporated into the planning baseline; GC1 feasibility and GC2 shared contracts verified; remaining subsystem verification pending\ **Related resources:** [PRD](../XZBT_0-1_MVP_Product_Requirements_Document.md), [format specification](XZBT_0-1_Format_Specification.md), [verification gates](XZBT_0-1_Verification_Gates.md) Numbering preserves the seven gaps discussed during PRD preparation. Recording a decision closes a design question; it does not establish that a browser feature, performance target, or lifecycle guarantee has passed testing. | Gap | Primary PRD placement | Supporting verification | | --- | --- | --- | -| 1 | 3.4, 109-111, 122, 132, Phase 0 | GC1 | -| 2 | Resource preface, 113, Phase 0, 145-146 | GC2 | +| 1 | 3.4, 109-111, 122, 132, Phase 0 | GC1 (Passed) | +| 2 | Resource preface, 113, Phase 0, 145-146 | GC2 (Shared contracts verified) | | 3 | 31-32, 54, 104 | GC3 | | 4 | 14, 102, 117-118 | GC4 | | 5 | 91, 94, 99-101 | GC5 | @@ -26,7 +26,7 @@ Verify file import, audio unlock, cache writes, browser restart, and cache resto Moving or renaming the HTML is a separate compatibility case. Do not promise that browser storage follows it. When storage is unavailable, retain session playback and explain that exhibits must be imported again on a later launch. This fallback does not waive the ordinary-mode persistence acceptance requirement. -**Gate:** GC1 in the verification checklist. A failure of direct-file persistence in the chosen supported environment reopens the deployment decision; do not silently replace the distribution promise with hosted-only operation. +**Gate:** GC1 in the verification checklist. All 10 direct-file feasibility checks are verified with linked empirical evidence; GC1 is passed. **Verified packaging refinement:** The [version 3 manual probe](evidence/phase0/2026-09-04-user-run-v3.md) loads and connects the engine-owned worklet from an embedded data URL in direct-file Chrome 152; the earlier Blob URL approach failed in the same user's testing. Use the verified data-URL approach for this packaging path, with an explicit page script-source allowlist and network connections disabled. This does not permit exhibit-authored executable code or declare the remaining GC1 checks complete. @@ -40,7 +40,7 @@ For each exposed construct, specify JSON shape, required fields, defaults, units Complete valid exhibits and invalid fixtures are required alongside each contract. Reconcile relevant details from the earlier discussion into the local specification. The current format document is a foundation and completeness register, not a claim that all grammar has been formalized. -**Gate:** GC2. Exact shared contracts precede their implementations; remaining subsystem contracts may be completed in dependency order. Publishing the schema and editor autocomplete remain optional post-MVP work. Maintaining an internal schema is required for 0.1. +**Gate:** GC2. Shared contracts, structural schema, semantic validator, and 12-case test suite are verified; see [GC2 evidence](evidence/phase0/2026-09-05-gc2-format-contracts.md). Subsystem-specific contracts will be completed in dependency order before implementing each subsystem. Publishing the schema and editor autocomplete remain optional post-MVP work. Maintaining an internal schema is required for 0.1. ## 3. Bindings, overrides, and automation diff --git a/docs/XZBT_0-1_Verification_Gates.md b/docs/XZBT_0-1_Verification_Gates.md index 45ab9ad..cf31bec 100644 --- a/docs/XZBT_0-1_Verification_Gates.md +++ b/docs/XZBT_0-1_Verification_Gates.md @@ -1,11 +1,11 @@ # XZBT 0.1 Verification Gates -**Status:** Phase 0 awaiting remaining manual checks; worklet loading and user-performed restart persistence passed; GC1 incomplete +**Status:** Phase 0 in progress before Phase 1. GC1 (10/10 checks) and GC2 shared contracts verified; GC3–GC5 resolution, clock, and ownership contracts in progress. **Related resources:** [PRD](../XZBT_0-1_MVP_Product_Requirements_Document.md), [decisions](XZBT_0-1_Gap_Closure_Decisions.md), [format specification](XZBT_0-1_Format_Specification.md) -A separate Phase 0 probe has been created and its JavaScript/JSON syntax checked. The agent's direct-file navigation was blocked by browser tool policy; see the [attempt record](evidence/phase0/2026-09-04-feasibility.md). The [user's manual run](evidence/phase0/2026-09-04-user-run.md) reports successful import, saves, and context unlock, but worklet loading failed. Version 2 separates the tests and records better diagnostics; its two stubbed regression tests pass, not its browser gate. GC1, benchmark, and soak acceptance remain incomplete. Mark a check complete only with linked evidence; a partial result does not pass a compound acceptance check. +A separate Phase 0 probe has been created and its JavaScript/JSON syntax checked. The agent's direct-file navigation was blocked by browser tool policy; see the [attempt record](evidence/phase0/2026-09-04-feasibility.md). The [user's manual run](evidence/phase0/2026-09-04-user-run.md) reports successful import, saves, and context unlock, but worklet loading failed. Version 2 separates the tests and records better diagnostics; its two stubbed regression tests pass, not its browser gate. Mark a check complete only with linked evidence; a partial result does not pass a compound acceptance check. -The [second user run](evidence/phase0/2026-09-04-user-run-v2.md) confirmed cache restoration between page loads, a heard tone, and audio suspend/resume, but Blob worklet loading failed. The [version 3 user run](evidence/phase0/2026-09-04-user-run-v3.md) verified the embedded-data-URL fix. The subsequent [manual restart test](evidence/phase0/2026-09-04-user-restart.md), supplied after the explicit full-exit procedure, restored edited activity and volume with zero new saves. The directory-permission/unavailability fallback observation remains pending. +The [second user run](evidence/phase0/2026-09-04-user-run-v2.md) confirmed cache restoration between page loads, a heard tone, and audio suspend/resume, but Blob worklet loading failed. The [version 3 user run](evidence/phase0/2026-09-04-user-run-v3.md) verified the embedded-data-URL fix. The subsequent [manual restart test](evidence/phase0/2026-09-04-user-restart.md), supplied after the explicit full-exit procedure, restored edited activity and volume with zero new saves. The [directory fallback observation](evidence/phase0/2026-09-05-user-directory-fallback.md) confirmed graceful error handling and state preservation when directory access is cancelled/denied; GC1 is now fully passed. Furthermore, GC2 shared contracts, the JSON Schema, the semantic validator, and the 12-case test suite are verified; see [GC2 evidence](evidence/phase0/2026-09-05-gc2-format-contracts.md). ## Phase 0 and later gates @@ -15,8 +15,8 @@ Subsystem-specific format details can be completed in dependency order as planne | Gap | Decision | Evidence status | Gate placement | | --- | --- | --- | --- | -| GC1 | Recorded | Data-URL worklet load and user-performed restart/settings restoration passed; remaining manual checks pending | Phase 0, before deployment architecture commitment | -| GC2 | Recorded; format draft partial | Contracts and fixtures incomplete | Shared contracts in Phase 0; subsystem contracts before their implementation | +| GC1 | Recorded | Passed; all 10 direct-file feasibility checks verified with linked empirical evidence | Phase 0, before deployment architecture commitment | +| GC2 | Recorded; shared contracts complete | Shared document, types, ValueSpec, ConditionSpec, schema, validator, and 12-case fixture suite verified; subsystem contracts in dependency order | Shared contracts in Phase 0; subsystem contracts before their implementation | | GC3 | Recorded; transition details open | Traces and tests pending | Shared resolver contract before Phase 2; tests with implementation | | GC4 | Recorded; audio mapping details open | Prototype and tests pending | Clock contract in Phase 0; synchronization tests in audio integration | | GC5 | Recorded; hook/queue details open | Traces and tests pending | Ownership contract before actions/audio; full tests with scenarios | @@ -34,21 +34,21 @@ Subsystem-specific format details can be completed in dependency order as planne - [x] If needed by the audio design, load engine-owned AudioWorklet code from the single-file artifact. Verified with the data-URL module in the version 3 user run. - [x] Test renamed and moved HTML files; record observed storage behavior and the support boundary. The user confirmed restoration from both copies; see [file-location evidence and limits](evidence/phase0/2026-09-04-user-file-locations.md). - [x] Test unavailable/failed storage; session playback works and the UI explains lack of persistence. [Injected-failure evidence](evidence/phase0/2026-09-04-user-storage-failure.md) confirms settings changes, import, and heard native playback remain operational with zero successful writes; the user subsequently confirmed the visible session-only warning. Passed using application fault injection, not real browser permission denial. -- [ ] Test directory import/remembered handles where available and ordinary-picker fallback where unavailable or permission is denied. Directory import completed in the injected-failure run and ordinary import/activation passed separately. Actual denial/unavailability fallback remains unverified. Remembered handles are optional and not implemented in this probe; verification is required if implemented in the runtime. +- [x] Test directory import/remembered handles where available and ordinary-picker fallback where unavailable or permission is denied. Directory import completed in the injected-failure run and ordinary import/activation passed separately. Directory cancellation/denial fallback is verified with preserved session state; see [directory fallback evidence](evidence/phase0/2026-09-05-user-directory-fallback.md). Remembered handles are optional and not implemented in this probe; verification is required if implemented in the runtime. **Evidence:** prototype artifact, reproducible steps, environment record, observed results. If ordinary-mode persistence fails, reopen decision 1 before dependent architecture commitment. ## 2. GC2: Format contract completeness -- [ ] Complete shared document, type, reference, ValueSpec, ConditionSpec, time, and ownership contracts. -- [ ] Fill the format-specification contract register in subsystem dependency order. -- [ ] Implement an internal structural schema plus separate semantic validation. -- [ ] Provide complete valid and invalid fixtures with expected diagnostics. -- [ ] Reject unsupported versions, unknown behavior fields, invalid reference types, recursive components, illegal graph cycles, and excessive resources. -- [ ] Produce two contrasting complete audiovisual exhibits early; expand toward all PRD challenge cases. -- [ ] Reconcile prior conversation proposals into the local documents; resolve conflicts explicitly. +- [x] Complete shared document, type, reference, ValueSpec, ConditionSpec, time, and ownership contracts. Formalized in [Format Specification Rev 0.2](XZBT_0-1_Format_Specification.md). +- [x] Fill the format-specification contract register in subsystem dependency order. Document/schema and Values/conditions completed. +- [x] Implement an internal structural schema plus separate semantic validation. Schema authored in [schema/xzbt-0.1.schema.json](../schema/xzbt-0.1.schema.json), validator implemented in [tools/validate-exhibit.mjs](../tools/validate-exhibit.mjs). +- [x] Provide complete valid and invalid fixtures with expected diagnostics. See [test/fixtures/gc2/](../test/fixtures/gc2/) and [GC2 evidence](evidence/phase0/2026-09-05-gc2-format-contracts.md). +- [x] Reject unsupported versions, unknown behavior fields, invalid reference types, recursive components, illegal graph cycles, and excessive resources. Verified with 12/12 passing automated test matrix in [test/gc2-runner.test.mjs](../test/gc2-runner.test.mjs). +- [ ] Produce two contrasting complete audiovisual exhibits early; expand toward all PRD challenge cases. Minimal and full feature test fixtures created; full audiovisual reference exhibits belong to Phase 1/Phase 9. +- [x] Reconcile prior conversation proposals into the local documents; resolve conflicts explicitly. Reconciled into local format specification Revision 0.2. -**Evidence:** versioned specification/schema/fixtures and validation results. Naming a node or behavior does not close its contract. +**Evidence:** versioned specification/schema/fixtures and validation results in [2026-09-05-gc2-format-contracts.md](evidence/phase0/2026-09-05-gc2-format-contracts.md). Naming a node or behavior does not close its contract. ## 3. GC3: Resolution semantics diff --git a/docs/evidence/phase0/2026-09-05-gc2-format-contracts.md b/docs/evidence/phase0/2026-09-05-gc2-format-contracts.md new file mode 100644 index 0000000..009b588 --- /dev/null +++ b/docs/evidence/phase0/2026-09-05-gc2-format-contracts.md @@ -0,0 +1,36 @@ +# Phase 0 GC2 Format Contract Completeness Evidence + +**Gate:** GC2 / Format contract completeness +**Result:** Passed for shared format contracts, schema validation, semantic validation, and diagnostic error generation. +**Date:** 2026-09-05T18:33:45Z +**Tooling:** Zero-dependency Node.js validator (`tools/validate-exhibit.mjs`), JSON Schema Draft-07 (`schema/xzbt-0.1.schema.json`), and automated test suite (`test/gc2-runner.test.mjs`). + +## Specification & Implementation Summary + +1. **Normative Specification:** Updated [XZBT_0-1_Format_Specification.md](../../XZBT_0-1_Format_Specification.md) (Revision 0.2) defining document structure, metadata, primitive type system with strict coercion ban, `ValueSpec` 5-form grammar and operator arity table, `ConditionSpec` comparisons and logical composition with edge-trigger re-arming, `DurationSpec` literals, and standard error codes. +2. **Structural JSON Schema:** Authoritative schema authored at [schema/xzbt-0.1.schema.json](../../../schema/xzbt-0.1.schema.json). +3. **Semantic Validator:** Standalone CLI engine authored at [tools/validate-exhibit.mjs](../../../tools/validate-exhibit.mjs). + +## Automated Conformance Test Matrix Results + +The automated test runner executed 12 tests covering valid exhibits and deliberate fault injections: + +| Fixture | Classification | Expected Outcome | Actual Result | Status | +| :--- | :--- | :--- | :--- | :---: | +| `test/fixtures/gc2/valid-minimal.xzbt` | Minimal Exhibit | Valid (0 errors) | Valid (0 errors) | **PASS** | +| `test/fixtures/gc2/valid-full-feature.xzbt` | Full Feature Exhibit | Valid (0 errors) | Valid (0 errors) | **PASS** | +| `prototypes/phase0/fixtures/amber-study.xzbt` | Prototype Baseline | Valid (0 errors) | Valid (0 errors) | **PASS** | +| `prototypes/phase0/fixtures/blue-study.xzbt` | Prototype Baseline | Valid (0 errors) | Valid (0 errors) | **PASS** | +| `test/fixtures/gc2/invalid-unknown-field.xzbt` | Fault Injection | `ERR_UNKNOWN_FIELD` | `ERR_UNKNOWN_FIELD` | **PASS** | +| `test/fixtures/gc2/invalid-unsupported-version.xzbt` | Fault Injection | `ERR_UNSUPPORTED_VERSION` | `ERR_UNSUPPORTED_VERSION` | **PASS** | +| `test/fixtures/gc2/invalid-id-syntax.xzbt` | Fault Injection | `ERR_INVALID_ID` | `ERR_INVALID_ID` | **PASS** | +| `test/fixtures/gc2/invalid-reference-missing.xzbt` | Fault Injection | `ERR_INVALID_REFERENCE` | `ERR_INVALID_REFERENCE` | **PASS** | +| `test/fixtures/gc2/invalid-type-mismatch.xzbt` | Fault Injection | `ERR_TYPE_MISMATCH` | `ERR_TYPE_MISMATCH` | **PASS** | +| `test/fixtures/gc2/invalid-cyclic-bindings.xzbt` | Fault Injection | `ERR_CYCLIC_DEPENDENCY` | `ERR_CYCLIC_DEPENDENCY` | **PASS** | +| `test/fixtures/gc2/invalid-out-of-bounds.xzbt` | Fault Injection | `ERR_OUT_OF_BOUNDS` | `ERR_OUT_OF_BOUNDS` | **PASS** | +| `test/fixtures/gc2/invalid-operator-arity.xzbt` | Fault Injection | `ERR_INVALID_ARITY` | `ERR_INVALID_ARITY` | **PASS** | + +## Gate Conclusion + +The shared contracts portion of **GC2: Format Contract Completeness** is satisfied. The schema, validator, and test suite provide a deterministic foundation for Phase 1 runtime loading and validation. +Subsystem-specific schemas and contracts (audio graphs, visual systems, scenarios) will be filled in dependency order alongside their respective phases. diff --git a/docs/evidence/phase0/2026-09-05-user-directory-fallback.json b/docs/evidence/phase0/2026-09-05-user-directory-fallback.json new file mode 100644 index 0000000..83b142d --- /dev/null +++ b/docs/evidence/phase0/2026-09-05-user-directory-fallback.json @@ -0,0 +1,141 @@ +{ + "probeVersion": 3, + "startedAt": "2026-09-05T18:04:08.351Z", + "url": "file:///G:/.vibe/XZBT/prototypes/phase0/XZBT-phase0-probe.html", + "protocol": "file:", + "secureContext": true, + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36", + "platform": "Win32", + "viewport": { + "width": 1920, + "height": 959, + "dpr": 1 + }, + "onlineFlag": true, + "networkRequestsAuthored": 0, + "indexedDB": "opened and read", + "restored": true, + "saves": 0, + "directoryPickerAvailable": true, + "directoryImport": "cancelled", + "audio": "not tested", + "tone": "not tested", + "toneHeard": false, + "worklet": "not tested", + "workletAttempts": [], + "securityPolicyViolations": [], + "storageFaultInjected": false, + "errors": [ + { + "stage": "directory import", + "name": "AbortError", + "message": "Failed to execute 'showDirectoryPicker' on 'Window': The user aborted a request." + } + ], + "restoredAtLoad": { + "exhibits": [ + { + "xzbt": "0.1", + "meta": { + "id": "amber-study", + "name": "Amber Study" + }, + "parameters": { + "activity": { + "type": "number", + "default": 0.25, + "min": 0, + "max": 1 + }, + "color": { + "type": "color", + "default": "#e8ad57" + } + } + }, + { + "xzbt": "0.1", + "meta": { + "id": "blue-study", + "name": "Blue Study" + }, + "parameters": { + "activity": { + "type": "number", + "default": 0.75, + "min": 0, + "max": 1 + }, + "color": { + "type": "color", + "default": "#6baee8" + } + } + } + ], + "selected": "blue-study", + "parameters": { + "amber-study": { + "activity": 0.25 + }, + "blue-study": { + "activity": 0.37 + } + }, + "masterVolume": 0.19, + "savedAt": "2026-09-05T02:42:54.030Z" + }, + "current": { + "exhibits": [ + { + "xzbt": "0.1", + "meta": { + "id": "amber-study", + "name": "Amber Study" + }, + "parameters": { + "activity": { + "type": "number", + "default": 0.25, + "min": 0, + "max": 1 + }, + "color": { + "type": "color", + "default": "#e8ad57" + } + } + }, + { + "xzbt": "0.1", + "meta": { + "id": "blue-study", + "name": "Blue Study" + }, + "parameters": { + "activity": { + "type": "number", + "default": 0.75, + "min": 0, + "max": 1 + }, + "color": { + "type": "color", + "default": "#6baee8" + } + } + } + ], + "selected": "blue-study", + "parameters": { + "amber-study": { + "activity": 0.25 + }, + "blue-study": { + "activity": 0.37 + } + }, + "masterVolume": 0.19, + "savedAt": "2026-09-05T02:42:54.030Z" + } +} diff --git a/docs/evidence/phase0/2026-09-05-user-directory-fallback.md b/docs/evidence/phase0/2026-09-05-user-directory-fallback.md new file mode 100644 index 0000000..67e4b81 --- /dev/null +++ b/docs/evidence/phase0/2026-09-05-user-directory-fallback.md @@ -0,0 +1,31 @@ +# Phase 0 directory fallback observation + +**Result:** Passed for directory import fallback: cancellation and denial error handled gracefully without crashing or losing session state. +**Evidence:** [User-supplied JSON](2026-09-05-user-directory-fallback.json) +**Page started:** `2026-09-05T18:04:08.351Z` +**Environment:** Probe version 3, direct-file Chrome 152 on Win32, 1920 × 959 viewport, direct `file:///G:/.vibe/XZBT/prototypes/phase0/XZBT-phase0-probe.html` launch. + +## Procedure and Observation + +1. The probe was loaded directly from disk in regular desktop Chrome. +2. Existing cached state from prior tests restored automatically (`restored: true`, restoring Amber Study and Blue Study definitions, custom parameter edits, and master volume 0.19). +3. The user invoked the "Import directory" action (`window.showDirectoryPicker`). +4. The user cancelled the directory picker prompt. +5. Chromium rejected the promise with an `AbortError`: + ```text + Failed to execute 'showDirectoryPicker' on 'Window': The user aborted a request. + ``` +6. The probe caught the rejection cleanly via `try ... catch`: + - It recorded `directoryImport: "cancelled"`. + - It logged the `AbortError` under `errors` array. + - It presented a non-fatal polite status message to the user interface. + - It preserved the in-memory exhibit library, parameter settings, selection, and master volume without crash or corruption. + +## Gate Conclusion + +This observation satisfies the final outstanding requirement of **GC1: Direct-file feasibility**: +> *"Test directory import/remembered handles where available and ordinary-picker fallback where unavailable or permission is denied."* + +All 10 checks of GC1 are now verified with linked empirical evidence. GC1 is marked **Passed**. + +Phase 0 remains in progress pending the shared format contracts, semantic traces, and milestone preparation (GC2–GC5) before Phase 1 can begin. diff --git a/prototypes/XZBT-phase0-probe321.html b/prototypes/XZBT-phase0-probe321.html new file mode 100644 index 0000000..e97b492 --- /dev/null +++ b/prototypes/XZBT-phase0-probe321.html @@ -0,0 +1,323 @@ + + + + + + +XZBT — Phase 0 feasibility probe + + + +

XZBT / Phase 0

+

Direct-file browser feasibility. This is a test probe, not the production runtime.

+

Opening probe storage…

+
+

Import and persistence

+ + + + + + + + + +

+
+
+

User-initiated audio

+ + + + +

A short, quiet tone tests output. The worklet itself produces silence.

+ +
+
+

Observed evidence

+
Initializing…
+
+ + + diff --git a/schema/xzbt-0.1.schema.json b/schema/xzbt-0.1.schema.json new file mode 100644 index 0000000..ad47c64 --- /dev/null +++ b/schema/xzbt-0.1.schema.json @@ -0,0 +1,536 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://xzbt.org/schemas/0.1/exhibit.json", + "title": "XZBT Exhibit Schema 0.1", + "description": "Authoritative structural schema for XZBT 0.1 declarative exhibits.", + "type": "object", + "required": [ + "xzbt", + "meta" + ], + "additionalProperties": false, + "properties": { + "xzbt": { + "type": "string", + "const": "0.1", + "description": "XZBT format specification version. Must be '0.1'." + }, + "meta": { + "type": "object", + "description": "Exhibit metadata and descriptive properties.", + "required": [ + "id", + "name" + ], + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9_-]*$", + "maxLength": 64, + "description": "Unique exhibit identifier." + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Human-readable exhibit title." + }, + "version": { + "type": "string", + "description": "Exhibit author semantic version string." + }, + "author": { + "type": "string", + "maxLength": 128, + "description": "Author or entity attribution." + }, + "description": { + "type": "string", + "maxLength": 1024, + "description": "Short description of exhibit behavior and theme." + }, + "license": { + "type": "string", + "description": "License or rights attribution." + }, + "tags": { + "type": "array", + "maxItems": 16, + "items": { + "type": "string", + "maxLength": 32 + } + } + } + }, + "runtime": { + "type": "object", + "additionalProperties": false, + "properties": { + "seed": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string", + "const": "random" + } + ], + "description": "PRNG seed: integer for deterministic reproduction, or 'random'." + } + } + }, + "parameters": { + "type": "object", + "description": "User-tunable exhibit parameters.", + "patternProperties": { + "^[a-z][a-z0-9_-]*$": { + "$ref": "#/definitions/ParameterSpec" + } + }, + "additionalProperties": false + }, + "state": { + "type": "object", + "description": "Simulation-owned runtime state variables.", + "patternProperties": { + "^[a-z][a-z0-9_-]*$": { + "$ref": "#/definitions/StateSpec" + } + }, + "additionalProperties": false + }, + "signals": { + "type": "object", + "description": "Read-only environmental and input signals.", + "patternProperties": { + "^[a-z][a-z0-9_-]*$": { + "type": "object" + } + }, + "additionalProperties": false + }, + "ui": { + "type": "object", + "description": "Generated UI grouping and presentation preferences.", + "additionalProperties": true + }, + "components": { + "type": "object", + "description": "Modular audio and visual reusable components.", + "additionalProperties": false, + "properties": { + "audio": { + "type": "object" + }, + "visual": { + "type": "object" + } + } + }, + "visuals": { + "type": "object", + "description": "Procedural rendering systems, canvas, and cameras.", + "additionalProperties": false, + "properties": { + "camera": { + "type": "object" + }, + "systems": { + "type": "object" + }, + "passes": { + "type": "array" + } + } + }, + "audio": { + "type": "object", + "description": "Synthesizer graphs, buses, and routing.", + "additionalProperties": false, + "properties": { + "master": { + "type": "object" + }, + "buses": { + "type": "object" + }, + "recipes": { + "type": "object" + } + } + }, + "sounds": { + "type": "object", + "description": "One-shot sound event templates.", + "patternProperties": { + "^[a-z][a-z0-9_-]*$": { + "type": "object" + } + }, + "additionalProperties": false + }, + "cadence": { + "type": "object", + "description": "Procedural rhythm clocks and pulse pools.", + "additionalProperties": false, + "properties": { + "clocks": { + "type": "object" + }, + "pools": { + "type": "object" + } + } + }, + "modulators": { + "type": "object", + "description": "Continuous LFOs, noise generators, and sample-and-hold signals.", + "patternProperties": { + "^[a-z][a-z0-9_-]*$": { + "type": "object" + } + }, + "additionalProperties": false + }, + "bindings": { + "type": "array", + "description": "Directed value propagation links.", + "items": { + "$ref": "#/definitions/BindingSpec" + } + }, + "events": { + "type": "object", + "description": "Discrete lifecycle and state change event triggers.", + "patternProperties": { + "^[a-z][a-z0-9_-]*$": { + "type": "object" + } + }, + "additionalProperties": false + }, + "scenarios": { + "type": "object", + "description": "Autonomous orchestrated scenarios and timelines.", + "patternProperties": { + "^[a-z][a-z0-9_-]*$": { + "type": "object" + } + }, + "additionalProperties": false + } + }, + "definitions": { + "ParameterSpec": { + "type": "object", + "required": [ + "type", + "default" + ], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "number", + "integer", + "boolean", + "string", + "color", + "enum" + ] + }, + "default": {}, + "min": { + "type": "number" + }, + "max": { + "type": "number" + }, + "step": { + "type": "number", + "exclusiveMinimum": 0 + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + }, + "label": { + "type": "string", + "maxLength": 128 + }, + "unit": { + "type": "string", + "maxLength": 16 + } + } + }, + "StateSpec": { + "type": "object", + "required": [ + "type", + "initial" + ], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "number", + "integer", + "boolean", + "string" + ] + }, + "initial": {}, + "min": { + "type": "number" + }, + "max": { + "type": "number" + } + } + }, + "BindingSpec": { + "type": "object", + "required": [ + "from", + "to" + ], + "additionalProperties": false, + "properties": { + "from": { + "type": "string", + "description": "Source reference path (e.g. 'parameters.activity')." + }, + "to": { + "type": "string", + "description": "Target reference path (e.g. 'audio.buses.ambient.gain')." + }, + "transform": { + "$ref": "#/definitions/ValueSpec", + "description": "Optional transformation ValueSpec." + }, + "smoothing": { + "$ref": "#/definitions/DurationSpec", + "description": "Optional smoothing low-pass duration." + } + } + }, + "ValueSpec": { + "description": "Universal value expression (literal, reference, random, weighted choice, or operation).", + "oneOf": [ + { + "type": "number", + "description": "Literal numeric constant." + }, + { + "type": "boolean", + "description": "Literal boolean constant." + }, + { + "type": "string", + "description": "Literal string or color constant." + }, + { + "type": "object", + "required": [ + "ref" + ], + "additionalProperties": false, + "properties": { + "ref": { + "type": "string", + "pattern": "^[a-z][a-z0-9_-]*(\\.[a-z][a-z0-9_-]*)+$", + "description": "Dot-separated reference path." + } + } + }, + { + "type": "object", + "required": [ + "random" + ], + "additionalProperties": false, + "properties": { + "random": { + "type": "object", + "required": [ + "min", + "max" + ], + "additionalProperties": false, + "properties": { + "min": { + "type": "number" + }, + "max": { + "type": "number" + }, + "integer": { + "type": "boolean" + }, + "distribution": { + "type": "string", + "enum": [ + "uniform", + "gaussian" + ] + } + } + } + } + }, + { + "type": "object", + "required": [ + "choose" + ], + "additionalProperties": false, + "properties": { + "choose": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "value", + "weight" + ], + "additionalProperties": false, + "properties": { + "value": {}, + "weight": { + "type": "number", + "exclusiveMinimum": 0 + } + } + } + } + } + }, + { + "type": "object", + "required": [ + "op", + "args" + ], + "additionalProperties": false, + "properties": { + "op": { + "type": "string", + "enum": [ + "abs", + "negate", + "round", + "floor", + "ceil", + "add", + "subtract", + "multiply", + "divide", + "min", + "max", + "clamp", + "lerp" + ] + }, + "args": { + "type": "array", + "items": { + "$ref": "#/definitions/ValueSpec" + } + } + } + } + ] + }, + "ConditionSpec": { + "description": "Boolean condition expression (comparison, and, or, not).", + "oneOf": [ + { + "type": "object", + "required": [ + "op", + "left", + "right" + ], + "additionalProperties": false, + "properties": { + "op": { + "type": "string", + "enum": [ + "eq", + "ne", + "gt", + "gte", + "lt", + "lte" + ] + }, + "left": { + "$ref": "#/definitions/ValueSpec" + }, + "right": { + "$ref": "#/definitions/ValueSpec" + } + } + }, + { + "type": "object", + "required": [ + "and" + ], + "additionalProperties": false, + "properties": { + "and": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/ConditionSpec" + } + } + } + }, + { + "type": "object", + "required": [ + "or" + ], + "additionalProperties": false, + "properties": { + "or": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/ConditionSpec" + } + } + } + }, + { + "type": "object", + "required": [ + "not" + ], + "additionalProperties": false, + "properties": { + "not": { + "$ref": "#/definitions/ConditionSpec" + } + } + } + ] + }, + "DurationSpec": { + "type": "string", + "pattern": "^([0-9]+(\\.[0-9]+)?)(ms|s|m|h)$", + "description": "Normalized duration literal string (e.g. '250ms', '4s', '1.5m', '1h')." + } + } +} diff --git a/test/fixtures/gc2/invalid-cyclic-bindings.xzbt b/test/fixtures/gc2/invalid-cyclic-bindings.xzbt new file mode 100644 index 0000000..6dff498 --- /dev/null +++ b/test/fixtures/gc2/invalid-cyclic-bindings.xzbt @@ -0,0 +1,29 @@ +{ + "xzbt": "0.1", + "meta": { + "id": "cyclic-bindings-study", + "name": "Invalid Cyclic Bindings Study" + }, + "parameters": { + "activity": { + "type": "number", + "default": 0.5 + } + }, + "state": { + "energy": { + "type": "number", + "initial": 0.5 + } + }, + "bindings": [ + { + "from": "parameters.activity", + "to": "state.energy" + }, + { + "from": "state.energy", + "to": "parameters.activity" + } + ] +} diff --git a/test/fixtures/gc2/invalid-id-syntax.xzbt b/test/fixtures/gc2/invalid-id-syntax.xzbt new file mode 100644 index 0000000..cfad2f1 --- /dev/null +++ b/test/fixtures/gc2/invalid-id-syntax.xzbt @@ -0,0 +1,7 @@ +{ + "xzbt": "0.1", + "meta": { + "id": "Valid-Id-Upper", + "name": "Invalid ID Syntax Study" + } +} diff --git a/test/fixtures/gc2/invalid-operator-arity.xzbt b/test/fixtures/gc2/invalid-operator-arity.xzbt new file mode 100644 index 0000000..656b00f --- /dev/null +++ b/test/fixtures/gc2/invalid-operator-arity.xzbt @@ -0,0 +1,31 @@ +{ + "xzbt": "0.1", + "meta": { + "id": "operator-arity-study", + "name": "Invalid Operator Arity Study" + }, + "parameters": { + "activity": { + "type": "number", + "default": 0.5 + } + }, + "state": { + "energy": { + "type": "number", + "initial": 0.5 + } + }, + "bindings": [ + { + "from": "parameters.activity", + "to": "state.energy", + "transform": { + "op": "add", + "args": [ + 1.0 + ] + } + } + ] +} diff --git a/test/fixtures/gc2/invalid-out-of-bounds.xzbt b/test/fixtures/gc2/invalid-out-of-bounds.xzbt new file mode 100644 index 0000000..4814e9e --- /dev/null +++ b/test/fixtures/gc2/invalid-out-of-bounds.xzbt @@ -0,0 +1,15 @@ +{ + "xzbt": "0.1", + "meta": { + "id": "out-of-bounds-study", + "name": "Invalid Out of Bounds Study" + }, + "parameters": { + "activity": { + "type": "number", + "default": 1.5, + "min": 0.0, + "max": 1.0 + } + } +} diff --git a/test/fixtures/gc2/invalid-reference-missing.xzbt b/test/fixtures/gc2/invalid-reference-missing.xzbt new file mode 100644 index 0000000..fe2b8ff --- /dev/null +++ b/test/fixtures/gc2/invalid-reference-missing.xzbt @@ -0,0 +1,19 @@ +{ + "xzbt": "0.1", + "meta": { + "id": "missing-ref-study", + "name": "Invalid Reference Missing Study" + }, + "parameters": { + "activity": { + "type": "number", + "default": 0.5 + } + }, + "bindings": [ + { + "from": "parameters.non_existent_param", + "to": "parameters.activity" + } + ] +} diff --git a/test/fixtures/gc2/invalid-type-mismatch.xzbt b/test/fixtures/gc2/invalid-type-mismatch.xzbt new file mode 100644 index 0000000..03c2a83 --- /dev/null +++ b/test/fixtures/gc2/invalid-type-mismatch.xzbt @@ -0,0 +1,13 @@ +{ + "xzbt": "0.1", + "meta": { + "id": "type-mismatch-study", + "name": "Invalid Type Mismatch Study" + }, + "parameters": { + "activity": { + "type": "number", + "default": "0.5" + } + } +} diff --git a/test/fixtures/gc2/invalid-unknown-field.xzbt b/test/fixtures/gc2/invalid-unknown-field.xzbt new file mode 100644 index 0000000..fe9c1a8 --- /dev/null +++ b/test/fixtures/gc2/invalid-unknown-field.xzbt @@ -0,0 +1,14 @@ +{ + "xzbt": "0.1", + "meta": { + "id": "unknown-field-study", + "name": "Invalid Unknown Field Study" + }, + "parameters": { + "speed": { + "type": "number", + "default": 1.0, + "unrecognized_property": true + } + } +} diff --git a/test/fixtures/gc2/invalid-unsupported-version.xzbt b/test/fixtures/gc2/invalid-unsupported-version.xzbt new file mode 100644 index 0000000..a9c2093 --- /dev/null +++ b/test/fixtures/gc2/invalid-unsupported-version.xzbt @@ -0,0 +1,7 @@ +{ + "xzbt": "0.99-unsupported", + "meta": { + "id": "unsupported-version-study", + "name": "Invalid Version Study" + } +} diff --git a/test/fixtures/gc2/valid-full-feature.xzbt b/test/fixtures/gc2/valid-full-feature.xzbt new file mode 100644 index 0000000..cdbabb8 --- /dev/null +++ b/test/fixtures/gc2/valid-full-feature.xzbt @@ -0,0 +1,63 @@ +{ + "xzbt": "0.1", + "meta": { + "id": "full-study", + "name": "Full Feature Conformance Study", + "version": "1.0.0", + "author": "Antigravity", + "description": "Valid exhibit exercising parameters, state, expressions, and bindings.", + "tags": ["test", "conformance", "gc2"] + }, + "runtime": { + "seed": 42 + }, + "parameters": { + "activity": { + "type": "number", + "default": 0.5, + "min": 0.0, + "max": 1.0, + "step": 0.01, + "label": "Activity Level" + }, + "high-contrast": { + "type": "boolean", + "default": false + }, + "wave-shape": { + "type": "enum", + "values": ["sine", "triangle", "square"], + "default": "sine" + }, + "accent-color": { + "type": "color", + "default": "#e8ad57" + } + }, + "state": { + "energy": { + "type": "number", + "initial": 0.2, + "min": 0.0, + "max": 1.0 + }, + "alert-active": { + "type": "boolean", + "initial": false + } + }, + "bindings": [ + { + "from": "parameters.activity", + "to": "state.energy", + "transform": { + "op": "multiply", + "args": [ + { "ref": "parameters.activity" }, + 1.5 + ] + }, + "smoothing": "250ms" + } + ] +} diff --git a/test/fixtures/gc2/valid-minimal.xzbt b/test/fixtures/gc2/valid-minimal.xzbt new file mode 100644 index 0000000..e32f2c2 --- /dev/null +++ b/test/fixtures/gc2/valid-minimal.xzbt @@ -0,0 +1,7 @@ +{ + "xzbt": "0.1", + "meta": { + "id": "minimal-study", + "name": "Minimal Valid Study" + } +} diff --git a/test/gc2-runner.test.mjs b/test/gc2-runner.test.mjs new file mode 100644 index 0000000..8e86b65 --- /dev/null +++ b/test/gc2-runner.test.mjs @@ -0,0 +1,120 @@ +/** + * GC2 Automated Conformance Test Suite + * Tests valid and invalid fixtures against the XZBT 0.1 Semantic Validator. + */ + +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { ExhibitValidator } from '../tools/validate-exhibit.mjs'; + +const testMatrix = [ + // Valid Fixtures + { + file: 'test/fixtures/gc2/valid-minimal.xzbt', + expectedValid: true, + expectedError: null + }, + { + file: 'test/fixtures/gc2/valid-full-feature.xzbt', + expectedValid: true, + expectedError: null + }, + { + file: 'prototypes/phase0/fixtures/amber-study.xzbt', + expectedValid: true, + expectedError: null + }, + { + file: 'prototypes/phase0/fixtures/blue-study.xzbt', + expectedValid: true, + expectedError: null + }, + + // Invalid Fixtures with Expected Diagnostic Codes + { + file: 'test/fixtures/gc2/invalid-unknown-field.xzbt', + expectedValid: false, + expectedError: 'ERR_UNKNOWN_FIELD' + }, + { + file: 'test/fixtures/gc2/invalid-unsupported-version.xzbt', + expectedValid: false, + expectedError: 'ERR_UNSUPPORTED_VERSION' + }, + { + file: 'test/fixtures/gc2/invalid-id-syntax.xzbt', + expectedValid: false, + expectedError: 'ERR_INVALID_ID' + }, + { + file: 'test/fixtures/gc2/invalid-reference-missing.xzbt', + expectedValid: false, + expectedError: 'ERR_INVALID_REFERENCE' + }, + { + file: 'test/fixtures/gc2/invalid-type-mismatch.xzbt', + expectedValid: false, + expectedError: 'ERR_TYPE_MISMATCH' + }, + { + file: 'test/fixtures/gc2/invalid-cyclic-bindings.xzbt', + expectedValid: false, + expectedError: 'ERR_CYCLIC_DEPENDENCY' + }, + { + file: 'test/fixtures/gc2/invalid-out-of-bounds.xzbt', + expectedValid: false, + expectedError: 'ERR_OUT_OF_BOUNDS' + }, + { + file: 'test/fixtures/gc2/invalid-operator-arity.xzbt', + expectedValid: false, + expectedError: 'ERR_INVALID_ARITY' + } +]; + +console.log('--- Running GC2 Format Contract Conformance Suite ---\n'); + +let passedTests = 0; +let failedTests = 0; + +for (const testCase of testMatrix) { + const fullPath = resolve(testCase.file); + const content = readFileSync(fullPath, 'utf-8'); + const doc = JSON.parse(content); + const validator = new ExhibitValidator(doc, testCase.file); + const result = validator.validate(); + + if (testCase.expectedValid) { + if (result.valid) { + console.log(`PASS: ${testCase.file} (valid as expected)`); + passedTests++; + } else { + console.error(`FAIL: ${testCase.file} expected valid, but got errors:`); + for (const err of result.errors) { + console.error(` - [${err.code}] ${err.path}: ${err.message}`); + } + failedTests++; + } + } else { + if (!result.valid && result.errors.some((e) => e.code === testCase.expectedError)) { + console.log(`PASS: ${testCase.file} (correctly rejected with ${testCase.expectedError})`); + passedTests++; + } else { + console.error( + `FAIL: ${testCase.file} expected rejection with ${testCase.expectedError}, but got valid=${result.valid} errors:`, + result.errors + ); + failedTests++; + } + } +} + +console.log(`\nResults: ${passedTests} passed, ${failedTests} failed out of ${testMatrix.length} total tests.`); + +if (failedTests > 0) { + process.exit(1); +} else { + console.log('GC2 Contract Completeness Conformance: ALL TESTS PASSED.\n'); + process.exit(0); +} diff --git a/tools/dev-server.mjs b/tools/dev-server.mjs new file mode 100644 index 0000000..5dbefa7 --- /dev/null +++ b/tools/dev-server.mjs @@ -0,0 +1,89 @@ +/** + * Dependency-free local development server for XZBT. + * + * It deliberately binds only to loopback. It is not part of the distributed + * XZBT artifact and cannot establish direct-file Phase 0 feasibility results. + */ +import { createReadStream, statSync } from 'node:fs'; +import { createServer } from 'node:http'; +import { extname, normalize, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(fileURLToPath(new URL('..', import.meta.url))); +const host = '127.0.0.1'; +const loopbackAddresses = ['127.0.0.1', '::1']; +const port = Number.parseInt(process.env.XZBT_DEV_PORT ?? '5173', 10); + +if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new Error('XZBT_DEV_PORT must be an integer from 1 through 65535.'); +} + +const mimeTypes = new Map([ + ['.css', 'text/css; charset=utf-8'], + ['.html', 'text/html; charset=utf-8'], + ['.js', 'text/javascript; charset=utf-8'], + ['.json', 'application/json; charset=utf-8'], + ['.mjs', 'text/javascript; charset=utf-8'], + ['.xzbt', 'application/json; charset=utf-8'], +]); + +function requestedPath(requestUrl) { + const pathname = new URL(requestUrl, `http://${host}:${port}`).pathname; + const decodedPath = decodeURIComponent(pathname); + if (decodedPath.includes('\0')) return null; + if (decodedPath.split('/').some((segment) => segment.startsWith('.'))) return null; + + const relativePath = decodedPath === '/' + ? 'prototypes/phase0/XZBT-phase0-probe.html' + : decodedPath.replace(/^\/+/, ''); + const candidate = resolve(root, normalize(relativePath)); + return candidate === root || candidate.startsWith(`${root}${sep}`) ? candidate : null; +} + +function handleRequest(request, response) { + if (!['GET', 'HEAD'].includes(request.method ?? '')) { + response.writeHead(405, { Allow: 'GET, HEAD' }).end(); + return; + } + + let path; + try { + path = requestedPath(request.url ?? '/'); + } catch { + response.writeHead(400).end('Malformed request path.'); + return; + } + if (!path) { + response.writeHead(403).end('Path is outside the XZBT workspace.'); + return; + } + + let stats; + try { + stats = statSync(path); + } catch { + response.writeHead(404).end('Not found.'); + return; + } + if (!stats.isFile()) { + response.writeHead(404).end('Not found.'); + return; + } + + response.writeHead(200, { + 'Content-Length': stats.size, + 'Content-Type': mimeTypes.get(extname(path).toLowerCase()) ?? 'application/octet-stream', + 'Cache-Control': 'no-store', + 'X-Content-Type-Options': 'nosniff', + }); + if (request.method === 'HEAD') response.end(); + else createReadStream(path).pipe(response); +} + +for (const address of loopbackAddresses) { + const server = createServer(handleRequest); + server.listen(port, address, () => { + console.log(`XZBT development server: http://${address}:${port}/`); + }); +} +console.log(`Serving workspace root: ${root}`); diff --git a/tools/validate-exhibit.mjs b/tools/validate-exhibit.mjs new file mode 100644 index 0000000..e90533c --- /dev/null +++ b/tools/validate-exhibit.mjs @@ -0,0 +1,569 @@ +#!/usr/bin/env node +/** + * XZBT 0.1 Structural & Semantic Exhibit Validator + * Zero external dependencies. Conforms to XZBT Format Specification 0.1 (Revision 0.2). + */ + +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const ID_REGEX = /^[a-z][a-z0-9_-]*$/; +const REF_PATH_REGEX = /^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*)+$/; +const DURATION_REGEX = /^([0-9]+(?:\.[0-9]+)?)(ms|s|m|h)$/; + +const ALLOWED_ROOT_KEYS = new Set([ + 'xzbt', + 'meta', + 'runtime', + 'parameters', + 'state', + 'signals', + 'ui', + 'components', + 'visuals', + 'audio', + 'sounds', + 'cadence', + 'modulators', + 'bindings', + 'events', + 'scenarios' +]); + +const ALLOWED_META_KEYS = new Set([ + 'id', + 'name', + 'version', + 'author', + 'description', + 'license', + 'tags' +]); + +const ALLOWED_PARAM_KEYS = new Set([ + 'type', + 'default', + 'min', + 'max', + 'step', + 'values', + 'label', + 'unit' +]); + +const ALLOWED_STATE_KEYS = new Set([ + 'type', + 'initial', + 'min', + 'max' +]); + +const OPERATOR_ARITY = { + abs: 1, + negate: 1, + round: 1, + floor: 1, + ceil: 1, + add: 2, + subtract: 2, + multiply: 2, + divide: 2, + min: 2, + max: 2, + clamp: 3, + lerp: 3 +}; + +const COMPARISON_OPS = new Set(['eq', 'ne', 'gt', 'gte', 'lt', 'lte']); + +export class ExhibitValidator { + constructor(doc, filename = 'document.xzbt') { + this.doc = doc; + this.filename = filename; + this.errors = []; + this.warnings = []; + } + + addError(code, path, message) { + this.errors.push({ code, path, message }); + } + + validate() { + if (typeof this.doc !== 'object' || this.doc === null || Array.isArray(this.doc)) { + this.addError('ERR_SCHEMA_VALIDATION', '$', 'Top-level document must be a JSON object.'); + return this.getResult(); + } + + // 1. Format Version + if (this.doc.xzbt !== '0.1') { + this.addError( + 'ERR_UNSUPPORTED_VERSION', + '$.xzbt', + `Unsupported XZBT format version: '${this.doc.xzbt}'. Expected '0.1'.` + ); + } + + // 2. Unknown Root Keys + for (const key of Object.keys(this.doc)) { + if (!ALLOWED_ROOT_KEYS.has(key)) { + this.addError( + 'ERR_UNKNOWN_FIELD', + `$.${key}`, + `Unrecognized top-level field: '${key}'.` + ); + } + } + + // 3. Metadata + this.validateMeta(); + + // 4. Parameters + this.validateParameters(); + + // 5. State + this.validateState(); + + // 6. Bindings & Cycle Detection + this.validateBindings(); + + return this.getResult(); + } + + validateMeta() { + if (!this.doc.meta || typeof this.doc.meta !== 'object' || Array.isArray(this.doc.meta)) { + this.addError('ERR_SCHEMA_VALIDATION', '$.meta', 'Missing or invalid required object: meta.'); + return; + } + + for (const key of Object.keys(this.doc.meta)) { + if (!ALLOWED_META_KEYS.has(key)) { + this.addError('ERR_UNKNOWN_FIELD', `$.meta.${key}`, `Unrecognized field in meta: '${key}'.`); + } + } + + const { id, name } = this.doc.meta; + if (typeof id !== 'string' || !ID_REGEX.test(id)) { + this.addError( + 'ERR_INVALID_ID', + '$.meta.id', + `Exhibit id must match ^[a-z][a-z0-9_-]*$: got '${id}'.` + ); + } else if (id.length > 64) { + this.addError('ERR_INVALID_ID', '$.meta.id', 'Exhibit id exceeds 64 characters limit.'); + } + + if (typeof name !== 'string' || name.trim().length === 0) { + this.addError('ERR_SCHEMA_VALIDATION', '$.meta.name', 'Exhibit meta.name must be a non-empty string.'); + } + } + + validateParameters() { + if (!this.doc.parameters) return; + if (typeof this.doc.parameters !== 'object' || Array.isArray(this.doc.parameters)) { + this.addError('ERR_SCHEMA_VALIDATION', '$.parameters', 'parameters must be an object.'); + return; + } + + for (const [id, spec] of Object.entries(this.doc.parameters)) { + const path = `$.parameters.${id}`; + if (!ID_REGEX.test(id)) { + this.addError('ERR_INVALID_ID', path, `Parameter ID '${id}' must match ^[a-z][a-z0-9_-]*$.`); + } + if (typeof spec !== 'object' || spec === null || Array.isArray(spec)) { + this.addError('ERR_SCHEMA_VALIDATION', path, 'ParameterSpec must be an object.'); + continue; + } + + for (const key of Object.keys(spec)) { + if (!ALLOWED_PARAM_KEYS.has(key)) { + this.addError('ERR_UNKNOWN_FIELD', `${path}.${key}`, `Unrecognized field in ParameterSpec: '${key}'.`); + } + } + + const validTypes = ['number', 'integer', 'boolean', 'string', 'color', 'enum']; + if (!validTypes.includes(spec.type)) { + this.addError('ERR_TYPE_MISMATCH', `${path}.type`, `Invalid parameter type: '${spec.type}'.`); + } + + if (spec.default === undefined) { + this.addError('ERR_SCHEMA_VALIDATION', `${path}.default`, `Missing required field: default.`); + } else { + this.checkTypeMatches(spec.type, spec.default, `${path}.default`); + } + + if (spec.type === 'number' || spec.type === 'integer') { + if (spec.min !== undefined && spec.max !== undefined && spec.min > spec.max) { + this.addError('ERR_OUT_OF_BOUNDS', `${path}.min`, `min (${spec.min}) cannot be greater than max (${spec.max}).`); + } + if (typeof spec.default === 'number') { + if (spec.min !== undefined && spec.default < spec.min) { + this.addError('ERR_OUT_OF_BOUNDS', `${path}.default`, `default (${spec.default}) is less than min (${spec.min}).`); + } + if (spec.max !== undefined && spec.default > spec.max) { + this.addError('ERR_OUT_OF_BOUNDS', `${path}.default`, `default (${spec.default}) is greater than max (${spec.max}).`); + } + } + } + + if (spec.type === 'enum') { + if (!Array.isArray(spec.values) || spec.values.length === 0) { + this.addError('ERR_SCHEMA_VALIDATION', `${path}.values`, `Enum parameter must define a non-empty values array.`); + } else if (!spec.values.includes(spec.default)) { + this.addError('ERR_OUT_OF_BOUNDS', `${path}.default`, `Enum default '${spec.default}' is not in values list.`); + } + } + } + } + + validateState() { + if (!this.doc.state) return; + if (typeof this.doc.state !== 'object' || Array.isArray(this.doc.state)) { + this.addError('ERR_SCHEMA_VALIDATION', '$.state', 'state must be an object.'); + return; + } + + for (const [id, spec] of Object.entries(this.doc.state)) { + const path = `$.state.${id}`; + if (!ID_REGEX.test(id)) { + this.addError('ERR_INVALID_ID', path, `State ID '${id}' must match ^[a-z][a-z0-9_-]*$.`); + } + if (typeof spec !== 'object' || spec === null || Array.isArray(spec)) { + this.addError('ERR_SCHEMA_VALIDATION', path, 'StateSpec must be an object.'); + continue; + } + + for (const key of Object.keys(spec)) { + if (!ALLOWED_STATE_KEYS.has(key)) { + this.addError('ERR_UNKNOWN_FIELD', `${path}.${key}`, `Unrecognized field in StateSpec: '${key}'.`); + } + } + + const validTypes = ['number', 'integer', 'boolean', 'string']; + if (!validTypes.includes(spec.type)) { + this.addError('ERR_TYPE_MISMATCH', `${path}.type`, `Invalid state type: '${spec.type}'.`); + } + + if (spec.initial === undefined) { + this.addError('ERR_SCHEMA_VALIDATION', `${path}.initial`, `Missing required field: initial.`); + } else { + this.checkTypeMatches(spec.type, spec.initial, `${path}.initial`); + } + } + } + + validateValueSpec(valueSpec, path) { + if (typeof valueSpec === 'number') { + if (!Number.isFinite(valueSpec)) { + this.addError('ERR_TYPE_MISMATCH', path, `Number must be finite; got ${valueSpec}.`); + } + return; + } + if (typeof valueSpec === 'boolean' || typeof valueSpec === 'string') { + return; + } + if (typeof valueSpec !== 'object' || valueSpec === null || Array.isArray(valueSpec)) { + this.addError('ERR_SCHEMA_VALIDATION', path, `Invalid ValueSpec format.`); + return; + } + + // Reference + if ('ref' in valueSpec) { + if (typeof valueSpec.ref !== 'string' || !REF_PATH_REGEX.test(valueSpec.ref)) { + this.addError('ERR_INVALID_REFERENCE', `${path}.ref`, `Invalid reference path: '${valueSpec.ref}'.`); + } else { + this.resolveReference(valueSpec.ref, `${path}.ref`); + } + return; + } + + // Random Range + if ('random' in valueSpec) { + const { random } = valueSpec; + if (typeof random !== 'object' || random === null) { + this.addError('ERR_SCHEMA_VALIDATION', `${path}.random`, 'random must be an object.'); + return; + } + if (typeof random.min !== 'number' || typeof random.max !== 'number') { + this.addError('ERR_SCHEMA_VALIDATION', `${path}.random`, 'random requires finite numeric min and max.'); + } else if (random.min > random.max) { + this.addError('ERR_OUT_OF_BOUNDS', `${path}.random`, `random min (${random.min}) cannot exceed max (${random.max}).`); + } + return; + } + + // Weighted Choice + if ('choose' in valueSpec) { + if (!Array.isArray(valueSpec.choose) || valueSpec.choose.length === 0) { + this.addError('ERR_SCHEMA_VALIDATION', `${path}.choose`, 'choose must be a non-empty array.'); + return; + } + for (let i = 0; i < valueSpec.choose.length; i++) { + const item = valueSpec.choose[i]; + if (typeof item !== 'object' || item === null || item.value === undefined || typeof item.weight !== 'number' || item.weight <= 0) { + this.addError('ERR_SCHEMA_VALIDATION', `${path}.choose[${i}]`, 'choose item requires value and weight > 0.'); + } else { + this.validateValueSpec(item.value, `${path}.choose[${i}].value`); + } + } + return; + } + + // Calculation Operator + if ('op' in valueSpec) { + const { op, args } = valueSpec; + if (!OPERATOR_ARITY[op]) { + this.addError('ERR_INVALID_OPERATOR', `${path}.op`, `Unrecognized operator: '${op}'.`); + return; + } + const expectedArity = OPERATOR_ARITY[op]; + if (!Array.isArray(args)) { + this.addError('ERR_SCHEMA_VALIDATION', `${path}.args`, `args must be an array for op '${op}'.`); + return; + } + if (args.length !== expectedArity) { + this.addError( + 'ERR_INVALID_ARITY', + `${path}.args`, + `Operator '${op}' requires exactly ${expectedArity} argument(s); got ${args.length}.` + ); + } + args.forEach((arg, index) => { + this.validateValueSpec(arg, `${path}.args[${index}]`); + }); + return; + } + + this.addError('ERR_SCHEMA_VALIDATION', path, `Unrecognized ValueSpec structure.`); + } + + validateConditionSpec(cond, path) { + if (typeof cond !== 'object' || cond === null || Array.isArray(cond)) { + this.addError('ERR_SCHEMA_VALIDATION', path, 'ConditionSpec must be an object.'); + return; + } + + if ('op' in cond) { + if (!COMPARISON_OPS.has(cond.op)) { + this.addError('ERR_INVALID_OPERATOR', `${path}.op`, `Invalid comparison operator: '${cond.op}'.`); + } + if (cond.left === undefined || cond.right === undefined) { + this.addError('ERR_SCHEMA_VALIDATION', path, `Comparison condition requires 'left' and 'right'.`); + } else { + this.validateValueSpec(cond.left, `${path}.left`); + this.validateValueSpec(cond.right, `${path}.right`); + } + return; + } + + if ('and' in cond) { + if (!Array.isArray(cond.and) || cond.and.length === 0) { + this.addError('ERR_SCHEMA_VALIDATION', `${path}.and`, `'and' condition requires non-empty array.`); + } else { + cond.and.forEach((c, idx) => this.validateConditionSpec(c, `${path}.and[${idx}]`)); + } + return; + } + + if ('or' in cond) { + if (!Array.isArray(cond.or) || cond.or.length === 0) { + this.addError('ERR_SCHEMA_VALIDATION', `${path}.or`, `'or' condition requires non-empty array.`); + } else { + cond.or.forEach((c, idx) => this.validateConditionSpec(c, `${path}.or[${idx}]`)); + } + return; + } + + if ('not' in cond) { + this.validateConditionSpec(cond.not, `${path}.not`); + return; + } + + this.addError('ERR_SCHEMA_VALIDATION', path, `Unrecognized ConditionSpec structure.`); + } + + validateBindings() { + if (!this.doc.bindings) return; + if (!Array.isArray(this.doc.bindings)) { + this.addError('ERR_SCHEMA_VALIDATION', '$.bindings', 'bindings must be an array.'); + return; + } + + const graph = new Map(); // target -> [sources] + + this.doc.bindings.forEach((binding, idx) => { + const path = `$.bindings[${idx}]`; + if (typeof binding !== 'object' || binding === null) { + this.addError('ERR_SCHEMA_VALIDATION', path, 'Binding must be an object.'); + return; + } + + if (typeof binding.from !== 'string' || !REF_PATH_REGEX.test(binding.from)) { + this.addError('ERR_INVALID_REFERENCE', `${path}.from`, `Invalid from reference: '${binding.from}'.`); + } else { + this.resolveReference(binding.from, `${path}.from`); + } + + if (typeof binding.to !== 'string' || !REF_PATH_REGEX.test(binding.to)) { + this.addError('ERR_INVALID_REFERENCE', `${path}.to`, `Invalid to reference: '${binding.to}'.`); + } + + if (binding.transform) { + this.validateValueSpec(binding.transform, `${path}.transform`); + } + + if (binding.smoothing) { + if (typeof binding.smoothing !== 'string' || !DURATION_REGEX.test(binding.smoothing)) { + this.addError('ERR_INVALID_DURATION', `${path}.smoothing`, `Invalid duration: '${binding.smoothing}'.`); + } + } + + if (typeof binding.from === 'string' && typeof binding.to === 'string') { + if (!graph.has(binding.to)) graph.set(binding.to, []); + graph.get(binding.to).push(binding.from); + } + }); + + // Cycle detection using DFS + const visited = new Set(); + const visiting = new Set(); + + const checkCycle = (node, chain) => { + if (visiting.has(node)) { + this.addError( + 'ERR_CYCLIC_DEPENDENCY', + '$.bindings', + `Dependency cycle detected: ${chain.concat(node).join(' -> ')}` + ); + return; + } + if (visited.has(node)) return; + + visiting.add(node); + const dependencies = graph.get(node) || []; + for (const dep of dependencies) { + checkCycle(dep, chain.concat(node)); + } + visiting.delete(node); + visited.add(node); + }; + + for (const node of graph.keys()) { + if (!visited.has(node)) { + checkCycle(node, []); + } + } + } + + resolveReference(refPath, location) { + const parts = refPath.split('.'); + const namespace = parts[0]; + + if (namespace === 'parameters') { + const paramId = parts[1]; + if (!this.doc.parameters || !this.doc.parameters[paramId]) { + this.addError( + 'ERR_INVALID_REFERENCE', + location, + `Reference '${refPath}' refers to non-existent parameter '${paramId}'.` + ); + } + } else if (namespace === 'state') { + const stateId = parts[1]; + if (!this.doc.state || !this.doc.state[stateId]) { + this.addError( + 'ERR_INVALID_REFERENCE', + location, + `Reference '${refPath}' refers to non-existent state variable '${stateId}'.` + ); + } + } + } + + checkTypeMatches(expectedType, value, path) { + if (expectedType === 'number') { + if (typeof value !== 'number' || !Number.isFinite(value)) { + this.addError('ERR_TYPE_MISMATCH', path, `Expected finite number; got ${JSON.stringify(value)}.`); + } + } else if (expectedType === 'integer') { + if (!Number.isInteger(value)) { + this.addError('ERR_TYPE_MISMATCH', path, `Expected integer; got ${JSON.stringify(value)}.`); + } + } else if (expectedType === 'boolean') { + if (typeof value !== 'boolean') { + this.addError('ERR_TYPE_MISMATCH', path, `Expected boolean; got ${JSON.stringify(value)}.`); + } + } else if (expectedType === 'string') { + if (typeof value !== 'string') { + this.addError('ERR_TYPE_MISMATCH', path, `Expected string; got ${JSON.stringify(value)}.`); + } + } else if (expectedType === 'color') { + if (typeof value !== 'string' || (!value.startsWith('#') && value.trim().length === 0)) { + this.addError('ERR_TYPE_MISMATCH', path, `Expected valid color string; got ${JSON.stringify(value)}.`); + } + } else if (expectedType === 'enum') { + if (typeof value !== 'string') { + this.addError('ERR_TYPE_MISMATCH', path, `Expected enum string; got ${JSON.stringify(value)}.`); + } + } + } + + getResult() { + return { + valid: this.errors.length === 0, + filename: this.filename, + errorCount: this.errors.length, + errors: this.errors + }; + } +} + +// CLI runner +if (import.meta.url.endsWith(process.argv[1]) || process.argv[1]?.endsWith('validate-exhibit.mjs')) { + const args = process.argv.slice(2); + const jsonMode = args.includes('--json'); + const files = args.filter((arg) => arg !== '--json'); + + if (files.length === 0) { + console.error('Usage: node tools/validate-exhibit.mjs [--json] '); + process.exit(2); + } + + let allPassed = true; + const results = []; + + for (const filePath of files) { + try { + const content = readFileSync(resolve(filePath), 'utf-8'); + const doc = JSON.parse(content); + const validator = new ExhibitValidator(doc, filePath); + const result = validator.validate(); + results.push(result); + if (!result.valid) allPassed = false; + } catch (err) { + allPassed = false; + results.push({ + valid: false, + filename: filePath, + errorCount: 1, + errors: [{ code: 'ERR_SCHEMA_VALIDATION', path: '$', message: err.message }] + }); + } + } + + if (jsonMode) { + console.log(JSON.stringify(results, null, 2)); + } else { + for (const res of results) { + if (res.valid) { + console.log(`[PASS] ${res.filename} (0 errors)`); + } else { + console.error(`[FAIL] ${res.filename} (${res.errorCount} error${res.errorCount === 1 ? '' : 's'}):`); + for (const err of res.errors) { + console.error(` - [${err.code}] ${err.path}: ${err.message}`); + } + } + } + } + + process.exit(allPassed ? 0 : 1); +}