Skip to main content

max / audiofiles

Content A+: seal the tombstone filter (enforce live_samples, not convention) Ultra-fuzz --deep, CHRONIC #2 from convention to enforcement. The run-6 standard fix added the live_samples view but left "use the view" as a convention a new raw `FROM samples` listing read could still bypass (the exact recurrence shape). This makes the bypass a build failure. - tests/tombstone_seal.rs: scans production source (test modules excluded) and fails on any raw `FROM samples` read not routed through live_samples and not justified by a `tombstone-ok: <reason>` marker within 3 lines. Writes (DELETE FROM) and the CREATE VIEW definition are exempt by construction. Proven to fail on an injected unmarked read. - Redirected the remaining inline-filtered listing reads to live_samples so the filter is structural, not a removable WHERE clause: analysis backfill/exemplar/ trained-head selections, the orphan-GC and loose-files scans, the rules load_context point-lookup, storage_stats, and the per-VFS storage stats (which counted tombstoned samples' file_size, inflating the "would upload" estimate -- the one genuine residual leak the audit's CHRONIC #2 implied but the standard pass hadn't reached). - Tagged the legitimately-raw reads with `tombstone-ok`: the blob-naming point-lookup (intentionally includes tombstones), sample_extension_any, the expiry sweep, and the Trash listing. 586 core tests pass + the seal; lib clippy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-23 16:49 UTC
Commit: 67b31add71a13588751f0d57d33f4b5785ac4e3e
Parent: 387fcc6
7 files changed, +115 insertions, -20 deletions
@@ -429,9 +429,8 @@ pub fn auto_apply_library(db: &Database, k: usize) -> Result<usize> {
429 429 }
430 430 let hashes: Vec<String> = {
431 431 let mut stmt = db.conn().prepare(
432 - "SELECT s.hash FROM samples s
433 - JOIN sample_features f ON f.hash = s.hash AND f.feat_version = ?1
434 - WHERE s.deleted_at IS NULL",
432 + "SELECT s.hash FROM live_samples s
433 + JOIN sample_features f ON f.hash = s.hash AND f.feat_version = ?1",
435 434 )?;
436 435 stmt.query_map([FEATURE_VERSION], |row| row.get(0))?
437 436 .collect::<std::result::Result<Vec<_>, _>>()?
@@ -429,9 +429,8 @@ pub fn load_analysis(db: &Database, hash: &str) -> Option<AnalysisResult> {
429 429 #[instrument(skip_all)]
430 430 pub fn samples_needing_features(db: &Database) -> Result<Vec<(String, String)>, CoreError> {
431 431 let mut stmt = db.conn().prepare(
432 - "SELECT s.hash, s.file_extension FROM samples s
433 - WHERE s.deleted_at IS NULL
434 - AND NOT EXISTS (
432 + "SELECT s.hash, s.file_extension FROM live_samples s
433 + WHERE NOT EXISTS (
435 434 SELECT 1 FROM sample_features f
436 435 WHERE f.hash = s.hash AND f.feat_version = ?1)",
437 436 )?;
@@ -291,9 +291,8 @@ pub fn apply_ml_suggestions(db: &Database, hash: &str, head: &TrainedHead) -> Re
291 291 pub fn auto_apply_library(db: &Database, head: &TrainedHead) -> Result<usize> {
292 292 let hashes: Vec<String> = {
293 293 let mut stmt = db.conn().prepare(
294 - "SELECT s.hash FROM samples s
295 - JOIN sample_features f ON f.hash = s.hash AND f.feat_version = ?1
296 - WHERE s.deleted_at IS NULL",
294 + "SELECT s.hash FROM live_samples s
295 + JOIN sample_features f ON f.hash = s.hash AND f.feat_version = ?1",
297 296 )?;
298 297 stmt.query_map([FEATURE_VERSION], |row| row.get(0))?
299 298 .collect::<std::result::Result<Vec<_>, _>>()?
@@ -1780,7 +1780,7 @@ impl Database {
1780 1780 /// like every other sample read site.
1781 1781 pub fn storage_stats(&self) -> Result<(u64, u64), DbError> {
1782 1782 let (count, total): (u64, u64) = self.conn.query_row(
1783 - "SELECT COUNT(*), COALESCE(SUM(file_size), 0) FROM samples WHERE deleted_at IS NULL",
1783 + "SELECT COUNT(*), COALESCE(SUM(file_size), 0) FROM live_samples",
1784 1784 [],
1785 1785 |row| Ok((row.get(0)?, row.get(1)?)),
1786 1786 )?;
@@ -1792,8 +1792,11 @@ impl Database {
1792 1792 /// same VFS counts once. Used by the sync panel's per-VFS toggle rows so
1793 1793 /// the user can see how much would upload before enabling blob sync.
1794 1794 pub fn vfs_storage_stats(&self, vfs_id: i64) -> Result<(u64, u64), DbError> {
1795 + // Soft-delete keeps vfs placements, so a tombstoned sample would still be
1796 + // counted/summed here and inflate the "would upload" estimate; read through
1797 + // live_samples to exclude it.
1795 1798 let (count, total): (u64, u64) = self.conn.query_row(
1796 - "SELECT COUNT(*), COALESCE(SUM(file_size), 0) FROM samples \
1799 + "SELECT COUNT(*), COALESCE(SUM(file_size), 0) FROM live_samples \
1797 1800 WHERE hash IN (\
1798 1801 SELECT DISTINCT sample_hash FROM vfs_nodes \
1799 1802 WHERE vfs_id = ? AND sample_hash IS NOT NULL\
@@ -372,7 +372,7 @@ pub fn load_context(db: &Database, hash: &str) -> Result<Option<RuleContext>> {
372 372 let base = conn
373 373 .query_row(
374 374 "SELECT original_name, file_extension, file_size, source_path
375 - FROM samples WHERE hash = ?1 AND deleted_at IS NULL",
375 + FROM live_samples WHERE hash = ?1",
376 376 [hash],
377 377 |row| {
378 378 Ok((
@@ -148,6 +148,7 @@ impl SampleStore {
148 148 // unreachable and leak disk forever. Query without the deleted_at filter so
149 149 // the blob naming stays consistent even for soft-deleted rows. Fresh import
150 150 // falls back to the incoming file's extension.
151 + // tombstone-ok: point-lookup that intentionally includes tombstoned rows
151 152 let ext = match db.conn().query_row(
152 153 "SELECT file_extension FROM samples WHERE hash = ?1",
153 154 [hash],
@@ -300,9 +301,9 @@ impl SampleStore {
300 301 // sweep and pre-emptively destroy still-recoverable data.
301 302 let mut stmt = db.conn().prepare(
302 303 "SELECT s.hash, s.file_extension
303 - FROM samples s
304 + FROM live_samples s
304 305 LEFT JOIN vfs_nodes vn ON s.hash = vn.sample_hash
305 - WHERE vn.id IS NULL AND s.deleted_at IS NULL",
306 + WHERE vn.id IS NULL",
306 307 )?;
307 308 let orphans: Vec<(String, String)> = stmt
308 309 .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
@@ -376,6 +377,7 @@ impl SampleStore {
376 377 pub fn sweep_expired_tombstones(&self, db: &Database) -> Result<usize> {
377 378 let cutoff = unix_now() - tombstone_retain_days(db) * 86_400;
378 379
380 + // tombstone-ok: the sweep operates on tombstones by definition
379 381 let mut stmt = db.conn().prepare(
380 382 "SELECT hash FROM samples WHERE deleted_at IS NOT NULL AND deleted_at < ?1",
381 383 )?;
@@ -438,7 +440,7 @@ fn query_sample_field(db: &Database, hash: &str, field: &str) -> Result<String>
438 440 )));
439 441 }
440 442
441 - let sql = format!("SELECT {field} FROM samples WHERE hash = ?1 AND deleted_at IS NULL");
443 + let sql = format!("SELECT {field} FROM live_samples WHERE hash = ?1");
442 444 db.conn()
443 445 .query_row(&sql, [hash], |row| row.get(0))
444 446 .map_err(|e| match e {
@@ -458,6 +460,7 @@ pub fn sample_extension(db: &Database, hash: &str) -> Result<String> {
458 460 /// rows. Used by the hard-delete and sweep paths, which must resolve the blob
459 461 /// path for soft-deleted rows that `sample_extension` deliberately hides.
460 462 fn sample_extension_any(db: &Database, hash: &str) -> Result<String> {
463 + // tombstone-ok: point-lookup that must resolve blobs for tombstoned rows too
461 464 db.conn()
462 465 .query_row(
463 466 "SELECT file_extension FROM samples WHERE hash = ?1",
@@ -496,6 +499,7 @@ pub struct TombstonedSample {
496 499
497 500 /// List every tombstoned sample, most recently deleted first.
498 501 pub fn tombstoned_samples(db: &Database) -> Result<Vec<TombstonedSample>> {
502 + // tombstone-ok: this IS the tombstone listing (Trash view)
499 503 let mut stmt = db.conn().prepare(
500 504 "SELECT hash, original_name, file_extension, file_size, deleted_at
501 505 FROM samples
@@ -581,7 +585,7 @@ fn clamp_original_name(name: String) -> String {
581 585 pub fn sample_source_path(db: &Database, hash: &str) -> Result<Option<String>> {
582 586 db.conn()
583 587 .query_row(
584 - "SELECT source_path FROM samples WHERE hash = ?1 AND deleted_at IS NULL",
588 + "SELECT source_path FROM live_samples WHERE hash = ?1",
585 589 [hash],
586 590 |row| row.get(0),
587 591 )
@@ -726,7 +730,7 @@ pub fn relocate_sample(
726 730 /// exists vs. does not exist on disk.
727 731 pub fn check_loose_files_integrity(db: &Database) -> Result<(usize, usize)> {
728 732 let mut stmt = db.conn().prepare(
729 - "SELECT source_path FROM samples WHERE source_path IS NOT NULL AND deleted_at IS NULL",
733 + "SELECT source_path FROM live_samples WHERE source_path IS NOT NULL",
730 734 )?;
731 735 let paths: Vec<String> = stmt
732 736 .query_map([], |row| row.get(0))?
@@ -762,8 +766,8 @@ pub fn relocate_missing_loose_files(
762 766 // 1. Gather missing samples — hash, basename of stored source_path, and
763 767 // the recorded file_size (used for cheap pre-filter before re-hashing).
764 768 let mut stmt = db.conn().prepare(
765 - "SELECT hash, source_path, file_size FROM samples \
766 - WHERE source_path IS NOT NULL AND deleted_at IS NULL",
769 + "SELECT hash, source_path, file_size FROM live_samples \
770 + WHERE source_path IS NOT NULL",
767 771 )?;
768 772 let rows: Vec<(String, String, i64)> = stmt
769 773 .query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))?
@@ -867,7 +871,7 @@ pub fn relocate_missing_loose_files(
867 871 /// Returns the number of samples purged. CASCADE handles VFS nodes, tags, etc.
868 872 pub fn purge_missing_loose_files(db: &Database) -> Result<usize> {
869 873 let mut stmt = db.conn().prepare(
870 - "SELECT hash, source_path FROM samples WHERE source_path IS NOT NULL AND deleted_at IS NULL",
874 + "SELECT hash, source_path FROM live_samples WHERE source_path IS NOT NULL",
871 875 )?;
872 876 let rows: Vec<(String, String)> = stmt
873 877 .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
@@ -0,0 +1,91 @@
1 + //! CHRONIC #2 enforcement seal (ultra-fuzz run-6 --deep).
2 + //!
3 + //! The tombstone filter (`deleted_at IS NULL`) was opt-in per query and drifted
4 + //! across runs: a new read of the raw `samples` table that forgot the filter
5 + //! leaked deleted samples into tag/collection/mirror views. The `live_samples`
6 + //! view centralizes the filter, but a view alone does not *prevent* a new raw
7 + //! `FROM samples` listing from drifting back in.
8 + //!
9 + //! This test makes that drift a build failure: every production read of the raw
10 + //! `samples` table must either go through `live_samples` (auto-filtered) or carry
11 + //! an explicit `tombstone-ok: <reason>` justification within 3 lines. A new
12 + //! unfiltered `FROM samples` listing read fails here until the author routes it
13 + //! through the view or marks it — a visible, reviewed decision, not a silent
14 + //! regression.
15 + //!
16 + //! Exemptions, by construction:
17 + //! - `DELETE FROM samples` (a write, not a read).
18 + //! - The `CREATE VIEW live_samples` definition itself.
19 + //! - `#[cfg(test)]` modules (they legitimately assert raw table state). This
20 + //! assumes the repo convention of test modules at file end.
21 +
22 + use std::fs;
23 + use std::path::{Path, PathBuf};
24 +
25 + /// True if `line` reads from the raw `samples` table (word-bounded), i.e. not
26 + /// `live_samples` and not `samples_*` (e.g. `samples_fts`, `sample_features`).
27 + fn reads_raw_samples(line: &str) -> bool {
28 + const NEEDLE: &str = "FROM samples";
29 + let mut idx = 0;
30 + while let Some(pos) = line[idx..].find(NEEDLE) {
31 + let after = idx + pos + NEEDLE.len();
32 + let boundary = line.as_bytes().get(after);
33 + let is_word = boundary.is_some_and(|&c| c.is_ascii_alphanumeric() || c == b'_');
34 + if !is_word {
35 + return true; // exact table `samples`, not `samples_...`
36 + }
37 + idx = after;
38 + }
39 + false
40 + }
41 +
42 + #[test]
43 + fn tombstone_filter_seal() {
44 + let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
45 + let mut stack = vec![src];
46 + let mut violations: Vec<String> = Vec::new();
47 +
48 + while let Some(dir) = stack.pop() {
49 + for entry in fs::read_dir(&dir).unwrap() {
50 + let path: PathBuf = entry.unwrap().path();
51 + if path.is_dir() {
52 + stack.push(path);
53 + continue;
54 + }
55 + if path.extension().and_then(|e| e.to_str()) != Some("rs") {
56 + continue;
57 + }
58 + let text = fs::read_to_string(&path).unwrap();
59 + let lines: Vec<&str> = text.lines().collect();
60 + // Test modules (conventionally at file end) legitimately read raw rows.
61 + let end = lines
62 + .iter()
63 + .position(|l| l.contains("#[cfg(test)]"))
64 + .unwrap_or(lines.len());
65 +
66 + for (i, line) in lines[..end].iter().enumerate() {
67 + if !reads_raw_samples(line) || line.contains("DELETE FROM samples") {
68 + continue;
69 + }
70 + let lo = i.saturating_sub(3);
71 + let window = lines[lo..=i].join("\n");
72 + if window.contains("tombstone-ok") || window.contains("CREATE VIEW") {
73 + continue;
74 + }
75 + let rel = path
76 + .strip_prefix(env!("CARGO_MANIFEST_DIR"))
77 + .unwrap_or(&path);
78 + violations.push(format!("{}:{} {}", rel.display(), i + 1, line.trim()));
79 + }
80 + }
81 + }
82 +
83 + assert!(
84 + violations.is_empty(),
85 + "CHRONIC #2 seal: raw `FROM samples` read(s) without `live_samples` or a \
86 + `tombstone-ok` justification:\n {}\n\nRoute visible-sample reads through \
87 + `FROM live_samples`, or add a `tombstone-ok: <reason>` comment within 3 \
88 + lines if the raw access is intentional (point-lookup, tombstone sweep).",
89 + violations.join("\n ")
90 + );
91 + }