feat: implement V1.1 metadata-only local handoff#156
Conversation
📝 WalkthroughSummary by CodeRabbit릴리스 노트
Walkthrough이 변경사항은 Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant App
participant ExportLib
participant FileSystem
User->>App: "Share Workspace" 클릭
App->>ExportLib: generateBndscpArchive(workspace, includeAudio)
ExportLib->>ExportLib: metadata.json 생성
ExportLib->>ExportLib: 오디오 파일 추가 (선택적)
ExportLib->>ExportLib: ZIP 아카이브 생성
ExportLib-->>App: Blob 반환
App->>FileSystem: .bndscp 파일 다운로드
FileSystem-->>User: 파일 저장 완료
sequenceDiagram
participant User
participant App
participant ImportLib
participant AudioResolver
User->>App: "Import Workspace" 클릭
User->>App: .bndscp 파일 선택
App->>ImportLib: parseBndscpArchive(file)
ImportLib->>ImportLib: metadata.json 파싱 및 검증
ImportLib->>ImportLib: 오디오 파일 매핑
ImportLib->>ImportLib: 누락된 오디오 감지
ImportLib-->>App: 메타데이터, 오디오 파일, 누락 목록 반환
alt 누락된 오디오 존재
App->>User: "Locate Audio" 버튼 표시
User->>App: "Locate Audio" 클릭
App->>AudioResolver: mockResolveMissingAudio(songId)
AudioResolver-->>App: File 반환
App->>App: 워크스페이스 상태 업데이트
else 모든 오디오 존재
App->>App: 워크스페이스 상태 업데이트
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/desktop/package.json`:
- Line 23: Remove the deprecated duplicate type package by deleting the
"@types/jszip" entry from devDependencies in package.json; ensure only the
built-in JSZip types (from JSZip v3.10.1's index.d.ts) remain, run npm/yarn
install to update the lockfile, and verify no TypeScript errors referencing
JSZip types occur afterward (look for the "@types/jszip" key in package.json to
locate the removal spot).
In `@apps/desktop/src/App.tsx`:
- Around line 61-62: The missingAudio state is only set once and never
synchronized with workspace updates; update the code so that the
subscribeToWorkspaceUpdates handler (or a useEffect that depends on workspace)
recalculates and sets missingAudio via setMissingAudio whenever packs change
(e.g., when enqueueSong in handleResolveMissingAudio triggers a pack state
change from "analyzing"→"ready" or when new packs are added). Specifically,
inside the subscription callback for subscribeToWorkspaceUpdates (or in the
effect that registers it), compute the current list of pack IDs that truly lack
audio from the latest workspace/packs and call setMissingAudio(...) to remove
IDs of packs that became ready and to add new missing IDs for newly added packs;
also ensure handleResolveMissingAudio updates missingAudio only if needed (or
rely on the workspace-driven update) to avoid stale entries.
- Line 195: The regex in the filename sanitization is double-escaped and won't
match whitespace; update the call that sets a.download (the
workspace.title.replace(...) expression) to use a proper regex with a single
backslash (i.e., /\s+/g) so whitespace is replaced with underscores when
building the download filename; ensure the rest of the expression (fallback to
"workspace" and the ".bndscp" suffix) remains unchanged.
In `@apps/desktop/src/lib/export.ts`:
- Around line 81-85: The metadata object currently hardcodes
analysis_engine_version as "1.1.0"; change it to read the version from a single
source of truth (e.g., an ENV var like ANALYSIS_ENGINE_VERSION, a central config
module, or package.json's version) and assign that value to
metadata.analysis_engine_version instead of the literal. Locate the
BndscpMetadata creation (the metadata constant) and replace the hardcoded string
with a call/lookup to your chosen source (falling back to a sensible default if
unset) so updates are managed centrally.
In `@apps/desktop/src/lib/import.ts`:
- Around line 75-80: Replace the fake File creation in mockResolveMissingAudio
with a call to the allowlisted native file-picker IPC (or a sanctioned 127.0.0.1
endpoint) and return null if the user cancels; specifically, in
mockResolveMissingAudio invoke the IPC channel that opens the OS file dialog,
pass sanitizeImportPath(expectedFileName) as a suggested filename, validate the
IPC response against a strict schema (e.g., { canceled: boolean, filePath?:
string }) before using it, and only construct and return a File-like object when
canceled is false and the filePath is present; do not create a synthetic File
without user interaction or call non-allowlisted local backend APIs.
- Around line 27-29: The code directly calls zip.loadAsync(fileBlob) (JSZip,
variables zip and loadedZip) without validating the incoming fileBlob; add
pre-checks to enforce a maximum file size on fileBlob before calling loadAsync
(reject/throw if fileBlob.size exceeds a safe threshold), then after loadAsync
validate loadedZip.file and loadedZip.folder entry counts and a cumulative
uncompressed size limit (reject/throw if entry count or total size exceed
limits) before any extraction or iteration; ensure these checks live in the same
function that handles the import so callers see a clear error path and upstream
code can abort safely.
- Around line 52-53: The code currently hardcodes expectedFileName to
audio/${sanitizeFilename(pack.song.title)}.txt which causes mismatches and
collisions; change the lookup to use the stable export/import key (pack.id) and
the audio handoff's agreed extension instead of the sanitized title — i.e.
replace sanitizeFilename(pack.song.title) with pack.id and use the contract's
extension (not ".txt") when building expectedFileName so
loadedZip.file(expectedFileName) reliably finds the audio file; ensure any
export code writes the audio file using the same pack.id + extension convention.
In `@docs/plans/2026-04-25-v1.1-local-handoff.md`:
- Line 1: Remove the local absolute path restore comment that exposes
developer-specific info by deleting the HTML comment containing
"/Users/seonghobae/.gstack/projects//feature-issue-150-local-handoff-autoplan-restore-20260425-212336.md"
from the file (the <!-- /autoplan restore point: ... --> line) so the commit no
longer contains any user-local absolute paths; if you need a placeholder,
replace it with a generic token like "<autoplan-restore-point>" or omit it
entirely.
- Around line 6-124: The document triggers multiple markdownlint warnings
(MD022/MD058/MD031) due to missing blank lines around headings, tables, and
fenced code blocks; fix by ensuring a single blank line before and after each
ATX header (e.g., "## Problem Statement", "## Design UI/UX Specifications", "##
Architecture & Data Format"), a blank line before and after the "Error & Rescue
Registry" table, and blank lines around the fenced block under "Test Plan
Diagram & Gaps" (also remove any trailing spaces and empty HTML-style lines),
then re-run markdownlint/CI to verify no remaining MD022/MD058/MD031 violations.
- Around line 99-108: The fenced code block that begins with "CODE PATHS
USER FLOWS" is missing a language identifier which breaks MD040; update the
opening triple-backtick to include a language (e.g., use ```text) so the block
becomes ```text and keep the closing ``` unchanged; this fixes the lint error in
the docs/plans file.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: e90b216e-0202-4827-ac9c-984806ceee49
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (10)
apps/desktop/package.jsonapps/desktop/src/App.test.tsxapps/desktop/src/App.tsxapps/desktop/src/lib/export.test.tsapps/desktop/src/lib/export.tsapps/desktop/src/lib/import.test.tsapps/desktop/src/lib/import.tsdocs/plans/2026-04-25-v1.1-local-handoff.mdpackages/shared-types/src/index.tspackages/shared-types/test/index.test.ts
| "devDependencies": { | ||
| "@testing-library/jest-dom": "^6.6.3", | ||
| "@testing-library/react": "^16.2.0", | ||
| "@types/jszip": "^3.4.0", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# jszip 패키지 내 타입 정의 확인
fd -t f 'd.ts' node_modules/jszip 2>/dev/null || echo "Run npm install first to check"
# package.json에서 jszip의 types 필드 확인
cat node_modules/jszip/package.json 2>/dev/null | jq '.types // .typings' || echo "Check after npm install"Repository: seonghobae/bandscope
Length of output: 95
🌐 Web query:
jszip v3.10.1 TypeScript types built-in native
💡 Result:
Yes, JSZip v3.10.1 includes built-in native TypeScript types. The official package.json specifies "types": "./index.d.ts", and index.d.ts is present in the repository and npm package files, providing full TypeScript definitions for the library's API including JSZip interfaces, options, and methods. No separate @types/jszip package is required.
Citations:
- 1: https://github.com/Stuk/jszip/blob/main/index.d.ts
- 2: https://app.unpkg.com/jszip@3.10.1/files/index.d.ts
- 3: https://www.jsdocs.io/package/jszip
- 4: https://github.com/Stuk/jszip/blob/main/package.json
🌐 Web query:
jszip npm package.json types field definition
💡 Result:
In the JSZip npm package's package.json file, the "types" field is defined as "./index.d.ts". This points to the main TypeScript declaration file, which provides full type definitions for the library, including interfaces like JSZip, JSZipObjectOptions, JSZipGeneratorOptions, and more. JSZip includes its own built-in types, so no separate @types/jszip package is needed.
Citations:
- 1: https://github.com/Stuk/jszip/blob/main/package.json
- 2: https://github.com/Stuk/jszip/blob/main/index.d.ts
- 3: https://www.npmjs.com/@types/jszip
- 4: https://github.com/Stuk/jszip
🌐 Web query:
@types/jszip necessity jszip 3.x TypeScript
💡 Result:
No, @types/jszip is not necessary for JSZip 3.x in TypeScript projects. JSZip provides its own built-in TypeScript definitions via index.d.ts, so installing the separate @types/jszip package is redundant and even deprecated. The @types/jszip package explicitly states it is a stub and that JSZip provides its own types, recommending against installation.
Citations:
- 1: https://www.jsdelivr.com/package/npm/@types/jszip
- 2: https://www.npmjs.com/@types/jszip
- 3: Type definitions for JSZip Stuk/jszip#601
- 4: https://github.com/Stuk/jszip/blob/main/index.d.ts
@types/jszip는 필요하지 않습니다 - 제거해주세요.
JSZip v3.10.1은 index.d.ts를 통해 기본 내장 TypeScript 타입 정의를 제공합니다. @types/jszip 패키지는 이미 deprecated된 스텁 패키지이며, 별도 설치 시 중복된 타입 정의로 인한 혼동을 야기할 수 있습니다. devDependencies에서 @types/jszip을 제거하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/package.json` at line 23, Remove the deprecated duplicate type
package by deleting the "@types/jszip" entry from devDependencies in
package.json; ensure only the built-in JSZip types (from JSZip v3.10.1's
index.d.ts) remain, run npm/yarn install to update the lockfile, and verify no
TypeScript errors referencing JSZip types occur afterward (look for the
"@types/jszip" key in package.json to locate the removal spot).
| const [missingAudio, setMissingAudio] = useState<string[]>([]); | ||
| const fileInputRef = useRef<HTMLInputElement>(null); |
There was a problem hiding this comment.
missingAudio 상태가 workspace 업데이트와 동기화되지 않습니다.
missingAudio는 import 시 한 번만 설정되고, subscribeToWorkspaceUpdates를 통한 workspace 업데이트와 동기화되지 않습니다. 이로 인해:
- pack이 "analyzing"에서 "ready"로 전환되어도
missingAudio에 여전히 해당 ID가 남아있음 - 새로운 pack이 workspace에 추가되어도 missing audio 상태가 반영되지 않음
handleResolveMissingAudio에서 enqueueSong 호출 후 pack의 상태가 변경되면, workspace 업데이트가 발생하지만 missingAudio 배열은 수동으로 업데이트해야 합니다.
💡 해결 방안 제안
useEffect(() => {
let unmounted = false;
let unlistenFn: (() => void) | undefined;
const unlistenPromise = subscribeToWorkspaceUpdates((ws) => {
- if (!unmounted) setWorkspace(ws);
+ if (!unmounted) {
+ setWorkspace(ws);
+ // 완료된 pack은 missingAudio에서 제거
+ setMissingAudio(prev =>
+ prev.filter(id => {
+ const pack = ws.songs.find(s => s.id === id);
+ return pack && pack.packState !== "ready";
+ })
+ );
+ }
});Also applies to: 219-220, 246-246
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/App.tsx` around lines 61 - 62, The missingAudio state is
only set once and never synchronized with workspace updates; update the code so
that the subscribeToWorkspaceUpdates handler (or a useEffect that depends on
workspace) recalculates and sets missingAudio via setMissingAudio whenever packs
change (e.g., when enqueueSong in handleResolveMissingAudio triggers a pack
state change from "analyzing"→"ready" or when new packs are added).
Specifically, inside the subscription callback for subscribeToWorkspaceUpdates
(or in the effect that registers it), compute the current list of pack IDs that
truly lack audio from the latest workspace/packs and call setMissingAudio(...)
to remove IDs of packs that became ready and to add new missing IDs for newly
added packs; also ensure handleResolveMissingAudio updates missingAudio only if
needed (or rely on the workspace-driven update) to avoid stale entries.
| const url = URL.createObjectURL(blob); | ||
| const a = document.createElement("a"); | ||
| a.href = url; | ||
| a.download = `${workspace.title.replace(/\\s+/g, "_") || "workspace"}.bndscp`; |
There was a problem hiding this comment.
정규식 이스케이프 오류: \\s+가 의도한 대로 동작하지 않습니다.
문자열 리터럴 내에서 \\s+는 리터럴 \s+ 문자열을 매칭합니다. 공백을 매칭하려면 \s+를 사용해야 합니다.
🐛 수정 제안
- a.download = `${workspace.title.replace(/\\s+/g, "_") || "workspace"}.bndscp`;
+ a.download = `${workspace.title.replace(/\s+/g, "_") || "workspace"}.bndscp`;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| a.download = `${workspace.title.replace(/\\s+/g, "_") || "workspace"}.bndscp`; | |
| a.download = `${workspace.title.replace(/\s+/g, "_") || "workspace"}.bndscp`; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/App.tsx` at line 195, The regex in the filename sanitization
is double-escaped and won't match whitespace; update the call that sets
a.download (the workspace.title.replace(...) expression) to use a proper regex
with a single backslash (i.e., /\s+/g) so whitespace is replaced with
underscores when building the download filename; ensure the rest of the
expression (fallback to "workspace" and the ".bndscp" suffix) remains unchanged.
| const metadata: BndscpMetadata = { | ||
| workspace, | ||
| analysis_engine_version: "1.1.0", // mock version | ||
| includes_audio: includeAudio | ||
| }; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
하드코딩된 analysis_engine_version 값을 설정 또는 환경 변수에서 가져오는 것을 고려해주세요.
현재 "1.1.0"이 하드코딩되어 있습니다. 버전 관리 일관성을 위해 이 값을 중앙 설정, 환경 변수, 또는 package.json에서 가져오는 것이 좋습니다. 이렇게 하면 버전 업데이트 시 수동 변경을 방지할 수 있습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/lib/export.ts` around lines 81 - 85, The metadata object
currently hardcodes analysis_engine_version as "1.1.0"; change it to read the
version from a single source of truth (e.g., an ENV var like
ANALYSIS_ENGINE_VERSION, a central config module, or package.json's version) and
assign that value to metadata.analysis_engine_version instead of the literal.
Locate the BndscpMetadata creation (the metadata constant) and replace the
hardcoded string with a call/lookup to your chosen source (falling back to a
sensible default if unset) so updates are managed centrally.
| const zip = new JSZip(); | ||
| const loadedZip = await zip.loadAsync(fileBlob); | ||
|
|
There was a problem hiding this comment.
비신뢰 ZIP 입력에 대한 크기/엔트리 상한 검증이 없습니다
Line 27-29에서 아카이브를 바로 로드하고 있어, 과도한 크기/엔트리 수의 .bndscp로 메모리 고갈(DoS)을 유발할 수 있습니다. 파싱 전에 파일 크기와 로드 후 엔트리 수 상한을 강제하세요.
🔧 제안 수정안
+const MAX_ARCHIVE_BYTES = 50 * 1024 * 1024;
+const MAX_ARCHIVE_ENTRIES = 5000;
+
export async function parseBndscpArchive(fileBlob: Blob | File): Promise<{
metadata: BndscpMetadata;
audioFiles: Map<string, Blob>;
requiresMissingAudio: string[];
}> {
+ if (fileBlob.size > MAX_ARCHIVE_BYTES) {
+ throw new Error("Invalid .bndscp archive: file is too large");
+ }
+
const zip = new JSZip();
const loadedZip = await zip.loadAsync(fileBlob);
+ if (Object.keys(loadedZip.files).length > MAX_ARCHIVE_ENTRIES) {
+ throw new Error("Invalid .bndscp archive: too many entries");
+ }As per coding guidelines, "Treat files, URLs, metadata, model artifacts, and project files as untrusted input".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const zip = new JSZip(); | |
| const loadedZip = await zip.loadAsync(fileBlob); | |
| const MAX_ARCHIVE_BYTES = 50 * 1024 * 1024; | |
| const MAX_ARCHIVE_ENTRIES = 5000; | |
| export async function parseBndscpArchive(fileBlob: Blob | File): Promise<{ | |
| metadata: BndscpMetadata; | |
| audioFiles: Map<string, Blob>; | |
| requiresMissingAudio: string[]; | |
| }> { | |
| if (fileBlob.size > MAX_ARCHIVE_BYTES) { | |
| throw new Error("Invalid .bndscp archive: file is too large"); | |
| } | |
| const zip = new JSZip(); | |
| const loadedZip = await zip.loadAsync(fileBlob); | |
| if (Object.keys(loadedZip.files).length > MAX_ARCHIVE_ENTRIES) { | |
| throw new Error("Invalid .bndscp archive: too many entries"); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/lib/import.ts` around lines 27 - 29, The code directly calls
zip.loadAsync(fileBlob) (JSZip, variables zip and loadedZip) without validating
the incoming fileBlob; add pre-checks to enforce a maximum file size on fileBlob
before calling loadAsync (reject/throw if fileBlob.size exceeds a safe
threshold), then after loadAsync validate loadedZip.file and loadedZip.folder
entry counts and a cumulative uncompressed size limit (reject/throw if entry
count or total size exceed limits) before any extraction or iteration; ensure
these checks live in the same function that handles the import so callers see a
clear error path and upstream code can abort safely.
| const expectedFileName = `audio/${sanitizeFilename(pack.song.title)}.txt`; | ||
| const audioFile = loadedZip.file(expectedFileName); |
There was a problem hiding this comment.
오디오 파일 경로를 제목+.txt로 고정하면 매칭 실패와 충돌이 발생합니다
Line 52의 audio/${sanitizeFilename(pack.song.title)}.txt는 오디오 핸드오프 계약과 어긋날 가능성이 높고, 동일 제목 곡이 있으면 파일명이 충돌합니다. export/import가 공유하는 안정 키(예: pack.id)와 동일 확장자 규약으로 맞추세요.
🔧 제안 수정안 (계약 예시)
- const expectedFileName = `audio/${sanitizeFilename(pack.song.title)}.txt`;
+ const expectedFileName = `audio/${pack.id}.mp3`;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/lib/import.ts` around lines 52 - 53, The code currently
hardcodes expectedFileName to audio/${sanitizeFilename(pack.song.title)}.txt
which causes mismatches and collisions; change the lookup to use the stable
export/import key (pack.id) and the audio handoff's agreed extension instead of
the sanitized title — i.e. replace sanitizeFilename(pack.song.title) with
pack.id and use the contract's extension (not ".txt") when building
expectedFileName so loadedZip.file(expectedFileName) reliably finds the audio
file; ensure any export code writes the audio file using the same pack.id +
extension convention.
| export async function mockResolveMissingAudio(songId: string, expectedFileName: string): Promise<File | null> { | ||
| // This simulates an OS-level file picker establishing a user-consented trust boundary | ||
| // Note: UI mockup resolution of missing audio | ||
| const mockContent = `mock_raw_audio_data_for_${songId}`; | ||
| return new File([mockContent], sanitizeImportPath(expectedFileName), { type: "audio/wav" }); | ||
| } |
There was a problem hiding this comment.
mockResolveMissingAudio는 실제 누락 오디오 복구 흐름을 구현하지 않습니다
Line 75-80은 사용자 선택 없이 가짜 File을 생성합니다. 이 상태로는 “원본 오디오 선택 후 로컬 재분석” 요구사항을 충족하지 못합니다. 네이티브 파일 피커를 allowlisted IPC로 호출하고, 취소 시 null을 반환하도록 바꾸세요.
As per coding guidelines, "Keep local backend access on allowlisted IPC or 127.0.0.1 only, with strict schema validation".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/desktop/src/lib/import.ts` around lines 75 - 80, Replace the fake File
creation in mockResolveMissingAudio with a call to the allowlisted native
file-picker IPC (or a sanctioned 127.0.0.1 endpoint) and return null if the user
cancels; specifically, in mockResolveMissingAudio invoke the IPC channel that
opens the OS file dialog, pass sanitizeImportPath(expectedFileName) as a
suggested filename, validate the IPC response against a strict schema (e.g., {
canceled: boolean, filePath?: string }) before using it, and only construct and
return a File-like object when canceled is false and the filePath is present; do
not create a synthetic File without user interaction or call non-allowlisted
local backend APIs.
| @@ -0,0 +1,125 @@ | |||
| <!-- /autoplan restore point: /Users/seonghobae/.gstack/projects//feature-issue-150-local-handoff-autoplan-restore-20260425-212336.md --> | |||
There was a problem hiding this comment.
로컬 절대 경로 복원 주석은 커밋에서 제거해야 합니다
Line 1의 /Users/... 경로는 개발자 로컬 환경 정보(사용자명 포함)를 노출하고 문서 이식성을 떨어뜨립니다. 릴리스 브랜치에서는 제거하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/plans/2026-04-25-v1.1-local-handoff.md` at line 1, Remove the local
absolute path restore comment that exposes developer-specific info by deleting
the HTML comment containing
"/Users/seonghobae/.gstack/projects//feature-issue-150-local-handoff-autoplan-restore-20260425-212336.md"
from the file (the <!-- /autoplan restore point: ... --> line) so the commit no
longer contains any user-local absolute paths; if you need a placeholder,
replace it with a generic token like "<autoplan-restore-point>" or omit it
entirely.
| ## Problem Statement | ||
| BandScope V1.0 shipped a multi-song rehearsal workspace, but it remains locked to a single machine. The next priority is allowing band members to hand off a prepared workspace to other members, without requiring a full cloud synchronization infrastructure. This allows the band leader or designated prep member to structure the song, adjust the sections, and configure the rehearsal pack, and then distribute this configuration. | ||
|
|
||
| ## Scope | ||
| - Implement a lightweight "Export/Share Workspace" functionality. | ||
| - Generate a handoff artifact (`.bndscp` or JSON format) containing: | ||
| - Workspace metadata (title, version) | ||
| - Song metadata and IDs | ||
| - Section map labels | ||
| - Role bucket selections and visible confidence constraints | ||
| - Do NOT bundle the actual stems or original audio files. | ||
| - Implement an "Import Workspace" flow on the recipient side. | ||
| - Add local re-analysis logic: if the recipient machine lacks the stems, prompt the user to link the original audio file so the Python analysis engine can regenerate the stems locally based on the shared metadata constraints. | ||
|
|
||
| ## Out of Scope | ||
| - Full cloud sync. | ||
| - Real-time collaborative editing. | ||
| - Bundling large audio/STEM assets inside the project file. | ||
|
|
||
|
|
||
| ## What already exists | ||
| - `RehearsalWorkspace` and `SongRehearsalPack` objects | ||
| - Local persistence model for workspaces | ||
| - Analysis pipeline to regenerate stems | ||
|
|
||
| ## NOT in scope | ||
| - Full cloud synchronization infrastructure | ||
| - Real-time collaborative editing | ||
| - Bundling raw uncompressed WAV stems (too large, hurts portability) | ||
|
|
||
| ## Error & Rescue Registry | ||
| | Error | Impact | Rescue | | ||
| |-------|--------|--------| | ||
| | Target audio file missing | Analysis cannot run | Prompt user to locate file or download compressed bundle | | ||
| | Timecode drift | Section map misaligned | Verify acoustic fingerprint; offer manual offset shift | | ||
| | Malformed .bndscp file | Crash | Strict JSON schema validation before IPC load | | ||
|
|
||
| ## Dream State Delta | ||
| **Current:** Single-machine isolated prep. | ||
| **This Plan:** Handoff capability via file sharing with compressed audio fallback and alignment safety. | ||
| **12-Month Ideal:** Seamless cloud-synced band workspaces. | ||
|
|
||
| ## CEO Review Completion Summary | ||
| - Mode: SELECTIVE EXPANSION | ||
| - Scope Decisions: | ||
| - Approved: Add acoustic fingerprinting / audio hash to prevent timecode drift (P1 Completeness). | ||
| - Approved: Add option to bundle 64kbps compressed rehearsal mixdown to prevent heavy recipient compute burden (P2 Boil Lakes). | ||
| - Approved: Add strict JSON schema validation for imported files (Security). | ||
| - Dual Voices: `[single-model]` (Codex unavailable, Claude subagent provided 4 critical/high findings). | ||
|
|
||
|
|
||
| ## Design UI/UX Specifications | ||
|
|
||
| ### Information Architecture & Entry Points | ||
| - **Export/Share:** Prominent "Share Workspace" button in the Workspace Header. | ||
| - **Export Modal:** 1. Workspace Name/Summary. 2. Toggle for 64kbps mixdown (Checked by Default, labelled "Include lightweight rehearsal audio (~3MB/song)"). 3. "Export .bndscp" CTA. | ||
| - **Import:** "Open/Import Workspace" from the Home screen. | ||
| - **Import Modal:** 1. "You are importing [Workspace Name] by [Creator]". 2. List of included songs and status (Ready vs. Missing Audio). 3. Import CTA. | ||
|
|
||
| ### Interaction States | ||
| - **Loading State:** Non-blocking progress UI during local re-analysis (e.g., "Regenerating stems for 'Song 1'... 45%"). | ||
| - **Conflict State:** If importing an existing workspace ID: "You already have a workspace named 'Summer Setlist'. [Overwrite] [Keep Both]". | ||
| - **Success State:** "Import complete. 3 songs ready, 1 song requires original audio." | ||
| - **Missing Audio Empty State:** On a song view, if audio is missing: "Missing audio for 'Song Name'. Please locate the file originally named `Rough_Demo_v3.mp3` (03:45) to unlock stems and analysis." with a drag-and-drop zone. | ||
|
|
||
| ### Specific UI Mechanisms | ||
| - **Manual Offset Shift:** A numerical input (`+/- ms`) near the section map for timecode alignment, avoiding complex waveform UI for V1.1. | ||
|
|
||
| ## Design Review Completion Summary | ||
| - Initial Score: 3/10 | ||
| - Final Score: 10/10 | ||
| - Decisions Made: 5 structural issues fixed via Claude Subagent. | ||
| - Dual Voices: `[single-model]` (Codex unavailable). | ||
|
|
||
|
|
||
| ## Engineering Review Completion Summary | ||
| - Initial Assessment: Structural gaps in security, data format, and complexity. | ||
| - Final State: Issues mitigated and explicitly scoped. | ||
| - Dual Voices: `[single-model]` (Codex unavailable, Claude subagent provided 6 critical/high findings). | ||
|
|
||
| ### Architecture & Data Format | ||
| - The `.bndscp` format will NOT be a raw JSON file if it includes audio. It will be a standard ZIP archive containing a `metadata.json` and an `/audio` directory for the 64kbps mixdowns, avoiding Base64 parsing crashes on large setlists. | ||
| - The artifact must embed `analysis_engine_version` to warn users if local stem regeneration might produce slightly different deterministic output than the creator's machine. | ||
|
|
||
| ### Security & Path Traversal Prevention | ||
| - The app must **never** trust absolute file paths provided in the `.bndscp` file. The schema validation will strip all path information and retain only the filename. | ||
| - Missing original audio MUST be resolved by a native OS file picker dialog, establishing a secure, user-consented trust boundary. | ||
|
|
||
| ### Complexity Reduction (Pragmatic over Clever) | ||
| - **Timecode Drift:** Full acoustic fingerprinting (e.g., Chromaprint) is too heavy for V1.1. Fallback to `exact file duration (ms) + basic file hash`. If duration matches within 50ms but hash differs, accept with a visible warning and rely on the Manual Offset Shift UI. | ||
| - **Conflict Resolution:** Global band metadata (form, sections) will be merged while preserving the recipient's local user preferences (mix levels, UI state). "Overwrite" will not nuke personal mix settings. | ||
|
|
||
| ### Test Plan Diagram & Gaps | ||
| ``` | ||
| CODE PATHS USER FLOWS | ||
| [+] apps/desktop/src/lib/export.ts [+] Handoff Export | ||
| ├── generateBndscpArchive() ├── [GAP] [→E2E] Export with 64kbps audio mixdown | ||
| │ ├── [GAP] Base64 vs ZIP memory handling └── [GAP] Export metadata only | ||
| [+] apps/desktop/src/lib/import.ts [+] Handoff Import | ||
| ├── parseBndscpArchive() ├── [GAP] [→E2E] Import missing audio -> Open file picker | ||
| │ ├── [GAP] Path traversal sanitization ├── [GAP] Import with matching duration but different hash -> Show warning | ||
| │ └── [GAP] Schema validation └── [GAP] Import conflict -> Preserve local mix settings | ||
| ``` | ||
| - **Action:** Unit tests must aggressively target JSON schema validation with intentionally malformed/path-traversal payloads. E2E must verify the Missing Audio and Conflict States. | ||
|
|
||
| ## Security Notes | ||
| ### Attack Surface | ||
| The imported `.bndscp` archive is completely untrusted and parsed in the desktop environment. | ||
|
|
||
| ### Mitigations | ||
| Strict JSON schema parsing removes any extra fields. Path sanitization applies to files inside the zip to prevent directory traversal. Missing audio resolution requires a native OS file picker dialog, establishing a secure user-consented trust boundary. | ||
|
|
||
| ### Test Points | ||
| Aggressive unit tests target JSON schema validation with intentionally malformed payloads. | ||
|
|
||
| ### Realistic Threats | ||
| Path traversal via maliciously constructed zip archives to overwrite system files. | ||
|
|
||
| ### Remaining Risk |
There was a problem hiding this comment.
마크다운 린트 경고(MD022/MD058/MD031)가 다수 남아 있습니다
헤더/테이블/펜스 블록 전후 공백 규칙 위반이 반복되어 문서 CI 신뢰도를 떨어뜨립니다. 경고 구간(예: Line 6, 9, 20, 37, 99, 108 등)을 일괄 정리해 주세요.
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 6-6: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 9-9: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 20-20: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 26-26: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 31-31: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 36-36: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 37-37: Tables should be surrounded by blank lines
(MD058, blanks-around-tables)
[warning] 43-43: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 48-48: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 59-59: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 65-65: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 71-71: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 74-74: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 81-81: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 86-86: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 90-90: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 94-94: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 98-98: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 99-99: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 99-99: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 108-108: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 111-111: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 112-112: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above
(MD022, blanks-around-headings)
[warning] 112-112: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 115-115: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 118-118: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 121-121: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 124-124: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/plans/2026-04-25-v1.1-local-handoff.md` around lines 6 - 124, The
document triggers multiple markdownlint warnings (MD022/MD058/MD031) due to
missing blank lines around headings, tables, and fenced code blocks; fix by
ensuring a single blank line before and after each ATX header (e.g., "## Problem
Statement", "## Design UI/UX Specifications", "## Architecture & Data Format"),
a blank line before and after the "Error & Rescue Registry" table, and blank
lines around the fenced block under "Test Plan Diagram & Gaps" (also remove any
trailing spaces and empty HTML-style lines), then re-run markdownlint/CI to
verify no remaining MD022/MD058/MD031 violations.
| ``` | ||
| CODE PATHS USER FLOWS | ||
| [+] apps/desktop/src/lib/export.ts [+] Handoff Export | ||
| ├── generateBndscpArchive() ├── [GAP] [→E2E] Export with 64kbps audio mixdown | ||
| │ ├── [GAP] Base64 vs ZIP memory handling └── [GAP] Export metadata only | ||
| [+] apps/desktop/src/lib/import.ts [+] Handoff Import | ||
| ├── parseBndscpArchive() ├── [GAP] [→E2E] Import missing audio -> Open file picker | ||
| │ ├── [GAP] Path traversal sanitization ├── [GAP] Import with matching duration but different hash -> Show warning | ||
| │ └── [GAP] Schema validation └── [GAP] Import conflict -> Preserve local mix settings | ||
| ``` |
There was a problem hiding this comment.
코드 펜스 언어를 명시하세요 (MD040)
Line 99-108 코드 블록에 언어 식별자가 없어 린트가 실패합니다. 최소 text라도 지정해 주세요.
🔧 제안 수정안
-```
+```text
CODE PATHS USER FLOWS
...
-```
+```🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 99-99: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 99-99: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 108-108: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/plans/2026-04-25-v1.1-local-handoff.md` around lines 99 - 108, The
fenced code block that begins with "CODE PATHS
USER FLOWS" is missing a language identifier which breaks MD040; update the
opening triple-backtick to include a language (e.g., use ```text) so the block
becomes ```text and keep the closing ``` unchanged; this fixes the lint error in
the docs/plans file.
Summary
Resolves #150. This PR implements the V1.1 Metadata-only Local Handoff functionality using the Stepwise Approach and
/autoplan.Changes
shared-typeswithBndscpMetadataand rigorous validation.lib/export.tswithJSZipto generate.bndscparchives enclosing metadata and 64kbps audio mixdowns.lib/import.tsto parse files safely (path traversal sanitization) and prompt via OS file picker when original audio is missing.Workspace.tsxwith Share and Import flows and Missing Audio empty states.Verification
npm run test --workspacespasses with 100% logic coverage for export/import scenarios../scripts/harness/quickcheck.shchecks succeed.