agentic_codebase/engine/
incremental.rs1use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::time::Instant;
10
11use crate::graph::CodeGraph;
12use crate::parse::parser::ParseOptions;
13use crate::parse::parser::Parser;
14use crate::semantic::analyzer::{AnalyzeOptions, SemanticAnalyzer};
15use crate::types::AcbResult;
16
17#[derive(Debug, Clone)]
19pub struct ChangeSet {
20 pub added: Vec<PathBuf>,
22 pub modified: Vec<PathBuf>,
24 pub removed: Vec<PathBuf>,
26}
27
28impl ChangeSet {
29 pub fn is_empty(&self) -> bool {
31 self.added.is_empty() && self.modified.is_empty() && self.removed.is_empty()
32 }
33
34 pub fn total(&self) -> usize {
36 self.added.len() + self.modified.len() + self.removed.len()
37 }
38}
39
40#[derive(Debug, Clone)]
42pub struct IncrementalResult {
43 pub units_added: usize,
45 pub units_removed: usize,
47 pub units_modified: usize,
49 pub duration: std::time::Duration,
51}
52
53pub struct IncrementalCompiler {
55 hashes: HashMap<PathBuf, String>,
57}
58
59impl IncrementalCompiler {
60 pub fn new() -> Self {
62 Self {
63 hashes: HashMap::new(),
64 }
65 }
66
67 pub fn from_graph(graph: &CodeGraph) -> Self {
72 let mut hashes = HashMap::new();
73
74 for unit in graph.units() {
75 let path = &unit.file_path;
76 if hashes.contains_key(path) {
77 continue;
78 }
79 if let Ok(content) = std::fs::read(path) {
80 let hash = blake3::hash(&content).to_hex().to_string();
81 hashes.insert(path.clone(), hash);
82 }
83 }
84
85 Self { hashes }
86 }
87
88 pub fn detect_changes(&self, dir: &Path) -> AcbResult<ChangeSet> {
93 let current_files = collect_source_files(dir)?;
94
95 let mut added = Vec::new();
96 let mut modified = Vec::new();
97
98 for path in ¤t_files {
99 let current_hash = match std::fs::read(path) {
100 Ok(content) => blake3::hash(&content).to_hex().to_string(),
101 Err(_) => continue,
102 };
103
104 match self.hashes.get(path) {
105 Some(stored_hash) => {
106 if *stored_hash != current_hash {
107 modified.push(path.clone());
108 }
109 }
110 None => {
111 added.push(path.clone());
112 }
113 }
114 }
115
116 let current_set: std::collections::HashSet<&PathBuf> = current_files.iter().collect();
118 let removed: Vec<PathBuf> = self
119 .hashes
120 .keys()
121 .filter(|p| !current_set.contains(p))
122 .cloned()
123 .collect();
124
125 Ok(ChangeSet {
126 added,
127 modified,
128 removed,
129 })
130 }
131
132 pub fn recompile(
138 &mut self,
139 dir: &Path,
140 changes: &ChangeSet,
141 ) -> AcbResult<(CodeGraph, IncrementalResult)> {
142 let start = Instant::now();
143
144 let changed_file_count = changes.total();
146
147 tracing::info!(
148 "Incremental: {} added, {} modified, {} removed",
149 changes.added.len(),
150 changes.modified.len(),
151 changes.removed.len()
152 );
153
154 let parser = Parser::new();
157 let parse_result = parser.parse_directory(dir, &ParseOptions::default())?;
158
159 let analyzer = SemanticAnalyzer::new();
160 let graph = analyzer.analyze(parse_result.units, &AnalyzeOptions::default())?;
161
162 self.hashes.clear();
164 for unit in graph.units() {
165 let path = &unit.file_path;
166 if self.hashes.contains_key(path) {
167 continue;
168 }
169 if let Ok(content) = std::fs::read(path) {
170 let hash = blake3::hash(&content).to_hex().to_string();
171 self.hashes.insert(path.clone(), hash);
172 }
173 }
174
175 let duration = start.elapsed();
176
177 let result = IncrementalResult {
178 units_added: changes.added.len(),
179 units_removed: changes.removed.len(),
180 units_modified: changes.modified.len(),
181 duration,
182 };
183
184 tracing::info!(
185 "Incremental recompile: {} changed files in {:.2?}",
186 changed_file_count,
187 duration
188 );
189
190 Ok((graph, result))
191 }
192}
193
194impl Default for IncrementalCompiler {
195 fn default() -> Self {
196 Self::new()
197 }
198}
199
200fn collect_source_files(dir: &Path) -> AcbResult<Vec<PathBuf>> {
202 use ignore::WalkBuilder;
203
204 let extensions = ["rs", "py", "ts", "tsx", "js", "jsx", "go"];
205 let mut files = Vec::new();
206
207 let walker = WalkBuilder::new(dir).hidden(true).git_ignore(true).build();
208
209 for entry in walker {
210 let entry = entry
211 .map_err(|e| crate::AcbError::Io(std::io::Error::other(format!("Walk error: {e}"))))?;
212 let path = entry.path();
213 if path.is_file() {
214 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
215 if extensions.contains(&ext) {
216 files.push(path.to_path_buf());
217 }
218 }
219 }
220 }
221
222 Ok(files)
223}