feat(editor): section completeness strip - #107
Conversation
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
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThis PR adds a section completeness strip in ChangesSection Completeness Strip
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
screen.js (1)
1491-1514: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueCoverage recomputed from scratch every frame.
_currentNoteTimes()remaps all notes and_sectionCoveragePurere-scans note times per section on everydrawSections()call. For charts with many notes/sections this repeats O(sections × notes) work per frame with no caching, though the earlybreakon 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
📒 Files selected for processing (3)
CHANGELOG.mdscreen.jstests/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>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/section_coverage.test.js (1)
123-135: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winFixed 1200-char slice may not cover the full
drawSectionsbody.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
📒 Files selected for processing (2)
screen.jstests/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>
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>
…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>
…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>
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.
_sectionCoveragePurecomputes 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-finitestart_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 att=4lands 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/NaNno-ops.durationmoves the last span's end.node --checkclean; all 26 JS test files pass.🤖 Generated with Claude Code
https://claude.ai/code/session_01JEoFeTPSnz4NpwwCG52hnu
Summary by CodeRabbit