1use std::collections::{HashMap, HashSet, VecDeque};
8
9use crate::graph::code_graph::CodeGraph;
10use crate::graph::traversal::{self, Direction, TraversalOptions};
11use crate::types::{
12 AcbError, AcbResult, CodeUnit, CodeUnitType, EdgeType, Language, Span, Visibility,
13};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum MatchMode {
22 Exact,
24 Prefix,
26 Contains,
28 Fuzzy,
30}
31
32#[derive(Debug, Clone)]
34pub struct SymbolLookupParams {
35 pub name: String,
37 pub mode: MatchMode,
39 pub unit_types: Vec<CodeUnitType>,
41 pub languages: Vec<Language>,
43 pub limit: usize,
45 pub fuzzy_threshold: usize,
47}
48
49impl Default for SymbolLookupParams {
50 fn default() -> Self {
51 Self {
52 name: String::new(),
53 mode: MatchMode::Exact,
54 unit_types: Vec::new(),
55 languages: Vec::new(),
56 limit: 0,
57 fuzzy_threshold: 2,
58 }
59 }
60}
61
62#[derive(Debug, Clone)]
64pub struct DependencyParams {
65 pub unit_id: u64,
67 pub max_depth: u32,
69 pub edge_types: Vec<EdgeType>,
71 pub include_transitive: bool,
73}
74
75#[derive(Debug, Clone)]
77pub struct DependencyNode {
78 pub unit_id: u64,
80 pub depth: u32,
82 pub path: Vec<u64>,
84}
85
86#[derive(Debug, Clone)]
88pub struct DependencyResult {
89 pub root_id: u64,
91 pub nodes: Vec<DependencyNode>,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
97pub enum CallDirection {
98 Callers,
100 Callees,
102 Both,
104}
105
106#[derive(Debug, Clone)]
108pub struct CallGraphParams {
109 pub unit_id: u64,
111 pub direction: CallDirection,
113 pub max_depth: u32,
115}
116
117#[derive(Debug, Clone)]
119pub struct CallSite {
120 pub caller_id: u64,
122 pub callee_id: u64,
124 pub span: Span,
126}
127
128#[derive(Debug, Clone)]
130pub struct CallGraphResult {
131 pub root_id: u64,
133 pub nodes: Vec<(u64, u32)>,
135 pub call_sites: Vec<CallSite>,
137}
138
139#[derive(Debug, Clone)]
141pub struct HierarchyParams {
142 pub unit_id: u64,
144 pub include_ancestors: bool,
146 pub include_descendants: bool,
148}
149
150#[derive(Debug, Clone)]
152pub struct HierarchyNode {
153 pub unit_id: u64,
155 pub relation: EdgeType,
157 pub depth: i32,
159}
160
161#[derive(Debug, Clone)]
163pub struct HierarchyResult {
164 pub root_id: u64,
166 pub nodes: Vec<HierarchyNode>,
168}
169
170#[derive(Debug, Clone)]
172pub struct PatternParams {
173 pub pattern: String,
181}
182
183#[derive(Debug, Clone)]
185pub struct PatternMatch {
186 pub unit_id: u64,
188 pub confidence: f32,
190 pub matched_rule: String,
192}
193
194#[derive(Debug, Clone)]
196pub struct SemanticParams {
197 pub query_vec: Vec<f32>,
199 pub top_k: usize,
201 pub unit_types: Vec<CodeUnitType>,
203 pub languages: Vec<Language>,
205 pub min_similarity: f32,
207}
208
209#[derive(Debug, Clone)]
211pub struct SemanticMatch {
212 pub unit_id: u64,
214 pub score: f32,
216}
217
218#[derive(Debug, Clone)]
220pub struct ImpactParams {
221 pub unit_id: u64,
223 pub max_depth: u32,
225 pub edge_types: Vec<EdgeType>,
227}
228
229#[derive(Debug, Clone)]
231pub struct ImpactedUnit {
232 pub unit_id: u64,
234 pub depth: u32,
236 pub risk_score: f32,
238 pub has_tests: bool,
240}
241
242#[derive(Debug, Clone)]
244pub struct ImpactResult {
245 pub root_id: u64,
247 pub impacted: Vec<ImpactedUnit>,
249 pub overall_risk: f32,
251 pub recommendations: Vec<String>,
253}
254
255#[derive(Debug, Clone)]
257pub struct CoverageResult {
258 pub unit_id: u64,
260 pub direct_tests: Vec<u64>,
262 pub indirect_tests: Vec<u64>,
264 pub coverage_ratio: f32,
266}
267
268#[derive(Debug, Clone)]
270pub struct TraceParams {
271 pub unit_id: u64,
273 pub max_hops: u32,
275}
276
277#[derive(Debug, Clone)]
279pub struct TraceHop {
280 pub unit_id: u64,
282 pub language: Language,
284 pub via_edge: Option<EdgeType>,
286}
287
288#[derive(Debug, Clone)]
290pub struct TraceResult {
291 pub hops: Vec<TraceHop>,
293 pub languages_crossed: Vec<Language>,
295}
296
297#[derive(Debug, Clone)]
299pub struct CollectiveParams {
300 pub unit_type: Option<CodeUnitType>,
302 pub min_usage: u64,
304 pub limit: usize,
306}
307
308#[derive(Debug, Clone)]
310pub struct CollectivePatternEntry {
311 pub unit_id: u64,
313 pub usage_count: u64,
315 pub confidence: f32,
317}
318
319#[derive(Debug, Clone)]
321pub struct CollectiveResult {
322 pub patterns: Vec<CollectivePatternEntry>,
324 pub collective_available: bool,
326}
327
328#[derive(Debug, Clone)]
330pub struct EvolutionResult {
331 pub unit_id: u64,
333 pub change_count: u32,
335 pub created_at: u64,
337 pub last_modified: u64,
339 pub stability_score: f32,
341 pub trend: String,
343}
344
345#[derive(Debug, Clone)]
347pub struct StabilityFactor {
348 pub name: String,
350 pub value: f32,
352 pub description: String,
354}
355
356#[derive(Debug, Clone)]
358pub struct StabilityResult {
359 pub unit_id: u64,
361 pub overall_score: f32,
363 pub factors: Vec<StabilityFactor>,
365 pub recommendation: String,
367}
368
369#[derive(Debug, Clone)]
371pub struct CouplingParams {
372 pub unit_id: Option<u64>,
374 pub min_strength: f32,
376}
377
378#[derive(Debug, Clone)]
380pub struct Coupling {
381 pub unit_a: u64,
383 pub unit_b: u64,
385 pub strength: f32,
387 pub kind: CouplingKind,
389}
390
391#[derive(Debug, Clone, PartialEq, Eq)]
393pub enum CouplingKind {
394 Explicit,
396 Temporal,
398 Hidden,
400}
401
402#[derive(Debug, Clone)]
404pub struct DeadCodeParams {
405 pub unit_types: Vec<CodeUnitType>,
407 pub include_tests_as_roots: bool,
409}
410
411#[derive(Debug, Clone)]
413pub struct ProphecyParams {
414 pub top_k: usize,
416 pub min_risk: f32,
418}
419
420#[derive(Debug, Clone)]
422pub struct Prediction {
423 pub unit_id: u64,
425 pub risk_score: f32,
427 pub reason: String,
429}
430
431#[derive(Debug, Clone)]
433pub struct ProphecyResult {
434 pub predictions: Vec<Prediction>,
436}
437
438#[derive(Debug, Clone)]
440pub struct ConceptUnit {
441 pub unit_id: u64,
443 pub role: ConceptRole,
445 pub relevance: f32,
447}
448
449#[derive(Debug, Clone, PartialEq, Eq)]
451pub enum ConceptRole {
452 Definition,
454 Usage,
456 Extension,
458 Test,
460}
461
462#[derive(Debug, Clone)]
464pub struct ConceptMap {
465 pub concept: String,
467 pub units: Vec<ConceptUnit>,
469}
470
471#[derive(Debug, Clone)]
473pub struct MigrationParams {
474 pub from_unit: u64,
476 pub to_unit: u64,
478}
479
480#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
482pub enum SafetyLevel {
483 Safe,
485 Caution,
487 Risky,
489}
490
491#[derive(Debug, Clone)]
493pub struct MigrationStep {
494 pub unit_id: u64,
496 pub order: u32,
498 pub safety: SafetyLevel,
500 pub description: String,
502}
503
504#[derive(Debug, Clone)]
506pub struct MigrationPlan {
507 pub from_unit: u64,
509 pub to_unit: u64,
511 pub steps: Vec<MigrationStep>,
513}
514
515#[derive(Debug, Clone)]
517pub struct TestGapParams {
518 pub min_changes: u32,
520 pub min_complexity: u32,
522 pub unit_types: Vec<CodeUnitType>,
524}
525
526#[derive(Debug, Clone)]
528pub struct TestGap {
529 pub unit_id: u64,
531 pub reason: String,
533 pub priority: f32,
535}
536
537#[derive(Debug, Clone)]
539pub struct DriftParams {
540 pub rules: Vec<ArchRule>,
542}
543
544#[derive(Debug, Clone)]
546pub enum ArchRule {
547 LayerDependency {
549 upper: String,
551 lower: String,
553 },
554 ModuleBoundary {
556 module: String,
558 },
559 NamingConvention {
561 prefix: String,
563 pattern: String,
565 },
566 Cyclic {
568 scope: String,
570 },
571}
572
573#[derive(Debug, Clone)]
575pub struct DriftViolation {
576 pub rule_index: usize,
578 pub description: String,
580 pub units: Vec<u64>,
582}
583
584#[derive(Debug, Clone)]
586pub struct DriftReport {
587 pub violations: Vec<DriftViolation>,
589 pub conformance_score: f32,
591}
592
593#[derive(Debug, Clone)]
595pub struct SimilarityParams {
596 pub unit_id: u64,
598 pub top_k: usize,
600 pub min_similarity: f32,
602}
603
604#[derive(Debug, Clone)]
606pub struct SimilarityMatch {
607 pub unit_id: u64,
609 pub score: f32,
611}
612
613#[derive(Debug, Clone)]
615pub struct PathResult {
616 pub found: bool,
618 pub path: Vec<u64>,
620 pub edge_types: Vec<EdgeType>,
622 pub length: usize,
624}
625
626#[derive(Debug, Clone)]
628pub struct HotspotParams {
629 pub top_k: usize,
631 pub min_score: f32,
633 pub unit_types: Vec<CodeUnitType>,
635}
636
637#[derive(Debug, Clone)]
639pub struct Hotspot {
640 pub unit_id: u64,
642 pub score: f32,
644 pub factors: HashMap<String, f32>,
646}
647
648fn levenshtein(a: &str, b: &str) -> usize {
654 let a_len = a.len();
655 let b_len = b.len();
656
657 if a_len == 0 {
658 return b_len;
659 }
660 if b_len == 0 {
661 return a_len;
662 }
663
664 let a_bytes = a.as_bytes();
665 let b_bytes = b.as_bytes();
666
667 let mut prev = vec![0usize; b_len + 1];
669 let mut curr = vec![0usize; b_len + 1];
670
671 for (j, item) in prev.iter_mut().enumerate().take(b_len + 1) {
672 *item = j;
673 }
674
675 for i in 1..=a_len {
676 curr[0] = i;
677 for j in 1..=b_len {
678 let cost = if a_bytes[i - 1] == b_bytes[j - 1] {
679 0
680 } else {
681 1
682 };
683 curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
684 }
685 std::mem::swap(&mut prev, &mut curr);
686 }
687
688 prev[b_len]
689}
690
691fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
693 if a.len() != b.len() || a.is_empty() {
694 return 0.0;
695 }
696
697 let mut dot = 0.0_f64;
698 let mut norm_a = 0.0_f64;
699 let mut norm_b = 0.0_f64;
700
701 for (x, y) in a.iter().zip(b.iter()) {
702 let xf = *x as f64;
703 let yf = *y as f64;
704 dot += xf * yf;
705 norm_a += xf * xf;
706 norm_b += yf * yf;
707 }
708
709 let denom = norm_a.sqrt() * norm_b.sqrt();
710 if denom < 1e-12 {
711 return 0.0;
712 }
713
714 (dot / denom) as f32
715}
716
717#[derive(Debug, Clone)]
727pub struct QueryEngine;
728
729impl QueryEngine {
730 pub fn new() -> Self {
732 Self
733 }
734
735 pub fn symbol_lookup<'g>(
741 &self,
742 graph: &'g CodeGraph,
743 params: SymbolLookupParams,
744 ) -> AcbResult<Vec<&'g CodeUnit>> {
745 let candidates: Vec<&CodeUnit> = match params.mode {
746 MatchMode::Exact => graph.find_units_by_exact_name(¶ms.name),
747 MatchMode::Prefix => graph.find_units_by_name(¶ms.name),
748 MatchMode::Contains => {
749 let lower = params.name.to_lowercase();
750 graph
751 .units()
752 .iter()
753 .filter(|u| u.name.to_lowercase().contains(&lower))
754 .collect()
755 }
756 MatchMode::Fuzzy => {
757 let lower = params.name.to_lowercase();
758 let threshold = params.fuzzy_threshold;
759 graph
760 .units()
761 .iter()
762 .filter(|u| levenshtein(&u.name.to_lowercase(), &lower) <= threshold)
763 .collect()
764 }
765 };
766
767 let filtered: Vec<&CodeUnit> = candidates
769 .into_iter()
770 .filter(|u| params.unit_types.is_empty() || params.unit_types.contains(&u.unit_type))
771 .filter(|u| params.languages.is_empty() || params.languages.contains(&u.language))
772 .collect();
773
774 let result = if params.limit > 0 {
776 filtered.into_iter().take(params.limit).collect()
777 } else {
778 filtered
779 };
780
781 Ok(result)
782 }
783
784 pub fn dependency_graph(
790 &self,
791 graph: &CodeGraph,
792 params: DependencyParams,
793 ) -> AcbResult<DependencyResult> {
794 self.validate_unit(graph, params.unit_id)?;
795
796 let effective_depth = if params.include_transitive {
797 params.max_depth
798 } else {
799 1
800 };
801
802 let edge_types = if params.edge_types.is_empty() {
803 vec![
804 EdgeType::Calls,
805 EdgeType::Imports,
806 EdgeType::Inherits,
807 EdgeType::Implements,
808 EdgeType::UsesType,
809 EdgeType::FfiBinds,
810 ]
811 } else {
812 params.edge_types
813 };
814
815 let mut nodes = Vec::new();
817 let mut visited = HashSet::new();
818 let mut queue = VecDeque::new();
819 visited.insert(params.unit_id);
820 queue.push_back((params.unit_id, 0u32, vec![params.unit_id]));
821
822 while let Some((current, depth, path)) = queue.pop_front() {
823 if current != params.unit_id {
824 nodes.push(DependencyNode {
825 unit_id: current,
826 depth,
827 path: path.clone(),
828 });
829 }
830
831 if depth >= effective_depth {
832 continue;
833 }
834
835 for edge in graph.edges_from(current) {
836 if !edge_types.contains(&edge.edge_type) {
837 continue;
838 }
839 if visited.insert(edge.target_id) {
840 let mut new_path = path.clone();
841 new_path.push(edge.target_id);
842 queue.push_back((edge.target_id, depth + 1, new_path));
843 }
844 }
845 }
846
847 Ok(DependencyResult {
848 root_id: params.unit_id,
849 nodes,
850 })
851 }
852
853 pub fn reverse_dependency(
859 &self,
860 graph: &CodeGraph,
861 params: DependencyParams,
862 ) -> AcbResult<DependencyResult> {
863 self.validate_unit(graph, params.unit_id)?;
864
865 let effective_depth = if params.include_transitive {
866 params.max_depth
867 } else {
868 1
869 };
870
871 let edge_types = if params.edge_types.is_empty() {
872 vec![
873 EdgeType::Calls,
874 EdgeType::Imports,
875 EdgeType::Inherits,
876 EdgeType::Implements,
877 EdgeType::UsesType,
878 EdgeType::FfiBinds,
879 ]
880 } else {
881 params.edge_types
882 };
883
884 let mut nodes = Vec::new();
885 let mut visited = HashSet::new();
886 let mut queue = VecDeque::new();
887 visited.insert(params.unit_id);
888 queue.push_back((params.unit_id, 0u32, vec![params.unit_id]));
889
890 while let Some((current, depth, path)) = queue.pop_front() {
891 if current != params.unit_id {
892 nodes.push(DependencyNode {
893 unit_id: current,
894 depth,
895 path: path.clone(),
896 });
897 }
898
899 if depth >= effective_depth {
900 continue;
901 }
902
903 for edge in graph.edges_to(current) {
905 if !edge_types.contains(&edge.edge_type) {
906 continue;
907 }
908 if visited.insert(edge.source_id) {
909 let mut new_path = path.clone();
910 new_path.push(edge.source_id);
911 queue.push_back((edge.source_id, depth + 1, new_path));
912 }
913 }
914 }
915
916 Ok(DependencyResult {
917 root_id: params.unit_id,
918 nodes,
919 })
920 }
921
922 pub fn call_graph(
928 &self,
929 graph: &CodeGraph,
930 params: CallGraphParams,
931 ) -> AcbResult<CallGraphResult> {
932 self.validate_unit(graph, params.unit_id)?;
933
934 let mut all_nodes: Vec<(u64, u32)> = Vec::new();
935 let mut call_sites = Vec::new();
936 let mut seen = HashSet::new();
937
938 if params.direction == CallDirection::Callees || params.direction == CallDirection::Both {
940 let opts = TraversalOptions {
941 max_depth: params.max_depth as i32,
942 edge_types: vec![EdgeType::Calls],
943 direction: Direction::Forward,
944 };
945 let results = traversal::bfs(graph, params.unit_id, &opts);
946 for (id, depth) in &results {
947 if seen.insert(*id) {
948 all_nodes.push((*id, *depth));
949 }
950 }
951
952 self.collect_call_sites_forward(
954 graph,
955 params.unit_id,
956 params.max_depth,
957 &mut call_sites,
958 );
959 }
960
961 if params.direction == CallDirection::Callers || params.direction == CallDirection::Both {
963 let opts = TraversalOptions {
964 max_depth: params.max_depth as i32,
965 edge_types: vec![EdgeType::Calls],
966 direction: Direction::Backward,
967 };
968 let results = traversal::bfs(graph, params.unit_id, &opts);
969 for (id, depth) in &results {
970 if seen.insert(*id) {
971 all_nodes.push((*id, *depth));
972 }
973 }
974
975 self.collect_call_sites_backward(
977 graph,
978 params.unit_id,
979 params.max_depth,
980 &mut call_sites,
981 );
982 }
983
984 Ok(CallGraphResult {
985 root_id: params.unit_id,
986 nodes: all_nodes,
987 call_sites,
988 })
989 }
990
991 pub fn type_hierarchy(
997 &self,
998 graph: &CodeGraph,
999 params: HierarchyParams,
1000 ) -> AcbResult<HierarchyResult> {
1001 self.validate_unit(graph, params.unit_id)?;
1002
1003 let mut nodes = Vec::new();
1004
1005 if params.include_ancestors {
1006 let mut visited = HashSet::new();
1008 let mut queue = VecDeque::new();
1009 visited.insert(params.unit_id);
1010 queue.push_back((params.unit_id, 0i32));
1011
1012 while let Some((current, depth)) = queue.pop_front() {
1013 for edge in graph.edges_from(current) {
1014 if edge.edge_type != EdgeType::Inherits
1015 && edge.edge_type != EdgeType::Implements
1016 {
1017 continue;
1018 }
1019 if visited.insert(edge.target_id) {
1020 let ancestor_depth = depth + 1;
1021 nodes.push(HierarchyNode {
1022 unit_id: edge.target_id,
1023 relation: edge.edge_type,
1024 depth: ancestor_depth,
1025 });
1026 queue.push_back((edge.target_id, ancestor_depth));
1027 }
1028 }
1029 }
1030 }
1031
1032 if params.include_descendants {
1033 let mut visited = HashSet::new();
1035 let mut queue = VecDeque::new();
1036 visited.insert(params.unit_id);
1037 queue.push_back((params.unit_id, 0i32));
1038
1039 while let Some((current, depth)) = queue.pop_front() {
1040 for edge in graph.edges_to(current) {
1041 if edge.edge_type != EdgeType::Inherits
1042 && edge.edge_type != EdgeType::Implements
1043 {
1044 continue;
1045 }
1046 if visited.insert(edge.source_id) {
1047 let desc_depth = depth - 1;
1048 nodes.push(HierarchyNode {
1049 unit_id: edge.source_id,
1050 relation: edge.edge_type,
1051 depth: desc_depth,
1052 });
1053 queue.push_back((edge.source_id, desc_depth));
1054 }
1055 }
1056 }
1057 }
1058
1059 Ok(HierarchyResult {
1060 root_id: params.unit_id,
1061 nodes,
1062 })
1063 }
1064
1065 pub fn containment<'g>(
1071 &self,
1072 graph: &'g CodeGraph,
1073 unit_id: u64,
1074 ) -> AcbResult<Vec<&'g CodeUnit>> {
1075 self.validate_unit(graph, unit_id)?;
1076
1077 let opts = TraversalOptions {
1078 max_depth: -1,
1079 edge_types: vec![EdgeType::Contains],
1080 direction: Direction::Forward,
1081 };
1082
1083 let traversal = traversal::bfs(graph, unit_id, &opts);
1084 let mut result = Vec::new();
1085 for (id, _depth) in traversal {
1086 if id == unit_id {
1087 continue; }
1089 if let Some(unit) = graph.get_unit(id) {
1090 result.push(unit);
1091 }
1092 }
1093
1094 Ok(result)
1095 }
1096
1097 pub fn pattern_match(
1103 &self,
1104 graph: &CodeGraph,
1105 params: PatternParams,
1106 ) -> AcbResult<Vec<PatternMatch>> {
1107 let pattern = params.pattern.trim();
1108 let mut results = Vec::new();
1109
1110 if pattern.starts_with("async function") || pattern.starts_with("async_function") {
1112 let complexity_threshold = self.parse_complexity_constraint(pattern);
1114
1115 for unit in graph.units() {
1116 if unit.unit_type == CodeUnitType::Function && unit.is_async {
1117 if let Some(threshold) = complexity_threshold {
1118 if unit.complexity <= threshold {
1119 continue;
1120 }
1121 }
1122 results.push(PatternMatch {
1123 unit_id: unit.id,
1124 confidence: 1.0,
1125 matched_rule: "async function".to_string(),
1126 });
1127 }
1128 }
1129 return Ok(results);
1130 }
1131
1132 if pattern.starts_with("function") && pattern.contains("calls:") {
1134 let call_targets = self.parse_call_list(pattern);
1135 let complexity_threshold = self.parse_complexity_constraint(pattern);
1136
1137 for unit in graph.units() {
1138 if unit.unit_type != CodeUnitType::Function {
1139 continue;
1140 }
1141 if let Some(threshold) = complexity_threshold {
1142 if unit.complexity <= threshold {
1143 continue;
1144 }
1145 }
1146 let callees: HashSet<String> = graph
1147 .edges_from_of_type(unit.id, EdgeType::Calls)
1148 .iter()
1149 .filter_map(|e| graph.get_unit(e.target_id))
1150 .map(|u| u.name.clone())
1151 .collect();
1152
1153 if call_targets.iter().all(|t| callees.contains(t)) {
1154 results.push(PatternMatch {
1155 unit_id: unit.id,
1156 confidence: 1.0,
1157 matched_rule: format!("function calls {:?}", call_targets),
1158 });
1159 }
1160 }
1161 return Ok(results);
1162 }
1163
1164 if pattern.starts_with("class") && pattern.contains("inherits:") {
1166 let base_name = self.parse_inherits_target(pattern);
1167 if let Some(base) = base_name {
1168 for unit in graph.units() {
1169 if unit.unit_type != CodeUnitType::Type {
1170 continue;
1171 }
1172 let parents: Vec<String> = graph
1173 .edges_from_of_type(unit.id, EdgeType::Inherits)
1174 .iter()
1175 .filter_map(|e| graph.get_unit(e.target_id))
1176 .map(|u| u.name.clone())
1177 .collect();
1178 if parents.contains(&base) {
1179 results.push(PatternMatch {
1180 unit_id: unit.id,
1181 confidence: 1.0,
1182 matched_rule: format!("class inherits {}", base),
1183 });
1184 }
1185 }
1186 }
1187 return Ok(results);
1188 }
1189
1190 if pattern.starts_with("function") && pattern.contains("complexity:") {
1192 let threshold = self.parse_complexity_constraint(pattern);
1193 if let Some(t) = threshold {
1194 for unit in graph.units() {
1195 if unit.unit_type == CodeUnitType::Function && unit.complexity > t {
1196 results.push(PatternMatch {
1197 unit_id: unit.id,
1198 confidence: 1.0,
1199 matched_rule: format!("function complexity > {}", t),
1200 });
1201 }
1202 }
1203 }
1204 return Ok(results);
1205 }
1206
1207 Err(AcbError::QueryError(format!(
1209 "Unrecognized pattern: {}",
1210 pattern
1211 )))
1212 }
1213
1214 pub fn semantic_search(
1220 &self,
1221 graph: &CodeGraph,
1222 params: SemanticParams,
1223 ) -> AcbResult<Vec<SemanticMatch>> {
1224 if params.query_vec.is_empty() {
1225 return Err(AcbError::QueryError("Query vector is empty".to_string()));
1226 }
1227
1228 let mut scored: Vec<SemanticMatch> = graph
1229 .units()
1230 .iter()
1231 .filter(|u| params.unit_types.is_empty() || params.unit_types.contains(&u.unit_type))
1232 .filter(|u| params.languages.is_empty() || params.languages.contains(&u.language))
1233 .filter_map(|u| {
1234 let score = cosine_similarity(¶ms.query_vec, &u.feature_vec);
1235 if score >= params.min_similarity {
1236 Some(SemanticMatch {
1237 unit_id: u.id,
1238 score,
1239 })
1240 } else {
1241 None
1242 }
1243 })
1244 .collect();
1245
1246 scored.sort_by(|a, b| {
1247 b.score
1248 .partial_cmp(&a.score)
1249 .unwrap_or(std::cmp::Ordering::Equal)
1250 });
1251
1252 if params.top_k > 0 {
1253 scored.truncate(params.top_k);
1254 }
1255
1256 Ok(scored)
1257 }
1258
1259 pub fn impact_analysis(
1265 &self,
1266 graph: &CodeGraph,
1267 params: ImpactParams,
1268 ) -> AcbResult<ImpactResult> {
1269 self.validate_unit(graph, params.unit_id)?;
1270
1271 let dep_params = DependencyParams {
1273 unit_id: params.unit_id,
1274 max_depth: params.max_depth,
1275 edge_types: params.edge_types.clone(),
1276 include_transitive: true,
1277 };
1278 let deps = self.reverse_dependency(graph, dep_params)?;
1279
1280 let mut impacted = Vec::new();
1281 let mut total_risk = 0.0_f32;
1282
1283 for node in &deps.nodes {
1284 let has_tests = !graph
1285 .edges_to_of_type(node.unit_id, EdgeType::Tests)
1286 .is_empty();
1287
1288 let coupling_count = graph
1290 .edges_from_of_type(node.unit_id, EdgeType::CouplesWith)
1291 .len()
1292 + graph
1293 .edges_to_of_type(node.unit_id, EdgeType::CouplesWith)
1294 .len();
1295
1296 let depth_factor = 1.0 / (1.0 + node.depth as f32);
1298 let test_factor = if has_tests { 0.2 } else { 0.6 };
1299 let coupling_factor = (coupling_count as f32 * 0.05).min(0.3);
1300 let risk = (depth_factor * 0.4 + test_factor + coupling_factor).min(1.0);
1301
1302 total_risk += risk;
1303
1304 impacted.push(ImpactedUnit {
1305 unit_id: node.unit_id,
1306 depth: node.depth,
1307 risk_score: risk,
1308 has_tests,
1309 });
1310 }
1311
1312 let overall_risk = if impacted.is_empty() {
1313 0.0
1314 } else {
1315 (total_risk / impacted.len() as f32).min(1.0)
1316 };
1317
1318 let mut recommendations = Vec::new();
1319 let untested_count = impacted.iter().filter(|i| !i.has_tests).count();
1320 if untested_count > 0 {
1321 recommendations.push(format!(
1322 "Add tests for {} impacted units that lack test coverage.",
1323 untested_count
1324 ));
1325 }
1326 if impacted.len() > 10 {
1327 recommendations.push(
1328 "Consider breaking this unit into smaller pieces to reduce blast radius."
1329 .to_string(),
1330 );
1331 }
1332 if overall_risk > 0.7 {
1333 recommendations
1334 .push("High overall risk. Deploy incrementally with feature flags.".to_string());
1335 }
1336
1337 Ok(ImpactResult {
1338 root_id: params.unit_id,
1339 impacted,
1340 overall_risk,
1341 recommendations,
1342 })
1343 }
1344
1345 pub fn test_coverage(&self, graph: &CodeGraph, unit_id: u64) -> AcbResult<CoverageResult> {
1351 self.validate_unit(graph, unit_id)?;
1352
1353 let direct_tests: Vec<u64> = graph
1355 .edges_to_of_type(unit_id, EdgeType::Tests)
1356 .iter()
1357 .map(|e| e.source_id)
1358 .collect();
1359
1360 let callers: Vec<u64> = graph
1362 .edges_to_of_type(unit_id, EdgeType::Calls)
1363 .iter()
1364 .map(|e| e.source_id)
1365 .collect();
1366
1367 let mut indirect_set: HashSet<u64> = HashSet::new();
1368 for caller_id in &callers {
1369 for test_edge in graph.edges_to_of_type(*caller_id, EdgeType::Tests) {
1370 indirect_set.insert(test_edge.source_id);
1371 }
1372 }
1373 for dt in &direct_tests {
1375 indirect_set.remove(dt);
1376 }
1377 let indirect_tests: Vec<u64> = indirect_set.into_iter().collect();
1378
1379 let total_tests = direct_tests.len() + indirect_tests.len();
1381 let coverage_ratio = if total_tests > 0 {
1382 let effective = direct_tests.len() as f32 + indirect_tests.len() as f32 * 0.5;
1384 (effective / (effective + 1.0)).min(1.0)
1385 } else {
1386 0.0
1387 };
1388
1389 Ok(CoverageResult {
1390 unit_id,
1391 direct_tests,
1392 indirect_tests,
1393 coverage_ratio,
1394 })
1395 }
1396
1397 pub fn cross_language_trace(
1403 &self,
1404 graph: &CodeGraph,
1405 params: TraceParams,
1406 ) -> AcbResult<TraceResult> {
1407 self.validate_unit(graph, params.unit_id)?;
1408
1409 let start_unit = graph
1410 .get_unit(params.unit_id)
1411 .ok_or(AcbError::UnitNotFound(params.unit_id))?;
1412
1413 let mut hops = Vec::new();
1414 let mut languages_crossed = Vec::new();
1415
1416 hops.push(TraceHop {
1417 unit_id: params.unit_id,
1418 language: start_unit.language,
1419 via_edge: None,
1420 });
1421 languages_crossed.push(start_unit.language);
1422
1423 let mut visited = HashSet::new();
1424 visited.insert(params.unit_id);
1425 let mut current_frontier = vec![params.unit_id];
1426
1427 for _hop in 0..params.max_hops {
1428 let mut next_frontier = Vec::new();
1429 for current_id in ¤t_frontier {
1430 for edge in graph.edges_from(*current_id) {
1432 if edge.edge_type != EdgeType::FfiBinds {
1433 continue;
1434 }
1435 if visited.insert(edge.target_id) {
1436 if let Some(target_unit) = graph.get_unit(edge.target_id) {
1437 hops.push(TraceHop {
1438 unit_id: edge.target_id,
1439 language: target_unit.language,
1440 via_edge: Some(EdgeType::FfiBinds),
1441 });
1442 if !languages_crossed.contains(&target_unit.language) {
1443 languages_crossed.push(target_unit.language);
1444 }
1445 next_frontier.push(edge.target_id);
1446 }
1447 }
1448 }
1449 for edge in graph.edges_to(*current_id) {
1450 if edge.edge_type != EdgeType::FfiBinds {
1451 continue;
1452 }
1453 if visited.insert(edge.source_id) {
1454 if let Some(source_unit) = graph.get_unit(edge.source_id) {
1455 hops.push(TraceHop {
1456 unit_id: edge.source_id,
1457 language: source_unit.language,
1458 via_edge: Some(EdgeType::FfiBinds),
1459 });
1460 if !languages_crossed.contains(&source_unit.language) {
1461 languages_crossed.push(source_unit.language);
1462 }
1463 next_frontier.push(edge.source_id);
1464 }
1465 }
1466 }
1467 }
1468 if next_frontier.is_empty() {
1469 break;
1470 }
1471 current_frontier = next_frontier;
1472 }
1473
1474 Ok(TraceResult {
1475 hops,
1476 languages_crossed,
1477 })
1478 }
1479
1480 pub fn collective_patterns(
1489 &self,
1490 graph: &CodeGraph,
1491 params: CollectiveParams,
1492 ) -> AcbResult<CollectiveResult> {
1493 let mut patterns: Vec<CollectivePatternEntry> = graph
1495 .units()
1496 .iter()
1497 .filter(|u| {
1498 if let Some(ref ut) = params.unit_type {
1499 u.unit_type == *ut
1500 } else {
1501 true
1502 }
1503 })
1504 .filter(|u| u.collective_usage >= params.min_usage)
1505 .map(|u| CollectivePatternEntry {
1506 unit_id: u.id,
1507 usage_count: u.collective_usage,
1508 confidence: if u.collective_usage > 0 {
1509 (u.collective_usage as f32).ln().min(1.0)
1510 } else {
1511 0.0
1512 },
1513 })
1514 .collect();
1515
1516 patterns.sort_by(|a, b| b.usage_count.cmp(&a.usage_count));
1517
1518 if params.limit > 0 {
1519 patterns.truncate(params.limit);
1520 }
1521
1522 Ok(CollectiveResult {
1523 patterns,
1524 collective_available: false,
1525 })
1526 }
1527
1528 pub fn temporal_evolution(
1534 &self,
1535 graph: &CodeGraph,
1536 unit_id: u64,
1537 ) -> AcbResult<EvolutionResult> {
1538 let unit = graph
1539 .get_unit(unit_id)
1540 .ok_or(AcbError::UnitNotFound(unit_id))?;
1541
1542 let trend = if unit.stability_score > 0.8 {
1543 "Stable — rarely changes.".to_string()
1544 } else if unit.stability_score > 0.5 {
1545 "Moderate — changes occasionally.".to_string()
1546 } else if unit.stability_score > 0.2 {
1547 "Volatile — changes frequently.".to_string()
1548 } else {
1549 "Highly volatile — constantly changing, potential hotspot.".to_string()
1550 };
1551
1552 Ok(EvolutionResult {
1553 unit_id,
1554 change_count: unit.change_count,
1555 created_at: unit.created_at,
1556 last_modified: unit.last_modified,
1557 stability_score: unit.stability_score,
1558 trend,
1559 })
1560 }
1561
1562 pub fn stability_analysis(
1568 &self,
1569 graph: &CodeGraph,
1570 unit_id: u64,
1571 ) -> AcbResult<StabilityResult> {
1572 let unit = graph
1573 .get_unit(unit_id)
1574 .ok_or(AcbError::UnitNotFound(unit_id))?;
1575
1576 let mut factors = Vec::new();
1577
1578 let change_factor = 1.0 / (1.0 + unit.change_count as f32 * 0.1);
1580 factors.push(StabilityFactor {
1581 name: "change_frequency".to_string(),
1582 value: change_factor,
1583 description: format!(
1584 "Unit has {} changes. Lower change count = more stable.",
1585 unit.change_count
1586 ),
1587 });
1588
1589 let test_count = graph.edges_to_of_type(unit_id, EdgeType::Tests).len();
1591 let test_factor = if test_count > 0 {
1592 (test_count as f32 * 0.3).min(1.0)
1593 } else {
1594 0.0
1595 };
1596 factors.push(StabilityFactor {
1597 name: "test_coverage".to_string(),
1598 value: test_factor,
1599 description: format!(
1600 "{} direct tests. More tests = more stability confidence.",
1601 test_count
1602 ),
1603 });
1604
1605 let complexity_factor = 1.0 / (1.0 + unit.complexity as f32 * 0.05);
1607 factors.push(StabilityFactor {
1608 name: "complexity".to_string(),
1609 value: complexity_factor,
1610 description: format!(
1611 "Cyclomatic complexity is {}. Lower = more stable.",
1612 unit.complexity
1613 ),
1614 });
1615
1616 let coupling_count = graph
1618 .edges_from_of_type(unit_id, EdgeType::CouplesWith)
1619 .len()
1620 + graph.edges_to_of_type(unit_id, EdgeType::CouplesWith).len();
1621 let coupling_factor = 1.0 / (1.0 + coupling_count as f32 * 0.2);
1622 factors.push(StabilityFactor {
1623 name: "coupling".to_string(),
1624 value: coupling_factor,
1625 description: format!(
1626 "{} temporal couplings. Fewer couplings = more independent.",
1627 coupling_count
1628 ),
1629 });
1630
1631 let overall = change_factor * 0.3
1633 + test_factor * 0.25
1634 + complexity_factor * 0.25
1635 + coupling_factor * 0.2;
1636
1637 let recommendation = if overall > 0.7 {
1638 "Unit is stable. No immediate action needed.".to_string()
1639 } else if overall > 0.4 {
1640 "Unit has moderate stability. Consider adding tests and reducing complexity."
1641 .to_string()
1642 } else {
1643 "Unit is unstable. Prioritize adding tests, reducing complexity, and decoupling."
1644 .to_string()
1645 };
1646
1647 Ok(StabilityResult {
1648 unit_id,
1649 overall_score: overall,
1650 factors,
1651 recommendation,
1652 })
1653 }
1654
1655 pub fn coupling_detection(
1661 &self,
1662 graph: &CodeGraph,
1663 params: CouplingParams,
1664 ) -> AcbResult<Vec<Coupling>> {
1665 let mut couplings = Vec::new();
1666 let mut seen_pairs: HashSet<(u64, u64)> = HashSet::new();
1667
1668 let units_to_check: Vec<u64> = if let Some(uid) = params.unit_id {
1669 self.validate_unit(graph, uid)?;
1670 vec![uid]
1671 } else {
1672 graph.units().iter().map(|u| u.id).collect()
1673 };
1674
1675 for uid in &units_to_check {
1676 for edge in graph.edges_from(*uid) {
1678 if !edge.edge_type.is_dependency() {
1679 continue;
1680 }
1681 let pair = normalize_pair(*uid, edge.target_id);
1682 if edge.weight >= params.min_strength && seen_pairs.insert(pair) {
1683 couplings.push(Coupling {
1684 unit_a: pair.0,
1685 unit_b: pair.1,
1686 strength: edge.weight,
1687 kind: CouplingKind::Explicit,
1688 });
1689 }
1690 }
1691
1692 for edge in graph.edges_from_of_type(*uid, EdgeType::CouplesWith) {
1694 let pair = normalize_pair(*uid, edge.target_id);
1695 if edge.weight >= params.min_strength && seen_pairs.insert(pair) {
1696 couplings.push(Coupling {
1697 unit_a: pair.0,
1698 unit_b: pair.1,
1699 strength: edge.weight,
1700 kind: CouplingKind::Temporal,
1701 });
1702 }
1703 }
1704 for edge in graph.edges_to_of_type(*uid, EdgeType::CouplesWith) {
1705 let pair = normalize_pair(edge.source_id, *uid);
1706 if edge.weight >= params.min_strength && seen_pairs.insert(pair) {
1707 couplings.push(Coupling {
1708 unit_a: pair.0,
1709 unit_b: pair.1,
1710 strength: edge.weight,
1711 kind: CouplingKind::Temporal,
1712 });
1713 }
1714 }
1715 }
1716
1717 if let Some(uid) = params.unit_id {
1719 let my_targets: HashSet<u64> = graph
1720 .edges_from(uid)
1721 .iter()
1722 .filter(|e| e.edge_type.is_dependency())
1723 .map(|e| e.target_id)
1724 .collect();
1725
1726 if !my_targets.is_empty() {
1727 for other_unit in graph.units() {
1728 if other_unit.id == uid {
1729 continue;
1730 }
1731 let other_targets: HashSet<u64> = graph
1732 .edges_from(other_unit.id)
1733 .iter()
1734 .filter(|e| e.edge_type.is_dependency())
1735 .map(|e| e.target_id)
1736 .collect();
1737
1738 if other_targets.is_empty() {
1739 continue;
1740 }
1741
1742 let intersection = my_targets.intersection(&other_targets).count();
1743 let union = my_targets.union(&other_targets).count();
1744 let jaccard = if union > 0 {
1745 intersection as f32 / union as f32
1746 } else {
1747 0.0
1748 };
1749
1750 if jaccard >= params.min_strength {
1751 let pair = normalize_pair(uid, other_unit.id);
1752 if seen_pairs.insert(pair) {
1753 couplings.push(Coupling {
1754 unit_a: pair.0,
1755 unit_b: pair.1,
1756 strength: jaccard,
1757 kind: CouplingKind::Hidden,
1758 });
1759 }
1760 }
1761 }
1762 }
1763 }
1764
1765 couplings.sort_by(|a, b| {
1767 b.strength
1768 .partial_cmp(&a.strength)
1769 .unwrap_or(std::cmp::Ordering::Equal)
1770 });
1771
1772 Ok(couplings)
1773 }
1774
1775 pub fn dead_code<'g>(
1781 &self,
1782 graph: &'g CodeGraph,
1783 params: DeadCodeParams,
1784 ) -> AcbResult<Vec<&'g CodeUnit>> {
1785 let mut roots: HashSet<u64> = HashSet::new();
1787
1788 for unit in graph.units() {
1789 let is_entry = match unit.unit_type {
1790 CodeUnitType::Function => {
1791 unit.name == "main"
1792 || unit.name.starts_with("test_")
1793 || unit.visibility == Visibility::Public
1794 }
1795 CodeUnitType::Test => true,
1796 CodeUnitType::Module => {
1797 unit.visibility == Visibility::Public
1799 }
1800 _ => unit.visibility == Visibility::Public,
1801 };
1802 if is_entry {
1803 roots.insert(unit.id);
1804 }
1805 }
1806
1807 if params.include_tests_as_roots {
1808 for unit in graph.find_units_by_type(CodeUnitType::Test) {
1809 roots.insert(unit.id);
1810 }
1811 }
1812
1813 let mut reachable: HashSet<u64> = HashSet::new();
1815 let opts = TraversalOptions {
1816 max_depth: -1,
1817 edge_types: Vec::new(), direction: Direction::Forward,
1819 };
1820
1821 for root_id in &roots {
1822 let visited = traversal::bfs(graph, *root_id, &opts);
1823 for (id, _) in visited {
1824 reachable.insert(id);
1825 }
1826 }
1827
1828 let back_opts = TraversalOptions {
1831 max_depth: -1,
1832 edge_types: Vec::new(),
1833 direction: Direction::Backward,
1834 };
1835 for root_id in &roots {
1836 let visited = traversal::bfs(graph, *root_id, &back_opts);
1837 for (id, _) in visited {
1838 reachable.insert(id);
1839 }
1840 }
1841
1842 let mut dead: Vec<&CodeUnit> = graph
1844 .units()
1845 .iter()
1846 .filter(|u| {
1847 if params.unit_types.is_empty() {
1848 true
1849 } else {
1850 params.unit_types.contains(&u.unit_type)
1851 }
1852 })
1853 .filter(|u| !reachable.contains(&u.id))
1854 .collect();
1855
1856 dead.sort_by(|a, b| a.name.cmp(&b.name));
1858
1859 Ok(dead)
1860 }
1861
1862 pub fn prophecy(&self, graph: &CodeGraph, params: ProphecyParams) -> AcbResult<ProphecyResult> {
1868 let mut predictions = Vec::new();
1869
1870 for unit in graph.units() {
1871 let mut risk = 0.0_f32;
1872 let mut reasons = Vec::new();
1873
1874 if unit.stability_score < 0.3 {
1876 risk += 0.4;
1877 reasons.push(format!("Low stability score ({:.2})", unit.stability_score));
1878 } else if unit.stability_score < 0.6 {
1879 risk += 0.2;
1880 reasons.push(format!(
1881 "Moderate stability score ({:.2})",
1882 unit.stability_score
1883 ));
1884 }
1885
1886 if unit.complexity > 20 {
1888 risk += 0.3;
1889 reasons.push(format!("High complexity ({})", unit.complexity));
1890 } else if unit.complexity > 10 {
1891 risk += 0.15;
1892 reasons.push(format!("Moderate complexity ({})", unit.complexity));
1893 }
1894
1895 if unit.change_count > 50 {
1897 risk += 0.2;
1898 reasons.push(format!("Frequently changed ({} times)", unit.change_count));
1899 } else if unit.change_count > 20 {
1900 risk += 0.1;
1901 reasons.push(format!("Changed {} times", unit.change_count));
1902 }
1903
1904 let breaks_count = graph
1906 .edges_from_of_type(unit.id, EdgeType::BreaksWith)
1907 .len()
1908 + graph.edges_to_of_type(unit.id, EdgeType::BreaksWith).len();
1909 if breaks_count > 0 {
1910 risk += 0.2 * (breaks_count as f32).min(3.0) / 3.0;
1911 reasons.push(format!(
1912 "{} historical breakage relationships",
1913 breaks_count
1914 ));
1915 }
1916
1917 let test_count = graph.edges_to_of_type(unit.id, EdgeType::Tests).len();
1919 if test_count == 0 && unit.unit_type == CodeUnitType::Function {
1920 risk += 0.1;
1921 reasons.push("No test coverage".to_string());
1922 }
1923
1924 risk = risk.min(1.0);
1925
1926 if risk >= params.min_risk {
1927 predictions.push(Prediction {
1928 unit_id: unit.id,
1929 risk_score: risk,
1930 reason: reasons.join("; "),
1931 });
1932 }
1933 }
1934
1935 predictions.sort_by(|a, b| {
1937 b.risk_score
1938 .partial_cmp(&a.risk_score)
1939 .unwrap_or(std::cmp::Ordering::Equal)
1940 });
1941
1942 if params.top_k > 0 {
1943 predictions.truncate(params.top_k);
1944 }
1945
1946 Ok(ProphecyResult { predictions })
1947 }
1948
1949 pub fn concept_mapping(&self, graph: &CodeGraph, concept: &str) -> AcbResult<ConceptMap> {
1955 let concept_lower = concept.to_lowercase();
1956 let mut units = Vec::new();
1957
1958 for unit in graph.units() {
1959 let name_match = unit.name.to_lowercase().contains(&concept_lower)
1960 || unit.qualified_name.to_lowercase().contains(&concept_lower);
1961 let doc_match = unit
1962 .doc_summary
1963 .as_ref()
1964 .map(|d| d.to_lowercase().contains(&concept_lower))
1965 .unwrap_or(false);
1966
1967 if !name_match && !doc_match {
1968 continue;
1969 }
1970
1971 let role = match unit.unit_type {
1973 CodeUnitType::Type | CodeUnitType::Trait | CodeUnitType::Module => {
1974 ConceptRole::Definition
1975 }
1976 CodeUnitType::Test => ConceptRole::Test,
1977 CodeUnitType::Impl => ConceptRole::Extension,
1978 _ => {
1979 let has_inherit = graph
1981 .edges_from_of_type(unit.id, EdgeType::Inherits)
1982 .iter()
1983 .any(|e| {
1984 graph
1985 .get_unit(e.target_id)
1986 .map(|t| t.name.to_lowercase().contains(&concept_lower))
1987 .unwrap_or(false)
1988 });
1989 if has_inherit {
1990 ConceptRole::Extension
1991 } else {
1992 ConceptRole::Usage
1993 }
1994 }
1995 };
1996
1997 let mut relevance = 0.0_f32;
1999 if unit.name.to_lowercase() == concept_lower {
2000 relevance = 1.0;
2001 } else if name_match {
2002 relevance = 0.7;
2003 }
2004 if doc_match {
2005 relevance = (relevance + 0.3).min(1.0);
2006 }
2007
2008 units.push(ConceptUnit {
2009 unit_id: unit.id,
2010 role,
2011 relevance,
2012 });
2013 }
2014
2015 units.sort_by(|a, b| {
2017 b.relevance
2018 .partial_cmp(&a.relevance)
2019 .unwrap_or(std::cmp::Ordering::Equal)
2020 });
2021
2022 Ok(ConceptMap {
2023 concept: concept.to_string(),
2024 units,
2025 })
2026 }
2027
2028 pub fn migration_path(
2034 &self,
2035 graph: &CodeGraph,
2036 params: MigrationParams,
2037 ) -> AcbResult<MigrationPlan> {
2038 self.validate_unit(graph, params.from_unit)?;
2039 self.validate_unit(graph, params.to_unit)?;
2040
2041 let dep_params = DependencyParams {
2043 unit_id: params.from_unit,
2044 max_depth: 10,
2045 edge_types: vec![
2046 EdgeType::Calls,
2047 EdgeType::Imports,
2048 EdgeType::UsesType,
2049 EdgeType::Inherits,
2050 EdgeType::Implements,
2051 ],
2052 include_transitive: false,
2053 };
2054 let deps = self.reverse_dependency(graph, dep_params)?;
2055
2056 let mut steps: Vec<MigrationStep> = Vec::new();
2057 let mut order = 0u32;
2058
2059 steps.push(MigrationStep {
2061 unit_id: params.to_unit,
2062 order,
2063 safety: SafetyLevel::Safe,
2064 description: "Prepare the target unit as the replacement.".to_string(),
2065 });
2066 order += 1;
2067
2068 let mut dependent_steps: Vec<(u64, SafetyLevel)> = Vec::new();
2070
2071 for node in &deps.nodes {
2072 let has_direct_tests = !graph
2073 .edges_to_of_type(node.unit_id, EdgeType::Tests)
2074 .is_empty();
2075 let callers = graph.edges_to_of_type(node.unit_id, EdgeType::Calls);
2076 let caller_tested = callers.iter().any(|e| {
2077 !graph
2078 .edges_to_of_type(e.source_id, EdgeType::Tests)
2079 .is_empty()
2080 });
2081
2082 let safety = if has_direct_tests {
2083 SafetyLevel::Safe
2084 } else if caller_tested {
2085 SafetyLevel::Caution
2086 } else {
2087 SafetyLevel::Risky
2088 };
2089
2090 dependent_steps.push((node.unit_id, safety));
2091 }
2092
2093 dependent_steps.sort_by(|a, b| a.1.cmp(&b.1));
2095
2096 for (uid, safety) in dependent_steps {
2097 let unit_name = graph
2098 .get_unit(uid)
2099 .map(|u| u.qualified_name.clone())
2100 .unwrap_or_else(|| format!("unit_{}", uid));
2101 let desc = match safety {
2102 SafetyLevel::Safe => {
2103 format!("Update {} (has tests, safe to migrate).", unit_name)
2104 }
2105 SafetyLevel::Caution => {
2106 format!(
2107 "Update {} (no direct tests, but callers are tested — exercise caution).",
2108 unit_name
2109 )
2110 }
2111 SafetyLevel::Risky => {
2112 format!(
2113 "Update {} (no test coverage — add tests before migrating).",
2114 unit_name
2115 )
2116 }
2117 };
2118
2119 steps.push(MigrationStep {
2120 unit_id: uid,
2121 order,
2122 safety,
2123 description: desc,
2124 });
2125 order += 1;
2126 }
2127
2128 steps.push(MigrationStep {
2130 unit_id: params.from_unit,
2131 order,
2132 safety: SafetyLevel::Caution,
2133 description: "Remove or deprecate the original unit.".to_string(),
2134 });
2135
2136 Ok(MigrationPlan {
2137 from_unit: params.from_unit,
2138 to_unit: params.to_unit,
2139 steps,
2140 })
2141 }
2142
2143 pub fn test_gap(&self, graph: &CodeGraph, params: TestGapParams) -> AcbResult<Vec<TestGap>> {
2149 let target_types = if params.unit_types.is_empty() {
2150 vec![CodeUnitType::Function]
2151 } else {
2152 params.unit_types
2153 };
2154
2155 let mut gaps = Vec::new();
2156
2157 for unit in graph.units() {
2158 if !target_types.contains(&unit.unit_type) {
2159 continue;
2160 }
2161
2162 let has_tests = !graph.edges_to_of_type(unit.id, EdgeType::Tests).is_empty();
2163 if has_tests {
2164 continue;
2165 }
2166
2167 let high_changes = unit.change_count >= params.min_changes;
2168 let high_complexity = unit.complexity >= params.min_complexity;
2169
2170 if !high_changes && !high_complexity {
2171 continue;
2172 }
2173
2174 let mut reasons = Vec::new();
2175 if high_changes {
2176 reasons.push(format!("changed {} times", unit.change_count));
2177 }
2178 if high_complexity {
2179 reasons.push(format!("complexity {}", unit.complexity));
2180 }
2181
2182 let change_score =
2184 (unit.change_count as f32 / params.min_changes.max(1) as f32).min(2.0);
2185 let complexity_score =
2186 (unit.complexity as f32 / params.min_complexity.max(1) as f32).min(2.0);
2187 let priority = (change_score * 0.5 + complexity_score * 0.5).min(1.0);
2188
2189 gaps.push(TestGap {
2190 unit_id: unit.id,
2191 reason: format!("No tests, but {}", reasons.join(" and ")),
2192 priority,
2193 });
2194 }
2195
2196 gaps.sort_by(|a, b| {
2198 b.priority
2199 .partial_cmp(&a.priority)
2200 .unwrap_or(std::cmp::Ordering::Equal)
2201 });
2202
2203 Ok(gaps)
2204 }
2205
2206 pub fn architectural_drift(
2212 &self,
2213 graph: &CodeGraph,
2214 params: DriftParams,
2215 ) -> AcbResult<DriftReport> {
2216 let mut violations = Vec::new();
2217
2218 for (rule_idx, rule) in params.rules.iter().enumerate() {
2219 match rule {
2220 ArchRule::LayerDependency { upper, lower } => {
2221 let lower_units: Vec<&CodeUnit> = graph
2223 .units()
2224 .iter()
2225 .filter(|u| u.qualified_name.starts_with(lower.as_str()))
2226 .collect();
2227
2228 for lu in &lower_units {
2229 for edge in graph.edges_from(lu.id) {
2230 if !edge.edge_type.is_dependency() {
2231 continue;
2232 }
2233 if let Some(target) = graph.get_unit(edge.target_id) {
2234 if target.qualified_name.starts_with(upper.as_str()) {
2235 violations.push(DriftViolation {
2236 rule_index: rule_idx,
2237 description: format!(
2238 "Lower layer '{}' depends on upper layer '{}': {} -> {}",
2239 lower, upper, lu.qualified_name, target.qualified_name
2240 ),
2241 units: vec![lu.id, target.id],
2242 });
2243 }
2244 }
2245 }
2246 }
2247 }
2248 ArchRule::ModuleBoundary { module } => {
2249 let module_units: Vec<&CodeUnit> = graph
2251 .units()
2252 .iter()
2253 .filter(|u| u.qualified_name.starts_with(module.as_str()))
2254 .collect();
2255
2256 for mu in &module_units {
2257 for edge in graph.edges_from(mu.id) {
2258 if !edge.edge_type.is_dependency() {
2259 continue;
2260 }
2261 if let Some(target) = graph.get_unit(edge.target_id) {
2262 if !target.qualified_name.starts_with(module.as_str()) {
2263 violations.push(DriftViolation {
2264 rule_index: rule_idx,
2265 description: format!(
2266 "Module boundary violation: {} depends on external {}",
2267 mu.qualified_name, target.qualified_name
2268 ),
2269 units: vec![mu.id, target.id],
2270 });
2271 }
2272 }
2273 }
2274 }
2275 }
2276 ArchRule::NamingConvention { prefix, pattern } => {
2277 let (check_start, check_end, _check_contains, literal) =
2285 parse_simple_glob(pattern);
2286
2287 for unit in graph.units() {
2288 if !unit.qualified_name.starts_with(prefix.as_str()) {
2289 continue;
2290 }
2291
2292 let name_lower = unit.name.to_lowercase();
2293 let lit_lower = literal.to_lowercase();
2294
2295 let name_matches = if check_start && check_end {
2296 name_lower.contains(&lit_lower)
2298 } else if check_start {
2299 name_lower.ends_with(&lit_lower)
2301 } else if check_end {
2302 name_lower.starts_with(&lit_lower)
2304 } else {
2305 name_lower == lit_lower
2307 };
2308
2309 if !name_matches {
2310 violations.push(DriftViolation {
2311 rule_index: rule_idx,
2312 description: format!(
2313 "Naming convention violation: '{}' does not match pattern '{}'",
2314 unit.name, pattern
2315 ),
2316 units: vec![unit.id],
2317 });
2318 }
2319 }
2320 }
2321 ArchRule::Cyclic { scope } => {
2322 let scoped_ids: HashSet<u64> = graph
2324 .units()
2325 .iter()
2326 .filter(|u| u.qualified_name.starts_with(scope.as_str()))
2327 .map(|u| u.id)
2328 .collect();
2329
2330 if let Some(cycle) = self.detect_cycle(graph, &scoped_ids) {
2331 let description = format!(
2332 "Dependency cycle detected in scope '{}': {}",
2333 scope,
2334 cycle
2335 .iter()
2336 .filter_map(|id| graph.get_unit(*id).map(|u| u.name.clone()))
2337 .collect::<Vec<_>>()
2338 .join(" -> ")
2339 );
2340 violations.push(DriftViolation {
2341 rule_index: rule_idx,
2342 description,
2343 units: cycle,
2344 });
2345 }
2346 }
2347 }
2348 }
2349
2350 let total_rules = params.rules.len();
2351 let violated_rules: HashSet<usize> = violations.iter().map(|v| v.rule_index).collect();
2352 let conformance_score = if total_rules > 0 {
2353 1.0 - (violated_rules.len() as f32 / total_rules as f32)
2354 } else {
2355 1.0
2356 };
2357
2358 Ok(DriftReport {
2359 violations,
2360 conformance_score,
2361 })
2362 }
2363
2364 pub fn similarity(
2370 &self,
2371 graph: &CodeGraph,
2372 params: SimilarityParams,
2373 ) -> AcbResult<Vec<SimilarityMatch>> {
2374 let target = graph
2375 .get_unit(params.unit_id)
2376 .ok_or(AcbError::UnitNotFound(params.unit_id))?;
2377
2378 let target_vec = target.feature_vec.clone();
2379
2380 let mut matches: Vec<SimilarityMatch> = graph
2381 .units()
2382 .iter()
2383 .filter(|u| u.id != params.unit_id)
2384 .filter_map(|u| {
2385 let score = cosine_similarity(&target_vec, &u.feature_vec);
2386 if score >= params.min_similarity {
2387 Some(SimilarityMatch {
2388 unit_id: u.id,
2389 score,
2390 })
2391 } else {
2392 None
2393 }
2394 })
2395 .collect();
2396
2397 matches.sort_by(|a, b| {
2398 b.score
2399 .partial_cmp(&a.score)
2400 .unwrap_or(std::cmp::Ordering::Equal)
2401 });
2402
2403 if params.top_k > 0 {
2404 matches.truncate(params.top_k);
2405 }
2406
2407 Ok(matches)
2408 }
2409
2410 pub fn shortest_path(&self, graph: &CodeGraph, from: u64, to: u64) -> AcbResult<PathResult> {
2416 self.validate_unit(graph, from)?;
2417 self.validate_unit(graph, to)?;
2418
2419 match traversal::shortest_path(graph, from, to, &[]) {
2420 Some(path) => {
2421 let mut edge_types = Vec::new();
2423 for window in path.windows(2) {
2424 let src = window[0];
2425 let tgt = window[1];
2426 let et = graph
2427 .edges_from(src)
2428 .iter()
2429 .find(|e| e.target_id == tgt)
2430 .map(|e| e.edge_type)
2431 .unwrap_or(EdgeType::References);
2432 edge_types.push(et);
2433 }
2434 let length = path.len().saturating_sub(1);
2435 Ok(PathResult {
2436 found: true,
2437 path,
2438 edge_types,
2439 length,
2440 })
2441 }
2442 None => Ok(PathResult {
2443 found: false,
2444 path: Vec::new(),
2445 edge_types: Vec::new(),
2446 length: 0,
2447 }),
2448 }
2449 }
2450
2451 pub fn hotspot_detection(
2457 &self,
2458 graph: &CodeGraph,
2459 params: HotspotParams,
2460 ) -> AcbResult<Vec<Hotspot>> {
2461 let mut hotspots = Vec::new();
2462
2463 for unit in graph.units() {
2464 if !params.unit_types.is_empty() && !params.unit_types.contains(&unit.unit_type) {
2465 continue;
2466 }
2467
2468 let mut factors: HashMap<String, f32> = HashMap::new();
2469
2470 let change_factor = (unit.change_count as f32 / 50.0).min(1.0);
2472 factors.insert("change_frequency".to_string(), change_factor);
2473
2474 let instability = 1.0 - unit.stability_score;
2476 factors.insert("instability".to_string(), instability);
2477
2478 let complexity_factor = (unit.complexity as f32 / 30.0).min(1.0);
2480 factors.insert("complexity".to_string(), complexity_factor);
2481
2482 let coupling_count = graph.edges_from(unit.id).len() + graph.edges_to(unit.id).len();
2484 let coupling_factor = (coupling_count as f32 / 20.0).min(1.0);
2485 factors.insert("coupling".to_string(), coupling_factor);
2486
2487 let score = change_factor * 0.35
2489 + instability * 0.25
2490 + complexity_factor * 0.25
2491 + coupling_factor * 0.15;
2492
2493 if score >= params.min_score {
2494 hotspots.push(Hotspot {
2495 unit_id: unit.id,
2496 score,
2497 factors,
2498 });
2499 }
2500 }
2501
2502 hotspots.sort_by(|a, b| {
2504 b.score
2505 .partial_cmp(&a.score)
2506 .unwrap_or(std::cmp::Ordering::Equal)
2507 });
2508
2509 if params.top_k > 0 {
2510 hotspots.truncate(params.top_k);
2511 }
2512
2513 Ok(hotspots)
2514 }
2515
2516 fn validate_unit(&self, graph: &CodeGraph, unit_id: u64) -> AcbResult<()> {
2522 if graph.get_unit(unit_id).is_none() {
2523 return Err(AcbError::UnitNotFound(unit_id));
2524 }
2525 Ok(())
2526 }
2527
2528 fn collect_call_sites_forward(
2530 &self,
2531 graph: &CodeGraph,
2532 start_id: u64,
2533 max_depth: u32,
2534 call_sites: &mut Vec<CallSite>,
2535 ) {
2536 let mut visited = HashSet::new();
2537 let mut queue = VecDeque::new();
2538 visited.insert(start_id);
2539 queue.push_back((start_id, 0u32));
2540
2541 while let Some((current, depth)) = queue.pop_front() {
2542 if depth >= max_depth {
2543 continue;
2544 }
2545 for edge in graph.edges_from_of_type(current, EdgeType::Calls) {
2546 call_sites.push(CallSite {
2547 caller_id: current,
2548 callee_id: edge.target_id,
2549 span: Span::point(edge.context, 0),
2550 });
2551 if visited.insert(edge.target_id) {
2552 queue.push_back((edge.target_id, depth + 1));
2553 }
2554 }
2555 }
2556 }
2557
2558 fn collect_call_sites_backward(
2560 &self,
2561 graph: &CodeGraph,
2562 start_id: u64,
2563 max_depth: u32,
2564 call_sites: &mut Vec<CallSite>,
2565 ) {
2566 let mut visited = HashSet::new();
2567 let mut queue = VecDeque::new();
2568 visited.insert(start_id);
2569 queue.push_back((start_id, 0u32));
2570
2571 while let Some((current, depth)) = queue.pop_front() {
2572 if depth >= max_depth {
2573 continue;
2574 }
2575 for edge in graph.edges_to_of_type(current, EdgeType::Calls) {
2576 call_sites.push(CallSite {
2577 caller_id: edge.source_id,
2578 callee_id: current,
2579 span: Span::point(edge.context, 0),
2580 });
2581 if visited.insert(edge.source_id) {
2582 queue.push_back((edge.source_id, depth + 1));
2583 }
2584 }
2585 }
2586 }
2587
2588 fn parse_call_list(&self, pattern: &str) -> Vec<String> {
2590 if let Some(start) = pattern.find('[') {
2591 if let Some(end) = pattern.find(']') {
2592 if start < end {
2593 let inner = &pattern[start + 1..end];
2594 return inner
2595 .split(',')
2596 .map(|s| s.trim().to_string())
2597 .filter(|s| !s.is_empty())
2598 .collect();
2599 }
2600 }
2601 }
2602 Vec::new()
2603 }
2604
2605 fn parse_inherits_target(&self, pattern: &str) -> Option<String> {
2607 if let Some(pos) = pattern.find("inherits:") {
2608 let after = &pattern[pos + "inherits:".len()..];
2609 let trimmed = after.trim().trim_end_matches('}').trim();
2610 let name = trimmed.split_whitespace().next()?;
2611 if !name.is_empty() {
2612 return Some(name.to_string());
2613 }
2614 }
2615 None
2616 }
2617
2618 fn parse_complexity_constraint(&self, pattern: &str) -> Option<u32> {
2620 if let Some(pos) = pattern.find("complexity:") {
2621 let after = &pattern[pos + "complexity:".len()..];
2622 let trimmed = after.trim().trim_start_matches('>').trim();
2623 let num_str: String = trimmed.chars().take_while(|c| c.is_ascii_digit()).collect();
2624 return num_str.parse::<u32>().ok();
2625 }
2626 None
2627 }
2628
2629 fn detect_cycle(&self, graph: &CodeGraph, scope: &HashSet<u64>) -> Option<Vec<u64>> {
2633 let mut visited = HashSet::new();
2634 let mut in_stack = HashSet::new();
2635 let mut stack = Vec::new();
2636
2637 for &uid in scope {
2638 if !visited.contains(&uid) {
2639 if let Some(cycle) = self.detect_cycle_dfs(
2640 graph,
2641 uid,
2642 scope,
2643 &mut visited,
2644 &mut in_stack,
2645 &mut stack,
2646 ) {
2647 return Some(cycle);
2648 }
2649 }
2650 }
2651 None
2652 }
2653
2654 #[allow(clippy::only_used_in_recursion)]
2655 fn detect_cycle_dfs(
2656 &self,
2657 graph: &CodeGraph,
2658 uid: u64,
2659 scope: &HashSet<u64>,
2660 visited: &mut HashSet<u64>,
2661 in_stack: &mut HashSet<u64>,
2662 stack: &mut Vec<u64>,
2663 ) -> Option<Vec<u64>> {
2664 visited.insert(uid);
2665 in_stack.insert(uid);
2666 stack.push(uid);
2667
2668 for edge in graph.edges_from(uid) {
2669 if !edge.edge_type.is_dependency() {
2670 continue;
2671 }
2672 let target = edge.target_id;
2673 if !scope.contains(&target) {
2674 continue;
2675 }
2676
2677 if !visited.contains(&target) {
2678 if let Some(cycle) =
2679 self.detect_cycle_dfs(graph, target, scope, visited, in_stack, stack)
2680 {
2681 return Some(cycle);
2682 }
2683 } else if in_stack.contains(&target) {
2684 let pos = stack.iter().position(|&x| x == target)?;
2686 let mut cycle: Vec<u64> = stack[pos..].to_vec();
2687 cycle.push(target); return Some(cycle);
2689 }
2690 }
2691
2692 stack.pop();
2693 in_stack.remove(&uid);
2694 None
2695 }
2696}
2697
2698impl Default for QueryEngine {
2699 fn default() -> Self {
2700 Self::new()
2701 }
2702}
2703
2704fn parse_simple_glob(pattern: &str) -> (bool, bool, bool, String) {
2712 let starts = pattern.starts_with('*');
2713 let ends = pattern.ends_with('*');
2714 let literal = pattern
2715 .trim_start_matches('*')
2716 .trim_end_matches('*')
2717 .to_string();
2718 let contains = starts && ends;
2719 (starts, ends, contains, literal)
2720}
2721
2722fn normalize_pair(a: u64, b: u64) -> (u64, u64) {
2724 if a <= b {
2725 (a, b)
2726 } else {
2727 (b, a)
2728 }
2729}
2730
2731#[cfg(test)]
2736mod tests {
2737 use super::*;
2738 use crate::types::{CodeUnit, CodeUnitType, Edge, EdgeType, Language, Span};
2739 use std::path::PathBuf;
2740
2741 fn build_test_graph() -> CodeGraph {
2743 let mut graph = CodeGraph::with_default_dimension();
2744
2745 let m = CodeUnit::new(
2747 CodeUnitType::Module,
2748 Language::Rust,
2749 "app".to_string(),
2750 "app".to_string(),
2751 PathBuf::from("src/lib.rs"),
2752 Span::new(1, 0, 100, 0),
2753 );
2754 graph.add_unit(m);
2755
2756 let mut f1 = CodeUnit::new(
2758 CodeUnitType::Function,
2759 Language::Rust,
2760 "process".to_string(),
2761 "app::process".to_string(),
2762 PathBuf::from("src/lib.rs"),
2763 Span::new(10, 0, 20, 0),
2764 );
2765 f1.complexity = 5;
2766 f1.visibility = Visibility::Public;
2767 graph.add_unit(f1);
2768
2769 let mut f2 = CodeUnit::new(
2771 CodeUnitType::Function,
2772 Language::Rust,
2773 "helper".to_string(),
2774 "app::helper".to_string(),
2775 PathBuf::from("src/lib.rs"),
2776 Span::new(25, 0, 35, 0),
2777 );
2778 f2.complexity = 2;
2779 f2.visibility = Visibility::Private;
2780 graph.add_unit(f2);
2781
2782 let t = CodeUnit::new(
2784 CodeUnitType::Test,
2785 Language::Rust,
2786 "test_process".to_string(),
2787 "app::test_process".to_string(),
2788 PathBuf::from("src/lib.rs"),
2789 Span::new(40, 0, 50, 0),
2790 );
2791 graph.add_unit(t);
2792
2793 let ty = CodeUnit::new(
2795 CodeUnitType::Type,
2796 Language::Rust,
2797 "Config".to_string(),
2798 "app::Config".to_string(),
2799 PathBuf::from("src/lib.rs"),
2800 Span::new(55, 0, 65, 0),
2801 );
2802 graph.add_unit(ty);
2803
2804 graph.add_edge(Edge::new(0, 1, EdgeType::Contains)).ok();
2807 graph.add_edge(Edge::new(0, 2, EdgeType::Contains)).ok();
2808 graph.add_edge(Edge::new(0, 3, EdgeType::Contains)).ok();
2809 graph.add_edge(Edge::new(0, 4, EdgeType::Contains)).ok();
2810
2811 graph
2813 .add_edge(Edge::new(1, 2, EdgeType::Calls).with_context(15))
2814 .ok();
2815
2816 graph.add_edge(Edge::new(3, 1, EdgeType::Tests)).ok();
2818
2819 graph
2820 }
2821
2822 #[test]
2823 fn test_symbol_lookup_exact() {
2824 let graph = build_test_graph();
2825 let engine = QueryEngine::new();
2826
2827 let params = SymbolLookupParams {
2828 name: "process".to_string(),
2829 mode: MatchMode::Exact,
2830 ..Default::default()
2831 };
2832
2833 let result = engine.symbol_lookup(&graph, params).expect("lookup failed");
2834 assert_eq!(result.len(), 1);
2835 assert_eq!(result[0].name, "process");
2836 }
2837
2838 #[test]
2839 fn test_symbol_lookup_prefix() {
2840 let graph = build_test_graph();
2841 let engine = QueryEngine::new();
2842
2843 let params = SymbolLookupParams {
2844 name: "proc".to_string(),
2845 mode: MatchMode::Prefix,
2846 ..Default::default()
2847 };
2848
2849 let result = engine.symbol_lookup(&graph, params).expect("lookup failed");
2850 assert_eq!(result.len(), 1);
2851 }
2852
2853 #[test]
2854 fn test_symbol_lookup_contains() {
2855 let graph = build_test_graph();
2856 let engine = QueryEngine::new();
2857
2858 let params = SymbolLookupParams {
2859 name: "elp".to_string(),
2860 mode: MatchMode::Contains,
2861 ..Default::default()
2862 };
2863
2864 let result = engine.symbol_lookup(&graph, params).expect("lookup failed");
2865 assert_eq!(result.len(), 1);
2866 assert_eq!(result[0].name, "helper");
2867 }
2868
2869 #[test]
2870 fn test_containment() {
2871 let graph = build_test_graph();
2872 let engine = QueryEngine::new();
2873
2874 let result = engine.containment(&graph, 0).expect("containment failed");
2875 assert_eq!(result.len(), 4); }
2877
2878 #[test]
2879 fn test_call_graph_callees() {
2880 let graph = build_test_graph();
2881 let engine = QueryEngine::new();
2882
2883 let params = CallGraphParams {
2884 unit_id: 1,
2885 direction: CallDirection::Callees,
2886 max_depth: 3,
2887 };
2888
2889 let result = engine
2890 .call_graph(&graph, params)
2891 .expect("call graph failed");
2892 assert!(result.nodes.len() >= 2); assert!(!result.call_sites.is_empty());
2894 }
2895
2896 #[test]
2897 fn test_test_coverage() {
2898 let graph = build_test_graph();
2899 let engine = QueryEngine::new();
2900
2901 let result = engine.test_coverage(&graph, 1).expect("coverage failed");
2902 assert_eq!(result.direct_tests.len(), 1);
2903 assert_eq!(result.direct_tests[0], 3);
2904 }
2905
2906 #[test]
2907 fn test_shortest_path_found() {
2908 let graph = build_test_graph();
2909 let engine = QueryEngine::new();
2910
2911 let result = engine.shortest_path(&graph, 1, 2).expect("path failed");
2912 assert!(result.found);
2913 assert_eq!(result.path, vec![1, 2]);
2914 assert_eq!(result.length, 1);
2915 }
2916
2917 #[test]
2918 fn test_shortest_path_not_found() {
2919 let graph = build_test_graph();
2920 let engine = QueryEngine::new();
2921
2922 let result = engine.shortest_path(&graph, 2, 0).expect("path failed");
2924 assert!(!result.found);
2925 }
2926
2927 #[test]
2928 fn test_dependency_graph() {
2929 let graph = build_test_graph();
2930 let engine = QueryEngine::new();
2931
2932 let params = DependencyParams {
2933 unit_id: 1,
2934 max_depth: 3,
2935 edge_types: vec![EdgeType::Calls],
2936 include_transitive: true,
2937 };
2938
2939 let result = engine
2940 .dependency_graph(&graph, params)
2941 .expect("dep graph failed");
2942 assert_eq!(result.root_id, 1);
2943 assert!(!result.nodes.is_empty());
2944 }
2945
2946 #[test]
2947 fn test_reverse_dependency() {
2948 let graph = build_test_graph();
2949 let engine = QueryEngine::new();
2950
2951 let params = DependencyParams {
2952 unit_id: 2,
2953 max_depth: 3,
2954 edge_types: vec![EdgeType::Calls],
2955 include_transitive: true,
2956 };
2957
2958 let result = engine
2959 .reverse_dependency(&graph, params)
2960 .expect("rev dep failed");
2961 assert!(result.nodes.iter().any(|n| n.unit_id == 1));
2963 }
2964
2965 #[test]
2966 fn test_levenshtein() {
2967 assert_eq!(levenshtein("kitten", "sitting"), 3);
2968 assert_eq!(levenshtein("", "abc"), 3);
2969 assert_eq!(levenshtein("abc", "abc"), 0);
2970 assert_eq!(levenshtein("abc", ""), 3);
2971 }
2972
2973 #[test]
2974 fn test_cosine_similarity_identical() {
2975 let a = vec![1.0, 0.0, 1.0];
2976 let b = vec![1.0, 0.0, 1.0];
2977 let sim = cosine_similarity(&a, &b);
2978 assert!((sim - 1.0).abs() < 1e-5);
2979 }
2980
2981 #[test]
2982 fn test_cosine_similarity_orthogonal() {
2983 let a = vec![1.0, 0.0];
2984 let b = vec![0.0, 1.0];
2985 let sim = cosine_similarity(&a, &b);
2986 assert!(sim.abs() < 1e-5);
2987 }
2988
2989 #[test]
2990 fn test_unit_not_found_error() {
2991 let graph = build_test_graph();
2992 let engine = QueryEngine::new();
2993
2994 let result = engine.containment(&graph, 999);
2995 assert!(result.is_err());
2996 }
2997}