codemem_engine/enrichment/
change_impact.rs1use super::EnrichResult;
4use crate::CodememEngine;
5use codemem_core::{CodememError, GraphBackend, NodeKind, RelationshipType};
6use serde_json::json;
7
8impl CodememEngine {
9 pub fn enrich_change_impact(
12 &self,
13 file_path: &str,
14 namespace: Option<&str>,
15 ) -> Result<EnrichResult, CodememError> {
16 let graph = self.lock_graph()?;
17
18 let file_id = format!("file:{file_path}");
19 if graph.get_node(&file_id).ok().flatten().is_none() {
20 return Err(CodememError::NotFound(format!(
21 "File node not found: {file_path}"
22 )));
23 }
24
25 let mut co_changed: Vec<String> = Vec::new();
26 let mut callers: Vec<String> = Vec::new();
27 let mut callees: Vec<String> = Vec::new();
28 let mut test_files: Vec<String> = Vec::new();
29
30 if let Ok(edges) = graph.get_edges(&file_id) {
32 for edge in &edges {
33 match edge.relationship {
34 RelationshipType::CoChanged => {
35 let other = if edge.src == file_id {
36 &edge.dst
37 } else {
38 &edge.src
39 };
40 if let Some(path) = other.strip_prefix("file:") {
41 co_changed.push(path.to_string());
42 }
43 }
44 RelationshipType::Calls => {
45 let other = if edge.src == file_id {
46 callees.push(edge.dst.clone());
47 &edge.dst
48 } else {
49 callers.push(edge.src.clone());
50 &edge.src
51 };
52 let _ = other;
53 }
54 RelationshipType::RelatesTo => {
55 if edge.properties.contains_key("test_mapping") {
57 let other = if edge.src == file_id {
58 &edge.dst
59 } else {
60 &edge.src
61 };
62 if let Ok(Some(node)) = graph.get_node(other) {
63 if node.kind == NodeKind::Test {
64 if let Some(fp) =
65 node.payload.get("file_path").and_then(|v| v.as_str())
66 {
67 test_files.push(fp.to_string());
68 }
69 }
70 }
71 }
72 }
73 _ => {}
74 }
75 }
76 }
77
78 let all_nodes = graph.get_all_nodes();
80 for node in &all_nodes {
81 if !matches!(node.kind, NodeKind::Function | NodeKind::Method) {
82 continue;
83 }
84 let sym_file = node
85 .payload
86 .get("file_path")
87 .and_then(|v| v.as_str())
88 .unwrap_or("");
89 if sym_file != file_path {
90 continue;
91 }
92 if let Ok(edges) = graph.get_edges(&node.id) {
93 for edge in &edges {
94 if edge.relationship == RelationshipType::Calls && edge.dst == node.id {
95 if let Ok(Some(caller_node)) = graph.get_node(&edge.src) {
97 if let Some(fp) = caller_node
98 .payload
99 .get("file_path")
100 .and_then(|v| v.as_str())
101 {
102 if fp != file_path {
103 callers.push(fp.to_string());
104 }
105 }
106 }
107 }
108 }
109 }
110 }
111
112 drop(graph);
113
114 co_changed.sort();
116 co_changed.dedup();
117 callers.sort();
118 callers.dedup();
119 callees.sort();
120 callees.dedup();
121 test_files.sort();
122 test_files.dedup();
123
124 let impact_score = co_changed.len() + callers.len() + callees.len();
125
126 let mut insights_stored = 0;
127
128 if impact_score > 0 {
129 let mut parts: Vec<String> = Vec::new();
130 if !callers.is_empty() {
131 parts.push(format!(
132 "{} caller files ({})",
133 callers.len(),
134 callers
135 .iter()
136 .take(5)
137 .cloned()
138 .collect::<Vec<_>>()
139 .join(", ")
140 ));
141 }
142 if !co_changed.is_empty() {
143 parts.push(format!(
144 "{} co-changed files ({})",
145 co_changed.len(),
146 co_changed
147 .iter()
148 .take(5)
149 .cloned()
150 .collect::<Vec<_>>()
151 .join(", ")
152 ));
153 }
154 if !test_files.is_empty() {
155 parts.push(format!(
156 "{} test files ({})",
157 test_files.len(),
158 test_files.join(", ")
159 ));
160 }
161 let content = format!("Change impact for {}: {}", file_path, parts.join("; "));
162 let importance = (impact_score as f64 / 20.0).clamp(0.4, 0.9);
163 if self
164 .store_insight(&content, "impact", &[], importance, namespace, &[file_id])
165 .is_some()
166 {
167 insights_stored += 1;
168 }
169 }
170
171 self.save_index();
172
173 Ok(EnrichResult {
174 insights_stored,
175 details: json!({
176 "file": file_path,
177 "callers": callers.len(),
178 "callees": callees.len(),
179 "co_changed": co_changed.len(),
180 "test_files": test_files.len(),
181 "impact_score": impact_score,
182 "insights_stored": insights_stored,
183 }),
184 })
185 }
186}