Files
XZBT/reviews/02-triage.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

387 lines
28 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.
# XZBT 0.1 Phase 4d4g — 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 §§1719 specification triage) and [00-triage.md](.completed-artifacts/00-triage.md) (§§1416 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 4d4g 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 |