aster/map/
incremental_cache.rs1use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7
8use super::types::{CacheData, CacheEntry, ModuleNode};
9
10pub struct IncrementalCache {
12 cache_file: PathBuf,
13 cache: Option<CacheData>,
14 dirty: bool,
15}
16
17impl IncrementalCache {
18 pub fn new(project_root: impl AsRef<Path>) -> Self {
19 let cache_file = project_root.as_ref().join(".claude").join("map-cache.json");
20 Self {
21 cache_file,
22 cache: None,
23 dirty: false,
24 }
25 }
26
27 pub fn load(&mut self) -> bool {
29 if !self.cache_file.exists() {
30 self.cache = None;
31 return false;
32 }
33
34 match std::fs::read_to_string(&self.cache_file) {
35 Ok(content) => match serde_json::from_str(&content) {
36 Ok(data) => {
37 self.cache = Some(data);
38 self.dirty = false;
39 true
40 }
41 Err(_) => {
42 self.cache = None;
43 false
44 }
45 },
46 Err(_) => {
47 self.cache = None;
48 false
49 }
50 }
51 }
52
53 pub fn save(&mut self) -> bool {
55 if self.cache.is_none() || !self.dirty {
56 return true;
57 }
58
59 if let Some(ref mut cache) = self.cache {
60 cache.generated_at = chrono::Utc::now().to_rfc3339();
61 }
62
63 if let Some(parent) = self.cache_file.parent() {
64 let _ = std::fs::create_dir_all(parent);
65 }
66
67 if let Ok(content) = serde_json::to_string_pretty(&self.cache) {
68 if std::fs::write(&self.cache_file, content).is_ok() {
69 self.dirty = false;
70 return true;
71 }
72 }
73 false
74 }
75
76 pub fn needs_reanalysis(&self, file_path: &Path) -> bool {
78 let cache = match &self.cache {
79 Some(c) => c,
80 None => return true,
81 };
82
83 let relative = self.get_relative_path(file_path);
84 let entry = match cache.entries.get(&relative) {
85 Some(e) => e,
86 None => return true,
87 };
88
89 match std::fs::metadata(file_path) {
90 Ok(meta) => {
91 let mtime = meta
92 .modified()
93 .map(|t| {
94 t.duration_since(std::time::UNIX_EPOCH)
95 .unwrap_or_default()
96 .as_millis() as u64
97 })
98 .unwrap_or(0);
99 if entry.mtime != mtime {
100 if let Ok(content) = std::fs::read_to_string(file_path) {
101 let hash = self.calculate_hash(&content);
102 return hash != entry.hash;
103 }
104 }
105 false
106 }
107 Err(_) => true,
108 }
109 }
110
111 pub fn check_files(&self, file_paths: &[PathBuf]) -> FileCheckResult {
113 let mut changed = Vec::new();
114 let mut unchanged = Vec::new();
115 let mut removed = Vec::new();
116
117 let current_files: std::collections::HashSet<_> = file_paths
118 .iter()
119 .map(|f| self.get_relative_path(f))
120 .collect();
121
122 for path in file_paths {
123 if self.needs_reanalysis(path) {
124 changed.push(path.clone());
125 } else {
126 unchanged.push(path.clone());
127 }
128 }
129
130 if let Some(ref cache) = self.cache {
131 for cached_path in cache.entries.keys() {
132 if !current_files.contains(cached_path) {
133 removed.push(cached_path.clone());
134 }
135 }
136 }
137
138 FileCheckResult {
139 changed,
140 unchanged,
141 removed,
142 }
143 }
144
145 pub fn get_cached_module(&self, file_path: &Path) -> Option<ModuleNode> {
147 let cache = self.cache.as_ref()?;
148 let relative = self.get_relative_path(file_path);
149 cache.entries.get(&relative).map(|e| e.module.clone())
150 }
151
152 pub fn update_entry(&mut self, file_path: &Path, module: ModuleNode) {
154 if self.cache.is_none() {
155 self.cache = Some(CacheData {
156 version: "1.0.0".to_string(),
157 root_path: self
158 .cache_file
159 .parent()
160 .and_then(|p| p.parent())
161 .map(|p| p.to_string_lossy().to_string())
162 .unwrap_or_default(),
163 generated_at: chrono::Utc::now().to_rfc3339(),
164 entries: HashMap::new(),
165 });
166 }
167
168 if let Ok(meta) = std::fs::metadata(file_path) {
169 if let Ok(content) = std::fs::read_to_string(file_path) {
170 let hash = self.calculate_hash(&content);
171 let mtime = meta
172 .modified()
173 .map(|t| {
174 t.duration_since(std::time::UNIX_EPOCH)
175 .unwrap_or_default()
176 .as_millis() as u64
177 })
178 .unwrap_or(0);
179
180 let relative = self.get_relative_path(file_path);
181 if let Some(cache) = self.cache.as_mut() {
182 cache.entries.insert(
183 relative,
184 CacheEntry {
185 hash,
186 mtime,
187 module,
188 },
189 );
190 self.dirty = true;
191 }
192 }
193 }
194 }
195
196 pub fn remove_entry(&mut self, file_path: &Path) {
198 let relative = self.get_relative_path(file_path);
199 if let Some(ref mut cache) = self.cache {
200 if cache.entries.remove(&relative).is_some() {
201 self.dirty = true;
202 }
203 }
204 }
205
206 pub fn clear(&mut self) {
208 self.cache = None;
209 self.dirty = false;
210 let _ = std::fs::remove_file(&self.cache_file);
211 }
212
213 pub fn get_stats(&self) -> CacheStats {
215 let cache_file_size = std::fs::metadata(&self.cache_file)
216 .map(|m| m.len() as usize)
217 .unwrap_or(0);
218
219 CacheStats {
220 entry_count: self.cache.as_ref().map(|c| c.entries.len()).unwrap_or(0),
221 cache_file_size,
222 last_generated: self.cache.as_ref().map(|c| c.generated_at.clone()),
223 }
224 }
225
226 fn get_relative_path(&self, file_path: &Path) -> String {
227 if let Some(ref cache) = self.cache {
228 if let Ok(rel) = file_path.strip_prefix(&cache.root_path) {
229 return rel.to_string_lossy().replace('\\', "/");
230 }
231 }
232 if let Some(parent) = self.cache_file.parent().and_then(|p| p.parent()) {
233 if let Ok(rel) = file_path.strip_prefix(parent) {
234 return rel.to_string_lossy().replace('\\', "/");
235 }
236 }
237 file_path.to_string_lossy().replace('\\', "/")
238 }
239
240 fn calculate_hash(&self, content: &str) -> String {
241 use std::hash::{Hash, Hasher};
242 let mut hasher = std::collections::hash_map::DefaultHasher::new();
243 content.hash(&mut hasher);
244 format!("{:x}", hasher.finish())
245 }
246}
247
248#[derive(Debug, Clone, Default)]
250pub struct FileCheckResult {
251 pub changed: Vec<PathBuf>,
252 pub unchanged: Vec<PathBuf>,
253 pub removed: Vec<String>,
254}
255
256#[derive(Debug, Clone, Default)]
258pub struct CacheStats {
259 pub entry_count: usize,
260 pub cache_file_size: usize,
261 pub last_generated: Option<String>,
262}
263
264pub fn create_cache(project_root: impl AsRef<Path>) -> IncrementalCache {
266 IncrementalCache::new(project_root)
267}