Skip to content

Stage 0: extract pure DSP (tested) + split dashboard.js by feature#1

Merged
astrobyte-dev merged 4 commits into
mainfrom
stage-0-foundation
May 25, 2026
Merged

Stage 0: extract pure DSP (tested) + split dashboard.js by feature#1
astrobyte-dev merged 4 commits into
mainfrom
stage-0-foundation

Conversation

@astrobyte-dev

@astrobyte-dev astrobyte-dev commented May 25, 2026

Copy link
Copy Markdown
Owner

Stage 0: foundation — pure DSP extracted + tested, dashboard.js split by feature, selftest verifies audio

Closes all three Stage 0 gaps. Two commits:

  • d16633a — GAP 1 (extract + test pure DSP) and GAP 2 (split dashboard.js)
  • a79a2e5 — GAP 3 (selftest asserts master-meter activity)

What changed

  • GAP 1: stabilize / resample / audioPhase extracted into a single-source dashboard-dsp.js (window.AbxDsp, globalThis.AbxDsp under Node). One body — the browser and the new src/dsp.test.ts import the same file, so there is no TS copy to drift; dashboard-dsp.d.ts is the type contract. audioPhase purified to (clk, nowMs). 12 new tests; suite is 19/19.
  • GAP 2: the 672-line dashboard.js split into a core shell + five feature modules: AbxScope, AbxCheats, AbxCurves, AbxSeq, and a shared Abx core surface (state/send/cmd/poll/color/esc/fnum/clock.phase). dashboard.ts serves any /dashboard-*.js. Core 672 → 334; every feature module < 200.
  • GAP 3: selftest.ts now binds a Meter on the master-meter UDP port, samples L/R across the 8s phase-B beat, and asserts peak(L+R) > 0.05; on failure it dumps sclang + tidal tails and exits non-zero. Local-only (npm run selftest), never CI.

Standing review

1. Karpathy / process. Design alternatives were surfaced before code (the namespace gate presented both the literal four-file split and the core+features cut with concrete line budgets; you chose the latter). Each of the six steps was a surgical, behaviour-preserving move, verified before the next.

2. Tests. 12 new cases for the pure DSP, behaviour-focused (sine locks to ~2 cycles vs kick/noise return raw; resample endpoints+interp+degenerate; audioPhase null/fractional/advance/lead-compensation). npm test ran green before and after every step. Single source means no JS/TS drift. GAP 3 adds the live audio gate (local-only) so a silently dead chain fails loudly.

3. Architecture. Core is 334 lines — the irreducible shell (render loop, the four dispatchers, master meters, transport, console). Intentionally over the 200 soft cap, as agreed; chasing sub-200 would mean shredding it into ~8 modules, a separate effort. Feature modules: dsp 28, scope 47, cheats 149, curves 91, seq 134. No duplication.

4. Security. No new user-controllable strings reach SC/Tidal. The new static-file route is regex-restricted (/^\/(dashboard(?:-[a-z0-9-]+)?\.js)/i — lowercase alnum + dash, ends .js, no slashes/dots), so it cannot path-traverse. Host/Origin guards untouched. No new persisted state.

5. Concurrency / lifecycle. No new long-lived timers: the existing 80ms debounce and rAF loops are reused; AbxSeq owns its row/swing queue, drained via flushPending() from the same interval. Each feature self-registers its own listeners. The core render loop guards window.AbxSeq/AbxCurves so it can start before the later scripts parse and survives a module failing to load. selftest binds + stop()s its own meter socket (unref'd).

6. Performance. Render-loop work is delegated, not added; guards are cheap truthy checks. No new poll endpoints or SSE streams.

7. Comments. Hard-won fixes carry the WHY: the load-race guard rationale, the noise-seed choice (a low-bit LCG faked a tone — mulberry32 instead), the DOMContentLoaded deferral, the clock.phase purity note, and the selftest threshold rationale (why 0.05).

8. UX regressions. Dispatch branches are mutually exclusive (distinct data-attrs), so reordering them into delegates changes nothing. Listener-order note: the curve-paint vs seq-paint pointerdown registration order is restored to the original (curves loads before seq), and the two act on mutually-exclusive targets (.curvecv vs .pad), so order is immaterial regardless. No keyboard shortcut or debounce threshold changed.

9. README / docs. No user-facing features added (pure refactor + a test), so the feature list, env-var table, and MCP tool list are unchanged.

10. Next-stage readiness. Stage 1 (mixer) can reuse AbxScope.draw (exposed) for mini-scopes, Abx.color for per-channel colour, Abx.state().scopes for meters, and the AbxX.handleClick delegate pattern. Harder than expected: the inter-script load race affected the core loop's calls into later-loaded namespaces, not just the deep-link — now guarded. No known debt carried into Stage 1.


Manual test checklist

Verified headlessly (Edge + puppeteer-core, 3 consecutive runs, zero console errors):

  • Dashboard loads with no console/page errors
  • Step grid / cheats / curves / samples panels open
  • #steps and #cheats deep-links open via the new DOMContentLoaded bootstrap

Needs your ears + hands (audio + drag gestures I can't drive headlessly):

  • Boot, play a beat → per-card scopes render
  • Open step grid, paint a row → hear it; switch A→B→C; toggle song mode
  • Open curves, paint a curve, toggle ON → hear modulation, OFF → stops
  • Drag a sample onto a sequencer row → sound changes; drag a grip → row reorders
  • Scroll a lit pad → velocity (brightness) changes; right-click → erase
  • hush / record / reset behave as before
  • npm run selftest (standalone — kills any live engine, makes ~8s of audio) prints DONE — audio verified.

…modules

GAP 1 (extract + test pure DSP):
- New single-source dashboard-dsp.js (window.AbxDsp / globalThis) holding stabilize,
  resample, and a purified audioPhase(clk, nowMs). One body, consumed by the browser
  and by a new src/dsp.test.ts (no TS port, so no copy to drift); dashboard-dsp.d.ts
  gives the type contract.
- 12 new cases: stabilize silence->null / sine->locks ~2 cycles / kick->raw / noise->raw;
  resample length+endpoints+interp+degenerate; audioPhase null/fractional/advance/lead.
- Test runner widened to dist/*.test.js. 19 tests pass.

GAP 2 (split the 672-line dashboard.js by feature):
- Core shell + five modules behind namespaces: AbxScope (oscilloscope), AbxCheats
  (cheat sheet + sample browser), AbxCurves (modulation curves), AbxSeq (step grid +
  patterns + song), plus a shared Abx core surface (state/send/cmd/poll/color/esc/fnum/
  clock.phase). clock.phase is a one-line wrapper over AbxDsp so the math stays testable.
- dashboard.ts now serves any /dashboard-*.js. Core 672 -> 334 lines; every module <200.

Behaviour-preserving:
- One click/input/change dispatcher delegating to AbxX.handleClick/handleInput; branches
  stay mutually exclusive (distinct data-attrs), so dispatch order is unchanged.
- Deep-link bootstrap deferred to DOMContentLoaded (calls now cross files).
- Core render loop guards window.AbxSeq/AbxCurves against the inter-script load race.
- Verified via a headless smoke test (panels + #steps/#cheats deep-links, 0 console errors).
Copilot AI review requested due to automatic review settings May 25, 2026 11:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is a Stage 0 refactor that (1) extracts the dashboard’s DSP math into a single shared JS implementation with Node unit tests, and (2) splits the previously-monolithic dashboard UI script into a small core plus feature modules that consume a shared window.Abx surface. It also updates the dashboard HTTP server to serve any dashboard-*.js module from disk.

Changes:

  • Add dashboard-dsp.js as the single-source DSP implementation and introduce src/dsp.test.ts to unit-test it under Node.
  • Split dashboard.js into feature modules (dashboard-scope.js, dashboard-cheats.js, dashboard-curves.js, dashboard-seq.js) and load them from dashboard.html.
  • Update the dashboard server route to serve dashboard.js and dashboard-*.js, and broaden the test runner to execute all compiled *.test.js files.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
mcp/src/dsp.test.ts Adds Node unit tests that import the browser DSP script for side effects and validate stabilize/resample/audioPhase.
mcp/dashboard-dsp.js New single-source DSP implementation shared by browser and Node tests via globalThis.AbxDsp.
mcp/dashboard-dsp.d.ts Adds a type contract for the global AbxDsp surface.
mcp/src/dashboard.ts Expands static serving to include dashboard-*.js modules.
mcp/package.json Updates npm test to run all compiled test files.
mcp/dashboard.html Loads DSP + core dashboard + feature modules as separate scripts.
mcp/dashboard.js Shrinks core dashboard shell and delegates features to AbxScope/AbxCheats/AbxCurves/AbxSeq.
mcp/dashboard-scope.js Moves per-channel scope rendering and its rAF loop into a dedicated module.
mcp/dashboard-cheats.js Moves cheat sheet + sample browser into a dedicated module.
mcp/dashboard-curves.js Moves modulation curves UI and paint listeners into a dedicated module.
mcp/dashboard-seq.js Moves sequencer/pattern/song logic and grid input handlers into a dedicated module.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mcp/dashboard.js
Comment thread mcp/dashboard.js
Comment thread mcp/package.json Outdated
…l beat

selftest.ts binds a Meter on the master-meter UDP port and samples L/R across the 8s
phase-B beat, asserting peak(L+R) > 0.05 — well above the noise floor, ~10x below a normal
beat, so only a fully dead chain fails. On failure it dumps the sclang + tidal tails and
exits non-zero. peakL/peakR are tracked and reported separately to expose a one-channel /
mono-summing regression (diagnostic, not asserted). Stays a local-only gate (real SC +
WASAPI): run via `npm run selftest`, never in CI. Engine surfaces no meter, so selftest
binds Meter (meter.ts) directly on the UDP port (free since selftest runs standalone).
Closes a silent-green hole where `node --test dist/*.test.js` returns
exit 0 with zero tests run when the glob matches nothing (e.g. unbuilt
dist/). The new runner discovers tests via fs, fails loudly on
zero-match, and enforces a test-count floor so future regressions in
test discovery cannot pass silently.

Investigated after a Copilot review flagged shell glob expansion on
Windows cmd.exe. Node 21+ expands the glob itself so cmd was not the
active issue, but the zero-match silent-green was. Copilot's suggested
`node --test dist` directory form errors with 'Cannot find module' and
was rejected as broken.
The loop/flush/render paths (added in the Stage 0 split) guard
cross-namespace calls with `if(window.AbxX)`, with a comment claiming
resilience if a module fails to load. The four dispatcher functions
(click/input/change/keydown) and the deep-link bootstrap did not
carry the same guards, so a missing module would throw ReferenceError
on first relevant event and kill the entire dispatcher chain
(including downstream core branches).

Guards every cross-namespace dispatcher reference to match the loop
pattern. The resilience property the comment promised is now
delivered.

Caught by Copilot review on PR #1 (threads 1 & 2).
@astrobyte-dev
astrobyte-dev merged commit c52744e into main May 25, 2026
1 check passed
@astrobyte-dev
astrobyte-dev deleted the stage-0-foundation branch May 25, 2026 13:03
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.

2 participants