Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
fa27cdf
feat(workspace): name tonight's first drop plan on the map
seonghobae Aug 27, 2026
7f16388
fix(review): remove unused drop-plan constant
seonghobae Aug 27, 2026
620aed1
test(review): pin user drop guidance after open
seonghobae Aug 27, 2026
1c0fa8a
test(i18n): pin consistent Korean drop-plan guidance
seonghobae Aug 27, 2026
b340f15
fix(i18n): align Korean drop-plan guidance tone
seonghobae Aug 27, 2026
2664c8e
test(workspace): reject untranslated model drop guidance
seonghobae Aug 27, 2026
cf938af
fix(workspace): reject free-form model drop guidance
seonghobae Aug 27, 2026
a1c5a57
fix(ci): restore extractor formatter contract
seonghobae Aug 27, 2026
f36fa45
test(core): reject orphan drop-plan provenance
seonghobae Aug 27, 2026
be72fe2
test(i18n): align particle-safe drop copy expectation
seonghobae Aug 27, 2026
0aab1d6
test(i18n): align provenance guidance expectation
seonghobae Aug 27, 2026
0892572
test(analysis): preserve shared source across role swap
seonghobae Aug 27, 2026
86363f3
test(drop-plan): pin shared-source continuity
seonghobae Aug 27, 2026
fc0d3c8
fix(drop-plan): compare continuing sources
seonghobae Aug 27, 2026
72a3012
fix(drop-plan): preserve shared source continuity
seonghobae Aug 27, 2026
ea6c8d9
test(drop-plan): reject unknown native provenance
seonghobae Aug 27, 2026
431eae7
fix(drop-plan): validate native provenance
seonghobae Aug 27, 2026
ea3284a
style(drop-plan): apply pinned Ruff formatting
seonghobae Aug 27, 2026
3ed69f3
test(review): reject accompaniment drop-plan ownership
seonghobae Aug 27, 2026
e4d48df
fix(review): reject accompaniment drop-plan ownership
seonghobae Aug 27, 2026
af95ce4
fix(ci): apply pinned Ruff formatting
seonghobae Aug 27, 2026
bfc35d4
fix(ci): match pinned Ruff set-comprehension layout
seonghobae Aug 27, 2026
ed677cb
fix(ci): restore pinned Ruff drop-plan layout
seonghobae Aug 27, 2026
bfd8c02
style(ci): apply Ruff formatting to drop-plan extractor
seonghobae Aug 27, 2026
c043158
style(ci): make named drop entries formatter-stable
seonghobae Aug 27, 2026
21ec157
test(review): reset drop armed state on display identity changes
seonghobae Aug 27, 2026
0fe3cf7
fix(review): reset drop armed state on display identity changes
seonghobae Aug 27, 2026
db4d992
fix(ci): apply canonical Ruff formatting
seonghobae Aug 27, 2026
3c2da0c
fix(ci): preserve formatter-required final newline
seonghobae Aug 27, 2026
b0b425d
test(drop-plan): build accompaniment swap fixture safely
seonghobae Aug 27, 2026
be14490
test(drop-plan): reject blank sourced guidance
seonghobae Aug 27, 2026
0d5a454
test(drop-plan): reject blank native guidance
seonghobae Aug 27, 2026
86de898
test(drop-plan): cover multiline sourced guidance
seonghobae Aug 27, 2026
b8f7889
test(drop-plan): cover multiline native guidance
seonghobae Aug 27, 2026
ec12e75
fix(drop-plan): reject invalid sourced guidance
seonghobae Aug 28, 2026
5a2847b
fix(drop-plan): reject invalid native guidance
seonghobae Aug 28, 2026
3880092
test(native): reproduce shared role persistence mismatch
seonghobae Aug 28, 2026
5ee84d8
fix(native): preserve optional shared role fields
seonghobae Aug 28, 2026
59efc24
test(native): bound shared practice progress
seonghobae Aug 28, 2026
28a8579
fix(native): enforce shared practice progress range
seonghobae Aug 28, 2026
4ff8e1d
test(review): reject drop copy without provenance
seonghobae Aug 28, 2026
ab8c90a
test(review): reject native drop copy without provenance
seonghobae Aug 28, 2026
eb7432c
fix(contract): require provenance for plan copy
seonghobae Aug 28, 2026
191189d
fix(workspace): enforce drop plan provenance
seonghobae Aug 28, 2026
f797503
fix(contract): align drop plan validation
seonghobae Aug 29, 2026
b90859a
fix(cache): invalidate stale analysis results
seonghobae Aug 29, 2026
693732d
fix(cache): preserve reusable feature files
seonghobae Aug 29, 2026
ff4ee36
fix(workspace): fail closed on ambiguous drop targets
seonghobae Aug 29, 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 @@ -2,6 +2,7 @@

## Project overview
- BandScope is a local-first desktop app for rehearsal prep: a practical song view with likely harmony by section and by instrument or vocal role, form and groove cues, stems, playable ranges, simplification guidance, transposition or setup cues, part-overlap cues, visible confidence, and rehearsal priorities.
- Name tonight's first drop plan with the owning part when an entering role is corroborated, the owned `dropPlan` copy, the labeled section, and the time so the next action is obvious. Do not invent that copy from groove, cue, simplification, overlap, range, chord labels, function labels, setup notes, transposition plans, vamp plans, fill plans, tuning plans, dynamics plans, articulation plans, hook plans, solo plans, pad plans, hit plans, cutoff plans, turnaround plans, pickup plans, breakdown plans, confirmed overrides, harmonic explanations, or confidence notes.
- Authoritative delivery rules live in `ARCHITECTURE.md`, `docs/plans/`, and the root verification scripts.
- Brand, tone, UX copy, and prioritization rules live in `docs/brand-story.md` and must be applied to PRDs, TRDs, UI copy, onboarding, empty states, and error messages.
- App security rules live in `docs/security/app-security.md` and must be applied to file handling, URL intake, subprocesses, IPC, WebView usage, model loading, updates, logging, cache handling, and export behavior.
Expand Down
1 change: 1 addition & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Last updated: 2026-03-11
- section roadmap with entries, dropouts, pickups, stops, tags, and 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
- tonight's first drop plan on the mounted map when section-level stem activity shows a corroborated density fill (previous graph 1–2 distinct sources, current graph ≥3 sources, previous sources stay, new entrance), with Open moving to the matching rendered map section. Heuristic-only topology stays unnamed. Distinct from first-breakdown, first-dropout, first-cutoff, first-stop, first-pickup, and first-turnaround.
- simplification, transposition, capo, tuning, or setup cues where applicable
- role-specific rehearsal priorities and confidence flags
- cue-sheet or chart-style exports that summarize the analysis in rehearsal-friendly form
Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- Name tonight's first drop plan in the mounted rehearsal workspace so the part that enters after a thin texture can land the full-band arrival on the map; real analyzed songs now receive this guidance only when section-level stem activity shows the previous graph with one or two distinct sources and the current graph holding at least three sources with those previous sources staying and a new entrance, while heuristic-only topology remains unavailable. Open moves to the matching rendered map section, and inherited, accessor-backed, or Proxy-substituted runtime metadata remains guidance-only instead of becoming copy, identity, timing, or navigation authority.
- 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 @@ -14,6 +15,7 @@

### Fixed

- Fail closed when duplicate rendered sections make a drop-plan navigation target ambiguous.
- 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.

## [0.1.3] - 2026-04-29
Expand Down Expand Up @@ -74,4 +76,4 @@

- `ChordsFeature` (코드 분석) 화면에서 각 파트(Role)의 `transpositionPlan`(이조/조옮김 계획)을 표시하는 기능을 추가했습니다.
- `RangesFeature` (음역대 분석) 화면에서 겹침 경고(Overlap warning) 외에 해당 파트의 채보(Transcription) 가능 노드 수를 요약하여 보여주는 기능을 추가했습니다.
- 신규 UI 요소에 대한 단위 테스트를 추가했습니다 (`apps/desktop/src/features/chords/index.test.tsx`, `apps/desktop/src/features/ranges/index.test.tsx`).
- 신규 UI 요소에 대한 단위 테스트를 추가했습니다 (`apps/desktop/src/features/chords/index.test.tsx`, `apps/desktop/src/features/ranges/index.test.tsx`).
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 drop plan 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 copy from groove, cue, simplification, overlap, range, chord labels, function labels, setup notes, transposition plans, vamp plans, fill plans, tuning plans, dynamics plans, articulation plans, hook plans, solo plans, pad plans, hit plans, cutoff plans, turnaround plans, pickup plans, breakdown plans, confirmed overrides, harmonic explanations, or confidence notes. Distinct from first-breakdown, first-dropout, first-cutoff, first-stop, first-pickup, and first-turnaround. `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
98 changes: 96 additions & 2 deletions apps/desktop/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,21 +176,49 @@ pub struct ManualOverridePayload {
source: String,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct TranscriptionNotePayload {
pitch: String,
onset: f64,
offset: f64,
velocity: f64,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
enum DropPlanSourcePayload {
Model,
User,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RehearsalRolePayload {
id: String,
name: String,
role_type: String,
harmony: HarmonyPayload,
#[serde(default, skip_serializing_if = "Option::is_none")]
harmonic_explanation: Option<String>,
cue: CuePayload,
range: RangePayload,
confidence: ConfidencePayload,
rehearsal_priority: String,
simplification: String,
setup_note: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
transposition_plan: Option<String>,
manual_overrides: Vec<ManualOverridePayload>,
overlap_warnings: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
transcription: Option<Vec<TranscriptionNotePayload>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
practice_progress: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
drop_plan: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
drop_plan_source: Option<DropPlanSourcePayload>,
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
}

#[derive(Clone, Debug, Serialize)]
Expand Down Expand Up @@ -527,9 +555,73 @@ pub fn is_youtube_video_id(value: &str) -> bool {
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-')
}

fn is_plan_whitespace(value: char) -> bool {
matches!(
value,
'\u{0009}'..='\u{000D}'
| '\u{0020}'
| '\u{0085}'
| '\u{00A0}'
| '\u{1680}'
| '\u{2000}'..='\u{200A}'
| '\u{2028}'
| '\u{2029}'
| '\u{202F}'
| '\u{205F}'
| '\u{3000}'
| '\u{FEFF}'
)
}

/// Mirrors shared-types plan validation without normalizing persisted text.
fn is_valid_drop_plan(value: &str) -> bool {
let mut has_non_whitespace = false;
for character in value.chars() {
if matches!(
character,
'\n' | '\r' | '\u{0085}' | '\u{2028}' | '\u{2029}'
) {
return false;
}
if !is_plan_whitespace(character) {
has_non_whitespace = true;
}
}
has_non_whitespace
}

fn validate_drop_plan_provenance(
payload: RehearsalSongPayload,
) -> Result<RehearsalSongPayload, String> {
for section in &payload.sections {
for role in &section.roles {
if role
.practice_progress
.is_some_and(|progress| progress > 100)
{
return Err("Invalid project file format".to_string());
}
if role
.drop_plan
.as_deref()
.is_some_and(|drop_plan| !is_valid_drop_plan(drop_plan))
{
return Err("Invalid project file format".to_string());
}
if role.drop_plan.is_none() && role.drop_plan_source.is_some() {
return Err("Invalid project file format".to_string());
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
if role.drop_plan.is_some() && role.drop_plan_source.is_none() {
return Err("Invalid project file format".to_string());
}
}
}
Ok(payload)
}

pub fn project_payload_from_content(content: &str) -> Result<RehearsalSongPayload, String> {
if let Ok(parsed) = serde_json::from_str::<RehearsalSongPayload>(content) {
return Ok(parsed);
return validate_drop_plan_provenance(parsed);
}

let payload = serde_json::from_str::<Value>(content)
Expand All @@ -547,7 +639,9 @@ pub fn project_payload_from_content(content: &str) -> Result<RehearsalSongPayloa
}
}

serde_json::from_value(payload).map_err(|_| "Invalid project file format".to_string())
let parsed =
serde_json::from_value(payload).map_err(|_| "Invalid project file format".to_string())?;
validate_drop_plan_provenance(parsed)
}

#[derive(Clone, Debug, Serialize)]
Expand Down
Loading
Loading