diff --git a/.github/workflows/build-baseline.yml b/.github/workflows/build-baseline.yml index 6454e8b4..600cfe47 100644 --- a/.github/workflows/build-baseline.yml +++ b/.github/workflows/build-baseline.yml @@ -94,18 +94,10 @@ jobs: - name: Package Windows amd64 artifact run: python scripts/release/package_desktop_artifact.py - name: Upload Windows amd64 artifact - id: upload-windows-amd64 - continue-on-error: ${{ github.event_name == 'pull_request' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: bandscope-windows-amd64-${{ github.sha }} path: artifacts/* - - name: Explain non-blocking Windows amd64 artifact upload failure - if: ${{ steps.upload-windows-amd64.outcome == 'failure' }} - run: | - echo "Artifact upload failed after the Windows amd64 bundle was packaged." >> "$GITHUB_STEP_SUMMARY" - echo "Pull request builds keep artifact upload non-blocking because GitHub artifact service or DNS failures do not invalidate the build evidence." >> "$GITHUB_STEP_SUMMARY" - echo "Tag and release builds remain blocking because release publication requires uploaded artifacts." >> "$GITHUB_STEP_SUMMARY" build-windows-arm64: name: build / windows / arm64 @@ -181,18 +173,10 @@ jobs: - name: Package Windows arm64 artifact run: python scripts/release/package_desktop_artifact.py - name: Upload Windows arm64 artifact - id: upload-windows-arm64 - continue-on-error: ${{ github.event_name == 'pull_request' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: bandscope-windows-arm64-${{ github.sha }} path: artifacts/* - - name: Explain non-blocking Windows arm64 artifact upload failure - if: ${{ steps.upload-windows-arm64.outcome == 'failure' }} - run: | - echo "Artifact upload failed after the Windows arm64 bundle was packaged." >> "$GITHUB_STEP_SUMMARY" - echo "Pull request builds keep artifact upload non-blocking because GitHub artifact service or DNS failures do not invalidate the build evidence." >> "$GITHUB_STEP_SUMMARY" - echo "Tag and release builds remain blocking because release publication requires uploaded artifacts." >> "$GITHUB_STEP_SUMMARY" gate-windows: name: gate / build / windows @@ -247,18 +231,10 @@ jobs: - name: Package macOS amd64 artifact run: python3 scripts/release/package_desktop_artifact.py - name: Upload macOS amd64 artifact - id: upload-macos-amd64 - continue-on-error: ${{ github.event_name == 'pull_request' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: bandscope-macos-amd64-${{ github.sha }} path: artifacts/* - - name: Explain non-blocking macOS amd64 artifact upload failure - if: ${{ steps.upload-macos-amd64.outcome == 'failure' }} - run: | - echo "Artifact upload failed after the macOS amd64 bundle was packaged." >> "$GITHUB_STEP_SUMMARY" - echo "Pull request builds keep artifact upload non-blocking because GitHub artifact service or DNS failures do not invalidate the build evidence." >> "$GITHUB_STEP_SUMMARY" - echo "Tag and release builds remain blocking because release publication requires uploaded artifacts." >> "$GITHUB_STEP_SUMMARY" build-macos-arm64: name: build / macos / arm64 @@ -303,18 +279,10 @@ jobs: - name: Package macOS arm64 artifact run: python3 scripts/release/package_desktop_artifact.py - name: Upload macOS arm64 artifact - id: upload-macos-arm64 - continue-on-error: ${{ github.event_name == 'pull_request' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: bandscope-macos-arm64-${{ github.sha }} path: artifacts/* - - name: Explain non-blocking macOS arm64 artifact upload failure - if: ${{ steps.upload-macos-arm64.outcome == 'failure' }} - run: | - echo "Artifact upload failed after the macOS arm64 bundle was packaged." >> "$GITHUB_STEP_SUMMARY" - echo "Pull request builds keep artifact upload non-blocking because GitHub artifact service or DNS failures do not invalidate the build evidence." >> "$GITHUB_STEP_SUMMARY" - echo "Tag and release builds remain blocking because release publication requires uploaded artifacts." >> "$GITHUB_STEP_SUMMARY" gate-macos: name: gate / build / macos diff --git a/.jules/bolt.md b/.jules/bolt.md index 38d4b732..6c10f968 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -41,3 +41,7 @@ ## 2025-02-15 - Replace Array.from(map.values()).map with a for...of loop **Learning:** Using `Array.from(map.values()).map(...)` creates an unnecessary intermediate array which wastes memory allocation and garbage collection time, particularly for frequently re-rendered components handling large collections. **Action:** Use a `for...of` loop over `map.values()` to iterate and push mapped elements directly into the final array for O(1) memory and avoiding intermediate array allocations. + +## 2025-02-15 - Vectorize diagonal windowed operations +**Learning:** Iterating through a large NumPy matrix along the diagonal using a Python `for` loop with array slicing and summation causes high constant time overhead due to repeated inner-loop allocations. +**Action:** When performing windowed operations strictly along the diagonal of large matrices, use sub-matrix diagonal vectorization (`numpy.lib.stride_tricks.sliding_window_view` coupled with `np.diagonal` and `np.einsum`) to delegate iteration and computation to optimized C-level routines. diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 6a57e902..e88c357a 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -17,9 +17,3 @@ **Vulnerability:** Any project identifier that can reach a filesystem path join must be treated as untrusted, even when it is generated internally or passed through IPC lookup flows. **Learning:** Reject only dangerous path segments (`.` and `..`) and path separators (`/` and `\`) so the guard blocks traversal without rejecting ordinary identifiers such as `my..id`. **Prevention:** Keep project ID validation centralized before `base_root.join(project_id)`, and cover forward-slash, backslash, parent-component, and benign interior-dot cases in unit tests. - -## 2025-02-09 - Ensure Maximum URL Length Limit on Backend - -**Vulnerability:** The Rust backend (`apps/desktop/src-tauri/src/main.rs`) did not enforce a maximum URL length limit when processing YouTube URLs via `import_youtube_url`. While the frontend enforced `MAX_YOUTUBE_URL_LENGTH = 2000` via the input element, this could be bypassed by an attacker sending requests directly to the Tauri backend API, potentially causing a Denial of Service (DoS) due to unbounded URL parsing and regex matching. -**Learning:** Input validation must occur at the entry point of untrusted data on the backend, even if it is also validated on the frontend. Relying solely on frontend validation for constraints like string length can expose the backend to resource exhaustion vulnerabilities. -**Prevention:** Always enforce constraints like maximum length, format validation, and sanitization at the earliest possible point on the backend, typically at the API boundary, regardless of frontend safeguards. diff --git a/CHANGELOG.md b/CHANGELOG.md index eea69689..3b228b48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,11 +58,3 @@ - Issue #36: Implemented rehearsal priority calculation and cue-sheet (CSV) / chart (JSON) exports - Issue #30: Added policy-constrained YouTube import with local fallback - Issue #26: Finalized roadmap and prepared application for initial release - -## [0.1.4] - 2026-05-15 - -### 추가됨 (Added) - -- `ChordsFeature` (코드 분석) 화면에서 각 파트(Role)의 `transpositionPlan`(이조/조옮김 계획)을 표시하는 기능을 추가했습니다. -- `RangesFeature` (음역대 분석) 화면에서 겹침 경고(Overlap warning) 외에 해당 파트의 채보(Transcription) 가능 노드 수를 요약하여 보여주는 기능을 추가했습니다. -- 신규 UI 요소에 대한 100% 테스트 커버리지를 보장하는 단위 테스트를 추가했습니다 (`apps/desktop/src/features/chords/index.test.tsx`, `apps/desktop/src/features/ranges/index.test.tsx`). diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 82c2c704..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,75 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Canonical agent guide - -`AGENTS.md` is the canonical agent operating guide — read and follow it before making changes. It defines the security workflow (`Security Notes`), supply-chain workflow, cross-platform build rules, GitHub bootstrap rules, code style, and safety guardrails. This file complements it with commands and architecture; when in doubt, `AGENTS.md` and the docs it references win. - -Agent execution and delegation rules live in `docs/agents/README.md`. PR canonicalization rules live in `docs/workflow/pr-continuity.md`. - -## Common commands - -Setup (Node >=22.13 <23, Python >=3.12 via `uv`, Rust stable only for the Tauri shell): - -```bash -npm install -uv sync --project services/analysis-engine --group dev -``` - -Primary verification — run before claiming any change complete; CI (`ci / build-and-test`) runs exactly this: - -```bash -./scripts/harness/quickcheck.sh # lint + typecheck + test + build + doc/security/supply-chain gates -BANDSCOPE_ENABLE_RUST_CHECK=1 ./scripts/harness/quickcheck.sh # adds the opt-in Rust/Tauri cargo check lane -``` - -Individual root gates (all part of quickcheck): - -```bash -npm run lint # workspace ESLint + doc/security/supply-chain checks + ruff + ruff format + bandit + docstring gate -npm run typecheck # tsc per workspace + mypy --strict on the Python engine -npm run test # JS workspace vitest suites + pytest with 100% coverage gate -npm run build # vite builds per workspace -``` - -Per-workspace and single-test: - -```bash -npm run test --workspace @bandscope/desktop # desktop suite (vitest + coverage) -npm --workspace @bandscope/desktop exec vitest run src/lib/export.test.ts # one frontend test file -npm run dev --workspace @bandscope/desktop # Vite dev server (browser fallback mode) -npm run storybook --workspace @bandscope/desktop # component workbench - -uv run --project services/analysis-engine pytest tests/test_chords.py # one Python test file (no coverage gate) -uv run --project services/analysis-engine pytest --cov=src/bandscope_analysis --cov-report=term-missing --cov-fail-under=100 # full Python gate -``` - -## Architecture - -BandScope is a local-first desktop app for rehearsal prep: it turns a song into likely harmony by section and role, a section roadmap, groove cues, stems, playable ranges, simplification/transposition cues, confidence flags, and rehearsal priorities. `ARCHITECTURE.md` is the authoritative reference; the analysis target is a `song -> section -> role` hierarchy, never a single song-wide chord track. - -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). `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. - -Data flow: React UI → Tauri IPC command → Rust validation + Python subprocess over stdin/stdout → job status and progress events emitted back to the UI. - -Supporting packages: - -- `packages/shared-types` — the stable TypeScript contracts (rehearsal domain, analysis jobs, confidence, provenance, export summaries) shared by the UI and the orchestration layer. Both sides must use these types instead of inventing parallel schemas; contracts are property-tested with fast-check. -- `packages/shared-config` — shared tsconfig/ESLint bases. -- `scripts/harness/quickcheck.sh` and `scripts/checks/` — fail-fast mechanical gates, including doc presence, plan `Security Notes`, forbidden patterns, and supply-chain/workflow-pinning verification. - -## Key conventions - -- Coverage is a hard gate: the Python engine requires 100% test coverage and 100% docstring coverage (Ruff `D100`–`D107` across `src`, `tests`, and repo scripts). Exported TypeScript declarations in `packages/shared-types` and `apps/desktop/src` require JSDoc with a description; `no-console` is an error. -- Gitflow: `develop` is the default branch; `feature/*` targets `develop`, `main` is the protected release branch. Direct pushes to protected branches are not allowed, and every merge needs the required checks plus a passing CodeRabbit review (see `CONTRIBUTING.md` and `docs/repository/gitflow.md`). -- The PR template (`.github/PULL_REQUEST_TEMPLATE.md`) requires a quickcheck confirmation, `Security Notes` (attack surface, trust boundary, mitigations, test points), a dependency/supply-chain checklist, and i18n impact. -- i18n: the UI ships Korean and English locales (`apps/desktop/src/locales/ko`, `en`). Any user-visible string change must update both. -- Documents under `docs/plans/` must include `Security Notes`; `scripts/checks/verify_security_notes.py` enforces this mechanically. -- Lockfiles (`package-lock.json`, `uv.lock`, `Cargo.lock`) are committed and must stay in sync; GitHub Actions are SHA-pinned. Adding a direct dependency requires the admission rationale defined in `AGENTS.md` and `docs/security/dependency-policy.md`. -- CI beyond quickcheck: `gate / ci / rust-check` (Tauri cargo check on macOS) and `build-baseline` Windows/macOS amd64+arm64 native builds are merge gates, alongside CodeQL, dependency-review, sbom, bandit, trivy, secret-scan, and security-audit workflows. Do not weaken or skip them. -- Version metadata lives in `VERSION`, the root `package.json`, and `CHANGELOG.md`; release flow is tag-driven (see `docs/operations/deploy-runbook.md`). diff --git a/README.md b/README.md index 74312e3e..6ae16c44 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # BandScope -BandScope is a public GitHub project for a local-first desktop app that turns a song into a practical rehearsal view: likely harmony by section and by instrument or vocal role, section roadmap, tempo and groove cues, rough stem previews, playable ranges, simplification hints, transposition or capo guidance, overlap cues, visible confidence, and rehearsal priorities without DAW complexity. +BandScope is a public GitHub project for a local-first desktop app that turns a song into a practical rehearsal view: likely harmony by section and by instrument or vocal role, section roadmap, tempo and groove cues, separated stems, playable ranges, simplification hints, transposition or capo guidance, overlap cues, visible confidence, and rehearsal priorities without DAW complexity. It does not promise notation-grade full arrangement transcription or DAW-style production editing. diff --git a/apps/desktop/.gitignore b/apps/desktop/.gitignore deleted file mode 100644 index 20687473..00000000 --- a/apps/desktop/.gitignore +++ /dev/null @@ -1 +0,0 @@ -storybook-static diff --git a/apps/desktop/.storybook/main.ts b/apps/desktop/.storybook/main.ts deleted file mode 100644 index fa5c0b52..00000000 --- a/apps/desktop/.storybook/main.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { StorybookConfig } from "@storybook/react-vite" -import tailwindcss from "@tailwindcss/vite" -import path from "node:path" -import { fileURLToPath } from "node:url" - -const dir = path.dirname(fileURLToPath(import.meta.url)) - -const config: StorybookConfig = { - stories: ["../src/**/*.stories.@(ts|tsx)"], - addons: [], - framework: { name: "@storybook/react-vite", options: {} }, - viteFinal: async (viteConfig) => { - viteConfig.plugins = [...(viteConfig.plugins ?? []), tailwindcss()] - viteConfig.resolve = viteConfig.resolve ?? {} - viteConfig.resolve.alias = { - ...(viteConfig.resolve.alias ?? {}), - "@": path.resolve(dir, "../src"), - } - return viteConfig - }, -} - -export default config diff --git a/apps/desktop/.storybook/preview.ts b/apps/desktop/.storybook/preview.ts deleted file mode 100644 index 81f8c701..00000000 --- a/apps/desktop/.storybook/preview.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Preview } from "@storybook/react-vite" - -import "../src/index.css" - -const preview: Preview = { - parameters: { - layout: "centered", - controls: { - matchers: { color: /(background|color)$/i, date: /Date$/i }, - }, - }, -} - -export default preview diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 3a3f2011..e8b42e09 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -6,8 +6,6 @@ "scripts": { "dev": "vite", "build": "vite build", - "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build", "lint": "eslint \"src/**/*.{ts,tsx}\" vite.config.ts", "typecheck": "tsc --noEmit", "test": "node -e \"require('node:fs').mkdirSync('coverage/.tmp', { recursive: true })\" && vitest run --coverage" @@ -22,12 +20,10 @@ "lucide-react": "^1.20.0", "react": "^19.2.4", "react-dom": "^19.2.7", - "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", "tw-animate-css": "^1.4.0" }, "devDependencies": { - "@storybook/react-vite": "^10.4.6", "@tailwindcss/vite": "^4.3.1", "@tauri-apps/cli": "^2.11.2", "@testing-library/jest-dom": "^6.6.3", @@ -39,7 +35,6 @@ "@vitest/coverage-v8": "^4.1.5", "eslint": "^10.5.0", "jsdom": "^29.1.1", - "storybook": "^10.4.6", "tailwindcss": "^4.2.4", "typescript": "^6.0.3", "typescript-eslint": "^8.60.1", diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index af45bdfc..27645d0c 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -1107,13 +1107,7 @@ async fn import_youtube_url( Err("YouTube import failed with an unknown error.".to_string()) } -const MAX_YOUTUBE_URL_LENGTH: usize = 2000; - fn is_supported_youtube_url(url: &str) -> bool { - if url.len() > MAX_YOUTUBE_URL_LENGTH { - return false; - } - let parsed_url = match url::Url::parse(url) { Ok(u) => u, Err(_) => return false, @@ -1526,9 +1520,6 @@ mod tests { )); assert!(!is_supported_youtube_url("https://youtu.be/abc123")); assert!(!is_supported_youtube_url("https://youtu.be/abc123DEF4!")); - - let long_url = format!("https://youtube.com/watch?v={}", "a".repeat(2000)); - assert!(!is_supported_youtube_url(&long_url)); } #[test] diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 838dcb8c..66a59ff1 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -49,7 +49,6 @@ import { EmptyState, ErrorState, LoadingState } from "./features/workspace/Works import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Progress } from "@/components/ui/progress"; -import { Toaster } from "@/components/ui/sonner"; const ANALYSIS_POLL_INTERVAL_MS = 250; const MAX_ERROR_DETAIL_LENGTH = 220; @@ -801,8 +800,6 @@ export function App() { - - ); } diff --git a/apps/desktop/src/components/ui/accordion.tsx b/apps/desktop/src/components/ui/accordion.tsx deleted file mode 100644 index 752e33a0..00000000 --- a/apps/desktop/src/components/ui/accordion.tsx +++ /dev/null @@ -1,76 +0,0 @@ -"use client" - -import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion" -import { ChevronDown } from "lucide-react" - -import { cn } from "@/lib/utils" - -/** Render a vertically stacked set of collapsible sections. */ -function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) { - return ( - - ) -} - -/** Render one collapsible accordion item. */ -function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) { - return ( - - ) -} - -/** Render the clickable header that toggles an item open or closed. */ -function AccordionTrigger({ - className, - children, - ...props -}: AccordionPrimitive.Trigger.Props) { - return ( - - svg]:rotate-180", - className - )} - {...props} - > - {children} - - - - ) -} - -/** Render the collapsible content panel of an item. */ -function AccordionContent({ - className, - children, - ...props -}: AccordionPrimitive.Panel.Props) { - return ( - -
{children}
-
- ) -} - -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/apps/desktop/src/components/ui/breadcrumb.tsx b/apps/desktop/src/components/ui/breadcrumb.tsx deleted file mode 100644 index 76061293..00000000 --- a/apps/desktop/src/components/ui/breadcrumb.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import * as React from "react" -import { ChevronRight, MoreHorizontal } from "lucide-react" - -import { cn } from "@/lib/utils" - -/** Render the breadcrumb navigation landmark. */ -function Breadcrumb(props: React.ComponentProps<"nav">) { - return