From the 2026-07-06 codebase audit (docs/audits/codebase-audit-2026-07-06.md, in-repo). Finding IDs are stable — cite them in fixes.
Severity: High (C15, C16) + Medium (C54) · Status: CONFIRMED
C15 — CanvasBox physics props look controlled but aren't (canvas-box.tsx:89-104; canvas-elastica.tsx:274-282). The update effect covers mass, restitution, isStatic, width, height… and calls updateParticle — which only rewrites a Map entry. But mass/restitution/static are read exclusively inside the init block, and collision dimensions are baked at initialCondition (the engine reads rect only there). Animate <CanvasBox width={w}> 10→100: the drawn rect grows (renderer reads the Map each frame) while the collision body stays 10px until an unrelated re-init — visible interpenetration. mass={5} does nothing, ever, unless something else resets the world.
C16 — DomElastica silently drops solver.substeps (dom-elastica.tsx:100-135). stableConfig rebuilds solver from an allowlist {slop, percent, fixedDeltaTime} — substeps (typed, documented in the README table) is discarded. config={{ solver: { substeps: 4 } }} → engine runs 1 substep, no warning. Canvas passes the raw config through — the identical config behaves differently per mode.
C54 — three config strategies, two default sets (dom-elastica.tsx:100-135 vs canvas-elastica.tsx:90-107). DOM: allowlist-reconstruction with its own DEFAULT_CONFIG (gridSize: 8) over engine defaults (4). Canvas: useMemo(() => config, [13 hand-picked deps]) — missing config?.solver?.substeps in the deps, so runtime substep changes are ignored. Net: <DomElastica> with no config runs gridSize 8/substeps dropped; <CanvasElastica> runs gridSize 4/substeps honored. The README claims one set of defaults for both (C12).
Direction
One shared useStableConfig hook, one source of defaults, spread config.solver instead of allowlisting. For C15: wire updateParticle to elastica.setMass/setRestitution + dimension updates, or document the props as init-only and remove them from the update effect.
Severity: High (C15, C16) + Medium (C54) · Status: CONFIRMED
C15 — CanvasBox physics props look controlled but aren't (
canvas-box.tsx:89-104;canvas-elastica.tsx:274-282). The update effect coversmass, restitution, isStatic, width, height…and callsupdateParticle— which only rewrites a Map entry. But mass/restitution/static are read exclusively inside the init block, and collision dimensions are baked atinitialCondition(the engine readsrectonly there). Animate<CanvasBox width={w}>10→100: the drawn rect grows (renderer reads the Map each frame) while the collision body stays 10px until an unrelated re-init — visible interpenetration.mass={5}does nothing, ever, unless something else resets the world.C16 — DomElastica silently drops
solver.substeps(dom-elastica.tsx:100-135).stableConfigrebuildssolverfrom an allowlist{slop, percent, fixedDeltaTime}—substeps(typed, documented in the README table) is discarded.config={{ solver: { substeps: 4 } }}→ engine runs 1 substep, no warning. Canvas passes the raw config through — the identical config behaves differently per mode.C54 — three config strategies, two default sets (
dom-elastica.tsx:100-135vscanvas-elastica.tsx:90-107). DOM: allowlist-reconstruction with its own DEFAULT_CONFIG (gridSize: 8) over engine defaults (4). Canvas:useMemo(() => config, [13 hand-picked deps])— missingconfig?.solver?.substepsin the deps, so runtime substep changes are ignored. Net:<DomElastica>with no config runs gridSize 8/substeps dropped;<CanvasElastica>runs gridSize 4/substeps honored. The README claims one set of defaults for both (C12).Direction
One shared
useStableConfighook, one source of defaults, spreadconfig.solverinstead of allowlisting. For C15: wireupdateParticletoelastica.setMass/setRestitution+ dimension updates, or document the props as init-only and remove them from the update effect.