Skip to content

feat(editor): section completeness strip - #107

Merged
byrongamatos merged 3 commits into
mainfrom
feat/editor-section-coverage
Jul 7, 2026
Merged

feat(editor): section completeness strip#107
byrongamatos merged 3 commits into
mainfrom
feat/editor-section-coverage

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

A thin band at the top of the lane area tints each section by whether the active arrangement has notes in it — an at-a-glance "where is this chart still empty" while working through a long song. It pairs naturally with the now-undoable sections in #104.

Ambient and neutral by design (per the charrette's gamification seat: descriptive, not a score): a gentle blue where there's content, a faint wash where there isn't — no percentage, no red, nothing to click. Drawn under the existing section lines/labels, so there's no layout change and no new chrome.

_sectionCoveragePure computes each section's [start, nextStart) span (the last section runs to the song duration, or open-ended when duration is unknown) with a half-open boundary rule — a note exactly on a boundary belongs to the later section. Sections are sorted defensively and non-finite start_times dropped.

Verification — held to the testing habits

This is a display feature, so there's no undoable command to round-trip; the habits that bite are adversarial inputs and proving the helper uses all its arguments.

  • tests/section_coverage.test.js (8 cases) drive the real _sectionCoveragePure: per-span content detection, half-open boundaries (a note at t=4 lands in [4,8) not [0,4)), last-section-runs-to-duration, open-ended unknown-duration, sorted + dropped-NaN sections, a note before the first section counts for none, degenerate []/null/NaN no-ops.
  • Proven to use both arguments: different note sets → different coverage; different duration moves the last span's end.
  • node --check clean; all 26 JS test files pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JEoFeTPSnz4NpwwCG52hnu

Summary by CodeRabbit

  • New Features
    • Added a “completeness strip” to the top of the section lane to tint sections based on whether they contain notes.
  • Bug Fixes
    • Improved coverage/tint accuracy using boundary-aware interval logic, including correct handling for the final section and unknown/zero duration.
    • Ensures the strip updates live during note moves and refreshes correctly after committed edits.
    • More resilient behavior for incomplete or invalid section timing inputs.
  • Tests
    • Added coverage-math and memoization/invalidation tests to lock down interval semantics and rendering behavior.

A thin band at the top of the lane area tints each section by whether the
active arrangement has notes in it — an at-a-glance "where is this chart
still empty" while transcribing a long song. Ambient and neutral: a gentle
blue where there's content, a faint wash where there isn't. No percentage,
no red, nothing to click; drawn under the existing section lines/labels, no
layout change.

_sectionCoveragePure computes per-section [start, nextStart) spans (the
last section runs to the song duration, or open-ended when unknown) with a
half-open boundary rule — a note exactly on a boundary belongs to the later
section. Sections are sorted defensively and non-finite start_times dropped.

Tests: tests/section_coverage.test.js (8 cases) drive the real function:
per-span content detection, half-open boundaries, last-section-to-duration,
open-ended unknown-duration, sorted/dropped-NaN sections, note-before-first
counts for none, degenerate no-ops — and prove it uses BOTH arguments (notes
AND duration change the result). node --check clean; all 26 JS test files
pass.

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

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 619e2471-cbb5-4fd8-8d89-aeea58872ddc

📥 Commits

Reviewing files that changed from the base of the PR and between e860e89 and dc8431b.

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

📝 Walkthrough

Walkthrough

This PR adds a section completeness strip in screen.js: section coverage is computed from active note times, cached across draws, rendered as a tinted lane band, and invalidated after committed edits. A new test suite covers the pure coverage logic and related source-level behavior, and CHANGELOG.md documents the feature.

Changes

Section Completeness Strip

Layer / File(s) Summary
Coverage computation
screen.js
Adds _sectionCoveragePure() and _currentNoteTimes() to derive per-section {start, end, hasContent} from section times and active note timestamps.
Memoized strip rendering
screen.js
Adds _sectionCoverage() caching, draws the completeness tint bar in drawSections(), and bumps _coverageEditGen in _afterEdit() to invalidate cached coverage after edits.
Coverage tests
tests/section_coverage.test.js
Adds a Node test suite that extracts the pure helper and checks interval semantics, sorting, invalid inputs, duplicate boundaries, memo usage, and edit invalidation.
Changelog entry
CHANGELOG.md
Documents the new section completeness strip entry and references the test file.

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

Possibly related PRs

🚥 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 and concisely summarizes the main editor UI change: adding a section completeness strip.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/editor-section-coverage

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)

1491-1514: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Coverage recomputed from scratch every frame.

_currentNoteTimes() remaps all notes and _sectionCoveragePure re-scans note times per section on every drawSections() call. For charts with many notes/sections this repeats O(sections × notes) work per frame with no caching, though the early break on match keeps the common case cheap. Likely fine for typical chart sizes; consider memoizing/invalidating on note or section change only if profiling shows this is hot.

🤖 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 1491 - 1514, _currentNoteTimes() and drawSections()
recompute note times and section coverage from scratch on every render, so cache
the derived note-time list and section coverage instead of rescanning notes()
each frame. Add memoization or invalidation tied to the symbols that change the
inputs (notes(), S.sections, S.duration, and any arrangement edits), and have
drawSections() reuse the cached coverage so the hot path no longer does repeated
O(sections × notes) work.
🤖 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 1491-1514: _currentNoteTimes() and drawSections() recompute note
times and section coverage from scratch on every render, so cache the derived
note-time list and section coverage instead of rescanning notes() each frame.
Add memoization or invalidation tied to the symbols that change the inputs
(notes(), S.sections, S.duration, and any arrangement edits), and have
drawSections() reuse the cached coverage so the hot path no longer does repeated
O(sections × notes) work.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 23241ba4-6a22-4944-9e83-58eee4132d51

📥 Commits

Reviewing files that changed from the base of the PR and between 57d154a and 3c093ec.

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

drawSections() runs every requestAnimationFrame during playback and
recomputed section coverage from scratch each frame (an O(N) note-time
pass plus an O(sections×notes) scan), the same per-frame O(N) trap the
lanes()/laneLabels() caches deliberately avoid.

- Memoize via _sectionCoverage() behind a cheap cache key: an edit
  generation counter (_coverageEditGen, bumped in EditHistory._afterEdit
  — the edit-contract hook every mutation flows through), the active
  arrangement, notes-array identity + length, duration, and an
  O(sections) start_time fingerprint. Playback (no active drag, no edit)
  now reuses the cached result; edits invalidate it. A live note-move
  drag bypasses the memo so the strip stays live before mouseUp commits.
- Correctness: the last section span is now inclusive on its upper edge
  and extends to max(dur, lastNoteTime), so a note exactly at (or past a
  stale/short) duration is no longer invisible. Interior spans stay
  half-open [start, next) — no double-count at interior boundaries.
- Tests: add note-at-duration, notes-past-duration, no-interior-double-
  count, duplicate-start_time lock, and memo/invalidation wiring cases.

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

@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)
tests/section_coverage.test.js (1)

123-135: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fixed 1200-char slice may not cover the full drawSections body.

Estimating the actual length of drawSections (per the line-range change details, lines 1547-1585), it's roughly ~1590 characters — longer than the 1200-char window sliced here. The negative assertion (drawSections must not call the O(N) pure helper directly every frame) currently only inspects the first ~1200 chars of the function, missing the tail (the section-label/dashed-line loop). A future stray _sectionCoveragePure( call placed later in the function wouldn't be caught by this guard.

Consider slicing up to the next top-level function boundary instead of a magic length:

♻️ Proposed fix
-    assert.ok(!/drawSections[\s\S]*?_sectionCoveragePure\(/.test(
-        src.slice(src.indexOf('function drawSections'), src.indexOf('function drawSections') + 1200)),
+    const fnStart = src.indexOf('function drawSections');
+    const nextFnStart = src.indexOf('\nfunction ', fnStart + 1);
+    const fnBody = src.slice(fnStart, nextFnStart === -1 ? undefined : nextFnStart);
+    assert.ok(!/_sectionCoveragePure\(/.test(fnBody),
🤖 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/section_coverage.test.js` around lines 123 - 135, The negative
assertion in the drawSections coverage test uses a fixed 1200-character slice,
which can miss later calls to _sectionCoveragePure() inside the full
drawSections function. Update the test to slice or scope the search using a real
function boundary around drawSections (for example, from function drawSections
to the next top-level function) so the guard checks the entire body, while still
asserting that drawSections only uses _sectionCoverage() and not the pure helper
directly.
🤖 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/section_coverage.test.js`:
- Around line 123-135: The negative assertion in the drawSections coverage test
uses a fixed 1200-character slice, which can miss later calls to
_sectionCoveragePure() inside the full drawSections function. Update the test to
slice or scope the search using a real function boundary around drawSections
(for example, from function drawSections to the next top-level function) so the
guard checks the entire body, while still asserting that drawSections only uses
_sectionCoverage() and not the pure helper directly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e06aff58-ff65-43a4-9640-68cf6c690f6f

📥 Commits

Reviewing files that changed from the base of the PR and between 3c093ec and e860e89.

📒 Files selected for processing (2)
  • screen.js
  • tests/section_coverage.test.js

The negative assertion in the drawSections coverage test sliced a fixed
1200-char window from `function drawSections`, but the function body is
~1798 chars long — the slice missed the tail (section-label/dashed-line
loop), so a stray _sectionCoveragePure( call placed there wouldn't have
been caught. Slice to the next top-level `function ` boundary instead
(falling back to end-of-string if there isn't one) so the guard covers
the whole function.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@byrongamatos
byrongamatos merged commit b674590 into main Jul 7, 2026
1 check passed
@byrongamatos
byrongamatos deleted the feat/editor-section-coverage branch July 7, 2026 08:30
byrongamatos added a commit that referenced this pull request Jul 7, 2026
Resolve CHANGELOG.md [Unreleased] conflict (take-both: keep the key/scale
entry alongside the section-undo/duplicate/inspector-time/coverage entries).

Fix cross-PR integration break: #104-#107 landing together left EditHistory
._afterEdit() (in the @pure:edit-history block) bumping _coverageEditGen,
which is declared outside that block — so the undo-test sandboxes that extract
edit-history in isolation threw "_coverageEditGen is not defined". Guard the
bump with typeof, matching the isKeysMode guard two lines below. Full JS suite
46/46 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
byrongamatos added a commit that referenced this pull request Jul 7, 2026
…ding pitch (#115)

* feat(editor): in-key highlight on the fretted lanes — capo-aware sounding pitch

Roadmap 4.16a remainder (guitar-lane scale-degree tint). Extends the merged
song key/scale highlight (#108) from the piano roll to guitar/bass lanes.

- New _soundingPitchPure: openMidi + tuning offset + CAPO + fret, capo
  added exactly ONCE. Chart frets are capo-relative — verified against
  core lib/song.py pitch_from_base, the single source of the formula the
  tuner and highway scale-degree derivation share.
- The flagged double-count trap is now pinned in code and tests:
  _absolutePitch (string-moves) still deliberately omits capo (it cancels
  when comparing two pitches on one arrangement) and both helpers document
  the division of labor.
- Out-of-key fretted notes dim (body alpha cc->55, softened fret number —
  the piano-roll treatment; never red), unresolvable pitches stay fully
  lit. Highlight context is hoisted once per draw, zero per-note
  arrangement work. Key controls now show for any pitched arrangement.

Tests: tests/fret_key_highlight.test.js (8 cases) — the formula against
known pitches, Drop-D + capo composition, the capo-flips-membership case
an uncapoed resolver gets wrong, and the omits-capo pin on _absolutePitch.
Full JS suite green except tests/section_coverage.test.js, which fails on
current MAIN itself (pre-existing _afterEdit/#107 merge interaction).

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

* test(editor): document extractFn brace-count assumption for #115 (CodeRabbit nitpick)

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

---------

Co-authored-by: ChrisBeWithYou <chris@rifflarr.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: byrongamatos <xasiklas@gmail.com>
byrongamatos added a commit that referenced this pull request Jul 7, 2026
…l (read-first) (#119)

* feat(editor): in-key highlight on the fretted lanes — capo-aware sounding pitch

Roadmap 4.16a remainder (guitar-lane scale-degree tint). Extends the merged
song key/scale highlight (#108) from the piano roll to guitar/bass lanes.

- New _soundingPitchPure: openMidi + tuning offset + CAPO + fret, capo
  added exactly ONCE. Chart frets are capo-relative — verified against
  core lib/song.py pitch_from_base, the single source of the formula the
  tuner and highway scale-degree derivation share.
- The flagged double-count trap is now pinned in code and tests:
  _absolutePitch (string-moves) still deliberately omits capo (it cancels
  when comparing two pitches on one arrangement) and both helpers document
  the division of labor.
- Out-of-key fretted notes dim (body alpha cc->55, softened fret number —
  the piano-roll treatment; never red), unresolvable pitches stay fully
  lit. Highlight context is hoisted once per draw, zero per-note
  arrangement work. Key controls now show for any pitched arrangement.

Tests: tests/fret_key_highlight.test.js (8 cases) — the formula against
known pitches, Drop-D + capo composition, the capo-flips-membership case
an uncapoed resolver gets wrong, and the omits-capo pin on _absolutePitch.
Full JS suite green except tests/section_coverage.test.js, which fails on
current MAIN itself (pre-existing _afterEdit/#107 merge interaction).

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

* feat(editor): per-part view switcher — any fretted part opens in the piano roll (read-first)

EDITOR-VIEW-MODALITY-DESIGN P1 (VA.1+VA.2, decisions V1-V4/V9). The editing
view was derived from the arrangement NAME; it is now a per-part choice.

- viewFor(part): per-part pref in editor localStorage keyed song + stable
  part id (never index/display-name; keys parts piano-locked). Kind
  inference stays the default.
- isKeysMode() split: piano-SURFACE predicate (draw geometry, hit-testing,
  viewport) vs new isKeysArr() keys-DATA predicate (string moves,
  chord-sibling grouping, anchors, resize chord-expansion) — a fretted
  part in the roll still groups chords and keeps string-move machinery
  (P5 position cycling depends on exactly this).
- Read-first roll for fretted parts: one sounding-pitch mapping
  (_rollMidiForNote via _soundingPitchPure — capo once) hoisted per pass
  and shared by draw, hitNote, marquee, and updatePianoRange; null
  pitches skip, never render wrong.
- Edit-lock (V4): central gate in EditHistory.exec (typeof-guarded for
  extracted-test envs) + the live-mutating drag starts (move/resize) +
  dblclick add + EOF right-click edit; selection still works; a visible
  pill + status explain why. Lock lifts live on switching back.
- Toolbar String/Piano-roll segmented switcher + registry cycleViewMode;
  selection/drag/note-UI cleared on switch (V3).

STACKED ON #115 (feat/editor-key-highlight-guitar) — needs its
_soundingPitchPure; merge #115 first.

Tests: tests/view_switcher.test.js (11) — pure view resolution, pref
persistence/rename stability over stub localStorage, sounding-pitch roll
mapping + viewport fit (asserts NOT the wire packing), and the exec gate
(inert+notice / regression / live-unlock). Full suite green except the
pre-existing CRLF section_coverage failure (#116).

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

* feat(editor): review fixes for #119 (view switcher)

Read-only roll (fretted part in the piano roll) was enforced only at the
EditHistory exec chokepoint and the mouse/right-click-add handlers. Two
gaps:

- exec blocked ALL commands, including songScope (drum tab, tempo grid)
  edits, so switching an unrelated part into the roll froze tempo/drum
  editing. songScope commands now pass through the lock (exec + undo/redo).
- Several note-edit paths bypass EditHistory entirely and so escaped the
  lock: note-scope undo/redo, promptSlide/promptSlideUnpitch, the inspector
  setters (editorInspectorSetTech/SetFlag), and the context-menu
  editorToggleTech. The context menu opens in the roll under the default
  right-click behavior and the inspector renders for any selection, so all
  were reachable. Each is now guarded with _rollReadOnly()/_rollLockNotice.

Regression tests (tests/view_switcher.test.js) fail on pre-fix code.

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

---------

Co-authored-by: ChrisBeWithYou <chris@rifflarr.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: byrongamatos <xasiklas@gmail.com>
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