Skip to content
Draft
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
416 commits
Select commit Hold shift + click to select a range
6a329fe
style(audio): restore transcription EOF formatting
seonghobae Sep 7, 2026
44a9087
style(audio): restore temporal EOF formatting
seonghobae Sep 7, 2026
31a7b79
style(audio): restore separator EOF formatting
seonghobae Sep 7, 2026
8adcec8
test(audio): align resource policy version regression
seonghobae Sep 7, 2026
f294100
test(audio): bind metadata errors to policy version
seonghobae Sep 7, 2026
370a3d5
test(runtime): require native analysis cancellation
seonghobae Sep 7, 2026
0238c67
fix(runtime): add typed analysis cancellation error
seonghobae Sep 7, 2026
7aa8055
fix(runtime): expose cancelled job error contract
seonghobae Sep 7, 2026
4b85582
fix(runtime): cancel active analysis children
seonghobae Sep 7, 2026
c4e2f30
docs(runtime): record native cancellation boundary
seonghobae Sep 7, 2026
0a54d60
docs(runtime): record analysis cancellation behavior
seonghobae Sep 7, 2026
12cc6bd
test(runtime): bind cancellation error wire value
seonghobae Sep 7, 2026
36c5605
fix(runtime): declare cancellation command permission
seonghobae Sep 7, 2026
a90a7ea
fix(runtime): grant analysis cancellation capability
seonghobae Sep 7, 2026
b746f60
fix(runtime): add generated cancellation permission contract
seonghobae Sep 7, 2026
9e5d1d5
test(runtime): bind cancellation command capability
seonghobae Sep 7, 2026
2dac5b6
docs(audio): bind cancellation to Tauri runtime authority
seonghobae Sep 7, 2026
2f254ac
docs(security): bind analysis cancellation IPC authority
seonghobae Sep 7, 2026
1756759
docs(architecture): record Tauri command authority boundary
seonghobae Sep 7, 2026
d3f6b53
test(tauri): require cancellation permission in generated schemas
seonghobae Sep 7, 2026
a4c1d71
fix(tauri): refresh generated cancellation capability snapshot
seonghobae Sep 7, 2026
ae4d4d3
fix(tauri): regenerate cancellation permission schema
seonghobae Sep 7, 2026
3bfe659
fix(tauri): restore complete generated desktop schema
seonghobae Sep 7, 2026
a9a7e27
test(tauri): fail on tracked generated schema drift
seonghobae Sep 7, 2026
ea97fc0
fix(tauri): avoid build-mutated schema worktree assertion
seonghobae Sep 7, 2026
5e0180e
docs(architecture): align resource admission and cancellation truth
seonghobae Sep 7, 2026
2d21b6e
docs(architecture): sync local-first resource boundary
seonghobae Sep 7, 2026
1161252
docs(doctoring): trace process-tree containment constraints
seonghobae Sep 7, 2026
6b416c0
test(audio): enforce encoded size at decode boundary
seonghobae Sep 7, 2026
6afb2d8
fix(audio): enforce encoded bytes in decode port
seonghobae Sep 7, 2026
959891e
test(audio): reject non-mono decoder output before normalization
seonghobae Sep 7, 2026
65c9b6a
fix(audio): fail closed on non-mono decoder shapes
seonghobae Sep 7, 2026
ec6c41e
style(audio): restore canonical Python file terminator
seonghobae Sep 7, 2026
b34be05
test(audio): require renderer cancellation bridge
seonghobae Sep 7, 2026
aa07f08
fix(audio): expose cancellation through renderer bridge
seonghobae Sep 7, 2026
4fed274
test(audio): cover cancellation bridge races
seonghobae Sep 7, 2026
d3a50f2
test(audio): expose terminal cancellation race
seonghobae Sep 7, 2026
7991631
test(audio): pin cancellation acceptance lock
seonghobae Sep 7, 2026
2fba61c
fix(audio): serialize terminal cancellation
seonghobae Sep 7, 2026
fced227
docs(audio): record cancellation ordering invariant
seonghobae Sep 7, 2026
23445c1
test(audio): expose queued cancellation cleanup race
seonghobae Sep 7, 2026
4956aac
fix(audio): serialize queued cancellation cleanup
seonghobae Sep 7, 2026
c8a4869
fix(score): restore project score root call
seonghobae Sep 7, 2026
f402f54
docs: establish product technical gap baseline
seonghobae Sep 7, 2026
3dd62bb
test(tauri): gate cancellation on runtime authority
seonghobae Sep 7, 2026
aa1c938
test(tauri): match generic cancellation finalizer
seonghobae Sep 7, 2026
db1a18f
test(tauri): parse generated cancellation schemas
seonghobae Sep 7, 2026
a8bd64a
test(tauri): validate cancellation schema structure
seonghobae Sep 7, 2026
8f0d89e
style(tauri): format schema contract helper
seonghobae Sep 7, 2026
b0384cb
test(audio): reject post-admission decode growth
seonghobae Sep 7, 2026
c0b9c2f
fix(audio): bound decoder reads to admitted bytes
seonghobae Sep 7, 2026
f995307
test(audio): cover bounded readinto EOF semantics
seonghobae Sep 7, 2026
7152b92
test(audio): cover bounded seek branches
seonghobae Sep 7, 2026
7413f92
docs(audio): doctor bounded decode authority
seonghobae Sep 7, 2026
a7282aa
test(audio): reject invalid publication identity JSON
seonghobae Sep 7, 2026
43b889e
fix(audio): validate publication identity deserialization
seonghobae Sep 7, 2026
1789da7
docs(product): refresh audio admission gap baseline
seonghobae Sep 7, 2026
593d44e
test(audio): expose pre-normalization decoder budget gap
seonghobae Sep 7, 2026
f606236
fix(audio): reject over-budget decoder buffers before normalization
seonghobae Sep 7, 2026
2299b21
docs(product): record decoder pre-normalization resource guard
seonghobae Sep 7, 2026
c040eed
docs(audio): distinguish decoder allocation from canonical artifact b…
seonghobae Sep 7, 2026
24465db
docs(changelog): clarify decoder allocation admission
seonghobae Sep 7, 2026
dcfa90b
test(audio): cap finiteness validation temporary memory
seonghobae Sep 7, 2026
a98165a
fix(audio): bound finiteness scan temporary memory
seonghobae Sep 7, 2026
bfeefe8
docs(audio): record bounded finiteness validation memory
seonghobae Sep 7, 2026
f7f5be0
docs(audio): trace bounded finiteness scan memory
seonghobae Sep 7, 2026
6c559ca
docs(audio): record bounded finiteness scan fix
seonghobae Sep 7, 2026
9b5d1e9
test(audio): assert finiteness rejection provenance
seonghobae Sep 7, 2026
8a4d6f0
test(audio): freeze decoder dtype and resampler contract
seonghobae Sep 7, 2026
2d98bfb
fix(audio): pin canonical decode dtype and resampler
seonghobae Sep 7, 2026
bcb533f
test(audio): align decode-port contract with pinned resampler
seonghobae Sep 7, 2026
3761c75
fix(audio): satisfy reproducibility regression lint
seonghobae Sep 7, 2026
e24d7ce
fix(audio): apply pinned Ruff formatter output
seonghobae Sep 7, 2026
5e303e0
docs(audio): pin canonical decoder reproducibility evidence
seonghobae Sep 7, 2026
815841d
docs(changelog): record canonical decoder pin
seonghobae Sep 7, 2026
c60fb7c
docs(audio): narrow decoder reproducibility claim
seonghobae Sep 7, 2026
fb000fb
docs(changelog): narrow decoder reproducibility claim
seonghobae Sep 7, 2026
da43fea
test(audio): reject decoder views retaining oversized backing memory
seonghobae Sep 7, 2026
e2d39f9
fix(audio): reject decoder views with hidden backing memory
seonghobae Sep 7, 2026
f80b962
docs(audio): record hidden decoder backing-memory admission gap
seonghobae Sep 7, 2026
ab94470
test(audio): require owned PCM for decoder views
seonghobae Sep 7, 2026
1a35277
fix(audio): detach decoder views into owned PCM
seonghobae Sep 7, 2026
44fbdcf
docs(audio): distinguish retained artifact memory from decoder peak RSS
seonghobae Sep 7, 2026
eab2c68
docs(audio): record owned canonicalization of decoder views
seonghobae Sep 7, 2026
8fc4da8
docs(audio): doctor decoder view ownership boundary
seonghobae Sep 7, 2026
574e86d
test(audio): reject malformed decoder dtypes
seonghobae Sep 7, 2026
8f7e1a5
fix(audio): fail closed on malformed decoder dtypes
seonghobae Sep 7, 2026
45b1727
docs(audio): record decoder dtype admission boundary
seonghobae Sep 7, 2026
bc8aa65
test(audio): prove dtype rejection precedes PCM allocation
seonghobae Sep 7, 2026
25ff54f
test(audio): reject canonicalization budget expansion
seonghobae Sep 7, 2026
2216a1d
fix(audio): budget canonical PCM before allocation
seonghobae Sep 7, 2026
43ce16f
docs(audio): record canonicalization budget boundary
seonghobae Sep 7, 2026
0510b57
test(audio): resolve duplicate resource-policy import
seonghobae Sep 7, 2026
0467043
test(audio): pin allocation rejection provenance
seonghobae Sep 7, 2026
4861e78
test(audio): reject canonical allocation exhaustion
seonghobae Sep 7, 2026
c19f60b
fix(audio): normalize canonical allocation exhaustion
seonghobae Sep 7, 2026
664efd0
docs(audio): record allocator-failure claim boundary
seonghobae Sep 7, 2026
ba53156
docs(audio): record canonical allocation exhaustion
seonghobae Sep 7, 2026
fb94f21
test(audio): distinguish pre-canonical allocation failure
seonghobae Sep 7, 2026
cacc982
fix(audio): scope allocator budget mapping to canonical copy
seonghobae Sep 7, 2026
69adec3
docs(audio): distinguish canonical allocation exhaustion
seonghobae Sep 7, 2026
95d9cc7
test(audio): pin allocator exception provenance
seonghobae Sep 7, 2026
cd3e0d5
test(audio): reject empty YouTube artifact with correct reason
seonghobae Sep 7, 2026
f925e23
fix(audio): preserve YouTube admission failure reason
seonghobae Sep 7, 2026
eed3f8e
fix(audio): lease YouTube artifact ownership per video
seonghobae Sep 7, 2026
d04d1c1
test(audio): create malformed YouTube output under active lease
seonghobae Sep 7, 2026
ef95475
test(audio): isolate same-ID YouTube cache ownership
seonghobae Sep 7, 2026
ab7a4c1
fix(audio): bind YouTube cleanup to leased video identity
seonghobae Sep 7, 2026
8facfa0
test(audio): reject noncanonical YouTube completed filename
seonghobae Sep 7, 2026
c4c2c9e
fix(audio): require canonical YouTube completed artifact name
seonghobae Sep 7, 2026
ef5b223
docs(audio): bind YouTube completion to canonical filename
seonghobae Sep 7, 2026
f23ba9f
test(audio): preserve nontransient same-ID cache files
seonghobae Sep 7, 2026
3dc1bb4
test(audio): bound transient sibling sweep
seonghobae Sep 7, 2026
b82bfdf
fix(audio): restrict YouTube abort cleanup authority
seonghobae Sep 7, 2026
d2b72b9
test(audio): preserve yt-dlp part-fragment cleanup
seonghobae Sep 7, 2026
09bb020
fix(audio): normalize yt-dlp part-fragment cleanup stems
seonghobae Sep 7, 2026
be97c73
test(audio): preserve YouTube IDs containing fragment tokens
seonghobae Sep 8, 2026
98366b6
fix(audio): preserve fragment tokens in YouTube video IDs
seonghobae Sep 8, 2026
0e11700
style(audio): restore youtube module terminator
seonghobae Sep 8, 2026
3c39fa7
docs(audio): record YouTube fragment identity boundary
seonghobae Sep 8, 2026
7664e34
docs(audio): record fragment identity cleanup fix
seonghobae Sep 8, 2026
c09ec58
test(audio): constrain yt-dlp fragment cleanup to ASCII indices
seonghobae Sep 8, 2026
668d726
fix(audio): match yt-dlp ASCII fragment indices exactly
seonghobae Sep 8, 2026
ab569ca
docs(audio): narrow yt-dlp fragment cleanup authority
seonghobae Sep 8, 2026
740c357
docs(audio): trace yt-dlp fragment index grammar
seonghobae Sep 8, 2026
21458ae
docs(audio): record ASCII fragment cleanup contract
seonghobae Sep 8, 2026
f551694
test(audio): reject fragment lookalikes ending in part
seonghobae Sep 8, 2026
7a0387f
fix(audio): reject fragment suffix lookalikes before part cleanup
seonghobae Sep 8, 2026
af94694
test(audio): require Unix analysis process-group containment
seonghobae Sep 8, 2026
4e0aa5d
fix(audio): contain analysis descendants on Unix
seonghobae Sep 8, 2026
3ece757
docs(audio): record Unix cancellation containment
seonghobae Sep 8, 2026
e6f5d36
docs(audio): trace Unix process-group cancellation
seonghobae Sep 8, 2026
55764a1
docs(audio): log Unix descendant cancellation
seonghobae Sep 8, 2026
a07be5c
docs(architecture): record Unix analysis containment
seonghobae Sep 8, 2026
143f04b
docs(security): narrow Unix cancellation claim
seonghobae Sep 8, 2026
65c94f7
test(audio): expose YouTube descendant timeout leak
seonghobae Sep 8, 2026
81a4890
fix(audio): contain YouTube import descendants on Unix
seonghobae Sep 8, 2026
9c36cf0
test(audio): verify YouTube containment in shell gate
seonghobae Sep 8, 2026
e8b887c
docs(audio): record YouTube descendant containment gap repair
seonghobae Sep 8, 2026
5f7692f
docs(audio): record YouTube subprocess containment
seonghobae Sep 8, 2026
066d891
docs(architecture): include YouTube process containment
seonghobae Sep 8, 2026
76ca2c0
docs(security): include YouTube descendant containment
seonghobae Sep 8, 2026
91b64cf
docs(doctoring): trace YouTube process containment
seonghobae Sep 8, 2026
04e4d5c
test(process): require shared containment owner
seonghobae Sep 8, 2026
25dd7a5
refactor(process): share containment primitives
seonghobae Sep 8, 2026
e772af6
docs(process): record shared containment owner
seonghobae Sep 8, 2026
e395dc4
docs(gap): mark containment ownership consolidated
seonghobae Sep 8, 2026
650d93f
docs(changelog): record containment consolidation
seonghobae Sep 8, 2026
ce96055
docs(architecture): point process containment to shared owner
seonghobae Sep 8, 2026
6619840
docs(security): consolidate subprocess containment owner
seonghobae Sep 8, 2026
5771e9b
test(process): align containment platform guards
seonghobae Sep 8, 2026
bb14420
fix(process): align containment platform guards
seonghobae Sep 8, 2026
3b2df5d
test(process): scope core containment regression
seonghobae Sep 8, 2026
c1c2106
test(process): scope Tauri containment regression
seonghobae Sep 8, 2026
9b6340a
test(process): reject success-path descendant pipe hang
seonghobae Sep 8, 2026
650ff2f
fix(process): drain residual descendants after parent exit
seonghobae Sep 8, 2026
04ebdf1
docs(product): cover terminal descendant containment
seonghobae Sep 8, 2026
5e1903d
docs(changelog): record terminal descendant cleanup
seonghobae Sep 8, 2026
6d3af83
docs(security): doctor subprocess terminal containment
seonghobae Sep 8, 2026
1c2f9b9
docs(architecture): align subprocess containment truth
seonghobae Sep 8, 2026
26b16ae
docs(security): cover terminal descendant pipe cleanup
seonghobae Sep 8, 2026
fb04b95
test(process): reject unbounded import output capture
seonghobae Sep 8, 2026
90cbe99
fix(process): bound captured helper output
seonghobae Sep 8, 2026
6abb294
refactor(process): expose bounded output owner
seonghobae Sep 8, 2026
2b99124
docs(process): bound helper output evidence
seonghobae Sep 8, 2026
0fbe2cf
docs(gap): include bounded subprocess output admission
seonghobae Sep 8, 2026
c7334e5
docs(security): bound subprocess capture memory
seonghobae Sep 8, 2026
d766304
docs(changelog): record bounded helper output
seonghobae Sep 8, 2026
a0d7f13
fix(process): compile Unix group termination
seonghobae Sep 8, 2026
11acaf5
test(audio): reject slow helper after output overflow
seonghobae Sep 8, 2026
ab9b8a1
fix(audio): terminate helper on output admission failure
seonghobae Sep 8, 2026
0dc2faf
docs(audio): record prompt output-admission termination
seonghobae Sep 8, 2026
8770768
docs(product): align helper-output admission baseline
seonghobae Sep 8, 2026
170a2e8
docs(changelog): record helper overflow termination
seonghobae Sep 8, 2026
0e27839
test(audio): bound helper timeout polling latency
seonghobae Sep 8, 2026
da1652c
fix(audio): clamp process polling to deadline
seonghobae Sep 8, 2026
3dac5d8
docs(audio): record deadline-bounded process polling
seonghobae Sep 8, 2026
06618e0
docs(audio): make process deadline contract code-current
seonghobae Sep 8, 2026
fe3c593
docs(audio): record bounded helper deadline polling
seonghobae Sep 8, 2026
5f370f7
test(audio): require one process-output owner
seonghobae Sep 8, 2026
b887cae
fix(audio): remove legacy process-output owner
seonghobae Sep 8, 2026
067a600
merge: reconcile concurrent process deadline repair
seonghobae Sep 8, 2026
9b8542f
fix(desktop): restore PDF read trait import
seonghobae Sep 8, 2026
ef6fd2d
test(desktop): scope containment guard assertion
seonghobae Sep 8, 2026
be3b27c
test(desktop): require reader failure wakeup
seonghobae Sep 8, 2026
5bf6d3f
fix(desktop): wake process owner on output failure
seonghobae Sep 8, 2026
daee11d
docs: record reader-failure wakeup contract
seonghobae Sep 8, 2026
89fdf3e
docs: record prompt process-failure wakeup
seonghobae Sep 8, 2026
b02062a
docs: make output-failure wakeup code-current
seonghobae Sep 8, 2026
1ae37e7
test(audio): require terminal analysis descendant cleanup
seonghobae Sep 8, 2026
f612c04
fix(audio): clean analysis descendants on terminal parent exit
seonghobae Sep 8, 2026
81fe166
docs(audio): record terminal analysis cleanup repair
seonghobae Sep 8, 2026
c7cd9eb
docs(changelog): note terminal analysis descendant cleanup
seonghobae Sep 8, 2026
6cdaca2
test(audio): require bounded analysis process streams
seonghobae Sep 8, 2026
240c6b1
fix(audio): centralize bounded analysis stream admission
seonghobae Sep 8, 2026
b3c9373
fix(audio): expose bounded stream readers to desktop adapter
seonghobae Sep 8, 2026
50b5f01
fix(audio): bound analysis process streams
seonghobae Sep 8, 2026
494d0b5
test(audio): preserve terminal containment contract
seonghobae Sep 8, 2026
4405049
test(audio): bind containment check to success-path join
seonghobae Sep 8, 2026
7e6a01c
test(audio): require terminal analysis status after process exit
seonghobae Sep 8, 2026
743d7b7
fix(audio): fail closed on non-terminal engine exit
seonghobae Sep 8, 2026
f2ee73a
test(audio): bind helper progress to native job identity
seonghobae Sep 8, 2026
57e2ae4
fix(audio): bind analysis progress to native job identity
seonghobae Sep 8, 2026
532471c
test(audio): reject pre-exit terminal analysis publication
seonghobae Sep 8, 2026
7d0f4a7
fix(audio): defer analysis terminal publication until process exit
seonghobae Sep 8, 2026
ce3be1f
fix(audio): restore score PDF owner call after protocol repair
seonghobae Sep 8, 2026
f8424cb
docs(changelog): record native analysis protocol authority
seonghobae Sep 8, 2026
bd3008d
docs(security): doctor analysis subprocess protocol authority
seonghobae Sep 8, 2026
50886a3
docs(product): record native analysis protocol acceptance
seonghobae Sep 8, 2026
476c3a7
test(desktop): reject malformed analysis JSONL
seonghobae Sep 8, 2026
b075212
fix(desktop): fail closed on invalid analysis JSONL
seonghobae Sep 8, 2026
fb9d3ba
fix(desktop): preserve score removal project authority
seonghobae Sep 8, 2026
be6b361
docs(security): record fail-closed analysis JSONL
seonghobae Sep 8, 2026
f497cbd
docs(product): record strict analysis JSONL admission
seonghobae Sep 8, 2026
9aad97a
docs(changelog): record fail-closed analysis protocol
seonghobae Sep 8, 2026
e3a6a20
test(audio): reject semantically invalid analysis status payloads
seonghobae Sep 8, 2026
2dbab44
fix(audio): validate analysis status payload semantics
seonghobae Sep 8, 2026
81940fc
docs(audio): record status payload admission boundary
seonghobae Sep 8, 2026
182a250
docs(product): align analysis protocol buyer gap
seonghobae Sep 8, 2026
a899db8
docs(changelog): record native status semantics
seonghobae Sep 8, 2026
e444365
test(audio): preserve exact process JSONL payload whitespace
seonghobae Sep 8, 2026
6258b97
fix(audio): preserve strict process protocol whitespace
seonghobae Sep 8, 2026
e07a1a5
docs(audio): record strict JSONL transport boundary
seonghobae Sep 8, 2026
6a8033b
docs(product): keep resource-admission baseline current
seonghobae Sep 8, 2026
64cb603
test(audio): retain protocol rejection after child exit
seonghobae Sep 8, 2026
ea9428c
fix(audio): keep protocol rejection in join result
seonghobae Sep 8, 2026
327aafc
docs(audio): record protocol exit-order boundary
seonghobae Sep 8, 2026
ea3ba91
docs(changelog): record fail-closed protocol exit ordering
seonghobae Sep 8, 2026
161de8d
test(audio): preserve blank JSONL records for parser rejection
seonghobae Sep 8, 2026
f8135e0
fix(audio): preserve empty JSONL records for fail-closed parsing
seonghobae Sep 8, 2026
0f33574
docs(audio): doctor blank JSONL fail-closed transport
seonghobae Sep 8, 2026
1ecb447
docs(audio): record blank JSONL rejection repair
seonghobae Sep 8, 2026
53b57d2
docs(audio): align buyer gap baseline with JSONL record fidelity
seonghobae Sep 8, 2026
082cf63
test(audio): bind analysis requested-at authority
seonghobae Sep 8, 2026
a253c09
test(audio): require native requested-at echo
seonghobae Sep 8, 2026
9358035
fix(audio): preserve native analysis request timestamp
seonghobae Sep 8, 2026
70e4d9d
fix(audio): bind analysis status to native request time
seonghobae Sep 8, 2026
e43921a
fix(audio): restore score removal project scope
seonghobae Sep 8, 2026
b11dff3
docs(audio): record native request-time authority
seonghobae Sep 8, 2026
8fe4337
docs(audio): note native request-time binding
seonghobae Sep 8, 2026
76cd953
docs(audio): align buyer gap with request-time authority
seonghobae Sep 8, 2026
15bf1fb
test(audio): forbid duplicate CLI temporal analysis
seonghobae Sep 8, 2026
f3235e8
fix(audio): keep local MIR under orchestration owner
seonghobae Sep 8, 2026
4e36937
docs(audio): record single MIR orchestration owner
seonghobae Sep 8, 2026
ea94d79
docs(audio): doctor single orchestration owner
seonghobae Sep 8, 2026
8ae19aa
fix(audio): preserve CLI logging while removing MIR pre-pass
seonghobae Sep 8, 2026
e2ca1f2
docs(audio): preserve CLI diagnostics claim boundary
seonghobae Sep 8, 2026
eef9461
test(audio): retire deleted CLI MIR pre-pass fixtures
seonghobae Sep 8, 2026
4b64f57
test(audio): cover supported CLI orchestration boundary
seonghobae Sep 8, 2026
572c490
docs(audio): record obsolete pre-pass test repair
seonghobae Sep 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
- Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace.
- 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함.

### Fixed

- Enforce one canonical local-audio resource policy across native local-file/YouTube bootstrap intake, the desktop bridge, Python request preflight, temporal decoding, and stem separation so oversized, overlong, malformed, wrong-rate, or non-finite input fails before bootstrap storage or expensive analysis/model work.
Comment thread
cursor[bot] marked this conversation as resolved.
- Align YouTube download admission with that same 100 MiB encoded-byte ceiling: abort in-flight with yt-dlp `max_filesize` and a progress hook, reject announced oversize before `download=True`, delete owned `.part` / `.ytdl` / `-Frag*` siblings from that import directory on abort, and delete post-download artifacts that still exceed the policy. A 60 MiB import that the old 50 MB check rejected is now accepted; a file one byte over 100 MiB is not.
- Bound native stored-score PDF reads to the 25 MiB product limit before heap allocation and revalidate PDF magic on the same opened descriptor, preventing an attached score that later grows from bypassing the local resource boundary.

## [0.1.3] - 2026-04-29

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ publish = false

[lib]
name = "bandscope_desktop_core"
path = "src/lib.rs"
path = "src/root.rs"

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage)'] }
Expand Down
22 changes: 22 additions & 0 deletions apps/desktop/core/src/audio_resource.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// Maximum encoded local-audio file size accepted by the desktop bootstrap boundary.
pub const MAX_LOCAL_AUDIO_FILE_BYTES: u64 = 100 * 1024 * 1024;

const LOCAL_AUDIO_READ_ERROR: &str = "Could not read the selected audio file.";
const LOCAL_AUDIO_TOO_LARGE_ERROR: &str =
"Selected audio file exceeds the 100 MiB analysis limit.";

/// Validate a native local-audio file length before storing bootstrap metadata.
///
/// The caller must obtain this length from the native filesystem descriptor or
/// metadata boundary rather than from renderer-controlled JSON. The function
/// intentionally returns only bounded product messages and never includes a
/// local path or payload content.
pub fn validate_local_audio_file_size(file_size_bytes: u64) -> Result<u64, String> {
if file_size_bytes == 0 {
return Err(LOCAL_AUDIO_READ_ERROR.to_string());
}
if file_size_bytes > MAX_LOCAL_AUDIO_FILE_BYTES {
return Err(LOCAL_AUDIO_TOO_LARGE_ERROR.to_string());
}
Ok(file_size_bytes)
}
15 changes: 15 additions & 0 deletions apps/desktop/core/src/root.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//! Pure, GUI-independent logic for the BandScope desktop application.
//!
//! The historical desktop-core implementation remains in `lib.rs` as the
//! compatibility module while bounded resource boundaries are isolated in
//! auditable modules. Public symbols are re-exported so downstream callers keep
//! the same crate-root API.

#[path = "lib.rs"]
mod runtime_core;
mod audio_resource;
mod score_pdf;

pub use audio_resource::{validate_local_audio_file_size, MAX_LOCAL_AUDIO_FILE_BYTES};
pub use runtime_core::*;
pub use score_pdf::read_validated_score_pdf;
82 changes: 82 additions & 0 deletions apps/desktop/core/src/score_pdf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use crate::{MAX_SCORE_PDF_BYTES, PDF_MAGIC};
use std::{fs::File, io::Read, path::Path};

const SCORE_READ_ERROR: &str = "Could not read the score PDF.";
const SCORE_TOO_LARGE_ERROR: &str = "Score PDF is too large (exceeds 25MB limit).";
const SCORE_INVALID_PDF_ERROR: &str = "Stored score is not a valid PDF.";

fn read_validated_pdf_stream(
reader: &mut impl Read,
expected_len: u64,
) -> Result<Vec<u8>, String> {
if expected_len > MAX_SCORE_PDF_BYTES {
return Err(SCORE_TOO_LARGE_ERROR.to_string());
}

// MAX_SCORE_PDF_BYTES is 25 MiB, which fits every supported Rust `usize`.
let mut bytes = vec![0_u8; expected_len as usize];
reader
.read_exact(&mut bytes)
.map_err(|_| SCORE_READ_ERROR.to_string())?;

let mut growth_probe = [0_u8; 1];
if reader
.read(&mut growth_probe)
.map_err(|_| SCORE_READ_ERROR.to_string())?
!= 0
{
return Err(SCORE_TOO_LARGE_ERROR.to_string());
}

if !bytes.starts_with(PDF_MAGIC) {
return Err(SCORE_INVALID_PDF_ERROR.to_string());
}

Ok(bytes)
}

/// Read one already-authorized stored score without allocating beyond the PDF limit.
///
/// The caller remains responsible for path authority and containment. This helper
/// opens that resolved path once, snapshots the descriptor length, allocates only
/// that bounded size, reads exactly that many bytes, and then probes one additional
/// byte on the same descriptor. A file that was already oversized is rejected
/// before heap allocation; a file that grows after metadata inspection is rejected
/// by the one-byte probe without extending the heap buffer beyond the product cap.
/// Errors intentionally omit the local path and file content.
pub fn read_validated_score_pdf(path: &Path) -> Result<Vec<u8>, String> {
let mut file = File::open(path).map_err(|_| SCORE_READ_ERROR.to_string())?;
let metadata = file
.metadata()
.map_err(|_| SCORE_READ_ERROR.to_string())?;
if !metadata.is_file() {
return Err(SCORE_READ_ERROR.to_string());
}
read_validated_pdf_stream(&mut file, metadata.len())
}

#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;

#[test]
fn stream_rejects_growth_after_the_metadata_length_snapshot() {
let mut reader = Cursor::new(b"%PDF-extra".to_vec());

let error = read_validated_pdf_stream(&mut reader, PDF_MAGIC.len() as u64)
.expect_err("bytes beyond the metadata snapshot must fail closed");

assert_eq!(error, SCORE_TOO_LARGE_ERROR);
}

#[test]
fn stream_rejects_truncation_after_the_metadata_length_snapshot() {
let mut reader = Cursor::new(PDF_MAGIC.to_vec());

let error = read_validated_pdf_stream(&mut reader, (PDF_MAGIC.len() + 1) as u64)
.expect_err("truncation after the metadata snapshot must fail closed");

assert_eq!(error, SCORE_READ_ERROR);
}
}
25 changes: 25 additions & 0 deletions apps/desktop/core/tests/audio_resource_policy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use bandscope_desktop_core::{validate_local_audio_file_size, MAX_LOCAL_AUDIO_FILE_BYTES};

#[test]
fn local_audio_size_policy_accepts_the_exact_native_bootstrap_ceiling() {
assert_eq!(
validate_local_audio_file_size(MAX_LOCAL_AUDIO_FILE_BYTES),
Ok(MAX_LOCAL_AUDIO_FILE_BYTES)
);
}

#[test]
fn local_audio_size_policy_rejects_an_empty_native_bootstrap_source() {
assert_eq!(
validate_local_audio_file_size(0),
Err("Could not read the selected audio file.".to_string())
);
}

#[test]
fn local_audio_size_policy_rejects_a_native_source_above_the_canonical_ceiling() {
assert_eq!(
validate_local_audio_file_size(MAX_LOCAL_AUDIO_FILE_BYTES + 1),
Err("Selected audio file exceeds the 100 MiB analysis limit.".to_string())
);
}
91 changes: 91 additions & 0 deletions apps/desktop/core/tests/score_pdf_read.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use bandscope_desktop_core::{read_validated_score_pdf, MAX_SCORE_PDF_BYTES};
use std::io::Write;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};

fn unique_test_dir(name: &str) -> PathBuf {
let suffix = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock should be after epoch")
.as_nanos();
std::env::temp_dir().join(format!("bandscope-{name}-{suffix}"))
}

#[test]
fn score_pdf_read_returns_only_valid_bounded_pdf_bytes() {
let root = unique_test_dir("score-read-valid");
std::fs::create_dir_all(&root).expect("test directory should be created");
let path = root.join("score.pdf");
let expected = b"%PDF-1.7\nvalidated body";
std::fs::write(&path, expected).expect("valid PDF fixture should be written");

let actual = read_validated_score_pdf(&path).expect("valid stored PDF should be readable");

assert_eq!(actual, expected);
let _ = std::fs::remove_dir_all(root);
}

#[test]
fn score_pdf_read_rejects_empty_short_and_wrong_magic_content() {
let root = unique_test_dir("score-read-invalid");
std::fs::create_dir_all(&root).expect("test directory should be created");

for (name, content) in [
("empty.pdf", b"".as_slice()),
("short.pdf", b"%PD".as_slice()),
("wrong.pdf", b"PK\x03\x04 not a PDF".as_slice()),
] {
let path = root.join(name);
std::fs::write(&path, content).expect("invalid PDF fixture should be written");
let error = read_validated_score_pdf(&path).expect_err("invalid PDF must fail closed");
assert!(
error == "Could not read the score PDF." || error == "Stored score is not a valid PDF.",
"unexpected payload-safe error: {error}"
);
assert!(!error.contains(root.to_string_lossy().as_ref()));
}

let _ = std::fs::remove_dir_all(root);
}

#[test]
fn score_pdf_read_rejects_oversized_sparse_file_before_heap_allocation() {
let root = unique_test_dir("score-read-oversized");
std::fs::create_dir_all(&root).expect("test directory should be created");
let path = root.join("oversized.pdf");
let mut file = std::fs::File::create(&path).expect("oversized PDF fixture should be created");
file.write_all(b"%PDF-")
.expect("PDF magic should be written before extending sparse file");
file.set_len(MAX_SCORE_PDF_BYTES + 1)
.expect("sparse PDF fixture should exceed the product limit");
drop(file);

let error = read_validated_score_pdf(&path).expect_err("oversized PDF must fail closed");

assert_eq!(error, "Score PDF is too large (exceeds 25MB limit).");
let _ = std::fs::remove_dir_all(root);
}

#[cfg(unix)]
#[test]
fn score_pdf_read_rejects_non_file_descriptor() {
let root = unique_test_dir("score-read-directory");
std::fs::create_dir_all(&root).expect("test directory should be created");

let error = read_validated_score_pdf(&root).expect_err("directory must fail closed");

assert_eq!(error, "Could not read the score PDF.");
assert!(!error.contains(root.to_string_lossy().as_ref()));
let _ = std::fs::remove_dir_all(root);
}

#[test]
fn score_pdf_read_rejects_missing_file_without_exposing_path() {
let root = unique_test_dir("score-read-missing");
let path = root.join("private-score.pdf");

let error = read_validated_score_pdf(&path).expect_err("missing PDF must fail closed");

assert_eq!(error, "Could not read the score PDF.");
assert!(!error.contains("private-score.pdf"));
}
12 changes: 8 additions & 4 deletions apps/desktop/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,10 @@ fn normalize_local_audio_source(path: &Path) -> Result<LocalAudioSourcePayload,
}
let metadata = std::fs::metadata(&canonical)
.map_err(|_| "Could not read the selected audio file.".to_string())?;
if !metadata.is_file() || metadata.len() == 0 {
if !metadata.is_file() {
return Err("Could not read the selected audio file.".into());
}
let file_size_bytes = validate_local_audio_file_size(metadata.len())?;
let file_name = canonical
.file_name()
.and_then(|value| value.to_str())
Expand All @@ -167,7 +168,7 @@ fn normalize_local_audio_source(path: &Path) -> Result<LocalAudioSourcePayload,
source_path: canonical.to_string_lossy().into_owned(),
file_name: file_name.to_string(),
extension,
file_size_bytes: metadata.len(),
file_size_bytes,
})
}

Expand Down Expand Up @@ -712,6 +713,7 @@ async fn import_youtube_url(
if parsed.get("ok").and_then(|v| v.as_bool()) == Some(true) {
if let Some(metadata) = parsed.get("metadata") {
let source = youtube_source_from_metadata(metadata, &cache_root)?;
validate_local_audio_file_size(source.file_size_bytes)?;
Comment thread
cursor[bot] marked this conversation as resolved.

let summary = ProjectBootstrapSummaryPayload {
project_id,
Expand Down Expand Up @@ -826,7 +828,9 @@ fn attach_score_pdf(
/// Security Notes: no path crosses the IPC boundary. Both ids are validated
/// against strict allowlist shapes, the path is rebuilt locally, and the
/// canonicalize-plus-prefix guard in `resolve_existing_score_pdf` rejects any
/// escape from the app-owned scores root.
/// escape from the app-owned scores root. The resolved file is then read
/// through the bounded core helper so growth after attachment cannot trigger
/// an allocation beyond the 25 MiB product limit.
#[tauri::command]
fn read_score_pdf(
project_id: String,
Expand All @@ -838,7 +842,7 @@ fn read_score_pdf(
}
let scores_root = scores_root_for_project(&app, &project_id)?;
let path = resolve_existing_score_pdf(&scores_root, &score_id)?;
std::fs::read(path).map_err(|_| "Could not read the score PDF.".to_string())
read_validated_score_pdf(&path)
}

/// Security Notes: same id validation and traversal guard as `read_score_pdf`;
Expand Down
58 changes: 58 additions & 0 deletions apps/desktop/src/lib/analysis.resource-policy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { importYoutubeUrl, selectLocalAudioSource } from "./analysis";

type TauriWindow = Window & {
__TAURI_INTERNALS__?: unknown;
__TAURI_INVOKE__?: unknown;
};

const tauriWindow = window as TauriWindow;
const INVALID_RESOURCE_POLICY_MESSAGE =
"Selected audio file metadata violates the analysis resource policy.";

function fractionalBootstrap(projectId: string) {
return {
projectId,
sourceMode: "reference",
projectRoot: `/tmp/bandscope/projects/${projectId}`,
cacheRoot: `/tmp/bandscope/cache/${projectId}`,
tempRoot: `/tmp/bandscope/temp/${projectId}`,
source: {
sourcePath: `/tmp/bandscope/${projectId}/input.wav`,
fileName: "input.wav",
extension: "wav",
fileSizeBytes: 1.5
}
};
}

describe("analysis encoded-byte policy parity", () => {
beforeEach(() => {
delete tauriWindow.__TAURI_INTERNALS__;
delete tauriWindow.__TAURI_INVOKE__;
});

it("rejects fractional local-file metadata before project state", async () => {
tauriWindow.__TAURI_INVOKE__ = vi.fn().mockResolvedValue(fractionalBootstrap("local-project"));

await expect(selectLocalAudioSource()).resolves.toEqual({
ok: false,
error: {
code: "invalid_request",
message: INVALID_RESOURCE_POLICY_MESSAGE
}
});
});

it("rejects fractional imported-file metadata before project state", async () => {
tauriWindow.__TAURI_INVOKE__ = vi.fn().mockResolvedValue(fractionalBootstrap("youtube-project"));

await expect(importYoutubeUrl("https://youtu.be/4ozX4yFUC34")).resolves.toEqual({
ok: false,
error: {
code: "invalid_request",
message: INVALID_RESOURCE_POLICY_MESSAGE
}
});
});
});
Loading
Loading