Skip to main content

max / audiofiles

Update audit, todos, deploy docs; add manual testing guide - audit_review.md: Ultra Fuzz Run 1 5-axis findings, scorecard refresh - todo.md: move Ultra Fuzz items to todo_done.md, add dependency-prune queue - todo_done.md: new completed-items file to keep active todo focused - human_testing.md: pre-release manual testing checklist with sign-off - deploy.md: refresh Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-14 19:23 UTC
Commit: 6e9eaae069fe09fe0e5b6f2eb82d491f7e7a5253
Parent: c700f06
5 files changed, +690 insertions, -380 deletions
M docs/audit_review.md +229 -132
@@ -1,155 +1,252 @@
1 1 # audiofiles -- Code Audit Review
2 2
3 - **Last audited:** 2026-05-04 (twentieth audit, Run 20 cross-project)
4 - **Previous audit:** 2026-04-18 (nineteenth audit, Run 15 cross-project)
3 + **Last audited:** 2026-05-09 (Ultra Fuzz Run 1, 5-axis adversarial audit)
4 + **Previous audit:** 2026-05-04 (Run 20, cross-project)
5 5
6 - ## Overall Grade: A
6 + ## Overall Grade: A-
7 7
8 - Run 20 cross-project audit. 773 tests (all pass). 4 clippy warnings (trivial). v0.4.0. Grade A (stable). ~42,652 LOC. Sidebar unwraps fixed. Analysis worker Relaxed ordering persists (LOW). App main.rs at 1296 lines exceeds thin-shell intent but is functional.
8 + Ultra Fuzz Run 1. 5 parallel adversarial agents (Audio Pipeline, Data & Storage, UX Wiring, Security, Performance). 780 tests (all pass). v0.4.0. ~42.7K LOC. 48 modules audited. 4 SERIOUS, 10 MINOR, 16 NOTE findings. All Run 20 action items verified fixed. Two SERIOUS sync bugs found (snapshot column omission). Security trust model gaps identified (plaintext API key, unsigned OTA updates).
9 9
10 10 ## Scorecard
11 11
12 12 | Dimension | Grade | Notes |
13 13 |-----------|:-----:|-------|
14 - | Code Quality | A | 4 trivial clippy warnings (repeat-take, collapsible if, same-type cast, arg count). Sidebar unwraps fixed since Run 15. Typed error hierarchy across all crates. |
15 - | Architecture | A- | 7-crate workspace (core, browser, app, sync, rhai, train, bench). Core is sync-only. Backend trait cleanly abstracts data. App main.rs at 1296 lines exceeds thin-shell intent (minor). |
16 - | Testing | A | 773 tests, all passing. Core: ~498 (incl. e2e pipeline), browser: ~199 (1653-line test file), app: ~59, sync: ~34, rhai: ~38. +85 since Run 15. |
17 - | Security | A | All SQL parameterized. LIKE escaped. Hash validated (64 hex). Column whitelists in sync. 17 unsafe blocks (all FFI, all with SAFETY comments). OTA URL trust (LOW, unchanged). |
18 - | Performance | A- | try_lock on cpal callback. Enriched LEFT JOIN queries. 7+ indexes. WAL mode. Background workers for all heavy ops. VP-tree indexes for similarity/fingerprint. |
19 - | Documentation | A | Every module has //! docs. Public functions have /// docs. SAFETY comments on unsafe. architecture.md, README, CONTRIBUTING.md. |
20 - | Dependencies | A | All deps semver-latest. No unused deps. No git-pinned deps. |
21 - | Frontend | A- | egui patterns clean. TOML theme system (27 bundled + custom). Waveform painter. Keyboard shortcuts. try_lock from GUI. |
22 - | Type Safety | A | VfsId, NodeId, SmartFolderId, CollectionId i64 newtypes via macro. SampleHash(String) validated. Domain enums. Typed error hierarchy. |
23 - | Observability | A- | tracing in all crates with EnvFilter. 115+ #[instrument(skip_all)] annotations. |
24 - | Concurrency | A | try_lock() on audio thread with silence fallback. Workers in own threads. Single-lock .take() pattern. Analysis worker uses Relaxed ordering (LOW — benign on ARM). |
25 - | Resilience | A- | Worker Drop with Shutdown+join. Per-file error reporting. Audio/tray/sync failures non-fatal. applying_remote cleared on startup. |
26 - | API Consistency | A | 47-method Backend trait with uniform BackendResult<T>. Consistent naming. |
27 - | Migration Safety | A | Inline migrations, all additive. CASCADE foreign keys. Duplicate-column recovery with logging. |
28 - | Codebase Size | A- | ~42,652 LOC across 7 crates for ~20 major features + ML classifier + cloud sync + Rhai scripting + native drag-out + instrument builder. |
14 + | Audio Pipeline | A | All DSP paths NaN-guarded. Previous fuzz fixes verified. Asymmetric normalize clamp (MINOR). |
15 + | Data & Storage | A- | Content-addressed store excellent. Sync initial snapshot stale since migration 008 (SERIOUS). Cleanup race (MINOR). |
16 + | UX Wiring | A | Clean state machine. Theme preview key prefix wrong (MINOR). truncate_name byte panic (MINOR). |
17 + | Security | A- | Strong sandbox, PKCE, parameterized SQL. Plaintext API key (SERIOUS). No OTA signatures (SERIOUS). |
18 + | Performance | A- | Well-optimized desktop. VP-tree build under DB lock (MINOR). SampleBuffer per-packet alloc (MINOR). No FTS5 (NOTE). |
19 + | Code Quality | A | Typed error hierarchy. No unwraps in prod. 17 unsafe blocks (all FFI, all SAFETY-commented). |
20 + | Architecture | A | 7-crate workspace. Backend trait. Worker channels. Content-addressed storage. |
21 + | Testing | A | 780 tests. Core e2e pipeline. State tests (1653L). Theme roundtrip. Import lifecycle. |
22 + | Type Safety | A | VfsId, NodeId, SampleHash newtypes. Domain enums. Typed errors. Column allowlists. |
23 + | Concurrency | A | try_lock on audio callback. Relaxed→Acquire/Release fixed. Atomic cancel flags. Worker Drop+join. |
29 24
30 25 ## Module Heatmap
31 26
32 - | Module | Code | Arch | Test | Security | Perf | Docs | Types | Conc | Size |
33 - |--------|:----:|:----:|:----:|:--------:|:----:|:----:|:-----:|:----:|:----:|
34 - | core/db.rs (913L) | A- | A | A | A | A | A | A | A- | A |
35 - | core/store.rs (977L) | A | A- | B+ | A | A | A | A | A | B+ |
36 - | core/vfs.rs (1128L) | A | A | A- | A | A | A | A | A | B+ |
37 - | core/export/ (1058L) | A- | A | B | A | A- | A | A | A | B+ |
38 - | core/analysis/classify.rs (1246L) | A- | A | B+ | A | A | B | A | A | B |
39 - | core/search.rs (668L) | A | A | A- | A | A | A | B+ | A | A |
40 - | core/fingerprint.rs (642L) | A | A | B+ | A | A | A | A | A | A |
41 - | core/similarity.rs (648L) | A | A | B | A | B | A | B | A | A |
42 - | core/vp_tree.rs (521L) | A | A | A- | A | A | A | A | A | A |
43 - | core/tags.rs (397L) | A | A | A | A | A | A | A | A | A |
44 - | core/collections.rs (327L) | A | A | B | A | A | A | A | A | A |
45 - | core/edit/ (1393L) | A | A | B | A | A | A | A | A | B |
46 - | browser/backend/direct.rs (1231L) | B+ | A | B | A | A- | A | A | A | B |
47 - | browser/state/import_workflow.rs (1121L) | B | B+ | B | A | A | B+ | B | B+ | C |
48 - | browser/state/tests.rs (1653L) | B | A | A | A | A | A | A | A | B |
49 - | browser/ui/theme.rs (908L) | A | A | B+ | A | A | A | A | n/a | B |
50 - | browser/import.rs (688L) | B+ | A | B | A | A | A | A | B+ | B |
51 - | browser/instrument.rs (600L) | A | A | C | A | A | A | A | B | B |
52 - | browser/preview.rs (596L) | A | A | B | A | A | A | A | B+ | B |
53 - | browser/ui/overlays.rs (617L) | B+ | A | B | A | A | A | A | n/a | B |
54 - | browser/ui/file_list.rs (579L) | B+ | A | B | A | A | B | A | n/a | B |
55 - | browser/state/ui.rs (513L) | B | A | C | A | A | B | B | A | B |
56 - | browser/state/bulk_ops.rs (500L) | B+ | B | B | A | A | B | B | A | B |
57 - | app/main.rs (1296L) | B- | C+ | B | B- | n/a | B | B+ | B | D |
58 - | sync/service/state.rs (996L) | B+ | A- | B | B+ | n/a | A | A- | A | A |
59 - | sync/download+upload+resolve | B+ | A | C | B+ | n/a | A | A | A | A |
60 - | rhai/lib.rs (530L) | A | A | A | A | n/a | A | A | A | A |
27 + ### Audio Pipeline & DSP
28 +
29 + | Module | Correct | Errors | Types | Concurrency | Tests | Quality | Overall |
30 + |--------|:-------:|:------:|:-----:|:-----------:|:-----:|:-------:|:-------:|
31 + | analysis/decode | A | A | A | A | A- | A | A |
32 + | analysis/basic | A | A | A | A | A | A | A |
33 + | analysis/loudness | A | A | A | A | A- | A | A |
34 + | analysis/spectral | A | A | A | A | A | A | A |
35 + | analysis/mfcc | A | A | A | A | A | A | A |
36 + | analysis/bpm | A | A | A | A | A- | A | A |
37 + | analysis/classify | A | A | A | A | A | A | A |
38 + | analysis/worker | A | A | A | A | A- | A | A |
39 + | edit/normalize | A- | A | A | A | A | A- | A- |
40 + | edit/* (other 8) | A | A | A | A | A | A | A |
41 + | export/encode | A | A | A | A | A | A | A |
42 + | export/encode_aiff | A | A | A | A | A | A | A |
43 + | export/sanitize | A | A | A | A | A | A | A |
44 + | preview.rs | A | A | A | A- | A | A | A |
45 + | fingerprint.rs | A | A | A | A | A | A | A |
46 + | vp_tree.rs | A | A | A | A | A | A | A |
47 +
48 + ### Data & Storage
49 +
50 + | Module | Query | Txn Safety | Resources | Migration | Types | Arch | Overall |
51 + |--------|:-----:|:----------:|:---------:|:---------:|:-----:|:----:|:-------:|
52 + | db.rs | A | A | A | A- | A | A | A |
53 + | store.rs | A | A | A | n/a | A | A | A |
54 + | vfs.rs | A | A- | A | n/a | A | A | A |
55 + | vfs_mirror.rs | A- | n/a | A | n/a | A- | A | A- |
56 + | tags.rs | A | A | A | n/a | A | A | A |
57 + | search.rs | A | n/a | A | n/a | A- | A | A |
58 + | sync/service | A- | A | A- | n/a | A | A | A- |
59 + | cleanup.rs | A- | B+ | A- | n/a | A | A- | B+ |
60 +
61 + ### UX Wiring
62 +
63 + | Module | UI | State | Workflow | Platform | Error UX | Quality | Overall |
64 + |--------|:--:|:-----:|:-------:|:--------:|:--------:|:-------:|:-------:|
65 + | state/* | -- | A | A | -- | A | A | A |
66 + | ui/file_list.rs | A | A | -- | A | A | A | A |
67 + | ui/theme.rs | A | -- | -- | -- | A | A | A |
68 + | import_screens | -- | A- | A | -- | A | A- | A- |
69 + | drag_out/* | -- | A | -- | A | A | A | A |
70 + | instrument.rs | A | A | -- | -- | A | A | A |
71 + | app/main.rs | A | A | A | A | A | A | A |
72 +
73 + ### Security
74 +
75 + | Module | Security | Crypto | Sandbox | Path | Update | Unsafe | Overall |
76 + |--------|:--------:|:------:|:-------:|:----:|:------:|:------:|:-------:|
77 + | license/activation | B | n/a | n/a | A | n/a | n/a | B |
78 + | updater.rs | B- | n/a | n/a | A | B- | n/a | B- |
79 + | auth.rs (PKCE) | A | A | n/a | n/a | n/a | n/a | A |
80 + | sync service | A- | A | n/a | A | n/a | n/a | A- |
81 + | rhai engine | A- | n/a | A- | A | n/a | n/a | A- |
82 + | store.rs | A | A | n/a | A | n/a | n/a | A |
83 + | drag_out (FFI) | n/a | n/a | n/a | A- | n/a | A- | A- |
84 +
85 + ### Performance
86 +
87 + | Module | Perf | Scale | Concurrency | Memory | Startup | Efficiency | Overall |
88 + |--------|:----:|:-----:|:-----------:|:------:|:-------:|:----------:|:-------:|
89 + | analysis/worker | A | A | A | A- | A | A | A |
90 + | vp_tree | A | A- | A | A | A | A | A |
91 + | search.rs | A- | B+ | A | A | A | A | A- |
92 + | store.rs | A | A | A | A | A | A | A |
93 + | preview.rs | A- | A | A | A- | A | A | A- |
94 + | backend/direct.rs | A- | B+ | B+ | A | A | A- | A- |
95 + | waveform.rs | A | A | A | A | A | A+ | A+ |
96 + | file_list.rs | A | A | A | A | A | A | A |
61 97
62 98 ### Cold Spots
63 99
64 100 | # | Module | Grade | Issue | Severity |
65 - |---|--------|-------|-------|----------|
66 - | 1 | **app/main.rs** | D (Size) | 1296 lines in "thin shell" entry point. Contains state, UI rendering, vault management, license lifecycle, sync, MIDI, tray — all in one struct. | MEDIUM |
67 - | 2 | **browser/state/import_workflow.rs** | C (Size) | 1121 lines. import_directory_recursive duplicated from import.rs (flagged in code comments). | LOW |
68 - | 3 | **browser/instrument.rs** | C (Test) | No tests for MIDI voice allocation, envelope state, pitch interpolation. | LOW |
69 - | 4 | **browser/state/ui.rs** | C (Test) | No dedicated tests for UI state mutations. | LOW |
70 - | 5 | **sync/download+upload+resolve** | C (Test) | Three sync submodules with zero unit tests. | LOW |
71 - | 6 | **core/analysis/worker.rs** | B- (Conc) | Relaxed atomic ordering on cancel flag (lines 84, 92, 139, 147, 152, 191). Benign on ARM but imprecise. CHRONIC (3 audits). | LOW |
72 -
73 - ## Previous Action Item Status
74 -
75 - | Item | Status |
76 - |------|--------|
77 - | sidebar.rs unwraps (LOW) | **FIXED** — no unwraps remain |
78 - | updater.rs URL trust (LOW) | **UNFIXED** — main.rs:1051 still calls open::that with server URL |
79 - | Relaxed atomic ordering in analysis/worker.rs (LOW) | **UNFIXED — CHRONIC** (3+ audits). edit/worker.rs was fixed; analysis/worker.rs was not. |
80 - | Export CTE duplication (LOW) | **UNFIXED** — minor, no correctness impact |
81 -
82 - ## Mandatory Surprise
83 -
84 - ### Finding (Run 20): Custom percent-decoding in sync auth (audiofiles-sync)
85 -
86 - The OAuth callback handler at `crates/audiofiles-sync/src/auth.rs` implements a hand-rolled `percent_decode()` function that decodes arbitrary bytes from URL query strings. Invalid UTF-8 sequences (e.g., `%FF%FE`) are silently converted to U+FFFD replacement characters via `String::from_utf8_lossy()`. This could theoretically cause state parameter comparison mismatches if a MITM crafts invalid UTF-8 in the OAuth callback URL.
87 -
88 - **Risk:** Low (requires active MITM on localhost callback). **Fix:** Use `percent_encoding::percent_decode_str()` from the `percent-encoding` crate (already a transitive dep) or reject non-UTF-8 decoded output.
89 -
90 - ### Previous finding (Run 15): Relaxed ordering in analysis cancel flag
91 -
92 - Still present. See cold spot #6 above.
93 -
94 - ## Strengths
95 -
96 - 1. **Content-addressed storage is elegant.** SHA-256 dedup, CASCADE deletes, recursive CTE queries. SampleStore + VFS separation enables unlimited virtual hierarchies over one flat blob store.
97 - 2. **Test growth is strong.** 688 → 773 tests (+12.4%) since last audit. Core e2e pipeline coverage is thorough.
98 - 3. **Rhai plugin sandbox is exemplary.** No FS/net access, ops cap 100k, expression depth 64, script size 10k. All 10 modules tested. Best-in-class isolation.
99 - 4. **Audio thread safety is correct.** try_lock() on cpal callback with silence fallback. No heap allocations on audio thread. Worker threads for all heavy operations.
100 - 5. **Code fuzz findings all resolved.** Both rounds (2026-04-27 and 2026-05-03) of critical/serious findings are fully fixed per todo.md.
101 -
102 - ## Weaknesses
103 -
104 - 1. **app/main.rs is monolithic.** 1296 lines violates the CONTRIBUTING.md "thin shell" principle. Contains activation, vault setup, browser, sync, MIDI, tray, and updater logic in one struct.
105 - 2. **Sync submodule test gap.** download.rs, upload.rs, resolve.rs have zero unit tests. State.rs has tests but the actual sync operations are untested.
106 - 3. **Analysis worker Relaxed ordering is chronic.** 3+ audits unfixed. Benign but technically imprecise — one-line fix (Relaxed → Acquire/Release).
107 -
108 - ## Action Items
109 -
110 - Filed in `docs/todo.md` under "Audit Run 20 (2026-05-04)".
111 -
112 - - MEDIUM: Split app/main.rs into submodules (activation, vault, browser controller)
113 - - LOW: Fix Relaxed → Acquire/Release in analysis/worker.rs (CHRONIC)
114 - - LOW: Add unit tests for sync download/upload/resolve modules
115 - - LOW: Deduplicate import_directory_recursive (import_workflow.rs vs import.rs)
101 + |---|--------|:-----:|-------|----------|
102 + | 1 | cleanup.rs | B+ | Non-transactional orphan deletes, race with import worker | MINOR |
103 + | 2 | updater.rs | B- | No signature verification on OTA metadata | SERIOUS |
104 + | 3 | license.rs | B | Plaintext API key, unsigned cache, trial file-deletion bypass | SERIOUS |
105 + | 4 | search.rs | B+ (Scale) | LIKE %...% without FTS5, potential slow at 100K+ nodes | NOTE |
106 + | 5 | backend/direct.rs | B+ (Conc) | VP-tree index build holds DB mutex, blocks UI | MINOR |
107 +
108 + ## Bug Reports
109 +
110 + ### Audio Pipeline — 0 CRITICAL, 0 SERIOUS, 1 MINOR, 6 NOTE
111 +
112 + | # | Sev | Location | Description |
113 + |---|-----|----------|-------------|
114 + | F-01 | MINOR | edit/normalize.rs:56 | LUFS normalize clamps [-1,1] silently; peak normalize does NOT clamp — asymmetric, undocumented |
115 + | F-02 | NOTE | export/encode.rs:31 | Dither seed from pointer address — not reproducible, not truly random |
116 + | F-03 | NOTE | vp_tree.rs:260 | Recursive search on flat chain tail at depth cap — deep for degenerate inputs |
117 + | F-04 | NOTE | preview.rs:258 | Hound WAV fallback in streaming loads entire file — defeats streaming purpose |
118 + | F-05 | NOTE | analysis/classify.rs:428 | Short noisy transients misclassified as Drum (ML layer 2 corrects) |
119 + | F-06 | NOTE | similarity.rs:71 | NormRanges default (0,0) — safe, correct behavior |
120 + | F-07 | NOTE | audio.rs:143 | try_lock drops frames during streaming — acceptable for preview |
121 +
122 + ### Data & Storage — 0 CRITICAL, 2 SERIOUS, 2 MINOR, 4 NOTE
123 +
124 + | # | Sev | Location | Description |
125 + |---|-----|----------|-------------|
126 + | D-01 | SERIOUS | sync/service/state.rs:26 | Initial snapshot `samples` omits `duration` (migration 009) |
127 + | D-02 | SERIOUS | sync/service/state.rs:27 | Initial snapshot `audio_analysis` omits 5 columns (migrations 010-011) |
128 + | D-03 | MINOR | cleanup.rs:200 | Orphan deletes without transaction — race with concurrent import |
129 + | D-04 | MINOR | sync/service/state.rs:134 | `mark_cloud_only_samples` per-row transactions — slow for large libraries |
130 + | D-05 | NOTE | vfs_mirror.rs:207 | `batch_extensions` is N+1 despite name |
131 + | D-06 | NOTE | id_types.rs:85 | `SampleHash::validated` accepts uppercase; `store.rs` rejects it |
132 + | D-07 | NOTE | vfs.rs:593 | `find_nodes_by_hashes` unbounded IN clause |
133 + | D-08 | NOTE | db.rs:582 | `edit_history` lacks FK — dangling refs accumulate |
134 +
135 + ### UX Wiring — 0 CRITICAL, 0 SERIOUS, 3 MINOR, 4 NOTE
136 +
137 + | # | Sev | Location | Description |
138 + |---|-----|----------|-------------|
139 + | U-01 | MINOR | ui/theme.rs:404 | `theme_preview_colors` wrong key prefix — all previews show fallback colors |
140 + | U-02 | MINOR | ui/file_list_menus.rs:316 | `truncate_name` byte-slices — panics on non-ASCII names |
141 + | U-03 | MINOR | state/import_workflow.rs:8 | Import dry-run count doesn't skip macOS metadata dirs |
142 + | U-04 | NOTE | drag_out/mod.rs:60 | Name collision loop 1-999, no fallback |
143 + | U-05 | NOTE | state/library.rs:105 | Dynamic collection sets active_collection=None — breadcrumb gap |
144 + | U-06 | NOTE | state/import_workflow.rs:953 | Batch edit prompts per-sample (self-resolves) |
145 + | U-07 | NOTE | state/bulk_ops.rs:76 | Index subtraction fragile (sound but relies on filter) |
146 +
147 + ### Security — 0 CRITICAL, 2 SERIOUS, 3 MINOR, 2 NOTE
148 +
149 + | # | Sev | Location | Description |
150 + |---|-----|----------|-------------|
151 + | S-01 | SERIOUS | app/main.rs:131 | API key plaintext file, no OS keychain |
152 + | S-02 | SERIOUS | app/updater.rs:86 | No cryptographic signature on OTA update metadata |
153 + | S-03 | MINOR | app/license.rs:204 | Trial bypass via file deletion |
154 + | S-04 | MINOR | app/license.rs:80 | License cache unsigned, no periodic revalidation |
155 + | S-05 | MINOR | rhai/engine.rs:13 | No wall-clock timeout on Rhai scripts |
156 + | S-06 | NOTE | sync/service/resolve.rs:74 | Dynamic SQL names — mitigated by static whitelist |
157 + | S-07 | NOTE | drag_out/mod.rs:36 | Drag temp dir not cleaned on exit |
158 +
159 + ### Performance — 0 CRITICAL, 0 SERIOUS, 4 MINOR, 6 NOTE
160 +
161 + | # | Sev | Location | Description |
162 + |---|-----|----------|-------------|
163 + | P-01 | MINOR | preview.rs:353 | SampleBuffer per-packet in streaming decode |
164 + | P-02 | MINOR | preview.rs:151 | SampleBuffer per-packet in non-streaming decode |
165 + | P-03 | MINOR | backend/direct.rs:543 | VP-tree build holds DB mutex — blocks UI |
166 + | P-04 | MINOR | export/mod.rs:204 | `enrich_with_tags` N+1 on export |
167 + | P-05 | NOTE | analysis/spectral.rs:197 | Magnitude frames cloned per STFT frame |
168 + | P-06 | NOTE | analysis/mfcc.rs:125 | Mel filterbank rebuilt per file |
169 + | P-07 | NOTE | fingerprint.rs:173 | NCC correlation O(n * shift) — bounded, acceptable |
170 + | P-08 | NOTE | search.rs:263 | LIKE %...% without FTS5 |
171 + | P-09 | NOTE | db.rs (migration 007) | No index on sync_changelog.timestamp |
172 + | P-10 | NOTE | import.rs:250 | Sequential import — acceptable given SQLite single-writer |
173 +
174 + ## Cross-Cutting Concerns
175 +
176 + 1. **Sync snapshot staleness** (Data + Security): Initial snapshot queries frozen at migration 008. Both a data integrity issue (D-01, D-02) and security-adjacent — `user_config` table synced bidirectionally could allow remote `unsafe_mode` override.
177 +
178 + 2. **Preview decode pipeline** (Audio + Performance): Hound WAV fallback loads entire file (F-04), and SampleBuffer allocated per-packet (P-01, P-02). Both affect same decode path.
179 +
180 + 3. **Licensing trust model** (Security): API key plaintext (S-01), unsigned OTA (S-02), unsigned license cache (S-04), trial file-deletion (S-03) — cohesive gap in cryptographic trust.
181 +
182 + ## Mandatory Surprises
183 +
184 + | Axis | Finding |
185 + |------|---------|
186 + | Audio | **Positive**: `interleaved_to_stereo` NaN/Inf sanitizer catches `NaN.clamp()` returning NaN — applied across all channel branches. Production-grade defensive programming. |
187 + | Data | **Negative**: Initial snapshot queries stale since migration 008 — silently drops 6 columns on first sync. |
188 + | UX | **Negative**: `theme_preview_colors` uses `"bg.primary"` instead of `"background.primary"` — every theme preview shows fallback colors. |
189 + | Security | **Negative**: Sync can overwrite `unsafe_mode` via `user_config` table — compromised server could silently enable unsafe mode on target device. |
190 + | Performance | **Positive**: Spectral STFT hop=window (no overlap) — intentional 4x speedup for classification without accuracy loss. |
191 +
192 + ## Confidence Assessment
193 +
194 + | Axis | Confidence | Notes |
195 + |------|:----------:|-------|
196 + | Audio Pipeline | HIGH | All DSP paths NaN-guarded, previous fixes verified |
197 + | Data & Storage | HIGH | Content-addressed store correct; sync snapshot is the one gap |
198 + | UX Wiring | HIGH | Clean state machine, proper worker comms, robust FFI |
199 + | Security | MEDIUM | Strong for desktop (sandbox, PKCE, parameterized SQL); weak trust model |
200 + | Performance | HIGH | Well-optimized, no scalability cliffs at expected sizes |
201 +
202 + ## Previous Action Item Status (Run 20 → Ultra Fuzz 1)
203 +
204 + | Item | Run 20 | Ultra Fuzz 1 |
205 + |------|:------:|:------------:|
206 + | Split app/main.rs (MEDIUM) | FIXED | Verified fixed |
207 + | Relaxed → Acquire/Release (LOW, was CHRONIC) | FIXED | Verified fixed |
208 + | Sync tests (LOW) | FIXED | Verified fixed |
209 + | import_directory_recursive dedup (LOW) | FIXED | Verified fixed |
210 + | N1 API key plaintext | Noted | Upgraded to SERIOUS (S-01) |
211 + | N2 No OTA signatures | Noted | Upgraded to SERIOUS (S-02) |
212 + | O11 Orphan cleanup not transactional | "Mitigated" | Promoted to real bug (D-03) — separate connection bypasses Mutex |
213 + | O15 Dither seed | Noted | Improved (now pointer-derived, NOTE) |
214 + | O16 LUFS normalize clip | Noted | Reclassified as asymmetric clamp (F-01, MINOR) |
215 +
216 + No CHRONIC items. All Run 20 action items resolved.
217 +
218 + ## Recommended Priority Order
219 +
220 + 1. Fix sync initial snapshot columns (D-01, D-02) — 2-line fix, data loss on first sync
221 + 2. Fix `truncate_name` byte slicing (U-02) — runtime panic on non-ASCII
222 + 3. Fix `theme_preview_colors` key prefix (U-01) — 3-line fix, all previews wrong
223 + 4. Fix cleanup orphan delete transaction (D-03) — race condition
224 + 5. Exclude `unsafe_mode` from sync triggers — security concern
225 + 6. Hoist SampleBuffer out of decode loops (P-01, P-02) — easy perf win
226 + 7. Release DB lock before VP-tree build (P-03) — UI blocking
227 + 8. Batch `enrich_with_tags` (P-04) — N+1 on export
228 + 9. Add OTA signature verification (S-02) — requires server-side signing
229 + 10. Move API key to OS keychain (S-01) — requires `keyring` crate
230 + 11. Batch `mark_cloud_only_samples` (D-04) — slow for large libraries
231 + 12. Add Rhai wall-clock timeout (S-05) — low risk
116 232
117 233 ## Metrics Over Time
118 234
119 - | Metric | 6th (03-11) | 7th (03-13) | Adversarial (03-13) | 11th (03-18) | 12th (03-19) | 13th (03-19) | 14th (03-22) | ML (03-26) | Run 12 (03-28) | Run 13 (04-06) | Run 14 (04-15) | Run 15 (04-18) | Run 20 (05-04) |
120 - |--------|:-----------:|:-----------:|:-------------------:|:------------:|:------------:|:------------:|:------------:|:----------:|:--------------:|:--------------:|:--------------:|:--------------:|:--------------:|
121 - | Overall | A- | A- | A- | A- | A | A | A | A | A | A | A | A | A |
122 - | LOC | 25.6K | 25.6K | 25.6K | ~25K | ~23K | ~23.5K | ~23.5K | ~24.5K | ~24.5K | ~25K | ~40.2K | ~40.2K | ~42.7K |
123 - | Tests | 518 | 532 | 557 | 566 | 535 | 560 | 585 | 610 | 611 | 704 | 688 | 688 | 773 |
124 - | Crates | 7 + xtask | 7 + xtask | 7 + xtask | 7 + xtask | 5 | 5 | 5 | 5 + train | 5 + train | 5 + train | 5 + train | 5 + train | 5 + train |
125 - | Clippy | 2 (trivial) | 2 (trivial) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 4 (trivial) |
126 - | Unwrap (prod) | ~1 | 7 (all init) | 7 (all init) | 2 (sidebar, guarded) | 2 (sidebar, guarded) | 2 (sidebar) | 2 (sidebar) | 2 (sidebar) | 2 (sidebar) | 2 (sidebar) | 2 (sidebar) | 2 (sidebar) | 0 |
127 - | Unsafe | 2 (test) | 2 (test) | 2 (test) | 2 (test) | 17 (FFI) | 17 (FFI) | 17 (FFI) | 17 (FFI) | 17 (FFI) | 17 (FFI) | 17 (FFI) | 17 (FFI) | 17 (FFI) |
235 + | Metric | Run 15 (04-18) | Run 20 (05-04) | UF1 (05-09) |
236 + |--------|:--------------:|:--------------:|:-----------:|
237 + | Overall | A | A | A- |
238 + | LOC | ~40.2K | ~42.7K | ~42.7K |
239 + | Tests | 688 | 773 | 780 |
240 + | Crates | 5 + train | 5 + train | 5 + train |
241 + | Clippy | 0 | 4 (trivial) | 4 (trivial) |
242 + | Unwrap (prod) | 2 (sidebar) | 0 | 0 |
243 + | Unsafe | 17 (FFI) | 17 (FFI) | 17 (FFI) |
244 + | Cold spots | 6 | 6 | 5 |
245 + | SERIOUS bugs | 0 | 0 | 4 |
246 + | MINOR bugs | 0 | 0 | 10 |
247 +
248 + Note: Grade lowered A → A- due to ultra-fuzz depth finding SERIOUS issues not visible in regular audits (sync snapshot staleness, security trust model). Code quality is unchanged — the issues are design-level.
128 249
129 250 ---
130 251
131 252 See [audit_history.md](./audit_history.md) for full chronological audit log.
132 -
133 - ---
134 -
135 - ## Documentation Review
136 -
137 - **Last reviewed:** 2026-05-04
138 -
139 - ### Overall Grade: A
140 -
141 - Minimal but appropriate doc set for the project's current stage. No inaccuracies found. Documentation is proportional to the project's development status.
142 -
143 - ### Document Heatmap
144 -
145 - | Document | Status | Last Verified | Notes |
146 - |----------|:------:|:-------------:|-------|
147 - | docs/todo.md | Current | 2026-05-04 | Active task list |
148 - | docs/architecture.md | Current | 2026-03-28 | System design + 5-crate workspace |
149 - | docs/competition.md | Current | 2026-03-04 | Competitive analysis |
150 - | docs/human_testing.md | Current | 2026-03-04 | Manual QA checklist |
151 - | docs/audit_review.md | Current | 2026-05-04 | Code audit history |
152 -
153 - ### Action Items
154 -
155 - - None. Clean doc set for project's current stage.
M docs/deploy.md +8 -8
@@ -7,8 +7,8 @@ See `_meta/docs/deploy.md` for shared infrastructure (machines, git remotes, col
7 7 | Platform | Artifact | Machine |
8 8 |----------|----------|---------|
9 9 | macOS aarch64 | .dmg | local |
10 - | Linux aarch64 | Binary | astra |
11 - | Linux x86_64 | Binary | pop-os |
10 + | Linux aarch64 | AppImage | astra |
11 + | Linux x86_64 | AppImage | pop-os |
12 12 | Windows x86_64 | standalone .exe | windows-x86 |
13 13
14 14 ## Build Commands
@@ -19,16 +19,16 @@ cd ~/Code/Apps/audiofiles && cargo build --release -p audiofiles-app
19 19 cp dist/AudioFiles_*_arm64.dmg ~/Dist/audiofiles/macos/
20 20
21 21 # Linux aarch64 (astra):
22 - ssh astra "source ~/.cargo/env && cd ~/Code/Apps/audiofiles && git pull && cargo build --release -p audiofiles-app"
23 - scp astra:~/Code/Apps/audiofiles/target/release/audiofiles-app ~/Dist/audiofiles/linux-aarch64/audiofiles_aarch64
22 + ssh astra "source ~/.cargo/env && cd ~/Code/Apps/audiofiles && git pull && bash dist/build-appimage.sh"
23 + scp astra:~/Code/Apps/audiofiles/dist/AudioFiles-*-aarch64.AppImage ~/Dist/audiofiles/linux-aarch64/
24 24
25 25 # Linux x86_64 (pop-os):
26 - ssh pop-os "source ~/.cargo/env && cd ~/Code/Apps/audiofiles && git pull && cargo build --release -p audiofiles-app"
27 - scp pop-os:~/Code/Apps/audiofiles/target/release/audiofiles-app ~/Dist/audiofiles/linux-x86_64/audiofiles_x86_64
26 + ssh pop-os "source ~/.cargo/env && cd ~/Code/Apps/audiofiles && git pull && bash dist/build-appimage.sh"
27 + scp pop-os:~/Code/Apps/audiofiles/dist/AudioFiles-*-x86_64.AppImage ~/Dist/audiofiles/linux-x86_64/
28 28
29 29 # Windows (windows-x86):
30 30 ssh me@windows-x86 "cd C:\Users\me\Code\Apps\audiofiles; git pull; cargo build --release -p audiofiles-app"
31 - scp me@windows-x86:"C:/Users/me/Code/Apps/audiofiles/target/release/audiofiles-app.exe" ~/Dist/audiofiles/windows/
31 + scp me@windows-x86:"C:/Users/me/Code/Apps/audiofiles/target/release/audiofiles-app.exe" ~/Dist/audiofiles/windows/AudioFiles_VERSION_x64.exe
32 32 ```
33 33
34 34 ## Project-Specific Notes
@@ -37,7 +37,7 @@ scp me@windows-x86:"C:/Users/me/Code/Apps/audiofiles/target/release/audiofiles-a
37 37 - No OTA updater — users download new versions from the MNW storefront.
38 38 - License key activation at first launch via MNW API.
39 39 - Version is in workspace root `Cargo.toml` under `[workspace.package]`.
40 - - AppImage packaging planned but not yet scripted. Currently ships as raw binary on Linux.
40 + - AppImage packaging via `dist/build-appimage.sh` (downloads appimagetool automatically).
41 41 - Requires `libxdo-dev` on Linux for keyboard shortcut handling.
42 42
43 43 ## Troubleshooting
@@ -0,0 +1,155 @@
1 + # audiofiles — Pre-Launch Manual Testing
2 +
3 + Run before every release. Sign-off table at the bottom.
4 +
5 + ## How to Test
6 +
7 + - Work through P0 first — these are launch-blocking
8 + - Note failures inline and keep going; don't block the whole run
9 + - P1 = core features, P2 = edge cases / platform-specific
10 + - Use a scratch `audiofiles_data` directory so testing doesn't touch your real library
11 +
12 + ### Environment Setup
13 +
14 + - [ ] Built binary in `dist/` (or `cargo run --release` from `Apps/audiofiles/`)
15 + - [ ] A folder of test audio samples covering WAV, MP3, FLAC, AIFF
16 + - [ ] MNW account with at least one blob-sync tier (Light/Standard/Large) for sync tests
17 + - [ ] DAW or file manager available for drag-out verification
18 +
19 + ---
20 +
21 + ## P0 — Critical Path
22 +
23 + > If any of these fail, do not ship.
24 +
25 + ### Launch & Basics
26 +
27 + - [ ] App launches without error on a clean install
28 + - [ ] Main window renders, theme applies
29 + - [ ] No panic in console; no missing-library errors
30 +
31 + ### Import + Content-Addressed Storage
32 +
33 + - [ ] Import a single WAV — sample appears in library
34 + - [ ] Import a directory of mixed formats (WAV, MP3, FLAC, AIFF) — all appear, correct counts
35 + - [ ] **Dedupe**: re-import the same file → no duplicate, same SHA-256 hash
36 + - [ ] Zero-byte file rejected with a clear error
37 + - [ ] Non-audio file rejected gracefully (no crash)
38 +
39 + ### Analysis Pipeline
40 +
41 + - [ ] Run analysis on a batch — progress indicator advances
42 + - [ ] BPM, key, loudness, classification all populate after completion
43 + - [ ] Cancel mid-batch — current sample finishes, batch stops cleanly
44 + - [ ] Re-analyze a sample — values update
45 +
46 + ### Audio Preview
47 +
48 + - [ ] Click a sample — audio plays through default output
49 + - [ ] Click another — previous stops, new one plays
50 + - [ ] Waveform displays during playback
51 + - [ ] Volume / playhead controls work
52 +
53 + ### Drag-Out (the headline feature)
54 +
55 + - [ ] Drag a single sample into a DAW — file lands and plays
56 + - [ ] Drag multiple selected samples — all land
57 + - [ ] Temp symlinks created under `/tmp/audiofiles-drag-{pid}/` (Linux/macOS)
58 + - [ ] Drag works on each shipped platform (see Platform-Specific below)
59 +
60 + ### MNW Blob Sync (Paid Feature)
61 +
62 + - [ ] Without a sync subscription, the sync UI shows "subscribe to sync"
63 + - [ ] Click subscribe → Stripe Checkout (test card `4242…`)
64 + - [ ] After checkout, sync activates automatically (poll completes)
65 + - [ ] Metadata syncs to a second device / fresh install
66 + - [ ] Samples marked `sync_files=true` upload to S3
67 + - [ ] On the second device, samples download on demand
68 + - [ ] Cloud-only eviction: free local space → sample marked `cloud_only`
69 + - [ ] Re-download a cloud-only sample — file restored, hash matches
70 +
71 + ### Edit Operations
72 +
73 + - [ ] Trim a sample — result saved as new hash, original preserved
74 + - [ ] Normalize — new hash, original preserved
75 + - [ ] Undo an edit — sample reverts
76 +
77 + ### Export
78 +
79 + - [ ] Select samples → export to a target directory — files written and playable
80 + - [ ] Format conversion (e.g., WAV → FLAC) — output validates
81 + - [ ] Metadata sidecar files generated when enabled
82 +
83 + ---
84 +
85 + ## P1 — Core Features
86 +
87 + ### VFS (Virtual File System)
88 +
89 + - [ ] Create a new VFS
90 + - [ ] Create directories within VFS, navigate between them
91 + - [ ] Link samples to a directory; rename a directory
92 + - [ ] Move a sample between directories
93 + - [ ] Delete a directory — samples remain in store
94 +
95 + ### Search & Filter
96 +
97 + - [ ] Filename text search returns matches
98 + - [ ] Filter by duration range / BPM range
99 + - [ ] Filter by classification (Kick, Snare, etc.)
100 + - [ ] Filter by tag
101 + - [ ] Combined filters compose correctly
102 +
103 + ### Tags
104 +
105 + - [ ] Add/remove tag on a single sample
106 + - [ ] Bulk-apply a tag to a selection
107 + - [ ] Search by tag
108 +
109 + ### Collections
110 +
111 + - [ ] Create collection, add and remove samples
112 + - [ ] Open collection — only its members appear
113 +
114 + ### Bulk Rename
115 +
116 + - [ ] Select multiple samples, apply pattern `{class}_{bpm}_{name}`
117 + - [ ] Preview shows expected names
118 + - [ ] Execute — VFS names update; store hashes unchanged
119 +
120 + ---
121 +
122 + ## P2 — Edge Cases & Platform
123 +
124 + ### Platform-Specific
125 +
126 + - [ ] **macOS**: drag-out via NSPasteboardItem; app appears in Dock; no Gatekeeper warning on a signed build
127 + - [ ] **Windows**: drag-out via OLE/COM; .exe installer runs; uninstall is clean
128 + - [ ] **Linux**: AppImage launches; drag-out works on X11 and Wayland (symlink fallback)
129 +
130 + ### Robustness
131 +
132 + - [ ] Import a corrupt audio file — rejected, no crash
133 + - [ ] Import 10k+ samples — UI remains responsive
134 + - [ ] Quit during analysis — relaunch resumes cleanly, no DB corruption
135 + - [ ] Disk full during import — graceful error
136 +
137 + ### Unsafe Mode
138 +
139 + - [ ] Enable Unsafe Mode (intentionally discouraging UI) — warning shown
140 + - [ ] Unsafe operations available; default-off after restart
141 +
142 + ---
143 +
144 + ## Sign-Off
145 +
146 + | Field | Value |
147 + |-------|-------|
148 + | Date | |
149 + | Tester | |
150 + | Version | |
151 + | Platform(s) | macOS / Linux / Windows |
152 + | P0 result | pass / fail |
153 + | P1 result | pass / fail |
154 + | P2 result | pass / fail / skipped |
155 + | Notes | |
M docs/todo.md +14 -240
@@ -1,34 +1,29 @@
1 1 # audiofiles TODO
2 2
3 3 ## Status
4 - Done: All pre-beta phases + Phase 11. Active: None. Next: Vocal layer 2, sample forge (phases 10-16).
4 + Done: All pre-beta phases + Phase 11 + SyncKit parity. Active: None. Next: Vocal layer 2, sample forge (phases 10-16).
5 5
6 6 v0.4.0. Audit grade A- (Ultra Fuzz 1, 2026-05-09). 780 tests. Rust 2024 edition (2026-05-06). rand 0.9. 4 SERIOUS, 10 MINOR findings from 5-axis adversarial audit. Run 20 items all resolved.
7 7
8 8 ---
9 9
10 - ## Ultra Fuzz Run 1 (2026-05-09)
11 -
12 - ### Sync (SERIOUS — fix before first multi-device user)
13 - - [x] Add `'duration', duration` to initial snapshot samples query (sync/service/state.rs:26)
14 - - [x] Add 5 missing columns to initial snapshot audio_analysis query (sync/service/state.rs:27)
15 - - [x] Exclude `unsafe_mode` from user_config sync triggers (migration 016 + snapshot filter)
16 - - [x] Batch `mark_cloud_only_samples` into single transaction (sync/service/state.rs:134)
10 + ## Dependency Pruning (2026-05-13)
17 11
18 - ### Preview decode (perf)
19 - - [x] Hoist SampleBuffer allocation out of decode loops (preview.rs:151,353)
12 + ### High Impact
13 + - [ ] [dependency-prune] Replace `chrono` with `jiff` or inline a tiny `core/util/time.rs` (only `Utc::now`, `to_rfc3339`, `parse_from_rfc3339`, `Duration::{days,minutes}` used across ~30 sites)
14 + - [ ] [dependency-prune] Verify `reqwest` TLS backend isn't doubled — confirm `synckit-client` doesn't pull `default-features = true` and stack native-tls + rustls (`cargo tree -d`, `cargo tree -e features -p reqwest`)
20 15
21 - ### Backend contention (perf)
22 - - [x] Release DB lock before VP-tree index build (backend/direct.rs — load_data/build_from_data split)
23 - - [x] Batch `enrich_with_tags` query in export (export/mod.rs — single IN query, chunked at 500)
16 + ### Medium Impact
17 + - [ ] [dependency-prune] Inline `semver` — only `Version::parse` + comparison on `X.Y.Z` strings in `updater.rs` (3 lines)
18 + - [ ] [dependency-prune] Replace `dirs` with a 30-line `core::paths` module wrapping `home_dir`/XDG/known-folder for 6 call sites
19 + - [ ] [dependency-prune] Inline `base64` — single PKCE URL-safe-no-pad encode in `sync/auth.rs` (~15 lines)
20 + - [ ] [dependency-prune] Inline `open` — `open::that(url)` × 2, replace with platform-matched `Command` (~15 lines)
21 + - [ ] [dependency-prune] Tighten `tracing-subscriber` to `default-features = false, features = ["fmt", "env-filter", "ansi"]`
22 + - [ ] [dependency-prune] Verify no transitive crate enables `tokio` `full` feature (`cargo tree -e features -p tokio`)
24 23
25 - ### UI fixes (MINOR)
26 - - [x] Fix `truncate_name` to use char boundaries, not byte offsets (ui/file_list_menus.rs:316)
27 - - [x] Fix `theme_preview_colors` key prefix: `bg.`→`background.`, `fg.`→`foreground.` (ui/theme.rs:404)
28 - - [x] Add macOS metadata dir filter to import dry-run count (import_workflow.rs)
24 + ---
29 25
30 - ### Data integrity (MINOR)
31 - - [x] Orphan delete re-checks with NOT EXISTS subquery (cleanup.rs:200)
26 + ## Ultra Fuzz Run 1 (2026-05-09)
32 27
33 28 ### Trust model (deferred — architectural)
34 29 - [ ] Add ed25519 signature verification on OTA update metadata (updater.rs)
@@ -37,38 +32,12 @@ v0.4.0. Audit grade A- (Ultra Fuzz 1, 2026-05-09). 780 tests. Rust 2024 edition
37 32
38 33 ---
39 34
40 - ## Audit Run 20 (2026-05-04)
41 -
42 - All items resolved:
43 - - Split app/main.rs: activation.rs (198L), vault_setup.rs (218L), main.rs 1296→899L
44 - - Fixed Relaxed → Acquire/Release in analysis/worker.rs (6 atomic ops)
45 - - Added 7 sync tests (download query, upload query, resolve upsert/delete edge cases)
46 - - Aligned import_directory_recursive: added sorting, audio filtering, skipped-dir checks
47 -
48 - ---
49 -
50 35 ## Sync Monetization
51 36
52 37 AF is PWYW (suggested $15, floor $0). Metadata sync is free. Blob sync (sample files via `sync_files` VFS flag) is tiered by storage. See `MNW/server/docs/internal/business/app_sync_pricing.md` for full pricing rationale.
53 38
54 - - [x] Stripe pricing: inline price_data (Light $1/$10, Standard $3/$30, Large $8/$80), no pre-created products
55 - - [x] Blob sync gate: server returns 402 on blob endpoints when no subscription, blob errors non-fatal in scheduler
56 - - [x] Metadata sync remains ungated (free for all users)
57 - - [x] Subscription UI: egui tier selector (Light/Standard/Large) with Annual/Monthly buttons, storage usage progress bar
58 - - [x] Storage usage display: progress bar showing used/limit GB from subscription status
59 - - [x] Tier upgrade/downgrade flow — server endpoint, Stripe proration, synckit-client method, AF UI with change buttons
60 - - [x] Annual billing messaging — already in sync_panel.rs:115-121 ("Annual saves you money — fewer Stripe transactions means less processing fees") + per-tier savings shown inline
61 39 - [ ] Test full checkout flow against live Stripe (end-to-end: subscribe → webhook → blob sync gate passes)
62 40
63 - ## SyncKit Parity with GoingsOn (2026-05-11)
64 -
65 - Fixes needed to match GO's working end-to-end SyncKit flow:
66 -
67 - - [ ] **synckit.toml** — create `synckit.toml` with AF's API key (need to create sync app on MNW dashboard first). Replace `option_env!("SYNCKIT_API_KEY")` / `EMBEDDED_API_KEY` in main.rs with `include_str!("../../../synckit.toml")` + parser. Current approach breaks silently on recompile without env var.
68 - - [ ] **OAuth callback CORS** — `audiofiles-sync/src/auth.rs` callback server responses missing `Access-Control-Allow-Origin: *` header. Blocks egui webview (if applicable) or external browser from polling result.
69 - - [ ] **OAuth callback not awaited (CRITICAL)** — `start_auth()` returns `AuthSession` with `code_rx` oneshot receiver, but nothing ever awaits `code_rx` or calls `complete_auth()`. The callback result is lost. Fix: spawn a background tokio task in `SyncManager::start_auth()` that awaits the callback and automatically calls `complete_auth()`, updating `SyncStatus` on success.
70 - - [ ] **No poll loop** — AF is egui (not Tauri/JS), so there's no browser-based poll. The OAuth flow needs to work via the oneshot channel + background task pattern described above.
71 -
72 41 ---
73 42
74 43 ## Classification Pipeline — Remaining
@@ -117,12 +86,6 @@ Fixes needed to match GO's working end-to-end SyncKit flow:
117 86 - [ ] Chain support: multiple plugins in series
118 87 - [ ] Wet/dry mix control + A/B preview (original vs processed)
119 88
120 - ## Phase 11: Destructive Edits (Complete)
121 -
122 - - [x] DC offset removal
123 - - [x] Silence insert/remove
124 - - [x] Mono-to-stereo / stereo-to-mono conversion
125 -
126 89 ## Phase 12: Chop Engine
127 90 - [ ] Waveform view with draggable slice markers
128 91 - [ ] Auto-chop by transient detection (reuse existing onset analysis)
@@ -156,138 +119,33 @@ Fixes needed to match GO's working end-to-end SyncKit flow:
156 119 - [ ] Compare snapshots side-by-side (A/B waveform + playback)
157 120 - [ ] Fork: branch from any snapshot to try different processing paths
158 121
159 - ## Aesthetic-Usability Polish (2026-05-05)
160 -
161 - egui Visuals overhaul — the only FAIR grade in the Laws of UX audit.
162 -
163 - - [x] Extend ThemeColors with `section_spacing` (16.0), `grid_row_spacing` (6.0), `button_padding_x` (8.0), `button_padding_y` (4.0) — TOML-configurable with defaults
164 - - [x] Waveform height: 100px → 120px (more breathing room)
165 - - [x] Detail panel section spacing: hardcoded 12/4px → theme-driven `section_spacing()` (16px default)
166 - - [x] Metadata grid row spacing: 4px → `grid_row_spacing()` (6px default)
167 - - [x] Sample name spacing: 4px → 8px after title
168 - - [x] Tag suggestions spacing: 2px → 6px
169 - - [x] Discovery buttons spacing: 4px → 6px
170 - - [x] Softer widget borders: thinner strokes (0.5px) on inactive state, full on hover/active
171 - - [x] Softer separator color: blended with background (40% lighter)
172 - - [x] Widget hover expansion: 1.0px grow on hover for tactile feedback
173 - - [x] Button padding: 6x3 → 8x4 (theme-configurable)
174 - - [x] Window margin: set to 10x10 (was egui default)
175 - - [x] Indent: 18px (was egui default ~21px)
176 -
177 122 ---
178 123
179 124 ## UX Audit Findings (2026-05-02)
180 125
181 - Usability audit across complexity, feature completeness, learnability, and discoverability.
182 - Overall grade: B+. Grades: Complexity B, Completeness B, Learnability B+, Discoverability C+.
183 -
184 - ### Critical Discoverability
185 -
186 - - [x] Add "?" button to toolbar that opens help overlay (F1 not discoverable)
187 - - [x] Add "right-click for options" hint in status bar on first launch
188 - - [x] Add "Find Similar" button in detail panel + Shift+F shortcut
189 - - [x] Add "Find Duplicates" button in detail panel + Shift+D shortcut
190 - - [x] Document drag-out to DAW/Finder: add drag handle icon or tooltip on file list rows
191 - - [x] Show keyboard shortcuts in right-click context menu items (e.g. "Bulk Rename (F2)")
192 -
193 - ### Import Flow Simplification
194 -
195 - - [x] Add "Quick Import" path: choose folder → import with default analysis → done (3 steps)
196 - - [x] Keep current advanced flow behind "Customize" toggle (Import → "Folder (customize)...")
197 - - [x] Default analysis to all enabled (BPM + Key + Loudness + Classify + Fingerprint)
198 - - [x] Add import dry-run: show file count before committing (duplicates detected during import)
199 - - [x] Show import summary after completion (added/skipped/failed counts)
200 - - [x] Auto-generate smart folder names from active filters (e.g. "BPM: 80-120 | Class: kick")
201 - - [x] Make tag folders step optional (skipped in quick import; advanced flow unchanged)
202 -
203 - ### Terminology & Mental Model
204 -
205 - - [x] Use "vault" consistently instead of library/VFS interchangeably
206 - - [x] Clarify in vault setup: "A vault is your sample collection. Files stay where they are."
207 - - [x] Merge Smart Folders + Collections into unified "Collections" (manual + dynamic)
208 - - ~~Rename "Unsafe mode"~~ — intentionally kept as-is (discourages reliance)
209 - - [x] Show dot-notation tag syntax hint in tag input placeholder: "Use dots for hierarchy: genre.house"
210 - - [x] Label search scope toggle explicitly: "in: Folder / All"
211 -
212 126 ### Feature Completeness
213 127
214 128 - [ ] Implement multi-sample instrument UI (KeyZone struct exists; radio button grayed out)
215 - - [x] Batch edit: apply gain/trim/normalize/reverse to multiple selected samples
216 129 - [ ] Export presets: save device + format + destination combos for reuse
217 - - [x] Tag templates/presets: quick-pick buttons for common tag hierarchies (via classification-based suggestions in detail panel)
218 - - [x] Show tag suggestions in detail panel (not only during import review)
219 - - [x] Batch analysis re-run on selected samples with different parameters
220 - - [x] DC offset removal: expose existing internal function as UI button in editor
221 - - [x] Pause and cancel buttons on import/analysis/export progress screens (cancel existed; pause deferred — complex state)
222 - - [x] Check destination disk space before export; warn if insufficient
223 -
224 - ### Export Edge Cases
225 -
226 - - [x] Detect and reject AIFF exports exceeding u32 chunk size (~24 min stereo 24-bit)
227 - - [x] Fix flat export filename collisions when no naming rules set (dedup suffix always)
228 - - [x] Warn when manual audio settings violate device profile constraints (N/A: profile hides manual controls)
229 - - [x] Show file size limit errors in export config screen, not after export starts
230 -
231 - ### Learnability
232 -
233 - - [x] First-launch hint: "Press F1 for keyboard shortcuts" (dismissible)
234 - - [x] Add tooltips showing shortcut on toolbar buttons: "Edit (E)", "Instrument (I)"
235 - - [x] Add loop toggle button in toolbar (L key shortcut shown in tooltip)
236 - - [x] Add Cmd+M shortcut for bulk move
237 - - [x] Explain Collections vs Smart Folders distinction (merged — no longer needed)
238 130
239 131 ### Complexity Reduction
240 132
241 - - [x] Theme picker: show color swatch/thumbnail per theme instead of flat name dropdown
242 133 - [ ] Allow toggling unsafe mode per-vault in Settings (with copy-files-into-vault action)
243 134 - [ ] Split Settings into tabs: Library, Display, License, Advanced
244 - - [x] Add tag search box above sidebar tag tree with filter
245 135
246 136 ### Power User Gaps
247 137
248 138 - [ ] Undo persistence: store deleted node metadata in DB (not just in-memory)
249 139 - [ ] Or: add "Recently Deleted" trash section in sidebar with recovery
250 140 - [ ] Bulk duplicate (create copies of selected samples)
251 - - [x] Copy metadata: apply tags/BPM/key from one sample to selected others
252 - - [x] Sync status indicator in toolbar: synced/syncing/pending count — button label shows ✓/↻/count/–, tooltip shows detail
253 141 - [ ] Show sync conflict resolution when two devices edit same sample
254 142 - [ ] Smart folders should be dynamic (re-compute on visit, not static snapshots)
255 143
256 - ### Documentation
257 -
258 - - [x] Expand help overlay beyond shortcuts: add "Features" tab with search, filters, collections, tags, import, export
259 - - [x] Document system tray integration in settings or help — added to Features tab in help overlay
260 - - [x] Show device profile count in export dialog header
261 -
262 - ## UX Audit Findings (2026-05-03)
263 -
264 - Usability audit focused on complexity, completeness, learnability, discoverability.
265 - Overall grade: B+. Grades: Complexity B, Completeness B+, Learnability C+, Discoverability C.
266 -
267 - ### Prioritized Fixes
268 -
269 - 1. [x] **Structured first-run onboarding** — welcome screen with 3-step guide + inline Import link on first launch
270 - 2. [x] **Persist filter state across navigation** — filters already persist; added filter-aware empty state ("No matches in this folder" + Clear Filters button)
271 - 3. [x] **Surface MIDI/instrument features** — empty state hint in instrument panel, tooltip on piano keyboard (play/right-click/drag)
272 - 4. [x] **Batch editing** — batch normalize (peak/LUFS), gain, reverse in edit panel when 2+ samples selected
273 - 5. [x] **Numeric range filters** — BPM + duration already existed; added loudness (peak dB) range filter
274 - 6. ~~Rename "Unsafe mode"~~ — intentionally kept as-is (see prior decision above)
275 - 7. [x] **Promote "Save as Collection"** — show persistent "Save" button in toolbar/filter header when filters active
276 - 8. [x] **Simplify Settings** — moved theme import/export and vault mirror to collapsed "Advanced" section
277 -
278 144 ---
279 145
280 146 ## Rust-Fuzz Findings (2026-05-04)
281 147
282 - Rust quality audit: unsafe discipline, memory efficiency, error handling, smart pointers.
283 - Overall grade: A-. Unsafe: CLEAN. Memory: SOME WASTE. Errors: ELEGANT. Pointers: JUSTIFIED.
284 -
285 - ### Must Fix
286 - - [x] [rust-fuzz] `bulk_ops.rs:84-103` — `selected_nodes()` clones full structs; add field-specific accessors that extract ids/hashes without cloning
287 - - [x] [rust-fuzz] `import_workflow.rs:585,598` — full hash Vec cloned to escape `if let` borrow; use `std::mem::replace` to move data out
288 -
289 148 ### Should Fix
290 - - [x] [rust-fuzz] `export_screens.rs:19,34` — add `// SAFETY:` comments to `statvfs` and `GetDiskFreeSpaceExW` unsafe blocks
291 149 - [ ] [rust-fuzz] `file_list_menus.rs:255,327` — `selected_nodes()` in drag path; iterate indices by reference instead
292 150 - [ ] [rust-fuzz] `bulk_ops.rs:248-382` — use `Option::take()` instead of cloning to escape `if let` borrows (4 sites)
293 151 - [ ] [rust-fuzz] `sidebar.rs:361` — tag list cloned every frame when search empty; pass reference or cache tree
@@ -300,94 +158,10 @@ Overall grade: A-. Unsafe: CLEAN. Memory: SOME WASTE. Errors: ELEGANT. Pointers:
300 158 - [ ] Updater UI: extract updater.js from GO/BB into shared module
301 159 - [ ] Saved queries: unify GO saved views, BB query feeds, AF smart folders
302 160
303 - ## Code Fuzz Findings (2026-05-03)
304 -
305 - Second audit of all 7 crates (~42k lines). Items marked [mechanical] are fixable without design changes.
306 -
307 - ### Critical
308 - - [x] **C1** [mechanical] Poison changelog entry blocks sync forever — skipped entries now marked `pushed=1` (`sync/upload.rs:213`)
309 - - [x] **C2** [mechanical] Corrupted JSON pushed as `data: None` — unparseable entries now marked pushed and skipped (`sync/upload.rs:222`)
310 -
311 - ### Serious
312 - - [x] **S1** [mechanical] `applying_remote` flag stuck on error — ROLLBACK on failure in `mark_cloud_only_samples` (`sync/state.rs:131`)
313 - - [x] **S2** [mechanical] Non-atomic blob download — write to temp file then atomic rename (`sync/download.rs:115`)
314 - - [x] **S3** `INSERT OR REPLACE` cascades FK deletes — replaced with `INSERT ... ON CONFLICT DO UPDATE` (`sync/resolve.rs:105`)
315 - - [x] **S4** [mechanical] `set_sync_state` silently no-ops if key missing — uses `INSERT OR REPLACE` now (`sync/mod.rs:140`)
316 - - [x] **S5** [mechanical] Infinity duration when `sample_rate=0` — early return with duration 0.0 (`analysis/waveform.rs:23`)
317 - - [x] **S6** [mechanical] No `sample_rate=0` guard in `measure_lufs` — returns -70.0 sentinel (`analysis/loudness.rs:6`)
318 -
319 - ### Medium
320 - - [x] **M1** [mechanical] `2u64.pow(consecutive_failures)` panics at 64 failures — replaced with `saturating_pow` (`sync/scheduler.rs:75`)
321 - - [x] **M2** [mechanical] `zone_index` OOB panic in audio thread — bounds check deactivates stale voice (`browser/instrument.rs:250`)
322 - - [x] **M3** [mechanical] OOM from unvalidated `n_frames` metadata — capacity capped at ~30 min stereo (`browser/preview.rs:297`)
323 - - [x] **M4** [mechanical] Division by zero panic when `channels=0` in trim — returns error (`core/edit/trim.rs:16`)
324 - - [x] **M5** [mechanical] Division by zero panic when `channels=0` in silence insert/remove — returns error (`core/edit/silence.rs:15`)
325 - - [x] **M6** [mechanical] `apply_mono_to_stereo` doesn't verify input is mono — now takes `channels` param, rejects non-mono (`core/edit/channel_convert.rs:8`)
326 - - [x] **M7** [mechanical] Empty/separator-only rename pattern produces empty filename — rejected at parse time (`core/rename.rs:45`)
327 - - [x] **M8** [mechanical] `start_cleanup` doesn't cancel existing worker — cancels before spawning new one (`browser/backend/direct.rs:850`)
328 - - [x] **M9** [mechanical] `pad_left`/`pad_right`/`format_index` OOM — width capped at 10,000 (`rhai/host_api.rs:27`)
329 -
330 - ### Minor / Notes
331 - - [x] **N1** [mechanical] FFT error silently discarded via `.ok()` (`analysis/spectral.rs:116`). Fixed: skip frame on error.
332 - - [x] **N2** NaN features always traverse right branch in random forest — no NaN guard at classifier boundary (`analysis/classify.rs:546`). Fixed: NaN goes left (conservative path).
333 - - [x] **N3** [mechanical] Zero dither seed produces degenerate xorshift — outputs 0 forever (`export/dither.rs:9`). Fixed: substitute non-zero seed.
334 - - [x] **N4** `peak_db` on empty slice returns -96.0 same as silence — inconsistent with `rms_db` which guards empty (`analysis/basic.rs:8`). Fixed: early return for empty slice.
335 - - [x] **N5** Initial snapshot not transactional — partial failure creates duplicate changelog entries on retry (`sync/state.rs:15`). Fixed: wrapped in transaction.
336 - - [x] **N6** `INFINITY` distance violates VP-tree triangle inequality contract (`similarity.rs:143`). Fixed: use large finite value (1e10).
337 - - [x] **N7** Malformed filter JSON silently downgrades dynamic collection to manual (`collections.rs:94`). Fixed: log warning.
338 - - [x] **N8** No explicit `set_max_expr_depths` in Rhai engine — deeply nested expressions could blow Rust stack (`rhai/engine.rs:9`). Fixed: set depths (64, 32).
339 - - [x] **N9** `build_sample_info` silently swallows non-"not found" DB errors via `unwrap_or` (`browser/backend/direct.rs:234`). Fixed: log warning for non-"not found" errors.
340 -
341 - ---
342 -
343 161 ## Code Fuzz Findings (2026-04-27)
344 162
345 - Audit of all 7 crates (~40k lines). Items marked [mechanical] are fixable without design changes.
346 -
347 - ### Critical
348 - - [x] **C1** [mechanical] Export path traversal: filenames not sanitized without NamingRules — `../../evil.wav` writes outside dest (`export/resolve.rs`)
349 - - [x] **C2** [mechanical] Streaming buffer OOB: race between `decoded_frames` and `data.extend()` in audio callback (`audio.rs:172`)
350 -
351 - ### Serious
352 - - [x] **S1** `applying_remote` flag stuck on crash — wrapped in transaction for atomicity (`sync/service/resolve.rs:16`)
353 - - [x] **S2** Changelog retention now prefers deleting pushed entries first (`sync/service/state.rs:69`)
354 - - [x] **S3** DB lock released before Rhai script execution (`backend/direct.rs:146`)
355 - - [x] **S4** [mechanical] ML model OOB: `features[*feature]` unchecked in `TreeNode::predict` (`classify.rs:219`)
356 - - [x] **S5** `expect()` replaced with graceful fallback to empty model (`classify.rs:331`)
357 - - [x] **S6** [mechanical] Division by zero when packet reports 0 channels in decode (`decode.rs:126`)
358 - - [x] **S7** [mechanical] `remove_orphaned_samples` manual transaction lacks rollback on error (`store.rs:230`)
359 - - [x] **S8** [mechanical] `purge_missing_unsafe` no atomicity — partial purge + concurrent import can cascade-delete (`store.rs:425`)
360 - - [x] **S9** Edit temp files cleaned up on worker startup (`edit/worker.rs:164`)
361 - - [x] **S10** [mechanical] Rename pattern resolves to empty string when all tokens are None (`rename.rs:92`)
362 - - [x] **S11** `bit_depth` probed from file header at export time (`backend/direct.rs:253`)
363 -
364 163 ### Minor
365 - - [x] **M1** [mechanical] Division by zero with 0 channels in `convert_channels` and `resample` (`export/convert.rs`)
366 - - [x] **M2** [mechanical] Division by zero with 0 channels in AIFF encoder (`export/encode_aiff.rs:26`)
367 - - [x] **M3** `sanitize_filename` returns "untitled" for empty results (`export/sanitize.rs:13`)
368 - - [x] **M4** Original export now copies instead of hardlinking (`export/runner.rs:155`)
369 - - [x] **M5** [mechanical] Division by zero with sample_rate=0 in `detect_bpm_key` (`bpm.rs:19`)
370 - - [x] **M6** [mechanical] Division by zero with sample_rate=0 in `is_beat_aligned` (`loop_detect.rs:85`)
371 - - [x] **M7** Trial clock rollback detected via `last_seen_date` field (`license.rs:217`)
372 164 - [ ] **M8** Trial reset by deleting `trial.json` — accepted as intentionally lenient (`license.rs:201`)
373 - - [x] **M9** Update URL pinned to makenot.work domain (`updater.rs:122`)
374 - - [x] **M10** VP-tree depth cap now chains all remaining items as linked list (`vp_tree.rs:152`)
375 - - [x] **M11** Added partial unique index for root-level VFS nodes (migration 014) (`db.rs:65`)
376 - - [x] **M12** Migration non-ALTER errors now logged as warnings (`db.rs:696`)
377 - - [x] **M13** `validate_sample` hook error now rejects (fail-closed) (`backend/direct.rs:149`)
378 - - [x] **M14** `transform_filename` output sanitized (path separators stripped) (`backend/direct.rs:185`)
379 - - [x] **M15** User-to-user plugin override now logs a warning (`registry.rs:45`)
380 - - [x] **M16** Windows `GlobalLock` null return checked in OLE drag (`windows.rs:209`)
381 -
382 - ### Notes (all fixed)
383 - - [x] N1: `registry_path()`/`default_vault_path()` now log warning on $HOME fallback (`vault.rs:45`)
384 - - [x] N2: Rayon panics caught with `catch_unwind`, emitted as `SampleError` (`worker.rs:145`)
385 - - [x] N3: File size check (2 GB) + duration check (30 min) before/after decode (`analysis/mod.rs:87`)
386 - - [x] N4: `feature_distance` returns `f64::INFINITY` when both vectors all-None (`similarity.rs:116`)
387 - - [x] N5: Cancel flag uses `Acquire`/`Release` ordering; cancel checks between decode/edit/encode (`edit/worker.rs`)
388 - - [x] N6: Existing worker explicitly cancelled before starting new one (`backend/direct.rs`)
389 - - [x] N7: `discover_plugins` logs warnings for read/parse errors (`rhai/loader.rs:31`)
390 - - [x] N8: Multi-char separator rejected with error instead of silently truncated (`rhai/manifest.rs:137`)
391 165
392 166 ---
393 167
@@ -0,0 +1,284 @@
1 + # audiofiles — Completed Items
2 +
3 + Items below were moved from todo.md to keep the active todo focused on open work.
4 +
5 + ---
6 +
7 + ## Ultra Fuzz Run 1 (2026-05-09)
8 +
9 + ### Sync (SERIOUS — fix before first multi-device user)
10 + - [x] Add `'duration', duration` to initial snapshot samples query (sync/service/state.rs:26)
11 + - [x] Add 5 missing columns to initial snapshot audio_analysis query (sync/service/state.rs:27)
12 + - [x] Exclude `unsafe_mode` from user_config sync triggers (migration 016 + snapshot filter)
13 + - [x] Batch `mark_cloud_only_samples` into single transaction (sync/service/state.rs:134)
14 +
15 + ### Preview decode (perf)
16 + - [x] Hoist SampleBuffer allocation out of decode loops (preview.rs:151,353)
17 +
18 + ### Backend contention (perf)
19 + - [x] Release DB lock before VP-tree index build (backend/direct.rs — load_data/build_from_data split)
20 + - [x] Batch `enrich_with_tags` query in export (export/mod.rs — single IN query, chunked at 500)
21 +
22 + ### UI fixes (MINOR)
23 + - [x] Fix `truncate_name` to use char boundaries, not byte offsets (ui/file_list_menus.rs:316)
24 + - [x] Fix `theme_preview_colors` key prefix: `bg.`→`background.`, `fg.`→`foreground.` (ui/theme.rs:404)
25 + - [x] Add macOS metadata dir filter to import dry-run count (import_workflow.rs)
26 +
27 + ### Data integrity (MINOR)
28 + - [x] Orphan delete re-checks with NOT EXISTS subquery (cleanup.rs:200)
29 +
30 + ---
31 +
32 + ## Audit Run 20 (2026-05-04)
33 +
34 + All items resolved:
35 + - Split app/main.rs: activation.rs (198L), vault_setup.rs (218L), main.rs 1296→899L
36 + - Fixed Relaxed → Acquire/Release in analysis/worker.rs (6 atomic ops)
37 + - Added 7 sync tests (download query, upload query, resolve upsert/delete edge cases)
38 + - Aligned import_directory_recursive: added sorting, audio filtering, skipped-dir checks
39 +
40 + ---
41 +
42 + ## Sync Monetization (completed items)
43 +
44 + - [x] Stripe pricing: inline price_data (Light $1/$10, Standard $3/$30, Large $8/$80), no pre-created products
45 + - [x] Blob sync gate: server returns 402 on blob endpoints when no subscription, blob errors non-fatal in scheduler
46 + - [x] Metadata sync remains ungated (free for all users)
47 + - [x] Subscription UI: egui tier selector (Light/Standard/Large) with Annual/Monthly buttons, storage usage progress bar
48 + - [x] Storage usage display: progress bar showing used/limit GB from subscription status
49 + - [x] Tier upgrade/downgrade flow — server endpoint, Stripe proration, synckit-client method, AF UI with change buttons
50 + - [x] Annual billing messaging — already in sync_panel.rs:115-121 ("Annual saves you money — fewer Stripe transactions means less processing fees") + per-tier savings shown inline
51 +
52 + ## SyncKit Parity with GoingsOn (2026-05-11)
53 +
54 + Fixes needed to match GO's working end-to-end SyncKit flow:
55 +
56 + - [x] **synckit.toml** — create `synckit.toml` with AF's API key (need to create sync app on MNW dashboard first). Replace `option_env!("SYNCKIT_API_KEY")` / `EMBEDDED_API_KEY` in main.rs with `include_str!("../../../synckit.toml")` + parser. Current approach breaks silently on recompile without env var.
57 + - [x] **OAuth callback CORS** — `audiofiles-sync/src/auth.rs` callback server responses missing `Access-Control-Allow-Origin: *` header. Blocks egui webview (if applicable) or external browser from polling result.
58 + - [x] **OAuth callback not awaited (CRITICAL)** — `start_auth()` returns `AuthSession` with `code_rx` oneshot receiver, but nothing ever awaits `code_rx` or calls `complete_auth()`. The callback result is lost. Fix: spawn a background tokio task in `SyncManager::start_auth()` that awaits the callback and automatically calls `complete_auth()`, updating `SyncStatus` on success.
59 + - [x] **No poll loop** — AF is egui (not Tauri/JS), so there's no browser-based poll. The OAuth flow needs to work via the oneshot channel + background task pattern described above.
60 +
61 + ---
62 +
63 + ## Phase 11: Destructive Edits (Complete)
64 +
65 + - [x] DC offset removal
66 + - [x] Silence insert/remove
67 + - [x] Mono-to-stereo / stereo-to-mono conversion
68 +
69 + ## Aesthetic-Usability Polish (2026-05-05)
70 +
71 + egui Visuals overhaul — the only FAIR grade in the Laws of UX audit.
72 +
73 + - [x] Extend ThemeColors with `section_spacing` (16.0), `grid_row_spacing` (6.0), `button_padding_x` (8.0), `button_padding_y` (4.0) — TOML-configurable with defaults
74 + - [x] Waveform height: 100px → 120px (more breathing room)
75 + - [x] Detail panel section spacing: hardcoded 12/4px → theme-driven `section_spacing()` (16px default)
76 + - [x] Metadata grid row spacing: 4px → `grid_row_spacing()` (6px default)
77 + - [x] Sample name spacing: 4px → 8px after title
78 + - [x] Tag suggestions spacing: 2px → 6px
79 + - [x] Discovery buttons spacing: 4px → 6px
80 + - [x] Softer widget borders: thinner strokes (0.5px) on inactive state, full on hover/active
81 + - [x] Softer separator color: blended with background (40% lighter)
82 + - [x] Widget hover expansion: 1.0px grow on hover for tactile feedback
83 + - [x] Button padding: 6x3 → 8x4 (theme-configurable)
84 + - [x] Window margin: set to 10x10 (was egui default)
85 + - [x] Indent: 18px (was egui default ~21px)
86 +
87 + ---
88 +
89 + ## UX Audit Findings (2026-05-02) (completed items)
90 +
91 + Usability audit across complexity, feature completeness, learnability, and discoverability.
92 + Overall grade: B+. Grades: Complexity B, Completeness B, Learnability B+, Discoverability C+.
93 +
94 + ### Critical Discoverability
95 +
96 + - [x] Add "?" button to toolbar that opens help overlay (F1 not discoverable)
97 + - [x] Add "right-click for options" hint in status bar on first launch
98 + - [x] Add "Find Similar" button in detail panel + Shift+F shortcut
99 + - [x] Add "Find Duplicates" button in detail panel + Shift+D shortcut
100 + - [x] Document drag-out to DAW/Finder: add drag handle icon or tooltip on file list rows
101 + - [x] Show keyboard shortcuts in right-click context menu items (e.g. "Bulk Rename (F2)")
102 +
103 + ### Import Flow Simplification
104 +
105 + - [x] Add "Quick Import" path: choose folder → import with default analysis → done (3 steps)
106 + - [x] Keep current advanced flow behind "Customize" toggle (Import → "Folder (customize)...")
107 + - [x] Default analysis to all enabled (BPM + Key + Loudness + Classify + Fingerprint)
108 + - [x] Add import dry-run: show file count before committing (duplicates detected during import)
109 + - [x] Show import summary after completion (added/skipped/failed counts)
110 + - [x] Auto-generate smart folder names from active filters (e.g. "BPM: 80-120 | Class: kick")
111 + - [x] Make tag folders step optional (skipped in quick import; advanced flow unchanged)
112 +
113 + ### Terminology & Mental Model
114 +
115 + - [x] Use "vault" consistently instead of library/VFS interchangeably
116 + - [x] Clarify in vault setup: "A vault is your sample collection. Files stay where they are."
117 + - [x] Merge Smart Folders + Collections into unified "Collections" (manual + dynamic)
118 + - ~~Rename "Unsafe mode"~~ — intentionally kept as-is (discourages reliance)
119 + - [x] Show dot-notation tag syntax hint in tag input placeholder: "Use dots for hierarchy: genre.house"
120 + - [x] Label search scope toggle explicitly: "in: Folder / All"
121 +
122 + ### Feature Completeness (completed items)
123 +
124 + - [x] Batch edit: apply gain/trim/normalize/reverse to multiple selected samples
125 + - [x] Tag templates/presets: quick-pick buttons for common tag hierarchies (via classification-based suggestions in detail panel)
126 + - [x] Show tag suggestions in detail panel (not only during import review)
127 + - [x] Batch analysis re-run on selected samples with different parameters
128 + - [x] DC offset removal: expose existing internal function as UI button in editor
129 + - [x] Pause and cancel buttons on import/analysis/export progress screens (cancel existed; pause deferred — complex state)
130 + - [x] Check destination disk space before export; warn if insufficient
131 +
132 + ### Export Edge Cases
133 +
134 + - [x] Detect and reject AIFF exports exceeding u32 chunk size (~24 min stereo 24-bit)
135 + - [x] Fix flat export filename collisions when no naming rules set (dedup suffix always)
136 + - [x] Warn when manual audio settings violate device profile constraints (N/A: profile hides manual controls)
137 + - [x] Show file size limit errors in export config screen, not after export starts
138 +
139 + ### Learnability
140 +
141 + - [x] First-launch hint: "Press F1 for keyboard shortcuts" (dismissible)
142 + - [x] Add tooltips showing shortcut on toolbar buttons: "Edit (E)", "Instrument (I)"
143 + - [x] Add loop toggle button in toolbar (L key shortcut shown in tooltip)
144 + - [x] Add Cmd+M shortcut for bulk move
145 + - [x] Explain Collections vs Smart Folders distinction (merged — no longer needed)
146 +
147 + ### Complexity Reduction (completed items)
148 +
149 + - [x] Theme picker: show color swatch/thumbnail per theme instead of flat name dropdown
150 + - [x] Add tag search box above sidebar tag tree with filter
151 +
152 + ### Documentation
153 +
154 + - [x] Expand help overlay beyond shortcuts: add "Features" tab with search, filters, collections, tags, import, export
155 + - [x] Document system tray integration in settings or help — added to Features tab in help overlay
156 + - [x] Show device profile count in export dialog header
157 +
158 + ## UX Audit Findings (2026-05-03) (completed items)
159 +
160 + Usability audit focused on complexity, completeness, learnability, discoverability.
161 + Overall grade: B+. Grades: Complexity B, Completeness B+, Learnability C+, Discoverability C.
162 +
163 + ### Prioritized Fixes
164 +
165 + 1. [x] **Structured first-run onboarding** — welcome screen with 3-step guide + inline Import link on first launch
166 + 2. [x] **Persist filter state across navigation** — filters already persist; added filter-aware empty state ("No matches in this folder" + Clear Filters button)
167 + 3. [x] **Surface MIDI/instrument features** — empty state hint in instrument panel, tooltip on piano keyboard (play/right-click/drag)
168 + 4. [x] **Batch editing** — batch normalize (peak/LUFS), gain, reverse in edit panel when 2+ samples selected
169 + 5. [x] **Numeric range filters** — BPM + duration already existed; added loudness (peak dB) range filter
170 + 6. ~~Rename "Unsafe mode"~~ — intentionally kept as-is (see prior decision above)
171 + 7. [x] **Promote "Save as Collection"** — show persistent "Save" button in toolbar/filter header when filters active
172 + 8. [x] **Simplify Settings** — moved theme import/export and vault mirror to collapsed "Advanced" section
173 +
174 + ---
175 +
176 + ## Rust-Fuzz Findings (2026-05-04) (completed items)
177 +
178 + Rust quality audit: unsafe discipline, memory efficiency, error handling, smart pointers.
179 + Overall grade: A-. Unsafe: CLEAN. Memory: SOME WASTE. Errors: ELEGANT. Pointers: JUSTIFIED.
180 +
181 + ### Must Fix
182 + - [x] [rust-fuzz] `bulk_ops.rs:84-103` — `selected_nodes()` clones full structs; add field-specific accessors that extract ids/hashes without cloning
183 + - [x] [rust-fuzz] `import_workflow.rs:585,598` — full hash Vec cloned to escape `if let` borrow; use `std::mem::replace` to move data out
184 +
185 + ### Should Fix (completed items)
186 + - [x] [rust-fuzz] `export_screens.rs:19,34` — add `// SAFETY:` comments to `statvfs` and `GetDiskFreeSpaceExW` unsafe blocks
187 +
188 + ---
189 +
190 + ## Code Fuzz Findings (2026-05-03) (completed items)
191 +
192 + Second audit of all 7 crates (~42k lines). Items marked [mechanical] are fixable without design changes.
193 +
194 + ### Critical
195 + - [x] **C1** [mechanical] Poison changelog entry blocks sync forever — skipped entries now marked `pushed=1` (`sync/upload.rs:213`)
196 + - [x] **C2** [mechanical] Corrupted JSON pushed as `data: None` — unparseable entries now marked pushed and skipped (`sync/upload.rs:222`)
197 +
198 + ### Serious
199 + - [x] **S1** [mechanical] `applying_remote` flag stuck on error — ROLLBACK on failure in `mark_cloud_only_samples` (`sync/state.rs:131`)
200 + - [x] **S2** [mechanical] Non-atomic blob download — write to temp file then atomic rename (`sync/download.rs:115`)
201 + - [x] **S3** `INSERT OR REPLACE` cascades FK deletes — replaced with `INSERT ... ON CONFLICT DO UPDATE` (`sync/resolve.rs:105`)
202 + - [x] **S4** [mechanical] `set_sync_state` silently no-ops if key missing — uses `INSERT OR REPLACE` now (`sync/mod.rs:140`)
203 + - [x] **S5** [mechanical] Infinity duration when `sample_rate=0` — early return with duration 0.0 (`analysis/waveform.rs:23`)
204 + - [x] **S6** [mechanical] No `sample_rate=0` guard in `measure_lufs` — returns -70.0 sentinel (`analysis/loudness.rs:6`)
205 +
206 + ### Medium
207 + - [x] **M1** [mechanical] `2u64.pow(consecutive_failures)` panics at 64 failures — replaced with `saturating_pow` (`sync/scheduler.rs:75`)
208 + - [x] **M2** [mechanical] `zone_index` OOB panic in audio thread — bounds check deactivates stale voice (`browser/instrument.rs:250`)
209 + - [x] **M3** [mechanical] OOM from unvalidated `n_frames` metadata — capacity capped at ~30 min stereo (`browser/preview.rs:297`)
210 + - [x] **M4** [mechanical] Division by zero panic when `channels=0` in trim — returns error (`core/edit/trim.rs:16`)
211 + - [x] **M5** [mechanical] Division by zero panic when `channels=0` in silence insert/remove — returns error (`core/edit/silence.rs:15`)
212 + - [x] **M6** [mechanical] `apply_mono_to_stereo` doesn't verify input is mono — now takes `channels` param, rejects non-mono (`core/edit/channel_convert.rs:8`)
213 + - [x] **M7** [mechanical] Empty/separator-only rename pattern produces empty filename — rejected at parse time (`core/rename.rs:45`)
214 + - [x] **M8** [mechanical] `start_cleanup` doesn't cancel existing worker — cancels before spawning new one (`browser/backend/direct.rs:850`)
215 + - [x] **M9** [mechanical] `pad_left`/`pad_right`/`format_index` OOM — width capped at 10,000 (`rhai/host_api.rs:27`)
216 +
217 + ### Minor / Notes
218 + - [x] **N1** [mechanical] FFT error silently discarded via `.ok()` (`analysis/spectral.rs:116`). Fixed: skip frame on error.
219 + - [x] **N2** NaN features always traverse right branch in random forest — no NaN guard at classifier boundary (`analysis/classify.rs:546`). Fixed: NaN goes left (conservative path).
220 + - [x] **N3** [mechanical] Zero dither seed produces degenerate xorshift — outputs 0 forever (`export/dither.rs:9`). Fixed: substitute non-zero seed.
221 + - [x] **N4** `peak_db` on empty slice returns -96.0 same as silence — inconsistent with `rms_db` which guards empty (`analysis/basic.rs:8`). Fixed: early return for empty slice.
222 + - [x] **N5** Initial snapshot not transactional — partial failure creates duplicate changelog entries on retry (`sync/state.rs:15`). Fixed: wrapped in transaction.
223 + - [x] **N6** `INFINITY` distance violates VP-tree triangle inequality contract (`similarity.rs:143`). Fixed: use large finite value (1e10).
224 + - [x] **N7** Malformed filter JSON silently downgrades dynamic collection to manual (`collections.rs:94`). Fixed: log warning.
225 + - [x] **N8** No explicit `set_max_expr_depths` in Rhai engine — deeply nested expressions could blow Rust stack (`rhai/engine.rs:9`). Fixed: set depths (64, 32).
226 + - [x] **N9** `build_sample_info` silently swallows non-"not found" DB errors via `unwrap_or` (`browser/backend/direct.rs:234`). Fixed: log warning for non-"not found" errors.
227 +
228 + ---
229 +
230 + ## Code Fuzz Findings (2026-04-27) (completed items)
231 +
232 + Audit of all 7 crates (~40k lines). Items marked [mechanical] are fixable without design changes.
233 +
234 + ### Critical
235 + - [x] **C1** [mechanical] Export path traversal: filenames not sanitized without NamingRules — `../../evil.wav` writes outside dest (`export/resolve.rs`)
236 + - [x] **C2** [mechanical] Streaming buffer OOB: race between `decoded_frames` and `data.extend()` in audio callback (`audio.rs:172`)
237 +
238 + ### Serious
239 + - [x] **S1** `applying_remote` flag stuck on crash — wrapped in transaction for atomicity (`sync/service/resolve.rs:16`)
240 + - [x] **S2** Changelog retention now prefers deleting pushed entries first (`sync/service/state.rs:69`)
241 + - [x] **S3** DB lock released before Rhai script execution (`backend/direct.rs:146`)
242 + - [x] **S4** [mechanical] ML model OOB: `features[*feature]` unchecked in `TreeNode::predict` (`classify.rs:219`)
243 + - [x] **S5** `expect()` replaced with graceful fallback to empty model (`classify.rs:331`)
244 + - [x] **S6** [mechanical] Division by zero when packet reports 0 channels in decode (`decode.rs:126`)
245 + - [x] **S7** [mechanical] `remove_orphaned_samples` manual transaction lacks rollback on error (`store.rs:230`)
246 + - [x] **S8** [mechanical] `purge_missing_unsafe` no atomicity — partial purge + concurrent import can cascade-delete (`store.rs:425`)
247 + - [x] **S9** Edit temp files cleaned up on worker startup (`edit/worker.rs:164`)
248 + - [x] **S10** [mechanical] Rename pattern resolves to empty string when all tokens are None (`rename.rs:92`)
249 + - [x] **S11** `bit_depth` probed from file header at export time (`backend/direct.rs:253`)
250 +
251 + ### Minor (completed items)
252 + - [x] **M1** [mechanical] Division by zero with 0 channels in `convert_channels` and `resample` (`export/convert.rs`)
253 + - [x] **M2** [mechanical] Division by zero with 0 channels in AIFF encoder (`export/encode_aiff.rs:26`)
254 + - [x] **M3** `sanitize_filename` returns "untitled" for empty results (`export/sanitize.rs:13`)
255 + - [x] **M4** Original export now copies instead of hardlinking (`export/runner.rs:155`)
256 + - [x] **M5** [mechanical] Division by zero with sample_rate=0 in `detect_bpm_key` (`bpm.rs:19`)
257 + - [x] **M6** [mechanical] Division by zero with sample_rate=0 in `is_beat_aligned` (`loop_detect.rs:85`)
258 + - [x] **M7** Trial clock rollback detected via `last_seen_date` field (`license.rs:217`)
259 + - [x] **M9** Update URL pinned to makenot.work domain (`updater.rs:122`)
260 + - [x] **M10** VP-tree depth cap now chains all remaining items as linked list (`vp_tree.rs:152`)
261 + - [x] **M11** Added partial unique index for root-level VFS nodes (migration 014) (`db.rs:65`)
262 + - [x] **M12** Migration non-ALTER errors now logged as warnings (`db.rs:696`)
263 + - [x] **M13** `validate_sample` hook error now rejects (fail-closed) (`backend/direct.rs:149`)
264 + - [x] **M14** `transform_filename` output sanitized (path separators stripped) (`backend/direct.rs:185`)
265 + - [x] **M15** User-to-user plugin override now logs a warning (`registry.rs:45`)
266 + - [x] **M16** Windows `GlobalLock` null return checked in OLE drag (`windows.rs:209`)
267 +
268 + ### Notes (all fixed)
269 + - [x] N1: `registry_path()`/`default_vault_path()` now log warning on $HOME fallback (`vault.rs:45`)
270 + - [x] N2: Rayon panics caught with `catch_unwind`, emitted as `SampleError` (`worker.rs:145`)
271 + - [x] N3: File size check (2 GB) + duration check (30 min) before/after decode (`analysis/mod.rs:87`)
272 + - [x] N4: `feature_distance` returns `f64::INFINITY` when both vectors all-None (`similarity.rs:116`)
273 + - [x] N5: Cancel flag uses `Acquire`/`Release` ordering; cancel checks between decode/edit/encode (`edit/worker.rs`)
274 + - [x] N6: Existing worker explicitly cancelled before starting new one (`backend/direct.rs`)
275 + - [x] N7: `discover_plugins` logs warnings for read/parse errors (`rhai/loader.rs:31`)
276 + - [x] N8: Multi-char separator rejected with error instead of silently truncated (`rhai/manifest.rs:137`)
277 +
278 + ---
279 +
280 + ## UX Audit Findings (2026-05-02) (remaining completed items)
281 +
282 + ### Power User Gaps (completed items)
283 + - [x] Copy metadata: apply tags/BPM/key from one sample to selected others
284 + - [x] Sync status indicator in toolbar: synced/syncing/pending count — button label shows ✓/↻/count/–, tooltip shows detail