diff --git a/AGENTS.md b/AGENTS.md
index b9a67ce17..4b7e0d06a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,7 +1,7 @@
# AGENTS.md
## 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.
+- 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. The ready workspace names tonight's first playable range, and a selected part with a room-confirmed harmony override names that chord as the next lock-in.
- 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.
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index ca0df5ac4..49c64e2ac 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -79,7 +79,7 @@ Last updated: 2026-03-11
## Rehearsal outputs
- Core rehearsal artifacts should include:
- - likely harmony by section and by role
+ - likely harmony by section and by role, with a selected part naming a room-confirmed override chord as the next lock-in
- 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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b6f7e784..f0db24b21 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
### Added
- 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.
+- Name a selected part's room-confirmed harmony override and tell the player to lock that chord before the section.
- Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace.
- 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함.
diff --git a/CLAUDE.md b/CLAUDE.md
index b5a34c1fa..8a3e44add 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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 ready workspace names tonight's first playable range and the next instrument check; a selected part with a room-confirmed harmony override also names that chord as the next lock-in. `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.
diff --git a/apps/desktop/src/features/workspace/Workspace.confirmed-chord.test.tsx b/apps/desktop/src/features/workspace/Workspace.confirmed-chord.test.tsx
new file mode 100644
index 000000000..7850a7a76
--- /dev/null
+++ b/apps/desktop/src/features/workspace/Workspace.confirmed-chord.test.tsx
@@ -0,0 +1,53 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { createDemoRehearsalSong } from "@bandscope/shared-types";
+import { afterEach, describe, expect, it } from "vitest";
+import { Workspace } from "./Workspace";
+
+const originalLanguage = navigator.language;
+
+function setNavigatorLanguage(language: string) {
+ Object.defineProperty(navigator, "language", {
+ configurable: true,
+ value: language
+ });
+}
+
+describe("Workspace selected-part confirmed chord", () => {
+ afterEach(() => {
+ setNavigatorLanguage(originalLanguage);
+ });
+
+ it("stays hidden until a part with a room-confirmed chord is selected", () => {
+ setNavigatorLanguage("en-US");
+ render();
+
+ expect(screen.queryByTestId("selected-part-confirmed-chord")).toBeNull();
+
+ fireEvent.click(screen.getByRole("tab", { name: "Bass Guitar" }));
+ expect(screen.queryByTestId("selected-part-confirmed-chord")).toBeNull();
+ });
+
+ it("names the selected part's confirmed chord and the next lock-in action", () => {
+ setNavigatorLanguage("en-US");
+ render();
+
+ fireEvent.click(screen.getByRole("tab", { name: "Lead Vocal" }));
+
+ const callout = screen.getByTestId("selected-part-confirmed-chord");
+ expect(callout).toHaveTextContent("Tonight's confirmed chord");
+ expect(callout).toHaveTextContent(
+ "Lead Vocal uses the room's C#m11 in verse. Lock that chord before the verse."
+ );
+ });
+
+ it("keeps Korean copy particle-safe for Latin role names", () => {
+ setNavigatorLanguage("ko-KR");
+ render();
+
+ fireEvent.click(screen.getByRole("tab", { name: "Lead Vocal" }));
+
+ expect(screen.getByTestId("selected-part-confirmed-chord")).toHaveTextContent(
+ "verse의 Lead Vocal 파트는 방이 확인한 C#m11를 씁니다. verse 전에 그 코드를 고정하세요."
+ );
+ });
+});
diff --git a/apps/desktop/src/features/workspace/Workspace.tsx b/apps/desktop/src/features/workspace/Workspace.tsx
index d44e20777..485a78e60 100644
--- a/apps/desktop/src/features/workspace/Workspace.tsx
+++ b/apps/desktop/src/features/workspace/Workspace.tsx
@@ -5,6 +5,7 @@ import { SectionRoadmap } from "./SectionRoadmap";
import { GrooveMap } from "./GrooveMap";
import { PracticeProgress } from "./PracticeProgress";
import { fillRangeCopy, firstRangeSqueeze } from "./firstRangeSqueeze";
+import { fillConfirmedChordCopy, selectedPartConfirmedChord } from "./selectedPartConfirmedChord";
import { createTranslator, detectPreferredLocale } from "../../i18n";
import { generateCueSheetCsv, generateChartSummaryJson, generateMetadataHandoffJson, sanitizeFilename } from "../../lib/export";
import { Button } from "@/components/ui/button";
@@ -163,6 +164,17 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp
}
)
: t("workspaceFirstRangeMissing");
+ const confirmedChord = useMemo(
+ () => selectedPartConfirmedChord(song, activeRole),
+ [activeRole, song]
+ );
+ const confirmedChordCopy = confirmedChord
+ ? fillConfirmedChordCopy(t("workspaceConfirmedChordLock"), {
+ roleName: confirmedChord.roleName,
+ chord: confirmedChord.chord,
+ sectionLabel: confirmedChord.sectionLabel
+ })
+ : null;
/** Handle the practice progress change internally by immutably updating the song state. */
const handlePracticeProgressChange = (newProgress: number) => {
@@ -310,6 +322,17 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp
{firstRangeCopy}
+ {confirmedChordCopy ? (
+
+ {t("workspaceConfirmedChordTitle")}
+ {confirmedChordCopy}
+
+ ) : null}
+
{t("workspaceSongTimelineLabel")}
diff --git a/apps/desktop/src/features/workspace/selectedPartConfirmedChord.test.ts b/apps/desktop/src/features/workspace/selectedPartConfirmedChord.test.ts
new file mode 100644
index 000000000..c4f83c3c0
--- /dev/null
+++ b/apps/desktop/src/features/workspace/selectedPartConfirmedChord.test.ts
@@ -0,0 +1,185 @@
+import { createDemoRehearsalSong, type RehearsalSong } from "@bandscope/shared-types";
+import { describe, expect, it } from "vitest";
+import {
+ fillConfirmedChordCopy,
+ selectedPartConfirmedChord
+} from "./selectedPartConfirmedChord";
+
+function withSelectedOverride(
+ song: RehearsalSong,
+ roleId: string,
+ chord: string | null,
+ extras: Partial = {}
+): RehearsalSong {
+ return {
+ ...song,
+ sections: song.sections.map((section) => ({
+ ...section,
+ roles: section.roles.map((role) => {
+ if (role.id !== roleId) {
+ return role;
+ }
+ return {
+ ...role,
+ ...extras,
+ manualOverrides:
+ chord === null
+ ? []
+ : [
+ {
+ field: "harmony" as const,
+ value: {
+ chord,
+ functionLabel: "user confirmed",
+ source: "user" as const
+ },
+ source: "user" as const
+ }
+ ]
+ };
+ })
+ }))
+ };
+}
+
+describe("selectedPartConfirmedChord", () => {
+ it("names the selected part's first own user harmony override", () => {
+ expect(selectedPartConfirmedChord(createDemoRehearsalSong(), "lead-vocal")).toEqual({
+ sectionLabel: "verse",
+ roleName: "Lead Vocal",
+ chord: "C#m11"
+ });
+ });
+
+ it("stays hidden until a named part is selected", () => {
+ expect(selectedPartConfirmedChord(createDemoRehearsalSong(), null)).toBeNull();
+ expect(selectedPartConfirmedChord(createDemoRehearsalSong(), " ")).toBeNull();
+ });
+
+ it("stays hidden when the selected part has no trusted override", () => {
+ expect(selectedPartConfirmedChord(createDemoRehearsalSong(), "bass-guitar")).toBeNull();
+ expect(
+ selectedPartConfirmedChord(withSelectedOverride(createDemoRehearsalSong(), "bass-guitar", "none"), "bass-guitar")
+ ).toBeNull();
+ });
+
+ it("skips inherited, model, and non-harmony overrides", () => {
+ const song = createDemoRehearsalSong();
+ const bass = song.sections[0]!.roles[0]!;
+ const inherited = Object.create({
+ manualOverrides: [
+ {
+ field: "harmony",
+ value: { chord: "G", functionLabel: "inherited", source: "user" },
+ source: "user"
+ }
+ ]
+ }) as typeof bass;
+ Object.assign(inherited, { ...bass, manualOverrides: undefined });
+ delete (inherited as { manualOverrides?: unknown }).manualOverrides;
+ song.sections[0]!.roles[0] = inherited;
+
+ expect(selectedPartConfirmedChord(song, "bass-guitar")).toBeNull();
+
+ const modelOnly = withSelectedOverride(createDemoRehearsalSong(), "bass-guitar", "E3");
+ modelOnly.sections[0]!.roles[0] = {
+ ...modelOnly.sections[0]!.roles[0]!,
+ manualOverrides: [
+ {
+ field: "harmony",
+ value: {
+ chord: "Gmaj7",
+ functionLabel: "model leftover",
+ source: "model"
+ },
+ source: "model"
+ }
+ ]
+ };
+
+ expect(selectedPartConfirmedChord(modelOnly, "bass-guitar")).toBeNull();
+ });
+
+ it("fails closed on conflicting role copies and sparse collections", () => {
+ const conflict = createDemoRehearsalSong();
+ conflict.sections.push({
+ ...conflict.sections[0]!,
+ id: "verse-2",
+ roles: conflict.sections[0]!.roles.map((role) =>
+ role.id === "lead-vocal" ? { ...role, name: "Lead Vox" } : role
+ )
+ });
+ expect(selectedPartConfirmedChord(conflict, "lead-vocal")).toBeNull();
+
+ const chordConflict = createDemoRehearsalSong();
+ chordConflict.sections.push({
+ ...chordConflict.sections[0]!,
+ id: "chorus-1",
+ label: "chorus",
+ roles: chordConflict.sections[0]!.roles.map((role) =>
+ role.id === "lead-vocal"
+ ? {
+ ...role,
+ manualOverrides: [
+ {
+ field: "harmony" as const,
+ value: {
+ chord: "Bmaj7",
+ functionLabel: "other copy",
+ source: "user" as const
+ },
+ source: "user" as const
+ }
+ ]
+ }
+ : role
+ )
+ });
+ expect(selectedPartConfirmedChord(chordConflict, "lead-vocal")).toBeNull();
+
+ const sparse = createDemoRehearsalSong() as unknown as { sections: unknown[] };
+ sparse.sections = [];
+ sparse.sections[1] = createDemoRehearsalSong().sections[0];
+ expect(selectedPartConfirmedChord(sparse as unknown as RehearsalSong, "lead-vocal")).toBeNull();
+ });
+
+ it("fails closed on malformed roots, traps, and non-canonical labels", () => {
+ expect(selectedPartConfirmedChord(null as unknown as RehearsalSong, "lead-vocal")).toBeNull();
+ expect(selectedPartConfirmedChord({} as RehearsalSong, "lead-vocal")).toBeNull();
+
+ const trap = new Proxy(createDemoRehearsalSong(), {
+ has() {
+ throw new Error("has trap");
+ },
+ get(target, property, receiver) {
+ if (property === "sections") {
+ throw new Error("get trap");
+ }
+ return Reflect.get(target, property, receiver);
+ }
+ });
+ expect(selectedPartConfirmedChord(trap, "lead-vocal")).toBeNull();
+
+ const unknownLabel = createDemoRehearsalSong();
+ unknownLabel.sections[0] = { ...unknownLabel.sections[0]!, label: "vibe-check" as typeof unknownLabel.sections[0]["label"] };
+ expect(selectedPartConfirmedChord(unknownLabel, "lead-vocal")).toBeNull();
+ });
+});
+
+describe("fillConfirmedChordCopy", () => {
+ it("keeps placeholder-shaped chords literal", () => {
+ expect(
+ fillConfirmedChordCopy("{roleName} locks {chord} before {sectionLabel}.", {
+ roleName: "Lead Vocal",
+ chord: "C#m11 {sectionLabel}",
+ sectionLabel: "verse"
+ })
+ ).toBe("Lead Vocal locks C#m11 {sectionLabel} before verse.");
+ });
+
+ it("does not satisfy tokens with inherited object members", () => {
+ expect(fillConfirmedChordCopy("Use {toString} in {missingToken}.", { chord: "C#m11" })).toBe(
+ "Use {toString} in {missingToken}."
+ );
+ });
+});
diff --git a/apps/desktop/src/features/workspace/selectedPartConfirmedChord.ts b/apps/desktop/src/features/workspace/selectedPartConfirmedChord.ts
new file mode 100644
index 000000000..17705cc70
--- /dev/null
+++ b/apps/desktop/src/features/workspace/selectedPartConfirmedChord.ts
@@ -0,0 +1,173 @@
+import type { RehearsalSong } from "@bandscope/shared-types";
+import { fillRangeCopy, meaningfulRangeText } from "./firstRangeSqueeze";
+
+/** Room-confirmed chord a selected part should lock before the section. */
+export type SelectedPartConfirmedChord = {
+ sectionLabel: string;
+ roleName: string;
+ chord: string;
+};
+
+const CANONICAL_SECTION_LABELS = new Set([
+ "intro",
+ "verse",
+ "pre-chorus",
+ "chorus",
+ "bridge",
+ "outro",
+ "tag",
+ "pickup",
+ "stop",
+ "handoff"
+]);
+
+/** Return whether an untrusted runtime value is a plain object record. */
+function isRuntimeObject(value: unknown): value is Record {
+ return typeof value === "object" && value !== null && !Array.isArray(value);
+}
+
+/** Read an own data property and contain throwing membership or getter traps. */
+function ownValue(record: object, key: string): unknown {
+ try {
+ if (!Object.prototype.hasOwnProperty.call(record, key)) {
+ return undefined;
+ }
+ return (record as Record)[key];
+ } catch {
+ return undefined;
+ }
+}
+
+/** Admit a dense array or fail closed on holes and non-arrays. */
+function denseArray(value: unknown): unknown[] | null {
+ if (!Array.isArray(value)) {
+ return null;
+ }
+
+ for (let index = 0; index < value.length; index += 1) {
+ if (!Object.prototype.hasOwnProperty.call(value, index)) {
+ return null;
+ }
+ }
+
+ return value;
+}
+
+/** Pull the first trusted user harmony chord from own override records. */
+function ownHarmonyOverrideChord(roleValue: object): string | undefined {
+ const overrides = denseArray(ownValue(roleValue, "manualOverrides"));
+ if (!overrides) {
+ return undefined;
+ }
+
+ for (const item of overrides) {
+ if (!isRuntimeObject(item)) {
+ continue;
+ }
+ if (ownValue(item, "field") !== "harmony") {
+ continue;
+ }
+ if (ownValue(item, "source") !== "user") {
+ continue;
+ }
+
+ const overrideValue = ownValue(item, "value");
+ if (!isRuntimeObject(overrideValue) || ownValue(overrideValue, "source") !== "user") {
+ continue;
+ }
+
+ const chord = meaningfulRangeText(ownValue(overrideValue, "chord"));
+ if (chord) {
+ return chord;
+ }
+ }
+
+ return undefined;
+}
+
+/**
+ * Pick the selected part's first room-confirmed harmony chord.
+ *
+ * Hidden until a named part is selected. Only own user harmony overrides
+ * become buyer-visible chord authority. Conflicting role copies, inherited
+ * prototypes, sparse collections, and non-canonical section labels fail
+ * closed instead of inventing a rehearsal chord.
+ */
+export function selectedPartConfirmedChord(
+ song: RehearsalSong,
+ activeRole: string | null
+): SelectedPartConfirmedChord | null {
+ const selectedRoleId = meaningfulRangeText(activeRole);
+ if (!selectedRoleId) {
+ return null;
+ }
+
+ const runtimeSong: unknown = song;
+ if (!isRuntimeObject(runtimeSong)) {
+ return null;
+ }
+
+ const sections = denseArray(ownValue(runtimeSong, "sections"));
+ if (!sections) {
+ return null;
+ }
+
+ let found: SelectedPartConfirmedChord | null = null;
+ let seenName: string | undefined;
+
+ for (const sectionValue of sections) {
+ if (!isRuntimeObject(sectionValue)) {
+ continue;
+ }
+
+ const sectionLabel = meaningfulRangeText(ownValue(sectionValue, "label"));
+ if (!sectionLabel || !CANONICAL_SECTION_LABELS.has(sectionLabel)) {
+ continue;
+ }
+
+ const roles = denseArray(ownValue(sectionValue, "roles"));
+ if (!roles) {
+ continue;
+ }
+
+ for (const roleValue of roles) {
+ if (!isRuntimeObject(roleValue)) {
+ continue;
+ }
+
+ const roleId = meaningfulRangeText(ownValue(roleValue, "id"));
+ const roleName = meaningfulRangeText(ownValue(roleValue, "name"));
+ if (!roleId || !roleName || roleId !== selectedRoleId) {
+ continue;
+ }
+
+ if (seenName && seenName !== roleName) {
+ return null;
+ }
+ seenName = roleName;
+
+ const chord = ownHarmonyOverrideChord(roleValue);
+ if (!chord) {
+ continue;
+ }
+
+ if (found && found.chord !== chord) {
+ return null;
+ }
+
+ if (!found) {
+ found = { sectionLabel, roleName, chord };
+ }
+ }
+ }
+
+ return found;
+}
+
+/** Fill trusted `{token}` placeholders for confirmed-chord copy. */
+export function fillConfirmedChordCopy(
+ template: string,
+ values: Record
+): string {
+ return fillRangeCopy(template, values);
+}
diff --git a/apps/desktop/src/locales/en/common.json b/apps/desktop/src/locales/en/common.json
index d803a765e..c319594b6 100644
--- a/apps/desktop/src/locales/en/common.json
+++ b/apps/desktop/src/locales/en/common.json
@@ -153,6 +153,8 @@
"workspaceFirstRangeCheck": "{roleName} sits {lowestNote}–{highestNote} in {sectionLabel}. Check that span on your instrument before the {sectionLabel}.",
"workspaceFirstRangeClash": "{roleName} sits {lowestNote}–{highestNote} in {sectionLabel}. Hear that clash on your instrument before the {sectionLabel}.",
"workspaceFirstRangeMissing": "Tonight's first range still needs an ear check. Confirm the high and low notes on the selected part before the first section.",
+ "workspaceConfirmedChordTitle": "Tonight's confirmed chord",
+ "workspaceConfirmedChordLock": "{roleName} uses the room's {chord} in {sectionLabel}. Lock that chord before the {sectionLabel}.",
"sectionRangeLabel": "Range",
"sectionRangeNextAction": "Check this span on your instrument before {sectionLabel}."
}
diff --git a/apps/desktop/src/locales/ko/common.json b/apps/desktop/src/locales/ko/common.json
index 0f6c6c66d..6dca8842d 100644
--- a/apps/desktop/src/locales/ko/common.json
+++ b/apps/desktop/src/locales/ko/common.json
@@ -153,6 +153,8 @@
"workspaceFirstRangeCheck": "{sectionLabel}의 {roleName}은 {lowestNote}–{highestNote}입니다. {sectionLabel} 들어가기 전에 그 음역을 악기로 확인해 보세요.",
"workspaceFirstRangeClash": "{sectionLabel}의 {roleName}은 {lowestNote}–{highestNote}이고 다른 파트와 겹칩니다. {sectionLabel} 들어가기 전에 그 충돌을 악기로 들어 보세요.",
"workspaceFirstRangeMissing": "오늘 먼저 볼 음역은 아직 귀로 확인이 필요합니다. 선택한 파트의 최저·최고음을 첫 구간 전에 확인해 보세요.",
+ "workspaceConfirmedChordTitle": "오늘 방이 확인한 코드",
+ "workspaceConfirmedChordLock": "{sectionLabel}의 {roleName} 파트는 방이 확인한 {chord}를 씁니다. {sectionLabel} 전에 그 코드를 고정하세요.",
"sectionRangeLabel": "음역",
"sectionRangeNextAction": "{sectionLabel} 들어가기 전에 이 음역을 악기로 확인해 보세요."
}
diff --git a/docs/design-system/component-contract.md b/docs/design-system/component-contract.md
index 22602c313..f0f89d94d 100644
--- a/docs/design-system/component-contract.md
+++ b/docs/design-system/component-contract.md
@@ -35,6 +35,7 @@ The authoritative Figma view is `31 Component Contract Catalog`. This file mirro
| Source Control Stack | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=19-655 | `apps/desktop/src/App.tsx` | Feature-local source controls for local audio, YouTube URL import, project actions, and Start Analysis; keep before metrics at 375px. |
| Export Action Group | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=19-731 | `apps/desktop/src/features/workspace/Workspace.tsx` | Feature-local export buttons call `handleExportCueSheet`, `handleExportChart`, and `handleExportHandoff`. |
| Workspace State Matrix | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=99-560 | `apps/desktop/src/features/workspace/WorkspaceStates.tsx`, `apps/desktop/src/App.tsx` | Whole-workspace empty, loading, error, and ready state routing; use before changing `renderWorkspaceState()`. |
+| Selected-part confirmed chord | Ready workspace callout; Figma page `31 Component Contract Catalog` / Status Pill `19-283` for tone | `apps/desktop/src/features/workspace/Workspace.tsx`, `apps/desktop/src/features/workspace/selectedPartConfirmedChord.ts` | Hidden until a named part is selected. Names the first trusted user harmony override and the next lock-in. |
## Prop And State Mapping
diff --git a/docs/doctoring/selected-part-confirmed-chord.md b/docs/doctoring/selected-part-confirmed-chord.md
new file mode 100644
index 000000000..f994d7039
--- /dev/null
+++ b/docs/doctoring/selected-part-confirmed-chord.md
@@ -0,0 +1,37 @@
+# Selected-part confirmed chord
+
+## Product decision
+
+After a named part is selected, the ready rehearsal workspace names that part's first trusted user harmony override and tells the player to lock the room-confirmed chord before the section. The callout stays hidden until a part is selected and stays hidden when the part has no trusted override.
+
+This is selected-part confirmed-chord guidance only. It does not replace:
+
+- song-wide first confirmed chord ownership (`#1002`)
+- selected-part entrance cue (`#1150`)
+- selected-part first-pass simplification (`#1151`)
+- setup-before-entrance (`#910`)
+- Active Player (`#961`)
+- MIR / known-stem ownership (`#828` / `#770`)
+
+## Buyer-visible next action
+
+- Lead Vocal: **Lead Vocal uses the room's C#m11 in verse. Lock that chord before the verse.**
+- Bass Guitar / Keyboard: no callout, because those demo parts have no user harmony override.
+- Korean copy keeps Latin role names particle-safe (`Lead Vocal 파트는`).
+
+## Trust boundary
+
+- Untrusted input: in-memory project `manualOverrides`, role identity, section labels, and chord strings.
+- Own-property admission only. Inherited `manualOverrides`, throwing `has`/`get` traps, sparse arrays, and non-object members fail closed.
+- Only `field: "harmony"` overrides with `source: "user"` and a non-blank, non-`none` chord become buyer copy.
+- Only shared canonical section labels (`intro` through `handoff`) become localization authority.
+- Duplicate selected-role ids with conflicting display names or conflicting override chords fail closed.
+- `fillConfirmedChordCopy` uses own-property token lookup so inherited members such as `toString` cannot render function source, and placeholder-shaped chords stay literal.
+
+## Security Notes
+
+- Attack surface: rehearsal workspace UI copy from in-memory analysis output. No new file, URL, subprocess, IPC, WebView, model, credential, or export path.
+- Trust boundary: browser/React state → selector → translated callout.
+- Safe failure: missing selection, missing override, malformed runtime evidence, and conflicting copies hide the callout instead of inventing a chord.
+- Privacy: chord symbols and role names remain rehearsal display data already present in the project; nothing is logged or exported by this slice.
+- Test points: demo Lead Vocal override, hidden-until-selected, missing/`none` overrides, inherited/model overrides, conflicting copies, sparse collections, getter traps, non-canonical labels, and literal placeholder-shaped chords.