Skip to main content

semtree_rag/
manifest.rs

1use std::collections::hash_map::DefaultHasher;
2use std::collections::HashMap;
3use std::hash::{Hash, Hasher};
4use std::path::{Path, PathBuf};
5
6use serde::{Deserialize, Serialize};
7
8use crate::RagError;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct FileEntry {
12    /// Hash of file content for change detection
13    pub content_hash: u64,
14    /// Chunk IDs produced from this file
15    pub chunk_ids: Vec<String>,
16}
17
18/// Tracks per-file state to enable incremental re-indexing.
19#[derive(Debug, Default, Serialize, Deserialize)]
20pub struct FileManifest {
21    entries: HashMap<PathBuf, FileEntry>,
22}
23
24impl FileManifest {
25    pub fn load(index_dir: &Path) -> Self {
26        let path = index_dir.join("manifest.json");
27        std::fs::read_to_string(&path)
28            .ok()
29            .and_then(|raw| serde_json::from_str(&raw).ok())
30            .unwrap_or_default()
31    }
32
33    pub fn save(&self, index_dir: &Path) -> Result<(), RagError> {
34        let path = index_dir.join("manifest.json");
35        let data =
36            serde_json::to_string(self).map_err(|e| RagError::Io(std::io::Error::other(e)))?;
37        std::fs::write(path, data)?;
38        Ok(())
39    }
40
41    /// Returns `true` if the file is new or its content has changed.
42    pub fn is_changed(&self, path: &Path, content: &str) -> bool {
43        let hash = content_hash(content);
44        match self.entries.get(path) {
45            Some(entry) => entry.content_hash != hash,
46            None => true,
47        }
48    }
49
50    /// Returns the chunk IDs that were last indexed from this file.
51    pub fn chunk_ids(&self, path: &Path) -> &[String] {
52        self.entries
53            .get(path)
54            .map(|e| e.chunk_ids.as_slice())
55            .unwrap_or(&[])
56    }
57
58    /// Record the result of indexing a file.
59    pub fn record(&mut self, path: PathBuf, content: &str, chunk_ids: Vec<String>) {
60        self.entries.insert(
61            path,
62            FileEntry {
63                content_hash: content_hash(content),
64                chunk_ids,
65            },
66        );
67    }
68
69    /// Remove a file entry (e.g. when the file is deleted).
70    pub fn remove(&mut self, path: &Path) -> Option<FileEntry> {
71        self.entries.remove(path)
72    }
73
74    /// Returns all tracked paths.
75    pub fn paths(&self) -> impl Iterator<Item = &PathBuf> {
76        self.entries.keys()
77    }
78}
79
80fn content_hash(content: &str) -> u64 {
81    let mut h = DefaultHasher::new();
82    content.hash(&mut h);
83    h.finish()
84}