Scope: full re-read of packages/core/src/** (all .ts, all zig/*.zig, build.zig, bench infra).
Every finding below was re-verified against the current source in this pass, with file:line references.
Where an earlier report (AUDIT_REPORT.md, dated 2026-08-03) listed something that is now fixed, it is marked STALE — do not re-file.
writeStringToWasm(wasm-glue.ts:285-303) writes at u8 byte offsetDYNAMIC_START + _strWriteOffset * 4and increments_strWriteOffsetmonotonically (wasm-glue.ts:297).DYNAMIC_STARTis a u32 word index (matches Zigdominator_core.zig:44), but is used here as a byte base — the base should beDYNAMIC_START * 4.- Zig's own string region is words
DYNAMIC_START + _string_bytes_used/4(dominator_core.zig:177) and Zig does reset_string_bytes_usedininit()(dominator_core.zig:728). JS's_strWriteOffsetis a different variable that nothing resets. - Two competing exports exist: the real one (wasm-glue.ts:307-309) and a no-op (dom-cmd.ts:162,
// No-op — kept for API consistency). Grep confirms zero call sites for either. _resetSignals()(signal.ts:1109-1150) resets the JS arrays and calls_core.full_reset()but never touches_strWriteOffset.- Consequence: the JS staging byte pointer grows without bound. Cumulative string bytes of ~284 KB (byte
394976 + off*4reachingEFF_DEPS_REGION = DYNAMIC_START + 262144words ≈ byte 1,531,904, signal.ts:52 / dominator_core.zig:45,424) silently overwrite effect-dependency storage. Because the offset survives_resetSignals, even moderate long-lived apps (mount/unmount, SSR) cross the boundary. - Fix: reset
_strWriteOffsetinside_resetSignals(); multiply the base by 4 (or use word units consistently); add a bounds check before staging; consider deriving the JS offset from Zig_string_bytes_usedso both sides agree.
worker/physics.ts:77,92writes config atconfigBase = _count * 6(dynamic word offset).zig/physics.zig:8reads config atCONFIG_OFFSET_BASE = MAX_PARTICLES * 6(= 3,000,000), a compile-time constant.- These agree only when
_count == MAX_PARTICLES. For every real_count < 500,000, JS writes config to a word the Zig core never reads, and Zig reads zeros. - Consequence:
physics_initsees@max(getConfig(CFG_WIDTH),1) == 1→ the particle world is always 1×1 units, mouse repulsion/mode/tick are never applied. Particle sim is functionally broken at every count below the cap (and the_count*6+64target staging inphysicsSetTargets, physics.ts:154, plus thecount*8page sizing in physicsInitWasm:54, make the layout inconsistent end-to-end). - Fix: make both sides use one layout constant (e.g.
MAX_PARTICLES*8at the tail of the heap), or exportCONFIG_OFFSET_BASEfrom Zig and have JS compute it from the same constant; add a Zig test assertingphysics_inithonors a non-zero width/height.
_executeFramein frame-scheduler.ts has no try/catch around stage execution; if any stage throws, therequestAnimationFrametail is never reached → the app freezes with no recovery._runEffect(signal.ts:585) leaves_activeEffectset on throw.- Fix: wrap each stage in try/catch, report to the logging layer, and guarantee
_activeEffectreset viafinally.
- renderer.ts default case advances the read pointer by 1 instead of the real command size (
[1,10,4,5,5,5,4,1,1]), so any TEXT/SHADOW/TRANSFORM/CLIP command desynchronizes the whole command stream. - Fix: use a size table with a defensive
default.
const root = world.root;is declared twice in the same function scope: engine.ts:146 and engine.ts:184.tsc --noEmit→TS2451: Cannot redeclare block-scoped variable 'root'. Any transpile-only emit (esbuild) produces a SyntaxError at module load, socreateEnginecurrently cannot load at all.- Likely a leftover from the partial-init cleanup refactor (the cleanup block re-reads
rootunnecessarily). Fix: keep the single declaration at line 146 and delete line 184.
- All 20
@splat(Vec4f, value)calls (physics.zig:31-44,168-169,212-215) use the pre-0.11 two-argument form; Zig ≥0.11 requires@splat(value). The module only builds with an older toolchain. - The checked-in
dist/zig/physics.wasmwas therefore produced by a different Zig than the one in PATH — thenpm run build:wasmscript (package.json:7) will fail againstzig 0.14.1. dominator_core.zig has no@splatand is unaffected. - Fix: migrate all 20 call sites to single-arg
@splat, then re-runpnpm build:wasmandpnpm test:wasm.
- Arena never GC'd:
arenaCompact/arena_compact(dominator_core.zig:764) exists but has no JS caller; string/object arena grows monotonically in long-lived apps. - Global singletons:
_world,_graph,_scheduler,_signalCount,_configData,_textStore,_strWriteOffset, arena_objectMap, ECS shared buffer — no multi-engine/SSR isolation. _pushJob/submitToPoolfailure is ignored — returns false but every caller discards it (P2; see correction in §2b).- Canvas resize missing
resetTransform()beforescale()(renderer). - render-graph
_interneviction: overwrites slot 0 when full, never reclaims table length. - text.ts allocates every frame (
split(' ')+ per-wordmeasureText). signal.set()unbatched with engine active can run an effect synchronously via_dispatchSet(signal.ts:736-744) and again from the compute-graph SIGNALS stage viamarkSignalDirty(signal.ts:883 etc.). The_batchSeenGendedup may protect the batched path, but the unbatched direct-effect path bypasses it. Needs a regression test with a real engine frame.- Physics SIMD alignment:
posXPtr/load4cast to*align(16)over a 4-aligned staticu32array (physics.zig:47,80-82,150-156); misalignment is UB in ReleaseSafe and can fault on some engines. Also each SIMD iteration runs 32 serially-dependentxorshift32()calls, which serializes the vector loop. - Build paths diverge:
zig/build.zigdeclares simd128/bulk-memory/atomics and 262144-page memories, but the actual npmbuild:wasm(package.json:7) runszig build-obj+zig wasm-ld --export-allwith no feature flags — two different toolchains produce two different modules; thebuild.zigmemory comments (16MB/32MB vs 262144 pages = GBs) are also wrong.
- Test-suite ground truth:
pnpm test→ 16/17 suites, 278 tests pass, butengine-pipeline.test.tsfails at esbuild transform (engine.ts:184: The symbol "root" has already been declared). P0-E is therefore not cosmetic: the engine feature cannot load at all today. - P1 (new) Zig arena has no bounds checks:
arena_alloc_num/bool/obj/strall doid = _arena_size; _arena_size += 1;with no check against_arena_cap(dominator_core.zig:147-150,156-178). Values are written atheap[id*2](dominator_core.zig:113-116), which overruns the subscriber-offset table once_arena_sizeexceeds ~12k. Safety relies entirely on the JS-side convention "id >= ZIG_SIGNAL_CAP must NOT call WASM arena functions" (signal.ts:800). One violated call site (object/string signals above 4096, signalArray, SSR) = silent corruption. - P1 (new) WASM memory-growth failure is silent:
ensureHeapreturns early on@wasmMemoryGrowfailure (if (result == -1) return;, dominator_core.zig:70-71) andheap_growstill returnsheap_cap_words(the old cap). JS never checks the return, so on low-memory devices the core keeps writing past capacity with no OOM signal to the app. - P2 Zig
_dirty_bufis a static[1024]u32and only appends while_dirty_count < 1024(dominator_core.zig:526); the bitmap/snapshot path covers all 65536, so this is bounded — verify no code path depends on_dirty_bufalone. - Perf (measured): physics 500K particles step = 31.0 ms (2× over the 16.6 ms frame budget), 100K = 7.2 ms. Caveat: because of P0-B, the config never reaches Zig, so these numbers run the sim on a 1×1 world with mouse repulsion effectively inert.
- CORRECTED — job loss (was P1): the shared queue is single-producer (main thread, serialized event loop), so payload-read-before-CAS (worker-pool.ts:51-58) is benign, the full-check exists (worker-pool.ts:77), and the producer notifies sleeping workers (worker-pool.ts:289). Downgraded to P2 edge (per-worker local deques are SPSC and safe).
- CLEARED:
_jsDirtyList/_jsDirtyBitmapresize dynamically (signal.ts:386-405) — no 8192-signal cap; scale.test (20k dirty effects) passes.
- C1 (AUDIT_REPORT) / WASM views stale after
heap_grow: fixed —growCoreHeapcallsrefreshViews()(wasm-glue.ts:227-228). - P0-1 (prior pass) / BFS queue overflow: fixed — propagation grows the queue for the full fan-out batch (
_ensureBfsQueue(_bfsTail + batchWrites + 16), compute-graph.ts:464). - subs-flat.ts already guards
_u32null (if (!_u32) return-style) and callsfull_reseton reset.
WASM arena read bounds, generation-stamped event __did + per-call bubble-path pool (events.ts), change-detected DOM renderer, generation-deduped dispatch, signal tests asserting exact run counts, bench infra (time-boxed probe, GC-pause detection, percentile stats).
- P0-A
_strWriteOffset: reset in_resetSignals, fix base units, add bound + regression test. - P0-B Physics layout: unify config offset, add Zig test.
- P0-C Frame error boundary +
_activeEffectfinally. - P0-D Command size table.
- P1 batch — arena GC, lifecycle isolation, worker/job CAS, canvas resize, text allocation, physics SIMD alignment.
- Structured logging / observability (logging.ts is a stub today).