Watch selected folders and reconcile bounded changes with media freshness guards

This commit is contained in:
karl committed 2026-10-13 02:30:38 +02:00
1 parent 99748bd99f
commit 657de3de86
22 files changed
+2022 -149

No files matched your search

+1 -1
View File
@@ -272,7 +272,7 @@ impl HybridSearch {
});
}
ensure!(
(6..=7).contains(&version),
(6..=8).contains(&version),
"Unsupported semantic schema version {version}"
);
let (generation,fingerprint,revision,current):(Option<i64>,Option<String>,Option<i64>,i64)=connection.query_row(
+12 -3
View File
@@ -141,6 +141,11 @@ impl HybridSearch {
task: &SourceTask,
work: &mut BuildWork,
) -> Result<()> {
let change_revision = self.lexical.media_change_revision(
&path.to_string_lossy(),
identity.bytes,
identity.modified_ns,
)?;
if let Some(count) = self
.store
.reuse_media_source(generation, task, id, identity)?
@@ -152,7 +157,7 @@ impl HybridSearch {
if kind != MediaKind::Image {
// Metadata remains part of music search after path remapping.
let metadata = probe(tools, path, limits)?;
self.refresh_media_metadata(path, identity, &metadata)?;
self.refresh_media_metadata(path, identity, &metadata, change_revision)?;
}
identity.ensure_unchanged()?;
work.report.chunks += count;
@@ -169,6 +174,7 @@ impl HybridSearch {
tools,
limits,
identity,
change_revision,
&mut work.report,
)?;
self.store
@@ -182,6 +188,7 @@ impl HybridSearch {
path: &Path,
identity: &MediaIdentity,
metadata: &MediaProbe,
change_revision: i64,
) -> Result<()> {
let tags = metadata
.tags
@@ -189,12 +196,13 @@ impl HybridSearch {
.map(|(k, v)| format!("{k}: {v}"))
.collect::<Vec<_>>()
.join("\n");
self.lexical.set_media_metadata(
self.lexical.set_media_metadata_at_revision(
&path.to_string_lossy(),
&tags,
metadata.duration_ms,
identity.bytes,
identity.modified_ns,
change_revision,
)
}
@@ -208,6 +216,7 @@ impl HybridSearch {
tools: &FfmpegTools,
limits: &MediaLimits,
identity: &MediaIdentity,
change_revision: i64,
report: &mut EmbeddingReport,
) -> Result<()> {
let mut ordinal = 0;
@@ -228,7 +237,7 @@ impl HybridSearch {
report.chunks += 1;
} else {
let metadata = probe(tools, path, limits)?;
self.refresh_media_metadata(path, identity, &metadata)?;
self.refresh_media_metadata(path, identity, &metadata, change_revision)?;
if metadata.has_video && kind == MediaKind::Video {
let mut seen = std::collections::HashSet::new();
let mut scenes = 0;
+3 -3
View File
@@ -111,7 +111,7 @@ impl Store {
|row| row.get(0),
)?;
ensure!(
(1..=7).contains(&version),
(1..=8).contains(&version),
"Unsupported semantic schema version {version}"
);
initialization.execute_batch("
@@ -273,7 +273,7 @@ impl Store {
)
};
let bytes: Vec<u8> = vector.iter().flat_map(|v| v.to_le_bytes()).collect();
self.connection.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) VALUES(?1,?2,?3,?4,?5,?6,?7,0,0,?8,?9,?10,?11,?12)",params![generation,-file_id,ordinal as i64,path,kind,timestamp,identity.sha256,snippet,bytes,source_type,identity.bytes as i64,identity.modified_ns])?;
self.connection.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,file_change_revision) VALUES(?1,?2,?3,?4,?5,?6,?7,0,0,?8,?9,?10,?11,?12,(SELECT change_revision FROM files WHERE path=?4))",params![generation,-file_id,ordinal as i64,path,kind,timestamp,identity.sha256,snippet,bytes,source_type,identity.bytes as i64,identity.modified_ns])?;
Ok(())
}
@@ -991,7 +991,7 @@ fn collect_eligibility(
};
let mut statement=connection.prepare(&format!("SELECT c.rowid,c.passage_id,c.content_hash,p.text,c.path,c.source_type FROM semantic_chunks c
LEFT JOIN passages p ON c.source_type='text' AND p.rowid=c.passage_id AND p.file_path=c.path AND p.kind=c.kind AND p.number=c.number
JOIN files f ON f.path=c.path WHERE c.generation=?1 AND ((c.source_type='text' AND p.rowid IS NOT NULL) OR (c.source_type!='text' AND c.file_size=f.size AND c.file_mtime=f.mtime))
JOIN files f ON f.path=c.path WHERE c.generation=?1 AND ((c.source_type='text' AND p.rowid IS NOT NULL) OR (c.source_type!='text' AND c.file_size=f.size AND c.file_mtime=f.mtime AND c.file_change_revision=f.change_revision))
AND (?2 IS NULL OR substr(f.path,1,length(?2))=?2) AND (?3 IS NULL OR f.extension=?3)
AND (?4 IS NULL OR f.size>=?4) AND (?5 IS NULL OR f.size<=?5)
AND (?6 IS NULL OR f.mtime/1000000>=?6) AND (?7 IS NULL OR f.mtime/1000000<=?7)
@@ -22,6 +22,11 @@ pub(super) fn migrate(connection: &Connection) -> Result<()> {
"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)",
@@ -43,7 +48,7 @@ pub(super) fn migrate(connection: &Connection) -> Result<()> {
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=7 WHERE singleton=1;")?;
UPDATE semantic_meta SET schema_version=8 WHERE singleton=1;")?;
Ok(())
}
@@ -294,8 +299,8 @@ impl Store {
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)
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 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("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",
@@ -530,8 +535,8 @@ impl Store {
"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)
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 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])?;
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" {
@@ -321,14 +321,39 @@ fn genuine_google_mixed_media_reuse_checks_bytes_despite_identical_file_metadata
.open(&image)?
.set_times(fs::FileTimes::new().set_modified(metadata.modified()?))?;
assert_eq!(fs::metadata(&image)?.modified()?, metadata.modified()?);
// Core intentionally sees unchanged size/mtime: semantic reuse must still
// hash actual media bytes before accepting a completion manifest.
// Full metadata reconciliation intentionally retains its media fast path.
// A native event must durably invalidate old media even with identical stats.
lexical.index_folder(&root)?;
let canonical_root = fs::canonicalize(&root)?;
let image_hint = fs::canonicalize(&image)?;
lexical.index_paths_with_options(
&canonical_root,
std::slice::from_ref(&image_hint),
&Default::default(),
)?;
assert!(HybridSearch::embedding_status(&db)?.stale);
let options = SearchOptions {
extension: Some("bmp".into()),
..Default::default()
};
assert!(
service.search("a red square", 10, &options)?.is_empty(),
"The hinted old image must be hidden until actual byte-verified reindexing"
);
service.index_embeddings_with_media(&tools, &limits)?;
let changed = calls.lock().unwrap().clone();
assert_eq!(changed.documents, initial.documents);
assert_eq!(changed.audio, initial.audio);
assert_eq!(changed.images, initial.images + 1);
assert!(!HybridSearch::embedding_status(&db)?.stale);
let response = service.search("a green square", 10, &options)?;
assert_eq!(response[0].filename, "color.bmp");
let connection = Connection::open(&db)?;
let revisions: (i64,i64) = connection.query_row(
"SELECT f.change_revision,c.file_change_revision FROM files f JOIN semantic_chunks c ON c.path=f.path WHERE f.path=?1 AND c.generation=(SELECT active_generation FROM semantic_meta WHERE singleton=1)",
[image_hint.to_string_lossy().as_ref()], |row| Ok((row.get(0)?,row.get(1)?)),
)?;
assert_eq!(revisions, (1, 1));
let latest = chunks(&db)?;
for chunk in first.iter().filter(|chunk| !chunk.0.ends_with("color.bmp")) {
assert!(
@@ -348,6 +373,27 @@ fn genuine_google_mixed_media_reuse_checks_bytes_despite_identical_file_metadata
.unwrap()
.6
);
// Unchanged bytes still need fresh eligibility after a hint, but the SHA-
// verified completed source should reuse vectors and remap revision tokens.
let audio_hint = fs::canonicalize(root.join("tone.wav"))?;
lexical.index_paths_with_options(
&canonical_root,
&[image_hint.clone(), audio_hint.clone()],
&Default::default(),
)?;
assert!(service.search("a green square", 10, &options)?.is_empty());
let reused = service.index_embeddings_with_media(&tools, &limits)?;
assert_eq!(reused.reused_sources, 5);
assert_eq!(*calls.lock().unwrap(), changed);
assert_eq!(
service.search("a green square", 10, &options)?[0].filename,
"color.bmp"
);
let audio_revisions: (i64,i64) = connection.query_row(
"SELECT f.change_revision,c.file_change_revision FROM files f JOIN semantic_chunks c ON c.path=f.path WHERE f.path=?1 AND c.generation=(SELECT active_generation FROM semantic_meta WHERE singleton=1)",
[audio_hint.to_string_lossy().as_ref()], |row| Ok((row.get(0)?,row.get(1)?)),
)?;
assert_eq!(audio_revisions, (1, 1));
Ok(())
}