Skip to main content

codesynapse_core/
incremental.rs

1use crate::cache::FileCache;
2use crate::error::Result;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6
7const MANIFEST_FILE: &str = "manifest.json";
8
9#[derive(Debug, Serialize, Deserialize, Default)]
10pub struct Manifest {
11    pub files: HashMap<String, String>,
12}
13
14impl Manifest {
15    pub fn load(output_dir: &Path) -> Option<Self> {
16        let path = output_dir.join(MANIFEST_FILE);
17        let text = std::fs::read_to_string(&path).ok()?;
18        serde_json::from_str(&text).ok()
19    }
20
21    pub fn save(&self, output_dir: &Path) -> Result<()> {
22        std::fs::create_dir_all(output_dir)?;
23        let path = output_dir.join(MANIFEST_FILE);
24        let json = serde_json::to_string_pretty(self)?;
25        std::fs::write(path, json)?;
26        Ok(())
27    }
28}
29
30pub struct IncrementalScan {
31    pub is_incremental: bool,
32    pub changed_files: Vec<PathBuf>,
33    pub unchanged_files: Vec<PathBuf>,
34}
35
36pub struct IncrementalBuilder {
37    output_dir: PathBuf,
38}
39
40impl IncrementalBuilder {
41    pub fn new(output_dir: PathBuf) -> Self {
42        Self { output_dir }
43    }
44
45    pub fn scan(&self, files: &[PathBuf]) -> IncrementalScan {
46        let manifest = Manifest::load(&self.output_dir);
47        let is_incremental = manifest.is_some();
48        let prev = manifest.unwrap_or_default();
49
50        let mut changed = Vec::new();
51        let mut unchanged = Vec::new();
52
53        for path in files {
54            let key = path.to_string_lossy().to_string();
55            let current_hash = std::fs::read(path)
56                .map(|b| FileCache::compute_hash(&b))
57                .unwrap_or_default();
58            match prev.files.get(&key) {
59                Some(h) if h == &current_hash => unchanged.push(path.clone()),
60                _ => changed.push(path.clone()),
61            }
62        }
63
64        IncrementalScan {
65            is_incremental,
66            changed_files: changed,
67            unchanged_files: unchanged,
68        }
69    }
70
71    pub fn write_manifest(&self, files: &[PathBuf]) -> Result<()> {
72        let mut manifest = Manifest::default();
73        for path in files {
74            let key = path.to_string_lossy().to_string();
75            let hash = std::fs::read(path)
76                .map(|b| FileCache::compute_hash(&b))
77                .unwrap_or_default();
78            manifest.files.insert(key, hash);
79        }
80        manifest.save(&self.output_dir)
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use tempfile::tempdir;
88
89    fn write_file(dir: &Path, name: &str, content: &str) -> PathBuf {
90        let p = dir.join(name);
91        std::fs::write(&p, content).unwrap();
92        p
93    }
94
95    #[test]
96    fn incremental_manifest_written() {
97        let tmp = tempdir().unwrap();
98        let out = tmp.path().join("out");
99        std::fs::create_dir_all(&out).unwrap();
100
101        let src = tmp.path().join("src");
102        std::fs::create_dir_all(&src).unwrap();
103        let f = write_file(&src, "foo.py", "def foo(): pass");
104
105        let builder = IncrementalBuilder::new(out.clone());
106        assert!(!out.join("manifest.json").exists());
107
108        builder.write_manifest(&[f]).unwrap();
109
110        assert!(out.join("manifest.json").exists());
111        let m = Manifest::load(&out).unwrap();
112        assert_eq!(m.files.len(), 1);
113    }
114
115    #[test]
116    fn incremental_mode_detected_via_manifest() {
117        let tmp = tempdir().unwrap();
118        let out = tmp.path().join("out");
119        std::fs::create_dir_all(&out).unwrap();
120
121        let src = tmp.path().join("src");
122        std::fs::create_dir_all(&src).unwrap();
123        let f = write_file(&src, "intro.py", "x = 1");
124
125        let builder = IncrementalBuilder::new(out.clone());
126        builder.write_manifest(std::slice::from_ref(&f)).unwrap();
127
128        let scan = builder.scan(&[f]);
129        assert!(scan.is_incremental);
130        assert_eq!(scan.unchanged_files.len(), 1);
131        assert!(scan.changed_files.is_empty());
132    }
133
134    #[test]
135    fn incremental_no_manifest_full_scan() {
136        let tmp = tempdir().unwrap();
137        let out = tmp.path().join("out");
138        std::fs::create_dir_all(&out).unwrap();
139
140        let src = tmp.path().join("src");
141        std::fs::create_dir_all(&src).unwrap();
142        let f = write_file(&src, "api.py", "def api(): pass");
143
144        let builder = IncrementalBuilder::new(out.clone());
145        let scan = builder.scan(&[f]);
146
147        assert!(!scan.is_incremental);
148        assert_eq!(scan.changed_files.len(), 1);
149        assert!(scan.unchanged_files.is_empty());
150    }
151}