# XZBT 0.1 Phase 4d–4g — code review triage Inputs: - **R1** — [review-173154-a7c3e91b](review-2026-09-06-unknown-model-173154-a7c3e91b.md) (4 findings, code-focused). - **R2** — [review-180353-6b68e324](review-2026-09-06-unknown-model-180353-6b68e324.md) (9 findings, deep runtime probes). - **R3** — [review-183137-f3a91c2d](review-2026-09-06-unknown-model-183137-f3a91c2d.md) (8 numbered + 6 P3, broad coverage). - **SR** — [spec-review-173816-c4f8a2d1](spec-review-2026-09-06-unknown-model-173816-c4f8a2d1.md) (4 findings, spec-vs-impl traceability). This follows [01-triage.md](.completed-artifacts/01-triage.md) (visual contract §§17–19 specification triage) and [00-triage.md](.completed-artifacts/00-triage.md) (§§14–16 specification triage). Those triages addressed defects in the format specification text; this triage addresses defects in the **implementation code** and its conformance to the specification, discovered by four independent reviews of the Phase 4d–4g working tree at HEAD `0af58da`. All claims below were verified against the source files named, the specification (`docs/XZBT_0-1_Format_Specification.md` rev 0.8), and the test suites. No source files were modified. Where a reviewer's proposed fix is wrong or incomplete, the correction is noted. **Result: 20 distinct defects confirmed across 27 reported findings. No false positives.** 3 findings are duplicates (merged below), and 1 finding is confirmed but overstated in severity. All four reviews found real issues; R2 and R3 are substantially stronger — each found issues missed by the other three. The overlap pattern demonstrates the value of independent multi-pass review: **only 4 of 20 defects were found by more than one reviewer.** --- ## Scorecard | | R1 | R2 | R3 | SR | | --- | :---: | :---: | :---: | :---: | | Findings reported | 4 | 9 | 14 | 4 | | Real | 4 | 9 | 14 | 4 | | Unique catches | 0 | 4 | 8 | 0 | | Proposed fix wrong/incomplete | 0 | 0 | 0 | 0 | | Overstated severity | 0 | 0 | 1 (P2-7) | 0 | R2 is the strongest single pass: it is the only reviewer to identify the compositing surface gap (C1), the validator divergence (C6), the schema system-variant contradictions (C7), and the bundle self-containment regex weakness (C14). R3 is comparably strong, uniquely finding the velocity-integration gap (C3), the glow rendering defect (C10), the boolean coercion gap (C11), the face-motion smoothing error (C12), the trail fade defect (C13), the ring zero-sweep defect (C16), and several P3 issues. R1 and SR found nothing missed by R2 and R3, but independently confirmed critical issues. --- ## Tier 1 — Production failures: crashes, silent subsystem shutdown, or spec violations at wrong lifecycle stage ### C1. Production render path never creates compositing surfaces; masks crash, layer opacity silently ignored *Reported by: R2 §1. R3 P3.3 (partial). R1 did not find.* **Confirmed.** `VisualSubsystem.render` calls `renderFrame` without a `createSurface` factory. The mask branch of `drawNode` (`visual-canvas2d.js:233`) gets `null` from the dummy surface provider and throws `TypeError`. `app.js:42-49` catches, logs, and calls `visual.deactivate()` — permanent visual shutdown on the first masked-group frame. Layer `opacity`/`blend` are only applied in the buffered composite branch (`visual-canvas2d.js:236`); without a surface pool, layer opacity is silently dropped — a bindable 8.1 property does nothing in the browser. R3 P3.3 identifies the same null-surface crash but proposes only a guard/null-skip with warning and considers re-attaching visuals on a later frame. **R2's fix is more complete:** pass a surface factory from `VisualSubsystem.render` and add a render-level test exercising a masked group and a non-opaque layer through the subsystem, not just `renderFrame` with a manually-injected factory. **Files:** `src/runtime/visual-subsystem.js:61-63`, `src/runtime/visual-canvas2d.js:99,230-243`, `src/runtime/app.js:42-49`. **Correction:** Pass a surface factory (`OffscreenCanvas` or `document.createElement('canvas')` fallback) from `VisualSubsystem.render`. Add subsystem-level test for masked groups and non-opaque layers. Consider whether `app.js` should re-attach visuals on subsequent frames rather than permanently deactivating. --- ### C2. System-level `behaviors` arrays bypass semantic validation; accepted documents fail at wrong lifecycle stage *Reported by: R2 §2. R3 P1-3 (same defect, different angle). R1 did not find.* **Confirmed.** `visual-validation.js:332-333` checks only array length for system `behaviors`; `validateBehaviors` at `:614` runs only for object hosts. Six classes of semantic error (unknown type, invalid property channel, undeclared field, point-wander on point render, follow-path conflicts, and more) import clean and then throw at activation or silently no-op at runtime. Spec §18.9/18.10 trace 12 requires these errors at import (`ERR_INVALID_BEHAVIOR_TYPE`, `ERR_INVALID_BEHAVIOR_TARGET`, `ERR_INVALID_REFERENCE`, `ERR_SCHEMA_VALIDATION`). R2 and R3 converge on the same fix with different emphasis. R2 focuses on the system-scope call to `validateBehaviors` with field context; R3 adds that `options.fields` must be set and behavior/target incompatibility (e.g. point-wander on a non-point-list template) should be rejected before activation. **Files:** `src/runtime/visual-validation.js:332-333, :614, :653, :789`. **Correction:** Run `validateBehaviors` for system-level `behaviors` arrays at import, with `options.fields` set from the system context. Reject point-list/morph behaviors at system scope unless the template provides point lists. Reject unknown types, invalid references, and invalid targets with the documented diagnostic codes. --- ### C3. Velocity-accumulating behaviors are silently inert on graphic objects and repeater copies *Reported by: R3 P1-2 only.* **Confirmed.** Graphic-object hosts run `advanceItem` with a fake `{type:'repeater'}` context (`visual-motion.js:47`); the integrator in `visual-systems.js:314-325` is gated by `this.type !== 'repeater'`. Behaviors like `attract`, `repel`, `bounce`, `field-follow` force/velocity, and `wander` accumulate velocity (`item.vx`) but displacement is never applied. Probe: graphic point with `attract {x:500,y:300}, strength:80` had `vx ≈ 0.235` but remained at (0,0) after 2s of ticks. `drift` works on the same host, masking the gap in fixtures. Spec §18.6: "the integrator of 18.2 turns into displacement." **Files:** `src/runtime/visual-motion.js:47`, `src/runtime/visual-systems.js:314-325`, `src/runtime/visual-behaviors.js:307-319, 332`. **Correction:** Integrate `vx/vy` for object/repeater hosts in `advanceVisualMotion`, or reject velocity-accumulating behaviors on hosts without an integrator at semantic validation with the documented diagnostic code. --- ### C4. `morph` corrupts point `z` to NaN on z-less geometry; poisons perspective projection *Reported by: R2 §3. R3 P1-1 (same defect). R1 §3 (related, lower severity assessment).* **Confirmed.** `visual-behaviors.js:348`: `item.points[index].z += ((target[index].z ?? 0) - item.points[index].z) * amount` — the base `item.points[index].z` is `undefined`, so `undefined + … → NaN`. Under perspective projection, NaN flows through `primitiveSubpaths` into plan coordinates; `project()` guards with `subpath.start[2] ?? 0`, which does not catch NaN (not nullish) → coordinates become NaN → object disappears. Orthographic hides this because `z` is unused; the current suite has no perspective-morph case. R3 provides the most precise fix: guard both sides as `(item.points[index].z ?? 0)`, matching `point-wander`'s existing guard at line 235. R1's §3 is a different but related issue: bloom/color-adjust unclamped channel math and blur radius capped at `max(width,height)`. **Not a duplicate of C4** — see C15 below. **Files:** `src/runtime/visual-behaviors.js:348`. **Correction:** `item.points[index].z += ((target[index].z ?? 0) - (item.points[index].z ?? 0)) * amount;`. Add a perspective-scene morph test asserting finite plan coordinates. --- ### C5. `spawn.lifetime` / action lifetime: `parseDuration` rejects non-string values *Reported by: R1 §1. R2 §8. SR §1. All converge.* **Confirmed.** `visual-engine.js:466-468` always calls `parseDuration(sampleTree(…))`, which requires `typeof value === 'string'` (`types.js:55-56`). A numeric or already-resolved lifetime throws `ERR_INVALID_DURATION` instead of applying the duration. The spec (§6.1, §19.2) defines `spawn.lifetime` and `spawn.release` as `DurationSpec`, which includes the bounded `{random: …}` TimeSpec form. R1 proposes accepting already-numeric milliseconds. SR correctly identifies the design ambiguity: the Action Model (Phase 5/6) has not yet fixed `action.lifetime` typing, so this is a defect only for `spawn.lifetime` after `sampleTree`; action-lifetime typing needs a separate decision. R2 notes the dual-filing issue between spec and validator (validator is literal-only; spec says DurationSpec). **Files:** `src/runtime/visual-engine.js:466-468`, `src/runtime/actions.js:48-50`, `src/runtime/types.js:55-56`, `src/runtime/visual-validation.js:361-364`. **Correction:** Convert with `parseDuration` only when the value is a string; if already a finite number, treat as milliseconds. Alternatively, restrict the §19.2 table to "duration literal" — pick one and align spec, validator, and runtime. The validator's current literal-only check (`visual-validation.js:361-364`) is consistent with a restriction decision; the spec text needs to match whichever option is chosen. **Design decision required.** --- ### C6. The two repo validators disagree: standalone CLI rejects legal visual bindings *Reported by: R2 §6 only.* **Confirmed.** `tools/validate-exhibit.mjs:572` falls through to `ERR_UNSUPPORTED_TARGET` for every `visuals.*` binding target. `exhibits/minimal-visual.xzbt` (committed in `6587d3e`) and `exhibits/exhibit-d.xzbt` (untracked) both FAIL the CLI validator while passing the production validator (`parseAndValidateExhibit`). The gc2/gc3 tests instantiate `ExhibitValidator` only with their own fixtures, so the divergence is uncaught. **Files:** `tools/validate-exhibit.mjs:572`, `src/runtime/validator.js`. **Correction:** Either delegate from `tools/validate-exhibit.mjs` 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` to keep both paths in sync. --- ## Tier 2 — Spec violations, silent data corruption, or incorrect rendering (non-crashing) ### C7. JSON Schema system variants contradict the validator and spec §§18.4, 19.2 *Reported by: R2 §7 only.* **Confirmed.** `schema/xzbt-0.1.schema.json` system variants (`:2878/:2920/:3081/:3229`) allow top-level `release`, `ownership`, `inputs`, `cancelWithScenario` — spec §19.2 and the validator (`visual-validation.js:314`) make each `ERR_UNKNOWN_FIELD`. The emitter variant allows `links` (validator rejects at `:325`). The repeater variant allows `rate`/`burst`/`limit`/`capacity` (see C8). The runtime never executes the schema, so this ships an inaccurate contract artifact rather than breaking imports. **Files:** `schema/xzbt-0.1.schema.json`. **Correction:** Move spawn-only fields out of the variants' property lists. Drop `links` from the emitter variant. Align the repeater variant with the §18.5 table. Reconcile required-field declarations (see C9). --- ### C8. Repeater silently accepts emitter-only fields (`rate`/`burst`/`limit`/`capacity`/`trail`) *Reported by: R2 §5 only.* **Confirmed.** Spec §18.5 (line 2399): "A `repeater` has no `rate`, `burst`, `limit`, `capacity`, `lifetime`, or `inputs`; each is `ERR_UNKNOWN_FIELD`." Validation accepts all of them. `rate` alone then throws `ERR_UNBOUNDED_EMISSION` at activation (misleading for a system type with no emission); `rate` + `limit` passes and `rate` is silently ignored; `trail` silently records/draws trail history even though the field is not in the repeater table (the schema, inconsistently, *does* forbid `trail`). **Files:** `src/runtime/visual-validation.js:325-333`, `src/runtime/visual-systems.js:80,96`, `schema/xzbt-0.1.schema.json:3229`. **Correction:** Extend per-type unknown-field validation to reject `rate`/`burst`/`limit`/`capacity`/`trail` (and `fields` if unintended) on repeaters. Remove these from the schema's repeater variant. --- ### C9. Procedural system admission gaps: required fields and emission semantics deferred to activation *Reported by: R3 P2-6 only.* **Confirmed.** `particles` without `render` (spec §18.2 table: required), `emitter` without `emit` (spec §18.4: required), and `particles` with `rate` and neither `lifetime` nor `limit` — all import VALID. Spec §18.2/18.4 and the semantic-stage list (§18.9) demand import-time rejection. Instead they pass validation and only fail (or silently draw nothing) at activation/instantiation. **Files:** `src/runtime/visual-validation.js`, `src/runtime/visual-systems.js:96-99`. **Correction:** Enforce per-type required fields at import. Enforce the static half of the unbounded-emission rule at import (runtime check stays for stochastic rates). --- ### C10. Glow paints over the object interior; silently dropped on text *Reported by: R3 P2-2 only.* **Confirmed.** `visual-canvas2d.js:162-181`: the point disc is re-filled with `glow.color` (a white point with red glow renders as a solid red disc), shapes are re-stroked on the identical path with glow color overpainting the real stroke, and text gets no glow branch at all (silent, while the engine still grants text a buffer). Spec §17.12 adds glow *around* the result, not over it. **Files:** `src/runtime/visual-canvas2d.js:162-181`. **Correction:** Draw only the halo via a zero-offset blurred shadow of the original geometry without re-filling/re-stroking the path. Apply the same technique to `fillText`/`strokeText` for text. --- ### C11. Boolean leaves accept any literal; `visible: 0` renders visible, `enabled: "false"` executes *Reported by: R3 P2-3 only.* **Confirmed.** `visual-validation.js:233` validates effect `enabled` and object/layer/system `visible` only as ValueSpecs (structure); consumers use `=== false` semantics. Spec §2 bans coercion: "a non-zero number will not be coerced into a boolean. Type mismatches produce `ERR_TYPE_MISMATCH`." Probes: `visible: 0` imports valid and the runtime draws the object (expectation: hidden); `enabled: "false"` and `enabled: 0` execute the effect (only exact `false` skips). **Files:** `src/runtime/visual-validation.js:233`, `src/runtime/visual-engine.js:529, :643-645, :698`. **Correction:** Type-check boolean leaves against the declared type at validation (`ERR_TYPE_MISMATCH`), applying the effect color-literal check pattern (`visual-validation.js:244-247`) to booleans. --- ### C12. `face-motion` snaps on first tick and reverts on zero velocity *Reported by: R3 P2-4 only.* **Confirmed.** `visual-behaviors.js:279-283`: `const current = instance.state.rotation ?? target` initializes follow state to the *target*, so the first tick contributes the full turn regardless of smoothing. When XY speed drops below 1e-6, the behavior breaks, the fresh rotation bucket was zeroed, and the object pops back to the authored rotation. Spec §18.6: "An object with zero velocity holds its previous rotation." **Files:** `src/runtime/visual-behaviors.js:279-283`. **Correction:** Initialize `instance.state.rotation` lazily to the current drawn rotation on first tick. On the zero-velocity branch, emit the held (previous) value. --- ### C13. Trail `fade` is a flat uniform alpha; head never keeps the item's own opacity *Reported by: R3 P2-5 only.* **Confirmed.** `visual-engine.js:1014, :1053, :1064`: line/ribbon mode draws the whole trail at the flat tail factor; nothing multiplies by the item's ramped `opacityMultiplier`. Spec §18.8: "`fade` … Opacity multiplier at the tail; the head keeps the item's own opacity." An item at ramped opacity 0.81 renders a full-strength head point but a line trail at flat alpha 0.4. **Files:** `src/runtime/visual-engine.js:1014, :1053, :1064`. **Correction:** Per-vertex/per-segment alpha from head `itemOpacity` to tail `itemOpacity * fade` (segmented strokes), or state the uniform-color approximation with `WARN_VISUAL_APPROXIMATION`. --- ### C14. Bundle self-containment regex weakened past side-effect imports *Reported by: R2 §9 only.* **Confirmed.** `test/phase1-runtime.test.mjs:41`: the regex was changed from `\bimport\s+[^;(]` to `^[ \t]*import[ \t][^;(\n]*from[ \t]*['"]` (multiline), requiring `from`. A side-effect import (`import './chunk.js';`) in the bundle would no longer be caught. **Files:** `test/phase1-runtime.test.mjs:41`. **Correction:** Add a second assertion for `^[ \t]*import[ \t]*['"]` (side-effect form) so both statement shapes are covered while prose mentions remain tolerated. --- ### C15. Bloom/color-adjust write unclamped channel math; blur radius capped at `max(width,height)` pixels *Reported by: R1 §3. SR §4 (spec side).* **Confirmed.** `visual-effects.js:4-5`: `bloom` adds `pixels + glow * intensity`; `color-adjust` can emit values outside `[0,1]` before `* 255`. `Uint8ClampedArray` hides overflow (no crash), but a full-frame-radius blur is O(pixels × max(edge)) and can hitch on large backing stores. Spec §19.5/19.4 defines blur and bloom radius as 32 scene units; the device radius from `effectPlan` can exceed this under high zoom. SR's finding confirms that `POST_EFFECTS` max may be 32 in scene units, but the pixel filter can still run at huge projected radii. **Files:** `src/runtime/visual-effects.js:4-5, :38, :74`. **Correction:** Clamp processed channels explicitly before assignment. Cap device blur radius by a pixel budget derived from the scene-unit maximum. Assert §19.4 ranges in execution tests. --- ### C16. `ring` with equal start/end angles draws a visible radial spoke instead of nothing *Reported by: R3 P2-8 only.* **Confirmed.** `visual-geometry.js:348-351`: when `startAngle === endAngle`, `directedSweep` returns sweep 0, but the partial-sector branch still emits outer-point → `lineTo` inner-point → closed, which strokes as a straight radial line. Spec §17.9: "`d == 0` draws nothing and raises no diagnostic." The sibling `arc` primitive handles sweep 0 correctly. **Files:** `src/runtime/visual-geometry.js:348-351`. **Correction:** Return no segments when `sweep === 0` (keep the full-annulus branch). --- ### C17. `morph`/`point-wander` on `path` objects: validation allows, runtime silently no-ops *Reported by: R2 §4 only.* **Confirmed.** `visual-validation.js:31-33` includes `'path'` in `POINT_LIST_TYPES`/`MORPH_TYPES`. `pointsOf` (`visual-motion.js:44`) returns raw command objects (`{op, to}`) whose points live in `to`; the behaviors read/write `.x`/`.y`/`.z` on the command object itself, producing junk keys while geometry is untouched. An accepted, authored behavior silently does nothing. **Files:** `src/runtime/visual-validation.js:31-33`, `src/runtime/visual-motion.js:44`, `src/runtime/visual-behaviors.js:225-236, 344-349`. **Correction:** Either drop `'path'` from `POINT_LIST_TYPES`/`MORPH_TYPES` until command-point addressing exists, or make `pointsOf`/the behaviors address `command.to` points for `move`/`line` ops. --- ### C18. Resolved non-integer creation counts silently rounded instead of faulting *Reported by: R3 P2-7 only. **Overstated severity** — downgraded from P2 to borderline P2/P3.* **Confirmed.** `visual-systems.js:59, 63`: `Math.round` on resolved `count` and burst `count`. Spec §18.2: "an integer field whose resolved value is not an integer is `ERR_TYPE_MISMATCH` at that boundary rather than being rounded." The authoring error is unmarked. Severity is lower than R3's P2 because the behavior (rounding) is deterministic and reproducible — it masks an error but doesn't cause incorrect rendering in practice. **Files:** `src/runtime/visual-systems.js:59, 63`. **Correction:** After sampling, throw `RuntimeFault('ERR_TYPE_MISMATCH', …)` when `!Number.isInteger(resolved)` for `count` and each burst count. --- ## Tier 3 — Low-severity, editorial, or testing gaps ### C19. Local visual automation crashes on missing target object *Reported by: R1 §2. SR §2 (spec side).* **Confirmed.** `visual-automation.js:11-13`: `while (object?.children.some(…))` — if `find` returns `undefined`, `.some` is called on `undefined`. Should throw `RuntimeFault('ERR_INVALID_REFERENCE', …)` rather than an unguarded `TypeError`. SR correctly notes this should be `ERR_INVALID_REFERENCE` per §19.1 (undeclared object), not an untyped crash. R1 proposes `object?.children?.some(…)` with a no-op or fault. **Files:** `src/runtime/visual-automation.js:11-13`. **Correction:** Use `object?.children?.some(…)`. When `object` is missing, throw `RuntimeFault('ERR_INVALID_REFERENCE', …)`. > [!NOTE] > R1 rated this P1; however, the validator is supposed to reject bad targets at import. The runtime crash occurs only for exhibit-scope fallbacks and spawned substitutes that bypass validation. Downgraded to T3 for triage because the happy path (validated documents) is not affected. --- ### C20. `remove` actions always report `executed` even when the instance ID is unknown *Reported by: R1 §4 only.* **Confirmed.** `actions.js:53-55`, `visual-engine.js:475`: `this.instances.get(id)?.remove(this.time)` — callers cannot distinguish a successful release from a no-op. Low severity because spawn returns the ID and tests use that ID. **Files:** `src/runtime/actions.js:53-55`, `src/runtime/visual-engine.js:475`. **Correction:** Return `refused` / `failed` when the map has no entry. --- ## Additional P3 findings (from R3, confirmed) These are confirmed but lower-priority items from R3's P3 section: 1. **Diagnostics `raised` list grows unbounded** — `visual-diagnostics.js:19, 39, 63`: push per tick, no pruning in `endTick`, per-frame `slice()` copy. Slow memory growth over long sessions. **Fix:** clear or ring-buffer after the per-tick plan copy. 2. **Degenerate path arcs** — `visual-geometry.js:144-149, 153-186`: coincident-endpoint `arc` forces `acos(0)` and emits a phantom bulge; negative radius drawn mirrored instead of `ERR_OUT_OF_BOUNDS`. **Fix:** early-return on coincident endpoints; check raw radii before `Math.abs`. 3. **Post-effect transient memory unbounded at backing ceiling** — `visual-effects.js:6-39`: each blur pass allocates full-frame copies at up to 4096 device-px. No tie-in to the §19.5 pass budget. **Fix:** tile the blur or cap effect radius relative to the backing store. 4. **`window-traffic` challenge fixture** — `tools/visual-challenge-fixtures.mjs:56-60`: per-copy `wrap` bounds are ±30 px around each copy's own origin; cars cycle in place, never crossing the window. The 4g suite only checks finiteness/equality. **Fix:** if window-crossing is intended, use the window rect as bounds. If cell-stutter is intended, document it. 5. **Schema has no consumer; already diverges from validator** — `schema/xzbt-0.1.schema.json`: no code in `src/tools/tests` uses the schema for validation. Schema marks per-system fields like `render`/`emit` as required that the validator admits without (see C9). **Fix:** consider validating exhibit fixtures against the schema in CI. --- ## Spec-impl alignment items (from SR, confirmed) ### S1. Required traces 19.7.3 and 19.7.4 are not implemented as specified *Reported by: SR §3 only.* **Confirmed.** `test/phase4-execution.test.mjs` "19.7.3" only checks one exhibit-scope stream; "19.7.4" only checks `ERR_AUTOMATION_CONFLICT`. The spec requires: (3) system-scope `at` from instantiation — two spawns 4s apart produce identical curves offset by 4s; (4) cross-scope / undeclared / out-of-registry targets raise `ERR_INVALID_REFERENCE` vs `ERR_UNSUPPORTED_TARGET`. **Files:** `test/phase4-execution.test.mjs`. **Correction:** Add the two-spawn 4s offset assertion and the four negative target cases from §19.7.4. These are test gaps, not runtime defects. --- ## Canvas 2D backend rendering gaps (from R3, confirmed but implementation-layer) ### B1. Per-object buffer grants not consumed by the canvas backend; §17.12 stage order realized per-op *Reported by: R3 P2-1 only.* **Confirmed.** `visual-canvas2d.js:88-96, 97-106, 227-241` never reads `node.buffered`; `visual-engine.js:1171-1201` allocates per-object buffers and sets `plan.buffered`. Fill, stroke, glow, and filter ops each composite individually under the node's blend/alpha instead of the object being rasterized once and composited once (spec §17.12 buffer table/stage order). Observable: double-blending on translucent/blended stroke-over-fill overlap. **Files:** `src/runtime/visual-canvas2d.js:88-96, 97-106, 227-241`, `src/runtime/visual-engine.js:1171-1201`. **Correction:** When `node.buffered` is 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`. --- ## Testing gaps summary No test covers: - Perspective-projected morph (C4) - Velocity behaviors on graphic/repeater hosts (C3) - Semantic validation on per-item behavior arrays (C2) - Backend rendering of buffered nodes, glow, blur, filters, or masked groups through a real surface pool (C1, C10, B1) - Zero-sweep ring / degenerate path arcs (C16, P3.2) - Typed-boolean leaves (C11) - Face-motion smoothing / zero-velocity (C12) - Trail head-opacity / fade taper in line/ribbon (C13) - Non-integer resolved counts (C18) - Long-run diagnostics growth (P3.1) - Post-effect runs against real readback (P3.3) - System-scope spawn-relative automation offset (S1 / 19.7.3) - Target-error taxonomy (S1 / 19.7.4) - CLI validator over all exhibits (C6) - Side-effect imports in bundle (C14) --- ## Recommended correction order Ordered by impact and dependency. Items marked **(design)** require a decision before the fix can be written. | # | Files | Change | Kind | | --- | --- | --- | --- | | C1 | `visual-subsystem.js`, `visual-canvas2d.js`, `app.js` | Pass surface factory; add masked-group + layer-opacity render test | Fix + test | | C2 | `visual-validation.js` | Run `validateBehaviors` for system-level behavior arrays at import | Fix + test | | C3 | `visual-motion.js`, `visual-systems.js`, `visual-behaviors.js` | Integrate velocity for object/repeater hosts, or reject at validation | Fix + test | | C4 | `visual-behaviors.js` | Guard both sides of the `z` delta; add perspective-morph test | Fix + test | | C5 | `visual-engine.js`, `types.js`, `visual-validation.js`, spec | Accept numeric ms or restrict spec to literal — **(design)** | Design + fix | | C6 | `tools/validate-exhibit.mjs`, `src/runtime/validator.js` | Unify or sync the two validators; add cross-exhibit test | Fix + test | | C7 | `schema/xzbt-0.1.schema.json` | Remove spawn-only fields from variants; drop `links` from emitter | Schema fix | | C8 | `visual-validation.js`, schema | Reject emitter-only fields on repeaters; align schema | Fix + schema | | C9 | `visual-validation.js` | Enforce per-type required fields at import | Fix + test | | C10 | `visual-canvas2d.js` | Draw glow as blurred shadow halo, not re-fill/re-stroke; add text glow | Fix | | C11 | `visual-validation.js` | Type-check boolean leaves (`ERR_TYPE_MISMATCH`) | Fix + test | | C12 | `visual-behaviors.js` | Lazy init rotation; hold previous on zero velocity | Fix + test | | C13 | `visual-engine.js` | Per-segment alpha taper from head to tail | Fix + test | | C14 | `test/phase1-runtime.test.mjs` | Add side-effect import assertion | Test | | C15 | `visual-effects.js` | Clamp channels; cap device radius to pixel budget | Fix + test | | C16 | `visual-geometry.js` | Return no segments when sweep === 0 | Fix + test | | C17 | `visual-validation.js`, `visual-motion.js`, `visual-behaviors.js` | Drop `'path'` from morph/wander types, or address `command.to` | Fix | | C18 | `visual-systems.js` | Throw `ERR_TYPE_MISMATCH` on non-integer resolved count | Fix + test | | C19 | `visual-automation.js` | Guard `?.children?.some`; throw `ERR_INVALID_REFERENCE` | Fix | | C20 | `actions.js`, `visual-engine.js` | Return `refused`/`failed` for unknown instance ID | Fix | | B1 | `visual-canvas2d.js`, `visual-engine.js` | Consume `node.buffered` grants in the canvas backend | Fix + test | | S1 | `test/phase4-execution.test.mjs` | Add 19.7.3 spawn-offset and 19.7.4 target-error tests | Test |