Revision 0.9 adds section 20, the cadence and event subsystems contract, and carries two corrections the implementation forced. Section 6.1 now states that a duration is the authored literal or a non-negative finite number already in milliseconds, since a DurationSpec may be the resolved output of a ValueSpec or a bounded TimeSpec, with the one documented exception of an automation track's `at`, which 19.1 keeps literal-only so that point ordering stays decidable at import. Section 20.11 documents the rejection of an undeclared input name in an event action's `with` map as ERR_UNKNOWN_FIELD — the section's own convention for that shape of error, replacing an invented code that appeared nowhere in the registry. The review record is committed with the code it describes: the two code triages that found these defects, the reconciliation plan that sequenced the fixes, and a follow-up debt record listing what was deliberately left open — the unchecked JSON Schema artifact, degenerate path arcs, post-effect transient allocation, the window-traffic fixture's per-copy wrap bounds, and the unstated `ownership: "persistent"` value on a sound action. None of the five blocks phase 6; all five are written down rather than dropped. Devlog entries are backfilled for the two milestones that had none: phase 3c slice 2, the audio lifecycle and voice ceilings, and slice 4d, the renderer core. The implementation status summary now reflects the reconciled state rather than the in-flight one. 231 tests pass. tools/verify-spec-contract.py reports 46 declared diagnostic codes with every used code resolving and its two long-standing unresolved cross-references unchanged. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ShxxFqFmCUDQnQvFNm4TKy
94 lines
17 KiB
Markdown
94 lines
17 KiB
Markdown
# Independent review — 2026-09-06 (America/Los_Angeles)
|
||
|
||
## Review scope
|
||
|
||
- **Local review date:** 2026-09-06 (America/Los_Angeles; review executed ~10:30–18:04 UTC = 03:30–11:04 PDT).
|
||
- **Branch:** `main`. **HEAD:** `0af58da89dd6095fa9ca3ed546a2e48d7c7971b7` (`feat(visual): implement the slice 4d renderer core`).
|
||
- **Reviewed commits** (authored 2026-09-06 in America/Los_Angeles; day boundary = 07:00 UTC):
|
||
- `1bc4901` docs(visual): sections 17–19 review triage at revision 0.8 (spec/plan docs, new `tools/verify-spec-contract.py`).
|
||
- `6587d3e` feat(visual): align schema, validator, resolution engine with the 0.8 visual contract (`schema/xzbt-0.1.schema.json`, `resolution.js`, `validator.js`, new `visual-contract.js` / `visual-validation.js`, `exhibits/minimal-visual.xzbt`, tests).
|
||
- `699492f` build(visual): bundle visual modules into `XZBT.html`.
|
||
- `0af58da` feat(visual): slice 4d renderer core (`visual-engine.js`, `visual-geometry.js`, `visual-math.js`, `visual-canvas2d.js`, `visual-diagnostics.js`, `visual-subsystem.js`, app/performance wiring).
|
||
- Boundary note: `527220e` and earlier (04:30 UTC and before) fall on 2026-09-05 in America/Los_Angeles and were excluded.
|
||
- **Uncommitted work reviewed:** 17 modified tracked files (`XZBT.html`, three docs, `package.json`, `src/runtime/{actions,app,audio-automation,performance,resolution,visual-canvas2d,visual-diagnostics,visual-engine,visual-subsystem,visual-validation}.js`, `test/phase1-runtime.test.mjs`, `tools/build-xzbt.mjs`) and today's untracked additions: 9 new `src/runtime/visual-*.js` modules, `test/phase4-{procedural,execution,challenges}.test.mjs`, `tools/{build-visual-acceptance,visual-challenge-fixtures}.mjs`, `exhibits/exhibit-{a..d}.xzbt`, `exhibits/visual-challenges/` (14 fixtures), `prototypes/phase4/`, `docs/evidence/phase4/2026-09-06-phase4{e,f,g}-*.md`.
|
||
- **Timing uncertainty:** uncommitted changes carry no commit timestamps. The visual runtime modules' file mtimes (today 09:44–10:19 PDT) and the dated evidence docs indicate they are today's slices 4e–4g work. `.labyricorn/devlog/second-test-entry/` (content dated 2026-08-13), `Claude outputs/` (2026-09-05), and `.abacusai/` are older/unrelated leftovers, not reviewed as today's work. Nothing is staged.
|
||
- **Scope limitations / method:** Per instructions, existing files under `reviews/` were not opened. Conclusions are drawn from the code, the format specification (docs/XZBT_0-1_Format_Specification.md), the test suites, and runtime probe scripts (written, executed, and deleted; no implementation files modified). Two untracked files initially served stale read content, so every finding below was re-verified against current disk state and/or an executing probe. Browser rendering, GC6 performance measurement, and audio behavior are out of verifiable scope (no display/hardware here).
|
||
|
||
## Findings (ordered by severity)
|
||
|
||
### 1. P1 — Production render path never creates compositing surfaces: group masks crash the visual subsystem; layer opacity/blend silently ignored
|
||
|
||
- **Files:** `src/runtime/visual-subsystem.js:61-63`; `src/runtime/visual-canvas2d.js:99,230-243`; `src/runtime/app.js:42-49`.
|
||
- **Triggering conditions:** Any exhibit rendered through the product (`XZBT.html`) or the new acceptance page (both use `VisualSubsystem.render`) that (a) puts a `mask` on a `group` object, or (b) sets `opacity`/`blend` on a layer — including via the new 8.1 binding/automation on `visuals.layers.<id>.opacity` (today's `exhibits/exhibit-d.xzbt` does exactly this).
|
||
- **Impact:** (a) `renderFrame` is called without `createSurface`, so the mask branch of `drawNode` gets `null` from the dummy surface provider (`visual-canvas2d.js:233` returns `target`, which is `null` for unbuffered layers) and throws `TypeError: Cannot read properties of null (reading 'context')`; `app.js` `onFrame` catches, logs an error, and calls `visual.deactivate()` — the entire visual subsystem shuts down permanently on the first frame. (b) Layer `opacity`/`blend` are only applied in the buffered composite branch (`visual-canvas2d.js:236`); with no surface pool every layer draws inline and layer opacity is never applied — a bindable 8.1 property silently does nothing in the browser.
|
||
- **Evidence:** Probe: engine + `renderFrame(stub, plan, { warnEffect })` (the exact production call shape) on a masked-group document → `TypeError`; same plan with a `createSurface` factory → renders fine. Layer probe: `layers.main.opacity = 0.5`, plan reports `buffered: true`; with factory, `globalAlpha` sequence `[1, 1, 0.5]`; production path `[1, 1]` (opacity dropped). `tools/build-visual-acceptance.mjs:34` instantiates `VisualSubsystem` the same way, so the 4g acceptance page has the same gap.
|
||
- **Suggested correction:** Pass a surface factory from `VisualSubsystem.render`, e.g. `createSurface: (w, h) => typeof OffscreenCanvas === 'function' ? new OffscreenCanvas(w, h) : Object.assign(document.createElement('canvas'), { width: w, height: h })`, and add a render-level test that exercises a masked group and a non-opaque layer through `VisualSubsystem` (not just `renderFrame` with a factory).
|
||
|
||
### 2. P1 — System-level `behaviors` bypass semantic validation; accepted documents kill systems or the whole visual subsystem at runtime
|
||
|
||
- **Files:** `src/runtime/visual-validation.js:332-333` (only array length is checked for `system.behaviors`; `validateBehaviors` at :614 runs for objects only); runtime effects in `src/runtime/visual-behaviors.js:97,155,225-236` and `src/runtime/visual-systems.js:164` (`points: null` for procedural items).
|
||
- **Triggering conditions:** A `particles`/`emitter`/`repeater` system whose top-level `behaviors` array contains an unknown `type`, a `field-follow` naming an undeclared field, or a `point-wander`.
|
||
- **Impact:** Unknown type passes import validation and throws `ERR_INVALID_BEHAVIOR_TYPE` at activation inside the `VisualEngine` constructor → app's `attachVisuals` catch → the *entire* visual subsystem fails (spec §18.6 item 12 expects this error at import). `field-follow` with an undeclared field throws `ERR_INVALID_REFERENCE` from `FieldSet.sample` on the first tick → `failSystem` removes the system. System-level `point-wander` throws `ERR_INVALID_BEHAVIOR_TARGET` on the first tick (procedural items have `points: null`; only template-level behaviors get point lists via `node.motion`) → system removed. All three documents report `valid: true` at import.
|
||
- **Evidence:** Probes: `behaviors: [{type:'levitate'}]` → `valid: true`, runtime `threw: ERR_INVALID_BEHAVIOR_TYPE`; `[{type:'field-follow', field:'nope'}]` → `valid: true`, after two ticks `systems alive = 0`; system-level `point-wander` on a polyline render template → `valid: true`, `systems alive = 0` (the same behavior object-level on the template validates and runs fine).
|
||
- **Suggested correction:** Call `validateBehaviors` for system-level `behaviors` too — with `options.fields` set and either rejecting point-list/morph behaviors at system scope or wiring `item.points` from the item template's point list — so every behavior/type/field-reference error is an import error, and behavior/target incompatibility (e.g. point-wander on a non-point-list template) is rejected before activation.
|
||
|
||
### 3. P2 — `morph` writes NaN `z` into z-less point lists, poisoning geometry under perspective projection
|
||
|
||
- **File:** `src/runtime/visual-behaviors.js` (morph case, :335-350 — the line `item.points[index].z += ((target[index].z ?? 0) - item.points[index].z) * amount;`).
|
||
- **Triggering conditions:** A `morph` behavior between point-list objects (`polyline`/`polygon`/`spline`) whose points carry no `z`, in a scene using `perspective` projection.
|
||
- **Impact:** `item.points[index].z` is `undefined`, so `z` becomes `NaN` every tick. `visual-engine.js` `project()` guards with `subpath.start[2] ?? 0`, which does not catch `NaN` (not nullish) → perspective factor and screen coordinates become `NaN` → the object disappears (canvas ignores non-finite coordinates). The 4g suite's recursive `finite()` plan check passes because today's fixtures run orthographic, where `z` is unused.
|
||
- **Evidence:** Probe morphing two 2-point polylines: after one tick `points = [{"x":25,"y":25,"z":null(NaN)},…]`, `geometry start = [25, 25, null(NaN)]`.
|
||
- **Suggested correction:** `(item.points[index].z ?? 0)` on the source side (and add a perspective-scene morph test asserting finite plan coordinates).
|
||
|
||
### 4. P2 — `morph`/`point-wander` on `path` objects: validation explicitly allows, runtime silently no-ops
|
||
|
||
- **Files:** `src/runtime/visual-validation.js:31-33` (`POINT_LIST_TYPES`/`MORPH_TYPES` include `'path'`, with `MORPH_PATH_OPS` support); runtime `src/runtime/visual-motion.js:44` (`pointsOf` falls back to `commands`) and `src/runtime/visual-behaviors.js:225-236,344-349`.
|
||
- **Triggering conditions:** `point-wander` or `morph` on a `path` object (commands with `move`/`line` ops).
|
||
- **Impact:** `pointsOf` returns the raw command objects (`{op, to}`), whose points live in `to`; the behaviors read/write `.x`/`.y`/`.z`, producing junk keys (`"x":null,…` = NaN) while geometry is untouched — an accepted, authored behavior silently does nothing (no diagnostic, no failure).
|
||
- **Evidence:** Probe: path→path morph and path point-wander validate clean; after a tick, geometry JSON identical, commands polluted with NaN `x`/`y`/`z` fields.
|
||
- **Suggested correction:** Either drop `'path'` from `POINT_LIST_TYPES`/`MORPH_TYPES` (and the schema behavior target implications) until command-point addressing exists, or make `pointsOf`/the behaviors address `command.to` points for the `move`/`line` ops.
|
||
|
||
### 5. P2 — Repeater silently accepts `rate`/`burst`/`limit`/`capacity` (and `trail`) instead of `ERR_UNKNOWN_FIELD`
|
||
|
||
- **Files:** `src/runtime/visual-validation.js` `validateSystems` (:325 rejects `links` on emitters, :328 adds a graphic-only rejection list, but no repeater-specific check); `src/runtime/visual-systems.js:80,96`; schema `schema/xzbt-0.1.schema.json:3229` (repeater variant lists `rate`/`burst`/`limit`/`capacity` as allowed properties).
|
||
- **Triggering conditions:** A `repeater` system declaring `rate`, `burst`, `limit`, `capacity`, or `trail`.
|
||
- **Impact:** Spec 18.5 (docs line 2399): "A `repeater` has no `rate`, `burst`, `limit`, `capacity`, `lifetime`, or `inputs`; each is `ERR_UNKNOWN_FIELD`." Import validation accepts all of them. `rate` alone then throws `ERR_UNBOUNDED_EMISSION` ("Emission declares neither a lifetime nor a limit") at activation — a misleading error for a system type that has no emission; `rate` + `limit` passes and the rate is silently ignored forever; `trail` silently records/draws trail history even though the field is not in the repeater table (the schema, inconsistently, *does* forbid `trail` — so schema and validator disagree).
|
||
- **Evidence:** Probes: repeater + `rate: 5` → `valid: true`, activation throws `ERR_UNBOUNDED_EMISSION`; repeater + `trail` → `valid: true`, runs with 3 copies and live trail state.
|
||
- **Suggested correction:** Extend the per-type unknown-field validation to reject `rate`/`burst`/`limit`/`capacity`/`trail` (and `fields`, if unintended — it validates but is skipped by the repeater integrator at `visual-systems.js` `advanceItem`) on repeaters, and remove `rate`/`burst`/`limit`/`capacity` from the schema's repeater variant.
|
||
|
||
### 6. P2 — The two repo validators disagree: the standalone CLI rejects legal visual bindings
|
||
|
||
- **File:** `tools/validate-exhibit.mjs:572` (`ExhibitValidator.validateBindingTarget` falls through to `ERR_UNSUPPORTED_TARGET` … "not exposed by the shared 0.1 target registry" for every `visuals.*` target).
|
||
- **Triggering conditions:** Running `node tools/validate-exhibit.mjs` on an exhibit with bindings to the new 8.1 visual families (`visuals.camera.*`, `visuals.layers.<id>.opacity`, `visuals.effects[i].*`, `visuals.systems.<id>.visible`).
|
||
- **Impact:** `exhibits/minimal-visual.xzbt` (committed today in `6587d3e`) and `exhibits/exhibit-d.xzbt` (untracked today) both FAIL the repo's own validation CLI (`[FAIL]`, exit 1) while passing the production validator (`parseAndValidateExhibit` → `valid: true`, zero errors). The gc2/gc3 tests instantiate `ExhibitValidator` only with their own fixtures, so the divergence is uncaught; anyone gating on the CLI rejects conforming exhibits.
|
||
- **Evidence:** CLI run over all exhibits: the two files above fail with `ERR_UNSUPPORTED_TARGET`/`ERR_INVALID_REFERENCE` on legal targets; runtime validator accepts both. All 14 challenge fixtures and exhibits A–C pass both validators.
|
||
- **Suggested correction:** Make `tools/validate-exhibit.mjs` delegate to `src/runtime/validator.js` (single source of truth), or port the four 8.1 visual target families into `ExhibitValidator`; add a test that runs the CLI validator over `exhibits/*.xzbt` and `exhibits/visual-challenges/*.xzbt` so the artifact set is continuously checked by both paths.
|
||
|
||
### 7. P3 — JSON Schema system variants contradict the validator and spec 19.2/18.4
|
||
|
||
- **File:** `schema/xzbt-0.1.schema.json` (system variants at :2878/:2920/:3081/:3229), committed in `6587d3e`.
|
||
- **Details:** All four variants allow top-level `release`, `ownership`, `inputs`, `cancelWithScenario` — spec 19.2 and the validator (`visual-validation.js:314`) make each `ERR_UNKNOWN_FIELD` ("a field of the spawn container"). The emitter variant allows `links`, which the validator rejects (:325). The repeater variant allows `rate`/`burst`/`limit`/`capacity` (finding 5). The runtime never executes the schema (only a $ref-resolution test reads it), so this ships an inaccurate contract artifact rather than breaking imports directly.
|
||
- **Suggested correction:** Move the four spawn-only fields out of the variants' property lists (they already exist via the `spawn` sub-object), drop `links` from the emitter variant, and align the repeater variant with the 18.5 table.
|
||
|
||
### 8. P3 — `spawn.lifetime`/`spawn.release`: spec says DurationSpec, implementation requires literals
|
||
|
||
- **Files:** `src/runtime/visual-validation.js:361-364` (literal-only check); spec table docs/XZBT_0-1_Format_Specification.md:2936-2937 (`DurationSpec`, and section 6.2 defines DurationSpec to include the bounded `{"random": …}` TimeSpec); runtime `src/runtime/visual-engine.js` `spawn()` → `parseDuration(sampleTree(...))`, and `parseDuration` (`src/runtime/types.js`) throws on non-strings.
|
||
- **Impact:** A document using the spec-permitted random form is rejected at import; had it passed, spawn would throw `ERR_INVALID_DURATION` when the resolved value is a number. Currently consistent only because validation blocks first.
|
||
- **Suggested correction:** Either change the 19.2 table to "duration literal", or accept TimeSpec in `validateSpawn` and resolve it to milliseconds in `spawn()` before `parseDuration`.
|
||
|
||
### 9. P3 — Bundle self-containment regex weakened past side-effect imports
|
||
|
||
- **File:** `test/phase1-runtime.test.mjs:41`.
|
||
- **Details:** The check changed from `\bimport\s+[^;(]` to `^[ \t]*import[ \t][^;(\n]*from[ \t]*['"]` (multiline). The new pattern requires `from`, so a side-effect import (`import './chunk.js';`) in the bundle would no longer be caught; the change was made because bundled prose comments mention import syntax.
|
||
- **Suggested correction:** Add a second assertion for `^[ \t]*import[ \t]*['"]` (side-effect form) so both statement shapes are covered while prose mentions remain tolerated.
|
||
|
||
## Verification performed
|
||
|
||
- `npm test` (all suites): **212/212 tests pass**, 0 failures (16 suites, 15 files) — includes the 109 Phase 4 subtests; consistent with the 4f evidence doc's "223 checks" once the 12 GC2 fixture sub-checks are counted individually (212 − 1 + 12 = 223).
|
||
- `npm run build`: OK; two consecutive builds produce byte-identical output (SHA-256 46619d8f…, 464,653 bytes), confirming determinism and that the rebuild did not alter the pre-existing working-tree diff (diff stat unchanged at 2401 lines). `git diff --check` clean.
|
||
- `npm run build:visual-acceptance`: OK — acceptance page rebuilt with 18 fixtures; `phase4-challenges` test also validates determinism and fixture/file parity.
|
||
- `python tools/verify-spec-contract.py`: OK — 46 diagnostic codes declared / 43 used, 0 unresolved cross-references, fences/tables balanced.
|
||
- `node tools/validate-exhibit.mjs` over every `exhibits/**/*.xzbt` and `Claude outputs`: all pass except `exhibit-d.xzbt` and `minimal-visual.xzbt` (finding 6); both pass the production validator (verified directly via `parseAndValidateExhibit`).
|
||
- Runtime probes (temporary scripts, since deleted) against the live modules: system-scope automation on a behavior-less graphic object (works — `raw` refreshed every tick via `advanceVisualMotion`); the six defect confirmations cited in findings 1–5; repeat/point and burst/limit/capacity accounting in `ProceduralSystem` (correct by inspection + probes); lifecycle release/dispose transitions exercised by the passing 4f suite.
|
||
- **Could not verify:** actual browser pixels (no display available here — mask/layer findings are established at the `renderFrame` call-contract level, not by screenshot), slice 4h GC6 performance ceilings, audible acceptance, and long-run memory behavior of the offscreen `SurfacePool` (unbounded `allocated` growth across frames is untested). Untracked-file mtimes were used as a proxy for "today"; uncommitted work has no authoritative timestamps.
|