Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
87664d8
feat(workspace): name tonight's first part handoff on the map
seonghobae Aug 23, 2026
0588532
perf(workspace): memoize first handoff resolution
seonghobae Aug 23, 2026
421ff8b
test(workspace): pin stable handoff region label
seonghobae Aug 23, 2026
e06ca27
fix(workspace): keep handoff region name stable
seonghobae Aug 23, 2026
fb9a10e
Merge branch 'develop' into feat/workspace-first-part-handoff
seonghobae Aug 25, 2026
b83462b
fix(review): make the part-handoff callout id instance-safe
seonghobae Aug 26, 2026
679e518
Merge remote-tracking branch 'origin/develop' into HEAD
seonghobae Aug 26, 2026
e3e61ae
fix(workspace): keep part handoff navigation scoped
seonghobae Aug 28, 2026
69a02f5
test(workspace): reproduce destination handoff semantics
seonghobae Sep 1, 2026
4cfe4a8
fix(workspace): resolve corroborated handoff at destination
seonghobae Sep 1, 2026
9180677
test(workspace): align handoff suite with source-to-destination contract
seonghobae Sep 1, 2026
5d26e9f
test(workspace): preserve hostile-metadata coverage for transitions
seonghobae Sep 1, 2026
ef87def
test(workspace): share activity-transition fixture
seonghobae Sep 1, 2026
4dfdc7c
test(workspace): navigate handoff callout to destination
seonghobae Sep 1, 2026
2d0e1f0
test(workspace): reduce motion on destination handoff
seonghobae Sep 1, 2026
83ac4df
test(workspace): keep Korean particles on destination handoff
seonghobae Sep 1, 2026
74f2a29
test(workspace): scope destination handoff navigation
seonghobae Sep 1, 2026
c59f8f0
test(workspace): integrate destination handoff navigation
seonghobae Sep 1, 2026
b8dc49b
docs(workspace): define source-to-destination handoff contract
seonghobae Sep 1, 2026
a995bcf
docs(architecture): align handoff transition semantics
seonghobae Sep 1, 2026
722f7d2
docs(ui): align handoff callout with destination transition
seonghobae Sep 1, 2026
0cde4db
docs(workspace): explain destination handoff navigation
seonghobae Sep 1, 2026
dde300b
chore(scope): restore lowercase palette baseline
seonghobae Sep 1, 2026
7c7b417
chore(scope): restore uppercase palette baseline
seonghobae Sep 1, 2026
5e9a712
chore(scope): match uppercase palette to protected base
seonghobae Sep 1, 2026
adc39b2
test(workspace): preserve selected-role handoff scope
seonghobae Sep 1, 2026
57c6b4a
fix(workspace): scope handoff to selected role
seonghobae Sep 1, 2026
6511daf
test(workspace): scope handoff callout to selected role
seonghobae Sep 1, 2026
27dcf0f
fix(workspace): scope handoff callout to role filter
seonghobae Sep 1, 2026
3ab7f6c
test(workspace): wire selected role into first handoff
seonghobae Sep 1, 2026
8030b06
chore(ci): activate PR 1007 source repair
seonghobae Sep 1, 2026
75105e5
chore(ci): run PR 1007 repair on synchronize
seonghobae Sep 1, 2026
504f2ba
chore(ci): retire dormant PR 1007 source repair
seonghobae Sep 1, 2026
f5c364f
test(workspace): expose mounted handoff role-scope gap
seonghobae Sep 1, 2026
234c316
fix(workspace): wire selected role into handoff callout
seonghobae Sep 1, 2026
98b2faf
test(workspace): cover stale selected role across songs
seonghobae Sep 1, 2026
cc20581
fix(workspace): ignore stale role for new-song handoffs
seonghobae Sep 1, 2026
2093732
test(analysis): reject heuristic handoff authority
seonghobae Sep 1, 2026
5261b1c
fix(analysis): keep heuristic fallback handoff-free
seonghobae Sep 1, 2026
de4d39f
test(roles): wrap heuristic provenance assertion
seonghobae Sep 1, 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
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.
- Name tonight's first corroborated part-to-part handoff from the source section's active giving role and owned `handoff_to` / receiving `handoff_from` evidence, then resolve the receiving role and cue against the immediately following destination section where that role becomes active. Show the destination label and start time, and make Open navigate to that rendered destination. Do not invent a pass from one-section same-state graphs, groove, cue, simplification, overlap, range, chord labels, function labels, setup notes, confirmed overrides, harmonic explanations, confidence notes, transposition plans, or a labeled `handoff` form. Distinct from labeled-form handoff (#937) and Part Handoff Map visualization (#850).
- 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
5 changes: 3 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# ARCHITECTURE.md

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

## Brand source

- Product identity, UX tone, copy rules, and prioritization tie-breakers live in `docs/brand-story.md`.
- The mounted workspace copy for tonight's first corroborated part-to-part handoff must treat `partGraph.handoff_to` as a transition from the current source section into the immediately following destination section: name the active giving role from the source, corroborate the receiver with source `handoff_from`, require the giver to deactivate and receiver to activate in the destination, and show the destination label/start time. Open moves to that rendered destination section. Do not invent a pass from a one-section same-state graph, groove, cue, simplification, overlap, range, chord labels, function labels, setup notes, confirmed overrides, harmonic explanations, confidence notes, transposition plans, or a labeled `handoff` form. Distinct from labeled-form handoff (#937) and Part Handoff Map visualization (#850).
- Future PRDs, TRDs, onboarding copy, empty states, error messages, and marketing copy should use that document as the single brand source of truth.

## Security source
Expand Down Expand Up @@ -80,7 +81,7 @@ Last updated: 2026-03-11

- Core rehearsal artifacts should include:
- likely harmony by section and by role
- section roadmap with entries, dropouts, pickups, stops, tags, and handoffs
- section roadmap with entries, dropouts, pickups, stops, tags, labeled form handoffs, and corroborated part-to-part handoffs
- groove and timing cues relevant to locking the band together
- playable ranges and density or overlap warnings, with the ready workspace naming tonight's first span and the next instrument check
- simplification, transposition, capo, tuning, or setup cues where applicable
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- Name tonight's first corroborated part-to-part handoff in the mounted rehearsal workspace so the room can lock the owned pass before rehearsal; the Open action moves to the matching rendered map section, while inherited or accessor-backed runtime metadata remains guidance-only instead of becoming navigation authority. Distinct from labeled-form handoff (#937) and Part Handoff Map visualization (#850).
- 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 Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ BandScope is a local-first desktop app for rehearsal prep: it turns a song into

Three layers, decoupled through shared contracts:

- `apps/desktop` — Tauri 2 + Vite + React 19 shell (Tailwind 4, Base UI, Storybook). Feature screens live in `src/features/` (home, workspace, chords, ranges, player, settings). The ready workspace names tonight's first playable range and the next instrument check. `src/lib/analysis.ts` and `src/lib/job_runner.ts` call typed Tauri IPC commands, with a browser fallback that serves demo data when not running inside Tauri.
- `apps/desktop` — Tauri 2 + Vite + React 19 shell (Tailwind 4, Base UI, Storybook). Feature screens live in `src/features/` (home, workspace, chords, ranges, player, settings). The mounted workspace names tonight's first corroborated part-to-part handoff and opens the matching rendered map section. The ready workspace names tonight's first playable range and the next instrument check. Do not invent that pass from groove, cue, simplification, overlap, range, chord labels, function labels, setup notes, confirmed overrides, harmonic explanations, confidence notes, transposition plans, or a labeled `handoff` form without owned bidirectional `partGraph` edges. Distinct from labeled-form handoff (#937) and Part Handoff Map visualization (#850). `src/lib/analysis.ts` and `src/lib/job_runner.ts` call typed Tauri IPC commands, with a browser fallback that serves demo data when not running inside Tauri.
- `apps/desktop/src-tauri/src/main.rs` — the Rust orchestration boundary. Tauri commands (`start_analysis_job`, `get_analysis_job_status`, `select_local_audio_source`, `import_youtube_url`) validate untrusted input (project IDs, file paths, URLs) and spawn the Python engine as a subprocess. There is no loopback HTTP listener and no network path for local analysis.
- `services/analysis-engine` — Python package `bandscope_analysis` (librosa/numpy). Entry point `cli.py` reads a JSON job request on stdin and prints a structured job-status JSON envelope on stdout (`--progress-jsonl` streams progress lines). `api.py` orchestrates the pipeline across the `separation`, `sections`, `roles`, `chords`, `ranges`, `temporal`, `transcription`, and `youtube` modules.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { FirstPartHandoffCallout } from "./FirstPartHandoffCallout";
import { createPartHandoffTransitionSong } from "./firstPartHandoff.test-fixture";

describe("FirstPartHandoffCallout Korean role copy", () => {
afterEach(() => {
vi.unstubAllGlobals();
});

it("keeps vowel-ending dynamic role names particle-safe before and after the destination handoff action", () => {
vi.stubGlobal("navigator", { language: "ko-KR" });
const song = createPartHandoffTransitionSong();
const source = song.sections[0]!;
const destination = song.sections[1]!;
source.roles[0] = { ...source.roles[0]!, id: "piano", name: "피아노", rehearsalPriority: "high" };
source.partGraph = [
{ role_id: "piano", is_active: true, handoff_to: ["vocal"], handoff_from: [] },
{ role_id: "vocal", is_active: false, handoff_to: [], handoff_from: ["piano"] }
];
destination.roles[0] = { ...destination.roles[0]!, id: "vocal", name: "보컬", rehearsalPriority: "medium" };
destination.partGraph = [
{ role_id: "piano", is_active: false, handoff_to: [], handoff_from: [] },
{ role_id: "vocal", is_active: true, handoff_to: [], handoff_from: [] }
];

const grid = document.createElement("div");
grid.id = "workspace-song-structure-grid";
grid.setAttribute("role", "region");
grid.setAttribute("aria-label", "Scrollable song structure timeline");
const sourceTarget = document.createElement("div");
sourceTarget.dataset.sectionIndex = "0";
const target = document.createElement("div");
target.dataset.sectionIndex = "1";
Object.defineProperty(target, "scrollIntoView", { configurable: true, value: vi.fn() });
grid.append(sourceTarget, target);
document.body.appendChild(grid);

render(<FirstPartHandoffCallout song={song} />);

expect(screen.getByText("0:10 코러스에서 피아노 파트가 보컬 파트로 넘깁니다.")).toBeTruthy();
expect(screen.queryByText(/피아노이/)).toBeNull();
expect(screen.queryByText(/피아노가/)).toBeNull();

fireEvent.click(screen.getByRole("button", { name: "0:10 피아노 핸드오프 위치 열기" }));

expect(screen.getByText("0:10에서 피아노 파트에서 보컬 파트로 넘긴 다음 합주를 시작하세요.")).toBeTruthy();
expect(screen.queryByText(/피아노과/)).toBeNull();

grid.remove();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { FirstPartHandoffCallout } from "./FirstPartHandoffCallout";
import { createPartHandoffTransitionSong } from "./firstPartHandoff.test-fixture";

describe("FirstPartHandoffCallout reduced motion", () => {
afterEach(() => {
vi.unstubAllGlobals();
});

it("scrolls immediately to the destination when the operating system requests reduced motion", () => {
vi.stubGlobal("matchMedia", (query: string) => ({
matches: query === "(prefers-reduced-motion: reduce)",
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn()
}));

const grid = document.createElement("div");
grid.id = "workspace-song-structure-grid";
grid.setAttribute("role", "region");
grid.setAttribute("aria-label", "Scrollable song structure timeline");
const source = document.createElement("div");
source.dataset.sectionIndex = "0";
const target = document.createElement("div");
target.dataset.sectionIndex = "1";
const scrollIntoView = vi.fn();
Object.defineProperty(target, "scrollIntoView", {
configurable: true,
value: scrollIntoView
});
grid.append(source, target);
document.body.appendChild(grid);

render(<FirstPartHandoffCallout song={createPartHandoffTransitionSong()} />);
fireEvent.click(screen.getByRole("button", { name: "Open Bass Guitar handoff at 0:10" }));
expect(scrollIntoView).toHaveBeenCalledWith({ block: "nearest", behavior: "auto" });

grid.remove();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { FirstPartHandoffCallout } from "./FirstPartHandoffCallout";
import { createPartHandoffTransitionSong } from "./firstPartHandoff.test-fixture";

describe("FirstPartHandoffCallout selected role scope", () => {
it("keeps the handoff when the selected role receives the pass", () => {
render(
<FirstPartHandoffCallout
song={createPartHandoffTransitionSong()}
activeRole="lead-vocal"
/>
);

expect(screen.getByText("Bass Guitar still hands off to Lead Vocal in the chorus at 0:10.")).toBeTruthy();
expect(screen.getByRole("button", { name: "Open Bass Guitar handoff at 0:10" })).toBeTruthy();
});

it("keeps an unrelated selected role guidance-only", () => {
render(
<FirstPartHandoffCallout
song={createPartHandoffTransitionSong()}
activeRole="keys-right"
/>
);

expect(
screen.getByText(
"Nothing still has a part handoff. Stay on tonight's map until a part owns a rehearsal-facing pass."
)
).toBeTruthy();
expect(screen.queryByRole("button")).toBeNull();
});
});
Loading
Loading