Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
df62f6e
test(chords): specify rehearsal guidance presentation
seonghobae Aug 6, 2026
d34b4e9
feat(chords): present actionable rehearsal guidance
seonghobae Aug 6, 2026
63d7c10
docs(changelog): record rehearsal guidance cards
seonghobae Aug 6, 2026
b50c6af
fix(security): require patched PDF.js
seonghobae Aug 11, 2026
f716201
fix(security): refresh vulnerable npm transitive pins
seonghobae Aug 11, 2026
0fb8417
fix(deps): restore manifest-lock consistency
seonghobae Aug 11, 2026
61438c9
chore(chords): remove unrelated dependency drift
seonghobae Aug 14, 2026
6230af3
ci(repair): move PR 776 guidance into the active workspace
seonghobae Aug 15, 2026
babbbd7
ci(repair): extract PR 776 repair logic from workflow YAML
seonghobae Aug 15, 2026
899087b
ci(repair): use a minimal PR 776 workflow launcher
seonghobae Aug 15, 2026
08e6f50
ci(repair): scope sentinel assertion to rehearsal guidance
seonghobae Aug 15, 2026
a3c1d1f
ci(repair): provision analysis toolchain for PR 776
seonghobae Aug 15, 2026
fb533c3
test(workspace): require reachable rehearsal guidance
Aug 15, 2026
624b2d6
fix(workspace): surface evidence-backed rehearsal guidance
Aug 15, 2026
c7c64ab
chore(ci): retrigger guidance verification
seonghobae Aug 15, 2026
8b38e48
fix(ui): address rehearsal guidance review findings
seonghobae Aug 15, 2026
8549ed1
Merge branch 'develop' into feat/chords-rehearsal-guidance-clean
seonghobae Aug 25, 2026
21ae6c2
Merge remote-tracking branch 'origin/develop' into HEAD
seonghobae Aug 26, 2026
82dfa1c
fix(workspace): deduplicate guidance case-insensitively
seonghobae Aug 29, 2026
b775679
docs(changelog): record guidance deduplication
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
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

- Display evidence-backed setup notes, simplification actions, and overlap warnings in the active rehearsal Workspace, with English/Korean action labels and no inference from blank or legacy `none` sentinel values.
- 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

- Deduplicate equivalent overlap guidance after trimming and case normalization while preserving the first source warning.
- 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`).
78 changes: 77 additions & 1 deletion apps/desktop/src/features/workspace/Workspace.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { fireEvent, render, screen, within } from "@testing-library/react";
import { createDemoRehearsalSong, type ProjectBootstrapSummary, type RehearsalSong } from "@bandscope/shared-types";
import { afterEach, describe, expect, it, vi } from "vitest";
import { Workspace } from "./Workspace";
Expand Down Expand Up @@ -221,6 +221,82 @@ describe("Workspace", () => {
expect(screen.getAllByText("Stay on roots if the chorus entrance gets muddy.").length).toBeGreaterThan(0);
});

it("shows normalized setup, simplification, and ordered overlap actions in the active workspace", () => {
setNavigatorLanguage("en-US");
const song = createDemoRehearsalSong();
song.sections[0]!.roles[0] = {
...song.sections[0]!.roles[0]!,
name: "Bass Guitar",
setupNote: " Lower the keyboard stand before the count-in. ",
simplification: " Hold roots on beats one and three. ",
overlapWarnings: [
" ",
" Leave the pickup to the lead vocal. ",
"Leave the pickup to the lead vocal.",
"LEAVE THE PICKUP TO THE LEAD VOCAL.",
"NONE",
"Double only after the chorus entrance."
]
};

render(<Workspace song={song} />);
fireEvent.click(screen.getByRole("tab", { name: "Bass Guitar" }));

const guidance = screen.getByRole("region", { name: "Actionable rehearsal guidance" });
expect(within(guidance).getByText("Set up before the take")).toBeTruthy();
expect(within(guidance).getByText("Lower the keyboard stand before the count-in.")).toBeTruthy();
expect(within(guidance).getByText("Simplify if the pass breaks down")).toBeTruthy();
expect(within(guidance).getByText("Hold roots on beats one and three.")).toBeTruthy();
expect(within(guidance).getByText("Resolve these overlaps")).toBeTruthy();

const warnings = within(guidance).getByRole("list", { name: "Resolve these overlaps" });
expect(within(warnings).getAllByRole("listitem").map((item) => item.textContent)).toEqual([
"Leave the pickup to the lead vocal.",
"Double only after the chorus entrance."
]);
expect(within(guidance).queryByText(/^none$/i)).toBeNull();
});

it("does not infer rehearsal guidance from blank or legacy sentinel evidence", () => {
setNavigatorLanguage("en-US");
const song = createDemoRehearsalSong();
song.sections[0]!.roles[0] = {
...song.sections[0]!.roles[0]!,
name: "Bass Guitar",
transpositionPlan: " none ",
setupNote: " NONE ",
simplification: " ",
overlapWarnings: ["", " none ", " "]
};

render(<Workspace song={song} />);
fireEvent.click(screen.getByRole("tab", { name: "Bass Guitar" }));

expect(screen.queryByRole("region", { name: "Actionable rehearsal guidance" })).toBeNull();
});

it("localizes actionable rehearsal guidance in Korean", () => {
setNavigatorLanguage("ko-KR");
const song = createDemoRehearsalSong();
song.sections[0]!.roles[0] = {
...song.sections[0]!.roles[0]!,
name: "Bass Guitar",
setupNote: "앰프 게인을 먼저 낮추세요.",
simplification: "첫 박의 근음만 유지하세요.",
overlapWarnings: ["보컬 픽업과 겹치지 않게 쉬세요."]
};

render(<Workspace song={song} />);
fireEvent.click(screen.getByRole("tab", { name: "Bass Guitar" }));

const guidance = screen.getByRole("region", { name: "실행 가능한 합주 가이드" });
expect(within(guidance).getByText("연주 전에 준비하세요")).toBeTruthy();
expect(within(guidance).getByText("합주가 흔들리면 이렇게 단순화하세요")).toBeTruthy();
expect(within(guidance).getByText("이 겹침을 먼저 해결하세요")).toBeTruthy();
const warnings = within(guidance).getByRole("list", { name: "이 겹침을 먼저 해결하세요" });
expect(within(warnings).getByText("보컬 픽업과 겹치지 않게 쉬세요.")).toBeTruthy();
});

it("exports a metadata-only handoff artifact from the workspace", async () => {
const song = createDemoRehearsalSong();
const sourceBootstrap: ProjectBootstrapSummary = {
Expand Down
104 changes: 91 additions & 13 deletions apps/desktop/src/features/workspace/Workspace.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useMemo, memo, type MouseEvent } from "react";
import { useState, useMemo, useId, memo, type MouseEvent } from "react";
import { parseProjectBootstrapSummary, type ProjectBootstrapSummary, type RehearsalSong, type RehearsalRole } from "@bandscope/shared-types";
import { RoleSwitcher } from "./RoleSwitcher";
import { SectionRoadmap } from "./SectionRoadmap";
Expand Down Expand Up @@ -52,12 +52,39 @@ function formatStatusLabel(status: string): string {
return status.replaceAll("_", " ");
}

/** Documented. */
/** Return trimmed source text when the analysis supplied nonblank evidence. */
function nonBlankText(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
}

/**
* Return buyer-visible guidance only when the producer supplied meaningful evidence.
*
* Historical analysis payloads used case-insensitive `none` text as an absence
* sentinel. The active Workspace must not turn that missing evidence into an
* instruction, warning, or transposition plan.
*/
function actionableGuidanceText(value: string | undefined): string | undefined {
const normalized = nonBlankText(value);
return normalized?.toLowerCase() === "none" ? undefined : normalized;
}

/** Preserve unique meaningful overlap-warning order without mutating analysis output. */
function actionableOverlapWarnings(values: readonly string[]): string[] {
const warnings: string[] = [];
const seen = new Set<string>();
for (const value of values) {
const normalized = actionableGuidanceText(value);
const dedupeKey = normalized?.toLowerCase();
if (normalized && dedupeKey && !seen.has(dedupeKey)) {
seen.add(dedupeKey);
warnings.push(normalized);
}
}
return warnings;
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

/** Documented. */
function safeProjectBootstrapSummary(value: ProjectBootstrapSummary | null): ProjectBootstrapSummary | null {
if (!value) {
Expand Down Expand Up @@ -122,6 +149,7 @@ const SongStructure = memo(function SongStructure({ sections, t }: { sections: R
export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: WorkspaceProps) {
const [activeRole, setActiveRole] = useState<string | null>(null);
const t = useMemo(() => createTranslator(detectPreferredLocale()), []);
const overlapWarningsHeadingId = useId();

// Extract all unique roles from the song's sections
const roleMap = useMemo(() => {
Expand Down Expand Up @@ -222,9 +250,13 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp
nonBlankText(activeRoleDetails?.harmonicExplanation) ??
nonBlankText(activeRoleDetails?.harmony.functionLabel) ??
t("workspaceHarmonyExplainFallback");
const roleTranspositionPlan =
nonBlankText(activeRoleDetails?.transpositionPlan) ??
nonBlankText(activeRoleDetails?.simplification);
const roleTranspositionPlan = actionableGuidanceText(activeRoleDetails?.transpositionPlan);
const roleSetupNote = actionableGuidanceText(activeRoleDetails?.setupNote);
const roleSimplification = actionableGuidanceText(activeRoleDetails?.simplification);
const roleOverlapWarnings = actionableOverlapWarnings(activeRoleDetails?.overlapWarnings ?? []);
const hasActionableGuidance = Boolean(
roleSetupNote || roleSimplification || roleOverlapWarnings.length > 0
);
Comment thread
seonghobae marked this conversation as resolved.

/** Documented. */
const handleExportCueSheet = () => {
Expand Down Expand Up @@ -438,16 +470,62 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp
{roleHarmonicExplanation}
</p>
</div>
<div className="rounded-xl border border-indigo-300/20 bg-indigo-300/[0.08] p-3">
<div className="flex items-center gap-2 text-indigo-100">
<ClipboardList className="size-4" aria-hidden="true" />
<p className="text-[0.7rem] font-black uppercase tracking-[0.22em]">{t("workspaceTranspositionLabel")}</p>
{roleTranspositionPlan && (
<div className="rounded-xl border border-indigo-300/20 bg-indigo-300/[0.08] p-3">
<div className="flex items-center gap-2 text-indigo-100">
<ClipboardList className="size-4" aria-hidden="true" />
<p className="text-[0.7rem] font-black uppercase tracking-[0.22em]">{t("workspaceTranspositionLabel")}</p>
</div>
<p className="mt-2 text-sm leading-6 text-slate-200">
{roleTranspositionPlan}
</p>
</div>
<p className="mt-2 text-sm leading-6 text-slate-200">
{roleTranspositionPlan}
</p>
</div>
)}
</div>
{hasActionableGuidance && (
<section
role="region"
aria-label={t("workspaceGuidanceRegionLabel")}
className="mt-4 rounded-2xl border border-amber-300/20 bg-amber-300/[0.05] p-3"
>
<div className="grid gap-3 lg:grid-cols-3">
{roleSetupNote && (
<article className="rounded-xl border border-teal-300/20 bg-teal-300/[0.07] p-3">
<h4 className="text-[0.7rem] font-black uppercase tracking-[0.2em] text-teal-100">
{t("workspaceSetupLabel")}
</h4>
<p className="mt-2 text-sm leading-6 text-slate-100">{roleSetupNote}</p>
</article>
)}
{roleSimplification && (
<article className="rounded-xl border border-violet-300/20 bg-violet-300/[0.07] p-3">
<h4 className="text-[0.7rem] font-black uppercase tracking-[0.2em] text-violet-100">
{t("workspaceSimplificationLabel")}
</h4>
<p className="mt-2 text-sm leading-6 text-slate-100">{roleSimplification}</p>
</article>
)}
{roleOverlapWarnings.length > 0 && (
<article className="rounded-xl border border-rose-300/20 bg-rose-300/[0.07] p-3">
<h4
id={overlapWarningsHeadingId}
className="text-[0.7rem] font-black uppercase tracking-[0.2em] text-rose-100"
>
{t("workspaceOverlapWarningsLabel")}
</h4>
<ul
aria-labelledby={overlapWarningsHeadingId}
className="mt-2 list-disc space-y-1 pl-5 text-sm leading-6 text-slate-100"
>
Comment thread
seonghobae marked this conversation as resolved.
{roleOverlapWarnings.map((warning, warningIndex) => (
<li key={`${activeRole}-${warningIndex}-${warning}`}>{warning}</li>
))}
</ul>
</article>
)}
</div>
</section>
)}
{song.collaboration && (
<div className="mt-4 grid gap-3 xl:grid-cols-3">
<div className="rounded-xl border border-white/10 bg-white/[0.04] p-3">
Expand Down
6 changes: 5 additions & 1 deletion apps/desktop/src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@
"workspaceApprovalsLabel": "Approvals",
"workspaceHarmonyExplainLabel": "Why it works",
"workspaceHarmonyExplainFallback": "The role-specific harmonic reason will appear here after the room confirms it.",
"workspaceTranspositionLabel": "Transpose / simplify",
"workspaceTranspositionLabel": "Transpose",
"workspaceGuidanceRegionLabel": "Actionable rehearsal guidance",
"workspaceSetupLabel": "Set up before the take",
"workspaceSimplificationLabel": "Simplify if the pass breaks down",
"workspaceOverlapWarningsLabel": "Resolve these overlaps",
"workspaceStemsLabel": "Stems",
"workspaceRehearsalPrioritiesLabel": "Rehearsal Priorities",
"workspaceRolesHarmonyLabel": "Roles & Harmony",
Expand Down
6 changes: 5 additions & 1 deletion apps/desktop/src/locales/ko/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@
"workspaceApprovalsLabel": "승인",
"workspaceHarmonyExplainLabel": "이 화성이 먹히는 이유",
"workspaceHarmonyExplainFallback": "역할별 화성 이유는 합주실에서 확인되면 여기에 정리됩니다.",
"workspaceTranspositionLabel": "전조 / 단순화",
"workspaceTranspositionLabel": "전조",
"workspaceGuidanceRegionLabel": "실행 가능한 합주 가이드",
"workspaceSetupLabel": "연주 전에 준비하세요",
"workspaceSimplificationLabel": "합주가 흔들리면 이렇게 단순화하세요",
"workspaceOverlapWarningsLabel": "이 겹침을 먼저 해결하세요",
"workspaceStemsLabel": "스템",
"workspaceRehearsalPrioritiesLabel": "합주 우선순위",
"workspaceRolesHarmonyLabel": "역할과 화성",
Expand Down
Loading