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