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

17 KiB
Raw Blame History

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.
  • 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.