From f180e790c6ef3189700fcae3a5af947f08529bc9 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 29 Jun 2026 05:28:38 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20[=EB=B9=84?= =?UTF-8?q?=ED=99=9C=EC=84=B1=ED=99=94=EB=90=9C=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=EC=97=90=20=EC=9D=B4=EC=9C=A0=EB=A5=BC=20=EC=84=A4=EB=AA=85?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=ED=88=B4=ED=8C=81=20=EC=B6=94=EA=B0=80]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App.tsx의 주요 작업 버튼이 비활성화되었을 때, 이를 감싸는 `` 태그를 추가하여 `tabIndex={0}` 및 `title` 속성을 적용했습니다. - 이를 통해 키보드 사용자 및 마우스 사용자가 버튼이 비활성화된 이유를 명확하게 알 수 있도록 접근성을 개선했습니다. - 한국어 및 영어 다국어 번역 문자열(common.json)에 비활성화 관련 이유를 설명하는 문구를 추가했습니다. --- .Jules/palette.md | 3 + apps/desktop/src/App.tsx | 128 +++++++++++++++--------- apps/desktop/src/locales/en/common.json | 7 +- apps/desktop/src/locales/ko/common.json | 7 +- 4 files changed, 94 insertions(+), 51 deletions(-) diff --git a/.Jules/palette.md b/.Jules/palette.md index f532079a..02b557f8 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -15,3 +15,6 @@ ## 2026-06-19 - Internationalization **Learning:** The desktop app uses i18n via json files located in `apps/desktop/src/locales/` **Action:** When adding new text strings, make sure to add it to all locale files. +## 2026-06-22 - Disabled button tooltips accessibility +**Learning:** Native `disabled` HTML attributes prevent elements from receiving keyboard focus and can block hover events in some browsers. Consequently, placing a `title` attribute directly on a disabled ` + +
- - + + - + - {isStarting ? ( -
diff --git a/apps/desktop/src/locales/en/common.json b/apps/desktop/src/locales/en/common.json index 4e098662..bd8b4d14 100644 --- a/apps/desktop/src/locales/en/common.json +++ b/apps/desktop/src/locales/en/common.json @@ -65,5 +65,10 @@ "youtubePlaceholder": "YouTube URL...", "importYoutube": "Import YouTube", "importingYoutube": "Importing...", - "youtubeImportFailed": "Failed to import YouTube URL." + "youtubeImportFailed": "Failed to import YouTube URL.", + "actionDisabledAnalysis": "Disabled during analysis", + "actionDisabledImporting": "Disabled while importing", + "saveProjectDisabledNoResult": "Nothing to save yet", + "startAnalysisDisabledNoAudio": "Choose or import audio first", + "importYoutubeDisabledEmpty": "Enter a valid YouTube URL first" } diff --git a/apps/desktop/src/locales/ko/common.json b/apps/desktop/src/locales/ko/common.json index 2b59d5ee..a683aa11 100644 --- a/apps/desktop/src/locales/ko/common.json +++ b/apps/desktop/src/locales/ko/common.json @@ -65,5 +65,10 @@ "youtubePlaceholder": "유튜브 URL...", "importYoutube": "유튜브 가져오기", "importingYoutube": "가져오는 중...", - "youtubeImportFailed": "유튜브 URL 가져오기에 실패했습니다." + "youtubeImportFailed": "유튜브 URL 가져오기에 실패했습니다.", + "actionDisabledAnalysis": "분석 중에는 비활성화됩니다", + "actionDisabledImporting": "가져오는 중에는 비활성화됩니다", + "saveProjectDisabledNoResult": "아직 저장할 프로젝트가 없습니다", + "startAnalysisDisabledNoAudio": "먼저 오디오를 선택하거나 가져오세요", + "importYoutubeDisabledEmpty": "먼저 유효한 유튜브 URL을 입력하세요" } From db32dfdb7e4bd1341edcc59a975b85627bd1f7e6 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 1 Jul 2026 03:50:37 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20[=EB=B9=84?= =?UTF-8?q?=ED=99=9C=EC=84=B1=ED=99=94=EB=90=9C=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=EC=97=90=20=EC=9D=B4=EC=9C=A0=EB=A5=BC=20=EC=84=A4=EB=AA=85?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=ED=88=B4=ED=8C=81=20=EC=B6=94=EA=B0=80]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App.tsx의 주요 작업 버튼이 비활성화되었을 때, 이를 감싸는 `` 태그를 추가하여 `tabIndex={0}` 및 `title` 속성을 적용했습니다. - 이를 통해 키보드 사용자 및 마우스 사용자가 버튼이 비활성화된 이유를 명확하게 알 수 있도록 접근성을 개선했습니다. - 한국어 및 영어 다국어 번역 문자열(common.json)에 비활성화 관련 이유를 설명하는 문구를 추가했습니다. --- .Jules/palette.md | 17 +- .github/workflows/bandit.yml | 2 +- .github/workflows/build-baseline.yml | 10 +- .github/workflows/ci.yml | 4 +- .github/workflows/codeql.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/opencode-review.yml | 2630 ------------- .github/workflows/ossf-scorecard.yml | 6 +- .../workflows/pr-review-merge-scheduler.yml | 98 - .github/workflows/release.yml | 2 +- .github/workflows/sbom.yml | 4 +- .github/workflows/secret-scan-gate.yml | 2 +- .github/workflows/security-audit.yml | 2 +- .github/workflows/strix.yml | 416 -- .github/workflows/trivy.yml | 2 +- apps/desktop/src-tauri/Cargo.lock | 9 +- apps/desktop/src/App.test.tsx | 339 +- apps/desktop/src/App.test.tsx.orig | 1367 +++++++ apps/desktop/src/App.tsx | 149 +- apps/desktop/src/components/ui/button.tsx | 14 +- apps/desktop/src/components/ui/input.tsx | 2 +- apps/desktop/src/components/ui/tabs.tsx | 2 +- .../features/workspace/RoleSwitcher.test.tsx | 15 + .../src/features/workspace/RoleSwitcher.tsx | 2 +- .../workspace/SectionRoadmap.test.tsx | 13 + .../src/features/workspace/SectionRoadmap.tsx | 69 +- .../src/features/workspace/Workspace.tsx | 48 +- apps/desktop/src/i18n/index.test.ts | 11 +- apps/desktop/src/lib/export.test.ts | 13 + apps/desktop/src/lib/job_runner.ts | 14 +- docs/design-system/README.md | 81 + docs/design-system/component-contract.md | 94 + docs/design-system/figma-to-code-workflow.md | 82 + docs/workflow/pr-review-merge-scheduler.md | 43 +- opencode.jsonc | 18 + package-lock.json | 8 +- package.json | 2 +- packages/shared-types/src/index.ts | 2 +- packages/shared-types/test/index.test.ts | 255 ++ patch_buttons.py | 137 + requirements-strix-ci-hashes.txt | 2387 ------------ requirements-strix-ci.txt | 4 - resolve_files.py | 36 + scripts/checks/verify_supply_chain.py | 61 +- scripts/ci/classify_failed_check_evidence.py | 311 -- scripts/ci/collect_failed_check_evidence.sh | 425 --- ...opencode_failed_check_fallback_findings.sh | 434 --- scripts/ci/opencode_review_approve_gate.sh | 278 -- .../ci/opencode_review_normalize_output.py | 278 -- scripts/ci/pr_review_merge_scheduler.py | 429 --- scripts/ci/strix_model_utils.sh | 124 - scripts/ci/strix_quick_gate.sh | 3339 ----------------- .../ci/test_opencode_fact_gate_contract.sh | 27 - .../validate_opencode_failed_check_review.sh | 391 -- .../src/bandscope_analysis/youtube.py | 6 +- .../{test_sections.py => test_extractor.py} | 28 +- .../analysis-engine/tests/test_priority.py | 40 + .../tests/test_sections_utils.py | 44 - .../analysis-engine/tests/test_separation.py | 12 +- .../tests/test_supply_chain_policy.py | 651 +--- .../analysis-engine/tests/test_youtube.py | 11 +- 61 files changed, 2904 insertions(+), 12400 deletions(-) delete mode 100644 .github/workflows/opencode-review.yml delete mode 100644 .github/workflows/pr-review-merge-scheduler.yml delete mode 100644 .github/workflows/strix.yml create mode 100644 apps/desktop/src/App.test.tsx.orig create mode 100644 docs/design-system/README.md create mode 100644 docs/design-system/component-contract.md create mode 100644 docs/design-system/figma-to-code-workflow.md create mode 100644 patch_buttons.py delete mode 100644 requirements-strix-ci-hashes.txt delete mode 100644 requirements-strix-ci.txt create mode 100644 resolve_files.py delete mode 100644 scripts/ci/classify_failed_check_evidence.py delete mode 100755 scripts/ci/collect_failed_check_evidence.sh delete mode 100755 scripts/ci/emit_opencode_failed_check_fallback_findings.sh delete mode 100755 scripts/ci/opencode_review_approve_gate.sh delete mode 100755 scripts/ci/opencode_review_normalize_output.py delete mode 100644 scripts/ci/pr_review_merge_scheduler.py delete mode 100755 scripts/ci/strix_model_utils.sh delete mode 100755 scripts/ci/strix_quick_gate.sh delete mode 100755 scripts/ci/test_opencode_fact_gate_contract.sh delete mode 100755 scripts/ci/validate_opencode_failed_check_review.sh rename services/analysis-engine/tests/{test_sections.py => test_extractor.py} (74%) delete mode 100644 services/analysis-engine/tests/test_sections_utils.py diff --git a/.Jules/palette.md b/.Jules/palette.md index 02b557f8..b6f5d5bf 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -9,12 +9,23 @@ ## 2026-06-13 - Added screen reader text for tooltip divs **Learning:** When using `title` attributes on non-interactive elements like icon-only `div`s for tooltips, screen readers might not announce them properly because they aren't focusable. The visual tooltip is not enough for accessibility. **Action:** Always add a visually hidden `[Tooltip Text]` inside non-interactive elements that rely on a `title` attribute so that screen readers have text content to announce. + ## 2026-06-18 - Added keyboard accessibility to scrollable regions **Learning:** Horizontally scrollable regions (like the `SectionRoadmap` component) are not accessible to keyboard-only users unless they can receive focus. Keyboard users must be able to focus the container to scroll its content using arrow keys. **Action:** For proper keyboard accessibility in custom scrollable regions, always include `tabIndex={0}`, an appropriate `aria-label`, `role="region"`, and explicit focus visible styling (e.g., `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300`). + ## 2026-06-19 - Internationalization **Learning:** The desktop app uses i18n via json files located in `apps/desktop/src/locales/` **Action:** When adding new text strings, make sure to add it to all locale files. -## 2026-06-22 - Disabled button tooltips accessibility -**Learning:** Native `disabled` HTML attributes prevent elements from receiving keyboard focus and can block hover events in some browsers. Consequently, placing a `title` attribute directly on a disabled ` - + + Settings coming soon + + + + Help coming soon + + @@ -514,7 +524,7 @@ export function App() { aria-disabled={active ? undefined : true} disabled={!active} title={active ? undefined : "Coming soon"} - className={`inline-flex min-h-10 shrink-0 items-center gap-2 rounded-xl px-3 text-sm font-semibold ${ + className={`inline-flex min-h-10 shrink-0 items-center gap-2 rounded-xl px-3 text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300 ${ active ? "bg-blue-600/70 text-white" : "cursor-not-allowed text-slate-500 opacity-70" }`} > @@ -524,14 +534,6 @@ export function App() { ))} -
-
-
@@ -546,55 +548,57 @@ export function App() {

-
+
-
-
-
+
- - + {jobResult ? ( - + ) : ( + + + + )} - {isStarting ? ( -
@@ -698,6 +709,14 @@ export function App() {
+
+
+
{renderWorkspaceState()}
diff --git a/apps/desktop/src/components/ui/button.tsx b/apps/desktop/src/components/ui/button.tsx index 572a2b6a..98a76ccd 100644 --- a/apps/desktop/src/components/ui/button.tsx +++ b/apps/desktop/src/components/ui/button.tsx @@ -4,20 +4,20 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( - "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/80", + default: "bg-primary text-primary-foreground hover:bg-primary/80 disabled:hover:bg-primary", outline: - "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground disabled:hover:bg-background disabled:hover:text-inherit dark:border-input dark:bg-input/30 dark:hover:bg-input/50 dark:disabled:hover:bg-input/30", secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground disabled:hover:bg-secondary disabled:hover:text-secondary-foreground", ghost: - "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", + "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground disabled:hover:bg-transparent disabled:hover:text-inherit dark:hover:bg-muted/50 dark:disabled:hover:bg-transparent", destructive: - "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", - link: "text-primary underline-offset-4 hover:underline", + "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 disabled:hover:bg-destructive/10 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40 dark:disabled:hover:bg-destructive/20", + link: "text-primary underline-offset-4 hover:underline disabled:hover:no-underline", }, size: { default: diff --git a/apps/desktop/src/components/ui/input.tsx b/apps/desktop/src/components/ui/input.tsx index 2a181128..cef9ebe7 100644 --- a/apps/desktop/src/components/ui/input.tsx +++ b/apps/desktop/src/components/ui/input.tsx @@ -10,7 +10,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) { type={type} data-slot="input" className={cn( - "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", + "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", className )} {...props} diff --git a/apps/desktop/src/components/ui/tabs.tsx b/apps/desktop/src/components/ui/tabs.tsx index 32e2ffba..7eff46a7 100644 --- a/apps/desktop/src/components/ui/tabs.tsx +++ b/apps/desktop/src/components/ui/tabs.tsx @@ -60,7 +60,7 @@ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) { ({ })); describe("RoleSwitcher", () => { + + it("renders the title and role options", () => { + const roles = [ + { id: "bass-guitar", name: "Bass Guitar" }, + { id: "lead-vocal", name: "Lead Vocal" } + ]; + + render(); + + expect(screen.getByText("Role-specific View")).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: "All Roles" })).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: "Bass Guitar" })).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: "Lead Vocal" })).toBeInTheDocument(); + }); + it("keeps the all-roles control distinct from a real role whose id is all", () => { const onRoleChange = vi.fn(); diff --git a/apps/desktop/src/features/workspace/RoleSwitcher.tsx b/apps/desktop/src/features/workspace/RoleSwitcher.tsx index f5275964..d5a222b5 100644 --- a/apps/desktop/src/features/workspace/RoleSwitcher.tsx +++ b/apps/desktop/src/features/workspace/RoleSwitcher.tsx @@ -43,7 +43,7 @@ export function RoleSwitcher({ roles, activeRole, onRoleChange }: RoleSwitcherPr return (
-
{ expect(screen.getAllByTitle("코드 수정").length).toBeGreaterThan(0); expect(onSongUpdate).toHaveBeenCalledTimes(1); }); + + it("does not update when the trimmed chord is unchanged", () => { + setNavigatorLanguage("en-US"); + const song = createDemoRehearsalSong(); + const onSongUpdate = vi.fn(); + vi.spyOn(window, "prompt").mockReturnValue(" C#m7 "); + + render(); + + fireEvent.click(screen.getByRole("button", { name: "Edit chord for Bass Guitar in verse, current C#m7" })); + + expect(onSongUpdate).not.toHaveBeenCalled(); + }); }); diff --git a/apps/desktop/src/features/workspace/SectionRoadmap.tsx b/apps/desktop/src/features/workspace/SectionRoadmap.tsx index 3ba06033..9281ee32 100644 --- a/apps/desktop/src/features/workspace/SectionRoadmap.tsx +++ b/apps/desktop/src/features/workspace/SectionRoadmap.tsx @@ -31,29 +31,48 @@ export function SectionRoadmap({ song, activeRole, onSongUpdate }: SectionRoadma const handleChordEdit = (sectionId: string, role: RehearsalRole) => { if (!onSongUpdate) return; const newChord = window.prompt(t("chordEditPrompt"), role.harmony.chord); - if (newChord !== null && newChord.trim() !== "" && newChord !== role.harmony.chord) { - const updatedSong = structuredClone(song); - const section = updatedSong.sections.find(s => s.id === sectionId); - if (section) { - const targetRole = section.roles.find(r => r.id === role.id); - if (targetRole) { - targetRole.harmony = { - ...targetRole.harmony, - chord: newChord.trim(), - source: "user" - }; - targetRole.manualOverrides = targetRole.manualOverrides.filter(o => o.field !== "harmony"); - targetRole.manualOverrides.push({ - field: "harmony", - value: { ...targetRole.harmony, source: "user" as const }, - source: "user" - }); - onSongUpdate(updatedSong); - } - } - } - }; + if (newChord === null) return; + + const trimmedChord = newChord.trim(); + if (trimmedChord === "" || trimmedChord === role.harmony.chord) return; + + let changed = false; + const updatedSong = { + ...song, + sections: song.sections.map((section) => { + if (section.id !== sectionId) return section; + + return { + ...section, + roles: section.roles.map((targetRole) => { + if (targetRole.id !== role.id) return targetRole; + changed = true; + const harmony = { + ...targetRole.harmony, + chord: trimmedChord, + source: "user" as const + }; + + return { + ...targetRole, + harmony, + manualOverrides: [ + ...targetRole.manualOverrides.filter((override) => override.field !== "harmony"), + { + field: "harmony" as const, + value: { ...harmony, source: "user" as const }, + source: "user" as const + } + ] + }; + }) + }; + }) + }; + + if (changed) onSongUpdate(updatedSong); + }; /** Documented. */ const getPriorityColor = (priority: string) => { if (priority === "high") return "border-rose-400 bg-rose-400/[0.08] shadow-[0_0_30px_rgba(251,113,133,0.10)]"; @@ -165,14 +184,14 @@ export function SectionRoadmap({ song, activeRole, onSongUpdate }: SectionRoadma {role.setupNote && (
-
)} {role.simplification && (
-
)} @@ -181,7 +200,7 @@ export function SectionRoadmap({ song, activeRole, onSongUpdate }: SectionRoadma
{role.overlapWarnings.map((warning, wIdx) => (
-
))} diff --git a/apps/desktop/src/features/workspace/Workspace.tsx b/apps/desktop/src/features/workspace/Workspace.tsx index 0dbb306c..2f990eec 100644 --- a/apps/desktop/src/features/workspace/Workspace.tsx +++ b/apps/desktop/src/features/workspace/Workspace.tsx @@ -222,7 +222,7 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp onClick={handleExportCueSheet} className="min-h-10 border-cyan-300/30 bg-cyan-300/10 font-semibold text-cyan-50 shadow-[0_10px_30px_rgba(34,211,238,0.16)] hover:bg-cyan-300/20 hover:text-white" > -
@@ -311,18 +311,36 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp

Stem Player

{activeRoleDetails?.name ?? activeRole}

- - - - + + + + + + + + + + {canTranscribeBass ? ( + + ) : ( + + + + )}
diff --git a/apps/desktop/src/i18n/index.test.ts b/apps/desktop/src/i18n/index.test.ts index 646ee710..dc49a0a2 100644 --- a/apps/desktop/src/i18n/index.test.ts +++ b/apps/desktop/src/i18n/index.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, vi, afterEach } from "vitest"; import { createTranslator, detectPreferredLocale } from "./index"; +import koCommon from "../locales/ko/common.json"; describe("i18n", () => { describe("detectPreferredLocale", () => { @@ -63,7 +64,15 @@ describe("i18n", () => { it("falls back to English when a Korean translation is missing", () => { const t = createTranslator("ko"); - expect(t("appTitle")).toBe("BandScope"); + const koDictionary = koCommon as Record; + const originalSubtitle = koDictionary.appSubtitle; + delete koDictionary.appSubtitle; + + try { + expect(t("appSubtitle")).toBe("Local-first desktop analysis tool for rehearsal prep"); + } finally { + koDictionary.appSubtitle = originalSubtitle; + } }); }); }); diff --git a/apps/desktop/src/lib/export.test.ts b/apps/desktop/src/lib/export.test.ts index 4250a416..265e983d 100644 --- a/apps/desktop/src/lib/export.test.ts +++ b/apps/desktop/src/lib/export.test.ts @@ -202,6 +202,19 @@ describe("export generation", () => { }); }); + it("uses the song identity as the default handoff workspace identity", () => { + const json = generateMetadataHandoffJson(mockSong, { + createdAt: "2026-06-15T08:30:00.000Z" + }); + const parsed = JSON.parse(json); + + expect(parsed.workspace).toEqual({ + id: "test", + title: "Test", + workspaceVersion: 1 + }); + }); + it("creates a local re-analysis request from a received handoff and selected replacement asset", () => { const handoff = JSON.parse(generateMetadataHandoffJson(mockSong, { createdAt: "2026-06-15T08:30:00.000Z", diff --git a/apps/desktop/src/lib/job_runner.ts b/apps/desktop/src/lib/job_runner.ts index 7809220e..b024ad5a 100644 --- a/apps/desktop/src/lib/job_runner.ts +++ b/apps/desktop/src/lib/job_runner.ts @@ -36,22 +36,16 @@ const mockWorkspace: RehearsalWorkspace = { workspaceVersion: 1 }; -const mockSongsById = new Map(); +const mockSongsById = new Map( + mockWorkspace.songs.map(song => [song.id, song]) +); type MockListener = (event: { payload: unknown }) => void; const mockListeners = new Set(); /** Documented. */ function getMockSong(jobId: string): SongRehearsalPack | undefined { - const cachedPack = mockSongsById.get(jobId); - if (cachedPack) { - return cachedPack; - } - const pack = mockWorkspace.songs.find(song => song.id === jobId); - if (pack) { - mockSongsById.set(jobId, pack); - } - return pack; + return mockSongsById.get(jobId); } /** diff --git a/docs/design-system/README.md b/docs/design-system/README.md new file mode 100644 index 00000000..b6ea134f --- /dev/null +++ b/docs/design-system/README.md @@ -0,0 +1,81 @@ +# BandScope Design System + +BandScope uses the Figma file as the self-contained design and implementation handoff. This repository mirrors the contract for review and maintenance, but the Figma file must remain usable without Code Connect, Figma access tokens, organization-tier platform features, or external repo docs. + +Figma file: https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk + +## Source Of Truth + +- Visual structure, component anatomy, states, layout examples, implementation paths, prop/state translation, UI repair guidance, screen blueprints, and QA rules live in Figma. +- Production component APIs, tokens, accessibility behavior, and implementation examples are mirrored in this directory for code review. +- Frontend work must resolve conflicts by checking both Figma-local contract pages and production code. +- Figma component names and variant names should mirror the repo contract so visual review remains straightforward. +- Do not introduce required Figma platform features into build, test, release, or CI flows. +- `Ponytail` and `Superpowers` are recorded on Figma page `33 Figma-Only Readiness Audit` as unavailable callable tools for this handoff. The explicit plugin links were rechecked on 2026-07-01 and still exposed no callable tools or install candidates, so treat them as named review perspectives only unless a future session exposes actual tools or project standards for them. + +## Working Model + +1. Start from the Figma component or screen to understand visual intent. +2. Read `28 Implementation Contract`, `29 UI Repair Playbook`, `30 Publisher + QA Matrix`, `31 Component Contract Catalog`, `32 Screen Blueprints`, and `33 Figma-Only Readiness Audit` in the Figma file. +3. Use [component-contract.md](component-contract.md) as a repo mirror of the Figma-only contract. +4. Implement with the listed code component and allowed props before adding local markup. +5. Use documented token classes and component variants first; add one-off classes only for domain-specific visual emphasis. +6. Review the PR against the publisher and frontend checklists below. + +Codex and other implementation agents must follow [figma-to-code-workflow.md](figma-to-code-workflow.md) when using the Figma file as development input. + +## Visual Audit Snapshot + +- Figma page `33 Figma-Only Readiness Audit` contains the `2026-07-01 visual pass - PASS` evidence row. +- Pages `28 Implementation Contract`, `29 UI Repair Playbook`, `30 Publisher + QA Matrix`, `31 Component Contract Catalog`, `32 Screen Blueprints`, and `33 Figma-Only Readiness Audit` each contain one visible root frame. +- The 2026-07-01 Figma audit found no empty root, hidden root, top-level overlap candidate, or remaining manual-height text clipping candidate on pages 28-33 after the intro and gap text was changed to auto-height. +- Figma page `33 Figma-Only Readiness Audit` also contains the `2026-07-01 placeholder section pass - PASS` evidence row. +- Page `32 Screen Blueprints` was hardened after the first visual pass: its mobile and desktop blocks now show concrete UI anatomy for header/role controls, source controls, status/progress, metric cards, navigation, console details, section roadmap, groove map, and export actions. +- The stricter page 32 audit found `0` label-only/empty blueprint sections and `0` text clipping candidates after those additions. +- A second-pass 2026-07-01 Figma audit found `10` overflow candidates and `2` sibling-overlap candidates across pages 28-33. Page `29 UI Repair Playbook`, page `31 Component Contract Catalog`, and page `32 Screen Blueprints` were repaired in Figma; the final audit reports `0` overflow candidates and `0` sibling-overlap candidates. +- `32 Screen Blueprints` remains the visual target for source-first mobile and desktop repair work. The current app implements that source-first order in [App.tsx](../../apps/desktop/src/App.tsx), and the regression is covered by [App.test.tsx](../../apps/desktop/src/App.test.tsx). +- If a Figma metadata overview appears to show pages 28-33 as empty, inspect the page root node directly before treating it as a defect. The verified root IDs are `50:2`, `50:20`, `50:59`, `51:2`, `50:86`, and `50:133`. +- If a blueprint block is only a large labeled box, treat that as a Figma handoff defect unless the corresponding runtime surface is genuinely unimplemented. The 2026-07-01 audit found the code was implemented, so Figma page 32 was corrected instead of changing app code. + +## Frontend Engineer Checklist + +- Use the canonical component path and current runtime API listed on Figma page `31 Component Contract Catalog`. +- Treat [component-contract.md](component-contract.md) as a review mirror, not a replacement for the Figma page. +- Keep `Button`, `Badge`, `Input`, `Tabs`, `Progress`, and `Card` semantics intact instead of recreating them with raw elements. +- Preserve focus states, disabled states, `aria-invalid`, labels, and keyboard-accessible regions. +- Keep mobile touch actions at 40px or larger when the design uses the Touch state. +- Keep source controls above the fold on narrow screens and allow wrapping before clipping. +- Preserve the contract test in `apps/desktop/src/App.test.tsx` that keeps `Source controls` before `Analysis summary`. +- Avoid nested card surfaces unless the inner surface is an actual repeated item or interactive module. +- Keep label letter spacing at `0` unless the current code already uses uppercase status metadata. + +## Publisher Checklist + +- Build pages from existing components and patterns before adding new UI. +- Treat Figma spacing, grouping, and hierarchy as the visual target, but use repo component APIs as the implementation target. +- Use concise headings inside panels; reserve display-scale type for page-level moments. +- Use icon buttons for recognizable actions and visible text buttons for commands that need wording. +- Check desktop and mobile screenshots for clipped text, cramped controls, hidden primary actions, and overlapping status content. +- When a Figma pattern has no extracted code component yet, keep it local to the feature and mark it for extraction in the backlog section of [component-contract.md](component-contract.md). Page 31 explicitly names these feature-local patterns. + +## Figma Maintenance + +- Keep the Figma Handoff Notes page linked to Figma-only pages first: `28 Implementation Contract`, `29 UI Repair Playbook`, `30 Publisher + QA Matrix`, `31 Component Contract Catalog`, `32 Screen Blueprints`, and `33 Figma-Only Readiness Audit`. +- Keep component descriptions focused on code path, usage, state mapping, and known UI defects. +- Update Figma variants only after confirming the repo component supports the state or after opening a follow-up implementation task. +- If a detail needed for implementation is absent from Figma, treat that absence as a design-system defect and update Figma before coding. +- If a Figma screen blueprint contains placeholder-only or label-only sections, compare against the runtime code first. Implement code only when the surface is missing; otherwise fill the Figma blueprint with concrete UI anatomy. +- If a Figma card or blueprint detail visibly overflows its parent, overlaps a sibling, or depends on unclipped spillover to be readable, treat it as a Figma handoff defect unless the corresponding runtime surface is missing. +- If Code Connect becomes available later, treat it as an optional publishing layer over this contract, not as the source of truth. +- Keep the `Ponytail and Superpowers access note`, `2026-07-01 visual pass`, `2026-07-01 placeholder section pass`, and `2026-07-01 overflow/overlap repair pass` rows on page 33 current whenever those tools, standards, or visual audit results change. + +## Current UI Defects Covered + +- Mobile source controls clipping: use wrapping source-control layout and Touch button sizing. +- First analysis path buried below metrics: keep source controls ahead of secondary metrics on narrow screens. +- Source-first reading order regression: covered by the `keeps source controls before the analysis summary` App test. +- Inconsistent action styling: route actions through `Button` and icon-button sizing. +- Small touch targets: use `size="lg"` or `size="icon-lg"` when the control is mobile-primary. +- Compact navigation clipping: allow trigger wrapping and avoid fixed-width labels. +- Heavy nested cards: prefer `Card` once per logical panel, with repeated rows inside. +- Dense uppercase labels: keep metadata short and use normal body text for explanations. diff --git a/docs/design-system/component-contract.md b/docs/design-system/component-contract.md new file mode 100644 index 00000000..76767bd5 --- /dev/null +++ b/docs/design-system/component-contract.md @@ -0,0 +1,94 @@ +# Component Contract + +This contract connects the BandScope Figma design system to production React components without requiring Figma Code Connect or Figma platform publishing. + +Figma file: https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk + +The authoritative Figma view is `31 Component Contract Catalog`. This file mirrors that page for review only. + +## Canonical Components + +| Figma component | Figma node | Code path | Use | +| --- | --- | --- | --- | +| Button / Default | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=18-84 | `apps/desktop/src/components/ui/button.tsx` | Primary actions with `variant="default"` and `size="default"`, `sm`, or `lg`. | +| Button / Outline | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=18-121 | `apps/desktop/src/components/ui/button.tsx` | Secondary or framed actions with `variant="outline"`. | +| Button / Secondary | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=18-167 | `apps/desktop/src/components/ui/button.tsx` | Low-emphasis filled actions with `variant="secondary"`. | +| Button / Ghost | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=18-213 | `apps/desktop/src/components/ui/button.tsx` | Toolbar actions with `variant="ghost"`. | +| Button / Destructive | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=18-250 | `apps/desktop/src/components/ui/button.tsx` | Destructive or high-risk actions with `variant="destructive"`. | +| Button / Link | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=18-287 | `apps/desktop/src/components/ui/button.tsx` | Inline actions with `variant="link"` and usually `size="sm"`. | +| Button / Source | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=18-324 | `apps/desktop/src/components/ui/button.tsx` | Design pattern only. Runtime uses `variant="secondary"` or `variant="outline"` plus source-control `className`; no dedicated source Button variant exists yet. | +| Icon Button | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=18-407 | `apps/desktop/src/components/ui/button.tsx` | Icon-only actions require `aria-label`; use `size="icon-sm"`, `icon`, or `icon-lg`. | +| Input | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=18-471 | `apps/desktop/src/components/ui/input.tsx` | Text, URL, and file inputs; use native `type`, `placeholder`, `disabled`, and `aria-invalid`. | +| Badge | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=18-494 | `apps/desktop/src/components/ui/badge.tsx` | Compact metadata with `variant="default"`, `secondary`, `destructive`, `outline`, `ghost`, or `link`. | +| Tabs Trigger | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=18-517 | `apps/desktop/src/components/ui/tabs.tsx` | Pair `TabsList variant="default" | "line"` with `TabsTrigger`; do not render triggers outside a `Tabs` root. | +| Navigation Item | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=18-559 | `apps/desktop/src/App.tsx` | Feature-local `NAV_ITEMS` button pattern; active maps to `aria-current="page"` and inactive maps to `disabled`. | +| Progress | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=18-602 | `apps/desktop/src/components/ui/progress.tsx` | Use `value` for progress; add indicator color classes only for semantic tone. | +| Console Panel | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=18-632 | `apps/desktop/src/components/ui/card.tsx` | Use `Card`, `CardHeader`, `CardTitle`, and `CardContent`; `size="sm"` is the compact state. | +| BandScope Mark | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=19-163 | `apps/desktop/src/App.tsx` | Feature-local `BandScopeMark()` currently has no props; Figma size variants are visual guidance only. | +| Metric Card | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=19-216 | `apps/desktop/src/App.tsx` | Feature-local `MetricCard({ icon, label, value, detail, accent? })`. Metrics follow source controls on mobile. | +| Confidence Badge | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=19-239 | `apps/desktop/src/features/workspace/ConfidenceBadge.tsx` | Use `level: ConfidenceLevel`; no `score` or `label` prop exists in current code. | +| Status Pill | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=19-283 | `apps/desktop/src/features/workspace/Workspace.tsx` | Design pattern only. Current code uses `formatStatusLabel(status)` inside local badge-like markup. | +| Role Switcher | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=19-337 | `apps/desktop/src/features/workspace/RoleSwitcher.tsx` | Use `roles`, `activeRole`, and `onRoleChange`; `null` means all roles. | +| Section Roadmap Card | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=19-402 | `apps/desktop/src/features/workspace/SectionRoadmap.tsx` | Use `song`, `activeRole`, and optional `onSongUpdate`; avoid rebuilding its internal card layout. | +| Song Structure Timeline | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=19-457 | `apps/desktop/src/features/workspace/Workspace.tsx` | Feature-local `SongStructure({ sections, t })` memo component; not exported. | +| Groove Map | https://www.figma.com/design/zthWmqfNKUgJBECvv002Qk/Bandscope-Design-System-v1?node-id=19-526 | `apps/desktop/src/features/workspace/GrooveMap.tsx` | Use `notes?: TranscriptionNote[]` and `isLoading?: boolean`; preserve scrollable region semantics and note labels. | +| 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`. | + +## Prop And State Mapping + +### Button + +- Figma `Size=Compact` maps to `size="sm"` for text buttons and `size="icon-sm"` for icon buttons. +- Figma `Size=Default` maps to `size="default"` for text buttons and `size="icon"` for icon buttons. +- Figma `Size=Touch` maps to `size="lg"` or `size="icon-lg"`; add `min-h-11` only when the surrounding layout needs a larger hit target. +- Figma `State=Disabled` maps to the native `disabled` prop. +- Figma focused states must be implemented through existing `focus-visible` classes, not custom hover-only styling. +- Figma `Button / Source` is a source-control pattern, not a runtime variant. Use `variant="secondary"` or `variant="outline"` with the documented source-control `className` until a dedicated variant is added to `button.tsx`. + +### Input + +- Figma `Type=Text`, `URL`, and `File` map to native `type="text"`, `url`, and `file`. +- Figma `State=Error` maps to `aria-invalid`. +- Figma `State=Disabled` maps to `disabled`. +- Keep placeholder text useful; do not use placeholder text as the only label for important workflows. + +### Badge And Confidence + +- Use `Badge` for general metadata and `ConfidenceBadge` for confidence status. +- Use `ConfidenceBadge` with `level` from shared types only: `low`, `medium`, `high`. +- Do not pass `score` or `label` to `ConfidenceBadge`; those props do not exist in the current runtime component. +- Keep badges short enough to avoid wrapping inside dense cards. + +### Tabs + +- Use `Tabs`, `TabsList`, `TabsTrigger`, and `TabsContent` together. +- Use `TabsList variant="line"` for compact timeline/navigation surfaces. +- Disabled triggers use the primitive `disabled` prop. + +### Progress + +- Use `Progress value={number}` for status progress. +- Tone-specific colors belong on `ProgressIndicator` or scoped child selectors. +- Provide adjacent live text when progress reflects an active asynchronous job. + +## Pattern Backlog + +These Figma patterns are valid visual guidance but are not yet extracted as standalone code components. Use the existing feature markup and open a follow-up extraction task when reuse appears twice. + +| Figma pattern | Current implementation home | Extraction trigger | +| --- | --- | --- | +| Source Control Stack | `apps/desktop/src/App.tsx` source controls section | Extract when another intake surface needs the same local audio, YouTube URL import, project, and analysis actions. | +| Navigation Item | `apps/desktop/src/App.tsx` shell navigation | Extract when navigation appears outside the app shell. | +| Metric Card | `apps/desktop/src/App.tsx` `MetricCard` | Extract when metrics move into feature pages or dashboards. | +| Status Pill | `apps/desktop/src/features/workspace/Workspace.tsx` | Extract when assignment/comment/approval status UI is reused. | +| Song Structure Timeline | `apps/desktop/src/features/workspace/Workspace.tsx` | Extract when timeline editing or playback controls are added. | +| Export Action Group | `apps/desktop/src/features/workspace/Workspace.tsx` | Extract when export controls are reused outside the workspace header. | + +## PR Review Rules + +- New UI should cite the matching Figma node and code path in the PR description when it implements a design-system component. +- A new component variant must update this file, the relevant component tests, and the Figma component notes. +- A new Figma-only pattern must enter the Pattern Backlog before being reused. +- Any deliberate visual divergence from Figma should state whether the repo contract or accessibility requirement caused it. +- Do not add Code Connect, Figma token, or Figma publish requirements to CI. diff --git a/docs/design-system/figma-to-code-workflow.md b/docs/design-system/figma-to-code-workflow.md new file mode 100644 index 00000000..c6b93fa9 --- /dev/null +++ b/docs/design-system/figma-to-code-workflow.md @@ -0,0 +1,82 @@ +# Figma To Code Workflow + +This workflow is for Codex, Frontend Engineers, and Publishers using the BandScope Figma file without Code Connect. + +Figma is the visual, structural, and handoff input. The repository remains the runtime source of truth for tests and release behavior, but the Figma file must carry enough implementation guidance to start work without opening these docs. Missing implementation detail inside Figma is a design-system defect. + +## Can Codex Develop From This Figma? + +Yes, with translation. Codex can read the Figma file for component anatomy, variants, layout, text hierarchy, visual states, implementation paths, current runtime prop mappings, TSX examples, screen blueprints, and design-defect guidance. Codex must then translate that intent into the existing React components and Tailwind classes in production code. + +Codex must not paste generated Figma code directly into the app. Generated Figma code is reference material only. + +## Required Loop + +1. Identify the target Figma node, screen, or component set. +2. Read Figma structure and variants through Figma MCP or the node URL. +3. Read `31 Component Contract Catalog` for the matching source path, current runtime API, TSX example, and QA note. +4. Read `32 Screen Blueprints` for mobile and desktop placement before changing layout. +5. Check `33 Figma-Only Readiness Audit` for current visual audit evidence and tool access limits before deciding a Figma page is empty or a plugin-backed review is required. +6. Use [component-contract.md](component-contract.md) only as a repo mirror when working in code review. +7. Inspect the actual code component before editing. +8. If a Figma blueprint block is placeholder-only, verify whether the matching runtime surface exists before coding. Fill Figma when the code already exists; implement code only when the surface is genuinely missing. +9. If a Figma card, contract row, or blueprint detail overflows its parent or overlaps a sibling, repair Figma first unless the runtime surface is genuinely missing. +10. Implement with existing components first. +11. Add or update tests when behavior, accessibility, reading order, or reusable component APIs change. +12. Verify with typecheck and the narrowest useful test command. +13. For visible UI changes, run the app and compare desktop and mobile screenshots against Figma intent. +14. If implementation needs to diverge from Figma, document whether the code API, accessibility, runtime behavior, or responsive layout caused the divergence. + +## What Figma Can Provide + +- Component names, node IDs, descriptions, variants, and component property definitions. +- Source paths, current runtime API notes, TSX examples, and QA notes visibly stored on `31 Component Contract Catalog` and mirrored in component descriptions plus `bandscope` shared metadata. +- Visual measurements, spacing, hierarchy, state examples, and screenshots. +- Mobile 375x812 and desktop 1440x900 repair targets on `32 Screen Blueprints`. +- Figma-only readiness evidence on `33 Figma-Only Readiness Audit`. +- Tool access limits on `33 Figma-Only Readiness Audit`, including the 2026-07-01 `Ponytail` and `Superpowers` recheck note. +- Visual audit evidence on `33 Figma-Only Readiness Audit`, including the 2026-07-01 pass that confirms pages 28-33 have visible root frames and no remaining manual-height text clipping candidates. +- Placeholder-section audit evidence on `33 Figma-Only Readiness Audit`, including the 2026-07-01 pass that confirms page 32 has no remaining label-only blueprint sections. +- Overflow/overlap repair evidence on `33 Figma-Only Readiness Audit`, including the 2026-07-01 pass that confirms pages 28-33 have no remaining parent overflow candidates or sibling-overlap candidates after page 29, page 31, and page 32 geometry repairs. +- Domain patterns such as Source Control Stack, Groove Map, Section Roadmap Card, and Export Action Group. +- UI-defect guidance for clipping, touch targets, source-control priority, and panel density. + +## What The Repo Must Provide + +- Canonical React component paths and prop names. +- Allowed variants, sizes, accessibility semantics, and composition rules. +- Tests, build behavior, and CI requirements. +- Decisions about whether a Figma pattern should become a reusable code component. + +## Translation Rules + +- Translate Figma `Button / Default` through `Button`, not raw `button` markup. +- Translate Figma `Input` states through native `type`, `disabled`, and `aria-invalid`. +- Translate Figma `Tabs Trigger` through `Tabs`, `TabsList`, and `TabsTrigger`. +- Translate confidence UI through `ConfidenceBadge`, not local color classes. +- Translate Figma pattern components in the backlog as feature-local markup until reuse justifies extraction. +- Keep generated Figma asset URLs out of production code unless the asset has been intentionally added to the repo. + +## When To Stop And Reassess + +- The Figma component has no matching contract entry. +- A Figma variant has no supported code prop or class strategy. +- A Figma contract names a prop that does not exist in the current runtime component. +- A required implementation detail exists only in repo docs and not in Figma. +- A named review perspective such as `Ponytail` or `Superpowers` is treated as a tool-backed requirement without an actual available tool or documented project standard. +- A page-level Figma metadata overview appears empty but the page root node has not been inspected directly. +- A Figma screen blueprint has a large placeholder-only or label-only section and the matching runtime surface has not been checked. +- A Figma row, card, or blueprint detail only reads correctly because text or nested content spills outside its parent or overlaps a neighboring element. +- A generated Figma layout would require duplicating an existing component. +- The implementation would add a Figma token, access token, publish step, or platform-plan requirement. +- Visual parity conflicts with accessibility, keyboard behavior, localization, or responsive constraints. + +## PR Notes + +PRs that implement Figma-driven UI should include: + +- Figma node URL or page name. +- Contract entry used. +- Code component paths touched. +- Verification commands, contract tests, and screenshot viewports, when applicable. +- Any divergence from Figma and the reason. diff --git a/docs/workflow/pr-review-merge-scheduler.md b/docs/workflow/pr-review-merge-scheduler.md index 768d37b1..adcb9bae 100644 --- a/docs/workflow/pr-review-merge-scheduler.md +++ b/docs/workflow/pr-review-merge-scheduler.md @@ -1,19 +1,33 @@ -# PR Review Merge Scheduler +# Central PR Review And Merge Automation ## Purpose -The PR review merge scheduler keeps the open `develop` PR queue moving without bypassing repository rules. -It runs hourly and can also be started manually from the `pr-review-merge-scheduler` workflow. +BandScope does not keep repo-local copies of the OpenCode Review or PR Review Merge Scheduler workflows. +Those checks are supplied by the ContextualWisdomLab organization ruleset from `ContextualWisdomLab/.github` +as central required workflows. + +The central scheduler keeps the open `develop` PR queue moving without bypassing repository rules. +It runs in the target repository context through the organization required workflow, so mechanical +update-branch, auto-merge, and merge actions are performed by the selected workflow mutation +credential, not by a maintainer's local `gh` session. The central scheduler may select +`PR_REVIEW_MERGE_TOKEN`, `OPENCODE_APPROVE_TOKEN`, an exchanged OpenCode GitHub App token, or the +workflow `GITHUB_TOKEN`, depending on which credential can perform the guarded repository mutation. + +The local repository may keep product CI, security, release, and build workflows. It must not restore +repo-local copies of `opencode-review.yml`, `pr-review-merge-scheduler.yml`, or their `scripts/ci` helper implementations. ## Behavior -- Inspect up to 20 open, non-draft PRs targeting `develop` by default. +- Inspect non-draft PRs targeting the repository default branch, currently `develop`. +- Use central OpenCode Review for current-head evidence, CodeGraph-backed review, peer-check waits, + review-agent status contexts, failed-check explanation, provider/runtime failures, OpenCode runtime + evidence, and approval publication failures. Publication failures are automation evidence, not + source-backed repository findings, and they must be summarized as OpenCode runtime evidence. +- Keep provider failure, external failed-check classification, and Strix evidence lookup diagnostics + in the central workflow. Strix evidence lookup failures must mention missing Actions read access + when that is the actual GitHub API scope problem. - Skip PRs with unresolved review threads. -- Request one CodeRabbit review per head SHA when a PR has zero unresolved threads but is not approved. - Check only GitHub-required checks before merge actions. -- Retry transient GitHub CLI/API read failures and skip only the affected PR when review-thread - state remains unavailable after retries, while keeping command stdout separate from retry - diagnostics so parsed JSON, counts, and booleans stay clean. - Update approved PRs that are behind `develop` and wait for fresh checks. - Merge only PRs that are approved, thread-clean, conflict-free, and passing required checks. - Fall back to GitHub auto-merge only when a direct normal merge does not complete. @@ -25,12 +39,19 @@ It runs hourly and can also be started manually from the `pr-review-merge-schedu - It does not resolve review threads. - It does not use admin merge or ruleset bypass. - It does not weaken required checks, branch protection, or repository rulesets. +- It does not require BandScope to carry repo-local OpenCode or scheduler workflow/helper copies. +- It does not move central token permissions into this repository. ## Security Notes -- Attack surface: scheduled GitHub Actions automation with write access to PR comments, PR branch updates, and normal merges. +- Attack surface: organization required workflows with write access to PR comments, PR branch updates, and normal merges. - Trust boundary touched: GitHub repository governance, PR review state, status checks, and CodeRabbit review requests. - Realistic threats: spammed review comments, merging a PR with unresolved conversations, merging without required checks, or hiding conflicts behind automation. -- Mitigations: idempotent per-head review comment marker, explicit unresolved-thread check, retry-bounded GitHub API reads, required-check verification through GitHub, conflict skip, normal merge only, and no admin bypass path. +- Mitigations: central required workflow source pinning, idempotent per-head review comment marker, + explicit unresolved-thread check, retry-bounded GitHub API reads, required-check verification + through GitHub, conflict skip, guarded merge with `--match-head-commit`, and no admin bypass path. - Remaining risk: CodeRabbit and GitHub check state can be delayed or stale; the scheduler therefore only advances eligible PRs and leaves code-fix work to agents or maintainers. -- Test points: `workflow_dispatch` dry run on a limited `max_prs`, transient GitHub API failure with stderr output, PR with unresolved thread, PR needing review, approved behind PR, approved conflict-free PR, and approved dirty PR. +- Test points: organization ruleset inheritance, current-head OpenCode approval, unresolved review + thread count, required-check rollup, approved behind PR, approved conflict-free PR, approved dirty PR, + external failed-check classification, provider/runtime failure summary, and Strix evidence lookup + scope diagnostics. diff --git a/opencode.jsonc b/opencode.jsonc index a5ab3396..888aa237 100644 --- a/opencode.jsonc +++ b/opencode.jsonc @@ -70,6 +70,24 @@ "context": 128000, "output": 4096 } + }, + "openai/o3": { + "name": "OpenAI o3", + "tool_call": true, + "reasoning": true, + "limit": { + "context": 200000, + "output": 100000 + } + }, + "openai/o4-mini": { + "name": "OpenAI o4-mini", + "tool_call": true, + "reasoning": true, + "limit": { + "context": 200000, + "output": 100000 + } } } } diff --git a/package-lock.json b/package-lock.json index 4bff7d1e..8a479475 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ ], "devDependencies": { "@eslint/js": "^10.0.1", - "eslint-plugin-jsdoc": "^63.0.5", + "eslint-plugin-jsdoc": "^63.0.7", "react": "^19.2.4", "react-dom": "^19.2.7" }, @@ -2755,9 +2755,9 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "63.0.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-63.0.5.tgz", - "integrity": "sha512-AzI9bgKhV9li049/mIblX0c41DeWMMfH9qNsRasc+fAxwURRKChIp03Pk57M7UTf+Y6hifTJ89kQyCOoOLtEDw==", + "version": "63.0.7", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-63.0.7.tgz", + "integrity": "sha512-pxrqGO733F7xmVYB5vQOiciiT9uddxqehawnbPjZmW2YaJR6fT5cP3UQd2BNoE85ATspCMtNL8w/a5WDGX3Qwg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { diff --git a/package.json b/package.json index 84946d84..974eadab 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "eslint-plugin-jsdoc": "^63.0.5", + "eslint-plugin-jsdoc": "^63.0.7", "react": "^19.2.4", "react-dom": "^19.2.7" } diff --git a/packages/shared-types/src/index.ts b/packages/shared-types/src/index.ts index 400f4d39..ff04618b 100644 --- a/packages/shared-types/src/index.ts +++ b/packages/shared-types/src/index.ts @@ -1807,7 +1807,7 @@ function validateSongRehearsalPack( if (value.song === undefined) return invalidField(`${path}.song`); const songError = validateRehearsalSong(value.song, options); if (songError) return songError; - } else if (value.packState === "failed") { + } else { const extraKey = unexpectedKey(value, ["id", "packState", "engineState", "sourceLabel", "error"], path); if (extraKey) return extraKey; if (value.error === undefined) return invalidField(`${path}.error`); diff --git a/packages/shared-types/test/index.test.ts b/packages/shared-types/test/index.test.ts index b74189f6..bd2bc6fa 100644 --- a/packages/shared-types/test/index.test.ts +++ b/packages/shared-types/test/index.test.ts @@ -221,6 +221,7 @@ describe("shared type helpers", () => { progressPercent: 0, cacheStatus: "disabled" }); + expect(parseAnalysisJobStatus(queuedStatus)).toEqual(queuedStatus); expect(isAnalysisJobStatus({ jobId: "job-1", state: "running", @@ -342,6 +343,14 @@ describe("shared type helpers", () => { updatedAt: "2026-03-12T00:00:00.000Z", error: { code: "not_found", message: "Missing", extraField: true } })).toBe(false); + expect(() => parseAnalysisJobStatus({ + jobId: "job-1", + state: "running", + requestedAt: "2026-03-12T00:00:00.000Z", + updatedAt: "2026-03-12T00:00:00.000Z", + cacheStatus: "warm" + })).toThrow("cacheStatus"); + expect(() => parseAnalysisJobStatus({ jobId: 7 })).toThrow("jobId"); }); it("validates local audio sources and bootstrap requests", () => { @@ -578,7 +587,9 @@ describe("shared type helpers", () => { { message: "sections[0].roleBuckets[0].id", payload: { ...artifact, sections: [{ ...artifact.sections[0], roleBuckets: [{ ...artifact.sections[0]!.roleBuckets[0], id: 3 }] }] } }, { message: "sections[0].roleBuckets[0].name", payload: { ...artifact, sections: [{ ...artifact.sections[0], roleBuckets: [{ ...artifact.sections[0]!.roleBuckets[0], name: 3 }] }] } }, { message: "sections[0].roleBuckets[0].roleType", payload: { ...artifact, sections: [{ ...artifact.sections[0], roleBuckets: [{ ...artifact.sections[0]!.roleBuckets[0], roleType: "drums" }] }] } }, + { message: "sections[0].roleBuckets[0].extraField", payload: { ...artifact, sections: [{ ...artifact.sections[0], roleBuckets: [{ ...artifact.sections[0]!.roleBuckets[0], extraField: true }] }] } }, { message: "sections[0].roleBuckets[0].rehearsalPriority", payload: { ...artifact, sections: [{ ...artifact.sections[0], roleBuckets: [{ ...artifact.sections[0]!.roleBuckets[0], rehearsalPriority: "urgent" }] }] } }, + { message: "sections[0].extraField", payload: { ...artifact, sections: [{ ...artifact.sections[0], extraField: true }] } }, { message: "sourceAssets", payload: { ...artifact, sourceAssets: "not-an-array" } }, { message: "sourceAssets[0]", payload: { ...artifact, sourceAssets: [null] } }, { message: "sourceAssets[0].referenceKind", payload: { ...artifact, sourceAssets: [{ ...artifact.sourceAssets[0], referenceKind: "stem" }] } }, @@ -1105,6 +1116,58 @@ describe("shared type helpers", () => { song.sections[0]!.roles[0]!.transpositionPlan = 2 as never; }) }, + { + message: "sections[0].roles[0].transcription", + payload: createInvalidSong((song) => { + song.sections[0]!.roles[0]!.transcription = "not-an-array" as never; + }) + }, + { + message: "sections[0].roles[0].transcription[0]", + payload: createInvalidSong((song) => { + song.sections[0]!.roles[0]!.transcription = [null as never]; + }) + }, + { + message: "sections[0].roles[0].transcription[0].extraField", + payload: createInvalidSong((song) => { + song.sections[0]!.roles[0]!.transcription = [ + { pitch: "E2", onset: 0, offset: 1, velocity: 0.7, extraField: true } as never + ]; + }) + }, + { + message: "sections[0].roles[0].transcription[0].pitch", + payload: createInvalidSong((song) => { + song.sections[0]!.roles[0]!.transcription = [ + { pitch: 42, onset: 0, offset: 1, velocity: 0.7 } as never + ]; + }) + }, + { + message: "sections[0].roles[0].transcription[0].onset", + payload: createInvalidSong((song) => { + song.sections[0]!.roles[0]!.transcription = [ + { pitch: "E2", onset: "0", offset: 1, velocity: 0.7 } as never + ]; + }) + }, + { + message: "sections[0].roles[0].transcription[0].offset", + payload: createInvalidSong((song) => { + song.sections[0]!.roles[0]!.transcription = [ + { pitch: "E2", onset: 0, offset: "1", velocity: 0.7 } as never + ]; + }) + }, + { + message: "sections[0].roles[0].transcription[0].velocity", + payload: createInvalidSong((song) => { + song.sections[0]!.roles[0]!.transcription = [ + { pitch: "E2", onset: 0, offset: 1, velocity: "loud" } as never + ]; + }) + }, { message: "sections[0].roles[2].manualOverrides[0]", payload: createInvalidSong((song) => { @@ -1213,24 +1276,162 @@ describe("shared type helpers", () => { song.collaboration!.syncMode = "shared_drive" as never; }) }, + { + message: "collaboration", + payload: createInvalidSong((song) => { + song.collaboration = null as never; + }) + }, + { + message: "collaboration.extraField", + payload: createInvalidSong((song) => { + (song.collaboration as unknown as Record).extraField = true; + }) + }, { message: "collaboration.syncNote", payload: createInvalidSong((song) => { song.collaboration!.syncNote = 2 as never; }) }, + { + message: "collaboration.assignments", + payload: createInvalidSong((song) => { + song.collaboration!.assignments = "not-an-array" as never; + }) + }, + { + message: "collaboration.assignments[0]", + payload: createInvalidSong((song) => { + song.collaboration!.assignments = [null as never]; + }) + }, + { + message: "collaboration.assignments[0].extraField", + payload: createInvalidSong((song) => { + (song.collaboration!.assignments[0] as unknown as Record).extraField = true; + }) + }, + { + message: "collaboration.assignments[0].id", + payload: createInvalidSong((song) => { + song.collaboration!.assignments[0]!.id = 2 as never; + }) + }, { message: "collaboration.assignments[0].assignee", payload: createInvalidSong((song) => { song.collaboration!.assignments[0]!.assignee = 2 as never; }) }, + { + message: "collaboration.assignments[0].summary", + payload: createInvalidSong((song) => { + song.collaboration!.assignments[0]!.summary = 2 as never; + }) + }, + { + message: "collaboration.assignments[0].sectionId", + payload: createInvalidSong((song) => { + song.collaboration!.assignments[0]!.sectionId = 2 as never; + }) + }, + { + message: "collaboration.assignments[0].roleId", + payload: createInvalidSong((song) => { + song.collaboration!.assignments[0]!.roleId = 2 as never; + }) + }, + { + message: "collaboration.comments", + payload: createInvalidSong((song) => { + song.collaboration!.comments = "not-an-array" as never; + }) + }, + { + message: "collaboration.comments[0]", + payload: createInvalidSong((song) => { + song.collaboration!.comments = [null as never]; + }) + }, + { + message: "collaboration.comments[0].extraField", + payload: createInvalidSong((song) => { + (song.collaboration!.comments[0] as unknown as Record).extraField = true; + }) + }, + { + message: "collaboration.comments[0].id", + payload: createInvalidSong((song) => { + song.collaboration!.comments[0]!.id = 2 as never; + }) + }, + { + message: "collaboration.comments[0].author", + payload: createInvalidSong((song) => { + song.collaboration!.comments[0]!.author = 2 as never; + }) + }, + { + message: "collaboration.comments[0].body", + payload: createInvalidSong((song) => { + song.collaboration!.comments[0]!.body = 2 as never; + }) + }, + { + message: "collaboration.comments[0].sectionId", + payload: createInvalidSong((song) => { + song.collaboration!.comments[0]!.sectionId = 2 as never; + }) + }, + { + message: "collaboration.comments[0].roleId", + payload: createInvalidSong((song) => { + song.collaboration!.comments[0]!.roleId = 2 as never; + }) + }, { message: "collaboration.comments[0].status", payload: createInvalidSong((song) => { song.collaboration!.comments[0]!.status = "pending" as never; }) }, + { + message: "collaboration.approvals", + payload: createInvalidSong((song) => { + song.collaboration!.approvals = "not-an-array" as never; + }) + }, + { + message: "collaboration.approvals[0]", + payload: createInvalidSong((song) => { + song.collaboration!.approvals = [null as never]; + }) + }, + { + message: "collaboration.approvals[0].extraField", + payload: createInvalidSong((song) => { + (song.collaboration!.approvals[0] as unknown as Record).extraField = true; + }) + }, + { + message: "collaboration.approvals[0].id", + payload: createInvalidSong((song) => { + song.collaboration!.approvals[0]!.id = 2 as never; + }) + }, + { + message: "collaboration.approvals[0].scope", + payload: createInvalidSong((song) => { + song.collaboration!.approvals[0]!.scope = 2 as never; + }) + }, + { + message: "collaboration.approvals[0].owner", + payload: createInvalidSong((song) => { + song.collaboration!.approvals[0]!.owner = 2 as never; + }) + }, { message: "collaboration.approvals[0].status", payload: createInvalidSong((song) => { @@ -1242,6 +1443,14 @@ describe("shared type helpers", () => { for (const testCase of cases) { expect(() => parseRehearsalSong(testCase.payload)).toThrow(testCase.message); } + + const songWithTranscription = createDemoRehearsalSong(); + songWithTranscription.sections[0]!.roles[0]!.transcription = [ + { pitch: "E2", onset: 0, offset: 1, velocity: 0.7 } + ]; + expect(parseRehearsalSong(songWithTranscription).sections[0]?.roles[0]?.transcription).toEqual([ + { pitch: "E2", onset: 0, offset: 1, velocity: 0.7 } + ]); }); it("validates SongRehearsalPack and RehearsalWorkspace", () => { @@ -1259,10 +1468,39 @@ describe("shared type helpers", () => { workspaceVersion: 1, songs: [validPack] }; + const queuedPack: SongRehearsalPack = { + id: "pack-queued", + packState: "queued", + engineState: "queued", + sourceLabel: "Queued Song" + }; + const analyzingPack: SongRehearsalPack = { + id: "pack-analyzing", + packState: "analyzing", + engineState: "running", + sourceLabel: "Analyzing Song" + }; + const failedPack: SongRehearsalPack = { + id: "pack-failed", + packState: "failed", + engineState: "failed", + sourceLabel: "Failed Song", + error: { code: "engine_unavailable", message: "Engine unavailable" } + }; expect(parseSongRehearsalPack(validPack)).toEqual(validPack); + expect(parseSongRehearsalPack(queuedPack)).toEqual(queuedPack); + expect(parseSongRehearsalPack(analyzingPack)).toEqual(analyzingPack); + expect(parseSongRehearsalPack(failedPack)).toEqual(failedPack); expect(isRehearsalWorkspace(validWorkspace)).toBe(true); expect(parseRehearsalWorkspace(validWorkspace)).toEqual(validWorkspace); + expect(parseRehearsalWorkspace({ + ...validWorkspace, + songs: [queuedPack, failedPack] + })).toEqual({ + ...validWorkspace, + songs: [queuedPack, failedPack] + }); const legacyNestedSong = createDemoRehearsalSong() as unknown as { sections: Array>; @@ -1285,6 +1523,23 @@ describe("shared type helpers", () => { // Invalid packs expect(() => parseSongRehearsalPack({ ...validPack, packState: "invalid" })).toThrow("packState"); expect(() => parseSongRehearsalPack({ ...validPack, extraField: true })).toThrow("extraField"); + expect(() => parseSongRehearsalPack({ + id: "pack-ready-missing-song", + packState: "ready", + sourceLabel: "Ready Song" + })).toThrow("song"); + expect(() => parseSongRehearsalPack({ ...queuedPack, extraField: true })).toThrow("extraField"); + expect(() => parseSongRehearsalPack({ + id: "pack-queued-missing-engine", + packState: "queued", + sourceLabel: "Queued Song" + })).toThrow("engineState"); + expect(() => parseSongRehearsalPack({ ...failedPack, extraField: true })).toThrow("extraField"); + expect(() => parseSongRehearsalPack({ + id: "pack-failed-missing-error", + packState: "failed", + sourceLabel: "Failed Song" + })).toThrow("error"); // Invalid workspaces expect(isRehearsalWorkspace({ ...validWorkspace, songs: [{...validPack, packState: "bad"}] })).toBe(false); diff --git a/patch_buttons.py b/patch_buttons.py new file mode 100644 index 00000000..8140b884 --- /dev/null +++ b/patch_buttons.py @@ -0,0 +1,137 @@ +import re + +def update_file(): + with open('apps/desktop/src/App.tsx', 'r', encoding='utf-8') as f: + content = f.read() + + # 1. Choose local audio + search_1 = ''' ''' + replace_1_end = '''