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
17 KiB
17 KiB
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):
1bc4901docs(visual): sections 17–19 review triage at revision 0.8 (spec/plan docs, newtools/verify-spec-contract.py).6587d3efeat(visual): align schema, validator, resolution engine with the 0.8 visual contract (schema/xzbt-0.1.schema.json,resolution.js,validator.js, newvisual-contract.js/visual-validation.js,exhibits/minimal-visual.xzbt, tests).699492fbuild(visual): bundle visual modules intoXZBT.html.0af58dafeat(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:
527220eand 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 newsrc/runtime/visual-*.jsmodules,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 useVisualSubsystem.render) that (a) puts amaskon agroupobject, or (b) setsopacity/blendon a layer — including via the new 8.1 binding/automation onvisuals.layers.<id>.opacity(today'sexhibits/exhibit-d.xzbtdoes exactly this). - Impact: (a)
renderFrameis called withoutcreateSurface, so the mask branch ofdrawNodegetsnullfrom the dummy surface provider (visual-canvas2d.js:233returnstarget, which isnullfor unbuffered layers) and throwsTypeError: Cannot read properties of null (reading 'context');app.jsonFramecatches, logs an error, and callsvisual.deactivate()— the entire visual subsystem shuts down permanently on the first frame. (b) Layeropacity/blendare 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 acreateSurfacefactory → renders fine. Layer probe:layers.main.opacity = 0.5, plan reportsbuffered: true; with factory,globalAlphasequence[1, 1, 0.5]; production path[1, 1](opacity dropped).tools/build-visual-acceptance.mjs:34instantiatesVisualSubsystemthe 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 throughVisualSubsystem(not justrenderFramewith 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 forsystem.behaviors;validateBehaviorsat :614 runs for objects only); runtime effects insrc/runtime/visual-behaviors.js:97,155,225-236andsrc/runtime/visual-systems.js:164(points: nullfor procedural items). - Triggering conditions: A
particles/emitter/repeatersystem whose top-levelbehaviorsarray contains an unknowntype, afield-follownaming an undeclared field, or apoint-wander. - Impact: Unknown type passes import validation and throws
ERR_INVALID_BEHAVIOR_TYPEat activation inside theVisualEngineconstructor → app'sattachVisualscatch → the entire visual subsystem fails (spec §18.6 item 12 expects this error at import).field-followwith an undeclared field throwsERR_INVALID_REFERENCEfromFieldSet.sampleon the first tick →failSystemremoves the system. System-levelpoint-wanderthrowsERR_INVALID_BEHAVIOR_TARGETon the first tick (procedural items havepoints: null; only template-level behaviors get point lists vianode.motion) → system removed. All three documents reportvalid: trueat import. - Evidence: Probes:
behaviors: [{type:'levitate'}]→valid: true, runtimethrew: ERR_INVALID_BEHAVIOR_TYPE;[{type:'field-follow', field:'nope'}]→valid: true, after two tickssystems alive = 0; system-levelpoint-wanderon a polyline render template →valid: true,systems alive = 0(the same behavior object-level on the template validates and runs fine). - Suggested correction: Call
validateBehaviorsfor system-levelbehaviorstoo — withoptions.fieldsset and either rejecting point-list/morph behaviors at system scope or wiringitem.pointsfrom 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 lineitem.points[index].z += ((target[index].z ?? 0) - item.points[index].z) * amount;). - Triggering conditions: A
morphbehavior between point-list objects (polyline/polygon/spline) whose points carry noz, in a scene usingperspectiveprojection. - Impact:
item.points[index].zisundefined, sozbecomesNaNevery tick.visual-engine.jsproject()guards withsubpath.start[2] ?? 0, which does not catchNaN(not nullish) → perspective factor and screen coordinates becomeNaN→ the object disappears (canvas ignores non-finite coordinates). The 4g suite's recursivefinite()plan check passes because today's fixtures run orthographic, wherezis 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_TYPESinclude'path', withMORPH_PATH_OPSsupport); runtimesrc/runtime/visual-motion.js:44(pointsOffalls back tocommands) andsrc/runtime/visual-behaviors.js:225-236,344-349. - Triggering conditions:
point-wanderormorphon apathobject (commands withmove/lineops). - Impact:
pointsOfreturns the raw command objects ({op, to}), whose points live into; 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/zfields. - Suggested correction: Either drop
'path'fromPOINT_LIST_TYPES/MORPH_TYPES(and the schema behavior target implications) until command-point addressing exists, or makepointsOf/the behaviors addresscommand.topoints for themove/lineops.
5. P2 — Repeater silently accepts rate/burst/limit/capacity (and trail) instead of ERR_UNKNOWN_FIELD
- Files:
src/runtime/visual-validation.jsvalidateSystems(:325 rejectslinkson emitters, :328 adds a graphic-only rejection list, but no repeater-specific check);src/runtime/visual-systems.js:80,96; schemaschema/xzbt-0.1.schema.json:3229(repeater variant listsrate/burst/limit/capacityas allowed properties). - Triggering conditions: A
repeatersystem declaringrate,burst,limit,capacity, ortrail. - Impact: Spec 18.5 (docs line 2399): "A
repeaterhas norate,burst,limit,capacity,lifetime, orinputs; each isERR_UNKNOWN_FIELD." Import validation accepts all of them.ratealone then throwsERR_UNBOUNDED_EMISSION("Emission declares neither a lifetime nor a limit") at activation — a misleading error for a system type that has no emission;rate+limitpasses and the rate is silently ignored forever;trailsilently records/draws trail history even though the field is not in the repeater table (the schema, inconsistently, does forbidtrail— so schema and validator disagree). - Evidence: Probes: repeater +
rate: 5→valid: true, activation throwsERR_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(andfields, if unintended — it validates but is skipped by the repeater integrator atvisual-systems.jsadvanceItem) on repeaters, and removerate/burst/limit/capacityfrom the schema's repeater variant.
6. P2 — The two repo validators disagree: the standalone CLI rejects legal visual bindings
- File:
tools/validate-exhibit.mjs:572(ExhibitValidator.validateBindingTargetfalls through toERR_UNSUPPORTED_TARGET… "not exposed by the shared 0.1 target registry" for everyvisuals.*target). - Triggering conditions: Running
node tools/validate-exhibit.mjson 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 in6587d3e) andexhibits/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 instantiateExhibitValidatoronly 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_REFERENCEon legal targets; runtime validator accepts both. All 14 challenge fixtures and exhibits A–C pass both validators. - Suggested correction: Make
tools/validate-exhibit.mjsdelegate tosrc/runtime/validator.js(single source of truth), or port the four 8.1 visual target families intoExhibitValidator; add a test that runs the CLI validator overexhibits/*.xzbtandexhibits/visual-challenges/*.xzbtso 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 in6587d3e. - Details: All four variants allow top-level
release,ownership,inputs,cancelWithScenario— spec 19.2 and the validator (visual-validation.js:314) make eachERR_UNKNOWN_FIELD("a field of the spawn container"). The emitter variant allowslinks, which the validator rejects (:325). The repeater variant allowsrate/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
spawnsub-object), droplinksfrom 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); runtimesrc/runtime/visual-engine.jsspawn()→parseDuration(sampleTree(...)), andparseDuration(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_DURATIONwhen 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
validateSpawnand resolve it to milliseconds inspawn()beforeparseDuration.
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 requiresfrom, 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 --checkclean.npm run build:visual-acceptance: OK — acceptance page rebuilt with 18 fixtures;phase4-challengestest 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.mjsover everyexhibits/**/*.xzbtandClaude outputs: all pass exceptexhibit-d.xzbtandminimal-visual.xzbt(finding 6); both pass the production validator (verified directly viaparseAndValidateExhibit).- Runtime probes (temporary scripts, since deleted) against the live modules: system-scope automation on a behavior-less graphic object (works —
rawrefreshed every tick viaadvanceVisualMotion); the six defect confirmations cited in findings 1–5; repeat/point and burst/limit/capacity accounting inProceduralSystem(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
renderFramecall-contract level, not by screenshot), slice 4h GC6 performance ceilings, audible acceptance, and long-run memory behavior of the offscreenSurfacePool(unboundedallocatedgrowth across frames is untested). Untracked-file mtimes were used as a proxy for "today"; uncommitted work has no authoritative timestamps.