Skip to main content

max / audiofiles

docs: refresh schema/architecture/description/troubleshooting Subagent stale-plan audit against the post-launch codebase. Four docs drifted across the May→June migration cycle: - database_schema.md: bumped migration count (12 → 18); added Schema Evolution rows for M013–M018; added samples.source_path, collections.filter_json, sync_state.row_id_salt; removed smart_folders block (dropped in M015); noted that sync_changelog.row_id is hashed for sensitive tables (M018). - architecture.md: bumped migration count; swapped smart_folders row for edit_history in the schema table; mentioned row_id hashing + canonical PK in encrypted data; updated import-extension list to wav/aiff/aif/mp3/flac/ogg/m4a/alac/caf/bwf; noted atomic export writes via write_atomic. - description.md: replaced "Smart Folders" feature block with "Dynamic Collections" (filter_json); added Cmd+I / Cmd+, About shortcut; fixed supported-extension list. - troubleshooting.md: migration count; sync-table list including edit_history + user_config; row_id_salt cautionary note; fixed wrong mirror path (~/.audiofiles-mirror → ~/audiofiles-mirror). design-system.md, loose-files-mode.md, ml_classifier.md, plugin_authoring.md, trial-mode.md graded CURRENT; left alone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-03 02:43 UTC
Commit: 0bacd9483fa3c39b2577d782a4bcd2a440f27450
Parent: b925f54
5 files changed, +26 insertions, -27 deletions
@@ -95,7 +95,7 @@ Export converts VFS subtrees into standalone file hierarchies on disk. The pipel
95 95 1. **Collect items**: Walk the VFS subtree, resolving each sample link to its content-addressed blob path and enriching with tags.
96 96 2. **Configure**: User selects format (original, WAV, AIFF), sample rate, bit depth, channel configuration, structure (preserve tree or flatten), naming pattern (with tokens like `{name}`, `{bpm}`, `{key}`, `{class}`), metadata sidecar, and destination directory.
97 97 3. **Device profiles**: Optionally select a hardware sampler profile (from the Rhai plugin registry) which pre-fills format constraints and may run custom hook scripts during export.
98 - 4. **Execute**: Background worker copies or transcodes each file, applying format conversion (via hound + rubato for resampling), channel conversion (mono/stereo), and naming rules. Progress is reported per file.
98 + 4. **Execute**: Background worker copies or transcodes each file, applying format conversion (via hound + rubato for resampling), channel conversion (mono/stereo), and naming rules. Progress is reported per file. Each output is written atomically via a `write_atomic(dest, |tmp| ...)` helper — the encoder/copier targets `dest.audiofiles_tmp`, then `fs::rename`s into place on success. A killed export never leaves a partial file in the user's export directory.
99 99
100 100 ## Instrument Engine
101 101
@@ -153,6 +153,6 @@ Cancellation is checked between files, keeping the UI responsive during large im
153 153 - **Backend trait** keeps the browser UI decoupled from the data layer, making it testable with mock backends.
154 154 - **Synchronous core** keeps the data layer simple and predictable. Async is confined to the sync layer (tokio) and the app.
155 155 - **try_lock on audio thread** guarantees real-time safety. The cpal callback never blocks -- it either gets the lock and produces audio, or outputs silence.
156 - - **Strongly-typed IDs** (VfsId, NodeId, SmartFolderId, SampleHash) prevent accidental mixups at compile time. Integer IDs use a macro-generated newtype; SampleHash wraps a hex string.
156 + - **Strongly-typed IDs** (VfsId, NodeId, SampleHash) prevent accidental mixups at compile time. Integer IDs use a macro-generated newtype; SampleHash wraps a hex string.
157 157 - **Device plugin system** with Rhai scripting allows hardware sampler export profiles to be extended by users without recompiling, while keeping the sandbox constrained.
158 158 - **Trigger-based sync changelog** captures all local mutations automatically without requiring callers to manually record changes, making sync integration transparent to the rest of the codebase.
@@ -1,6 +1,6 @@
1 1 # audiofiles Database Schema
2 2
3 - SQLite schema reference. 12 inline migrations. Migrations are embedded as Rust string constants in `crates/audiofiles-core/src/db.rs` and applied via `PRAGMA user_version` tracking -- not separate SQL files.
3 + SQLite schema reference. 18 inline migrations. Migrations are embedded as Rust string constants in `crates/audiofiles-core/src/db.rs` and applied via `PRAGMA user_version` tracking -- not separate SQL files.
4 4
5 5 ## Domain Map
6 6
@@ -9,7 +9,7 @@ SQLite schema reference. 12 inline migrations. Migrations are embedded as Rust s
9 9 | Samples | 1 | Content-addressed sample storage and metadata |
10 10 | Analysis | 3 | Audio analysis, waveform data, fingerprints |
11 11 | VFS | 2 | Virtual file system directories and nodes |
12 - | Organization | 4 | Tags, collections, collection members, smart folders |
12 + | Organization | 3 | Tags, collections, collection members (smart folders merged into collections.filter_json in M015) |
13 13 | Preferences | 1 | User configuration key-value store |
14 14 | SyncKit | 2 | Cloud sync metadata and local changelog |
15 15 | History | 1 | Destructive edit tracking |
@@ -31,6 +31,7 @@ Content-addressed sample storage. The hash (of file content) is the primary key
31 31 | `last_modified` | INTEGER | Unix timestamp |
32 32 | `cloud_only` | INTEGER | 1 when local blob deleted but exists in cloud (migration 008), default 0 |
33 33 | `duration` | REAL | Seconds, available immediately after import (migration 009), nullable |
34 + | `source_path` | TEXT | Original on-disk path when imported in loose-files mode (migration 013), nullable |
34 35
35 36 Index: `original_name`.
36 37
@@ -155,16 +156,7 @@ Samples within a collection. Many-to-many.
155 156
156 157 PK: `(collection_id, sample_hash)`.
157 158
158 - ### smart_folders
159 - Saved searches within a VFS. The query is stored as JSON and evaluated at runtime.
160 -
161 - | Column | Type | Notes |
162 - |--------|------|-------|
163 - | `id` | INTEGER PK | |
164 - | `vfs_id` | INTEGER FK | -> vfs (CASCADE) |
165 - | `name` | TEXT | |
166 - | `query_json` | TEXT | Serialized search query |
167 - | `created_at` | INTEGER | Unix timestamp |
159 + `collections.filter_json` (added in M015): when non-NULL, the collection is a dynamic / saved search; when NULL, it's a manual collection populated via `collection_members`. The standalone `smart_folders` table from M001 was dropped in M015 and migrated into this column.
168 160
169 161 ---
170 162
@@ -190,7 +182,7 @@ Sync metadata key-value store. Migration 007.
190 182 | `key` | TEXT PK | |
191 183 | `value` | TEXT | |
192 184
193 - Seeded keys: `device_id`, `pull_cursor`, `auto_sync_enabled`, `sync_interval_minutes`, `applying_remote`, `last_sync_at`, `initial_snapshot_done`.
185 + Seeded keys: `device_id`, `pull_cursor`, `auto_sync_enabled`, `sync_interval_minutes`, `applying_remote`, `last_sync_at`, `initial_snapshot_done`, `row_id_salt` (added in M018; 32 random bytes hex, never synced).
194 186
195 187 ### sync_changelog
196 188 Local change log for push/pull sync. Migration 007.
@@ -200,7 +192,7 @@ Local change log for push/pull sync. Migration 007.
200 192 | `id` | INTEGER PK | AUTOINCREMENT |
201 193 | `table_name` | TEXT | Source table name |
202 194 | `op` | TEXT | `INSERT`, `UPDATE`, or `DELETE` |
203 - | `row_id` | TEXT | PK of changed row |
195 + | `row_id` | TEXT | Opaque per-row identifier. For sensitive tables (samples, audio_analysis, tags, collection_members) M018 sets this to `hash_row_id(row_id_salt, canonical_key)` so the cleartext key never goes on the wire. For numeric-PK tables it remains the literal id. |
204 196 | `timestamp` | TEXT | ISO datetime, default `datetime('now')` |
205 197 | `data` | TEXT | JSON snapshot of row, nullable |
206 198 | `pushed` | INTEGER | Boolean, default 0 |
@@ -237,8 +229,8 @@ Indexes: `source_hash`, `result_hash`.
237 229 - **Sync guard triggers:** All sync triggers check `applying_remote != '1'` to prevent echo loops
238 230 - **Sync-excluded keys:** `user_config` sync triggers skip keys matching `sync_%` to avoid syncing sync-internal state
239 231 - **Cloud-only samples:** `samples.cloud_only` flag allows local blob eviction while keeping metadata and cloud copy
240 - - **Composite row IDs for sync:** Compound PKs encoded as `a:b` strings in `sync_changelog.row_id`
241 - - **Synced tables:** `samples`, `audio_analysis`, `vfs`, `vfs_nodes`, `tags`, `collections`, `collection_members`, `smart_folders`, `user_config`, `edit_history`
232 + - **Hashed row IDs (M018):** sensitive `sync_changelog.row_id` values go through `hash_row_id(row_id_salt, canonical_key)` so the server never sees raw sample hashes or tag strings. DELETE triggers also emit the canonical PK into the encrypted `data` field so pull-side replay doesn't need to parse row_id.
233 + - **Synced tables:** `samples`, `audio_analysis`, `vfs`, `vfs_nodes`, `tags`, `collections`, `collection_members`, `user_config`, `edit_history`
242 234
243 235 ## Key Indexes
244 236
@@ -264,3 +256,9 @@ Indexes: `source_hash`, `result_hash`.
264 256 | 010 | Extended spectral features (spectral_bandwidth, centroid_variance, crest_factor, attack_time) |
265 257 | 011 | ML classification confidence score |
266 258 | 012 | Edit history table (destructive edit tracking) |
259 + | 013 | `samples.source_path` for loose-files mode (sample lives at its original on-disk path) |
260 + | 014 | Unique partial index on `vfs_nodes(vfs_id, name) WHERE parent_id IS NULL` (prevent duplicate root nodes) |
261 + | 015 | Collections gain `filter_json`; `smart_folders` table merged into collections and dropped |
262 + | 016 | Exclude `loose_files` user_config key from sync (security: server can't flip the mode) |
263 + | 017 | Rename `unsafe_mode` user_config key to `loose_files` (trigger recreation only; row-copy lives in main.rs) |
264 + | 018 | Hash `sync_changelog.row_id` for sensitive tables; DELETE triggers emit canonical PK in `data`; per-user `row_id_salt` in sync_state |
@@ -15,7 +15,7 @@ audiofiles is a standalone desktop sample manager. It stores samples by content
15 15 ## Features
16 16
17 17 ### Sample Import
18 - - Import folders of audio samples (WAV, FLAC, MP3, OGG, AIFF)
18 + - Import folders of audio samples (WAV, AIFF, FLAC, MP3, OGG, M4A, ALAC, CAF, BWF)
19 19 - Three import strategies: flat, new VFS, or merge into existing VFS
20 20 - Content-addressed storage (SHA-256) with automatic deduplication
21 21 - Progress display with cancel support; partial imports remain valid
@@ -66,8 +66,8 @@ Two-layer ML system: rule-based broad classifier (Layer 1) + 200-tree Random For
66 66 - Create, rename, delete collections
67 67 - Add/remove samples from any VFS
68 68
69 - ### Smart Folders
70 - - Saved filter queries stored as JSON in the database
69 + ### Dynamic Collections
70 + - A collection with a non-NULL `filter_json` is a saved search (the old "smart folder" feature, merged into the collections table in migration M015)
71 71 - Sidebar section with collapsible list
72 72 - Click to apply saved filter instantly
73 73
@@ -171,8 +171,9 @@ Two-layer ML system: rule-based broad classifier (Layer 1) + 200-tree Random For
171 171 - Cmd+A: select all
172 172 - Cmd+Z: undo
173 173 - Cmd+T: bulk tag
174 - - F1: help overlay
174 + - F1: help overlay (also reachable via Help toolbar menu → Keyboard shortcuts)
175 175 - F2: bulk rename
176 + - Cmd+I / Cmd+,: About modal (toggle update-check preference)
176 177 - Delete: delete (with confirmation)
177 178 - Escape: close dialog / clear search
178 179
@@ -59,7 +59,7 @@ sqlite3 ~/.config/audiofiles/audiofiles.db "SELECT COUNT(*) FROM samples"
59 59 | "Invalid node name" | Contains `/`, `\`, null bytes, or is `.`/`..` | Use standard filenames |
60 60 | "Move would create circular parent reference" | Moving node under its own descendant | Move to a different folder |
61 61
62 - **Broken mirror symlinks** (Unix only): If VFS mirror has dead symlinks, re-run mirror sync (idempotent) or delete `~/.audiofiles-mirror/` and restart.
62 + **Broken mirror symlinks** (Unix only): If VFS mirror has dead symlinks, re-run mirror sync (idempotent) or delete `~/audiofiles-mirror/` and restart.
63 63
64 64 **Orphaned samples** (files in store not referenced by any VFS):
65 65 ```sql
@@ -91,7 +91,7 @@ AND hash NOT IN (SELECT DISTINCT sample_hash FROM collection_members WHERE sampl
91 91
92 92 **Database location:** `~/.config/audiofiles/audiofiles.db` (platform-dependent)
93 93
94 - **12 inline migrations** tracked via `PRAGMA user_version`. Run automatically on app startup.
94 + **18 inline migrations** tracked via `PRAGMA user_version`. Run automatically on app startup.
95 95
96 96 | Symptom | Cause | Fix |
97 97 |---------|-------|-----|
@@ -110,7 +110,7 @@ UPDATE sync_state SET value='0' WHERE key='applying_remote';
110 110
111 111 ## Sync Issues
112 112
113 - **What syncs:** VFS, samples (metadata), collections, vfs_nodes, audio_analysis, tags, collection_members, smart_folders. Sync order respects FK relationships.
113 + **What syncs:** VFS, samples (metadata), collections (manual and dynamic, via `filter_json`), vfs_nodes, audio_analysis, tags, collection_members, edit_history, user_config (excluding sync-internal keys and `loose_files`). Sync order respects FK relationships. Per migration M018, `sync_changelog.row_id` for sensitive tables is hashed with a per-device `row_id_salt` (never synced); the cleartext canonical key lives in the encrypted `data` field, so a lost salt makes any unpushed changelog rows un-attributable on the next push.
114 114
115 115 **Blob sync:** Sample audio files sync to cloud storage for VFS entries with `sync_files = true`. The `cloud_only` flag marks samples whose local blobs have been evicted.
116 116
@@ -139,5 +139,5 @@ sqlite3 ~/.config/audiofiles/audiofiles.db "SELECT COUNT(*) FROM sync_changelog
139 139 ls -lh ~/.config/audiofiles/audiofiles.db-wal
140 140
141 141 # Broken mirror symlinks (Unix)
142 - find ~/.audiofiles-mirror -type l ! -exec test -e {} \; -print 2>/dev/null
142 + find ~/audiofiles-mirror -type l ! -exec test -e {} \; -print 2>/dev/null
143 143 ```
M todo.md +1 -1
@@ -35,7 +35,7 @@ Launch shipped 2026-06-01 (see `/Users/max/Code/launchplan_final.md`). Post-laun
35 35
36 36 ### Repo hygiene (launchplan §2.3)
37 37 - [ ] Remove `crates/audiofiles-app/tests/harness/mod.rs.bak` if it ever reappears (deleted this session as part of the fix commit).
38 - - [ ] Audit `docs/` for stale plans; either delete or mark complete.
38 + - [x] **Audit `docs/` for stale plans.** Subagent-graded 2026-06-02: `database_schema.md` rewritten for the 12→18 migration count + M013/M015/M018 schema (source_path, filter_json, smart_folders dropped, row_id hashing, row_id_salt). `architecture.md` updated for the same plus the m4a/alac/caf/bwf extension list and the atomic-write helper in §Export System. `description.md` swapped "Smart Folders" for "Dynamic Collections" + added Cmd+, About entry. `troubleshooting.md` fixed migration count, sync table list, row_id_salt note, and a wrong mirror path (`~/.audiofiles-mirror/` → `~/audiofiles-mirror/`). `design-system.md`, `loose-files-mode.md`, `ml_classifier.md`, `plugin_authoring.md`, `trial-mode.md` were graded CURRENT and left alone.
39 39 - [ ] `CONTRIBUTING.md` walkthrough against current build commands.
40 40
41 41 ## Audit deltas to revisit