Files
XZBT/reviews/review-2026-09-06-unknown-model-180353-6b68e324.md
T
LabyricornandClaude Opus 5 c4332363a9 docs: raise the format specification to revision 0.9 and land the reconciliation
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
2026-09-06 21:54:09 +00:00

94 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.