Skip to content

Untangle the codebase - #39

Merged
ParthJadhav merged 29 commits into
mainfrom
refactor/untangle-codebase
Nov 7, 2026
Merged

ParthJadhav merged 29 commits into
mainfrom
refactor/untangle-codebase

Conversation

@ParthJadhav

Copy link
Copy Markdown
Owner

Summary

A structural pass over RageLayer, following one question: should these pieces change together? Values and logic that had to agree across modules now have one owner, the engine stops reaching into the overlay's canvases and the monitor's policy, the three ready-made toolbars (React, Vue, <rage-layer>) run from one framework-neutral session, and the toolbar entry no longer pulls in heavy tool art (dist/toolbar/index.js 13.3 KiB to 5.4 KiB gzip). Three pre-existing bugs were fixed along the way (snapshot effects layer at the wrong pixel ratio on 2x displays; decal stamps at a stale dpr after a resize or recapture; the gravity gun's hum muted every frame). Then comments that only restated the code were removed, and tests that could not fail were rewritten.

Public API is unchanged. Every entry's runtime export list is identical to main, and the bundled .d.ts files differ only in private members and in comments on private members (listed below). No runtime dependencies were added. Docs paths are untouched.

Problems found (before any change)

  • src/react/RageLayer.tsx:112, src/vue/RageLayer.ts:57 and src/element.ts:253 each build an engine with new RageLayerEngine + a registerTool loop (bypassing mountRageLayer), build a ToolbarModel, wire the window shortcut listener, save/restore focus, and tear down in their own order.
  • The same three files each compute roving-focus arrows and re-wrap the index that ToolbarModel.setFocusIndex already wraps; Vue lacks the empty-bar guard (% 0).
  • src/react/toolbar-styles.ts:202 and src/vue/styles.ts:173 each implement a refcounted <style> injector.
  • MAX_CAPTURE_HEIGHT = 12000 is defined in both src/engine.ts:64 and src/capture.ts:48.
  • The parked-pointer sentinel -1000 is written in pointer-input.ts, engine.ts, heavy-tools.ts, advanced-tools.ts, and tested as < -100 in two files and <= -999 / > -999 in the engine.
  • The void gradient stops are in overlay.ts:121 (CSS) and engine.ts:848 (snapshot canvas).
  • FxView is declared twice (overlay.ts:32, fx-render.ts:75).
  • fx-render.ts:645 branches on p.color === "#c98bff", a literal set in engine.ts:2160.
  • surface.ts:383, postfx.ts:257 and engine.ts:2365 each compare the upload probe with SLOW_UPLOAD_THRESHOLD_MS themselves.
  • engine.ts:1601 silences the tool loops by naming water, saw, flamethrower and void, apart from the if-chain in audio.ts that configures each loop name.
  • The engine copies, measures, restores, erases and clears the overlay's damage canvas by hand (engine.ts:693, 724, 742, 777, 997, 1011), rebuilds the fx transform from the overlay's offset fields (clearFxCanvas, which only works if positionFx ran first), and re-implements Overlay.dispose() inline.
  • Post-FX/heat-canvas sizing is written twice (engine.ts:1643, 1704).
  • decals.ts:47 caches each context's dpr forever ("targets keep a fixed dpr"), but overlay.ts and content.ts re-pick dpr on resize/recapture.
  • snapshot() reads the fx/post-FX canvas with overlay.dpr although it is backed at overlay.fxDpr.
  • Dead code: drawBrittleFracture, drawDent, drawSurfaceScore (decals), PerformanceMonitor.setQualityReason.
  • WOOD.flammability and burnRate are 1 (multiplications by one), WOOD.color is unread, Math.max(0.14, WOOD.restitution) is always 0.14, and WOOD.toughness < 2 makes the gun's crack branch unreachable.
  • toolart/icons.ts imports every base and heavy drawing to fill a bounds table, and toolbar.ts / tools.ts import art through toolart/index, so the toolbar and base-tools chunks pull in heavy ordnance art.
  • default-tools.ts:7 and lazy.ts:28 both spell out the official order formula; the lazy loader's doc promises a gravity tool it does not load.
  • The engine derives the frame-skip interval and target fps from the monitor's rate-cap flag (borrowing the balanced profile), and re-checks qualityMode === "auto" on recommendations the monitor only makes in auto mode.
  • qualityProfile is a second field that must be reassigned with qualityTier.
  • stepSingularity fades the "void" loop every frame with no singularity, muting the gravity gun that drives the same loop.
  • audio.ts:337 has an if (name === "fire") jitter special case inside the generic loop updater.
  • Tests: sdk.test.mjs isolation test never reads its counter; toolart.test.mjs cache test compares two deterministic renders; toolbar.test.mjs dispose test only checks that reading state does not throw; toolsets.test.mjs re-derives the order formula and hard-codes set sizes; engine.test.mjs:383 depends on loop iteration order; content.test.mjs:292 asserts a deleted method is absent; tools.test.mjs:50 restates the WOOD constants.

Fixes, with the problems they solve

  • One toolbar session for every ready-made toolbar (src/toolbar-session.ts): openToolbarSession mounts through mountRageLayer, builds the model, owns the shortcut listener and focus return; rovingFocusTarget owns arrow navigation. A shared sharedStyle helper refcounts <style> tags.
    • React, Vue and the element each built engine + model + listeners + focus handling.
    • Roving focus computed three times; Vue had no empty-bar guard.
    • Two refcounted style injectors.
  • Each cross-module value gets one owner: MAX_CAPTURE_HEIGHT from capture.ts; PARKED_POINTER / isParked in math.ts (exact sentinel match); paintVoid + VOID_STOPS in overlay.ts; FxView from overlay.ts; SPAGHETTI_COOL/WARM in fx-render.ts; canvasUploadsAreSlow / uploadsProbedFast in gl.ts.
    • Duplicated capture cap, sentinel spellings, void gradient, FxView, colour-keyed branch, threshold comparisons.
  • Sound loops are one table (LOOP_VOICES with filter, jitter and a held flag); the engine fades HELD_TOOL_LOOPS; the singularity fades "void" only on the frame it ends.
    • Engine naming tool loops; per-name if-chain; fire jitter special case; gravity gun hum muted every frame.
  • The overlay owns its canvases: snapshotDamage, restoreDamage, damagePixelCost, eraseDamage, clearFx, dispose, invalidateFxTransform are Overlay methods; post-FX/heat sizing is one engine method.
    • Engine reaching into damage/fx canvases and offsets; clearFxCanvas call-order dependency; inline copy of Overlay.dispose; duplicated heat sizing.
  • Snapshots read the effects layer at its own ratio (overlay.fxDpr).
    • Half-size, shifted effects in snapshots on 2x displays.
  • Decal scale cache keyed on canvas size, since a dpr change always resizes the canvas.
    • Stale dpr after resize/recapture.
  • Delete what has no reason to exist: three decal functions, setQualityReason, constant WOOD knobs and the unreachable crack branch.
    • Dead code and speculative config.
  • Register icon bounds beside each drawing and import art from its own module.
    • Toolbar and base-tools chunks pulling heavy art; bounds table separate from art.
  • officialToolOrder used by both eager and lazy default sets.
    • Order formula written twice; stale lazy doc.
  • Frame-rate policy lives in the monitor (minFrameIntervalMs(tier), targetFps(tier)); qualityProfile is a getter.
    • Engine re-deriving monitor policy; redundant auto-mode gate; duplicated quality state.
  • Tests: rewritten to fail on regressions (sdk isolation, icon cache, dispose teardown, toolset composition, order-insensitive loop test, all built-in icon bounds, roving focus); removed the deleted-method test and the WOOD-constant test.
    • Tests that could not fail or restated the implementation.

Found by the check-and-repeat passes (subagent hunts), and fixed

  • snapshot() read the fx/post-FX canvas with overlay.dpr instead of overlay.fxDpr (half-size, shifted effects in snapshots on 2x displays).
  • stepSingularity faded "void" every frame, muting the gravity gun's hum; it now fades only when its singularity ends.
  • The engine re-implemented Overlay.dispose() and wrote the overlay's offset fields instead of invalidateFxTransform(); snapshot read the damage canvas directly (drawDamageInto now).
  • isParked kept the < -100 heuristic and four tool ticks still used <= -100; all use the exact sentinel now.
  • The shared style helper had renamed Vue's <style data-rage-layer="toolbar">; restored.
  • if (name === "fire") jitter moved into the loop voice table; qualityProfile became a getter; rateCap60 and Overlay.damageReady lost their last callers and were removed.
  • Three comments the comments pass removed were rules, not restatements (decal-log call order, reference dropping, rocket-into-hole behaviour); restored.
  • New test for toolbar session teardown (shortcuts stop, engine disposed, focus returned).

Deliberately not changed (and why)

  • Three toolbar stylesheets (React inline styles, Vue CSS, element shadow CSS): their DOM structure differs (flash/chip placement), and unifying them changes visuals that no automated check covers.
  • React defaults history off while Vue and the element force it on: public behaviour, not a structural accident we can prove.
  • Toolbar z-index literal 2147483001 vs engine default 2147483000: tying them would change behaviour for hosts that pass a custom zIndex.
  • Public tool re-export lists (index.ts, entry files), COMBO_DEFINITIONS vs applyCombo, ContentLayer pass-throughs, engine.random(): public API that must stay identical.
  • engine.test.mjs scheduler test that drives private fields: rewriting it needs a scheduler harness.

What was removed, and why

Comments (net about 140 lines across 18 files, commit 081078a, three restored later): only comments that restated the code below them (for example "Dark torn core." above the drawing code, section narration in tool handlers, a 13-line paragraph in live.ts duplicated from the paint() doc, and a comment in the engine constructor that had been copied from capture-controller.ts and described unrelated code). Comments about browser behaviour, measured costs, call-order hazards and design intent were kept. All public JSDoc that ships in .d.ts was left byte-identical; a script confirmed the comments commit changed no code.

Tests (412 before, 412 after):

  • Removed: tools.test "all base tools share one immutable wood physics contract" (restated WOOD constants, two of which were removed as no-ops); content.test "freeze-specific melt support is absent" (asserted a deleted method is missing).
  • Rewritten so they can fail: sdk.test isolation (never read its counter), toolart.test cache (two deterministic renders match with or without a cache; now counts canvas creation) and bounds (all built-ins, not just the hammer), toolbar.test engine-dispose teardown (only checked that reading state does not throw), toolsets.test (re-derived the order formula and hard-coded set sizes), engine.test loop silencing (depended on iteration order), performance.test (reads the frame interval instead of the removed rateCap60 flag).
  • Added: roving focus (including the empty bar Vue did not guard), toolbar session teardown.
  • scripts/check-coverage.mjs: removed the pointer-input.ts lcov exemption, which the guard itself flags once the module's coverage reads correctly (95% now).

.d.ts diff against main (whole list)

RageLayerElement:  - private engine; - private model; - private previousFocus; - private onWindowKeyDown; (+ its private JSDoc)
                   + private session; + private get model();
PhysicsWorld:      - JSDoc on private `iterations`
RageLayerEngine:   - private qualityMode; - private clearFxCanvas (+ JSDoc); private qualityProfile field -> private getter
                   + private singularityHum; + private fitPostFXToOverlay (+ JSDoc)
ContentLayer / SurfaceRenderer: JSDoc removed from private members only
lazy: loadAdvancedTools doc no longer promises a "gravity" tool it never loaded

Verification (baseline main @ 4e27d35 vs this branch)

Command Baseline After
bun install --frozen-lockfile pass pass
bun run audit fail (14 dev-dependency advisories, pre-existing) same failure
bun run lint (Biome) pass pass
bun run typecheck pass pass
bun run check:artifacts pass pass
bun run test:coverage pass, 412 tests, 89.42% lines / 86.03% functions pass, 412 tests, 91.52% lines / 86.67% functions
bun run build pass pass
bun run typecheck:consumers pass pass
bun run check:package (publint, attw, gzip budgets) pass pass
bun run docs:build pass pass
Runtime export names, every entry identical
Bundled .d.ts public surface identical (see above)
node scripts/browser-test.mjs (as written) hangs locally (Chrome 157 on this macOS host never answers CDP with the SwiftShader flags; killed after 25 min) same
Same 20 runtime checks driven through Playwright with system Chrome (local port, not committed) 18/20: "capture status live, expected snapshot" (local Chrome supports live capture; CI Chrome does not) and two favicon 404s in the console same 18/20, same two failures
Demo page smoke test (Playwright): all 15 tools selected, dragged and clicked; smash/burn/cut via toolbar buttons; undo every tool redraws the page, flamethrower/chainsaw/laser/black hole/demolition/lightning/acid/sticky bombs leave damage, undo works, no page errors same (hammer damage is random per stroke: 0 to 0.7% across runs on both sides)
benchmark.mjs --scenarios idle,mixed --assert failed its assertion (machine under load from other jobs: idle 21 fps) passed
Memory leak gate (--leak-cycles 20) pass pass

Needs manual testing

  • React, Vue and <rage-layer> toolbars in a real app: open, use keyboard shortcuts (1 to 0, P, R, M, Escape), arrow keys in the bar, close, and check focus returns to where it was. All three now share openToolbarSession.
  • Snapshot export on a 2x display with effects visible: the effects layer should now line up with the page.
  • Gravity gun (exported but not in the default set): its hum should now be audible while held.
  • A page taller than 4500px, resized after some damage: scorch/decal stamps should stay crisp.
  • bun run demo:tools to review the tool reel (no automated check judges tool art).
  • The full Chrome runtime suite in CI (scripts/browser-test.mjs), which could not run as written on this machine.

Overlap with open PRs

🤖 Generated with Claude Code

ParthJadhav and others added 25 commits November 7, 2026 15:02
The capture-height cap, the parked-pointer sentinel, the void gradient,
the spaghetti tint the renderer branches on, the fx view type and the
slow-upload decision were each written out in two or three modules that
had to agree without anything tying them together. Each now lives with
the module that owns it and the others import it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The engine silenced held-tool loops by naming water, saw, flamethrower
and void itself, apart from the if-chain in audio.ts that configured
them. A table of loop voices now holds the filter settings and whether
a loop belongs to a held tool, and the engine fades whatever the table
marks as held.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The engine copied, measured, restored, erased and cleared the overlay's
damage canvas by hand, and rebuilt the fx transform from the overlay's
offset fields on the promise that positionFx had already run. Those
operations are now Overlay methods, so the canvas layout can change in
one file. Post-FX and heat-field sizing, written out twice, is one
method.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
decals.ts cached each context's dpr forever, assuming targets keep one
transform for life. The overlay and the content layer both re-pick dpr
on resize or recapture (pickPixelRatio drops to 1.5 past 4500px), and
after that stamps were rasterised at the stale scale. A dpr change
always resizes the canvas, so the cache is now keyed on the canvas size
as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
drawBrittleFracture, drawDent and drawSurfaceScore had no callers, and
neither did PerformanceMonitor.setQualityReason. WOOD carried
flammability and burnRate of 1 (pure multiplications by one), an unread
colour, and a restitution that Math.max always overrode; its toughness
check in the gun made a crack branch unreachable. Those knobs and the
branch go, along with the test that only restated the knob values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The measured bounds of the base and heavy tool art sat in a table in
toolart/icons.ts, which therefore imported every drawing; the toolbar
imported icons through toolart/index, and the base tools imported their
art through the same index. So the toolbar and the base tools chunk
both pulled in the heavy ordnance art. Bounds now register beside each
drawing (as the advanced art already did), and those modules import
only the art they use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
default-tools.ts and lazy.ts each spelled out "base tools minus the
last, then heavy, then advanced, then the last base tool", and the lazy
loader's doc promised a gravity tool it does not load. Both now call
officialToolOrder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each ready-made toolbar built its own engine, registered tools by hand
instead of through mountRageLayer, built a ToolbarModel, wired the
window shortcut listener, saved and restored focus, and computed
roving-focus arrows with its own index wrapping (Vue without the
empty-toolbar guard). openToolbarSession and rovingFocusTarget now do
that once; each adapter keeps only its rendering and its own defaults.
The two refcounted <style> injectors share one helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The engine derived the frame-skip interval and the target frame rate
from the monitor's rate-cap state and borrowed the balanced profile to
do it, while the monitor made the rate-cap decision itself; the engine
also re-checked quality mode on recommendations the monitor only makes
in auto mode. The monitor now answers both questions per tier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Narration such as "Dark torn core." above the code that draws it, a
13-line paragraph in live.ts duplicated from the paint() doc, and a
comment in the engine constructor copied from capture-controller.ts
that described unrelated code. Comments that record browser behaviour,
measured costs, call-order hazards and design intent stay, as does all
public JSDoc that ships in the .d.ts files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The coverage guard asks for this once the module's line coverage reads
correctly again (it now reports 95%), so the exemption no longer hides
anything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- sdk: the isolation test bumped a counter it never read; it now checks
  that two instances keep separate state and that reset starts fresh.
- toolart: the cache test compared two deterministic renders, which
  match with or without a cache; it now checks no canvas is created on
  a repeat request. The bounds test covers every built-in drawing, not
  just the hammer.
- toolbar: "disposing the engine tears the model down" only checked
  that reading state did not throw; it now checks the model stops
  notifying. Adds a test for roving focus, including an empty bar.
- toolsets: re-derived the default order formula and hard-coded set
  sizes; it now checks the behaviour (every set once, hammer first,
  broom last) and that the lazy loaders return copies.
- engine: the loop-silencing test depended on iteration order.
- content: removed a test that only asserted a deleted method is absent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
snapshot() cut the fx (or post-FX) canvas with the page's dpr, but that
canvas is backed at the effects ratio (min(dpr, effectsPixelRatio),
1 by default). On a 2x display every exported snapshot carried a
half-size, shifted effects layer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The engine re-implemented Overlay.dispose() inline (missing its damaged
flag reset) and reset the fx offsets by writing the overlay's fields,
which is what invalidateFxTransform() already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
isParked() kept the old "x < -100" heuristic, which also caught real
positions left of an offset container. The parked position is always
assigned exactly, so compare with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shared style helper had renamed the Vue <style> marker; it now
takes the attribute value too, so Vue keeps data-rage-layer="toolbar".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gravity gun drives the "void" loop while held, but the singularity
step faded that loop on every frame with no singularity, so the hum
never sounded. The step now fades it only on the frame its own
singularity ends. The fire crackle's filter jitter moves into the loop
voice table instead of an if on the loop name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qualityProfile was a second field that had to be reassigned whenever
qualityTier changed; it is now a getter over QUALITY_PROFILES.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The measured-bounds note was copied into two art modules; heavy and
advanced art now point at the one in toolart/base.ts. paintVoid shares
the void layer's colours, not its exact geometry, and says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gun, flamethrower, paintball and black hole still tested the
parked pointer as "x <= -100" beside the shared helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The comments pass dropped a call-order rule in content.ts (recorded
decals must land before a rebuild), the memory reason for clearing
decal-log references, and the second half of the rocket's note on what
happens when its target has been destroyed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The engine now asks the monitor for the frame interval and target rate,
so the rate-cap flag was read only by tests. They assert the interval
the cap produces instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
snapshot() was the last place the engine read the damage canvas and
its pixel ratio directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The session now does teardown for all three toolbars, so a test checks
that closing it stops shortcuts, disposes the engine and returns focus.
onClose is optional like the model's, so React passes its prop
straight through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
drawDamageInto replaced the getter's last caller. The session test now
removes the elements it adds and closes the session even when an
assertion fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown
Codex Review Summary: ❌ Failed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ❌ Failed 2026-11-07T10:39:35.537254Z
We couldn’t complete this review. Comment @codex review to try again.
fff2af0 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

ParthJadhav and others added 2 commits November 7, 2026 17:48
bun audit failed on main as well (vite, vue/server-renderer, happy-dom,
source-map-js, shell-quote, launch-editor advisories published after the
last green main run). Bump vite to 6.4.5, happy-dom and its registrator to
21.0.4, pin vue to 3.5.44 (also for vitepress's nested copy), and override
the transitive source-map-js, shell-quote and launch-editor to patched
releases. All are dev-only; no runtime dependency changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before the LOOP_VOICES table, SoundEngine.loop fell through to the saw voice
for any name it did not special-case. The table lookup threw a TypeError
instead, which an untyped custom tool calling engine.sound.loop() with its own
name would hit inside the frame loop. Fall back to the saw voice again, with
a test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ParthJadhav

ParthJadhav commented Nov 7, 2026 •

Copy link
Copy Markdown
Owner Author

Independent pre-merge review

Verdict: MERGE AFTER CI. I pushed three fixes (9a5903b, fc3d84c, eb74b2c). The new head needs a green CI run. The Chrome runtime suite is the one required check I could not reproduce locally (details below).

CI triage (run 42364880027, head fff2af0)

Check Result Cause Action
Lint, typecheck, test, build fail at Dependency audit (bun audit, 14 advisories in vite, happy-dom, vue/server-renderer, source-map-js, shell-quote, launch-editor) Already failing on main, not caused by this PR. I checked out main @ 4e27d35 and bun audit lists the same 6 packages. The advisories were published after the last green main CI run (2026-09-05). This PR does not touch package.json or bun.lock. Fixed anyway because it blocks every PR (9a5903b): vite 6.4.5 (dependency + override), happy-dom and global-registrator ^21.0.4, vue pinned to 3.5.44 (override also covers vitepress's nested 3.5.41), overrides for source-map-js ^1.2.2, shell-quote ^1.11.0 and launch-editor ^2.14.2. All are dev-only; runtime deps and the published package are unchanged. bun audit now reports no vulnerabilities.
Chrome runtime suite / Node 20, 22, 24 consumer build / nextjs, nuxt, sveltekit example build skipped needs: verify Ran locally (see below)
CodeQL, Analyze pass
Lint, typecheck, test, build (run 42375848839, head fc3d84c) audit passed; Unit tests then failed: 9 Vue component tests with null is not an object (doc.createComment) Order-dependent test bug that was already on main. Bun shares one module cache across test files, and adapters.test.mjs statically imports vue without the DOM harness. When CI's file order runs it first, @vue/runtime-dom caches a null document. Running CI's file order locally fails 9 tests on main @ 4e27d35 and on fff2af0, so it's not caused by this PR. The first run never got past the audit, so it was hidden there. Fixed (eb74b2c): load the DOM harness and import Vue dynamically after it, as vue-component.test.mjs already does. CI's order and 3 random orders now pass (413/413).

Branch protection: the PR is BLOCKED by the required status checks rule on main. The required checks are "Lint, typecheck, test, build", "Chrome runtime suite", "Node 20/22/24 consumer build" and "Analyze JavaScript/TypeScript", with strict "up to date" enabled. The branch is 0 commits behind main. Required reviews = 0, there are no unresolved conversations, and linear history is required (squash or rebase merge).

Findings

  1. Fixed (fc3d84c), minor regression: SoundEngine.loop() with a name outside the 5 built-in loops now threw TypeError (LOOP_VOICES[name] is undefined). On main the if-chain gave any unknown name the saw voice. engine.sound.loop is part of the public SoundApi handed to custom tools, so an untyped JS tool would have thrown inside the frame loop. It now falls back to the saw voice. Added a test, which fails on fff2af0 and passes now.
  2. Noted, not changed: isParked now matches only the exact -1000 sentinel. main treated x <= -100 as parked in the tools and x/y < -100 in pointer-input. While the pointer is captured and dragged more than 100px past the overlay's left or top edge, tools now keep firing (and dx/dy are no longer zeroed) where they used to stop. The PR declares this change, and I think it's more correct, but it is a behaviour change.
  3. Noted, not changed: the Vue toolbar's style injector now uses the shared helper, which skips injecting when another module copy's <style data-rage-layer="toolbar"> already exists. With two bundled copies of RageLayer, unmounting one copy's toolbar can remove the CSS from the other. React already behaved this way on main, but it is new for Vue. This only happens with duplicated module copies.

Toolbar session (React, Vue, element): I compared it line by line with main.

  • mountRageLayer({ initialTool: null }) behaves the same as the old new RageLayerEngine plus registerTool loop. setTool(null) on a fresh engine returns early, and an unknown initial-tool is still a no-op.
  • Engine options, toolStyle and history defaults are preserved, and the element no longer leaks tools/strings into the engine options.
  • Teardown removes the window keydown listener, destroys the model, disposes the engine and restores focus only if the previous element is still connected.
  • In React, focus is now captured when the session opens rather than in the ready effect. Both points come before the toolbar takes focus, so the same element is captured.
  • In a headless-Chrome run of the built <rage-layer>, I did 3 mount/unmount cycles. Each one: focus moved into the bar, shortcut 2 selected the gun, removal disposed the engine, rageLayerEngine became null, focus returned to the trigger, no overlay was left over, and Escape after removal did nothing. The result is identical to the same run against main's build.

Exports and bundle: runtime export names are identical to main for all 12 entries, and importing on Node 20, 22 and 24 works (SSR-safe). Static-import closure, gzip: ./toolbar 13.7 KB → 5.6 KB; . and ./svelte slightly smaller; ./react +0.2 KB, ./vue +0.2 KB, ./element +0.8 KB and ./lazy +0.2 KB (shared session/tool-order chunks). All are within the check:package budgets.

Local verification (head eb74b2c)

  • bun install --frozen-lockfile, audit, lint, typecheck, check:artifacts, test:coverage (413 pass, also in CI's file order and 3 random orders), build, typecheck:consumers, check:package, docs:build: all pass.
  • Node 20/22/24 consumer job: consumer typecheck, check:package with npm_config_ignore_scripts and the Node import smoke test pass on Node 20.20, 22.23 and 24.22.
  • Example builds: pack, bun install --frozen-lockfile, install the tarball, cmp dist and build all pass for nextjs, nuxt and sveltekit.
  • Chrome job: benchmark smoke (idle,mixed --assert) and the memory leak gate (--leak-cycles 20) pass with chrome-headless-shell 157. scripts/browser-test.mjs itself still hangs at "demo page to finish booting" on this macOS host, on main as well. CDP Runtime.evaluate never returns while the harness polls during boot, so it's a harness/host problem, not this PR. CI is the source of truth for that check.

Needs manual testing

  • React and Vue toolbars in a real app: open, shortcuts, arrow/Home/End roving, close, and check focus returns. React StrictMode double-mount too.
  • Snapshot export on a 2x display with effects visible (fxDpr fix).
  • Gravity gun hum while held (singularity fix).
  • Decal crispness after a resize on a page taller than 4500px.
  • Docs site dev server, since vitepress now runs on vite 6.4.5 / vue 3.5.44 (docs:build passes).

🤖 Generated with Claude Code

ParthJadhav and others added 2 commits November 7, 2026 18:44
Bun shares one module cache across test files, and @vue/runtime-dom caches
`document` when it first evaluates. adapters.test.mjs imported vue statically
with no DOM, so whenever CI's file order ran it first, every later Vue
component test failed with "null is not an object (doc.createComment)".
The same order fails on main. Register the DOM harness and import Vue
dynamically after it, as vue-component.test.mjs already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chrome 157 stable supports the live capture API, so auto mode reports
"live" instead of "snapshot" in CI. Fails the same way on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ParthJadhav
ParthJadhav merged commit 44dc3cb into main Nov 7, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant