Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
5e55876
feat(workspace): arm tonight's first setup on the role strip
seonghobae Aug 18, 2026
25bd74f
fix(workspace): preserve setup copy and transposition detail
seonghobae Aug 18, 2026
1db3ff0
test(workspace): lock review regressions
seonghobae Aug 18, 2026
12b1c4a
fix(workspace): make groove map role-aware and accessible
seonghobae Aug 18, 2026
ef4b354
fix(workspace): require actionable start evidence
seonghobae Aug 18, 2026
1c841b0
fix(workspace): explain missing start evidence
seonghobae Aug 18, 2026
6f686fe
fix(workspace): localize missing start evidence
seonghobae Aug 18, 2026
6d838ca
test(workspace): assert native setup disable
seonghobae Aug 18, 2026
098f634
docs(architecture): refresh current guidance date
seonghobae Aug 18, 2026
d27bff6
docs(design): keep groove-map contract code-current
seonghobae Aug 18, 2026
fd36651
docs(changelog): record workspace review repairs
seonghobae Aug 18, 2026
184ad06
test(workspace): expect role-specific groove map label
seonghobae Aug 18, 2026
b06be3f
test(workspace): keep setup placeholders literal
seonghobae Aug 18, 2026
0ad6315
fix(workspace): interpolate setup copy once
seonghobae Aug 18, 2026
16abeb1
test(security): forbid dynamic regex copy interpolation
seonghobae Aug 18, 2026
512c860
fix(security): use fixed copy interpolation pattern
seonghobae Aug 18, 2026
61aa51f
test(workspace): make setup regressions portable
seonghobae Aug 18, 2026
c49d0f3
Merge remote-tracking branch 'origin/develop' into HEAD
seonghobae Aug 25, 2026
d14634b
docs(changelog): remove duplicated test entry
seonghobae Aug 26, 2026
a6bc38d
test(workspace): require visible setup label in accessible name
seonghobae Aug 26, 2026
4850852
fix(a11y): include visible setup action in accessible name
seonghobae Aug 26, 2026
8d1fdd2
fix(a11y): align Korean setup accessible names
seonghobae Aug 26, 2026
1cc2699
test(a11y): align setup accessible-name oracle
seonghobae Aug 26, 2026
4e252a4
test(changelog): preserve shipped security fix classification
seonghobae Aug 26, 2026
cce6ff4
fix(changelog): preserve protected security history
seonghobae Aug 26, 2026
fbb6973
Merge remote-tracking branch 'origin/develop' into HEAD
seonghobae Aug 26, 2026
b6bcecb
fix(workspace): restore setup test contract
seonghobae Aug 28, 2026
09793c0
⚡ Bolt: GrooveMap의 reduce를 for 루프로 최적화
seonghobae Sep 5, 2026
e58dcc3
docs(perf): correct GrooveMap complexity claim
seonghobae Sep 5, 2026
e7b038c
docs(perf): bound reduce-to-loop optimization claims
seonghobae Sep 5, 2026
d77da9b
Trigger CI retry
seonghobae Sep 5, 2026
ec61228
test(workspace): preserve visible stem control names
seonghobae Sep 7, 2026
30ebfcc
fix(a11y): preserve visible stem control names
seonghobae Sep 7, 2026
ad2daa5
test(workspace): align disabled control contract
seonghobae Sep 7, 2026
9721802
Merge protected develop into workspace accessibility repair
seonghobae Sep 7, 2026
1c3248a
test(workspace): reject malformed setup ranges
seonghobae Sep 7, 2026
87e8640
fix(workspace): validate setup range evidence
seonghobae Sep 7, 2026
9bd8e1b
test(release): preserve semantic setup boundaries
seonghobae Sep 7, 2026
94f1c00
test(i18n): cover workspace rehearsal controls
seonghobae Sep 7, 2026
0044cd5
fix(i18n): localize rehearsal controls
seonghobae Sep 7, 2026
39a12a9
docs(workspace): define validated range fallback
seonghobae Sep 7, 2026
1796d5e
test(ui): preserve GrooveMap max-offset semantics
seonghobae Sep 22, 2026
8feaebb
fix(ui): preserve GrooveMap max-offset semantics
seonghobae Sep 22, 2026
341e586
docs(perf): bound GrooveMap loop claims
seonghobae Sep 22, 2026
a3a6ab6
test(ui): reject inert GrooveMap cancel control
seonghobae Sep 22, 2026
9a5bf5d
test(ui): reject fabricated GrooveMap loading progress
seonghobae Sep 22, 2026
340bf2e
fix(ui): expose GrooveMap cancel only with real capability
seonghobae Sep 22, 2026
103d2a2
fix(ui): remove fabricated GrooveMap loading percentage
seonghobae Sep 22, 2026
48783c0
fix(ui): remove fabricated Korean GrooveMap progress
seonghobae Sep 22, 2026
d496027
docs(ui): require truthful GrooveMap loading controls
seonghobae Sep 22, 2026
febeca2
docs(release): record truthful GrooveMap loading contract
seonghobae Sep 22, 2026
b559497
repair(changelog): restore full release history
seonghobae Sep 22, 2026
89de8e2
merge(workspace): adopt formatter prerequisite #1176
seonghobae Sep 22, 2026
9f4b545
test(ui): bind GrooveMap max offset to rendered geometry
seonghobae Sep 23, 2026
9fd44c9
Acknowledge Preservation Receipt and abandon task
seonghobae Sep 24, 2026
3511fe2
chore(ci): adopt canonical Ruff formatter prerequisite
seonghobae Sep 24, 2026
be321e3
merge(workspace): preserve buyer GrooveMap truth in performance lane
seonghobae Sep 24, 2026
07d9dc8
merge(workspace): adopt canonical GrooveMap max-offset evidence
seonghobae Sep 24, 2026
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
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,7 @@
## 2026-07-13 - Array.from mapping optimization
**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.
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ 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: 2 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ARCHITECTURE.md

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

## Brand source

Expand Down Expand Up @@ -68,6 +68,7 @@ Last updated: 2026-03-11
- 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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Analysis target model

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

### 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 @@ -15,7 +16,16 @@

### 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: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ 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: 50 additions & 0 deletions apps/desktop/src/features/workspace/GrooveMap.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { GrooveMap, maximumNoteOffset } from "./GrooveMap";

describe("maximumNoteOffset", () => {
it("preserves the ten-second floor and finite maximum", () => {
expect(maximumNoteOffset([])).toBe(10);
expect(
maximumNoteOffset([
{ onset: 0, offset: 1.5, pitch: "C4", velocity: 100 },
{ onset: 1.5, offset: 12, pitch: "D4", velocity: 100 }
])
).toBe(12);
});

it("preserves Math.max non-finite semantics until shared admission rejects them", () => {
expect(
Number.isNaN(
maximumNoteOffset([
{ onset: 0, offset: Number.NaN, pitch: "C4", velocity: 100 },
{ onset: 1, offset: 12, pitch: "D4", velocity: 100 }
])
)
).toBe(true);
expect(
maximumNoteOffset([
{ onset: 0, offset: Number.POSITIVE_INFINITY, pitch: "C4", velocity: 100 }
])
).toBe(Number.POSITIVE_INFINITY);
});
});

describe("GrooveMap maximum-offset geometry", () => {
it("uses an offset beyond the ten-second floor for rendered note geometry", () => {
render(
<GrooveMap
roleName="Bass"
notes={[
{ onset: 0, offset: 5, pitch: "C4", velocity: 100 },
{ onset: 10, offset: 20, pitch: "D4", velocity: 100 }
]}
/>
);

expect(screen.getByTitle("D4 (10.00s - 20.00s)")).toHaveStyle({
left: "50%",
width: "50%"
});
});
});
76 changes: 54 additions & 22 deletions apps/desktop/src/features/workspace/GrooveMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,36 @@ 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[] = [];

/** Documented. */
/** 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. */
interface GrooveMapProps {
notes?: TranscriptionNote[];
isLoading?: boolean;
entranceOnset?: number;
roleName: string;
onCancel?: () => void;
}

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

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

// Unique pitches to determine vertical lanes (avoiding 88-key piano roll)
const uniquePitches = useMemo(() => {
Expand All @@ -36,6 +49,13 @@ function GrooveMapComponent({ notes, isLoading }: GrooveMapProps) {
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 @@ -44,21 +64,27 @@ function GrooveMapComponent({ notes, isLoading }: GrooveMapProps) {
>
<span className="flex items-center font-medium text-teal-100">
<Loader2 className="mr-2 size-4 animate-spin" aria-hidden="true" />
Checking the bass line... 45%
{fillTranslation(t("grooveMapLoading"), { role: roleName })}
</span>
<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>
{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}
</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-400"
>
No bass line transcription yet. Use it when you want to check the groove before rehearsal.
<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>
);
}
Expand All @@ -68,13 +94,13 @@ function GrooveMapComponent({ notes, isLoading }: GrooveMapProps) {
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="Bass transcription groove map"
aria-label={fillTranslation(t("grooveMapRegionLabel"), { role: roleName })}
>
<div className="sr-only">
Transcription complete. {renderedNotes.length} notes analyzed.
{fillTranslation(t("grooveMapComplete"), { count: renderedNotes.length })}
</div>
<p className="mb-3 text-xs font-black uppercase tracking-[0.22em] text-cyan-200">
{renderedNotes.length} notes mapped for rehearsal
{fillTranslation(t("grooveMapMapped"), { count: renderedNotes.length })}
</p>

<div style={{ position: "relative", minWidth: "100%", height: `${uniquePitches.length * 40}px` }}>
Expand All @@ -95,20 +121,26 @@ function GrooveMapComponent({ notes, isLoading }: GrooveMapProps) {
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}
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)]"
id={isEntrance && index === entranceIndex ? "workspace-groove-entrance" : undefined}
Comment thread
seonghobae marked this conversation as resolved.
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"
}`}
style={{
top: `${pitchIndex * 40 + 8}px`,
left: `${leftPercent}%`,
width: `${widthPercent}%`
}}
title={noteLabel}
title={isEntrance ? fillTranslation(t("grooveMapEntranceTitle"), { note: noteLabel }) : noteLabel}
>
<span className="sr-only">
{noteLabel}
{isEntrance ? fillTranslation(t("grooveMapEntranceAnnouncement"), { note: noteLabel }) : noteLabel}
</span>
</div>
);
Expand All @@ -120,4 +152,4 @@ function GrooveMapComponent({ notes, isLoading }: GrooveMapProps) {

const GrooveMap = memo(GrooveMapComponent);

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