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
18 KiB
18 KiB
XZBT Daily Code Review — 2026-09-06 (America/Los_Angeles)
Review scope
- Local date/time (America/Los_Angeles, PDT): 2026-09-06 ~11:30. Branch:
main. HEAD:0af58da(feat(visual): implement the slice 4d renderer core). - Reviewed commits (all within LA 2026-09-06, per author dates):
1bc4901docs(visual) sections 17-19 review triage @ rev 0.8;6587d3eschema/validator/resolution aligned to the 0.8 visual contract;699492fstandalone-artifact bundle of contract+validation;0af58daslice 4d renderer core. Earlier same-session commits3e27a66…527220eare LA 2026-09-05 20:57-21:30 and were treated as the contract baseline, not reviewed work. - Uncommitted changes: nothing staged; 17 tracked files modified (+3188/-119), ~35 untracked paths, including 9 new runtime modules (visual-noise/behaviors/distributions/fields/motion/systems/effects/automation/lifecycle), 3 new test files (phase4-procedural/execution/challenges), tools/visual-challenge-fixtures.mjs, tools/build-visual-acceptance.mjs, exhibits/exhibit-a..d.xzbt, prototypes/phase4/, docs/evidence/phase4/*.md, regenerated XZBT.html. This is the phase 4e-g layer.
- Dating of uncommitted work: file mtimes (2026-09-06 09:44-11:13 LA) and the self-dated evidence docs are consistent with a single this-morning session, and XZBT.html is byte-identical to a fresh build of current sources, so the tree is coherent; but git cannot strictly prove when each edit was made, and some files may have been touched over several hours.
- Scope limitations: commits were not checked out individually (no interference); everything was reviewed at HEAD + working tree. Canvas-pixel output was not observed on a real display; claims about rendering are from code reading plus the mock-context test oracles.
Findings
P1-1 — morph corrupts point z to NaN on z-less geometry
- File:
src/runtime/visual-behaviors.js:348(untracked, phase 4e). - Condition: any morph between geometries whose points omit
z(the normal 2D case), ≥1 tick, rendered undercamera.projection: "perspective". - Impact:
item.points[index].z += ((target[index].z ?? 0) - item.points[index].z) * amountreads an undefined base (undefined + …→ NaN); NaN then flows throughprimitiveSubpaths(visual-motion.js:73) into plan coordinates; under perspective the object renders NaN (invisible/poisoned), orthographic hides it, so the current suite (no perspective-morph case) is green. Reproduced by probe: morphed pointzbecame NaN after 3 ticks. - Fix: guard the base read as well:
item.points[index].z += ((target[index].z ?? 0) - (item.points[index].z ?? 0)) * amount;(aspoint-wanderalready does at line 235).
P1-2 — Velocity-accumulating behaviors are silently inert on graphic objects and repeater copies
- Files:
src/runtime/visual-motion.js:47(graphic-object host runsadvanceItemwith a fake{type:'repeater'}context),src/runtime/visual-systems.js:314-325(integrator gated bythis.type !== 'repeater'),src/runtime/visual-behaviors.js:307-319, 332(attract/repel/field-follow writeitem.vxonly). - Condition: spec 18.6 accumulating behaviors (
attract,repel,bounce,field-followforce/velocity,wander) on a graphic object, graphic-system content, or repeater copy — validation accepts them (spec 18.6 attaches behaviors to objects/items; no host restriction enforced). - Impact: the behavior accumulates velocity forever with no displacement: probe — graphic point with
attract {x:500,y:300}, strength:80hadvx ≈ 0.235but still sat at (0,0) after 2 s of ticks (the identical behavior moves particles);drifton the same host works, which masks the gap in fixtures. Silent, no diagnostic, violates 18.6 ("the integrator of 18.2 turns into displacement"). - Fix: integrate
vx/vyfor object/repeater hosts inadvanceVisualMotion, or reject these behaviors on hosts without an integrator at semantic validation with the documented code.
P1-3 — Per-item/system behavior arrays escape semantic validation (wrong stage; silent no-op channels)
- Files:
src/runtime/visual-validation.js:332-333(systems: only the≤ 8count check) vs:653/:789(validateBehaviorsinvoked only for object hosts). - Condition: any spec-18.9 semantic violation inside a
particles/emitter/repeaterbehaviorsarray. - Impact: 18.9/18.10 trace 12 requires
ERR_INVALID_BEHAVIOR_TYPE,ERR_INVALID_BEHAVIOR_TARGET,ERR_INVALID_REFERENCE,ERR_SCHEMA_VALIDATIONat import; instead probe: six cases (bogus type on particles/repeater, invalid property channel, undeclared field, point-wander on point render, follow-path speed+duration) all import clean. Unknown types then throw insidecreateBehaviorat activation (whole-system failure at the wrong stage); invalid property channels (e.g.style.hue) write inert keys silently; undeclared fields throw per tick untilfailSystem. - Fix: run the object-host
validateBehaviorsover each procedural system'sbehaviorsat import with field context.
P2-1 — Canvas 2D backend never consumes per-object buffer grants; 17.12 stage order realized per-op
- Files:
src/runtime/visual-canvas2d.js:88-96, 97-106, 227-241(no read ofnode.bufferedanywhere in src/runtime) vssrc/runtime/visual-engine.js:1171-1201(allocates per-object buffers, setsplan.buffered, sheds beyond 16). - Condition: any object/group with non-default
blend,blur,glow, orfiltersinside an unbuffered layer (the common case). - Impact: fill, stroke, glow, and filter ops each composite individually against the live backdrop under the node's blend/alpha instead of the object being rasterized once and composited once (spec 17.12 buffer table/stage order; "A buffer is an offscreen surface the renderer must allocate to compute a stage correctly"). Observable: double-blending on translucent/blended stroke-over-fill overlap and on glow under non-normal blend; grant/shed bookkeeping has no rendering effect (granted and shed nodes differ only by feature presence). Only the layer-buffer and mask paths get real isolation.
- Fix: when
node.bufferedis true, draw the node (subtree for groups) into a pool surface with default state, then composite once with the node's alpha/blend, mirroring the layer path at visual-canvas2d.js:234-241.
P2-2 — Glow paints over the object interior and is silently dropped on text
- File:
src/runtime/visual-canvas2d.js:162-181. - Condition:
style.glowon a point with a fill, on any strokable shape, or on text. - Impact: 17.12 adds glow around the result; here the point disc is re-filled with
glow.color(full-strength disc at alpha 1/strength 1 — a white point with red glow renders as a solid red disc), shapes are re-stroked on the identical path with glow color (lineWidth = max(strokeWidth,1)), overpainting the real stroke, and text gets no glow branch at all (silent, while the engine still grants text a buffer — visual-engine.js:901). Verified by code read. - Fix: draw only the halo via a zero-offset blurred shadow of the original geometry without re-filling/re-stroking the path; apply the same to
fillText/strokeTextfor text.
P2-3 — Boolean leaves accept any literal; visible: 0 renders visible, enabled: "false" executes
- Files:
src/runtime/visual-validation.js:233(effectenabledand object/layer/systemvisibleare only structure-validated as ValueSpecs); consumers use=== falsesemantics:visual-engine.js:529(effectPlan),:643-645(systemVisible),:698(layerValue). - Condition: authoring
visible: 0/1/"no"on any object/layer/system, orenabled: 0/1/"false"on an effect entry. - Impact: spec section 2 bans coercion — "a non-zero number will not be coerced into a boolean. Type mismatches produce
ERR_TYPE_MISMATCH". Probes: all of the above import VALID, and the runtime draws avisible: 0object (expectation: hidden) and executes anenabled: "false"/enabled: 0effect (exactfalseis the only skip). No diagnostic anywhere. - Fix: type-check boolean leaves against the declared type at validation (
ERR_TYPE_MISMATCH), i.e. apply the effectcolor-literal check pattern (visual-validation.js:244-247) to booleans.
P2-4 — face-motion snaps on its first tick and reverts on zero velocity
- File:
src/runtime/visual-behaviors.js:279-283. - Condition:
face-motionwith anysmoothing, including 1 ("never turns", spec 18.6). - Impact:
const current = instance.state.rotation ?? target;initializes the follow state to the target, so the first tick contributes the full turn regardless of smoothing (probe: base 45, +y velocity → rotation 90 on tick 1 for smoothing 0, 0.5 and 1); when XY speed drops below 1e-6 the behaviorbreaks, the fresh rotation bucket was zeroed, and the object pops back to the authored rotation instead of holding the previous one (18.6: "An object with zero velocity holds its previous rotation"). - Fix: initialize
instance.state.rotationlazily to the current drawn rotation on first tick; on the zero-velocity branch emit the held value.
P2-5 — Trail fade is a flat uniform alpha; head never keeps the item's own opacity
- File:
src/runtime/visual-engine.js:1014(points mode per-sample taper),:1053(ribbon),:1064(line). - Condition: any trail in
line/ribbonmode, or any trail on an item whose opacity ramps/fades. - Impact: spec 18.8: "
fade… Opacity multiplier at the tail; the head keeps the item's own opacity." Line/ribbon draw the whole trail at the flat tail factor and nothing multiplies by the item's rampedopacityMultiplier; probe: item at ramped opacity 0.81 rendered a full-strength head point but a line trail at flat alpha 0.4 (default fade 1.0 would leave the trail at 1.0 while the head fades to 0). Fading particles leave permanent-looking trails. - Fix: per-vertex/per-segment alpha from head
itemOpacityto tailitemOpacity * fade(segmented strokes), or state the uniform-color approximation withWARN_VISUAL_APPROXIMATION.
P2-6 — Procedural system admission gaps at import: required fields and emission semantics deferred to activation
- Files:
src/runtime/visual-validation.js(system per-type field checks, cf.:332-333region) vssrc/runtime/visual-systems.js:96-99(ERR_UNBOUNDED_EMISSIONraised at construction). - Condition/probes (all import VALID):
particleswithoutrender(spec 18.2 table: required),emitterwithoutemit(spec 18.4: required),particleswithrateand neitherlifetimenorlimit. - Impact: spec 18.2/18.4 render/emit-required rows and the semantic-stage list (18.9) demand import-time rejection; the docs instead pass validation and only fail (or silently draw nothing, in the render-less case) at activation/instantiation — wrong stage, and one case is silent.
- Fix: enforce per-type required fields and the static half of the unbounded-emission rule at import (runtime check stays for stochastic rates).
P2-7 — Resolved non-integer creation counts silently rounded
- File:
src/runtime/visual-systems.js:59, 63(Math.roundon resolvedcountand burstcount). - Condition:
count/burst[].countauthored as a ValueSpec resolving non-integer (e.g.{random:{min:2.4,max:2.6}}). - Impact: spec 18.2: "an integer field whose resolved value is not an integer is
ERR_TYPE_MISMATCHat that boundary rather than being rounded". Runtime rounds silently, so the same seed is reproducible but the authoring error is unmarked. - Fix: after sampling, throw
RuntimeFault('ERR_TYPE_MISMATCH', …)when!Number.isInteger(resolved)forcountand each burst count.
P2-8 — ring with equal start/end angles draws a visible radial spoke
- File:
src/runtime/visual-geometry.js:348-351. - Condition:
{type:'ring', startAngle: 90, endAngle: 90, …}(validation accepts;directedSweepreturns sweep 0). - Impact: the partial-sector branch emits outer-point →
lineToinner-point → closed, which strokes as a straight radial line; spec 17.9: "d == 0draws nothing and raises no diagnostic" (the siblingarcprimitive handles sweep 0 correctly viaarcSegments). - Fix: return no segments when
sweep === 0(keep the full-annulus branch).
P3 findings
- Diagnostics
raisedlist grows unbounded and is copied every frame —src/runtime/visual-diagnostics.js:19, 39, 63(push),:45-52(endTickprunes nothing),src/runtime/visual-engine.js:734(per-frameslice()). Slow memory/frame-cost growth over long sessions with sustained sheds/onces. Fix: clear or ring-buffer after the per-tick plan copy. - Degenerate path arcs:
src/runtime/visual-geometry.js:144-149, 153-186— a patharcwhosetoequals the current point (dx = dy = 0) forcesacos(0)and emits a phantom 90°/270° cubic bulge; andMath.absis applied to the radius before the≤ 0throw, so a negative radius component (spec 17.13:ERR_OUT_OF_BOUNDS, "not silently degraded") draws a mirrored arc instead of failing. Fix: early-return on coincident endpoints; check the raw radii beforeMath.abs. - Masked-group rendering dereferences a null surface —
src/runtime/visual-canvas2d.js:97-106, 233—surface.create()is unguarded; with no usablecreateSurface(Node/headless, nodocument/OffscreenCanvas; subsystem fallback at visual-subsystem.js:63-65 can return undefined) a granted masked group throwsTypeErrorout ofrenderFrame, and the app treats any render exception as fatal:src/runtime/app.js:42-49callsvisual.deactivate()with no reattach, permanently blacking visuals for the activation (a transient fault should not be terminal; audio faults only diagnose). Fix: guard/null-skip the mask with a warning; consider re-attaching visuals on a later frame. - Post-effect transient memory is unbounded at the backing ceiling —
src/runtime/visual-effects.js:6-39: each blur pass allocates a full-frame copy +Float32Array+ output (bloom adds a bright pass + glow blur); at the 4096-device-px ceiling this is on the order of several hundred MB per effect with no tie-in to the 19.5 pass budget. Fix: tile the blur or cap effect radius relative to the backing store. window-trafficchallenge fixture: per-copywrapbounds (tools/visual-challenge-fixtures.mjs, ~lines 56-60) are ±30 px around each copy's own origin, so cars cycle in place every ~3 s and never cross the window — the authored bounds appear intended to be the window rect. The 4g suite only checks finiteness/equality, so this passes. Hedged: if cell-stutter was intended it should be documented.schema/xzbt-0.1.schema.json(visual additions, committed in6587d3e) has no consumer anywhere in src/tools/tests — the hand-written validator is the sole admission authority, and the schema already diverges from it (schema marks per-system fields likerender/emitrequired that the validator admits without, cf. P2-6), so the schema can silently rot. Consider validating the exhibit fixtures against it in CI. (Schema itself parses cleanly: no duplicate keys; all 191$refs resolve.)
Verification performed
- Full suite in a temp copy of the repo (
node --test test/*.test.mjs, node v24.11.0): 212/212 pass, 0 fail; Phase 4 = exactly 109 tests (renderer 22, visual-contract 29, procedural 19, execution 20, challenges 19) + gc2-runner's single test covering 12 fixtures → evidence's "223 checks" accounting (212 tests + 11 extra counted GC2 fixture sub-checks) is consistent. - Artifact integrity: two clean
npm run buildruns are byte-identical and identical to the working-treeXZBT.html(SHA-25646619d8ff2fbadf188136409d5cfe658bf5492ba0ba111d58ad488e95b343770);npm run build:visual-acceptanceoutput matches the evidence hash42981219…db74(18 fixtures embedded). Evidence claims in docs/evidence/phase4/2026-09-06-phase4f-execution.md verified.git diff --checkclean; nothing staged. python tools/verify-spec-contract.py: 0 unresolved cross-references, fences/tables balanced, 46 codes declared / 43 used (3 declared-only: ERR_INVALID_ARITY, ERR_INVALID_OPERATOR, ERR_UNSUPPORTED_VERSION).- Independent probes (node, read-only): attract-on-graphic inertness (P1-2); six-case behavior-array import acceptance (P1-3);
visible:0/enabled:0|1|"false"accepted (P2-3); particles/emitter/repeater required-field admission (P2-6); morph NaN via code trace (P1-1); plus positive controls: audio-bus draws are bit-identical with and without visual effects blocks; both automation call sites use consistent ms time bases; 120-tick runs at 1/2/4 draws/frame agree (execution suite). - Clean areas (verified by direct reading/tests): math/geometry conventions and sweep normalization; layer ordering and buffer-refusal order (nearest-first grants, farthest-first refusals); fog/gradient/alpha arithmetic; determinism story (keyed RNG substreams, no Math.random/Date/performance.now in the visual path); effect pass budget and per-effect warn routing; build determinism; phase1 bundler top-level-identifier collision scan is a good net for the concatenated artifact.
- Not verified: per-commit isolation of the four reviewed commits; real-browser pixel output, post-effect numerics on a real surface, and hardware performance (evidence doc itself disclaims these); memory footprint measurements; exact provenance/dating of each uncommitted edit.
Testing gaps (relevant to the findings)
No test covers: perspective-projected morph (P1-1); velocity behaviors on graphic/repeater hosts (P1-2); semantic-validation cases on per-item behavior arrays (P1-3); backend rendering of buffered nodes, glow, blur, filters, or masked groups through a real surface pool (P2-1, P2-2, P3.3); zero-sweep ring / degenerate path arcs (P2-8, P3.2); typed-boolean leaves (P2-3); face-motion smoothing/zero-velocity (P2-4); trail head-opacity/fade taper in line/ribbon (P2-5); non-integer resolved counts (P2-7); long-run diagnostics growth (P3.1); post-effect runs against real readback (P3.4). phase4-procedural/execution trace coverage is strong where it operates (draw counts, closed-form motion, budgets, ceilings, cadence).