Skip to main content

codesynapse_core/
query.rs

1use crate::error::Result;
2use crate::graph::GraphStore;
3use crate::types::{Edge, Node, NodeId, QueryResult};
4use std::collections::VecDeque;
5use std::collections::{HashMap, HashSet};
6
7pub struct QueryEngine {
8    store: Box<dyn GraphStore>,
9    max_nodes: usize,
10}
11
12impl QueryEngine {
13    pub fn new(store: Box<dyn GraphStore>) -> Self {
14        QueryEngine {
15            store,
16            max_nodes: 10_000,
17        }
18    }
19
20    pub fn with_max_nodes(store: Box<dyn GraphStore>, max_nodes: usize) -> Self {
21        QueryEngine { store, max_nodes }
22    }
23
24    pub fn store_ref(&self) -> &dyn GraphStore {
25        self.store.as_ref()
26    }
27
28    pub fn query_text(
29        &self,
30        query: &str,
31        mode: &str,
32        depth: usize,
33        token_budget: Option<usize>,
34        context_filter: Option<&str>,
35    ) -> Result<QueryResult> {
36        let seeds = self.store.search(query, 10)?;
37        let seed_nodes: Vec<Node> = seeds.into_iter().map(|(_, n)| n).collect();
38
39        if seed_nodes.is_empty() {
40            return Ok(QueryResult {
41                seed_nodes: vec![],
42                neighborhood: vec![],
43                edges: vec![],
44                truncated: false,
45            });
46        }
47
48        let node_count = self.store.node_count()?;
49        if node_count > self.max_nodes {
50            return Ok(QueryResult {
51                seed_nodes: vec![],
52                neighborhood: vec![],
53                edges: vec![],
54                truncated: true,
55            });
56        }
57
58        match mode {
59            "bfs" => self.traverse_bfs(&seed_nodes, depth, context_filter, token_budget),
60            "dfs" => self.traverse_dfs(&seed_nodes, depth, context_filter, token_budget),
61            _ => self.traverse_bfs(&seed_nodes, depth, context_filter, token_budget),
62        }
63    }
64
65    fn traverse_bfs(
66        &self,
67        seed_nodes: &[Node],
68        depth: usize,
69        context_filter: Option<&str>,
70        token_budget: Option<usize>,
71    ) -> Result<QueryResult> {
72        let mut visited: HashSet<NodeId> = HashSet::new();
73        let mut neighborhood: Vec<Node> = Vec::new();
74        let mut edges: Vec<Edge> = Vec::new();
75
76        for seed in seed_nodes {
77            visited.insert(seed.id.clone());
78            neighborhood.push(seed.clone());
79        }
80
81        if depth > 0 {
82            let mut frontier: Vec<NodeId> = seed_nodes.iter().map(|n| n.id.clone()).collect();
83            for _ in 0..depth {
84                let mut next_frontier = Vec::new();
85                for node_id in &frontier {
86                    if let Ok(neighbors) = self.store.neighbors(node_id, context_filter) {
87                        for (neighbor, edge) in &neighbors {
88                            if visited.insert(neighbor.id.clone()) {
89                                neighborhood.push(neighbor.clone());
90                                next_frontier.push(neighbor.id.clone());
91                            }
92                            edges.push(edge.clone());
93                        }
94                    }
95                }
96                frontier = next_frontier;
97                if frontier.is_empty() {
98                    break;
99                }
100                if let Some(budget) = token_budget {
101                    let token_count: usize =
102                        neighborhood.iter().map(|n| n.label.len()).sum::<usize>()
103                            + edges.iter().map(|e| e.relation.len()).sum::<usize>();
104                    if token_count >= budget {
105                        return Ok(QueryResult {
106                            seed_nodes: seed_nodes.to_vec(),
107                            neighborhood,
108                            edges,
109                            truncated: true,
110                        });
111                    }
112                }
113            }
114        }
115
116        Ok(QueryResult {
117            seed_nodes: seed_nodes.to_vec(),
118            neighborhood,
119            edges,
120            truncated: false,
121        })
122    }
123
124    fn traverse_dfs(
125        &self,
126        seed_nodes: &[Node],
127        depth: usize,
128        context_filter: Option<&str>,
129        token_budget: Option<usize>,
130    ) -> Result<QueryResult> {
131        let mut visited: HashSet<NodeId> = HashSet::new();
132        let mut neighborhood: Vec<Node> = Vec::new();
133        let mut edges: Vec<Edge> = Vec::new();
134
135        for seed in seed_nodes {
136            visited.insert(seed.id.clone());
137            neighborhood.push(seed.clone());
138        }
139
140        if depth > 0 {
141            let mut stack: Vec<(NodeId, usize)> =
142                seed_nodes.iter().map(|n| (n.id.clone(), 0)).collect();
143
144            while let Some((node_id, current_depth)) = stack.pop() {
145                if current_depth >= depth {
146                    continue;
147                }
148
149                if let Ok(neighbors) = self.store.neighbors(&node_id, context_filter) {
150                    for (neighbor, edge) in &neighbors {
151                        if visited.insert(neighbor.id.clone()) {
152                            neighborhood.push(neighbor.clone());
153                            stack.push((neighbor.id.clone(), current_depth + 1));
154                        }
155                        edges.push(edge.clone());
156                    }
157                }
158
159                if let Some(budget) = token_budget {
160                    let token_count: usize =
161                        neighborhood.iter().map(|n| n.label.len()).sum::<usize>()
162                            + edges.iter().map(|e| e.relation.len()).sum::<usize>();
163                    if token_count >= budget {
164                        return Ok(QueryResult {
165                            seed_nodes: seed_nodes.to_vec(),
166                            neighborhood,
167                            edges,
168                            truncated: true,
169                        });
170                    }
171                }
172            }
173        }
174
175        Ok(QueryResult {
176            seed_nodes: seed_nodes.to_vec(),
177            neighborhood,
178            edges,
179            truncated: false,
180        })
181    }
182
183    pub fn dijkstra_path(&self, src: &str, tgt: &str) -> Result<Option<(Vec<Node>, f64)>> {
184        if src == tgt {
185            return Ok(self.store.get_node(src)?.map(|n| (vec![n], 0.0)));
186        }
187
188        let all_nodes = self.store.get_all_nodes()?;
189        let all_edges = self.store.get_all_edges()?;
190
191        let mut dist: HashMap<&str, f64> = HashMap::new();
192        let mut prev: HashMap<&str, String> = HashMap::new();
193        let mut unvisited: HashSet<&str> = HashSet::new();
194
195        for node in &all_nodes {
196            dist.insert(node.id.as_str(), f64::INFINITY);
197            unvisited.insert(node.id.as_str());
198        }
199        dist.insert(src, 0.0);
200
201        while !unvisited.is_empty() {
202            let current = unvisited
203                .iter()
204                .min_by(|a, b| {
205                    dist.get(*a)
206                        .unwrap_or(&f64::INFINITY)
207                        .partial_cmp(dist.get(*b).unwrap_or(&f64::INFINITY))
208                        .unwrap_or(std::cmp::Ordering::Equal)
209                })
210                .cloned();
211
212            let current = match current {
213                Some(c) => c,
214                None => break,
215            };
216
217            if current == tgt {
218                break;
219            }
220
221            unvisited.remove(current);
222
223            let current_dist = *dist.get(current).unwrap_or(&f64::INFINITY);
224            if current_dist == f64::INFINITY {
225                break;
226            }
227
228            for edge in &all_edges {
229                let neighbor = if edge.source == current {
230                    Some(edge.target.as_str())
231                } else if edge.target == current {
232                    Some(edge.source.as_str())
233                } else {
234                    None
235                };
236
237                if let Some(neighbor) = neighbor {
238                    if unvisited.contains(neighbor) {
239                        let alt = current_dist + edge.weight;
240                        if alt < *dist.get(neighbor).unwrap_or(&f64::INFINITY) {
241                            dist.insert(neighbor, alt);
242                            prev.insert(neighbor, current.to_string());
243                        }
244                    }
245                }
246            }
247        }
248
249        if prev.contains_key(tgt) || src == tgt {
250            let mut path_nodes = Vec::new();
251            let mut current = tgt.to_string();
252            while current != src {
253                if let Some(node) = self.store.get_node(&current)? {
254                    path_nodes.push(node);
255                }
256                current = prev.get(current.as_str()).cloned().unwrap_or_default();
257            }
258            if let Some(node) = self.store.get_node(src)? {
259                path_nodes.push(node);
260            }
261            path_nodes.reverse();
262
263            let total_weight = *dist.get(tgt).unwrap_or(&f64::INFINITY);
264            Ok(Some((path_nodes, total_weight)))
265        } else {
266            Ok(None)
267        }
268    }
269
270    pub fn dfs(&self, src: &str, depth: usize, relation_filter: Option<&str>) -> Result<Vec<Node>> {
271        let mut visited: HashSet<NodeId> = HashSet::new();
272        let mut result: Vec<Node> = Vec::new();
273        let mut stack: Vec<(NodeId, usize)> = vec![(src.to_string(), 0)];
274
275        while let Some((node_id, d)) = stack.pop() {
276            if d > depth {
277                continue;
278            }
279            if !visited.insert(node_id.clone()) {
280                continue;
281            }
282            if let Some(node) = self.store.get_node(&node_id)? {
283                result.push(node);
284            }
285            if d < depth {
286                if let Ok(neighbors) = self.store.neighbors(&node_id, relation_filter) {
287                    for (neighbor, _) in &neighbors {
288                        if !visited.contains(&neighbor.id) {
289                            stack.push((neighbor.id.clone(), d + 1));
290                        }
291                    }
292                }
293            }
294        }
295
296        Ok(result)
297    }
298
299    pub fn get_node_by_label(&self, label: &str) -> Result<Option<Node>> {
300        let nodes = self.store.search(label, 1)?;
301        Ok(nodes.into_iter().next().map(|(_, n)| n))
302    }
303
304    pub fn get_node_by_id(&self, id: &str) -> Result<Option<Node>> {
305        self.store.get_node(id)
306    }
307
308    pub fn resolve_node(&self, id_or_label: &str) -> Result<Option<Node>> {
309        if let Some(node) = self.store.get_node(id_or_label)? {
310            return Ok(Some(node));
311        }
312        self.get_node_by_label(id_or_label)
313    }
314
315    pub fn get_neighbors(
316        &self,
317        id: &str,
318        relation_filter: Option<&str>,
319    ) -> Result<Vec<(Node, Edge)>> {
320        self.store.neighbors(id, relation_filter)
321    }
322
323    pub fn shortest_path(&self, src: &str, tgt: &str) -> Result<Option<Vec<Node>>> {
324        self.store.shortest_path(src, tgt)
325    }
326
327    pub fn graph_stats(&self) -> Result<String> {
328        let node_count = self.store.node_count()?;
329        let edge_count = self.store.edge_count()?;
330        Ok(format!("Nodes: {}, Edges: {}", node_count, edge_count))
331    }
332
333    pub fn get_community(&self, community_id: usize) -> Result<Vec<Node>> {
334        let nodes = self.store.get_all_nodes()?;
335        Ok(nodes
336            .into_iter()
337            .filter(|n| n.community == Some(community_id))
338            .collect())
339    }
340
341    pub fn god_nodes(&self, top_n: usize) -> Result<Vec<Node>> {
342        let edges = self.store.get_all_edges()?;
343        let all_nodes = self.store.get_all_nodes()?;
344        let mut degree: HashMap<NodeId, usize> = HashMap::new();
345        for edge in &edges {
346            *degree.entry(edge.source.clone()).or_insert(0) += 1;
347            *degree.entry(edge.target.clone()).or_insert(0) += 1;
348        }
349        let mut ranked: Vec<(usize, NodeId)> = degree.into_iter().map(|(k, v)| (v, k)).collect();
350        ranked.sort_by_key(|k| std::cmp::Reverse(k.0));
351        ranked.truncate(top_n);
352
353        let id_set: HashSet<&str> = ranked.iter().map(|(_, id)| id.as_str()).collect();
354        let mut result: Vec<Node> = all_nodes
355            .into_iter()
356            .filter(|n| id_set.contains(n.id.as_str()))
357            .collect();
358        result.sort_by(|a, b| {
359            let da = ranked
360                .iter()
361                .find(|(_, id)| id == &a.id)
362                .map(|(d, _)| *d)
363                .unwrap_or(0);
364            let db = ranked
365                .iter()
366                .find(|(_, id)| id == &b.id)
367                .map(|(d, _)| *d)
368                .unwrap_or(0);
369            db.cmp(&da)
370        });
371        Ok(result)
372    }
373
374    pub fn shortest_path_with_max_hops(
375        &self,
376        src: &str,
377        tgt: &str,
378        max_hops: usize,
379    ) -> Result<Option<Vec<Node>>> {
380        if src == tgt {
381            return self.store.get_node(src).map(|n| n.map(|n| vec![n]));
382        }
383
384        let mut visited: HashSet<NodeId> = HashSet::new();
385        let mut queue: VecDeque<(NodeId, usize)> = VecDeque::new();
386        let mut parent: HashMap<NodeId, NodeId> = HashMap::new();
387
388        visited.insert(src.to_string());
389        queue.push_back((src.to_string(), 0));
390
391        while let Some((current, depth)) = queue.pop_front() {
392            if depth >= max_hops {
393                continue;
394            }
395
396            let neighbors = self.store.neighbors(&current, None)?;
397            for (neighbor, _) in &neighbors {
398                if visited.insert(neighbor.id.clone()) {
399                    parent.insert(neighbor.id.clone(), current.clone());
400                    if neighbor.id == tgt {
401                        let mut path = Vec::new();
402                        let mut cur = tgt.to_string();
403                        while cur != src {
404                            if let Some(n) = self.store.get_node(&cur)? {
405                                path.push(n);
406                            }
407                            cur = parent.get(&cur).cloned().unwrap_or_default();
408                        }
409                        if let Some(n) = self.store.get_node(src)? {
410                            path.push(n);
411                        }
412                        path.reverse();
413                        return Ok(Some(path));
414                    }
415                    queue.push_back((neighbor.id.clone(), depth + 1));
416                }
417            }
418        }
419
420        Ok(None)
421    }
422
423    pub fn infer_context_filter(question: &str) -> Option<String> {
424        const CONTEXT_HINTS: &[(&str, &[&str])] = &[
425            (
426                "call",
427                &["call", "calls", "called", "invoke", "invokes", "invoked"],
428            ),
429            (
430                "import",
431                &["import", "imports", "imported", "module", "modules"],
432            ),
433            (
434                "field",
435                &[
436                    "field",
437                    "fields",
438                    "member",
439                    "members",
440                    "property",
441                    "properties",
442                ],
443            ),
444            (
445                "parameter_type",
446                &[
447                    "parameter",
448                    "parameters",
449                    "param",
450                    "params",
451                    "argument",
452                    "arguments",
453                ],
454            ),
455            ("return_type", &["return", "returns", "returned"]),
456            (
457                "generic_arg",
458                &["generic", "generics", "template", "templates"],
459            ),
460        ];
461        let tokens: std::collections::HashSet<String> = question
462            .split(|c: char| !c.is_alphanumeric())
463            .filter(|s| !s.is_empty())
464            .map(|s| s.to_lowercase())
465            .collect();
466        for (context, hints) in CONTEXT_HINTS {
467            if hints.iter().any(|h| tokens.contains(*h)) {
468                return Some(context.to_string());
469            }
470        }
471        None
472    }
473
474    pub fn resolve_context_filter(question: &str, explicit: Option<&str>) -> Option<String> {
475        const ALIASES: &[(&str, &str)] = &[
476            ("param", "parameter_type"),
477            ("params", "parameter_type"),
478            ("parameter", "parameter_type"),
479            ("parameters", "parameter_type"),
480            ("argument", "parameter_type"),
481            ("arguments", "parameter_type"),
482            ("arg", "parameter_type"),
483            ("args", "parameter_type"),
484            ("return", "return_type"),
485            ("returns", "return_type"),
486            ("returned", "return_type"),
487            ("generic", "generic_arg"),
488            ("generics", "generic_arg"),
489            ("template", "generic_arg"),
490            ("templates", "generic_arg"),
491            ("calls", "call"),
492            ("called", "call"),
493            ("invoke", "call"),
494            ("invocation", "call"),
495            ("fields", "field"),
496            ("property", "field"),
497            ("properties", "field"),
498            ("member", "field"),
499            ("members", "field"),
500            ("imports", "import"),
501            ("imported", "import"),
502            ("module", "import"),
503            ("modules", "import"),
504            ("exports", "export"),
505            ("exported", "export"),
506        ];
507        if let Some(exp) = explicit {
508            let key = exp.trim().to_lowercase();
509            let normalized = ALIASES
510                .iter()
511                .find(|(k, _)| *k == key)
512                .map(|(_, v)| v.to_string())
513                .unwrap_or(key);
514            return Some(normalized);
515        }
516        Self::infer_context_filter(question)
517    }
518
519    pub fn detailed_stats(&self) -> Result<String> {
520        let nodes = self.store.get_all_nodes()?;
521        let edges = self.store.get_all_edges()?;
522
523        let mut file_types: HashMap<&str, usize> = HashMap::new();
524        for node in &nodes {
525            let ext = node.source_file.rsplit('.').next().unwrap_or("unknown");
526            *file_types.entry(ext).or_insert(0) += 1;
527        }
528
529        let mut rel_types: HashMap<&str, usize> = HashMap::new();
530        for edge in &edges {
531            *rel_types.entry(edge.relation.as_str()).or_insert(0) += 1;
532        }
533
534        let mut stats = String::new();
535        stats.push_str(&format!("Nodes: {}, Edges: {}\n", nodes.len(), edges.len()));
536        stats.push_str(&format!("File types: {:?}\n", file_types));
537        stats.push_str(&format!("Relation types: {:?}\n", rel_types));
538        Ok(stats)
539    }
540}
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545    use crate::types::Node;
546    use std::collections::HashMap;
547    use std::sync::Mutex;
548
549    struct MockStore {
550        nodes: Mutex<HashMap<NodeId, Node>>,
551        edges: Mutex<Vec<Edge>>,
552    }
553
554    impl MockStore {
555        fn new() -> Self {
556            MockStore {
557                nodes: Mutex::new(HashMap::new()),
558                edges: Mutex::new(Vec::new()),
559            }
560        }
561    }
562
563    impl GraphStore for MockStore {
564        fn dijkstra_shortest_path(&self, _src: &str, _tgt: &str) -> Result<Option<Vec<Node>>> {
565            Ok(None)
566        }
567
568        fn add_node(&self, node: Node) -> Result<()> {
569            self.nodes.lock().unwrap().insert(node.id.clone(), node);
570            Ok(())
571        }
572
573        fn add_edge(&self, edge: Edge) -> Result<()> {
574            self.edges.lock().unwrap().push(edge);
575            Ok(())
576        }
577
578        fn get_node(&self, id: &str) -> Result<Option<Node>> {
579            Ok(self.nodes.lock().unwrap().get(id).cloned())
580        }
581
582        fn get_all_nodes(&self) -> Result<Vec<Node>> {
583            Ok(self.nodes.lock().unwrap().values().cloned().collect())
584        }
585
586        fn get_all_edges(&self) -> Result<Vec<Edge>> {
587            Ok(self.edges.lock().unwrap().clone())
588        }
589
590        fn neighbors(&self, id: &str, filter: Option<&str>) -> Result<Vec<(Node, Edge)>> {
591            let edges = self.edges.lock().unwrap();
592            let nodes = self.nodes.lock().unwrap();
593            let mut result = Vec::new();
594            for edge in edges.iter() {
595                if let Some(f) = filter {
596                    if edge.relation != f {
597                        continue;
598                    }
599                }
600                if edge.source == id {
601                    if let Some(node) = nodes.get(&edge.target) {
602                        result.push((node.clone(), edge.clone()));
603                    }
604                } else if edge.target == id {
605                    if let Some(node) = nodes.get(&edge.source) {
606                        result.push((node.clone(), edge.clone()));
607                    }
608                }
609            }
610            Ok(result)
611        }
612
613        fn search(&self, query: &str, _top_k: usize) -> Result<Vec<(f64, Node)>> {
614            let nodes = self.nodes.lock().unwrap();
615            let q = query.to_lowercase();
616            let mut results = Vec::new();
617            for node in nodes.values() {
618                if node.label.to_lowercase().contains(&q) || node.id.to_lowercase().contains(&q) {
619                    results.push((1.0, node.clone()));
620                }
621            }
622            Ok(results)
623        }
624
625        fn shortest_path(&self, src: &str, tgt: &str) -> Result<Option<Vec<Node>>> {
626            if src == tgt {
627                return Ok(self
628                    .nodes
629                    .lock()
630                    .unwrap()
631                    .get(src)
632                    .cloned()
633                    .map(|n| vec![n]));
634            }
635
636            let nodes = self.nodes.lock().unwrap();
637            let edges = self.edges.lock().unwrap();
638
639            let mut visited = HashSet::new();
640            let mut queue = VecDeque::new();
641            let mut parent: HashMap<String, String> = HashMap::new();
642
643            visited.insert(src.to_string());
644            queue.push_back(src.to_string());
645
646            while let Some(current) = queue.pop_front() {
647                if current == tgt {
648                    let mut path = Vec::new();
649                    let mut cur = tgt.to_string();
650                    while cur != src {
651                        if let Some(n) = nodes.get(&cur) {
652                            path.push(n.clone());
653                        }
654                        cur = parent.get(&cur).cloned().unwrap_or_default();
655                    }
656                    if let Some(n) = nodes.get(src) {
657                        path.push(n.clone());
658                    }
659                    path.reverse();
660                    return Ok(Some(path));
661                }
662
663                for edge in edges.iter() {
664                    let neighbor = if edge.source == current {
665                        Some(edge.target.clone())
666                    } else if edge.target == current {
667                        Some(edge.source.clone())
668                    } else {
669                        None
670                    };
671                    if let Some(neighbor) = neighbor {
672                        if visited.insert(neighbor.clone()) {
673                            parent.insert(neighbor.clone(), current.clone());
674                            queue.push_back(neighbor);
675                        }
676                    }
677                }
678            }
679
680            Ok(None)
681        }
682
683        fn subgraph(&self, _node_ids: &[&str]) -> Result<(Vec<Node>, Vec<Edge>)> {
684            Ok((vec![], vec![]))
685        }
686
687        fn node_count(&self) -> Result<usize> {
688            Ok(self.nodes.lock().unwrap().len())
689        }
690
691        fn edge_count(&self) -> Result<usize> {
692            Ok(self.edges.lock().unwrap().len())
693        }
694
695        fn remove_node(&self, id: &str) -> Result<()> {
696            self.nodes.lock().unwrap().remove(id);
697            Ok(())
698        }
699
700        fn remove_edge(&self, source: &str, target: &str, relation: &str) -> Result<()> {
701            let mut edges = self.edges.lock().unwrap();
702            edges.retain(|e| !(e.source == source && e.target == target && e.relation == relation));
703            Ok(())
704        }
705
706        fn clear(&self) -> Result<()> {
707            self.nodes.lock().unwrap().clear();
708            self.edges.lock().unwrap().clear();
709            Ok(())
710        }
711    }
712
713    fn make_node(id: &str, label: &str) -> Node {
714        Node {
715            id: id.to_string(),
716            label: label.to_string(),
717            file_type: "code".to_string(),
718            source_file: "test.py".to_string(),
719            source_location: None,
720            community: None,
721            rationale: None,
722            docstring: None,
723            metadata: HashMap::new(),
724        }
725    }
726
727    fn make_edge(src: &str, tgt: &str, relation: &str, weight: f64) -> Edge {
728        Edge {
729            source: src.to_string(),
730            target: tgt.to_string(),
731            relation: relation.to_string(),
732            confidence: "EXTRACTED".to_string(),
733            source_file: Some("test.py".to_string()),
734            weight,
735            context: None,
736        }
737    }
738
739    #[test]
740    fn test_get_node_by_id() {
741        let store = MockStore::new();
742        store.add_node(make_node("auth", "AuthService")).unwrap();
743        let engine = QueryEngine::new(Box::new(store));
744        let node = engine.get_node_by_id("auth").unwrap();
745        assert!(node.is_some());
746        assert_eq!(node.unwrap().label, "AuthService");
747    }
748
749    #[test]
750    fn test_get_node_not_found() {
751        let store = MockStore::new();
752        let engine = QueryEngine::new(Box::new(store));
753        let node = engine.get_node_by_id("nonexistent").unwrap();
754        assert!(node.is_none());
755    }
756
757    #[test]
758    fn test_graph_stats() {
759        let store = MockStore::new();
760        store.add_node(make_node("a", "A")).unwrap();
761        store.add_node(make_node("b", "B")).unwrap();
762        let engine = QueryEngine::new(Box::new(store));
763        let stats = engine.graph_stats().unwrap();
764        assert!(stats.contains("Nodes: 2"));
765    }
766
767    #[test]
768    fn test_detailed_stats() {
769        let store = MockStore::new();
770        store.add_node(make_node("a", "A")).unwrap();
771        store.add_node(make_node("b", "B")).unwrap();
772        store
773            .add_edge(make_edge("a", "b", "connects", 1.0))
774            .unwrap();
775        let engine = QueryEngine::new(Box::new(store));
776        let stats = engine.detailed_stats().unwrap();
777        assert!(stats.contains("Nodes: 2"));
778        assert!(stats.contains("Edges: 1"));
779    }
780
781    #[test]
782    fn test_bfs_traversal() {
783        let store = MockStore::new();
784        store.add_node(make_node("a", "A")).unwrap();
785        store.add_node(make_node("b", "B")).unwrap();
786        store.add_node(make_node("c", "C")).unwrap();
787        store
788            .add_edge(make_edge("a", "b", "connects", 1.0))
789            .unwrap();
790        store
791            .add_edge(make_edge("b", "c", "connects", 1.0))
792            .unwrap();
793
794        let engine = QueryEngine::new(Box::new(store));
795        let result = engine.query_text("a", "bfs", 2, None, None).unwrap();
796        assert_eq!(result.seed_nodes.len(), 1);
797        assert_eq!(result.seed_nodes[0].id, "a");
798        assert!(result.neighborhood.len() >= 2);
799    }
800
801    #[test]
802    fn test_dfs_traversal() {
803        let store = MockStore::new();
804        store.add_node(make_node("a", "A")).unwrap();
805        store.add_node(make_node("b", "B")).unwrap();
806        store.add_node(make_node("c", "C")).unwrap();
807        store
808            .add_edge(make_edge("a", "b", "connects", 1.0))
809            .unwrap();
810        store
811            .add_edge(make_edge("b", "c", "connects", 1.0))
812            .unwrap();
813
814        let engine = QueryEngine::new(Box::new(store));
815        let result = engine.query_text("a", "dfs", 2, None, None).unwrap();
816        assert_eq!(result.seed_nodes.len(), 1);
817        assert!(result.neighborhood.len() >= 2);
818    }
819
820    #[test]
821    fn test_dfs_method() {
822        let store = MockStore::new();
823        store.add_node(make_node("a", "A")).unwrap();
824        store.add_node(make_node("b", "B")).unwrap();
825        store.add_node(make_node("c", "C")).unwrap();
826        store
827            .add_edge(make_edge("a", "b", "connects", 1.0))
828            .unwrap();
829        store
830            .add_edge(make_edge("b", "c", "connects", 1.0))
831            .unwrap();
832
833        let engine = QueryEngine::new(Box::new(store));
834        let nodes = engine.dfs("a", 3, None).unwrap();
835        assert_eq!(nodes.len(), 3);
836    }
837
838    #[test]
839    fn test_dijkstra_path() {
840        let store = MockStore::new();
841        store.add_node(make_node("a", "A")).unwrap();
842        store.add_node(make_node("b", "B")).unwrap();
843        store.add_node(make_node("c", "C")).unwrap();
844        store
845            .add_edge(make_edge("a", "b", "connects", 1.5))
846            .unwrap();
847        store
848            .add_edge(make_edge("b", "c", "connects", 2.5))
849            .unwrap();
850
851        let engine = QueryEngine::new(Box::new(store));
852        let result = engine.dijkstra_path("a", "c").unwrap();
853        assert!(result.is_some());
854        let (path, weight) = result.unwrap();
855        assert_eq!(path.len(), 3);
856        assert!((weight - 4.0).abs() < 0.01);
857    }
858
859    #[test]
860    fn test_dijkstra_path_same_node() {
861        let store = MockStore::new();
862        store.add_node(make_node("a", "A")).unwrap();
863        let engine = QueryEngine::new(Box::new(store));
864        let result = engine.dijkstra_path("a", "a").unwrap();
865        assert!(result.is_some());
866        let (path, weight) = result.unwrap();
867        assert_eq!(path.len(), 1);
868        assert!((weight - 0.0).abs() < 0.01);
869    }
870
871    #[test]
872    fn test_dijkstra_path_no_path() {
873        let store = MockStore::new();
874        store.add_node(make_node("a", "A")).unwrap();
875        store.add_node(make_node("b", "B")).unwrap();
876        let engine = QueryEngine::new(Box::new(store));
877        let result = engine.dijkstra_path("a", "b").unwrap();
878        assert!(result.is_none());
879    }
880
881    #[test]
882    fn test_context_filter() {
883        let store = MockStore::new();
884        store.add_node(make_node("a", "A")).unwrap();
885        store.add_node(make_node("b", "B")).unwrap();
886        store.add_node(make_node("c", "C")).unwrap();
887        store.add_edge(make_edge("a", "b", "imports", 1.0)).unwrap();
888        store.add_edge(make_edge("a", "c", "calls", 1.0)).unwrap();
889
890        let engine = QueryEngine::new(Box::new(store));
891        let neighbors = engine.get_neighbors("a", Some("imports")).unwrap();
892        assert_eq!(neighbors.len(), 1);
893        assert_eq!(neighbors[0].0.label, "B");
894    }
895
896    #[test]
897    fn test_token_budget() {
898        let store = MockStore::new();
899        store.add_node(make_node("a", "A")).unwrap();
900        let engine = QueryEngine::new(Box::new(store));
901        let result = engine.query_text("a", "bfs", 1, Some(5), None).unwrap();
902        assert_eq!(result.seed_nodes.len(), 1);
903    }
904
905    // --- Gap test #102: exact match ---
906
907    #[test]
908    fn test_query_exact_match() {
909        let store = MockStore::new();
910        store.add_node(make_node("a1", "AuthService")).unwrap();
911        store.add_node(make_node("u1", "UserService")).unwrap();
912        let engine = QueryEngine::new(Box::new(store));
913        let result = engine
914            .query_text("AuthService", "bfs", 0, None, None)
915            .unwrap();
916        assert_eq!(result.seed_nodes.len(), 1);
917        assert_eq!(result.seed_nodes[0].label, "AuthService");
918    }
919
920    // --- Gap test #103: prefix match ---
921
922    #[test]
923    fn test_query_prefix_match() {
924        let store = MockStore::new();
925        store.add_node(make_node("a1", "AuthService")).unwrap();
926        store.add_node(make_node("a2", "AuthMiddleware")).unwrap();
927        store.add_node(make_node("u1", "UserService")).unwrap();
928        let engine = QueryEngine::new(Box::new(store));
929        let result = engine.query_text("Auth", "bfs", 0, None, None).unwrap();
930        assert_eq!(result.seed_nodes.len(), 2);
931        for n in &result.seed_nodes {
932            assert!(n.label.starts_with("Auth"));
933        }
934    }
935
936    // --- Gap test #104: substring match ---
937
938    #[test]
939    fn test_query_substring_match() {
940        let store = MockStore::new();
941        store.add_node(make_node("a1", "AuthService")).unwrap();
942        store.add_node(make_node("u1", "UserService")).unwrap();
943        store.add_node(make_node("r1", "RateLimiter")).unwrap();
944        let engine = QueryEngine::new(Box::new(store));
945        let result = engine.query_text("Service", "bfs", 0, None, None).unwrap();
946        assert_eq!(result.seed_nodes.len(), 2);
947        for n in &result.seed_nodes {
948            assert!(n.label.contains("Service"));
949        }
950    }
951
952    // --- Gap test #105-106: BFS depth ---
953
954    #[test]
955    fn test_query_bfs_depth_1() {
956        let store = MockStore::new();
957        store.add_node(make_node("a", "A")).unwrap();
958        store.add_node(make_node("b", "B")).unwrap();
959        store.add_node(make_node("c", "C")).unwrap();
960        store
961            .add_edge(make_edge("a", "b", "connects", 1.0))
962            .unwrap();
963        store
964            .add_edge(make_edge("b", "c", "connects", 1.0))
965            .unwrap();
966        let engine = QueryEngine::new(Box::new(store));
967        let result = engine.query_text("a", "bfs", 1, None, None).unwrap();
968        assert_eq!(
969            result.neighborhood.len(),
970            2,
971            "depth 1: seed + immediate neighbor"
972        );
973        assert!(result.neighborhood.iter().any(|n| n.id == "b"));
974        assert!(
975            !result.neighborhood.iter().any(|n| n.id == "c"),
976            "C should not be reachable at depth 1"
977        );
978    }
979
980    #[test]
981    fn test_query_bfs_depth_3() {
982        let store = MockStore::new();
983        store.add_node(make_node("a", "A")).unwrap();
984        store.add_node(make_node("b", "B")).unwrap();
985        store.add_node(make_node("c", "C")).unwrap();
986        store
987            .add_edge(make_edge("a", "b", "connects", 1.0))
988            .unwrap();
989        store
990            .add_edge(make_edge("b", "c", "connects", 1.0))
991            .unwrap();
992        let engine = QueryEngine::new(Box::new(store));
993        let result = engine.query_text("a", "bfs", 3, None, None).unwrap();
994        assert_eq!(result.neighborhood.len(), 3, "depth 3: all nodes reachable");
995        assert!(
996            result.neighborhood.iter().any(|n| n.id == "c"),
997            "C should be reachable at depth 3"
998        );
999    }
1000
1001    // --- Gap test #108: context filter in query ---
1002
1003    #[test]
1004    fn test_query_context_filter_call() {
1005        let store = MockStore::new();
1006        store.add_node(make_node("a", "A")).unwrap();
1007        store.add_node(make_node("b", "B")).unwrap();
1008        store.add_node(make_node("c", "C")).unwrap();
1009        store.add_edge(make_edge("a", "b", "imports", 1.0)).unwrap();
1010        store.add_edge(make_edge("a", "c", "calls", 1.0)).unwrap();
1011        let engine = QueryEngine::new(Box::new(store));
1012        let result = engine
1013            .query_text("a", "bfs", 1, None, Some("calls"))
1014            .unwrap();
1015        assert_eq!(result.seed_nodes.len(), 1);
1016        let neighbor_ids: Vec<&str> = result.neighborhood.iter().map(|n| n.id.as_str()).collect();
1017        assert!(
1018            neighbor_ids.contains(&"c"),
1019            "call target should be in neighborhood"
1020        );
1021        assert!(
1022            !neighbor_ids.contains(&"b"),
1023            "import target should be filtered out"
1024        );
1025    }
1026
1027    // --- Gap test #109: context filter inferred from question ---
1028
1029    #[test]
1030    fn test_query_context_filter_inferred() {
1031        assert_eq!(
1032            QueryEngine::infer_context_filter("who calls AuthService"),
1033            Some("call".to_string())
1034        );
1035        assert_eq!(
1036            QueryEngine::infer_context_filter("what imports UserModule"),
1037            Some("import".to_string())
1038        );
1039        assert_eq!(
1040            QueryEngine::infer_context_filter("list fields of Foo"),
1041            Some("field".to_string())
1042        );
1043        assert_eq!(
1044            QueryEngine::infer_context_filter("show me AuthService"),
1045            None
1046        );
1047    }
1048
1049    // --- Gap test #110: explicit context filter overrides inferred ---
1050
1051    #[test]
1052    fn test_query_context_filter_explicit() {
1053        assert_eq!(
1054            QueryEngine::resolve_context_filter("who calls AuthService", Some("imports")),
1055            Some("import".to_string())
1056        );
1057        assert_eq!(
1058            QueryEngine::resolve_context_filter("who calls AuthService", None),
1059            Some("call".to_string())
1060        );
1061        assert_eq!(
1062            QueryEngine::resolve_context_filter("show me AuthService", None),
1063            None
1064        );
1065    }
1066
1067    // --- Gap test #112: get_node_by_label ---
1068
1069    #[test]
1070    fn test_get_node_by_label() {
1071        let store = MockStore::new();
1072        store.add_node(make_node("a1", "AuthService")).unwrap();
1073        let engine = QueryEngine::new(Box::new(store));
1074        let node = engine.get_node_by_label("AuthService").unwrap();
1075        assert!(node.is_some());
1076        assert_eq!(node.unwrap().label, "AuthService");
1077    }
1078
1079    // --- Gap test #115: get_neighbors_basic ---
1080
1081    #[test]
1082    fn test_get_neighbors_basic() {
1083        let store = MockStore::new();
1084        store.add_node(make_node("a", "A")).unwrap();
1085        store.add_node(make_node("b", "B")).unwrap();
1086        store.add_node(make_node("c", "C")).unwrap();
1087        store
1088            .add_edge(make_edge("a", "b", "connects", 1.0))
1089            .unwrap();
1090        store.add_edge(make_edge("a", "c", "calls", 1.0)).unwrap();
1091        let engine = QueryEngine::new(Box::new(store));
1092        let neighbors = engine.get_neighbors("a", None).unwrap();
1093        assert_eq!(neighbors.len(), 2);
1094    }
1095
1096    // --- Gap test #117: get_community ---
1097
1098    #[test]
1099    fn test_get_community() {
1100        let store = MockStore::new();
1101        let mut n1 = make_node("a", "A");
1102        n1.community = Some(1);
1103        let mut n2 = make_node("b", "B");
1104        n2.community = Some(1);
1105        let n3 = make_node("c", "C");
1106        store.add_node(n1).unwrap();
1107        store.add_node(n2).unwrap();
1108        store.add_node(n3).unwrap();
1109        let engine = QueryEngine::new(Box::new(store));
1110        let members = engine.get_community(1).unwrap();
1111        assert_eq!(members.len(), 2);
1112        for n in &members {
1113            assert_eq!(n.community, Some(1));
1114        }
1115    }
1116
1117    // --- Gap test #118: god_nodes ---
1118
1119    #[test]
1120    fn test_god_nodes() {
1121        let store = MockStore::new();
1122        store.add_node(make_node("a", "A")).unwrap();
1123        store.add_node(make_node("b", "B")).unwrap();
1124        store.add_node(make_node("c", "C")).unwrap();
1125        store
1126            .add_edge(make_edge("a", "b", "connects", 1.0))
1127            .unwrap();
1128        store
1129            .add_edge(make_edge("a", "c", "connects", 1.0))
1130            .unwrap();
1131        store
1132            .add_edge(make_edge("b", "c", "connects", 1.0))
1133            .unwrap();
1134        let engine = QueryEngine::new(Box::new(store));
1135        let gods = engine.god_nodes(2).unwrap();
1136        assert_eq!(gods.len(), 2);
1137    }
1138
1139    // --- Gap test #120: shortest_path_found ---
1140
1141    #[test]
1142    fn test_shortest_path_found() {
1143        let store = MockStore::new();
1144        store.add_node(make_node("a", "A")).unwrap();
1145        store.add_node(make_node("b", "B")).unwrap();
1146        store.add_node(make_node("c", "C")).unwrap();
1147        store
1148            .add_edge(make_edge("a", "b", "connects", 1.0))
1149            .unwrap();
1150        store
1151            .add_edge(make_edge("b", "c", "connects", 1.0))
1152            .unwrap();
1153        let engine = QueryEngine::new(Box::new(store));
1154        let path = engine.shortest_path("a", "c").unwrap();
1155        assert!(path.is_some());
1156        assert_eq!(path.unwrap().len(), 3);
1157    }
1158
1159    // --- Gap test #123: shortest_path_max_hops ---
1160
1161    #[test]
1162    fn test_shortest_path_max_hops() {
1163        let store = MockStore::new();
1164        store.add_node(make_node("a", "A")).unwrap();
1165        store.add_node(make_node("b", "B")).unwrap();
1166        store.add_node(make_node("c", "C")).unwrap();
1167        store.add_node(make_node("d", "D")).unwrap();
1168        store
1169            .add_edge(make_edge("a", "b", "connects", 1.0))
1170            .unwrap();
1171        store
1172            .add_edge(make_edge("b", "c", "connects", 1.0))
1173            .unwrap();
1174        store
1175            .add_edge(make_edge("c", "d", "connects", 1.0))
1176            .unwrap();
1177
1178        let engine = QueryEngine::new(Box::new(store));
1179
1180        // Path a-d requires 3 hops, max_hops=2 → not found
1181        let limited = engine.shortest_path_with_max_hops("a", "d", 2).unwrap();
1182        assert!(limited.is_none(), "path exceeds max_hops");
1183
1184        // Path a-d with max_hops=3 → found
1185        let found = engine.shortest_path_with_max_hops("a", "d", 3).unwrap();
1186        assert!(found.is_some());
1187        assert_eq!(found.unwrap().len(), 4);
1188    }
1189
1190    #[test]
1191    fn test_resolve_node_by_label_when_id_missing() {
1192        let store = MockStore::new();
1193        store
1194            .add_node(make_node("api_authservice", "AuthService"))
1195            .unwrap();
1196        let engine = QueryEngine::new(Box::new(store));
1197
1198        // Exact id lookup fails — should fall back to label search
1199        let node = engine.resolve_node("AuthService").unwrap();
1200        assert!(node.is_some(), "resolve_node must find node by label");
1201        assert_eq!(node.unwrap().id, "api_authservice");
1202    }
1203
1204    #[test]
1205    fn test_resolve_node_by_exact_id() {
1206        let store = MockStore::new();
1207        store
1208            .add_node(make_node("api_authservice", "AuthService"))
1209            .unwrap();
1210        let engine = QueryEngine::new(Box::new(store));
1211
1212        let node = engine.resolve_node("api_authservice").unwrap();
1213        assert!(node.is_some());
1214        assert_eq!(node.unwrap().id, "api_authservice");
1215    }
1216
1217    #[test]
1218    fn test_resolve_node_not_found() {
1219        let store = MockStore::new();
1220        let engine = QueryEngine::new(Box::new(store));
1221        let node = engine.resolve_node("ghost").unwrap();
1222        assert!(node.is_none());
1223    }
1224}