max / audiofiles
5 files changed,
+690 insertions,
-380 deletions
| @@ -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. |
| @@ -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 | | |
| @@ -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 |