Skip to content

feat(editor): advisory drum limb-lint (physically-impossible hits) - #129

Merged
byrongamatos merged 4 commits into
mainfrom
feat/editor-drum-limb-lint
Jul 8, 2026
Merged

feat(editor): advisory drum limb-lint (physically-impossible hits)#129
byrongamatos merged 4 commits into
mainfrom
feat/editor-drum-limb-lint

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

DAW roadmap 4.7 (§F5.4 — the drum sibling of the fretted playability-lint posture). Editor-only.

What

The drum grid now flags hits a two-hand, two-foot drummer physically can't play — with a small amber warning triangle on the offending hits and an advisory line in the editor HUD. It never blocks, never auto-fixes, never mutates a hit.

Two rules, per near-simultaneous cluster:

  • hands — 3+ stick-struck pieces at one instant. A drummer has two hands, so three simultaneous non-foot pieces needs three hands. Feet (kick, hh_pedal) don't count toward the limit — two hands + two feet is the playable ceiling.
  • hihat — a contradictory hi-hat state: hh_open together with hh_closed (can't be both) or with hh_pedal (the foot can't be up-for-open and down-on-the-pedal at once). hh_closed + hh_pedal is fine.

Clustering is tight (~12 ms, bounded from the cluster's start), so a fast roll or a flam pair (~30 ms) never merges into a false positive.

Implementation

  • @pure:drum-limb-lint: _drumLimbConflictsPure(hits, epsilon) → one entry per conflicted cluster {time, indices, pieces, reasons}; _drumConflictIndexSetPure flattens to the marked indices. Read-only; skips NaN-time hits.
  • _drumEditorDraw: computes the conflict set once per draw over the already-sorted hits (single O(n) pass, drum-editor mode only — never touches the guitar/keys path), marks conflicted hits, and adds the amber HUD line.

Tests — tests/drum_limb_lint.test.js (15, all fail on main)

Both rules; feet-don't-count; duplicate-piece dedupe (2 snares = one hand piece); epsilon clustering + the no-runaway-chaining bound; a fast roll staying clear; adversarial (non-array, NaN times); the read-only guarantee; and the index flattener. Full JS suite green.

Scope / fresh region

New pure block after @pure:drum-density, plus the marker + HUD line in _drumEditorDraw. No core, no spec, no overlap with the open PRs (#112/#125/#126/#127/#128).

Summary by CodeRabbit

  • New Features
    • Added an advisory drum playability lint in the editor to flag physically impossible near-simultaneous hit combinations and contradictory hi-hat states.
    • Offending hits now show a small amber warning triangle, and the editor HUD adds an amber line with the total number of flagged spots.
    • The lint runs non-blockingly, and temporarily pauses during live drum movement to prevent transient false warnings.
  • Tests
    • Added unit tests for timing clustering, hi-hat/hand-piece rule logic, input safety, and memoized draw-loop integration behavior.

DAW roadmap 4.7 (F5.4 — the drum sibling of the fretted playability-lint
posture). Flags hits that can't be played by a two-hand, two-foot drummer,
without ever blocking or auto-fixing.

- @pure:drum-limb-lint: `_drumLimbConflictsPure(hits, epsilon)` groups hits
  into near-simultaneous clusters (within ~12 ms, bounded from the cluster
  start so a fast roll never chains) and flags two conditions:
    * 'hands' — 3+ stick-struck pieces at one instant (feet = kick + hh_pedal
      never count; two hands + two feet is the playable ceiling).
    * 'hihat' — hh_open with hh_closed, or hh_open with hh_pedal (the foot
      can't be up-for-open and down-on-the-pedal at once).
  `_drumConflictIndexSetPure` flattens the conflicts to the hit indices the
  renderer marks. Read-only — never mutates a hit; skips NaN-time hits.
- _drumEditorDraw: computes the conflict set once per draw over the already-
  sorted hits (single O(n) pass, drum-editor mode only), draws a small amber
  warning triangle above each conflicted hit, and adds an amber advisory line
  to the HUD ("N playability hints … nothing is blocked").

Advisory only, matching the fretted-lint / drum-lint posture in the DAW doc:
no gate, no auto-move. Tests: tests/drum_limb_lint.test.js (15) — both rules,
feet-don't-count, duplicate-piece dedupe, epsilon clustering + no-runaway-chain,
adversarial (non-array / NaN times), read-only guarantee, index flattening.
All fail on main. Full JS suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HzMBtxWnLGHYkMMXtK38Bg
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@byrongamatos, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 31 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f8bc0dd-a0d6-4db5-9de6-5af1ae7a817c

📥 Commits

Reviewing files that changed from the base of the PR and between e5da08e and ed44c31.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • screen.js
📝 Walkthrough

Walkthrough

Adds an advisory-only drum playability lint that detects physically impossible near-simultaneous drum hits, renders amber warnings and a HUD count in the editor, and documents and tests the behavior.

Changes

Drum playability lint

Layer / File(s) Summary
Pure lint helpers and constants
screen.js
Defines foot-piece constants, clustering epsilon, and pure functions that cluster near-simultaneous hits and detect hand-count and hi-hat contradictions, plus a helper to flatten conflicts into an index set.
Drum editor rendering and HUD integration
screen.js
Computes conflict data during drum rendering, builds a conflicted-index set, draws amber warning triangles above flagged hits, and adds an amber HUD line reporting the hint count.
Tests and changelog for lint behavior
tests/drum_limb_lint.test.js, CHANGELOG.md
Adds tests covering hand-count rules, hi-hat contradictions, clustering and epsilon boundaries, malformed-input robustness, non-mutation, index-set flattening, and the memoized wrapper; documents the feature in the changelog.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DrumEditorDraw
  participant DrumLimbConflicts as _drumLimbConflicts
  participant ConflictIndexSet as _drumConflictIndexSetPure
  participant HUD

  DrumEditorDraw->>DrumLimbConflicts: analyze current hits
  DrumLimbConflicts-->>DrumEditorDraw: conflict records
  DrumEditorDraw->>ConflictIndexSet: flatten conflict records
  ConflictIndexSet-->>DrumEditorDraw: conflicted hit indices
  DrumEditorDraw->>DrumEditorDraw: draw amber warning triangle per conflicted hit
  DrumEditorDraw->>HUD: report playability hint count
  HUD-->>DrumEditorDraw: render amber advisory line
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: an editor-only advisory drum limb-lint for physically impossible hits.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/editor-drum-limb-lint

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
screen.js (1)

14705-14712: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate flattening logic vs. _drumConflictIndexSetPure.

This block re-implements the index-flattening already provided by _drumConflictIndexSetPure (Line 14343-14349), just inline to avoid a second _drumLimbConflictsPure pass. Consider adding a small overload/parameter to _drumConflictIndexSetPure that accepts a precomputed conflicts array, so both call sites share one implementation.

♻️ Optional refactor
-function _drumConflictIndexSetPure(hits, epsilon) {
+function _drumConflictIndexSetPure(hits, epsilon, conflicts) {
     const set = new Set();
-    for (const c of _drumLimbConflictsPure(hits, epsilon)) {
+    for (const c of (conflicts || _drumLimbConflictsPure(hits, epsilon))) {
         for (const idx of c.indices) set.add(idx);
     }
     return set;
 }
     const _lintConflicts = _drumLimbConflictsPure(hits, DRUM_LIMB_EPSILON);
-    const _conflictIdx = new Set();
-    for (const c of _lintConflicts) for (const idx of c.indices) _conflictIdx.add(idx);
+    const _conflictIdx = _drumConflictIndexSetPure(hits, DRUM_LIMB_EPSILON, _lintConflicts);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@screen.js` around lines 14705 - 14712, The playability lint block is
duplicating the same conflict-index flattening logic already centralized in
_drumConflictIndexSetPure. Update _drumConflictIndexSetPure to accept an
optional precomputed conflicts array, and reuse it here instead of rebuilding
_conflictIdx inline after calling _drumLimbConflictsPure. Keep the existing
behavior in the draw path, but make both call sites share the same index-set
implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@screen.js`:
- Around line 14705-14712: The playability lint block is duplicating the same
conflict-index flattening logic already centralized in
_drumConflictIndexSetPure. Update _drumConflictIndexSetPure to accept an
optional precomputed conflicts array, and reuse it here instead of rebuilding
_conflictIdx inline after calling _drumLimbConflictsPure. Keep the existing
behavior in the draw path, but make both call sites share the same index-set
implementation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d79476cb-e3ee-4675-b271-4f656e8021e7

📥 Commits

Reviewing files that changed from the base of the PR and between 8eae01b and 0b502f7.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • screen.js
  • tests/drum_limb_lint.test.js

Memoize the advisory limb-lint behind the shared edit-generation counter
(_coverageEditGen) plus hits array identity/length so the O(n) cluster pass
no longer runs on every repaint, and pause it during a live drum-move drag —
mid-drag hit times are mutated in place and only re-sorted on drop, so the
sorted-input clusterer could flash spurious/missed markers on a transiently
unsorted array. Adds stateful-wrapper regression tests (drag bypass + memo
invalidation + draw-path source locks) that fail on pre-fix code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@byrongamatos

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (3)
tests/drum_limb_lint.test.js (3)

244-253: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Function-body extraction relies on top-level \nfunction boundary.

indexOf('\nfunction ', fnStart + 1) only terminates the body at another unindented function declaration. If _drumEditorDraw ever grows an inner helper declared with function starting at column 0 within its body (unlikely but possible with certain formatting), the extracted body would be truncated before or including unrelated code, silently weakening this source-lock assertion. Low risk given current formatting conventions, but worth a brace-matching approach (as used in body() in tests/key_highlight_hoist.test.js) for robustness if this pattern is reused frequently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/drum_limb_lint.test.js` around lines 244 - 253, The body extraction for
_drumEditorDraw is too fragile because it relies on finding the next top-level
“function” token instead of matching the function’s braces. Update the test in
drum_limb_lint.test.js to use a brace-aware body extraction approach like the
helper used in key_highlight_hoist.test.js, so the assertion still targets
_drumEditorDraw even if formatting or nested helpers change.

218-240: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant setGen(1) call.

At line 238, W.setGen(1) isn't needed to force recomputation: sorted is a distinct array reference from unsorted, so the hitsRef !== hits check alone already invalidates the memo per the wrapper's key logic (Context snippet 2). Not incorrect, just slightly muddies the test's intent by implying the gen bump is what triggers the recompute.

Simplify by dropping the redundant gen bump
     // Drag done → lint resumes (and this array, once dropped, is re-sorted).
     W.setDrag(null);
-    W.setGen(1);
     assert.deepStrictEqual(W.fn(sorted), [], 'after drop, sorted hits lint clean');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/drum_limb_lint.test.js` around lines 218 - 240, Remove the redundant
gen bump in the drum-move pause test so the intent stays focused on array
identity invalidation. In the `LIVE drum-move drag pauses the lint` test, drop
the `W.setGen(1)` call and keep the assertion that `W.fn(sorted)` recomputes
cleanly via the `hitsRef !== hits` path in the wrapper logic. This should leave
the `buildWrapper` / `W.fn` behavior under test without implying the generation
counter is required for the refresh.

255-258: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Loose proximity regex for the memo-key source lock.

/_coverageEditGen[\s\S]*?_drumLintCache|_drumLintCache[\s\S]*?_coverageEditGen/ only confirms both identifiers appear somewhere in src near each other — it doesn't confirm _coverageEditGen actually participates in cache-key construction inside _drumLimbConflicts. This is a secondary sanity check though; the behavioral test at Line 208-216 already exercises the real invalidation contract, so the risk of this being misleading is low.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/drum_limb_lint.test.js` around lines 255 - 258, The memo-key assertion
in the drum lint test is too loose because it only checks that _coverageEditGen
and _drumLintCache appear near each other in src. Tighten the check in
tests/drum_limb_lint.test.js so it verifies _coverageEditGen is actually part of
the cache-key construction used by _drumLimbConflicts, rather than just nearby
text, while keeping the existing invalidation behavior covered by the behavioral
test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/drum_limb_lint.test.js`:
- Around line 244-253: The body extraction for _drumEditorDraw is too fragile
because it relies on finding the next top-level “function” token instead of
matching the function’s braces. Update the test in drum_limb_lint.test.js to use
a brace-aware body extraction approach like the helper used in
key_highlight_hoist.test.js, so the assertion still targets _drumEditorDraw even
if formatting or nested helpers change.
- Around line 218-240: Remove the redundant gen bump in the drum-move pause test
so the intent stays focused on array identity invalidation. In the `LIVE
drum-move drag pauses the lint` test, drop the `W.setGen(1)` call and keep the
assertion that `W.fn(sorted)` recomputes cleanly via the `hitsRef !== hits` path
in the wrapper logic. This should leave the `buildWrapper` / `W.fn` behavior
under test without implying the generation counter is required for the refresh.
- Around line 255-258: The memo-key assertion in the drum lint test is too loose
because it only checks that _coverageEditGen and _drumLintCache appear near each
other in src. Tighten the check in tests/drum_limb_lint.test.js so it verifies
_coverageEditGen is actually part of the cache-key construction used by
_drumLimbConflicts, rather than just nearby text, while keeping the existing
invalidation behavior covered by the behavioral test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7852bd28-1b57-41d3-a2f5-a39127f27862

📥 Commits

Reviewing files that changed from the base of the PR and between 0b502f7 and 0d166da.

📒 Files selected for processing (2)
  • screen.js
  • tests/drum_limb_lint.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • screen.js

Brace-match the _drumEditorDraw body extraction (robust to inner function
declarations), assert _coverageEditGen participates in the memo key inside
_drumLimbConflicts rather than mere proximity, and drop a redundant edit-gen
bump in the drum-move pause test (the fresh array reference already
invalidates via hitsRef).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@byrongamatos

Copy link
Copy Markdown
Collaborator

Addressed all three nitpicks in e5da08e (test-only): brace-matched the _drumEditorDraw body extraction, tightened the memo-key source lock to verify _coverageEditGen is used in the cache key inside _drumLimbConflicts, and dropped the redundant setGen(1) in the drum-move pause test. Full JS suite green (61/61).

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

@byrongamatos Thanks for the update — sounds good. I'll re-review the changes in commit e5da08e now, focusing on the brace-matching fix for _drumEditorDraw extraction, the tightened memo-key source lock, and the cleanup in the drum-move pause test.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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