Skip to content

feat(editor): tempo-zone confirm bar + bounded per-zone refine (P2-3 completion) - #259

Merged
byrongamatos merged 3 commits into
mainfrom
feat/editor-segment-confirm
Jul 14, 2026
Merged

byrongamatos merged 3 commits into
mainfrom
feat/editor-segment-confirm

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What

The remaining half of segment-first tempo mapping (#252/#253 shipped the engine + rough Apply; their follow-up notes both point here):

  • Scan now opens a confirm surface, not just a status line. Tempo/Grid ▸ Scan for tempo zones… enters Tempo Map, paints the detected zones as colored bands (teal steady / violet ramp / grey unmapped, BPM labels, "?" on low-confidence zones) on the timeline, and docks a confirm bar.
  • Pre-commit adjust verbs, all pure (src/tempo-segment.js) and clamped: drag a boundary handle (both neighbours keep a minimum span; stale phase seeds drop for re-seed), Split at the playhead (a ramp interpolates its tempo at the cut), Merge → (the join is described by its own endpoints — a real rit never flattens into a lie), cycle Kind (steady/ramp/unmapped), type BPM (degenerate values refused — a 0 BPM would hang the seeder). Proposal state is module-scoped, editGen- AND session-keyed, Esc-cleared, dismissed on mode exit, mutually exclusive with Suggest ghosts.
  • Confirm & refine: seeds the grid from the adjusted zones (kick-band phase seed per zone), then runs _suggestFitPure bounded inside each zone (opts.toIdx) with the new opts.stretchClamp holding the drift tracker to ±12% of the zone tempo — the segment BPM becomes a real prior, the structural cure for the runaway march. Lands as ONE undoable TempoGridCmd (notes keep their seconds and ride).
  • Single tempo instead: duration-weighted-median constant grid, no refine (refining would un-uniform what the button promises).
  • New module src/tempo-zones.js (state + chrome; apply verbs stay in tempo.js and arrive as init hooks, so no import cycle). editorApplyTempoZones (the feat(editor): apply a segment-first rough map (P2-3 Apply) #253 menu row) is unchanged as the quick path.

Tests

tests/tempo_zones_confirm.test.mjs (9): every adjust verb pinned (clamps, ramp interpolation at a split, honest-endpoint merge, kind round-trip, BPM validation, weighted-median collapse); stretchClamp pinned on a miss-path march (a locked bar protects the miss from the trailing drop, so the clamped vs unclamped predictions are directly observable: 4.14 vs 4.20); bounded refine pinned on a two-zone fixture (anchors hold, each zone snaps to its own pulse, unmapped refines nothing). Behavioral cases fail on main. screen_markup.test.mjs extended to pin the new bar's nesting.

Full suite green, lint 0 errors (3 pre-existing ratchet warnings), routes.py untouched.

Live-verified

On a tester's real project (Insomnium — Where the Last Wave Broke, 584 beats / 1114 notes, stems-only audio): Scan → 5 zones detected + flagged LOW CONFIDENCE (double-kick metal — exactly the honesty the "?" marks are for), bands + bar render, split→merge round-trips, Confirm & refine: "Built the grid from 5 tempo zones and snapped 76 barlines to the recording" as exactly one undo step, Ctrl+Z restores the old grid bit-exact, zero page errors.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q

Summary by CodeRabbit

  • New Features
    • Added an interactive tempo-zone workflow with a confirm overlay, draggable boundary adjustments, split/merge, zone-kind selection, and BPM editing.
    • Added Confirm & refine to generate a reviewed tempo grid with per-zone barline fitting via a single undoable step.
    • Added “single tempo” mode to replace detected zones with one steady tempo, with clear confirm/dismiss controls (including Escape-to-dismiss).
  • Improvements
    • Updated Scan for tempo zones to a full enter-and-adjust workflow in Tempo Map, improving guidance and preserving note timing.
    • Improved tempo fitting stability by clamping stretch drift during snapping.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4cd7b4b5-4115-47be-836a-7fef7a9e6f76

📥 Commits

Reviewing files that changed from the base of the PR and between 435730b and b4c4dc0.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • screen.html
  • src/input.js
  • src/main.js
  • src/menu-bar.js
  • src/mouse.js
  • src/tempo-segment.js
  • src/tempo-suggest.js
  • src/tempo-zones.js
  • src/tempo.js
  • tests/screen_markup.test.mjs
  • tests/tempo_zones_confirm.test.mjs

📝 Walkthrough

Walkthrough

Adds an interactive tempo-zone proposal workflow with boundary and tempo editing, bounded per-zone grid refinement, a single-tempo fallback, confirm-bar controls, and Tempo Map integration. Tests cover segment operations, refinement behavior, overlay structure, and stretch clamping.

Changes

Tempo-zone confirmation workflow

Layer / File(s) Summary
Segment editing and bounded refinement
src/tempo-segment.js, src/tempo-suggest.js, tests/tempo_zones_confirm.test.mjs
Adds pure helpers for seeding beats, editing zones, collapsing zones to one tempo, clamping stretch, and refining barlines within each zone, with deterministic coverage for these operations.
Interactive zone proposal overlay
screen.html, src/tempo-zones.js, tests/screen_markup.test.mjs
Adds the tempo-zone overlay, zone-band rendering, selection and boundary hit testing, split/merge/kind/BPM controls, confirm-bar wiring, and guarded Escape dismissal.
Tempo Map integration and commit flow
src/tempo.js, src/main.js, src/input.js, src/mouse.js, src/menu-bar.js, CHANGELOG.md
Changes scanning to show editable proposals, wires confirmation and single-tempo commits, routes boundary drags, dismisses competing proposal surfaces, updates the menu label, and documents the workflow.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Editor as Tempo Map editor
  participant Zones as tempo-zones.js
  participant Tempo as tempo.js
  participant Grid as TempoGridCmd
  Editor->>Zones: adjust tempo-zone proposal
  Zones->>Tempo: confirm refined zones
  Tempo->>Grid: commit one refined grid
  Grid-->>Editor: redraw canvas and status
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is specific and matches the main change: a tempo-zone confirm bar with bounded per-zone refinement in the editor.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/editor-segment-confirm
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/editor-segment-confirm

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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/tempo-segment.js (1)

408-429: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Merging an unmapped zone with a mapped one silently produces a mapped result.

_segmentMergePure only keeps kind: 'unmapped' when both neighbours are unmapped (Line 419-420); merging an unmapped zone with a mapped one always yields 'ramp' or 'constant', using the unmapped side's bpmStart/bpmEnd in the endpoint math. Since an unmapped zone is user intent for "no barlines here," a merge silently overrides that intent and seeds a grid over what was explicitly marked unmapped. This is called out as deliberate in the comment above (Line 408-411), but the behavior isn't covered by tests/tempo_zones_confirm.test.mjs's merge test (which only merges two mapped zones).

Worth confirming this is the desired UX (vs., e.g., keeping 'unmapped' when either side is unmapped, or requiring an explicit re-map first), and adding a test case for the mixed unmapped/mapped merge either way.

🤖 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 `@src/tempo-segment.js` around lines 408 - 429, Update _segmentMergePure so a
merge involving either an unmapped or mapped segment preserves the unmapped
intent according to the desired UX; do not derive a mapped ramp or constant from
an unmapped endpoint. Add a test covering both mixed merge orders in the
existing tempo-zone merge tests, and ensure the two-unmapped behavior remains
unchanged.
src/mouse.js (1)

351-357: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a hover-cursor cue for the zone-boundary handle.

segment-boundary gets full drag support here, but the unchanged tempo-map hover-cursor block (around Line 290-295) only checks _tempoSyncAtX for the ew-resize cursor — hovering a zone-boundary handle gives no visual affordance, unlike sync-point poles.

♻️ Proposed addition to the tempo-map hover branch (outside this hunk, ~line 290-295)
         if (S.tempoMapMode) {
+            if (_zonesStripHit(y)) {
+                if (canvas) canvas.style.cursor = _zonesBoundaryHitAt(x) >= 0 ? 'ew-resize' : 'pointer';
+                return;
+            }
             const hit = _tempoSyncAtX(x, y);
             if (hit !== S.tempoHover) { S.tempoHover = hit; host.draw(); }
             if (canvas) canvas.style.cursor = hit >= 0 ? 'ew-resize' : '';
             return;
         }
🤖 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 `@src/mouse.js` around lines 351 - 357, Update the tempo-map hover-cursor logic
near the existing _tempoSyncAtX check to also detect segment-boundary handles
and apply the ew-resize cursor. Keep the current tempo-sync hover behavior
unchanged and ensure the cue is shown before drag state begins.
🤖 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.

Inline comments:
In `@src/tempo.js`:
- Around line 2468-2479: Update _tempoMapOnDragMove so segment-boundary
interactions use the same ≥3px movement threshold as tempo-sync before setting
dg.moved or calling _zonesDragBoundary. Preserve the existing no-op behavior for
sub-threshold pointer jitter and only reshape the proposal after the drag
threshold is exceeded.

---

Nitpick comments:
In `@src/mouse.js`:
- Around line 351-357: Update the tempo-map hover-cursor logic near the existing
_tempoSyncAtX check to also detect segment-boundary handles and apply the
ew-resize cursor. Keep the current tempo-sync hover behavior unchanged and
ensure the cue is shown before drag state begins.

In `@src/tempo-segment.js`:
- Around line 408-429: Update _segmentMergePure so a merge involving either an
unmapped or mapped segment preserves the unmapped intent according to the
desired UX; do not derive a mapped ramp or constant from an unmapped endpoint.
Add a test covering both mixed merge orders in the existing tempo-zone merge
tests, and ensure the two-unmapped behavior remains unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3037762a-3cf2-4b37-b77c-ac7fa37779a9

📥 Commits

Reviewing files that changed from the base of the PR and between a866705 and 435730b.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • screen.html
  • src/input.js
  • src/main.js
  • src/menu-bar.js
  • src/mouse.js
  • src/tempo-segment.js
  • src/tempo-suggest.js
  • src/tempo-zones.js
  • src/tempo.js
  • tests/screen_markup.test.mjs
  • tests/tempo_zones_confirm.test.mjs

Comment thread src/tempo.js
@byrongamatos

Copy link
Copy Markdown
Collaborator

Applied — the 3px click-vs-drag threshold now guards segment-boundary too, so all three tempo drag kinds share it. Agreed it's worth more than 'minor': the handle is ±6px and the reshape is pre-commit, so a jitter-nudge on an intended click moved the boundary with no undo to recover it (Ctrl+Z only reaches committed grids). Fixed in 46f5d73.

Also landed from my own pass (da07925):

  • _segmentBoundaryDragPure clamped into a possibly-empty window — when the zone pair is shorter than 2×minLen the Math.max wins and hands back a right zone below minLen, breaking the one invariant the contract states. The old guard only checked containment, which an empty-window result still satisfies. Now refused, with a test.
  • editorZonesSingleTempo blamed the zones for what was actually a missing onset analysis.
  • _segmentRefineGridPure's doc claimed refined counts onset-corroborated downbeats; it counts every non-locked proposal (an interior held-note bar marches on prediction and still counts).

@byrongamatos

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 14, 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.

ChrisBeWithYou and others added 3 commits July 14, 2026 21:03
P2-3 completion: Scan paints adjustable zone bands (drag boundary /
split / merge / kind / BPM, all pre-commit, Esc-cleared) with a docked
confirm bar; Confirm & refine seeds the grid then runs _suggestFitPure
bounded per zone with the new opts.stretchClamp as the segment-tempo
prior — ONE TempoGridCmd. Single-tempo escape hatch included.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
Review findings on the tempo-zone confirm bar:

1. _segmentBoundaryDragPure clamped into a possibly-EMPTY window. The
   feasible join is [a.tStart + minLen, b.tEnd - minLen]; when the PAIR
   is shorter than 2x minLen that range is empty, the Math.max wins, and
   the function hands back a right segment BELOW minLen — silently
   breaking the one invariant its contract states ("both neighbours keep
   at least minLen"). The old guard only checked containment in the pair,
   which an empty-window result still satisfies. Refuse instead.

   Not reachable from Scan today (the detector floors zones at
   minSegSec = 6, and split/merge both preserve >= minLen), but this is
   an exported pure and the guard is one line.

2. editorZonesSingleTempo blamed the zones ("No mapped zone to take a
   tempo from") for what is actually a missing onset analysis. Diagnose
   the two apart, matching editorConfirmTempoZones.

3. _segmentRefineGridPure's doc claimed `refined` counts onset-corroborated
   downbeats. It counts every non-locked proposal — an interior held-note
   bar marches on prediction and still counts. Say what it counts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CodeRabbit: segment-boundary reshaped the proposal on the FIRST mousemove
while tempo-sync and tempo-group both require >=3px first. Valid, and it
bites harder than it looks: the handle is narrow (+/-6px) and the reshape
is pre-commit, so pointer jitter on an intended click silently moved the
boundary with no undo to recover it — Ctrl+Z only reaches committed grids.

All three drag kinds now share the threshold.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@byrongamatos
byrongamatos force-pushed the feat/editor-segment-confirm branch from 46f5d73 to b4c4dc0 Compare July 14, 2026 19:03
@byrongamatos
byrongamatos merged commit 4547f19 into main Jul 14, 2026
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