feat(phase0): complete shared contracts and milestone plan
This commit is contained in:
@@ -8,11 +8,12 @@ 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 | 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 |
|
||||
| [MVP Product Requirements Document](XZBT_0-1_MVP_Product_Requirements_Document.md) | Product scope, user behavior, delivery requirements, and release acceptance | Document revision 0.3; format version remains 0.1 |
|
||||
| [Format Specification 0.1](docs/XZBT_0-1_Format_Specification.md) | Runtime semantics, contract inventory, and required authoring examples | Document revision 0.3; Phase 0 shared 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 | Record revision 0.3; GC1–GC5 Phase 0 evidence complete |
|
||||
| [Verification Gates](docs/XZBT_0-1_Verification_Gates.md) | Evidence required before architecture commitment, subsystem work, and release | Phase 0 complete; later subsystem, GC6, and GC7 checks scheduled |
|
||||
| [Implementation status](docs/IMPLEMENTATION_STATUS.md) | Current phase, stop reason, saved work, and resume prerequisites | Phase 0 complete; Phase 1 runtime skeleton ready |
|
||||
| [Implementation plan](docs/XZBT_0-1_Implementation_Plan.md) | Sequenced phases, completion/challenge mapping, and GC6/GC7 verification schedule | Phase 1–9 plan recorded |
|
||||
|
||||
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.
|
||||
|
||||
@@ -20,11 +21,11 @@ The earlier ChatGPT discussion, **Discuss Application Vision** (conversation `6a
|
||||
|
||||
## Planning entry point
|
||||
|
||||
Begin the implementation plan with Phase 0 from the verification gates. Complete the launch-model prototype and shared semantic contracts before committing dependent architecture and detailed estimates. Carry workload measurements and release soak tests as later explicit gates; they are not prerequisites for drafting a plan.
|
||||
Phase 0 is complete. Begin implementation with Phase 1 from the implementation plan, preserving the verified launch model and shared semantic contracts. Carry workload measurements and release soak tests as later explicit gates; they are not prerequisites for the runtime skeleton.
|
||||
|
||||
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 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.
|
||||
Phase 0 is complete. All 10 direct-file checks in GC1 are verified, including worklet loading, full-browser restart persistence, offline behavior, and directory fallback. GC2 shared format/schema validation passes its 12-case matrix. Format Specification Revision 0.3 closes GC3 resolution, GC4 clock/PRNG, and GC5 ownership/failure contracts with deterministic executable traces; see the [combined evidence](docs/evidence/phase0/2026-09-05-gc3-gc5-contracts.md). The implementation plan schedules GC6 measurements and maps GC7 library/build verification. Phase 1 may begin; later production, browser/audio, benchmark, and soak gates remain explicit.
|
||||
|
||||
## Repository configuration
|
||||
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
# XZBT 0.1 MVP Product Requirements Document
|
||||
|
||||
**Product:** XZBT
|
||||
**Document version:** 0.2
|
||||
**Document version:** 0.3
|
||||
**XZBT format version:** 0.1
|
||||
**Status:** MVP specification
|
||||
**Date:** September 4, 2026
|
||||
**Date:** September 5, 2026
|
||||
|
||||
**Revision 0.2:** Incorporates the seven gap-closure decisions from pre-implementation review. The XZBT format version and full MVP completion scope remain 0.1. Decisions are recorded; browser feasibility, conformance, performance, and soak verification remain pending.
|
||||
**Revision 0.3:** Preserves the full 0.1 MVP scope and incorporates completed Phase 0 GC1–GC5 evidence, exact shared binding/clock/ownership contracts, and the GC6/GC7 implementation and verification map. Production subsystem, performance, and soak verification remain pending.
|
||||
|
||||
Companion resources:
|
||||
|
||||
- [Planning resource index](README.md)
|
||||
- [Format Specification 0.1](docs/XZBT_0-1_Format_Specification.md): shared runtime semantics and the incomplete contract register
|
||||
- [Format Specification 0.1](docs/XZBT_0-1_Format_Specification.md): shared runtime semantics and subsystem contract register
|
||||
- [Implementation Plan](docs/XZBT_0-1_Implementation_Plan.md): sequenced milestones and verification stops
|
||||
- [Gap Closure Decisions](docs/XZBT_0-1_Gap_Closure_Decisions.md): numbered decisions and rationale
|
||||
- [Verification Gates](docs/XZBT_0-1_Verification_Gates.md): Phase 0 prerequisites and later implementation/release evidence
|
||||
|
||||
@@ -506,7 +507,9 @@ or:
|
||||
}
|
||||
```
|
||||
|
||||
A numeric seed must reproduce procedural decisions for the same runtime version and logical input sequence. Record the resolved numeric seed when `"random"` is requested. Isolate random streams for cadence, scenarios, visual systems, sound instances, and manual SAMPLE playback. Rendering frequency and manual sampling must not perturb unrelated choices. Pixel-identical rendering or sample-identical audio across devices is not promised; deterministic tests supply recorded or synthetic analysis signals where required.
|
||||
A numeric seed must reproduce procedural decisions for the same runtime version and logical input sequence. Record the resolved numeric seed when `"random"` is requested. Isolate random streams for cadence, scenarios, visual systems, sound instances, and manual SAMPLE playback. Rendering frequency and manual sampling must not perturb unrelated choices. Pixel-identical rendering or sample-identical audio across devices is not promised; deterministic tests supply recorded or synthetic analysis signals where required.
|
||||
|
||||
The [format specification](docs/XZBT_0-1_Format_Specification.md) fixes 0.1 seed normalization, FNV-1a stream derivation, SplitMix32 state expansion, and `xoshiro128**` output. Numeric seeds are unsigned 32-bit integers; `"random"` uses cryptographic browser entropy and the resolved integer is recorded.
|
||||
|
||||
The exhibit may not disable engine safety systems.
|
||||
|
||||
@@ -1101,7 +1104,7 @@ Where multiple overrides compete for one target:
|
||||
|
||||
Scope determines lifetime, not precedence. Duration overrides do not automatically outrank scenario overrides. Bindings and automation continue evaluating while masked. A release transition approaches the currently resolved lower value, including user changes made during the override, rather than a stale snapshot.
|
||||
|
||||
Reject conflicting ordinary bindings and unsupported dependency cycles. Exact target capabilities, priority defaults, smoothing, and interrupted transition rules must be completed in the [format specification](docs/XZBT_0-1_Format_Specification.md) before resolver implementation.
|
||||
Reject conflicting ordinary bindings and unsupported dependency cycles. Exact target capabilities, priority defaults, smoothing, and interrupted transition rules are defined in the [format specification](docs/XZBT_0-1_Format_Specification.md) and verified by the GC3 contract traces.
|
||||
|
||||
---
|
||||
|
||||
@@ -1146,13 +1149,13 @@ smoothing
|
||||
when
|
||||
```
|
||||
|
||||
Defaults:
|
||||
|
||||
```text
|
||||
scale = 1
|
||||
offset = 0
|
||||
smoothing = engine default
|
||||
```
|
||||
Defaults:
|
||||
|
||||
```text
|
||||
scale = 1
|
||||
offset = 0
|
||||
smoothing = 0ms
|
||||
```
|
||||
|
||||
Numeric transformation:
|
||||
|
||||
@@ -1163,9 +1166,11 @@ clamp(
|
||||
)
|
||||
```
|
||||
|
||||
Multiple ordinary bindings may not independently write the same scalar target in 0.1 unless that target explicitly supports additive input.
|
||||
|
||||
This avoids ambiguous precedence.
|
||||
Multiple ordinary bindings may not independently write the same scalar target in 0.1 unless that target explicitly supports additive input.
|
||||
|
||||
This avoids ambiguous precedence.
|
||||
|
||||
The [format specification](docs/XZBT_0-1_Format_Specification.md) is normative for the target-capability registry, same-tick topological evaluation, conditional disabling, exact one-pole smoothing equation, override instance ordering, and interrupted transition behavior. The canonical binding fields are the fields shown here; planning-only `from`/`to`/`transform` spellings are not aliases.
|
||||
|
||||
---
|
||||
|
||||
@@ -2952,7 +2957,9 @@ replace
|
||||
|
||||
`replace` requires higher priority than the scenario being replaced.
|
||||
|
||||
Each scenario definition may have at most one pending deferred start request. Requests expire and recheck eligibility at dispatch. Exact expiry defaults and queue ordering are required format contracts before implementation.
|
||||
Each scenario definition may have at most one pending deferred start request. Requests expire and recheck eligibility at dispatch. The format specification defines exact expiry defaults and queue ordering.
|
||||
|
||||
For 0.1, the default expiry is five minutes of logical time. Queue order is priority descending, creation tick ascending, then scenario document order. New opportunities do not duplicate or extend an existing request; all start constraints are rechecked immediately before dispatch as specified in the format contract.
|
||||
|
||||
---
|
||||
|
||||
@@ -2997,7 +3004,9 @@ When a scenario ends, XZBT must:
|
||||
|
||||
A scenario must be safe to run repeatedly during multi-hour sessions.
|
||||
|
||||
Termination first stops ordinary dispatch and cancels future work, then runs the applicable termination hook and guarantees cleanup even if that hook fails. A bounded cleanup owner may finish release fades before disposal. Hook ordering, allowed hook actions, and cleanup deadlines must be finalized in the format contract; this requirement does not introduce an undeclared failure-hook field.
|
||||
Termination first stops ordinary dispatch and cancels future work, then runs the applicable termination hook and guarantees cleanup even if that hook fails. A bounded cleanup owner may finish release fades before disposal. The format contract defines hook ordering, allowed hook actions, and cleanup deadlines; this requirement does not introduce an undeclared failure-hook field.
|
||||
|
||||
The finalized 0.1 contract uses `onComplete` for natural completion and `onCancel` for cancellation, replacement, deactivation, and critical failure; no `onFailure` field exists. Termination hooks permit only persistent `set` mutations and nonpersistent one-shot sounds. Temporary releases are capped at five logical seconds before forced disposal.
|
||||
|
||||
---
|
||||
|
||||
@@ -3015,7 +3024,7 @@ It must not implement long scenarios as thousands of unrelated `setTimeout()` ca
|
||||
|
||||
The runtime uses a shared logical simulation clock, initially stepping at 1/60 second, independently of rendering. Audio scheduling maps logical time to the audio clock with a bounded scheduling horizon. Tick ordering, long-stall handling, and audio synchronization are explicit contracts to complete under GC4.
|
||||
|
||||
Required in 0.1:
|
||||
Required in 0.1:
|
||||
|
||||
```text
|
||||
pause
|
||||
@@ -3030,7 +3039,9 @@ timeline inspection
|
||||
developer seeking
|
||||
```
|
||||
|
||||
Inspection and seeking need not be user-facing in MVP. Pause/resume and accelerated testing are required by the corresponding acceptance criteria.
|
||||
Inspection and seeking need not be user-facing in MVP. Pause/resume and accelerated testing are required by the corresponding acceptance criteria.
|
||||
|
||||
The fixed-step driver executes at most eight logical ticks per browser turn, accepts at most 250 ms from one wall-time observation, and uses a 100 ms audio lookahead. Pause reasons compose, wall-clock stalls are never replayed, and audio unlock skips earlier one-shots while aligning still-owned continuous sounds at the current logical position. The format specification contains the normative tick and mapping order.
|
||||
|
||||
---
|
||||
|
||||
@@ -3860,7 +3871,7 @@ Use the [verification gates](docs/XZBT_0-1_Verification_Gates.md) before committ
|
||||
- schedule early combined visual/audio workload measurements before fixing expensive subsystem choices (GC6)
|
||||
- map integrated reference-exhibit milestones and library/build behavior (GC7)
|
||||
|
||||
The implementation plan may be drafted now with these prerequisites explicit. Complete remaining subsystem contracts before their implementation. Full conformance and two/eight-hour soaks are later development/release gates, not prerequisites for drafting the plan. All verification is currently pending.
|
||||
The [implementation plan](docs/XZBT_0-1_Implementation_Plan.md) maps these prerequisites, completion criteria, reference challenges, early combined-load measurements, and later soak gates. Complete remaining subsystem contracts before their implementation. Full conformance and two/eight-hour soaks are later development/release gates, not Phase 0 completion requirements. Phase 0 evidence is tracked in the verification gates and linked evidence records.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
# XZBT implementation status
|
||||
|
||||
**Updated:** September 5, 2026
|
||||
**State:** Phase 0 in progress (GC1 and GC2 shared contracts complete; GC3–GC5 in progress before Phase 1)
|
||||
**State:** Phase 0 complete; ready to begin Phase 1 runtime skeleton
|
||||
**Planning baseline:** `05fe2b4e021ba86e4a290d05b63c7cae0e386128`
|
||||
|
||||
**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.
|
||||
**Exact demarcation:** GC1 direct-file feasibility (10/10 checks), GC2 shared format contracts, GC3 resolution semantics, GC4 clock/PRNG semantics, and GC5 ownership/failure semantics are complete at the Phase 0 contract-oracle level. The implementation plan maps GC6 measurements and GC7 completion/library/build checks to later milestones. No production runtime work has begun; real subsystem, Web Audio synchronization, performance, and soak evidence remain assigned to Phases 1–9.
|
||||
|
||||
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 | 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 |
|
||||
| 0 — Contracts and feasibility | Complete | GC1 passed; GC2–GC5 shared contracts and traces passed; GC6/GC7 later gates scheduled and mapped |
|
||||
| 1 — Runtime skeleton | Ready | Follow the [implementation plan](XZBT_0-1_Implementation_Plan.md) and preserve Phase 0 contract oracles |
|
||||
| 2 — Common grammar | Not started | Complete Phase 1 and shared contracts |
|
||||
| 3 — Audio engine | Not started | Earlier phases and audio contracts |
|
||||
| 4 — Visual engine | Not started | Earlier phases and visual contracts |
|
||||
@@ -28,9 +28,12 @@ The user requested sequential implementation with a stop on problems. The [manua
|
||||
- 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.
|
||||
- Format Specification Revision 0.3 with exact shared GC3 resolution/binding/transition, GC4 clock/PRNG/audio-alignment, GC5 ownership/failure/dispatch, and GC7 import/update/build contracts.
|
||||
- Executable Phase 0 contract models and tests for GC3–GC5; the combined Phase 0 run passes 27 tests with zero failures.
|
||||
- A sequenced [implementation plan](XZBT_0-1_Implementation_Plan.md) mapping every MVP completion group, reference challenge, GC6 measurement stage, and GC7 library/build verification stop.
|
||||
- Preserved user evidence runs for versions 1, 2, 3, offline, file relocation, and directory fallback.
|
||||
|
||||
## Resume condition
|
||||
## Phase 1 entry 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.
|
||||
|
||||
@@ -40,4 +43,4 @@ The browser rejection explicitly forbids agent workarounds through alternate bro
|
||||
|
||||
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.
|
||||
Phase 0 is complete. Begin Phase 1 with the runtime skeleton and reproducible modular-source build described in the implementation plan. Reuse the GC4 PRNG known vectors in the production `SeededRNG`; do not reinterpret the frozen GC3–GC5 contracts silently. The mandatory combined-load measurements, production subsystem conformance, two-hour development soak, and eight-hour release soak remain outstanding at their mapped later gates.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# XZBT Format Specification 0.1
|
||||
|
||||
**XZBT format version:** 0.1
|
||||
**Document revision:** 0.2
|
||||
**Status:** Normative specification for shared contracts (document, types, ValueSpec, ConditionSpec, resolution); subsystem contracts in progress
|
||||
**Document revision:** 0.3
|
||||
**Status:** Normative specification for shared contracts (document, types, ValueSpec, ConditionSpec, resolution, bindings, and transitions); 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 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.
|
||||
@@ -273,8 +273,17 @@ To ensure consistent error reporting between structural schema validation, seman
|
||||
| `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. |
|
||||
| `ERR_UNSUPPORTED_TARGET` | Semantic / Runtime | An action, binding, or resolution stage addresses a target that does not expose that operation. |
|
||||
| `ERR_CONFLICTING_BINDING` | Semantic | More than one enabled ordinary binding writes a scalar target. |
|
||||
| `ERR_INVALID_TRANSITION` | Semantic / Runtime | A transition is incompatible with its target, easing, scope, or duration. |
|
||||
| `ERR_DISPATCH_BUDGET` | Runtime | Ordinary event/action work exceeded the per-tick dispatch budget. |
|
||||
| `WARN_CLOCK_STALL` | Runtime | Elapsed wall time or accumulated work exceeded the fixed-step per-turn limits and was discarded. |
|
||||
| `WARN_CLEANUP_FORCED` | Runtime | A cleanup owner reached its deadline and force-disposed remaining resources. |
|
||||
| `INFO_AUDIO_UNLOCK_SKIP` | Runtime | One or more pre-unlock one-shots were intentionally not replayed. |
|
||||
|
||||
## 8. Shared value resolution
|
||||
## 8. Shared value resolution, bindings, and transitions
|
||||
|
||||
### 8.1 Resolution pipeline and target capabilities
|
||||
|
||||
For each supported target, evaluate:
|
||||
|
||||
@@ -282,48 +291,202 @@ For each supported target, evaluate:
|
||||
base -> binding -> automation -> winning override -> modulation -> safety clamp
|
||||
```
|
||||
|
||||
Skip stages not exposed by the target contract. A target cannot accept automation or modulation merely because it is numeric. Additive modulation is summed only where explicitly supported.
|
||||
Stages that the target does not expose are absent, not identity hooks available to authors. The shared 0.1 registry is:
|
||||
|
||||
Parameters store user configuration separately from exhibit defaults. State stores simulation values separately from parameters. A property may obtain its base through ValueSpec. State is not an implicit layer overwriting every parameter.
|
||||
| Target family | Type and base | Binding target | Automation | Override | Additive modulation | Safety clamp |
|
||||
| --- | --- | :---: | :---: | :---: | :---: | --- |
|
||||
| `parameters.<id>` | Declared primitive; stored user value | Yes | No | Yes | No | Declared `min`/`max`; integer targets round to the nearest integer after interpolation and before clamping |
|
||||
| `state.<id>` | Declared primitive; current state value | Yes | No | Yes | No | Declared `min`/`max` |
|
||||
| `audio.buses.<id>.gain` | Number; bus `gain` ValueSpec | Yes | Yes | Yes | Yes | `[0, 4]` in 0.1 before master output protection |
|
||||
|
||||
References ordinarily read resolved values. Parameter controls read and edit stored user values, and display an override indicator when appropriate. Underlying bindings and automation continue to evaluate while masked by an override.
|
||||
`signals.*` and `modulators.*` are read-only sources and never binding or action targets. `instances.*` is a runtime action-addressing namespace, not a ValueSpec or binding namespace. Sound inputs are sampled at sound invocation and cannot be ordinary binding targets. Visual properties, audio recipe-instance properties, and any target family absent from this table remain unsupported until their subsystem contract adds an explicit capability row. Merely being numeric does not grant automation, override, or modulation support.
|
||||
|
||||
Override lifetime and priority are independent. Use explicit priority when supported, otherwise inherit the originating scenario's priority; equal priorities resolve by activation order. Duration scope confers no additional priority. The complete action contract must define non-scenario default priority, permitted explicit priority fields, and ordering identifiers before implementation.
|
||||
Parameters store user configuration separately from exhibit defaults. State stores simulation values separately from parameters. State is not an implicit layer above user configuration. References read the value resolved for the current logical tick. Parameter controls read and edit stored user values and show an override indicator whenever any live override exists for the parameter, including a masked or releasing override.
|
||||
|
||||
On release, blend toward the current lower resolved value rather than a snapshot taken when the override started. User edits and changing bindings remain visible to that lower evaluation. Numeric release interpolation, interruptions by another override, and nonnumeric release behavior require exact contracts below.
|
||||
Within one logical tick, resolve the directed dependency graph in topological order. Each source reference observes the source's resolved value for that same tick. Dependency cycles are `ERR_CYCLIC_DEPENDENCY`; the runtime must not insert an undocumented previous-tick delay.
|
||||
|
||||
Reject conflicting ordinary bindings and dependency cycles that cannot be evaluated under documented semantics. Do not introduce an implicit previous-frame delay to make a cycle appear legal.
|
||||
### 8.2 BindingSpec
|
||||
|
||||
### Required resolution examples
|
||||
The canonical fields are `source`, `target`, `scale`, `offset`, `clamp`, `smoothing`, and `when`. Earlier planning-only spellings `from`, `to`, and `transform` are not 0.1 aliases and are rejected as `ERR_UNKNOWN_FIELD`.
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "state.machine-load",
|
||||
"target": "audio.buses.deep.gain",
|
||||
"scale": 0.5,
|
||||
"offset": 0.5,
|
||||
"clamp": [0, 1],
|
||||
"smoothing": "50ms",
|
||||
"when": {
|
||||
"op": "gt",
|
||||
"left": { "ref": "parameters.activity" },
|
||||
"right": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`source` and `target` are required reference paths. `scale` and `offset` are finite numbers with defaults `1` and `0`. `clamp`, when present, is a two-number inclusive `[minimum, maximum]` array with `minimum <= maximum`. `smoothing` is a DurationSpec and defaults to `"0ms"` (disabled). `when` is a ConditionSpec and defaults to `true`.
|
||||
|
||||
For numeric sources and targets, first calculate `clamp(source * scale + offset)`. A nonnumeric binding must omit `scale`, `offset`, `clamp`, and nonzero `smoothing`, and its source and target types must match exactly. A false `when` disables the binding for that tick and exposes the target's base stage. When a binding becomes enabled, its smoother initializes to the transformed source value; disabled time is not replayed.
|
||||
|
||||
Nonzero smoothing is the deterministic one-pole update below, evaluated once per fixed logical tick. `tau` is the authored smoothing duration in seconds, `dt` is the fixed logical step, `x` is the transformed and binding-clamped input, and `yPrevious` is the prior enabled tick's binding output:
|
||||
|
||||
```text
|
||||
alpha = 1 - exp(-dt / tau)
|
||||
y = yPrevious + alpha * (x - yPrevious)
|
||||
```
|
||||
|
||||
At most one ordinary binding may target a scalar property. Multiple writers are `ERR_CONFLICTING_BINDING` even when their `when` conditions appear mutually exclusive; conditional exclusivity is not a precedence mechanism. A binding to a missing or read-only target is `ERR_UNSUPPORTED_TARGET`.
|
||||
|
||||
### 8.3 Override action and precedence
|
||||
|
||||
An override action has required `type: "override"`, `target`, `value`, and `scope` fields. It may also contain common action fields plus `priority`, `duration`, and `transition`.
|
||||
|
||||
| Field | Contract |
|
||||
| --- | --- |
|
||||
| `target` | A target whose capability row permits override. |
|
||||
| `value` | ValueSpec sampled once when the override instance activates; result must match the target type. |
|
||||
| `scope` | `"scenario"` or `"duration"`. |
|
||||
| `priority` | Optional integer from `-1000` through `1000`. If omitted under a scenario, inherit the scenario instance priority; otherwise use `0`. |
|
||||
| `duration` | Required and greater than `0ms` for duration scope; forbidden for scenario scope. Time begins at activation, including while masked. |
|
||||
| `transition` | Optional OverrideTransitionSpec; defaults to zero-duration `in` and `out` with `linear` easing. |
|
||||
|
||||
Each activated override receives a monotonically increasing runtime `activationSequence` within the performance and a runtime ID `instances.override-<activationSequence>`. An author `id`, when supplied, identifies the action definition and does not replace the runtime ID. The live winner is the greatest tuple `(priority, activationSequence)`. Scope affects lifetime only: duration scope receives no precedence advantage over scenario scope.
|
||||
|
||||
Underlying binding and automation stages continue to evaluate while masked. Overrides also advance their own durations and envelopes while masked. When a winner changes, the newly winning override's attack begins from the target's current pre-modulation resolved value, preventing a discontinuity. If that override had already completed its attack while masked, it takes effect immediately at its sampled value.
|
||||
|
||||
### 8.4 TransitionSpec and interruption
|
||||
|
||||
A numeric `set` action accepts `{ "duration": DurationSpec, "easing": Easing }`, with defaults `"0ms"` and `"linear"`. An override accepts `{ "in": DurationSpec, "out": DurationSpec, "easing": Easing }`, with both durations defaulting to `"0ms"`. Easing is one of `linear`, `ease-in`, `ease-out`, or `ease-in-out`, defined for normalized `t` in `[0,1]`:
|
||||
|
||||
```text
|
||||
linear: t
|
||||
ease-in: t * t
|
||||
ease-out: 1 - (1 - t) * (1 - t)
|
||||
ease-in-out: 2 * t * t when t < 0.5
|
||||
1 - ((-2 * t + 2)^2) / 2 otherwise
|
||||
```
|
||||
|
||||
Zero duration is an immediate step. Non-numeric targets permit only zero-duration transitions; any nonzero duration is `ERR_INVALID_TRANSITION`. Numeric interpolation uses double precision. Integer targets round half away from zero after interpolation and then apply their declared clamp.
|
||||
|
||||
An attack interpolates from the pre-modulation value visible immediately before the override becomes winner to its sampled override value. Release begins when duration expires, its scenario owner terminates, or the override is explicitly removed. Release output on each tick is:
|
||||
|
||||
```text
|
||||
lerp(currentLowerValue, releaseStartValue, 1 - easing(elapsed / outDuration))
|
||||
```
|
||||
|
||||
`currentLowerValue` is recomputed on every tick without the releasing override, so stored user edits, bindings, and automation remain observable during release. `releaseStartValue` is the override's actual pre-modulation output when release began. A zero-duration release removes the override before that tick resolves.
|
||||
|
||||
A higher-priority interruption does not cancel a lower override. The lower override continues its lifetime and release while masked. If the higher override releases, the next live winner is selected and its current envelope value is used. Reactivating the same action creates a distinct override instance with a new activation sequence.
|
||||
|
||||
The safety clamp always runs after modulation. A constant override or binding value outside the target's legal range is `ERR_OUT_OF_BOUNDS` at validation; a dynamic result is clamped at runtime and emits a rate-limited `ERR_OUT_OF_BOUNDS` diagnostic rather than poisoning the graph.
|
||||
|
||||
### 8.5 Required resolution traces
|
||||
|
||||
The normative deterministic traces are stored with the GC3 tests. They cover:
|
||||
|
||||
| Case | Expected behavior |
|
||||
| --- | --- |
|
||||
| Bus gain is bound to activity, then directly overridden | The override supplies the pre-modulation value until release; the binding continues underneath |
|
||||
| Activity is overridden and referenced by a binding | The binding observes resolved activity |
|
||||
| User edits stored activity while its override is active | The stored edit persists; release approaches the updated lower value |
|
||||
| Two overrides compete | Higher priority wins; equal priority uses activation order |
|
||||
| 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 |
|
||||
| Bus gain is bound to activity, then directly overridden | Override supplies the pre-modulation value; the binding continues underneath and its current value becomes the release destination |
|
||||
| Activity is overridden and referenced by a binding | The binding observes resolved activity during the same logical tick |
|
||||
| User edits stored activity while its override is active | Stored edit persists; release approaches the updated lower value |
|
||||
| Two overrides compete | Higher priority wins; equal priority uses greater activation sequence |
|
||||
| Duration and scenario overrides compete | Priority and activation sequence decide, not scope |
|
||||
| A target has legal additive modulation | Modulation follows the winning override; safety clamp runs last |
|
||||
| Binding conditions, smoothing, clamps, and invalid graphs | Disabled binding exposes base, re-enable initializes smoothing, and conflicts/cycles are rejected |
|
||||
|
||||
## 9. Time and random evaluation
|
||||
## 9. Time, audio alignment, 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.
|
||||
### 9.1 Fixed logical clock
|
||||
|
||||
Application pause and document visibility loss suspend logical progression and audio. Resume continues the same logical performance without a wall-clock catch-up burst. Visibility resume does not clear a user pause. Audio unlock does not replay expired sound invocations.
|
||||
The 0.1 logical step is exactly `1000 / 60` milliseconds. Runtime time is represented by an integer `tickIndex`; logical seconds are `tickIndex / 60`. Rendering reads the most recently published state and never advances simulation time.
|
||||
|
||||
Random and weighted-choice ValueSpecs are sampled at the containing object's documented instantiation or invocation boundary, not every render frame. Evolving randomness belongs to modulators. Nested ValueSpec evaluation boundaries and random time sampling must be specified per construct.
|
||||
The foreground driver accumulates nonnegative elapsed monotonic time. One browser turn executes at most eight logical ticks. A single driver observation contributes at most `250ms`; excess elapsed time is discarded and emits one rate-limited `WARN_CLOCK_STALL` diagnostic. After eight ticks, the driver yields and retains at most one step of accumulator; additional accumulated time is discarded with the same diagnostic. These bounds prevent catch-up bursts. They are runtime responsiveness policy, not permission for accelerated tests to skip authored ticks.
|
||||
|
||||
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.
|
||||
Each logical tick uses this order:
|
||||
|
||||
## 10. Ownership and termination
|
||||
1. Increment `tickIndex` and latch queued user/external inputs in arrival-sequence order. Tests replace external inputs, including audio analysis, with deterministic fixtures.
|
||||
2. Advance time-based modulators and automation; resolve the dependency graph in topological order.
|
||||
3. Evaluate cadence, event conditions, and scenario timelines once against that tick's resolved snapshot; enqueue work using document order as the final tie-break.
|
||||
4. Drain ordinary event/action work up to the GC5 dispatch budget. Actions in one array execute in document order; a state mutation invalidates affected resolved values before the next action executes.
|
||||
5. Advance owned audio/visual instance state and perform mandatory cleanup, which is not charged to the ordinary dispatch budget.
|
||||
6. Schedule eligible audio work within the lookahead horizon, then publish the immutable state read by rendering and generated UI.
|
||||
|
||||
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.
|
||||
Application pause and document visibility are independent pause reasons. Logical progression and audio are paused while either reason is present. On any transition from paused to running, reset the monotonic wall-time anchor while preserving `tickIndex` and the sub-step accumulator. Do not add paused wall time or replay missed work. Removing the visibility reason does not remove an explicit application pause.
|
||||
|
||||
Persistent `set` changes survive scenario failure. Termination cancels future work and guarantees cleanup, including when a termination hook fails. Release work transfers to a bounded cleanup owner and ultimately disposes all temporary resources.
|
||||
### 9.2 Audio clock mapping and unlock
|
||||
|
||||
Condition triggers require a false condition before rearming after a successful firing. Each scenario definition may hold at most one deferred start request. Requests expire and recheck eligibility when dispatched. Exact timeout and ordering contracts remain required.
|
||||
The initial audio scheduling lookahead is `100ms`, refilled after every logical tick. Maintain an anchor pair `(logicalSeconds, audioContext.currentTime)` and map an eligible logical start to the audio clock relative to that pair. On audio unlock or resume, establish a fresh anchor at the current logical position. Engine-owned future starts from an obsolete anchor are cancelled and rescheduled where the Web Audio node contract permits it.
|
||||
|
||||
Statically check event/scenario feedback where possible and bound runtime dispatch. Resource cleanup must remain possible after the ordinary dispatch budget is exhausted.
|
||||
If audio is locked, logical sound invocations still receive their deterministic IDs, ownership, sampled values, and nominal logical intervals, but create no Web Audio nodes. At unlock:
|
||||
|
||||
- a one-shot whose logical start is earlier than the unlock position is skipped, whether expired or partially elapsed; emit one rate-limited `INFO_AUDIO_UNLOCK_SKIP` count rather than replaying it;
|
||||
- a continuous sound that is still logically owned starts at its current logical phase/state, not from its original attack, and applies an engine-owned `20ms` anti-click ramp from silence to its current gain;
|
||||
- future invocations inside the new lookahead are scheduled normally.
|
||||
|
||||
Pause or visibility loss suspends the AudioContext after cancelling engine-owned future starts. Resume re-anchors and refills the horizon. Accelerated logical tests validate scheduling decisions only; audible behavior, anti-click ramps, and real-time soak acceptance require browser/audio observations.
|
||||
|
||||
### 9.3 Seed normalization and PRNG
|
||||
|
||||
An authored numeric seed must be an integer in `[0, 4294967295]` and is normalized with unsigned 32-bit semantics. For `"random"`, obtain one unsigned 32-bit integer from `crypto.getRandomValues`, store it with the performance record, and use that recorded integer for every subsequent derivation. Failure to obtain entropy is a startup error; time or `Math.random()` is not a fallback.
|
||||
|
||||
XZBT 0.1 uses `xoshiro128**` with unsigned 32-bit arithmetic. A stream seed is FNV-1a-32 over the UTF-8 bytes of:
|
||||
|
||||
```text
|
||||
xzbt-0.1\0<root-seed-decimal>\0<domain>\0<stable-instance-key>
|
||||
```
|
||||
|
||||
Expand that hash into four words with SplitMix32. If all four words are zero, set the fourth word to `1`. Each sample converts the next unsigned result to `[0,1)` by division by `4294967296`. Integer ranges use unbiased rejection sampling; weighted choices consume one sample and select by cumulative positive weight in document order.
|
||||
|
||||
The reserved domains are `cadence`, `scenario`, `visual`, `sound`, and `manual-sample`. Stable instance keys use the declared definition ID plus that definition's monotonically increasing invocation ordinal within its domain. A subsystem may add a documented child key but may not draw from another domain. Rendering consumes no procedural stream. Manual SAMPLE always uses `manual-sample`; therefore it cannot perturb cadence, scenario, visual, or automatic sound choices.
|
||||
|
||||
Random and weighted-choice ValueSpecs are sampled once at the containing object's documented instantiation or invocation boundary. Nested random ValueSpecs use the same owning stream in depth-first, property-document order. Evolving randomness belongs to modulators and consumes only the owning modulator/visual stream at logical ticks.
|
||||
|
||||
Reproducibility means identical procedural decisions for the same XZBT runtime version, normalized seed, stable exhibit definition, and logical input sequence. It does not promise identical pixels, floating-point audio samples, or browser timing across devices.
|
||||
|
||||
## 10. Ownership, failure, and bounded dispatch
|
||||
|
||||
### 10.1 Ownership propagation
|
||||
|
||||
Every action dispatch carries an ownership context: a scenario instance owner or the performance root. An event action passes the same context to the invoked event; nesting never resets ownership. Sound, spawn, subscription, and override resources inherit that context unless their resource contract permits `ownership: "persistent"` and the action explicitly requests it. Persistent resources transfer to the performance root. Unsupported persistent ownership is `ERR_UNSUPPORTED_TARGET`.
|
||||
|
||||
A scenario instance owns its future timeline records, repeats, relative actions, subscriptions, scenario-scope overrides, duration overrides it creates, and nonpersistent continuous audio/visual resources, including resources created indirectly through nested events. A duration override owned by a scenario begins release at its authored deadline or owner termination, whichever occurs first.
|
||||
|
||||
`set` changes are persistent mutations, not owned resources. A successfully executed `set` is never rolled back because a later action, hook, or scenario fails.
|
||||
|
||||
### 10.2 Hook order and allowed termination work
|
||||
|
||||
`onStart` runs after the instance owner and deterministic streams exist but before timeline activation. It may use the full Action Model. A critical `onStart` failure cancels remaining start actions, prevents timeline activation, and terminates the instance as `FAILED`.
|
||||
|
||||
Termination first blocks new ordinary dispatch for the owner and cancels its future timeline/repeat/relative work. Exactly one termination hook then runs:
|
||||
|
||||
| Cause | Hook | Terminal state after cleanup |
|
||||
| --- | --- | --- |
|
||||
| Natural duration/timeline completion | `onComplete` | `COMPLETED` |
|
||||
| Manual cancel, replacement, exhibit deactivation | `onCancel` | `CANCELLED` |
|
||||
| Critical startup, ordinary action, or nested-event failure | `onCancel` | `FAILED` |
|
||||
|
||||
Termination hooks execute their actions in document order and may contain only `set` and one-shot `sound` actions. Termination sounds are owned by the cleanup owner and may not request persistent ownership. `override`, `event`, `spawn`, `remove`, and `control` actions in a termination hook are validation errors. A hook action failure is diagnosed and remaining hook actions continue regardless of `critical`; it cannot suppress cleanup or replace the original terminal cause.
|
||||
|
||||
After the hook attempt, detach subscriptions, release overrides and continuous audio, remove visual systems, clear scheduler records, and release runtime references. Release work transfers to an engine cleanup owner. Each release uses the shorter of its authored release and `5s`; the cleanup owner has an absolute deadline of five seconds of advancing logical time, after which remaining resources are force-disposed with `WARN_CLEANUP_FORCED`. Destroying or replacing the entire performance force-disposes immediately after its deactivation hook; it does not leave a detached timer.
|
||||
|
||||
### 10.3 Condition trigger state
|
||||
|
||||
A condition trigger has `disarmed`, `holding`, and `armed` states. A false evaluation clears its hold timer and arms it. Once armed, continuous true evaluations accumulate logical ticks; it fires once when the declared `for` interval is met, then becomes disarmed. It cannot fire again until at least one later logical tick evaluates false. Startup with a true condition begins disarmed, preventing an unobserved pre-launch edge from firing.
|
||||
|
||||
### 10.4 Deferred starts
|
||||
|
||||
Each scenario definition may have at most one deferred request. Its creation tick and sampled trigger inputs are retained. A later opportunity for the same definition neither adds a request nor extends expiry.
|
||||
|
||||
The expiry is `eligibility.timeout` when present, otherwise `5m`, measured in advancing logical time. At each tick, remove expired requests first. Then consider the remaining queue in descending scenario priority, ascending creation tick, and scenario document order. Immediately before dispatch, recheck that the definition is enabled, its eligibility is true, cooldown permits it, and its concurrency conflict has cleared. A failed recheck leaves the request pending until a later tick or expiry; it does not resample the original trigger.
|
||||
|
||||
### 10.5 Dispatch budget and feedback
|
||||
|
||||
Static validation rejects direct and indirect event invocation cycles and detectable event/scenario trigger feedback cycles as `ERR_CYCLIC_DEPENDENCY`. Runtime uses a second backstop because data-dependent feedback may remain.
|
||||
|
||||
One logical tick permits at most `1024` ordinary dispatch units. Entering an event consumes one unit and each attempted action consumes one unit, including skipped `when`/`chance` actions. When the next unit would exceed the budget, emit `ERR_DISPATCH_BUDGET`, discard the remaining ordinary queue for that tick, and terminate each scenario owner represented by discarded or currently executing feedback work as `FAILED`. Unowned work is dropped. Mandatory termination hooks and cleanup are budget-exempt and use a separate hard limit of `256` termination-hook actions per owner; exceeding it truncates the hook and continues cleanup.
|
||||
|
||||
The maximum nested event depth remains 16 and is checked before entering the next event. Budget and depth failures are critical runtime failures for an owned chain. Diagnostics include tick, owner, event/action path, consumed units, and discarded queue count.
|
||||
|
||||
## 11. Contract completion register
|
||||
|
||||
@@ -333,14 +496,14 @@ Complete shared contracts before implementing dependent subsystems. Use PRD sect
|
||||
| --- | --- | --- | --- |
|
||||
| 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) |
|
||||
| References and bindings | 13, 17, 31-32 | Target-capability table, instance/input scope, evaluation order, cycles, disabled bindings, exact smoothing | **Complete (Rev 0.3 / GC3)** |
|
||||
| Actions and transitions | 22-30 | Shared `set`/`override` fields and defaults, override target matrix, interrupted transitions, instance IDs; subsystem action matrices remain with their subsystems | **Shared contract complete (Rev 0.3 / 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) |
|
||||
| Events/scenarios | 90-102 | Shared ownership, hooks/failure ordering, condition rearming, deferred ordering/expiry, dispatch limits; full trigger/timeline shapes remain for Phase 6 | **Shared lifecycle contract complete (Rev 0.3 / GC5)** |
|
||||
| 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) |
|
||||
| Runtime/library | 108-112, 117-127 | Clock/audio synchronization, stalls, seed/stream derivation complete in GC4; import equality, update compatibility, transactions, failure recovery, persistence schema remain | **Shared clock/PRNG contract complete (Rev 0.3 / GC4); subsystem remainder Phase 1/8** |
|
||||
|
||||
## 12. Contract template and conformance artifacts
|
||||
|
||||
@@ -348,4 +511,33 @@ Each construct must record its JSON shape; required and optional fields; types,
|
||||
|
||||
Supply a valid minimal example, a meaningful composition example, invalid cases with expected diagnostics, and expected semantic traces where timing or ordering matters. Two contrasting complete exhibits must exercise parameters, sound, visuals, and a temporary scenario override early in development.
|
||||
|
||||
## 13. Library identity, update compatibility, and build boundary
|
||||
|
||||
### 13.1 Import identity
|
||||
|
||||
After successful UTF-8 decoding, remove one leading byte-order mark if present. The resulting source bytes are the import payload. Compute SHA-256 over those bytes; equality requires both the same `meta.id` and the same digest. An identical import is a no-op and does not change cached timestamps, source associations, selected exhibit, or parameter values.
|
||||
|
||||
The same ID with a different digest is a replacement candidate even if parsed values appear equivalent. Whitespace, property-order, or numeric-spelling changes may therefore require confirmation; this conservative rule preserves document-order semantics and exact provenance. A changed candidate is parsed and fully validated before the user is offered explicit replace/cancel choices. Invalid candidates never replace the cached valid definition.
|
||||
|
||||
### 13.2 Parameter reconciliation
|
||||
|
||||
After an explicit replacement choice, reconcile stored parameters by parameter ID:
|
||||
|
||||
| Change | Result |
|
||||
| --- | --- |
|
||||
| Same type and still valid | Preserve stored value. |
|
||||
| `number`/`integer` bounds changed | Clamp to the new inclusive range and show a notice; integer values are rounded half away from zero before clamping. |
|
||||
| Enum values changed | Preserve only if the stored token remains declared; otherwise use the new default and show a notice. |
|
||||
| Type changed, including `number` to `integer` | Use the new default and show an incompatibility notice. |
|
||||
| Parameter removed | Delete its stored value after successful activation. |
|
||||
| Parameter added | Initialize from the new default. |
|
||||
|
||||
Do not mutate the old cached definition or settings while validating or preparing a candidate. Preparation may allocate parsed data and inert compiled plans but must not start timers, render visible output, create audible nodes, or persist replacement state.
|
||||
|
||||
On successful candidate activation, atomically commit the new definition, reconciled settings, digest, and source metadata, then dispose the old performance. If activation fails after the old performance has stopped, dispose the candidate and attempt a fresh activation of the old cached definition with its unchanged saved settings. Report both failures if recovery fails; never represent the failed candidate as active.
|
||||
|
||||
### 13.3 Reproducible standalone build
|
||||
|
||||
Development uses separate source modules. The build must have pinned tool versions and inputs, stable module/asset order, no wall-clock timestamps or absolute paths in output, and a documented single command that produces `XZBT.html`. Two clean builds from the same revision must have identical SHA-256 digests. The artifact must contain all runtime code/assets/fonts and pass direct-file offline verification with no external runtime requests.
|
||||
|
||||
Structural JSON Schema does not replace semantic validation. The internal schema and fixtures belong to 0.1 implementation work; public schema distribution and editor integration may follow later.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# XZBT 0.1 Gap Closure Decisions
|
||||
|
||||
**Record version:** 0.2\
|
||||
**Status:** Incorporated into the planning baseline; GC1 feasibility and GC2 shared contracts verified; remaining subsystem verification pending\
|
||||
**Record version:** 0.3\
|
||||
**Status:** Phase 0 GC1–GC5 decisions and shared contract evidence complete; GC6/GC7 implementation evidence remains scheduled\
|
||||
**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.
|
||||
@@ -10,9 +10,9 @@ Numbering preserves the seven gaps discussed during PRD preparation. Recording a
|
||||
| --- | --- | --- |
|
||||
| 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 |
|
||||
| 3 | 31-32, 54, 104 | GC3 (Shared contract passed) |
|
||||
| 4 | 14, 102, 117-118 | GC4 (Shared contract passed) |
|
||||
| 5 | 91, 94, 99-101 | GC5 (Shared contract passed) |
|
||||
| 6 | 58, 119-120, 133, 144 | GC6 |
|
||||
| 7 | 108-110, 126-128, 134-135, 143-145 | GC7 |
|
||||
|
||||
@@ -50,23 +50,25 @@ References ordinarily read resolved values. Configuration controls read and edit
|
||||
|
||||
Override scope controls lifetime; explicit or inherited priority controls precedence. Equal priorities use activation order. Duration overrides do not automatically outrank scenario overrides. Release transitions approach the currently resolved underlying value, including changes made while masked. Additive modulation may remain after an override only on targets whose contracts permit it.
|
||||
|
||||
Reject competing ordinary bindings and unresolvable dependency cycles. The format specification must finish the target-capability and transition rules before the resolver is implemented.
|
||||
Reject competing ordinary bindings and unresolvable dependency cycles. The format specification now fixes the shared target-capability registry, canonical `source`/`target` binding fields, zero-duration default smoothing, the exact one-pole smoothing equation, override priority defaults and runtime IDs, easing equations, and interrupted attack/release behavior. Draft `from`/`to`/`transform` spellings were reconciled to the PRD and are rejected rather than retained as aliases.
|
||||
|
||||
**Gate:** GC3. Expected traces cover direct target overrides, parameter overrides flowing through bindings, overlapping priorities, user edits, and release behavior.
|
||||
**Gate:** GC3. Passed by deterministic traces covering direct target overrides, parameter overrides flowing through bindings, overlapping priorities, user edits, release behavior, smoothing, clamps, conflicts, cycles, and unsupported targets.
|
||||
|
||||
## 4. Clock and reproducibility
|
||||
|
||||
**Decision:** Use a logical simulation clock with an initial fixed update step of 1/60 second. Rendering is independent. Map logical audio scheduling to the audio clock with a bounded scheduling horizon; the exact horizon and late-work policy require prototype measurements and a written contract.
|
||||
**Decision:** Use a logical simulation clock with an initial fixed update step of 1/60 second. Rendering is independent. Map logical audio scheduling to the audio clock with the bounded horizon and late-work policy fixed by the format contract.
|
||||
|
||||
In 0.1, document visibility loss pauses the entire performance by default, including audio. Explicit application pause does the same. Resume from the previous logical position without replaying elapsed wall-clock time. Visibility return must not undo an explicit user pause. Background playback is a separately specified future capability.
|
||||
|
||||
Visuals may start before audio is unlocked, as permitted by the PRD. On unlock, begin audio at the current logical position without replaying expired sounds; the format contract must specify how continuous ambience and partially elapsed sounds initialize.
|
||||
Visuals may start before audio is unlocked, as permitted by the PRD. On unlock, begin audio at the current logical position without replaying earlier one-shots. Continuous ambience aligns at its current logical phase with a short engine-owned ramp; partially elapsed one-shots are skipped.
|
||||
|
||||
Derive isolated random streams from the exhibit seed for cadence, scenarios, visual systems, and sound instances. Manual SAMPLE playback has an independent stream. Record the actual numeric seed when the exhibit requests a random seed.
|
||||
Derive isolated random streams from the exhibit seed for cadence, scenarios, visual systems, and sound instances. Manual SAMPLE playback has an independent stream. Record the actual numeric seed when the exhibit requests a random seed. The 0.1 contract uses FNV-1a-32 domain derivation, SplitMix32 state expansion, and `xoshiro128**`, with stable known-vector tests.
|
||||
|
||||
Reproducibility means identical procedural decisions for the same runtime version, seed, and logical input sequence. It does not promise identical pixels or audio samples across devices. Deterministic tests use recorded or synthetic audio-analysis signals where such signals affect decisions.
|
||||
|
||||
**Gate:** GC4. Render frequency, pause duration, and independent sound sampling must not perturb unrelated logical choices.
|
||||
The fixed-step driver accepts at most 250 ms from one wall-time observation and runs at most eight logical ticks per browser turn. Audio scheduling uses a 100 ms horizon. Pre-unlock one-shots are skipped if their start is already past; still-owned continuous audio aligns to current logical state with a 20 ms engine ramp.
|
||||
|
||||
**Gate:** GC4. Shared clock/PRNG contract passed by deterministic render-frequency, stall, pause-reason, audio-unlock decision, known-vector, stream-isolation, and signal-fixture tests. Real Web Audio synchronization remains an audio-integration gate rather than a Phase 0 contract-oracle claim.
|
||||
|
||||
**Basis:** [MDN Page Visibility API](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API) documents background frame/timer behavior. This decision supplies XZBT's product policy rather than relying on browser throttling as its scheduler.
|
||||
|
||||
@@ -76,13 +78,13 @@ Reproducibility means identical procedural decisions for the same runtime versio
|
||||
|
||||
A duration override created under a scenario expires at its deadline or when the owner terminates, whichever happens first. State mutations are persistent and are not rolled back after completion, cancellation, or failure. Failure cleanup removes temporary effects; scenarios are not transactions.
|
||||
|
||||
On termination, stop ordinary dispatch, cancel future work, run the applicable termination hook, and guarantee cleanup even when the hook fails. Bounded release fades may continue under a cleanup owner until final disposal. Hook execution order and permitted hook actions must be finalized in the format specification; no new failure-hook field is introduced by this record.
|
||||
On termination, stop ordinary dispatch, cancel future work, run the applicable termination hook, and guarantee cleanup even when the hook fails. Natural completion uses `onComplete`; cancellation and critical failure use `onCancel`, with the original terminal result preserved. Termination hooks permit only `set` and nonpersistent one-shot `sound` actions. Bounded release fades continue under a cleanup owner for at most five logical seconds before forced disposal. No new failure-hook field is introduced.
|
||||
|
||||
Condition triggers fire once after the qualifying condition has held for the declared interval, then require a false condition before rearming. A scenario definition has at most one pending deferred request, with an expiry and an eligibility recheck at dispatch. Exact queue ordering and default expiry must be specified before implementation.
|
||||
Condition triggers fire once after the qualifying condition has held for the declared interval, then require a false condition before rearming. A scenario definition has at most one pending deferred request, with an expiry and an eligibility recheck at dispatch. Default expiry is five logical minutes; ordering is priority descending, creation tick ascending, then document order.
|
||||
|
||||
Validate event/scenario trigger cycles where statically detectable. Enforce a per-update dispatch budget as a runtime backstop, with a visible diagnostic when exhausted. Cleanup must still run when ordinary dispatch is curtailed.
|
||||
Validate event/scenario trigger cycles where statically detectable. Enforce a budget of 1024 ordinary dispatch units per logical tick as a runtime backstop, with a visible diagnostic when exhausted. Cleanup remains budget-exempt, with a separate 256-action termination-hook limit.
|
||||
|
||||
**Gate:** GC5. Repeated completion, cancellation, and injected failures return resource counts to the expected baseline after bounded releases finish.
|
||||
**Gate:** GC5. Passed by ownership, bounded-release, persistent-state, startup/action/hook failure, trigger-rearm, deferred-ordering, feedback-budget, and repeated lifecycle traces. Runtime subsystem implementations must repeat these traces with real resource counters.
|
||||
|
||||
## 6. Performance and audio acceptance
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# XZBT 0.1 implementation plan
|
||||
|
||||
**Planning baseline:** Format Specification 0.1 Revision 0.3
|
||||
**Entry state:** Phase 0 feasibility and shared contracts complete; production runtime not started
|
||||
**Rule:** Finish each subsystem contract immediately before its implementation and preserve the Phase 0 shared semantics.
|
||||
|
||||
## Sequenced milestones and verification stops
|
||||
|
||||
| Milestone | PRD coverage | Deliverable and verification stop |
|
||||
| --- | --- | --- |
|
||||
| Phase 1 — Runtime skeleton | 8–14, 108–118, 121–127, 134–135 | Modular shell, loader, diagnostics, seeded RNG, IndexedDB foundation, activation/deactivation, and deterministic standalone build. Stop only when two minimal exhibits import, cache, switch, restart, and restore directly from disk. |
|
||||
| Phase 2 — Common grammar | 15–33, 104, 136 | Parameters/state/signals, ValueSpec, ConditionSpec, actions, bindings, and override stack. Re-run GC2/GC3 traces against production code; demonstrate preserved user edits through masking and release. |
|
||||
| Phase 3 — Audio engine | 34–60, 118–120, 129, 137 | Complete audio subsystem contract, graph compiler/nodes/components/buses, lifecycle, automation, protection, unlock mapping, and voice limits. Run audio challenge and real GC4 synchronization checks. Begin reference Exhibits A/B audio. |
|
||||
| Phase 4 — Visual engine | 69–89, 119–120, 130, 138 | Complete visual subsystem contract and generic renderer features. Run visual challenge. Begin reference Exhibits A–D visuals, then execute the early combined GC6 benchmark before fixing later optimization strategy. |
|
||||
| Phase 5 — Events and cadence | 61–68, 90–91, 139 | Complete cadence/event shapes, selection/cooldown/overlap/anti-repetition, and event dispatch. Re-run dispatch budget and manual-stream isolation against production code. Integrate automatic behavior in Exhibits A–D. |
|
||||
| Phase 6 — Scenario director | 92–102, 131, 140 | Complete trigger/timeline shapes; implement eligibility, branching, priority, concurrency, ownership, cleanup, and accelerated time. Re-run all GC5 traces with real resource counters. Build Exhibit E and temporary overrides in A/B. Run the two-hour development soak. |
|
||||
| Phase 7 — Dynamic UI | 103–107, 117–118, 123, 141 | Complete UI contract; generated controls, soundboard, master volume, pause/resume, accessibility. Verify five distinct generated surfaces and SAMPLE isolation. |
|
||||
| Phase 8 — Library/persistence hardening | 108–116, 121–127, 132–133, 142 | Directory/multi-file/drop import, refresh, update reconciliation, handles, transactions, recovery, and diagnostics. Execute every GC7 library failure and replacement test. |
|
||||
| Phase 9 — Reference completion/audit | 128–133, 143–145 | Complete all five exhibits, all three challenge suites, direct/offline artifact audit, fixed benchmark, resource/leak cycles, and eight-hour release soak. Audit every MVP completion criterion before declaring 0.1 complete. |
|
||||
|
||||
Every phase ends with tests and an evidence record. A failed gate stops dependent work; it does not silently weaken the requirement.
|
||||
|
||||
## MVP completion-criteria map
|
||||
|
||||
| Completion criterion group | Primary milestone | Final evidence |
|
||||
| --- | --- | --- |
|
||||
| Self-contained `XZBT.html`, direct/offline operation, no external runtime dependencies | Phase 1 build foundation; Phase 9 audit | Reproducible digest comparison, network capture, direct-file launch record |
|
||||
| External import, cache/reopen, parameter persistence, malformed-input safety | Phase 1 foundation; Phase 8 hardening | Import/restart matrix, invalid/failed-refresh and transaction tests |
|
||||
| Generated UI and per-exhibit surfaces | Phase 7 | UI fixture snapshots, keyboard/accessibility checks, five-exhibit surface comparison |
|
||||
| ValueSpec, ConditionSpec, Action Model, Binding Model, overrides | Phase 2 | GC2/GC3 conformance suite against production modules |
|
||||
| Audio Graph 0.1 and audio challenge | Phase 3; Phase 9 audit | Node/graph/lifecycle tests, challenge traces, audible observations, peak/finite-sample capture |
|
||||
| Visual System 0.1 and visual challenge | Phase 4; Phase 9 audit | Primitive/behavior/effect fixtures, screenshots, benchmark frame samples |
|
||||
| Event and Cadence 0.1; SAMPLE isolation | Phase 5 and Phase 7 | Long logical cadence trace, collision/anti-repeat checks, PRNG stream comparison |
|
||||
| Scenario Model and scenario challenge | Phase 6; Phase 9 audit | Accelerated scenario traces, ownership/failure counters, completion/cancel/failure repetitions |
|
||||
| Clean exhibit switching and recovery | Phase 8 | Resource baselines, candidate activation fault injection, previous-exhibit restart record |
|
||||
| Seeded reproducibility | Phase 1 PRNG; Phases 2–6 consumers | Known vectors and equal decision traces across render rates/runs |
|
||||
| Five generic reference exhibits with no exhibit-specific JavaScript | Developed in Phases 3–7; audited Phase 9 | Source audit and capability/requirement matrix for Exhibits A–E |
|
||||
| Long-running stability, fixed benchmark, two/eight-hour soaks | Phase 6 development soak; Phase 9 release soak | GC6 environment/workload records, retained-memory/resource/frame/audio logs |
|
||||
| Direct-file feasibility and structural/semantic contract evidence | Phase 0 complete | GC1 manual evidence and GC2–GC5 contract evidence |
|
||||
|
||||
## Reference-exhibit and challenge map
|
||||
|
||||
| Exhibit | Required challenge coverage | First integrated milestone | Completion |
|
||||
| --- | --- | --- | --- |
|
||||
| A — Procedural Machine Environment | Multiple buses; oscillator/noise/impulse/resonator; continuous ambience; all cadence classes; geometry, gauges, paths, particles; temporary disturbance | Phase 4 after audio + visual cores | Phase 9 audio, visual, cadence, scenario, and soak audit |
|
||||
| B — Deep Abstract Field | Layered synthesis, evolving modulation, abstract depth, particles/links/trails/glow, audio-reactive bindings, scenario override | Phase 4 | Phase 9 audio/visual challenge and benchmark fixture candidate |
|
||||
| C — Natural Environment | Nonliteral procedural ambience, organic fields/distributions, slow cadence, restrained effects | Phase 4 | Phase 9 breadth and performance audit |
|
||||
| D — Instrument/Data Display | UI-driven parameters, state/signals, gauges/paths/text-like generic display primitives, accessibility | Phase 4 visuals; Phase 7 UI | Phase 9 dynamic-UI and visual challenge audit |
|
||||
| E — Long Scenario | Relative/random timing, repeats, branching, persistent state, cadence interaction, continuous audio, visual spawn, recovery/cleanup | Phase 6 | Phase 9 scenario challenge and lifecycle soak audit |
|
||||
|
||||
The audio acceptance challenge (PRD 129) is primarily Phases 3 and 9; visual challenge (130), Phases 4 and 9; scenario challenge (131), Phases 6 and 9; persistence acceptance (132), Phase 8; reliability tests (133), Phases 6, 8, and 9.
|
||||
|
||||
## GC6 measurement schedule and fixed method
|
||||
|
||||
GC6 is staged rather than treated as a Phase 0 pass:
|
||||
|
||||
1. **Phase 1 environment record:** CPU, GPU/driver, RAM, OS/browser, viewport `1920 × 1080`, device-pixel ratio, sample rate, power mode, and artifact revision.
|
||||
2. **End of Phase 4 early combined benchmark:** Freeze benchmark ID/version/seed and counts for particles, links, trails, passes, audio nodes, and voices. Use 30 seconds warm-up followed by a 120-second measurement window. Sample every rendered frame interval, runtime counters once per logical second, and browser-reported memory where available every 10 seconds.
|
||||
3. **Phase 6 development soak:** Run the frozen workload for two real hours; record p50/p95/p99 frame interval, dropped/long frames, output peak/nonfinite sample count, audio glitches, scheduler size, resource counters, and retained-memory trend.
|
||||
4. **Phase 9 release soak:** Repeat for eight real hours with the release artifact and the same environment/workload. Any benchmark change creates a new version and requires a documented rationale plus fresh results.
|
||||
|
||||
The provisional interactive threshold is p95 frame interval below `33.3ms`, with progress toward 60 FPS reported separately. Audio acceptance requires finite output, an exact peak ceiling/tolerance fixed in the Phase 3 audio contract, unavoidable master routing, bounded releases, and listening observations for clicks/clipping. Accelerated tests never substitute for real-duration audio or soak evidence.
|
||||
|
||||
## GC7 library/build verification schedule
|
||||
|
||||
Revision 0.3 defines byte-level import identity, explicit same-ID replacement, parameter compatibility, inert candidate preparation, activation recovery, and reproducible build boundaries. Execute them as follows:
|
||||
|
||||
- Phase 1: clean-build digest equality and direct/offline two-fixture import/restart.
|
||||
- Phase 7: the first two integrated exhibits generate distinct controls without runtime-specific UI.
|
||||
- Phase 8: identical no-op, changed-ID confirmation, invalid refresh preservation, every parameter reconciliation case, inert preparation, activation failure, candidate disposal, and previous-exhibit restart.
|
||||
- Phase 9: all five exhibits, source-code audit, final direct/offline build, challenge coverage, and completion-criteria audit.
|
||||
|
||||
Remembered directory handles remain optional. If implemented, they receive permission-loss and ordinary-picker fallback tests; failure cannot remove the ordinary import path.
|
||||
@@ -1,15 +1,15 @@
|
||||
# XZBT 0.1 Verification Gates
|
||||
|
||||
**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.
|
||||
**Status:** Phase 0 shared-contract preparation complete before Phase 1. GC1 direct-file feasibility and GC2–GC5 shared contracts are verified; subsystem integration gates remain in their assigned later phases.
|
||||
**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. 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 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).
|
||||
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 fully passed. GC2 schema/semantic validation and its 12-case matrix are verified in the [GC2 evidence](evidence/phase0/2026-09-05-gc2-format-contracts.md). GC3–GC5 shared resolution, clock/PRNG, and ownership/failure contracts and deterministic traces are verified in the [combined contract evidence](evidence/phase0/2026-09-05-gc3-gc5-contracts.md).
|
||||
|
||||
## Phase 0 and later gates
|
||||
|
||||
Drafting the implementation plan may proceed now. Before committing the deployment architecture, pass GC1. Before implementing shared runtime semantics, complete the shared portions of GC2 and GC3-GC5 contracts and their expected traces. Early GC6 measurements inform expensive subsystem design. Full conformance and real-duration soak results remain release requirements.
|
||||
The [implementation plan](XZBT_0-1_Implementation_Plan.md) is complete and Phase 1 may begin. GC1 and the shared portions of GC2–GC5 are satisfied. Early GC6 measurements still precede expensive subsystem optimization decisions. Full production conformance and real-duration soak results remain later requirements.
|
||||
|
||||
Subsystem-specific format details can be completed in dependency order as planned tasks. Do not require the finished engine or eight-hour soak before creating the plan.
|
||||
|
||||
@@ -17,11 +17,11 @@ Subsystem-specific format details can be completed in dependency order as planne
|
||||
| --- | --- | --- | --- |
|
||||
| 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 |
|
||||
| GC6 | Recorded; thresholds provisional | Reference setup and benchmark absent | Early combined-load prototype; development and release acceptance |
|
||||
| GC7 | Recorded | Plan/build/library checks pending | Milestone planning, then library/build integration |
|
||||
| GC3 | Recorded; shared transition rules complete | Deterministic resolver traces pass | Shared resolver contract complete; repeat with Phase 2 implementation |
|
||||
| GC4 | Recorded; shared clock/audio mapping/PRNG complete | Deterministic clock, unlock-decision, and stream tests pass | Clock contract complete; real synchronization tests in audio integration |
|
||||
| GC5 | Recorded; shared lifecycle rules complete | Deterministic ownership/failure traces pass | Ownership contract complete; repeat with real actions/audio/scenarios |
|
||||
| GC6 | Recorded; thresholds provisional | Measurement protocol and schedule defined; reference setup/benchmark results pending | Early combined-load prototype; development and release acceptance |
|
||||
| GC7 | Recorded; identity/update/build boundary complete | Milestone map complete; implementation checks pending | Planning complete; then library/build integration |
|
||||
|
||||
## 1. GC1: Direct-file feasibility
|
||||
|
||||
@@ -52,51 +52,51 @@ Subsystem-specific format details can be completed in dependency order as planne
|
||||
|
||||
## 3. GC3: Resolution semantics
|
||||
|
||||
- [ ] Record exact target capabilities, override defaults, smoothing, and interrupted transition rules.
|
||||
- [ ] Define expected traces for a bound bus gain receiving a direct override.
|
||||
- [ ] Define expected traces for a parameter override feeding a binding.
|
||||
- [ ] Verify stored user edits during masking and release toward the updated lower value.
|
||||
- [ ] Verify priority ties and competing duration/scenario overrides.
|
||||
- [ ] Verify automation continues while masked and permitted modulation applies after an override.
|
||||
- [ ] Verify numeric clamps and unsupported target/stage diagnostics.
|
||||
- [ ] Reject conflicting bindings and dependency cycles without introducing undocumented delays.
|
||||
- [x] Record exact target capabilities, override defaults, smoothing, and interrupted transition rules.
|
||||
- [x] Define expected traces for a bound bus gain receiving a direct override.
|
||||
- [x] Define expected traces for a parameter override feeding a binding.
|
||||
- [x] Verify stored user edits during masking and release toward the updated lower value.
|
||||
- [x] Verify priority ties and competing duration/scenario overrides.
|
||||
- [x] Verify automation continues while masked and permitted modulation applies after an override.
|
||||
- [x] Verify numeric clamps and unsupported target/stage diagnostics.
|
||||
- [x] Reject conflicting bindings and dependency cycles without introducing undocumented delays.
|
||||
|
||||
**Evidence:** contract examples and deterministic resolver tests when implemented.
|
||||
**Evidence:** [GC3–GC5 shared contract evidence](evidence/phase0/2026-09-05-gc3-gc5-contracts.md), executable resolution model, normative JSON traces, and deterministic resolver tests. Production resolver conformance repeats in Phase 2.
|
||||
|
||||
## 4. GC4: Time and reproducibility
|
||||
|
||||
- [ ] Define logical tick ordering, maximum work per turn, long-stall behavior, audio lookahead, and audio unlock alignment.
|
||||
- [ ] Define PRNG algorithm, seed normalization, and stream derivation for the runtime version.
|
||||
- [ ] Compare simulation traces at different render frequencies using the same logical inputs.
|
||||
- [ ] Hide/restore the document and explicitly pause/resume; no missed wall-clock work is replayed.
|
||||
- [ ] Verify visibility restoration does not undo an explicit pause.
|
||||
- [ ] Verify audio suspends/resumes consistently and expired pre-unlock one-shots are not replayed.
|
||||
- [ ] Verify manual SAMPLE does not perturb cadence/scenario random streams.
|
||||
- [ ] Use deterministic signal fixtures for audio-reactive decision tests.
|
||||
- [ ] Distinguish accelerated logical tests from real-time audio behavior and soak tests.
|
||||
- [x] Define logical tick ordering, maximum work per turn, long-stall behavior, audio lookahead, and audio unlock alignment.
|
||||
- [x] Define PRNG algorithm, seed normalization, and stream derivation for the runtime version.
|
||||
- [x] Compare simulation traces at different render frequencies using the same logical inputs.
|
||||
- [x] Simulate hide/restore and explicit pause/resume; no missed wall-clock work is replayed. Repeat with the Phase 1 runtime in a browser.
|
||||
- [x] Verify in the clock model that visibility restoration does not undo an explicit pause.
|
||||
- [x] Verify audio suspend/resume in the GC1 browser probe and verify pre-unlock one-shot suppression in the deterministic scheduling model. Repeat combined behavior during audio integration.
|
||||
- [x] Verify manual SAMPLE does not perturb cadence/scenario random streams.
|
||||
- [x] Use deterministic signal fixtures for audio-reactive decision tests.
|
||||
- [x] Distinguish accelerated logical tests from real-time audio behavior and soak tests.
|
||||
|
||||
**Evidence:** clock/PRNG contract, logical traces, and browser/audio observations. No cross-device pixel or waveform equality claim is required.
|
||||
**Evidence:** [GC3–GC5 shared contract evidence](evidence/phase0/2026-09-05-gc3-gc5-contracts.md), clock/PRNG model and known vectors, logical traces, deterministic signal fixture, plus the earlier GC1 browser audio observations. This closes the Phase 0 contract gate, not later real-time synchronization or soak gates. No cross-device pixel or waveform equality claim is required.
|
||||
|
||||
## 5. GC5: Ownership and failure
|
||||
|
||||
- [ ] Define hook ordering, allowed hook actions, failure propagation, cleanup deadlines, deferred expiry/order, and dispatch-budget behavior.
|
||||
- [ ] Start resources through nested events and confirm inherited scenario ownership.
|
||||
- [ ] Verify duration overrides terminate with their owner and bounded releases eventually dispose.
|
||||
- [ ] Verify persistent state mutations survive a later critical failure.
|
||||
- [ ] Inject failures into startup, ordinary actions, and termination hooks; cleanup still completes.
|
||||
- [ ] Verify condition triggers require a false condition before rearming.
|
||||
- [ ] Verify at most one pending deferred request per definition, expiry, and eligibility recheck.
|
||||
- [ ] Exercise event/scenario feedback and show bounded dispatch with diagnostics.
|
||||
- [ ] Repeat completion/cancellation/failure cycles; counters return to baseline after releases finish.
|
||||
- [x] Define hook ordering, allowed hook actions, failure propagation, cleanup deadlines, deferred expiry/order, and dispatch-budget behavior.
|
||||
- [x] Start resources through nested events and confirm inherited scenario ownership.
|
||||
- [x] Verify duration overrides terminate with their owner and bounded releases eventually dispose.
|
||||
- [x] Verify persistent state mutations survive a later critical failure.
|
||||
- [x] Inject failures into startup, ordinary actions, and termination hooks; cleanup still completes.
|
||||
- [x] Verify condition triggers require a false condition before rearming.
|
||||
- [x] Verify at most one pending deferred request per definition, expiry, and eligibility recheck.
|
||||
- [x] Exercise event/scenario feedback and show bounded dispatch with diagnostics.
|
||||
- [x] Repeat completion/cancellation/failure cycles; counters return to baseline after releases finish.
|
||||
|
||||
**Evidence:** lifecycle traces, resource-count samples, and failure-injection results.
|
||||
**Evidence:** [GC3–GC5 shared contract evidence](evidence/phase0/2026-09-05-gc3-gc5-contracts.md), lifecycle oracle, resource-count assertions, and failure-injection tests. Repeat with subsystem-owned resources during Phases 3–6.
|
||||
|
||||
## 6. GC6: Performance and audio acceptance
|
||||
|
||||
- [ ] Record CPU/GPU/RAM, OS, browser version, 1920 x 1080 viewport, device-pixel ratio, sample rate, and relevant power settings.
|
||||
- [ ] Fix benchmark exhibit/version/seed and counts for particles, links, trails, effect passes, audio nodes, and concurrent voices.
|
||||
- [ ] Prototype combined visual/audio load early and record supported limits.
|
||||
- [ ] Define warm-up duration, measurement window, frame interval sampling, and retained-memory sampling method before collecting acceptance data.
|
||||
- [x] Define warm-up duration, measurement window, frame interval sampling, and retained-memory sampling method before collecting acceptance data. See the implementation plan; no benchmark result is claimed yet.
|
||||
- [ ] Check the provisional p95 frame interval threshold of less than 33.3 ms and report progress toward the 60 FPS target.
|
||||
- [ ] Record active resource baselines and verify expected return after repeated lifecycle operations.
|
||||
- [ ] Specify and test digital output peak ceiling, numerical tolerance, finite samples, release behavior, and unavoidable master routing.
|
||||
@@ -107,9 +107,9 @@ Subsystem-specific format details can be completed in dependency order as planne
|
||||
|
||||
## 7. GC7: Scope, updates, and build
|
||||
|
||||
- [ ] Map all PRD completion criteria and challenge cases to milestones and evidence.
|
||||
- [x] Map all PRD completion criteria and challenge cases to milestones and evidence.
|
||||
- [ ] Integrate two contrasting exhibits early; make Phase 9 the completion/audit phase for all five reference exhibits.
|
||||
- [ ] Define import equality and parameter compatibility precisely.
|
||||
- [x] Define import equality and parameter compatibility precisely.
|
||||
- [ ] Verify identical imports are no-ops and changed same-ID content requires an explicit replacement choice.
|
||||
- [ ] Verify invalid refreshes leave the last valid definition and configuration intact.
|
||||
- [ ] Verify compatible parameter preservation, numeric bound changes with notice, removed/new parameters, and incompatible-type/enum resets.
|
||||
|
||||
@@ -34,3 +34,7 @@ The automated test runner executed 12 tests covering valid exhibits and delibera
|
||||
|
||||
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.
|
||||
|
||||
## Revision 0.3 reconciliation addendum
|
||||
|
||||
GC3 review found that the draft BindingSpec used `from`/`to`/`transform`, conflicting with the PRD's canonical `source`/`target`/`scale`/`offset`/`clamp` fields. Revision 0.3 corrected the schema, validator, and affected fixtures to the PRD vocabulary and added `when` plus exact smoothing semantics. The same 12-case GC2 matrix was rerun as part of the 27-test combined Phase 0 suite and still passed with zero failures. See the [GC3–GC5 evidence](2026-09-05-gc3-gc5-contracts.md) for the reconciliation rationale and combined command.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Phase 0 GC3–GC5 shared contract evidence
|
||||
|
||||
**Result:** Passed for Phase 0 shared resolution, clock/PRNG, and ownership/failure contracts
|
||||
**Scope:** Deterministic contract models and validation; not production runtime, real-time audio integration, or soak acceptance
|
||||
**Format specification:** Revision 0.3
|
||||
|
||||
## Contract reconciliation
|
||||
|
||||
The GC2 draft schema used `from`, `to`, and `transform` for ordinary bindings, while PRD section 32 already defined `source`, `target`, `scale`, `offset`, and `clamp`. No production runtime exists and no released exhibit depends on the draft vocabulary. Revision 0.3 adopts the PRD fields, sets `smoothing` default to `0ms`, rejects the draft names as unknown fields, and migrates the conformance fixtures. The complete GC2 matrix still passes.
|
||||
|
||||
## GC3 — resolution semantics
|
||||
|
||||
The normative contract now defines:
|
||||
|
||||
- shared target capabilities and safety clamps;
|
||||
- same-tick topological dependency evaluation;
|
||||
- exact binding transformation, conditional disabling, and one-pole smoothing;
|
||||
- override scope, priority, activation sequence, runtime IDs, and masking;
|
||||
- easing equations, attack/release behavior, interruption, and dynamic release destinations;
|
||||
- diagnostics for conflicts, cycles, unsupported targets, invalid transitions, and bounds.
|
||||
|
||||
The executable model and trace fixture verify direct bus overrides, parameter overrides feeding downstream bindings, preserved user edits, priority/scope competition, masked automation, post-override modulation, safety clamping, smoothing, interruption, conflicts, cycles, unsupported targets, and rejected draft aliases.
|
||||
|
||||
## GC4 — time and reproducibility
|
||||
|
||||
The normative contract fixes a `1/60s` logical step, eight ticks per browser turn, a `250ms` accepted wall-time limit, a `100ms` audio horizon, composable pause reasons, and unlock/resume alignment. Earlier one-shots are skipped at unlock; still-owned continuous audio aligns to current logical state with a `20ms` ramp.
|
||||
|
||||
Seed derivation is fixed to FNV-1a-32 plus SplitMix32 state expansion and `xoshiro128**`. Known vectors lock the implementation. Tests compare 60 Hz and 120 Hz render drivers, bound a one-second stall, preserve explicit pause across visibility restoration, verify unlock scheduling decisions, isolate manual sampling, and use a deterministic audio-analysis fixture.
|
||||
|
||||
The audio unlock test is a scheduling-decision model. It does not claim that future Web Audio nodes already implement the mapping or anti-click ramp. The GC1 browser evidence separately establishes user-unlocked audio and AudioContext suspend/resume in the direct-file environment. Combined synchronization must be repeated during audio integration.
|
||||
|
||||
## GC5 — ownership and failure
|
||||
|
||||
The normative contract fixes inherited scenario ownership, termination hook selection and allowed actions, persistent-state behavior, a five-logical-second cleanup bound, condition rearming, one deferred request per definition, five-minute default expiry, deterministic deferred ordering, a 1024-unit ordinary dispatch budget, and a separate 256-action termination-hook limit.
|
||||
|
||||
The lifecycle model verifies nested ownership, bounded override release, persistent state after critical failure, startup and termination-hook failures, condition rearming, deferred deduplication/expiry/eligibility ordering, bounded feedback, cleanup outside the ordinary budget, and 30 repeated completion/cancellation/failure cycles returning resource counts to zero.
|
||||
|
||||
## Verification performed
|
||||
|
||||
```text
|
||||
node --test test/gc2-runner.test.mjs test/gc3-resolution.test.mjs test/gc4-time-random.test.mjs test/gc5-lifecycle.test.mjs prototypes/phase0/audio-probe.test.mjs
|
||||
```
|
||||
|
||||
Result: **27 tests passed, 0 failed**.
|
||||
|
||||
Artifacts:
|
||||
|
||||
- `tools/gc3-resolution-model.mjs`
|
||||
- `test/fixtures/gc3/resolution-traces.json`
|
||||
- `test/gc3-resolution.test.mjs`
|
||||
- `tools/gc4-time-model.mjs`
|
||||
- `test/fixtures/gc4/deterministic-signals.json`
|
||||
- `test/gc4-time-random.test.mjs`
|
||||
- `tools/gc5-lifecycle-model.mjs`
|
||||
- `test/gc5-lifecycle.test.mjs`
|
||||
|
||||
These are Phase 0 contract oracles. Production implementations must pass equivalent traces with their actual resolver, scheduler, Web Audio nodes, event/scenario director, and resource counters.
|
||||
@@ -299,26 +299,43 @@
|
||||
"BindingSpec": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"from",
|
||||
"to"
|
||||
"source",
|
||||
"target"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"from": {
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "Source reference path (e.g. 'parameters.activity')."
|
||||
},
|
||||
"to": {
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "Target reference path (e.g. 'audio.buses.ambient.gain')."
|
||||
},
|
||||
"transform": {
|
||||
"$ref": "#/definitions/ValueSpec",
|
||||
"description": "Optional transformation ValueSpec."
|
||||
"scale": {
|
||||
"type": "number",
|
||||
"description": "Numeric source multiplier. Defaults to 1."
|
||||
},
|
||||
"offset": {
|
||||
"type": "number",
|
||||
"description": "Numeric offset applied after scaling. Defaults to 0."
|
||||
},
|
||||
"clamp": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
"items": {
|
||||
"type": "number"
|
||||
},
|
||||
"description": "Inclusive [minimum, maximum] clamp applied to the transformed binding value."
|
||||
},
|
||||
"smoothing": {
|
||||
"$ref": "#/definitions/DurationSpec",
|
||||
"description": "Optional smoothing low-pass duration."
|
||||
},
|
||||
"when": {
|
||||
"$ref": "#/definitions/ConditionSpec",
|
||||
"description": "Optional condition; defaults to true."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+4
-4
@@ -18,12 +18,12 @@
|
||||
},
|
||||
"bindings": [
|
||||
{
|
||||
"from": "parameters.activity",
|
||||
"to": "state.energy"
|
||||
"source": "parameters.activity",
|
||||
"target": "state.energy"
|
||||
},
|
||||
{
|
||||
"from": "state.energy",
|
||||
"to": "parameters.activity"
|
||||
"source": "state.energy",
|
||||
"target": "parameters.activity"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+11
-7
@@ -18,13 +18,17 @@
|
||||
},
|
||||
"bindings": [
|
||||
{
|
||||
"from": "parameters.activity",
|
||||
"to": "state.energy",
|
||||
"transform": {
|
||||
"op": "add",
|
||||
"args": [
|
||||
1.0
|
||||
]
|
||||
"source": "parameters.activity",
|
||||
"target": "state.energy",
|
||||
"when": {
|
||||
"op": "gt",
|
||||
"left": {
|
||||
"op": "add",
|
||||
"args": [
|
||||
1.0
|
||||
]
|
||||
},
|
||||
"right": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@
|
||||
},
|
||||
"bindings": [
|
||||
{
|
||||
"from": "parameters.non_existent_param",
|
||||
"to": "parameters.activity"
|
||||
"source": "parameters.non_existent_param",
|
||||
"target": "parameters.activity"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+3
-9
@@ -48,15 +48,9 @@
|
||||
},
|
||||
"bindings": [
|
||||
{
|
||||
"from": "parameters.activity",
|
||||
"to": "state.energy",
|
||||
"transform": {
|
||||
"op": "multiply",
|
||||
"args": [
|
||||
{ "ref": "parameters.activity" },
|
||||
1.5
|
||||
]
|
||||
},
|
||||
"source": "parameters.activity",
|
||||
"target": "state.energy",
|
||||
"scale": 1.5,
|
||||
"smoothing": "250ms"
|
||||
}
|
||||
]
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"tickSeconds": 0.016666666666666666,
|
||||
"traces": {
|
||||
"directBusOverride": [
|
||||
{ "tick": 0, "storedActivity": 0.4, "binding": 0.4, "override": 0.8, "resolved": 0.8 },
|
||||
{ "tick": 1, "storedActivity": 0.2, "binding": 0.2, "override": 0.8, "resolved": 0.8 },
|
||||
{ "tick": 2, "storedActivity": 0.2, "binding": 0.2, "releaseProgress": 0.5, "resolved": 0.5 },
|
||||
{ "tick": 3, "storedActivity": 0.2, "binding": 0.2, "releaseProgress": 1, "resolved": 0.2 }
|
||||
],
|
||||
"parameterOverrideFeedsBinding": [
|
||||
{ "tick": 0, "storedActivity": 0.3, "activityOverride": 0.9, "resolvedActivity": 0.9, "busGain": 0.95 },
|
||||
{ "tick": 1, "storedActivity": 0.2, "activityOverride": 0.9, "resolvedActivity": 0.9, "busGain": 0.95 },
|
||||
{ "tick": 2, "storedActivity": 0.2, "releaseProgress": 0.5, "resolvedActivity": 0.55, "busGain": 0.775 },
|
||||
{ "tick": 3, "storedActivity": 0.2, "releaseProgress": 1, "resolvedActivity": 0.2, "busGain": 0.6 }
|
||||
],
|
||||
"automationMaskedThenReleased": [
|
||||
{ "tick": 0, "automation": 0.25, "override": 0.75, "resolved": 0.75 },
|
||||
{ "tick": 1, "automation": 0.45, "override": 0.75, "resolved": 0.75 },
|
||||
{ "tick": 2, "automation": 0.65, "releaseProgress": 0.5, "resolved": 0.7 },
|
||||
{ "tick": 3, "automation": 0.85, "releaseProgress": 1, "resolved": 0.85 }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{ "tick": 1, "low": 0.1, "mid": 0.2, "high": 0.3, "energy": 0.2 },
|
||||
{ "tick": 2, "low": 0.8, "mid": 0.4, "high": 0.1, "energy": 0.55 },
|
||||
{ "tick": 3, "low": 0.3, "mid": 0.7, "high": 0.5, "energy": 0.6 },
|
||||
{ "tick": 4, "low": 0.0, "mid": 0.1, "high": 0.2, "energy": 0.1 }
|
||||
]
|
||||
@@ -0,0 +1,128 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
|
||||
import { ExhibitValidator } from '../tools/validate-exhibit.mjs';
|
||||
import {
|
||||
attackValue,
|
||||
bindingValue,
|
||||
releaseValue,
|
||||
resolveNumericTarget,
|
||||
selectWinningOverride,
|
||||
smoothBinding
|
||||
} from '../tools/gc3-resolution-model.mjs';
|
||||
|
||||
const fixture = JSON.parse(readFileSync(new URL('./fixtures/gc3/resolution-traces.json', import.meta.url)));
|
||||
const closeTo = (actual, expected, epsilon = 1e-12) => assert.ok(Math.abs(actual - expected) <= epsilon, `${actual} != ${expected}`);
|
||||
|
||||
test('direct bus override masks a live binding and releases toward its current value', () => {
|
||||
for (const row of fixture.traces.directBusOverride) {
|
||||
const overrides = row.override === undefined
|
||||
? []
|
||||
: [{ id: 'direct', priority: 0, activationSequence: 1, value: row.override }];
|
||||
let resolved = resolveNumericTarget({ base: 0, binding: row.binding, overrides, safetyClamp: [0, 1] }).resolved;
|
||||
if (row.releaseProgress !== undefined) resolved = releaseValue(0.8, row.binding, row.releaseProgress);
|
||||
closeTo(resolved, row.resolved);
|
||||
}
|
||||
});
|
||||
|
||||
test('a parameter override feeds a downstream binding in the same tick and preserves user edits', () => {
|
||||
for (const row of fixture.traces.parameterOverrideFeedsBinding) {
|
||||
let activity = row.activityOverride ?? row.storedActivity;
|
||||
if (row.releaseProgress !== undefined) activity = releaseValue(0.9, row.storedActivity, row.releaseProgress);
|
||||
const gain = bindingValue(activity, { scale: 0.5, offset: 0.5, clamp: [0, 1] });
|
||||
closeTo(activity, row.resolvedActivity);
|
||||
closeTo(gain, row.busGain);
|
||||
}
|
||||
});
|
||||
|
||||
test('priority and activation sequence decide competition independently of scope', () => {
|
||||
const overrides = [
|
||||
{ id: 'scenario', scope: 'scenario', priority: 4, activationSequence: 2, value: 0.4 },
|
||||
{ id: 'duration-low', scope: 'duration', priority: 3, activationSequence: 99, value: 0.9 }
|
||||
];
|
||||
assert.equal(selectWinningOverride(overrides).id, 'scenario');
|
||||
|
||||
overrides.push({ id: 'duration-tie', scope: 'duration', priority: 4, activationSequence: 3, value: 0.7 });
|
||||
assert.equal(selectWinningOverride(overrides).id, 'duration-tie');
|
||||
});
|
||||
|
||||
test('automation continues while masked and becomes the live release destination', () => {
|
||||
for (const row of fixture.traces.automationMaskedThenReleased) {
|
||||
const overrides = row.override === undefined
|
||||
? []
|
||||
: [{ id: 'mask', priority: 1, activationSequence: 1, value: row.override }];
|
||||
let resolved = resolveNumericTarget({ base: 0, automation: row.automation, overrides }).resolved;
|
||||
if (row.releaseProgress !== undefined) resolved = releaseValue(0.75, row.automation, row.releaseProgress);
|
||||
closeTo(resolved, row.resolved);
|
||||
}
|
||||
});
|
||||
|
||||
test('legal modulation follows override and safety clamp runs last', () => {
|
||||
const result = resolveNumericTarget({
|
||||
base: 0.2,
|
||||
overrides: [{ id: 'boost', priority: 0, activationSequence: 1, value: 0.9 }],
|
||||
modulation: 0.3,
|
||||
safetyClamp: [0, 1]
|
||||
});
|
||||
assert.equal(result.afterOverride, 0.9);
|
||||
closeTo(result.beforeClamp, 1.2);
|
||||
assert.equal(result.resolved, 1);
|
||||
});
|
||||
|
||||
test('binding smoothing is exact, and re-enable initializes without replaying disabled time', () => {
|
||||
const first = smoothBinding(undefined, 0.8, fixture.tickSeconds, 0.05);
|
||||
assert.equal(first, 0.8);
|
||||
const second = smoothBinding(first, 0.2, fixture.tickSeconds, 0.05);
|
||||
closeTo(second, 0.8 + (1 - Math.exp(-fixture.tickSeconds / 0.05)) * (0.2 - 0.8));
|
||||
assert.equal(smoothBinding(undefined, 0.35, fixture.tickSeconds, 0.05), 0.35);
|
||||
});
|
||||
|
||||
test('interrupted transitions begin from the visible value without cancelling lower overrides', () => {
|
||||
closeTo(attackValue(0.2, 0.8, 0.5, 'linear'), 0.5);
|
||||
const lower = { id: 'lower', priority: 1, activationSequence: 1, value: 0.5 };
|
||||
const higher = { id: 'higher', priority: 2, activationSequence: 2, value: attackValue(0.5, 1, 0, 'linear') };
|
||||
assert.equal(selectWinningOverride([lower, higher]).value, 0.5);
|
||||
higher.live = false;
|
||||
assert.equal(selectWinningOverride([lower, higher]).id, 'lower');
|
||||
});
|
||||
|
||||
test('validator rejects conflicting writers, cycles, unsupported targets, and draft aliases', () => {
|
||||
const base = {
|
||||
xzbt: '0.1',
|
||||
meta: { id: 'gc3-validation', name: 'GC3 validation' },
|
||||
parameters: { activity: { type: 'number', default: 0.5 } },
|
||||
state: { energy: { type: 'number', initial: 0.2 } }
|
||||
};
|
||||
|
||||
const cases = [
|
||||
{
|
||||
code: 'ERR_CONFLICTING_BINDING',
|
||||
bindings: [
|
||||
{ source: 'parameters.activity', target: 'state.energy' },
|
||||
{ source: 'parameters.activity', target: 'state.energy', when: { op: 'gt', left: 1, right: 0 } }
|
||||
]
|
||||
},
|
||||
{
|
||||
code: 'ERR_CYCLIC_DEPENDENCY',
|
||||
bindings: [
|
||||
{ source: 'parameters.activity', target: 'state.energy' },
|
||||
{ source: 'state.energy', target: 'parameters.activity' }
|
||||
]
|
||||
},
|
||||
{
|
||||
code: 'ERR_UNSUPPORTED_TARGET',
|
||||
bindings: [{ source: 'parameters.activity', target: 'signals.time.elapsed' }]
|
||||
},
|
||||
{
|
||||
code: 'ERR_UNKNOWN_FIELD',
|
||||
bindings: [{ from: 'parameters.activity', to: 'state.energy' }]
|
||||
}
|
||||
];
|
||||
|
||||
for (const entry of cases) {
|
||||
const result = new ExhibitValidator({ ...base, bindings: entry.bindings }).validate();
|
||||
assert.equal(result.valid, false);
|
||||
assert.ok(result.errors.some((error) => error.code === entry.code), JSON.stringify(result.errors));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
createRandomStream,
|
||||
deriveStreamState,
|
||||
FixedStepClock,
|
||||
GC4_CONSTANTS,
|
||||
planAudioUnlock
|
||||
} from '../tools/gc4-time-model.mjs';
|
||||
|
||||
function simulateFrames(frameDurationMs, durationMs) {
|
||||
const clock = new FixedStepClock();
|
||||
const decisions = [];
|
||||
clock.advance(0);
|
||||
for (let now = frameDurationMs; now <= durationMs + 1e-7; now += frameDurationMs) {
|
||||
for (const tick of clock.advance(Math.min(now, durationMs)).ticks) {
|
||||
if (tick % 10 === 0) decisions.push(tick);
|
||||
}
|
||||
}
|
||||
return { tickIndex: clock.tickIndex, decisions };
|
||||
}
|
||||
|
||||
test('render frequency does not change fixed-tick decisions', () => {
|
||||
const at60 = simulateFrames(1000 / 60, 1000);
|
||||
const at120 = simulateFrames(1000 / 120, 1000);
|
||||
assert.deepEqual(at60, at120);
|
||||
assert.equal(at60.tickIndex, 60);
|
||||
assert.deepEqual(at60.decisions, [10, 20, 30, 40, 50, 60]);
|
||||
});
|
||||
|
||||
test('long stalls have bounded work and do not replay discarded wall time', () => {
|
||||
const clock = new FixedStepClock();
|
||||
clock.advance(0);
|
||||
const stalled = clock.advance(1000);
|
||||
assert.equal(stalled.stalled, true);
|
||||
assert.equal(stalled.ticks.length, GC4_CONSTANTS.maxTicksPerTurn);
|
||||
assert.ok(clock.accumulatorMs < GC4_CONSTANTS.stepMs);
|
||||
});
|
||||
|
||||
test('visibility restoration does not clear explicit pause', () => {
|
||||
const clock = new FixedStepClock();
|
||||
clock.advance(0);
|
||||
clock.advance(GC4_CONSTANTS.stepMs);
|
||||
assert.equal(clock.tickIndex, 1);
|
||||
|
||||
clock.setPauseReason('explicit', true, 20);
|
||||
clock.setPauseReason('hidden', true, 20);
|
||||
clock.advance(10_000);
|
||||
clock.setPauseReason('hidden', false, 10_000);
|
||||
clock.advance(11_000);
|
||||
assert.equal(clock.tickIndex, 1);
|
||||
|
||||
clock.setPauseReason('explicit', false, 11_000);
|
||||
clock.advance(11_000 + GC4_CONSTANTS.stepMs);
|
||||
assert.equal(clock.tickIndex, 2);
|
||||
});
|
||||
|
||||
test('unlock skips pre-unlock one-shots and aligns a live continuous sound', () => {
|
||||
const plan = planAudioUnlock([
|
||||
{ id: 'expired', kind: 'one-shot', start: 1, end: 1.2 },
|
||||
{ id: 'partial', kind: 'one-shot', start: 4.9, end: 5.2 },
|
||||
{ id: 'ambience', kind: 'continuous', start: 2 },
|
||||
{ id: 'future', kind: 'one-shot', start: 5.05, end: 5.2 }
|
||||
], 5);
|
||||
assert.deepEqual(plan, [
|
||||
{ id: 'expired', action: 'skip' },
|
||||
{ id: 'partial', action: 'skip' },
|
||||
{ id: 'ambience', action: 'start-continuous', phase: 3, rampMs: 20 },
|
||||
{ id: 'future', action: 'schedule', delay: 0.04999999999999982 }
|
||||
]);
|
||||
});
|
||||
|
||||
test('PRNG state and output are stable known vectors', () => {
|
||||
assert.deepEqual(deriveStreamState(42, 'scenario', 'storm:1'), [
|
||||
471422921, 3327820418, 120898024, 15893603
|
||||
]);
|
||||
const stream = createRandomStream(42, 'scenario', 'storm:1');
|
||||
assert.deepEqual([
|
||||
stream.nextUint32(), stream.nextUint32(), stream.nextUint32(), stream.nextUint32()
|
||||
], [4101533927, 4149236977, 2113172072, 1672809915]);
|
||||
});
|
||||
|
||||
test('manual sampling does not perturb cadence or scenario streams', () => {
|
||||
const cadenceA = createRandomStream(7, 'cadence', 'routine:1');
|
||||
const cadenceB = createRandomStream(7, 'cadence', 'routine:1');
|
||||
const scenarioA = createRandomStream(7, 'scenario', 'surge:1');
|
||||
const scenarioB = createRandomStream(7, 'scenario', 'surge:1');
|
||||
const manual = createRandomStream(7, 'manual-sample', 'warning:1');
|
||||
|
||||
const before = [cadenceA.nextUint32(), scenarioA.nextUint32()];
|
||||
for (let index = 0; index < 100; index++) manual.nextUint32();
|
||||
const after = [cadenceA.nextUint32(), scenarioA.nextUint32()];
|
||||
|
||||
assert.deepEqual(before, [cadenceB.nextUint32(), scenarioB.nextUint32()]);
|
||||
assert.deepEqual(after, [cadenceB.nextUint32(), scenarioB.nextUint32()]);
|
||||
});
|
||||
|
||||
test('audio-reactive decisions use the same deterministic signal fixture at every render rate', () => {
|
||||
const signals = JSON.parse(readFileSync(new URL('./fixtures/gc4/deterministic-signals.json', import.meta.url)));
|
||||
const decide = () => signals.filter((signal) => signal.energy >= 0.5).map((signal) => signal.tick);
|
||||
assert.deepEqual(decide(), [2, 3]);
|
||||
assert.deepEqual(decide(), decide());
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
CLEANUP_TICKS,
|
||||
ConditionTrigger,
|
||||
DeferredStartQueue,
|
||||
DISPATCH_BUDGET,
|
||||
drainDispatch,
|
||||
OwnershipModel
|
||||
} from '../tools/gc5-lifecycle-model.mjs';
|
||||
|
||||
test('resources created through nested events inherit the scenario owner', () => {
|
||||
const runtime = new OwnershipModel();
|
||||
runtime.startScenario('scenario-1');
|
||||
runtime.executeActions([{ type: 'event', actions: [{ type: 'event', actions: [{ type: 'resource', kind: 'sound' }] }] }], 'scenario-1');
|
||||
assert.equal(runtime.ownedResourceCount('scenario-1'), 1);
|
||||
});
|
||||
|
||||
test('owner termination bounds duration override release and eventually disposes it', () => {
|
||||
const runtime = new OwnershipModel();
|
||||
runtime.startScenario('scenario-1');
|
||||
runtime.executeActions([{ type: 'resource', kind: 'override', releaseTicks: CLEANUP_TICKS * 10 }], 'scenario-1');
|
||||
runtime.terminate('scenario-1', 'cancel');
|
||||
assert.equal(runtime.ownedResourceCount('scenario-1'), 0);
|
||||
assert.equal(runtime.resources.size, 1);
|
||||
runtime.tick(CLEANUP_TICKS);
|
||||
assert.equal(runtime.resources.size, 0);
|
||||
assert.ok(runtime.diagnostics.some((entry) => entry.code === 'WARN_CLEANUP_FORCED'));
|
||||
});
|
||||
|
||||
test('persistent set survives a later critical ordinary-action failure', () => {
|
||||
const runtime = new OwnershipModel();
|
||||
runtime.startScenario('scenario-1', { onCancel: [{ type: 'set', target: 'state.cleanup-ran', value: true }] });
|
||||
runtime.executeActions([
|
||||
{ type: 'set', target: 'state.mode', value: 'changed' },
|
||||
{ type: 'resource', kind: 'visual' },
|
||||
{ type: 'fail', critical: true }
|
||||
], 'scenario-1');
|
||||
assert.equal(runtime.state.get('state.mode'), 'changed');
|
||||
assert.equal(runtime.state.get('state.cleanup-ran'), true);
|
||||
assert.equal(runtime.owners.get('scenario-1').status, 'FAILED');
|
||||
assert.equal(runtime.resources.size, 0);
|
||||
});
|
||||
|
||||
test('startup and termination-hook failures still complete cleanup with the original terminal cause', () => {
|
||||
const runtime = new OwnershipModel();
|
||||
runtime.startScenario('scenario-1', {
|
||||
onStart: [{ type: 'resource', kind: 'audio' }, { type: 'fail', critical: true }],
|
||||
onCancel: [{ type: 'fail', critical: true }, { type: 'set', target: 'state.after-hook-failure', value: 1 }]
|
||||
});
|
||||
assert.equal(runtime.owners.get('scenario-1').status, 'FAILED');
|
||||
assert.equal(runtime.state.get('state.after-hook-failure'), 1);
|
||||
assert.equal(runtime.resources.size, 0);
|
||||
assert.equal(runtime.diagnostics.filter((entry) => entry.code === 'ERR_ACTION_FAILURE').length, 2);
|
||||
});
|
||||
|
||||
test('condition trigger requires a false tick before initial and subsequent firing', () => {
|
||||
const trigger = new ConditionTrigger(2);
|
||||
assert.equal(trigger.evaluate(true), false);
|
||||
assert.equal(trigger.evaluate(false), false);
|
||||
assert.equal(trigger.evaluate(true), false);
|
||||
assert.equal(trigger.evaluate(true), true);
|
||||
assert.equal(trigger.evaluate(true), false);
|
||||
assert.equal(trigger.evaluate(false), false);
|
||||
assert.equal(trigger.evaluate(true), false);
|
||||
assert.equal(trigger.evaluate(true), true);
|
||||
});
|
||||
|
||||
test('deferred queue keeps one request per definition, expires, rechecks, and orders deterministically', () => {
|
||||
const queue = new DeferredStartQueue(10);
|
||||
assert.equal(queue.defer({ definitionId: 'low', priority: 10, creationTick: 1, documentOrder: 0, sampledInputs: 0.2 }), true);
|
||||
assert.equal(queue.defer({ definitionId: 'low', priority: 99, creationTick: 2, documentOrder: 0, sampledInputs: 0.9 }), false);
|
||||
queue.defer({ definitionId: 'high-late', priority: 80, creationTick: 2, documentOrder: 2 });
|
||||
queue.defer({ definitionId: 'high-early', priority: 80, creationTick: 1, documentOrder: 1 });
|
||||
|
||||
const started = queue.dispatch(3, (request) => request.definitionId !== 'low');
|
||||
assert.deepEqual(started.map((request) => request.definitionId), ['high-early', 'high-late']);
|
||||
assert.equal(queue.requests.get('low').sampledInputs, 0.2);
|
||||
assert.deepEqual(queue.dispatch(11, () => true), []);
|
||||
assert.equal(queue.requests.size, 0);
|
||||
});
|
||||
|
||||
test('feedback is bounded and leaves cleanup capacity outside the ordinary budget', () => {
|
||||
const result = drainDispatch([{ path: 'event.loop', ownerId: 'scenario-1' }], (unit) => [unit]);
|
||||
assert.equal(result.exhausted, true);
|
||||
assert.equal(result.consumed, DISPATCH_BUDGET);
|
||||
assert.equal(result.discarded.length, 1);
|
||||
assert.deepEqual(result.failedOwnerIds, ['scenario-1']);
|
||||
|
||||
const runtime = new OwnershipModel();
|
||||
runtime.startScenario('scenario-1');
|
||||
runtime.executeActions([{ type: 'resource' }], 'scenario-1');
|
||||
runtime.terminate('scenario-1', 'failure');
|
||||
assert.equal(runtime.resources.size, 0);
|
||||
});
|
||||
|
||||
test('repeated completion, cancellation, and failure cycles return counters to baseline', () => {
|
||||
const runtime = new OwnershipModel();
|
||||
for (let index = 0; index < 30; index++) {
|
||||
const id = `scenario-${index}`;
|
||||
runtime.startScenario(id);
|
||||
runtime.executeActions([
|
||||
{ type: 'resource', kind: 'sound', releaseTicks: 2 },
|
||||
{ type: 'event', actions: [{ type: 'resource', kind: 'visual', releaseTicks: 3 }] }
|
||||
], id);
|
||||
runtime.terminate(id, index % 3 === 0 ? 'complete' : index % 3 === 1 ? 'cancel' : 'failure');
|
||||
runtime.tick(3);
|
||||
}
|
||||
assert.equal(runtime.resources.size, 0);
|
||||
assert.equal(runtime.owners.size, 30);
|
||||
assert.ok([...runtime.owners.values()].every((owner) => ['COMPLETED', 'CANCELLED', 'FAILED'].includes(owner.status)));
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Deterministic executable model for the XZBT 0.1 GC3 resolution contract.
|
||||
*
|
||||
* This is a Phase 0 contract oracle, not the production runtime resolver.
|
||||
*/
|
||||
|
||||
export function clamp(value, minimum, maximum) {
|
||||
return Math.min(maximum, Math.max(minimum, value));
|
||||
}
|
||||
|
||||
export function bindingValue(source, { scale = 1, offset = 0, clamp: limits } = {}) {
|
||||
let result = source * scale + offset;
|
||||
if (limits) result = clamp(result, limits[0], limits[1]);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function smoothBinding(previous, input, dtSeconds, tauSeconds) {
|
||||
if (tauSeconds === 0 || previous === undefined) return input;
|
||||
const alpha = 1 - Math.exp(-dtSeconds / tauSeconds);
|
||||
return previous + alpha * (input - previous);
|
||||
}
|
||||
|
||||
export function easingValue(name, t) {
|
||||
const bounded = clamp(t, 0, 1);
|
||||
switch (name) {
|
||||
case 'linear':
|
||||
return bounded;
|
||||
case 'ease-in':
|
||||
return bounded * bounded;
|
||||
case 'ease-out':
|
||||
return 1 - (1 - bounded) * (1 - bounded);
|
||||
case 'ease-in-out':
|
||||
return bounded < 0.5
|
||||
? 2 * bounded * bounded
|
||||
: 1 - ((-2 * bounded + 2) ** 2) / 2;
|
||||
default:
|
||||
throw new RangeError(`Unsupported easing: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function lerp(from, to, amount) {
|
||||
return from + (to - from) * amount;
|
||||
}
|
||||
|
||||
export function attackValue(origin, target, progress, easing = 'linear') {
|
||||
return lerp(origin, target, easingValue(easing, progress));
|
||||
}
|
||||
|
||||
export function releaseValue(releaseStart, currentLower, progress, easing = 'linear') {
|
||||
return lerp(currentLower, releaseStart, 1 - easingValue(easing, progress));
|
||||
}
|
||||
|
||||
export function selectWinningOverride(overrides) {
|
||||
return overrides
|
||||
.filter((override) => override.live !== false)
|
||||
.reduce((winner, candidate) => {
|
||||
if (!winner) return candidate;
|
||||
if (candidate.priority !== winner.priority) {
|
||||
return candidate.priority > winner.priority ? candidate : winner;
|
||||
}
|
||||
return candidate.activationSequence > winner.activationSequence ? candidate : winner;
|
||||
}, null);
|
||||
}
|
||||
|
||||
export function resolveNumericTarget({
|
||||
base,
|
||||
binding,
|
||||
automation,
|
||||
overrides = [],
|
||||
modulation = 0,
|
||||
safetyClamp = [-Infinity, Infinity]
|
||||
}) {
|
||||
const afterBinding = binding ?? base;
|
||||
const lower = automation ?? afterBinding;
|
||||
const winner = selectWinningOverride(overrides);
|
||||
const afterOverride = winner ? winner.value : lower;
|
||||
const beforeClamp = afterOverride + modulation;
|
||||
return {
|
||||
base,
|
||||
afterBinding,
|
||||
lower,
|
||||
winner: winner?.id ?? null,
|
||||
afterOverride,
|
||||
beforeClamp,
|
||||
resolved: clamp(beforeClamp, safetyClamp[0], safetyClamp[1])
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/** Phase 0 executable contract model for XZBT 0.1 GC4. */
|
||||
|
||||
const UINT32_RANGE = 0x1_0000_0000;
|
||||
const STEP_MS = 1000 / 60;
|
||||
|
||||
function rotateLeft(value, count) {
|
||||
return ((value << count) | (value >>> (32 - count))) >>> 0;
|
||||
}
|
||||
|
||||
function multiply32(left, right) {
|
||||
return Math.imul(left, right) >>> 0;
|
||||
}
|
||||
|
||||
export function fnv1a32(text) {
|
||||
let hash = 0x811c9dc5;
|
||||
for (const byte of new TextEncoder().encode(text)) {
|
||||
hash ^= byte;
|
||||
hash = multiply32(hash, 0x01000193);
|
||||
}
|
||||
return hash >>> 0;
|
||||
}
|
||||
|
||||
export function splitMix32(seed) {
|
||||
let state = seed >>> 0;
|
||||
return () => {
|
||||
state = (state + 0x9e3779b9) >>> 0;
|
||||
let value = state;
|
||||
value = multiply32(value ^ (value >>> 16), 0x21f0aaad);
|
||||
value = multiply32(value ^ (value >>> 15), 0x735a2d97);
|
||||
return (value ^ (value >>> 15)) >>> 0;
|
||||
};
|
||||
}
|
||||
|
||||
export function deriveStreamState(rootSeed, domain, stableInstanceKey) {
|
||||
if (!Number.isInteger(rootSeed) || rootSeed < 0 || rootSeed >= UINT32_RANGE) {
|
||||
throw new RangeError('Root seed must be an unsigned 32-bit integer.');
|
||||
}
|
||||
const hash = fnv1a32(`xzbt-0.1\0${rootSeed}\0${domain}\0${stableInstanceKey}`);
|
||||
const expand = splitMix32(hash);
|
||||
const state = [expand(), expand(), expand(), expand()];
|
||||
if (state.every((word) => word === 0)) state[3] = 1;
|
||||
return state;
|
||||
}
|
||||
|
||||
export class Xoshiro128StarStar {
|
||||
constructor(state) {
|
||||
if (!Array.isArray(state) || state.length !== 4 || state.some((word) => !Number.isInteger(word))) {
|
||||
throw new TypeError('xoshiro128** state must contain four integer words.');
|
||||
}
|
||||
this.state = state.map((word) => word >>> 0);
|
||||
if (this.state.every((word) => word === 0)) throw new RangeError('xoshiro128** state cannot be all zero.');
|
||||
}
|
||||
|
||||
nextUint32() {
|
||||
const state = this.state;
|
||||
const result = multiply32(rotateLeft(multiply32(state[1], 5), 7), 9);
|
||||
const temporary = (state[1] << 9) >>> 0;
|
||||
state[2] ^= state[0];
|
||||
state[3] ^= state[1];
|
||||
state[1] ^= state[2];
|
||||
state[0] ^= state[3];
|
||||
state[2] ^= temporary;
|
||||
state[3] = rotateLeft(state[3], 11);
|
||||
for (let index = 0; index < 4; index++) state[index] >>>= 0;
|
||||
return result >>> 0;
|
||||
}
|
||||
|
||||
nextFloat() {
|
||||
return this.nextUint32() / UINT32_RANGE;
|
||||
}
|
||||
}
|
||||
|
||||
export function createRandomStream(rootSeed, domain, stableInstanceKey) {
|
||||
return new Xoshiro128StarStar(deriveStreamState(rootSeed, domain, stableInstanceKey));
|
||||
}
|
||||
|
||||
export class FixedStepClock {
|
||||
constructor({ stepMs = STEP_MS, maxTicksPerTurn = 8, maxElapsedMs = 250 } = {}) {
|
||||
this.stepMs = stepMs;
|
||||
this.maxTicksPerTurn = maxTicksPerTurn;
|
||||
this.maxElapsedMs = maxElapsedMs;
|
||||
this.tickIndex = 0;
|
||||
this.accumulatorMs = 0;
|
||||
this.lastNowMs = undefined;
|
||||
this.pauseReasons = new Set();
|
||||
}
|
||||
|
||||
setPauseReason(reason, active, nowMs) {
|
||||
const wasPaused = this.pauseReasons.size > 0;
|
||||
if (active) this.pauseReasons.add(reason);
|
||||
else this.pauseReasons.delete(reason);
|
||||
const isPaused = this.pauseReasons.size > 0;
|
||||
if (wasPaused !== isPaused) this.lastNowMs = nowMs;
|
||||
}
|
||||
|
||||
advance(nowMs) {
|
||||
if (this.lastNowMs === undefined) {
|
||||
this.lastNowMs = nowMs;
|
||||
return { ticks: [], stalled: false };
|
||||
}
|
||||
if (this.pauseReasons.size > 0) {
|
||||
this.lastNowMs = nowMs;
|
||||
return { ticks: [], stalled: false };
|
||||
}
|
||||
|
||||
const observed = Math.max(0, nowMs - this.lastNowMs);
|
||||
this.lastNowMs = nowMs;
|
||||
const accepted = Math.min(observed, this.maxElapsedMs);
|
||||
this.accumulatorMs += accepted;
|
||||
const available = Math.floor((this.accumulatorMs + 1e-9) / this.stepMs);
|
||||
const count = Math.min(available, this.maxTicksPerTurn);
|
||||
const ticks = [];
|
||||
for (let index = 0; index < count; index++) ticks.push(++this.tickIndex);
|
||||
this.accumulatorMs -= count * this.stepMs;
|
||||
|
||||
const stalled = observed > this.maxElapsedMs || available > this.maxTicksPerTurn;
|
||||
if (available > this.maxTicksPerTurn) {
|
||||
this.accumulatorMs = Math.min(this.accumulatorMs, this.stepMs - 1e-9);
|
||||
}
|
||||
return { ticks, stalled };
|
||||
}
|
||||
}
|
||||
|
||||
export function planAudioUnlock(invocations, logicalSeconds) {
|
||||
return invocations.map((invocation) => {
|
||||
if (invocation.start < logicalSeconds) {
|
||||
if (invocation.kind === 'one-shot') return { id: invocation.id, action: 'skip' };
|
||||
if (invocation.end === undefined || invocation.end > logicalSeconds) {
|
||||
return { id: invocation.id, action: 'start-continuous', phase: logicalSeconds - invocation.start, rampMs: 20 };
|
||||
}
|
||||
return { id: invocation.id, action: 'dispose' };
|
||||
}
|
||||
return { id: invocation.id, action: 'schedule', delay: invocation.start - logicalSeconds };
|
||||
});
|
||||
}
|
||||
|
||||
export const GC4_CONSTANTS = Object.freeze({
|
||||
stepMs: STEP_MS,
|
||||
maxTicksPerTurn: 8,
|
||||
maxElapsedMs: 250,
|
||||
audioLookaheadMs: 100,
|
||||
unlockRampMs: 20
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
/** Phase 0 executable contract model for XZBT 0.1 GC5. */
|
||||
|
||||
export const DISPATCH_BUDGET = 1024;
|
||||
export const TERMINATION_HOOK_LIMIT = 256;
|
||||
export const CLEANUP_TICKS = 5 * 60;
|
||||
|
||||
export class ConditionTrigger {
|
||||
constructor(holdTicks = 0) {
|
||||
this.holdTicks = holdTicks;
|
||||
this.armed = false;
|
||||
this.trueTicks = 0;
|
||||
}
|
||||
|
||||
evaluate(value) {
|
||||
if (!value) {
|
||||
this.armed = true;
|
||||
this.trueTicks = 0;
|
||||
return false;
|
||||
}
|
||||
if (!this.armed) return false;
|
||||
this.trueTicks++;
|
||||
if (this.trueTicks < Math.max(1, this.holdTicks)) return false;
|
||||
this.armed = false;
|
||||
this.trueTicks = 0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class DeferredStartQueue {
|
||||
constructor(defaultExpiryTicks = 5 * 60 * 60) {
|
||||
this.defaultExpiryTicks = defaultExpiryTicks;
|
||||
this.requests = new Map();
|
||||
}
|
||||
|
||||
defer({ definitionId, priority = 50, creationTick, documentOrder, expiryTicks, sampledInputs }) {
|
||||
if (this.requests.has(definitionId)) return false;
|
||||
this.requests.set(definitionId, {
|
||||
definitionId,
|
||||
priority,
|
||||
creationTick,
|
||||
documentOrder,
|
||||
expiresAt: creationTick + (expiryTicks ?? this.defaultExpiryTicks),
|
||||
sampledInputs
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
dispatch(tick, isEligible) {
|
||||
for (const [id, request] of this.requests) {
|
||||
if (tick >= request.expiresAt) this.requests.delete(id);
|
||||
}
|
||||
const ordered = [...this.requests.values()].sort((left, right) =>
|
||||
right.priority - left.priority || left.creationTick - right.creationTick || left.documentOrder - right.documentOrder
|
||||
);
|
||||
const started = [];
|
||||
for (const request of ordered) {
|
||||
if (!isEligible(request)) continue;
|
||||
started.push(request);
|
||||
this.requests.delete(request.definitionId);
|
||||
}
|
||||
return started;
|
||||
}
|
||||
}
|
||||
|
||||
export function drainDispatch(initialQueue, expand, budget = DISPATCH_BUDGET) {
|
||||
const queue = [...initialQueue];
|
||||
const processed = [];
|
||||
let consumed = 0;
|
||||
while (queue.length > 0 && consumed < budget) {
|
||||
const unit = queue.shift();
|
||||
consumed++;
|
||||
processed.push(unit);
|
||||
const added = expand(unit) || [];
|
||||
queue.push(...added);
|
||||
}
|
||||
return {
|
||||
processed,
|
||||
consumed,
|
||||
exhausted: queue.length > 0,
|
||||
discarded: queue,
|
||||
failedOwnerIds: [...new Set(queue.map((unit) => unit.ownerId).filter(Boolean))]
|
||||
};
|
||||
}
|
||||
|
||||
export class OwnershipModel {
|
||||
constructor() {
|
||||
this.tickIndex = 0;
|
||||
this.state = new Map();
|
||||
this.owners = new Map();
|
||||
this.resources = new Map();
|
||||
this.diagnostics = [];
|
||||
this.nextResourceId = 1;
|
||||
}
|
||||
|
||||
startScenario(id, { onStart = [], onComplete = [], onCancel = [] } = {}) {
|
||||
const owner = { id, status: 'STARTING', blocked: false, onComplete, onCancel, terminalCause: null };
|
||||
this.owners.set(id, owner);
|
||||
const succeeded = this.executeActions(onStart, id, 'ordinary');
|
||||
if (succeeded) owner.status = 'ACTIVE';
|
||||
return owner;
|
||||
}
|
||||
|
||||
executeActions(actions, ownerId, phase = 'ordinary') {
|
||||
const owner = this.owners.get(ownerId);
|
||||
for (let index = 0; index < actions.length; index++) {
|
||||
const action = actions[index];
|
||||
if (phase === 'ordinary' && owner?.blocked) return false;
|
||||
try {
|
||||
if (phase === 'termination' && !['set', 'sound', 'fail'].includes(action.type)) {
|
||||
throw new Error(`Action '${action.type}' is not permitted in a termination hook.`);
|
||||
}
|
||||
if (action.type === 'set') {
|
||||
this.state.set(action.target, action.value);
|
||||
} else if (action.type === 'sound') {
|
||||
if (action.continuous || action.ownership === 'persistent') {
|
||||
throw new Error('Termination hooks permit only nonpersistent one-shot sounds.');
|
||||
}
|
||||
this.createResource(ownerId, { kind: 'termination-sound', releaseTicks: action.releaseTicks ?? 0 });
|
||||
} else if (action.type === 'resource') {
|
||||
this.createResource(ownerId, action);
|
||||
} else if (action.type === 'event') {
|
||||
if (!this.executeActions(action.actions || [], ownerId, phase)) return false;
|
||||
} else if (action.type === 'fail') {
|
||||
throw new Error(action.message || 'injected failure');
|
||||
}
|
||||
} catch (error) {
|
||||
this.diagnostics.push({ code: 'ERR_ACTION_FAILURE', ownerId, phase, index, message: error.message });
|
||||
if (phase === 'termination') continue;
|
||||
if (action.critical) {
|
||||
this.terminate(ownerId, 'failure');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
createResource(ownerId, { kind = 'generic', releaseTicks = 0, ownership = 'scenario' } = {}) {
|
||||
const effectiveOwner = ownership === 'persistent' ? 'performance' : ownerId;
|
||||
const id = `resource-${this.nextResourceId++}`;
|
||||
this.resources.set(id, { id, kind, ownerId: effectiveOwner, releaseTicks, cleanupAt: null });
|
||||
return id;
|
||||
}
|
||||
|
||||
terminate(ownerId, cause) {
|
||||
const owner = this.owners.get(ownerId);
|
||||
if (!owner || owner.blocked) return;
|
||||
owner.blocked = true;
|
||||
owner.terminalCause = cause;
|
||||
const hook = cause === 'complete' ? owner.onComplete : owner.onCancel;
|
||||
this.executeActions(hook.slice(0, TERMINATION_HOOK_LIMIT), ownerId, 'termination');
|
||||
if (hook.length > TERMINATION_HOOK_LIMIT) {
|
||||
this.diagnostics.push({ code: 'ERR_DISPATCH_BUDGET', ownerId, phase: 'termination' });
|
||||
}
|
||||
|
||||
for (const resource of this.resources.values()) {
|
||||
if (resource.ownerId !== ownerId) continue;
|
||||
const boundedRelease = Math.min(resource.releaseTicks, CLEANUP_TICKS);
|
||||
if (boundedRelease === 0) this.resources.delete(resource.id);
|
||||
else {
|
||||
resource.ownerId = `cleanup:${ownerId}`;
|
||||
resource.cleanupAt = this.tickIndex + boundedRelease;
|
||||
resource.forceAt = this.tickIndex + CLEANUP_TICKS;
|
||||
resource.forceRequired = resource.releaseTicks > CLEANUP_TICKS;
|
||||
}
|
||||
}
|
||||
owner.status = cause === 'complete' ? 'COMPLETED' : cause === 'failure' ? 'FAILED' : 'CANCELLED';
|
||||
}
|
||||
|
||||
tick(count = 1) {
|
||||
for (let step = 0; step < count; step++) {
|
||||
this.tickIndex++;
|
||||
for (const resource of [...this.resources.values()]) {
|
||||
if (!resource.cleanupAt || this.tickIndex < resource.cleanupAt) continue;
|
||||
if (resource.forceRequired && this.tickIndex >= resource.forceAt) {
|
||||
this.diagnostics.push({ code: 'WARN_CLEANUP_FORCED', resourceId: resource.id });
|
||||
}
|
||||
this.resources.delete(resource.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ownedResourceCount(ownerId) {
|
||||
return [...this.resources.values()].filter((resource) => resource.ownerId === ownerId).length;
|
||||
}
|
||||
}
|
||||
+136
-13
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* XZBT 0.1 Structural & Semantic Exhibit Validator
|
||||
* Zero external dependencies. Conforms to XZBT Format Specification 0.1 (Revision 0.2).
|
||||
* Zero external dependencies. Conforms to XZBT Format Specification 0.1 (Revision 0.3).
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
@@ -11,6 +11,20 @@ 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 RUNTIME_SIGNAL_TYPES = new Map([
|
||||
['signals.time.elapsed', 'number'],
|
||||
['signals.time.delta', 'number'],
|
||||
['signals.audio.low', 'number'],
|
||||
['signals.audio.mid', 'number'],
|
||||
['signals.audio.high', 'number'],
|
||||
['signals.audio.energy', 'number'],
|
||||
['signals.pointer.x', 'number'],
|
||||
['signals.pointer.y', 'number'],
|
||||
['signals.viewport.width', 'number'],
|
||||
['signals.viewport.height', 'number'],
|
||||
['signals.scenario.active', 'boolean']
|
||||
]);
|
||||
|
||||
const ALLOWED_ROOT_KEYS = new Set([
|
||||
'xzbt',
|
||||
'meta',
|
||||
@@ -58,6 +72,16 @@ const ALLOWED_STATE_KEYS = new Set([
|
||||
'max'
|
||||
]);
|
||||
|
||||
const ALLOWED_BINDING_KEYS = new Set([
|
||||
'source',
|
||||
'target',
|
||||
'scale',
|
||||
'offset',
|
||||
'clamp',
|
||||
'smoothing',
|
||||
'when'
|
||||
]);
|
||||
|
||||
const OPERATOR_ARITY = {
|
||||
abs: 1,
|
||||
negate: 1,
|
||||
@@ -389,37 +413,92 @@ export class ExhibitValidator {
|
||||
}
|
||||
|
||||
const graph = new Map(); // target -> [sources]
|
||||
const targetWriters = new Map();
|
||||
|
||||
this.doc.bindings.forEach((binding, idx) => {
|
||||
const path = `$.bindings[${idx}]`;
|
||||
if (typeof binding !== 'object' || binding === null) {
|
||||
if (typeof binding !== 'object' || binding === null || Array.isArray(binding)) {
|
||||
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}'.`);
|
||||
for (const key of Object.keys(binding)) {
|
||||
if (!ALLOWED_BINDING_KEYS.has(key)) {
|
||||
this.addError('ERR_UNKNOWN_FIELD', `${path}.${key}`, `Unrecognized field in BindingSpec: '${key}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof binding.source !== 'string' || !REF_PATH_REGEX.test(binding.source)) {
|
||||
this.addError('ERR_INVALID_REFERENCE', `${path}.source`, `Invalid source reference: '${binding.source}'.`);
|
||||
} else {
|
||||
this.resolveReference(binding.from, `${path}.from`);
|
||||
this.resolveReference(binding.source, `${path}.source`);
|
||||
}
|
||||
|
||||
if (typeof binding.to !== 'string' || !REF_PATH_REGEX.test(binding.to)) {
|
||||
this.addError('ERR_INVALID_REFERENCE', `${path}.to`, `Invalid to reference: '${binding.to}'.`);
|
||||
if (typeof binding.target !== 'string' || !REF_PATH_REGEX.test(binding.target)) {
|
||||
this.addError('ERR_INVALID_REFERENCE', `${path}.target`, `Invalid target reference: '${binding.target}'.`);
|
||||
} else {
|
||||
this.validateBindingTarget(binding.target, `${path}.target`);
|
||||
}
|
||||
|
||||
if (binding.transform) {
|
||||
this.validateValueSpec(binding.transform, `${path}.transform`);
|
||||
if (binding.scale !== undefined && (typeof binding.scale !== 'number' || !Number.isFinite(binding.scale))) {
|
||||
this.addError('ERR_TYPE_MISMATCH', `${path}.scale`, 'Binding scale must be a finite number.');
|
||||
}
|
||||
|
||||
if (binding.smoothing) {
|
||||
if (binding.offset !== undefined && (typeof binding.offset !== 'number' || !Number.isFinite(binding.offset))) {
|
||||
this.addError('ERR_TYPE_MISMATCH', `${path}.offset`, 'Binding offset must be a finite number.');
|
||||
}
|
||||
|
||||
if (binding.clamp !== undefined) {
|
||||
if (
|
||||
!Array.isArray(binding.clamp) ||
|
||||
binding.clamp.length !== 2 ||
|
||||
!binding.clamp.every((value) => typeof value === 'number' && Number.isFinite(value))
|
||||
) {
|
||||
this.addError('ERR_SCHEMA_VALIDATION', `${path}.clamp`, 'Binding clamp must be two finite numbers.');
|
||||
} else if (binding.clamp[0] > binding.clamp[1]) {
|
||||
this.addError('ERR_OUT_OF_BOUNDS', `${path}.clamp`, 'Binding clamp minimum cannot exceed maximum.');
|
||||
}
|
||||
}
|
||||
|
||||
if (binding.smoothing !== undefined) {
|
||||
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);
|
||||
if (binding.when !== undefined) {
|
||||
this.validateConditionSpec(binding.when, `${path}.when`);
|
||||
}
|
||||
|
||||
const sourceType = this.getReferenceType(binding.source);
|
||||
const targetType = this.getReferenceType(binding.target);
|
||||
if (sourceType && targetType) {
|
||||
const usesNumericTransform =
|
||||
binding.scale !== undefined || binding.offset !== undefined || binding.clamp !== undefined ||
|
||||
(binding.smoothing !== undefined && binding.smoothing !== '0ms');
|
||||
const sourceNumeric = sourceType === 'number' || sourceType === 'integer';
|
||||
const targetNumeric = targetType === 'number' || targetType === 'integer';
|
||||
if (usesNumericTransform && (!sourceNumeric || !targetNumeric)) {
|
||||
this.addError('ERR_TYPE_MISMATCH', path, 'Binding transforms and smoothing require numeric source and target types.');
|
||||
} else if (!usesNumericTransform && sourceType !== targetType && !(sourceNumeric && targetNumeric)) {
|
||||
this.addError('ERR_TYPE_MISMATCH', path, `Binding source type '${sourceType}' does not match target type '${targetType}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof binding.source === 'string' && typeof binding.target === 'string') {
|
||||
if (!graph.has(binding.target)) graph.set(binding.target, []);
|
||||
graph.get(binding.target).push(binding.source);
|
||||
|
||||
const priorWriter = targetWriters.get(binding.target);
|
||||
if (priorWriter !== undefined) {
|
||||
this.addError(
|
||||
'ERR_CONFLICTING_BINDING',
|
||||
`${path}.target`,
|
||||
`Binding target '${binding.target}' is already written by $.bindings[${priorWriter}].`
|
||||
);
|
||||
} else {
|
||||
targetWriters.set(binding.target, idx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -454,6 +533,41 @@ export class ExhibitValidator {
|
||||
}
|
||||
}
|
||||
|
||||
validateBindingTarget(refPath, location) {
|
||||
const parts = refPath.split('.');
|
||||
const namespace = parts[0];
|
||||
|
||||
if (namespace === 'parameters' || namespace === 'state') {
|
||||
if (parts.length !== 2) {
|
||||
this.addError('ERR_UNSUPPORTED_TARGET', location, `Target '${refPath}' is not a scalar ${namespace} target.`);
|
||||
return;
|
||||
}
|
||||
this.resolveReference(refPath, location);
|
||||
return;
|
||||
}
|
||||
|
||||
if (namespace === 'audio' && parts.length === 4 && parts[1] === 'buses' && parts[3] === 'gain') {
|
||||
const busId = parts[2];
|
||||
if (!this.doc.audio?.buses?.[busId]) {
|
||||
this.addError('ERR_INVALID_REFERENCE', location, `Target '${refPath}' refers to a missing audio bus '${busId}'.`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.addError('ERR_UNSUPPORTED_TARGET', location, `Target '${refPath}' is not exposed by the shared 0.1 target registry.`);
|
||||
}
|
||||
|
||||
getReferenceType(refPath) {
|
||||
if (typeof refPath !== 'string') return null;
|
||||
const parts = refPath.split('.');
|
||||
if (parts[0] === 'parameters' && parts.length === 2) return this.doc.parameters?.[parts[1]]?.type || null;
|
||||
if (parts[0] === 'state' && parts.length === 2) return this.doc.state?.[parts[1]]?.type || null;
|
||||
if (parts[0] === 'audio' && parts[1] === 'buses' && parts[3] === 'gain') return 'number';
|
||||
if (parts[0] === 'signals') return RUNTIME_SIGNAL_TYPES.get(refPath) || null;
|
||||
if (parts[0] === 'modulators') return 'number';
|
||||
return null;
|
||||
}
|
||||
|
||||
resolveReference(refPath, location) {
|
||||
const parts = refPath.split('.');
|
||||
const namespace = parts[0];
|
||||
@@ -476,6 +590,15 @@ export class ExhibitValidator {
|
||||
`Reference '${refPath}' refers to non-existent state variable '${stateId}'.`
|
||||
);
|
||||
}
|
||||
} else if (namespace === 'signals') {
|
||||
if (!RUNTIME_SIGNAL_TYPES.has(refPath)) {
|
||||
this.addError('ERR_INVALID_REFERENCE', location, `Reference '${refPath}' is not a declared 0.1 runtime signal.`);
|
||||
}
|
||||
} else if (namespace === 'modulators') {
|
||||
const modulatorId = parts[1];
|
||||
if (!this.doc.modulators || !this.doc.modulators[modulatorId]) {
|
||||
this.addError('ERR_INVALID_REFERENCE', location, `Reference '${refPath}' refers to non-existent modulator '${modulatorId}'.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user