graphyn_mcp/
context_builder.rs1use graphyn_core::graph::GraphynGraph;
7use graphyn_core::ir::SymbolKind;
8use graphyn_core::query::QueryEdge;
9
10use std::collections::BTreeMap;
11
12pub fn format_blast_radius(
15 graph: &GraphynGraph,
16 symbol: &str,
17 file: Option<&str>,
18 depth: usize,
19 edges: &[QueryEdge],
20) -> String {
21 let mut out = String::new();
22
23 let header = symbol_header(graph, symbol, file);
25 out.push_str(&header);
26 out.push_str(&format!(
27 "\nBlast radius ({} dependent(s), depth={}):\n",
28 edges.len(),
29 depth
30 ));
31
32 if edges.is_empty() {
33 out.push_str("\nNo dependents found — safe to modify.\n");
34 return out;
35 }
36
37 let (direct, aliased) = partition_by_alias(edges);
38
39 if !direct.is_empty() {
41 out.push_str(&format!("\nDIRECT (imports/uses {} directly):\n", symbol));
42 for edge in &direct {
43 out.push_str(&format_blast_edge(graph, edge));
44 }
45 }
46
47 if !aliased.is_empty() {
49 out.push_str("\nALIASED (imports under different name — HIGH RISK):\n");
50 for edge in &aliased {
51 out.push_str(&format_blast_edge(graph, edge));
52 }
53 }
54
55 let props = collect_property_summary(edges);
57 if !props.is_empty() {
58 out.push_str("\nProperties at risk if changed:\n");
59 for (prop, count) in &props {
60 let aliased_note = if is_aliased_only_property(edges, prop) {
61 " (aliased import only)"
62 } else {
63 ""
64 };
65 out.push_str(&format!(
66 " .{:<16} → referenced in {} file(s){}\n",
67 prop, count, aliased_note
68 ));
69 }
70 }
71
72 out
73}
74
75fn format_blast_edge(graph: &GraphynGraph, edge: &QueryEdge) -> String {
76 let mut out = String::new();
77 out.push_str(&format!(" • {}:{}\n", edge.file, edge.line));
78
79 if let Some(alias) = &edge.alias {
80 out.push_str(&format!(" → imports as {} ← ALIAS\n", alias));
81 } else if let Some(sym) = graph.symbols.get(&edge.from) {
82 out.push_str(&format!(" → imports as {}\n", sym.name));
83 }
84
85 if !edge.properties_accessed.is_empty() {
86 let props: Vec<String> = edge
87 .properties_accessed
88 .iter()
89 .map(|p| format!(".{p}"))
90 .collect();
91 out.push_str(&format!(" → accesses: {}\n", props.join(", ")));
92 }
93
94 if !edge.context.is_empty() && edge.context != "import" && edge.context != "property access" {
95 let ctx = if edge.context.len() > 80 {
96 format!("{}…", &edge.context[..80])
97 } else {
98 edge.context.clone()
99 };
100 out.push_str(&format!(" → context: \"{}\"\n", ctx));
101 }
102
103 out
104}
105
106pub fn format_dependencies(
109 graph: &GraphynGraph,
110 symbol: &str,
111 file: Option<&str>,
112 depth: usize,
113 edges: &[QueryEdge],
114) -> String {
115 let mut out = String::new();
116
117 let header = symbol_header(graph, symbol, file);
118 out.push_str(&header);
119 out.push_str(&format!(
120 "\nDependencies ({} found, depth={}):\n",
121 edges.len(),
122 depth
123 ));
124
125 if edges.is_empty() {
126 out.push_str("\nNo dependencies found — this symbol is self-contained.\n");
127 return out;
128 }
129
130 for edge in edges {
131 let dep_name = graph
132 .symbols
133 .get(&edge.to)
134 .map(|s| s.name.clone())
135 .unwrap_or_else(|| edge.to.clone());
136 let dep_kind = graph
137 .symbols
138 .get(&edge.to)
139 .map(|s| format_kind(&s.kind).to_string())
140 .unwrap_or_default();
141
142 out.push_str(&format!(
143 " • {} [{}] — {}:{}\n",
144 dep_name, dep_kind, edge.file, edge.line
145 ));
146 if let Some(alias) = &edge.alias {
147 out.push_str(&format!(" → via alias {}\n", alias));
148 }
149 if edge.hop > 1 {
150 out.push_str(&format!(" → (hop {})\n", edge.hop));
151 }
152 }
153
154 out
155}
156
157pub fn format_symbol_usages(
160 graph: &GraphynGraph,
161 symbol: &str,
162 file: Option<&str>,
163 edges: &[QueryEdge],
164) -> String {
165 let mut out = String::new();
166
167 let header = symbol_header(graph, symbol, file);
168 out.push_str(&header);
169 out.push_str(&format!(
170 "\nUsages ({} found, including aliases):\n",
171 edges.len()
172 ));
173
174 if edges.is_empty() {
175 out.push_str("\nNo usages found.\n");
176 return out;
177 }
178
179 for edge in edges {
180 out.push_str(&format!(" • {}:{}\n", edge.file, edge.line));
181 if let Some(alias) = &edge.alias {
182 out.push_str(&format!(" → imports as {} ← ALIAS\n", alias));
183 }
184 if !edge.properties_accessed.is_empty() {
185 let props: Vec<String> = edge
186 .properties_accessed
187 .iter()
188 .map(|p| format!(".{p}"))
189 .collect();
190 out.push_str(&format!(" → accesses: {}\n", props.join(", ")));
191 }
192 if !edge.context.is_empty() && edge.context != "import" && edge.context != "property access"
193 {
194 let ctx = if edge.context.len() > 80 {
195 format!("{}…", &edge.context[..80])
196 } else {
197 edge.context.clone()
198 };
199 out.push_str(&format!(" → context: \"{}\"\n", ctx));
200 }
201 }
202
203 out
204}
205
206fn symbol_header(graph: &GraphynGraph, symbol: &str, file: Option<&str>) -> String {
209 if let Some(ids) = graph.name_index.get(symbol) {
210 let target_id = if let Some(file) = file {
211 ids.iter().find(|id| {
212 graph
213 .symbols
214 .get(*id)
215 .map(|s| s.file == file)
216 .unwrap_or(false)
217 })
218 } else {
219 ids.first()
220 };
221
222 if let Some(id) = target_id {
223 if let Some(sym) = graph.symbols.get(id) {
224 return format!(
225 "Symbol: {} [{}] — {}:{}",
226 sym.name,
227 format_kind(&sym.kind),
228 sym.file,
229 sym.line_start,
230 );
231 }
232 }
233 }
234 format!("Symbol: {}", symbol)
235}
236
237fn partition_by_alias(edges: &[QueryEdge]) -> (Vec<&QueryEdge>, Vec<&QueryEdge>) {
238 let mut direct = Vec::new();
239 let mut aliased = Vec::new();
240 for edge in edges {
241 if edge.alias.is_some() {
242 aliased.push(edge);
243 } else {
244 direct.push(edge);
245 }
246 }
247 (direct, aliased)
248}
249
250fn collect_property_summary(edges: &[QueryEdge]) -> Vec<(String, usize)> {
251 let mut counts: BTreeMap<String, usize> = BTreeMap::new();
252 for edge in edges {
253 for prop in &edge.properties_accessed {
254 *counts.entry(prop.clone()).or_insert(0) += 1;
255 }
256 }
257 let mut sorted: Vec<_> = counts.into_iter().collect();
258 sorted.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
259 sorted
260}
261
262fn is_aliased_only_property(edges: &[QueryEdge], property: &str) -> bool {
263 edges
264 .iter()
265 .filter(|e| e.properties_accessed.contains(&property.to_string()))
266 .all(|e| e.alias.is_some())
267}
268
269fn format_kind(kind: &SymbolKind) -> &'static str {
270 match kind {
271 SymbolKind::Class => "class",
272 SymbolKind::Interface => "interface",
273 SymbolKind::TypeAlias => "type",
274 SymbolKind::Function => "function",
275 SymbolKind::Method => "method",
276 SymbolKind::Property => "property",
277 SymbolKind::Variable => "variable",
278 SymbolKind::Module => "module",
279 SymbolKind::Enum => "enum",
280 SymbolKind::EnumVariant => "variant",
281 }
282}