max / audiofiles
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 | ``` |
| @@ -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 |