//! Completed-source manifests and resumable staging. Legacy generations stay //! searchable but cannot supply vectors without explicit completion evidence. use super::*; #[derive(Debug, Clone)] pub(crate) struct SourceTask { pub key: String, pub path: String, /// `text` for one extracted passage, `media` for the entire media file. pub source_type: String, pub source_hash: String, pub title_hash: String, pub policy: String, pub passage_id: i64, } pub(super) fn migrate(connection: &Connection) -> Result<()> { for (table, name, declaration) in [ ( "semantic_generations", "incremental", "INTEGER NOT NULL DEFAULT 0", ), ("semantic_chunks", "task_key", "TEXT"), ( "semantic_chunks", "file_change_revision", "INTEGER NOT NULL DEFAULT 0", ), ] { let exists: bool = connection.query_row( "SELECT EXISTS(SELECT 1 FROM pragma_table_info(?1) WHERE name=?2)", params![table, name], |r| r.get(0), )?; if !exists { connection.execute_batch(&format!( "ALTER TABLE {table} ADD COLUMN {name} {declaration}" ))?; } } connection.execute_batch("CREATE TABLE IF NOT EXISTS semantic_tasks( generation INTEGER NOT NULL REFERENCES semantic_generations(id) ON DELETE CASCADE, task_key TEXT NOT NULL,path TEXT NOT NULL,source_type TEXT NOT NULL, source_hash TEXT NOT NULL,title_hash TEXT NOT NULL,policy TEXT NOT NULL, passage_id INTEGER NOT NULL,state TEXT NOT NULL CHECK(state IN ('pending','running','done','failed')), attempts INTEGER NOT NULL DEFAULT 0,expected_chunks INTEGER,message TEXT, PRIMARY KEY(generation,task_key)); CREATE INDEX IF NOT EXISTS semantic_task_reuse ON semantic_tasks(source_hash,title_hash,policy,source_type,state); CREATE INDEX IF NOT EXISTS semantic_chunk_task ON semantic_chunks(generation,task_key); UPDATE semantic_meta SET schema_version=8 WHERE singleton=1;")?; Ok(()) } pub(super) fn validate_completion(connection: &Connection, generation: i64) -> Result<()> { let incomplete: bool = connection.query_row("SELECT EXISTS(SELECT 1 FROM semantic_tasks t WHERE generation=?1 AND (state NOT IN ('done','failed') OR (state='done' AND (expected_chunks IS NULL OR expected_chunks!=(SELECT count(*) FROM semantic_chunks c WHERE c.generation=t.generation AND c.task_key=t.task_key))) OR (state='failed' AND EXISTS(SELECT 1 FROM semantic_chunks c WHERE c.generation=t.generation AND c.task_key=t.task_key))))",[generation], |r|r.get(0))?; ensure!( !incomplete, "Semantic generation contains incomplete source tasks" ); let unowned: bool = connection.query_row("SELECT EXISTS(SELECT 1 FROM semantic_chunks c LEFT JOIN semantic_tasks t ON t.generation=c.generation AND t.task_key=c.task_key WHERE c.generation=?1 AND (t.state IS NULL OR t.state!='done'))",[generation],|r|r.get(0))?; ensure!( !unowned, "Semantic generation contains chunks without completed sources" ); Ok(()) } impl Store { pub(crate) fn revision(&self) -> Result { Ok(self.connection.query_row( "SELECT corpus_revision FROM semantic_meta WHERE singleton=1", [], |r| r.get(0), )?) } /// Caller must retain the OS lock through reconciliation and publication. pub(crate) fn start_or_resume( &mut self, fingerprint: &str, lock: &BuildLock, ) -> Result<(i64, bool)> { ensure!( lock.1 == self.lock_path, "Build lock belongs to a different index" ); ensure!( !fingerprint.is_empty() && fingerprint.len() <= 4096, "Invalid generation fingerprint" ); let tx = self .connection .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?; let generation: Option=tx.query_row("SELECT id FROM semantic_generations WHERE id!=COALESCE((SELECT active_generation FROM semantic_meta WHERE singleton=1),-1) AND fingerprint=?1 AND dimensions=?2 AND incremental=1 ORDER BY id DESC LIMIT 1",params![fingerprint,self.dimensions as i64],|r|r.get(0)).optional()?; let resumed = generation.is_some(); let id = if let Some(id) = generation { id } else { tx.execute("INSERT INTO semantic_generations(fingerprint,dimensions,corpus_revision,incremental) SELECT ?1,?2,corpus_revision,1 FROM semantic_meta WHERE singleton=1",params![fingerprint,self.dimensions as i64])?; tx.last_insert_rowid() }; tx.execute("DELETE FROM semantic_generations WHERE id!=?1 AND id!=COALESCE((SELECT active_generation FROM semantic_meta WHERE singleton=1),-1)",[id])?; // Untagged chunks are writes interrupted before the source transaction. tx.execute("UPDATE semantic_tasks SET state='pending',expected_chunks=NULL WHERE generation=?1 AND state='done' AND (expected_chunks IS NULL OR expected_chunks!=(SELECT count(*) FROM semantic_chunks c WHERE c.generation=semantic_tasks.generation AND c.task_key=semantic_tasks.task_key))",[id])?; tx.execute("DELETE FROM semantic_chunks WHERE generation=?1 AND NOT EXISTS(SELECT 1 FROM semantic_tasks t WHERE t.generation=semantic_chunks.generation AND t.task_key=semantic_chunks.task_key AND t.state='done')",[id])?; tx.execute("UPDATE semantic_tasks SET state='pending',expected_chunks=NULL,message=NULL WHERE generation=?1 AND state!='done'",[id])?; tx.commit()?; Ok((id, resumed)) } pub(crate) fn stage_task(&mut self, generation: i64, task: &SourceTask) -> Result { validate_task(task)?; let tx = self.connection.transaction()?; let same:bool=tx.query_row("SELECT EXISTS(SELECT 1 FROM semantic_tasks WHERE generation=?1 AND task_key=?2 AND source_hash=?3 AND title_hash=?4 AND policy=?5 AND source_type=?6 AND state='done')",params![generation,task.key,task.source_hash,task.title_hash,task.policy,task.source_type],|r|r.get(0))?; if !same { tx.execute( "DELETE FROM semantic_chunks WHERE generation=?1 AND task_key=?2", params![generation, task.key], )?; tx.execute("INSERT INTO semantic_tasks(generation,task_key,path,source_type,source_hash,title_hash,policy,passage_id,state,attempts) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,'running',1) ON CONFLICT(generation,task_key) DO UPDATE SET path=excluded.path,source_type=excluded.source_type,source_hash=excluded.source_hash,title_hash=excluded.title_hash,policy=excluded.policy,passage_id=excluded.passage_id,state='running',attempts=semantic_tasks.attempts+1,expected_chunks=NULL,message=NULL",params![generation,task.key,task.path,task.source_type,task.source_hash,task.title_hash,task.policy,task.passage_id])?; } tx.commit()?; Ok(same) } pub(crate) fn complete_source( &mut self, generation: i64, task: &SourceTask, expected_chunks: usize, ) -> Result<()> { ensure!( !task.source_hash.is_empty(), "Cannot complete a source without verified identity" ); ensure!( expected_chunks <= MAX_SCAN_CHUNKS, "Source exceeds semantic chunk budget" ); let tx = self .connection .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?; let count:i64=tx.query_row("SELECT count(*) FROM semantic_chunks WHERE generation=?1 AND path=?2 AND ((?3='text' AND source_type='text' AND passage_id=?4) OR (?3='media' AND source_type!='text'))",params![generation,task.path,task.source_type,task.passage_id],|r|r.get(0))?; ensure!( usize::try_from(count)? == expected_chunks, "Incomplete source chunk membership" ); let total: usize = tx.query_row( "SELECT count(*) FROM semantic_chunks WHERE generation=?1", [generation], |r| r.get(0), )?; ensure!( total <= MAX_SCAN_CHUNKS, "Generation exceeds semantic chunk budget" ); tx.execute("UPDATE semantic_chunks SET task_key=?5 WHERE generation=?1 AND path=?2 AND ((?3='text' AND source_type='text' AND passage_id=?4) OR (?3='media' AND source_type!='text'))",params![generation,task.path,task.source_type,task.passage_id,task.key])?; let changed=tx.execute("UPDATE semantic_tasks SET state='done',expected_chunks=?3,path=?4,passage_id=?5,message=NULL WHERE generation=?1 AND task_key=?2 AND source_hash=?6 AND title_hash=?7 AND policy=?8 AND source_type=?9",params![generation,task.key,count,task.path,task.passage_id,task.source_hash,task.title_hash,task.policy,task.source_type])?; ensure!(changed == 1, "Missing matching source task"); if task.source_type == "media" { tx.execute( "DELETE FROM semantic_media_errors WHERE generation=?1 AND path=?2", params![generation, task.path], )?; } tx.commit()?; Ok(()) } pub(crate) fn fail_source( &mut self, generation: i64, task: &SourceTask, message: &str, ) -> Result<()> { let tx = self.connection.transaction()?; tx.execute("DELETE FROM semantic_chunks WHERE generation=?1 AND (task_key=?2 OR (path=?3 AND ((?4='text' AND source_type='text' AND passage_id=?5) OR (?4='media' AND source_type!='text'))))",params![generation,task.key,task.path,task.source_type,task.passage_id])?; tx.execute("UPDATE semantic_tasks SET state='failed',expected_chunks=NULL,message=?3 WHERE generation=?1 AND task_key=?2",params![generation,task.key,message.chars().take(2048).collect::()])?; tx.commit()?; Ok(()) } /// Reconcile completed staging groups before any encoder call. A bounded /// SQL snapshot preserves renamed donors (including swaps and duplicates) /// while rebuilding only current compatible memberships. Active vectors /// are never touched. Full range/vector validation still happens on reuse. pub(crate) fn prune_before_inference( &mut self, generation: i64, tasks: &[SourceTask], revision: i64, ) -> Result<()> { ensure!( tasks.len() <= MAX_SCAN_CHUNKS, "Generation exceeds source task budget" ); for task in tasks { validate_task(task)?; } // The bounded snapshot can still contain 100K x 768d vectors. Keep it // SQLite-backed on disk rather than turning that into a Rust/heap copy. self.connection.pragma_update(None, "temp_store", "FILE")?; let tx = self .connection .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?; let current: i64 = tx.query_row( "SELECT corpus_revision FROM semantic_meta WHERE singleton=1", [], |r| r.get(0), )?; ensure!( current == revision, "Indexed corpus changed during semantic discovery; retry required" ); let active: bool = tx.query_row( "SELECT active_generation=?1 FROM semantic_meta WHERE singleton=1", [generation], |r| r.get::<_, Option>(0).map(|v| v.unwrap_or(false)), )?; ensure!(!active, "Cannot prune the active semantic generation"); let existing: usize = tx.query_row( "SELECT count(*) FROM semantic_chunks WHERE generation=?1", [generation], |r| r.get(0), )?; ensure!( existing <= MAX_SCAN_CHUNKS, "Staging generation exceeds semantic chunk budget" ); tx.execute_batch("CREATE TEMP TABLE IF NOT EXISTS semantic_discovery_tasks(task_key TEXT PRIMARY KEY,path TEXT NOT NULL,source_type TEXT NOT NULL,source_hash TEXT NOT NULL,title_hash TEXT NOT NULL,policy TEXT NOT NULL,passage_id INTEGER NOT NULL); DELETE FROM semantic_discovery_tasks; CREATE TEMP TABLE IF NOT EXISTS semantic_prune_tasks AS SELECT * FROM semantic_tasks WHERE 0; CREATE TEMP TABLE IF NOT EXISTS semantic_prune_chunks AS SELECT * FROM semantic_chunks WHERE 0; DELETE FROM semantic_prune_tasks; DELETE FROM semantic_prune_chunks;")?; for task in tasks { tx.execute( "INSERT INTO semantic_discovery_tasks VALUES(?1,?2,?3,?4,?5,?6,?7)", params![ task.key, task.path, task.source_type, task.source_hash, task.title_hash, task.policy, task.passage_id ], )?; } tx.execute("INSERT INTO semantic_prune_tasks SELECT * FROM semantic_tasks t WHERE generation=?1 AND state='done' AND source_hash!='' AND expected_chunks IS NOT NULL AND expected_chunks=(SELECT count(*) FROM semantic_chunks c WHERE c.generation=t.generation AND c.task_key=t.task_key)",[generation])?; tx.execute( "INSERT INTO semantic_prune_chunks SELECT * FROM semantic_chunks WHERE generation=?1", [generation], )?; tx.execute_batch("CREATE UNIQUE INDEX IF NOT EXISTS temp.semantic_prune_task_key ON semantic_prune_tasks(task_key); CREATE INDEX IF NOT EXISTS temp.semantic_prune_reuse ON semantic_prune_tasks(source_hash,title_hash,policy,source_type); CREATE INDEX IF NOT EXISTS temp.semantic_prune_membership ON semantic_prune_chunks(task_key);")?; let mut mappings = Vec::new(); { let mut statement = tx.prepare("SELECT d.task_key,COALESCE((SELECT old.task_key FROM semantic_prune_tasks old WHERE old.task_key=d.task_key AND old.source_hash=d.source_hash AND old.title_hash=d.title_hash AND old.policy=d.policy AND old.source_type=d.source_type),(SELECT old.task_key FROM semantic_prune_tasks old WHERE old.source_hash=d.source_hash AND old.title_hash=d.title_hash AND old.policy=d.policy AND old.source_type=d.source_type ORDER BY old.task_key LIMIT 1)) FROM semantic_discovery_tasks d")?; let rows = statement.query_map([], |r| { Ok((r.get::<_, String>(0)?, r.get::<_, Option>(1)?)) })?; for row in rows { let (key, donor) = row?; if let Some(donor) = donor { mappings.push((key, donor)); } } } let mut total = 0usize; for (_, donor) in &mappings { let count: usize = tx.query_row( "SELECT expected_chunks FROM semantic_prune_tasks WHERE task_key=?1", [donor], |r| r.get(0), )?; total = total .checked_add(count) .context("Semantic chunk count overflow")?; ensure!( total <= MAX_SCAN_CHUNKS, "Current completed sources exceed semantic chunk budget" ); } tx.execute( "DELETE FROM semantic_chunks WHERE generation=?1", [generation], )?; tx.execute( "DELETE FROM semantic_tasks WHERE generation=?1", [generation], )?; for (key, donor) in mappings { tx.execute("INSERT INTO semantic_tasks(generation,task_key,path,source_type,source_hash,title_hash,policy,passage_id,state,attempts,expected_chunks,message) SELECT ?1,d.task_key,d.path,d.source_type,d.source_hash,d.title_hash,d.policy,d.passage_id,'done',old.attempts,old.expected_chunks,NULL FROM semantic_discovery_tasks d JOIN semantic_prune_tasks old ON old.task_key=?3 WHERE d.task_key=?2",params![generation,key,donor])?; tx.execute("INSERT INTO semantic_chunks(generation,passage_id,ordinal,path,kind,number,content_hash,start_byte,end_byte,snippet,vector,source_type,file_size,file_mtime,task_key,file_change_revision) SELECT ?1,d.passage_id,c.ordinal,d.path,c.kind,c.number,c.content_hash,c.start_byte,c.end_byte,c.snippet,c.vector,c.source_type,c.file_size,c.file_mtime,d.task_key,COALESCE((SELECT change_revision FROM files WHERE path=d.path),c.file_change_revision) FROM semantic_prune_chunks c JOIN semantic_discovery_tasks d ON d.task_key=?2 WHERE c.task_key=?3",params![generation,key,donor])?; } tx.execute( "DELETE FROM semantic_media_errors WHERE generation=?1", [generation], )?; tx.execute_batch("DELETE FROM semantic_prune_tasks; DELETE FROM semantic_prune_chunks; DELETE FROM semantic_discovery_tasks;")?; tx.commit()?; Ok(()) } pub(crate) fn reconcile_tasks( &mut self, generation: i64, keys: &[String], revision: i64, ) -> Result<()> { ensure!( keys.len() <= MAX_SCAN_CHUNKS, "Generation exceeds source task budget" ); let tx = self .connection .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?; let current: i64 = tx.query_row( "SELECT corpus_revision FROM semantic_meta WHERE singleton=1", [], |r| r.get(0), )?; ensure!( current == revision, "Indexed corpus changed during semantic reconciliation; retry required" ); tx.execute_batch("CREATE TEMP TABLE IF NOT EXISTS semantic_current_tasks(task_key TEXT PRIMARY KEY); DELETE FROM semantic_current_tasks;")?; for key in keys { tx.execute( "INSERT OR IGNORE INTO semantic_current_tasks VALUES(?1)", [key], )?; } tx.execute("DELETE FROM semantic_chunks WHERE generation=?1 AND (task_key IS NULL OR task_key NOT IN (SELECT task_key FROM semantic_current_tasks))",[generation])?; tx.execute("DELETE FROM semantic_tasks WHERE generation=?1 AND task_key NOT IN (SELECT task_key FROM semantic_current_tasks)",[generation])?; tx.execute("DELETE FROM semantic_media_errors WHERE generation=?1 AND path NOT IN (SELECT path FROM semantic_tasks WHERE generation=?1)",[generation])?; tx.execute( "UPDATE semantic_generations SET corpus_revision=?2 WHERE id=?1", params![generation, revision], )?; tx.commit()?; Ok(()) } #[cfg(test)] pub(crate) fn activate_checked(&mut self, generation: i64, revision: i64) -> Result<()> { self.activate_checked_with_validation(generation, revision, || Ok(())) } /// Validate live sources after graph preparation, outside the SQLite write /// transaction. Rejection preserves both the active reader and staging. pub(crate) fn activate_checked_with_validation( &mut self, generation: i64, revision: i64, validate_sources: impl FnOnce() -> Result<()>, ) -> Result<()> { validate_completion(&self.connection, generation)?; self.activate_revision(generation, Some(revision), validate_sources) } } fn validate_task(task: &SourceTask) -> Result<()> { ensure!( !task.key.is_empty() && task.key.len() <= 256 && !task.path.is_empty() && task.path.len() <= 32768, "Invalid source task identity" ); ensure!( matches!(task.source_type.as_str(), "text" | "media"), "Invalid source task type" ); ensure!( (!task.source_hash.is_empty() || task.source_type == "media") && task.source_hash.len() <= 256 && task.title_hash.len() <= 256 && !task.policy.is_empty() && task.policy.len() <= 4096, "Invalid source completion policy" ); Ok(()) } impl Store { fn reusable_task( &self, generation: i64, task: &SourceTask, ) -> Result> { Ok(self.connection.query_row("SELECT t.generation,t.task_key,t.expected_chunks FROM semantic_tasks t JOIN semantic_generations g ON g.id=t.generation JOIN semantic_generations destination ON destination.id=?1 WHERE t.source_hash=?2 AND t.title_hash=?3 AND t.policy=?4 AND t.source_type=?5 AND t.state='done' AND t.expected_chunks IS NOT NULL AND g.fingerprint=destination.fingerprint AND g.dimensions=destination.dimensions AND t.expected_chunks=(SELECT count(*) FROM semantic_chunks c WHERE c.generation=t.generation AND c.task_key=t.task_key) ORDER BY (t.generation=?1) DESC,t.generation DESC LIMIT 1",params![generation,task.source_hash,task.title_hash,task.policy,task.source_type],|r|Ok((r.get(0)?,r.get(1)?,r.get(2)?))).optional()?) } /// No tokenizer or neural call: only a completed, policy-compatible group /// whose persisted ranges still equal the current extracted bytes qualifies. pub(crate) fn reuse_text_source( &mut self, generation: i64, task: &SourceTask, passage: &IndexedPassage, ) -> Result> { ensure!( task.source_type == "text" && task.passage_id == passage.id && task.path == passage.path, "Text task mapping differs from current source" ); ensure!( task.source_hash == content_hash(&passage.text), "Text task hash differs from source bytes" ); let Some((source, key, count)) = self.reusable_task(generation, task)? else { return Ok(None); }; ensure!( count <= MAX_SCAN_CHUNKS, "Reusable source exceeds chunk budget" ); { let mut stmt=self.connection.prepare("SELECT start_byte,end_byte,snippet,vector,source_type,ordinal,content_hash FROM semantic_chunks WHERE generation=?1 AND task_key=?2 ORDER BY ordinal")?; let mut rows = stmt.query(params![source, key])?; let mut seen = 0usize; while let Some(row) = rows.next()? { let start: usize = row.get(0)?; let end: usize = row.get(1)?; let snippet: String = row.get(2)?; ensure!( passage.text.get(start..end) == Some(snippet.as_str()), "Reusable text range differs from current source" ); ensure!( row.get::<_, String>(4)? == "text" && row.get::<_, usize>(5)? == seen && row.get::<_, String>(6)? == task.source_hash, "Corrupt completed text source membership" ); validate_vector(&row.get::<_, Vec>(3)?, self.dimensions)?; seen += 1; } ensure!(seen == count, "Reusable text completion count differs"); } self.copy_completed( generation, task, source, &key, count, Some(&passage.source), None, )?; Ok(Some(count)) } pub(crate) fn reuse_media_source( &mut self, generation: i64, task: &SourceTask, file_id: i64, identity: &searchi_media::MediaIdentity, ) -> Result> { ensure!( task.source_type == "media" && task.passage_id == -file_id && task.path == identity.path.to_string_lossy() && task.source_hash == identity.sha256, "Media task mapping differs from current source" ); let Some((source, key, count)) = self.reusable_task(generation, task)? else { return Ok(None); }; ensure!( count <= MAX_SCAN_CHUNKS, "Reusable source exceeds chunk budget" ); { let mut stmt=self.connection.prepare("SELECT vector,source_type,ordinal,content_hash FROM semantic_chunks WHERE generation=?1 AND task_key=?2 ORDER BY ordinal")?; let mut rows = stmt.query(params![source, key])?; let mut seen = 0usize; while let Some(row) = rows.next()? { ensure!( matches!( row.get::<_, String>(1)?.as_str(), "image" | "audio" | "video" ) && row.get::<_, usize>(2)? == seen && row.get::<_, String>(3)? == task.source_hash, "Corrupt completed media source membership" ); validate_vector(&row.get::<_, Vec>(0)?, self.dimensions)?; seen += 1; } ensure!(seen == count, "Reusable media completion count differs"); } self.copy_completed(generation, task, source, &key, count, None, Some(identity))?; Ok(Some(count)) } #[allow(clippy::too_many_arguments)] fn copy_completed( &mut self, generation: i64, task: &SourceTask, source: i64, key: &str, count: usize, location: Option<&SourceLocation>, identity: Option<&searchi_media::MediaIdentity>, ) -> Result<()> { let tx = self .connection .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?; let total:usize=tx.query_row("SELECT count(*) FROM semantic_chunks WHERE generation=?1 AND (task_key IS NULL OR task_key!=?2)",params![generation,task.key],|r|r.get(0))?; ensure!( total.saturating_add(count) <= MAX_SCAN_CHUNKS, "Generation exceeds semantic chunk budget" ); // A temporary SQL table allows remapping a completed staging task onto // itself without retaining its old citations or allocating all vectors. tx.execute_batch("CREATE TEMP TABLE IF NOT EXISTS semantic_reuse_chunks AS SELECT * FROM semantic_chunks WHERE 0; DELETE FROM semantic_reuse_chunks;")?; tx.execute("INSERT INTO semantic_reuse_chunks SELECT * FROM semantic_chunks WHERE generation=?1 AND task_key=?2",params![source,key])?; tx.execute( "DELETE FROM semantic_chunks WHERE generation=?1 AND task_key=?2", params![generation, task.key], )?; tx.execute("INSERT INTO semantic_chunks(generation,passage_id,ordinal,path,kind,number,content_hash,start_byte,end_byte,snippet,vector,source_type,file_size,file_mtime,task_key,file_change_revision) SELECT ?1,?2,ordinal,?3,COALESCE(?4,kind),COALESCE(?5,number),content_hash,start_byte,end_byte,snippet,vector,source_type,COALESCE(?6,file_size),COALESCE(?7,file_mtime),?8,COALESCE((SELECT change_revision FROM files WHERE path=?3),file_change_revision) FROM semantic_reuse_chunks",params![generation,task.passage_id,task.path,location.map(|s|s.kind.as_str()),location.map(|s|s.number),identity.map(|i|i.bytes as i64),identity.map(|i|i.modified_ns),task.key])?; let changed=tx.execute("UPDATE semantic_tasks SET state='done',expected_chunks=?3,path=?4,passage_id=?5,message=NULL WHERE generation=?1 AND task_key=?2 AND source_hash=?6 AND title_hash=?7 AND policy=?8 AND source_type=?9",params![generation,task.key,count as i64,task.path,task.passage_id,task.source_hash,task.title_hash,task.policy,task.source_type])?; ensure!(changed == 1, "Missing source task during reuse"); if task.source_type == "media" { tx.execute( "DELETE FROM semantic_media_errors WHERE generation=?1 AND path=?2", params![generation, task.path], )?; } tx.execute_batch("DELETE FROM semantic_reuse_chunks;")?; tx.commit()?; Ok(()) } } fn validate_vector(bytes: &[u8], dimensions: usize) -> Result<()> { ensure!( bytes.len() == dimensions * 4, "Corrupt reusable vector length" ); let mut norm = 0.0f32; for value in bytes.as_chunks::<4>().0 { let value = f32::from_le_bytes(*value); ensure!(value.is_finite(), "Corrupt reusable vector value"); norm += value * value; } ensure!( norm.is_finite() && (norm.sqrt() - 1.0).abs() < 1e-3, "Reusable vector is not normalized" ); Ok(()) } #[cfg(test)] mod tests { use super::*; use searchi_core::SearchIndex; // Storage-only vectors exercise durability and membership. They are not // simulated production inference or evidence of semantic relevance. fn vector() -> Vec { vec![1.0, 0.0] } fn task(p: &IndexedPassage) -> SourceTask { SourceTask { key: format!("text-{}-{}", p.path, p.source.number), path: p.path.clone(), source_type: "text".into(), source_hash: content_hash(&p.text), title_hash: content_hash(&p.filename), policy: "storage-test-window-policy".into(), passage_id: p.id, } } fn insert_complete( store: &mut Store, g: i64, p: &IndexedPassage, t: &SourceTask, ) -> Result<()> { store.stage_task(g, t)?; store.insert( g, p, 0, &[TextWindow { text: p.text.clone(), start_byte: 0, end_byte: p.text.len(), }], &[vector()], )?; store.complete_source(g, t, 1) } #[test] fn pre_inference_pruning_preserves_renamed_donors_at_the_chunk_ceiling() -> Result<()> { // Actual 100K membership boundary; vectors are storage-only fixtures. let dir = tempfile::tempdir()?; let root = dir.path().join("docs"); std::fs::create_dir(&root)?; std::fs::write(root.join("notes.txt"), "complete source")?; let db = dir.path().join("index.db"); let mut lexical = SearchIndex::open(&db)?; lexical.index_folder(&root)?; let p = lexical.passage_batch(0, 1)?.remove(0); let mut store = Store::open(&db, 2)?; let lock = store.build_lock()?; let active = store.start_or_resume("prune-policy", &lock)?.0; insert_complete(&mut store, active, &p, &task(&p))?; store.activate_checked(active, store.revision()?)?; let staging = store.start_or_resume("prune-policy", &lock)?.0; let original = task(&p); insert_complete(&mut store, staging, &p, &original)?; store.connection.execute("WITH RECURSIVE ord(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM ord WHERE n+1 Result<()> { let dir = tempfile::tempdir()?; let root = dir.path().join("docs"); std::fs::create_dir(&root)?; std::fs::write(root.join("first.txt"), "complete source")?; std::fs::write(root.join("second.txt"), "interrupted source")?; let db = dir.path().join("index.db"); let mut lexical = SearchIndex::open(&db)?; lexical.index_folder(&root)?; let passages = lexical.passage_batch(0, 64)?; let first = task(&passages[0]); let second = task(&passages[1]); let generation; { let mut store = Store::open(&db, 2)?; let lock = store.build_lock()?; generation = store.start_or_resume("storage-only-model", &lock)?.0; insert_complete(&mut store, generation, &passages[0], &first)?; store.stage_task(generation, &second)?; store.insert( generation, &passages[1], 0, &[TextWindow { text: passages[1].text.clone(), start_byte: 0, end_byte: passages[1].text.len(), }], &[vector()], )?; assert!(store .activate_checked(generation, store.revision()?) .is_err()); } let mut store = Store::open(&db, 2)?; let lock = store.build_lock()?; assert_eq!( store.start_or_resume("storage-only-model", &lock)?, (generation, true) ); assert_eq!( store.connection.query_row( "SELECT count(*) FROM semantic_chunks WHERE generation=?1", [generation], |r| r.get::<_, usize>(0) )?, 1 ); store.stage_task(generation, &first)?; assert_eq!( store.reuse_text_source(generation, &first, &passages[0])?, Some(1) ); assert!(store .reuse_text_source(generation, &second, &passages[1])? .is_none()); assert!(store .activate_checked(generation, store.revision()?) .is_err()); store.reconcile_tasks(generation, &[first.key], store.revision()?)?; store.activate_checked(generation, store.revision()?)?; Ok(()) } #[test] fn legacy_vectors_remain_searchable_but_do_not_qualify_for_reuse() -> Result<()> { let dir = tempfile::tempdir()?; let root = dir.path().join("docs"); std::fs::create_dir(&root)?; std::fs::write(root.join("notes.txt"), "legacy searchable source")?; let db = dir.path().join("index.db"); let mut lexical = SearchIndex::open(&db)?; lexical.index_folder(&root)?; let p = lexical.passage_batch(0, 1)?.remove(0); let mut store = Store::open(&db, 2)?; let legacy = store.begin("legacy-policy")?; store.insert( legacy, &p, 0, &[TextWindow { text: p.text.clone(), start_byte: 0, end_byte: p.text.len(), }], &[vector()], )?; store.activate(legacy)?; // Reopen a schema6 DB with legacy active chunks and no manifests. store.connection.execute_batch( "DROP INDEX semantic_chunk_task; ALTER TABLE semantic_chunks DROP COLUMN task_key; DROP TABLE semantic_tasks; ALTER TABLE semantic_generations DROP COLUMN incremental; UPDATE semantic_meta SET schema_version=6;", )?; drop(store); let mut store = Store::open(&db, 2)?; assert_eq!( store .search(&vector(), "legacy-policy", 1, &SearchOptions::default())? .len(), 1 ); let lock = store.build_lock()?; let (g, _) = store.start_or_resume("legacy-policy", &lock)?; let t = task(&p); store.stage_task(g, &t)?; assert_eq!(store.reuse_text_source(g, &t, &p)?, None); assert_eq!( store.active_fingerprint()?.as_deref(), Some("legacy-policy") ); Ok(()) } #[test] fn late_source_validation_rejects_publication_without_holding_write_lock() -> Result<()> { let dir = tempfile::tempdir()?; let root = dir.path().join("docs"); std::fs::create_dir(&root)?; std::fs::write(root.join("notes.txt"), "source identity before graph build")?; let db = dir.path().join("index.db"); let mut lexical = SearchIndex::open(&db)?; lexical.index_folder(&root)?; let passage = lexical.passage_batch(0, 1)?.remove(0); let source = task(&passage); let mut store = Store::open(&db, 2)?; let lock = store.build_lock()?; let (active, _) = store.start_or_resume("old-policy", &lock)?; insert_complete(&mut store, active, &passage, &source)?; store.activate_checked(active, store.revision()?)?; let (staging, _) = store.start_or_resume("new-policy", &lock)?; insert_complete(&mut store, staging, &passage, &source)?; let revision = store.revision()?; let mut called = false; let error = store .activate_checked_with_validation(staging, revision, || { called = true; // An independent write succeeds only when validation runs outside // the publishing connection's Immediate transaction. let other = rusqlite::Connection::open(&db)?; other.busy_timeout(std::time::Duration::ZERO)?; other.execute_batch("BEGIN IMMEDIATE; COMMIT;")?; std::fs::write(root.join("notes.txt"), "direct filesystem mutation")?; anyhow::bail!("source changed after graph preparation") }) .unwrap_err(); assert!(called); assert!(error .to_string() .contains("source changed after graph preparation")); assert_eq!(store.active_fingerprint()?.as_deref(), Some("old-policy")); assert_eq!( store.ann_cache.as_ref().map(|(generation, _)| *generation), Some(active) ); assert_eq!(store.start_or_resume("new-policy", &lock)?, (staging, true)); assert_eq!( store.connection.query_row( "SELECT count(*) FROM semantic_chunks WHERE generation=?1", [staging], |row| row.get::<_, usize>(0), )?, 1 ); Ok(()) } #[test] fn revision_race_cannot_publish_and_failed_activation_keeps_staging() -> Result<()> { let dir = tempfile::tempdir()?; let root = dir.path().join("docs"); std::fs::create_dir(&root)?; std::fs::write(root.join("notes.txt"), "source before change")?; let db = dir.path().join("index.db"); let mut lexical = SearchIndex::open(&db)?; lexical.index_folder(&root)?; let p = lexical.passage_batch(0, 1)?.remove(0); let mut store = Store::open(&db, 2)?; let lock = store.build_lock()?; let (g, _) = store.start_or_resume("checked-policy", &lock)?; let t = task(&p); insert_complete(&mut store, g, &p, &t)?; let old_revision = store.revision()?; std::fs::write(root.join("notes.txt"), "source after change longer")?; lexical.index_folder(&root)?; assert!(store.activate_checked(g, old_revision).is_err()); assert_eq!(store.active_fingerprint()?, None); assert_eq!(store.start_or_resume("checked-policy", &lock)?, (g, true)); assert_eq!( store.connection.query_row( "SELECT count(*) FROM semantic_chunks WHERE generation=?1", [g], |r| r.get::<_, usize>(0) )?, 1 ); Ok(()) } } #[cfg(test)] mod completion_failure_tests { use super::*; use searchi_core::SearchIndex; #[test] fn failed_completion_rolls_back_chunk_membership_and_malformed_ranges_never_reuse() -> Result<()> { // SQLite durability fixture, not neural inference evidence. let dir = tempfile::tempdir()?; let root = dir.path().join("docs"); std::fs::create_dir(&root)?; std::fs::write(root.join("notes.txt"), "UTF-8 source with 中文 preserved")?; let db = dir.path().join("index.db"); let mut lexical = SearchIndex::open(&db)?; lexical.index_folder(&root)?; let p = lexical.passage_batch(0, 1)?.remove(0); let t = SourceTask { key: "durability-task".into(), path: p.path.clone(), source_type: "text".into(), source_hash: content_hash(&p.text), title_hash: content_hash(&p.filename), policy: "exact-window-test".into(), passage_id: p.id, }; let mut store = Store::open(&db, 2)?; let lock = store.build_lock()?; let (g, _) = store.start_or_resume("storage-geometry", &lock)?; store.stage_task(g, &t)?; store.insert( g, &p, 0, &[TextWindow { text: p.text.clone(), start_byte: 0, end_byte: p.text.len(), }], &[vec![1.0, 0.0]], )?; assert!(store.complete_source(g, &t, 2).is_err()); let (state,owned):(String,usize)=store.connection.query_row("SELECT state,(SELECT count(*) FROM semantic_chunks WHERE generation=?1 AND task_key IS NOT NULL) FROM semantic_tasks WHERE generation=?1 AND task_key=?2",params![g,t.key],|r|Ok((r.get(0)?,r.get(1)?)))?; assert_eq!(state, "running"); assert_eq!(owned, 0); assert!(store.activate_checked(g, store.revision()?).is_err()); store.complete_source(g, &t, 1)?; store.connection.execute("UPDATE semantic_chunks SET start_byte=1,snippet='different snippet' WHERE generation=?1",[g])?; assert!(store.reuse_text_source(g, &t, &p).is_err()); assert!(store.active_fingerprint()?.is_none()); Ok(()) } }