Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
**Learning:** Using `Array.from({ length: N }).map(...)` creates an intermediate array of `undefined` values which requires memory allocation and garbage collection, adding O(N) unnecessary overhead in frequently re-rendered UI components.
**Action:** Use `Array.from({ length: N }, (_, index) => ...)` to map elements directly during array creation, avoiding intermediate allocations.

## 2026-09-05 - Min/max scans remain linear
**Learning:** Replacing `Array.prototype.reduce()` with an indexed loop for an extremum search can remove callback dispatch, but both forms still scan every element and remain O(N). A simple `if (value > max)` is not automatically semantics-preserving: it ignores `NaN`, whereas `Math.max` propagates it. A microbenchmark on an arbitrary array does not establish buyer-path improvement.
**Action:** Preserve the existing reduction semantics first, then profile representative GrooveMap transcription sizes in the actual Chromium/Electron render path before claiming material latency, heap, or GC gains.
## 2024-09-24 - Missing useCallback on handlers breaks React.memo

**Learning:** In React, passing unmemoized event handler functions (like `handlePracticeProgressChange`) to a `React.memo`-wrapped child component (`PracticeProgress`) completely breaks its memoization, causing it to re-render every time the parent (`Workspace`) renders, even if its props semantically didn't change.
**Action:** Always wrap event handlers passed as props to memoized components in `useCallback` to ensure referential stability across parent renders.
1 change: 0 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ This section applies to any agent (Claude, Codex, Cursor, opencode, ...) working
- Keep UI and analysis engine decoupled through shared contracts.
- Prefer minimal, test-first changes for production code.
- Prefer practical, friendly, rehearsal-first wording over academic or authority-heavy language.
- After a part is selected, the role strip must name tonight's setup from `setupNote` / transposition / simplification and point at the earliest analyzed note, or a validated playable range when no exact note exists. Do not leave `Transcribe Bass` as a no-op, and do not invent Stem Lab isolation here.
- Do not reduce the product to a chord analyzer when form, timing, player coordination, playable ranges, simplification, and setup cues are the real rehearsal blockers.
- Do not frame usability as a reason to accept weak analysis quality; BandScope should aim for both easy use and high accuracy.

Expand Down
3 changes: 1 addition & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ARCHITECTURE.md

Last updated: 2026-08-18
Last updated: 2026-03-11

## Brand source

Expand Down Expand Up @@ -68,7 +68,6 @@ Last updated: 2026-08-18
- BandScope is not only a shell around chord labels, stems, and ranges.
- The technical scope includes rehearsal-facing outputs for harmony, section roadmap, groove cues, role entry and dropout cues, simplification guidance, transposition or setup guidance, confidence flags, and rehearsal priority.
- These outputs must stay aligned with `docs/brand-story.md` rather than drifting back to a song-summary-only analyzer.
- Ready-workspace role-strip setup must arm tonight's `setupNote` (then transposition / simplification) and name the first analyzed entrance on the groove map. Isolation playback stays out of this lane.

## Analysis target model

Expand Down
10 changes: 0 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

### Added

- The ready workspace can set up tonight's selected part from the analyzed setup cue and name the first entrance on the groove map, instead of leaving `Transcribe Bass` inert.
- Name tonight's first playable range on the ready rehearsal map and tell the player to check that span on their instrument before the section.
- Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace.
- 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함.
Expand All @@ -16,16 +15,7 @@

### Fixed

- Keep Groove Map loading truthfully indeterminate unless real progress exists, and expose Cancel only when a cancellation callback is actually available.
- Keep disabled Stem Player controls discoverable by their visible labels for
assistive technology and speech input while retaining the translated
unavailable reason in each accessible name and tooltip.
- Reject sentinel, malformed, and inverted setup ranges before they can enable
a buyer-visible rehearsal action or render as playable evidence.
- Localize Groove Map states and the unavailable Loop control through the
owned translation boundary, with literal fail-closed placeholder handling.
- Upgraded the local score PDF parser to `pdfjs-dist` 6.2.108, pinned Undici 7.29.0 across the workspace, and constrained PDF loading to copied in-memory bytes with a same-origin bundled worker and npm-generated lock provenance.
- Keep the Groove Map role-aware for non-bass parts, preserve a visible keyboard focus indicator, emit only one first-entrance DOM anchor for simultaneous notes, and fail closed when setup lacks both an analyzed entrance and a playable range.

## [0.1.3] - 2026-04-29

Expand Down
2 changes: 0 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

`AGENTS.md` is the canonical agent operating guide — read and follow it before making changes. It defines the security workflow (`Security Notes`), supply-chain workflow, cross-platform build rules, GitHub bootstrap rules, code style, and safety guardrails. This file complements it with commands and architecture; when in doubt, `AGENTS.md` and the docs it references win.

After a part is selected, the role-strip setup control must name tonight's setup cue and the earliest analyzed note, or a validated playable range when no exact note exists. Do not leave `Transcribe Bass` as a no-op.

Agent execution and delegation rules live in `docs/agents/README.md`. PR canonicalization rules live in `docs/workflow/pr-continuity.md`.

## Common commands
Expand Down
50 changes: 0 additions & 50 deletions apps/desktop/src/features/workspace/GrooveMap.test.tsx

This file was deleted.

76 changes: 22 additions & 54 deletions apps/desktop/src/features/workspace/GrooveMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,23 @@ import { memo, useMemo } from "react";
import type { TranscriptionNote } from "@bandscope/shared-types";
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
import { createTranslator, detectPreferredLocale, fillTranslation } from "../../i18n";

const EMPTY_NOTES: TranscriptionNote[] = [];

/** Preserve the former reduction semantics while avoiding reducer callback dispatch. */
function maximumNoteOffset(notes: readonly TranscriptionNote[]): number {
let max = 10;
for (let i = 0; i < notes.length; i++) {
// Keep Math.max here: shared timing admission does not yet reject every non-finite value.
// A simple `>` comparison would therefore silently change NaN handling at this UI boundary.
max = Math.max(max, notes[i]!.offset);
}
return max;
}

/** Inputs for the selected role's rehearsal groove map. */
/** Documented. */
interface GrooveMapProps {
notes?: TranscriptionNote[];
isLoading?: boolean;
entranceOnset?: number;
roleName: string;
onCancel?: () => void;
}

/** Render the selected role's transcription and optional first-entrance emphasis. */
function GrooveMapComponent({ notes, isLoading, entranceOnset, roleName, onCancel }: GrooveMapProps) {
/** Documented. */
function GrooveMapComponent({ notes, isLoading }: GrooveMapProps) {
const renderedNotes = notes ?? EMPTY_NOTES;
const t = useMemo(() => createTranslator(detectPreferredLocale()), []);

const maxTime = useMemo(() => maximumNoteOffset(renderedNotes), [renderedNotes]);
// Find max offset to determine timeline width
const maxTime = useMemo(() => {
return renderedNotes.reduce((max, n) => Math.max(max, n.offset), 10);
}, [renderedNotes]);

// Unique pitches to determine vertical lanes (avoiding 88-key piano roll)
const uniquePitches = useMemo(() => {
Expand All @@ -49,13 +36,6 @@ function GrooveMapComponent({ notes, isLoading, entranceOnset, roleName, onCance
return map;
}, [uniquePitches]);

const entranceIndex = useMemo(() => {
if (entranceOnset === undefined) {
return -1;
}
return renderedNotes.findIndex((note) => note.onset === entranceOnset);
}, [entranceOnset, renderedNotes]);

if (isLoading) {
return (
<div
Expand All @@ -64,27 +44,21 @@ function GrooveMapComponent({ notes, isLoading, entranceOnset, roleName, onCance
>
<span className="flex items-center font-medium text-teal-100">
<Loader2 className="mr-2 size-4 animate-spin" aria-hidden="true" />
{fillTranslation(t("grooveMapLoading"), { role: roleName })}
Checking the bass line... 45%
</span>
{onCancel ? (
<Button
type="button"
variant="outline"
size="sm"
onClick={onCancel}
className="border-teal-300/20 bg-teal-300/10 text-teal-100 hover:bg-teal-300/20 hover:text-white"
>
{t("grooveMapCancel")}
</Button>
) : null}
<Button variant="outline" size="sm" className="border-teal-300/20 bg-teal-300/10 text-teal-100 hover:bg-teal-300/20 hover:text-white">
Cancel
</Button>
</div>
);
}

if (renderedNotes.length === 0) {
return (
<div className="mt-4 rounded-lg border border-dashed border-cyan-200/15 bg-slate-950/60 p-6 text-center text-sm text-slate-300">
{fillTranslation(t("grooveMapEmpty"), { role: roleName })}
<div
className="mt-4 rounded-lg border border-dashed border-cyan-200/15 bg-slate-950/60 p-6 text-center text-sm text-slate-400"
>
No bass line transcription yet. Use it when you want to check the groove before rehearsal.
</div>
);
}
Expand All @@ -94,13 +68,13 @@ function GrooveMapComponent({ notes, isLoading, entranceOnset, roleName, onCance
className="relative mt-4 overflow-x-auto rounded-lg border border-cyan-200/15 bg-slate-950/80 p-4 shadow-inner shadow-cyan-950/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
role="region"
tabIndex={0}
aria-label={fillTranslation(t("grooveMapRegionLabel"), { role: roleName })}
aria-label="Bass transcription groove map"
>
<div className="sr-only">
{fillTranslation(t("grooveMapComplete"), { count: renderedNotes.length })}
Transcription complete. {renderedNotes.length} notes analyzed.
</div>
<p className="mb-3 text-xs font-black uppercase tracking-[0.22em] text-cyan-200">
{fillTranslation(t("grooveMapMapped"), { count: renderedNotes.length })}
{renderedNotes.length} notes mapped for rehearsal
</p>

<div style={{ position: "relative", minWidth: "100%", height: `${uniquePitches.length * 40}px` }}>
Expand All @@ -121,26 +95,20 @@ function GrooveMapComponent({ notes, isLoading, entranceOnset, roleName, onCance
const leftPercent = (note.onset / maxTime) * 100;
const widthPercent = ((note.offset - note.onset) / maxTime) * 100;
const noteLabel = `${note.pitch} (${note.onset.toFixed(2)}s - ${note.offset.toFixed(2)}s)`;
const isEntrance = entranceOnset !== undefined && note.onset === entranceOnset;

return (
<div
key={index}
id={isEntrance && index === entranceIndex ? "workspace-groove-entrance" : undefined}
className={`absolute h-6 rounded shadow-[0_0_18px_rgba(94,234,212,0.28)] ${
isEntrance
? "bg-gradient-to-r from-amber-300 via-orange-300 to-rose-300 ring-2 ring-amber-200"
: "bg-gradient-to-r from-teal-300 via-cyan-300 to-violet-300"
}`}
className="absolute h-6 rounded bg-gradient-to-r from-teal-300 via-cyan-300 to-violet-300 shadow-[0_0_18px_rgba(94,234,212,0.28)]"
style={{
top: `${pitchIndex * 40 + 8}px`,
left: `${leftPercent}%`,
width: `${widthPercent}%`
}}
title={isEntrance ? fillTranslation(t("grooveMapEntranceTitle"), { note: noteLabel }) : noteLabel}
title={noteLabel}
>
<span className="sr-only">
{isEntrance ? fillTranslation(t("grooveMapEntranceAnnouncement"), { note: noteLabel }) : noteLabel}
{noteLabel}
</span>
</div>
);
Expand All @@ -152,4 +120,4 @@ function GrooveMapComponent({ notes, isLoading, entranceOnset, roleName, onCance

const GrooveMap = memo(GrooveMapComponent);

export { GrooveMap, maximumNoteOffset };
export { GrooveMap };
Loading