1use crate::error::Result;
2use crate::model::*;
3use crate::ports::{
4 EmbeddingProvider, FileData, GraphStore, ParseProvider, SearchIndex, VectorStore,
5};
6use std::path::{Path, PathBuf};
7use std::sync::atomic::{AtomicUsize, Ordering};
8
9pub struct InMemoryGraphStore {
11 pub files: Vec<FileNode>,
12 pub symbols: Vec<SymbolNode>,
13 pub edges: Vec<Edge>,
14 pub symbols_for_files_calls: AtomicUsize,
15 pub edges_streaming_calls: AtomicUsize,
16}
17
18impl Clone for InMemoryGraphStore {
19 fn clone(&self) -> Self {
20 Self {
21 files: self.files.clone(),
22 symbols: self.symbols.clone(),
23 edges: self.edges.clone(),
24 symbols_for_files_calls: AtomicUsize::new(
25 self.symbols_for_files_calls.load(Ordering::Relaxed),
26 ),
27 edges_streaming_calls: AtomicUsize::new(
28 self.edges_streaming_calls.load(Ordering::Relaxed),
29 ),
30 }
31 }
32}
33
34impl Default for InMemoryGraphStore {
35 fn default() -> Self {
36 Self {
37 files: Vec::new(),
38 symbols: Vec::new(),
39 edges: Vec::new(),
40 symbols_for_files_calls: AtomicUsize::new(0),
41 edges_streaming_calls: AtomicUsize::new(0),
42 }
43 }
44}
45
46impl InMemoryGraphStore {
47 pub fn new() -> Self {
48 Self::default()
49 }
50
51 pub fn insert_file(&mut self, file: FileNode) {
52 self.files.push(file);
53 }
54
55 pub fn insert_symbol(&mut self, symbol: SymbolNode) {
56 self.symbols.push(symbol);
57 }
58
59 pub fn insert_edge(&mut self, edge: Edge) {
60 self.edges.push(edge);
61 }
62}
63
64impl GraphStore for InMemoryGraphStore {
65 fn upsert_file(&self, _file: &FileNode) -> Result<()> {
66 Ok(())
67 }
68 fn upsert_symbol(&self, _symbol: &SymbolNode) -> Result<()> {
69 Ok(())
70 }
71 fn upsert_edge(&self, _edge: &Edge) -> Result<()> {
72 Ok(())
73 }
74 fn get_file(&self, path: &Path) -> Result<Option<FileNode>> {
75 Ok(self.files.iter().find(|f| f.path == path).cloned())
76 }
77 fn get_symbol(&self, qualified_name: &str) -> Result<Option<SymbolNode>> {
78 Ok(self
79 .symbols
80 .iter()
81 .find(|s| s.qualified_name == qualified_name)
82 .cloned())
83 }
84 fn get_edges_from(&self, source: &str) -> Result<Vec<Edge>> {
85 Ok(self
86 .edges
87 .iter()
88 .filter(|e| e.source == source)
89 .cloned()
90 .collect())
91 }
92 fn get_edges_to(&self, target: &str) -> Result<Vec<Edge>> {
93 Ok(self
94 .edges
95 .iter()
96 .filter(|e| e.target == target)
97 .cloned()
98 .collect())
99 }
100 fn all_files(&self) -> Result<Vec<FileNode>> {
101 Ok(self.files.clone())
102 }
103 fn all_symbols(&self) -> Result<Vec<SymbolNode>> {
104 Ok(self.symbols.clone())
105 }
106 fn all_edges(&self) -> Result<Vec<Edge>> {
107 Ok(self.edges.clone())
108 }
109 fn remove_file(&self, _path: &Path) -> Result<()> {
110 Ok(())
111 }
112 fn remove_symbols_in_file(&self, _path: &Path) -> Result<()> {
113 Ok(())
114 }
115 fn stats(&self) -> Result<GraphStats> {
116 Ok(GraphStats {
117 files: self.files.len(),
118 symbols: self.symbols.len(),
119 edges: self.edges.len(),
120 entry_point_count: None,
121 avg_criticality: None,
122 clone_clusters: None,
123 duplication_pct: None,
124 most_duplicated: None,
125 avg_risk: None,
126 p90_risk: None,
127 community_count: None,
128 modularity: None,
129 })
130 }
131 fn find_by_name(&self, pattern: &str) -> Result<Vec<SymbolNode>> {
132 let exact: Vec<SymbolNode> = self
133 .symbols
134 .iter()
135 .filter(|s| s.name == pattern)
136 .cloned()
137 .collect();
138 if !exact.is_empty() {
139 return Ok(exact);
140 }
141 Ok(self
142 .symbols
143 .iter()
144 .filter(|s| s.name.starts_with(pattern))
145 .cloned()
146 .collect())
147 }
148
149 fn symbols_for_files(&self, paths: &[&Path]) -> Result<Vec<SymbolNode>> {
150 self.symbols_for_files_calls.fetch_add(1, Ordering::Relaxed);
151 Ok(self
152 .symbols
153 .iter()
154 .filter(|s| paths.contains(&&*s.location.file))
155 .cloned()
156 .collect())
157 }
158
159 fn edges_streaming(&self, callback: &mut dyn FnMut(Edge) -> Result<()>) -> Result<()> {
160 self.edges_streaming_calls.fetch_add(1, Ordering::Relaxed);
161 for edge in &self.edges {
162 callback(edge.clone())?;
163 }
164 Ok(())
165 }
166
167 fn store_file_data(
168 &self,
169 _file: &FileNode,
170 _symbols: &[SymbolNode],
171 _edges: &[Edge],
172 ) -> Result<()> {
173 Ok(())
174 }
175
176 fn remove_file_data(&self, _path: &Path) -> Result<()> {
177 Ok(())
178 }
179}
180
181impl SearchIndex for InMemoryGraphStore {
182 fn index_symbol(&self, _symbol: &SymbolNode) -> Result<()> {
183 Ok(())
184 }
185 fn search(&self, query: &str, limit: usize) -> Result<Vec<SearchResult>> {
186 let results: Vec<SearchResult> = self
187 .symbols
188 .iter()
189 .filter(|s| s.name.contains(query) || s.qualified_name.contains(query))
190 .take(limit)
191 .map(|s| SearchResult {
192 qualified_name: s.qualified_name.clone(),
193 name: s.name.clone(),
194 kind: s.kind,
195 file_path: s.location.file.clone(),
196 score: 1.0,
197 score_source: None,
198 })
199 .collect();
200 Ok(results)
201 }
202 fn rebuild(&self) -> Result<()> {
203 Ok(())
204 }
205}
206
207pub struct MockParseProvider {
209 pub results: Vec<FileData>,
210}
211
212impl MockParseProvider {
213 pub fn new(results: Vec<FileData>) -> Self {
214 Self { results }
215 }
216}
217
218impl ParseProvider for MockParseProvider {
219 fn parse_and_resolve(
220 &self,
221 _files: &[(PathBuf, Vec<u8>)],
222 _project_root: &Path,
223 ) -> crate::error::Result<Vec<FileData>> {
224 Ok(self.results.clone())
225 }
226}
227
228pub struct MockFileSystem {
230 pub files: Vec<(PathBuf, String)>,
231 pub hashes: Vec<(PathBuf, String)>,
232}
233
234impl MockFileSystem {
235 pub fn new(files: Vec<(PathBuf, String)>) -> Self {
236 Self {
237 files,
238 hashes: vec![],
239 }
240 }
241
242 pub fn with_hashes(mut self, hashes: Vec<(PathBuf, String)>) -> Self {
243 self.hashes = hashes;
244 self
245 }
246}
247
248impl crate::ports::FileSystem for MockFileSystem {
249 fn list_files(&self, _root: &Path, extensions: &[&str]) -> Result<Vec<PathBuf>> {
250 Ok(self
251 .files
252 .iter()
253 .filter(|(p, _)| {
254 p.extension()
255 .and_then(|e| e.to_str())
256 .is_some_and(|e| extensions.contains(&e))
257 })
258 .map(|(p, _)| p.clone())
259 .collect())
260 }
261
262 fn read_file(&self, path: &Path) -> Result<String> {
263 self.files
264 .iter()
265 .find(|(p, _)| p == path)
266 .map(|(_, content)| content.clone())
267 .ok_or_else(|| {
268 crate::error::CodeGraphError::Other(format!("file not found: {}", path.display()))
269 })
270 }
271
272 fn file_hash(&self, path: &Path) -> Result<String> {
273 if !self.hashes.is_empty() {
274 return self
275 .hashes
276 .iter()
277 .find(|(p, _)| p == path)
278 .map(|(_, h)| h.clone())
279 .ok_or_else(|| {
280 crate::error::CodeGraphError::Other(format!(
281 "file not found: {}",
282 path.display()
283 ))
284 });
285 }
286 Ok("mock_hash".to_string())
287 }
288}
289
290pub struct MockGitProvider {
292 pub modified: Vec<PathBuf>,
293}
294
295impl Default for MockGitProvider {
296 fn default() -> Self {
297 Self::new()
298 }
299}
300
301impl MockGitProvider {
302 pub fn new() -> Self {
303 Self { modified: vec![] }
304 }
305
306 pub fn with_modified(files: Vec<PathBuf>) -> Self {
307 Self { modified: files }
308 }
309}
310
311impl crate::ports::GitProvider for MockGitProvider {
312 fn current_head(&self) -> Result<String> {
313 Ok("abcd1234".to_string())
314 }
315
316 fn changed_files(&self, _from: &str, _to: &str) -> Result<Vec<PathBuf>> {
317 Ok(vec![])
318 }
319
320 fn diff_hunks(&self, _from: &str, _to: Option<&str>) -> Result<Vec<DiffHunk>> {
321 Ok(vec![])
322 }
323
324 fn modified_files(&self) -> Result<Vec<PathBuf>> {
325 Ok(self.modified.clone())
326 }
327}
328
329pub struct InMemoryVectorStore {
336 pub entries: std::sync::Mutex<Vec<EmbeddingEntry>>,
337}
338
339impl Default for InMemoryVectorStore {
340 fn default() -> Self {
341 Self::new()
342 }
343}
344
345impl InMemoryVectorStore {
346 pub fn new() -> Self {
347 Self {
348 entries: std::sync::Mutex::new(Vec::new()),
349 }
350 }
351}
352
353fn cosine_similarity(a: &[f32], b: &[f32]) -> f64 {
354 let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
355 let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
356 let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
357 if norm_a == 0.0 || norm_b == 0.0 {
358 return 0.0;
359 }
360 (dot / (norm_a * norm_b)) as f64
361}
362
363impl VectorStore for InMemoryVectorStore {
364 fn store_embeddings(&self, entries: &[EmbeddingEntry]) -> Result<()> {
365 let mut store = self.entries.lock().unwrap();
366 for entry in entries {
367 store.retain(|e| e.qualified_name != entry.qualified_name);
368 store.push(entry.clone());
369 }
370 Ok(())
371 }
372
373 fn search_nearest(&self, query_vec: &[f32], limit: usize) -> Result<Vec<(String, f64)>> {
374 let store = self.entries.lock().unwrap();
375 let mut scored: Vec<(String, f64)> = store
376 .iter()
377 .map(|e| {
378 let score = cosine_similarity(query_vec, &e.vector);
379 (e.qualified_name.clone(), score)
380 })
381 .collect();
382 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
383 scored.truncate(limit);
384 Ok(scored)
385 }
386
387 fn has_embeddings(&self) -> bool {
388 !self.entries.lock().unwrap().is_empty()
389 }
390
391 fn count(&self) -> Result<usize> {
392 Ok(self.entries.lock().unwrap().len())
393 }
394
395 fn remove_embeddings(&self, qualified_names: &[&str]) -> Result<()> {
396 let mut store = self.entries.lock().unwrap();
397 store.retain(|e| !qualified_names.contains(&e.qualified_name.as_str()));
398 Ok(())
399 }
400
401 fn get_stored_hashes(&self) -> Result<Vec<(String, String)>> {
402 let store = self.entries.lock().unwrap();
403 Ok(store
404 .iter()
405 .map(|e| (e.qualified_name.clone(), e.text_hash.clone()))
406 .collect())
407 }
408}
409
410#[derive(Clone)]
417pub struct InMemoryEmbeddingProvider {
418 pub dimension: usize,
419}
420
421impl Default for InMemoryEmbeddingProvider {
422 fn default() -> Self {
423 Self::new(4)
424 }
425}
426
427impl InMemoryEmbeddingProvider {
428 pub fn new(dimension: usize) -> Self {
429 Self { dimension }
430 }
431
432 fn text_to_vec(&self, text: &str) -> Vec<f32> {
433 let mut v = vec![0.0f32; self.dimension];
434 for (i, b) in text.bytes().enumerate() {
435 v[i % self.dimension] += b as f32;
436 }
437 let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
439 if norm > 0.0 {
440 for x in &mut v {
441 *x /= norm;
442 }
443 }
444 v
445 }
446}
447
448impl EmbeddingProvider for InMemoryEmbeddingProvider {
449 fn embed_batch(&self, texts: &[String]) -> Result<Vec<Vec<f32>>> {
450 Ok(texts.iter().map(|t| self.text_to_vec(t)).collect())
451 }
452
453 fn embed_query(&self, text: &str) -> Result<Vec<f32>> {
454 Ok(self.text_to_vec(text))
455 }
456
457 fn dimension(&self) -> usize {
458 self.dimension
459 }
460}