1use crate::CodememEngine;
8use codemem_core::{CodememError, MemoryNode, NodeKind};
9use std::collections::HashSet;
10
11#[derive(Debug, Clone)]
15pub struct PagerankEntry {
16 pub node_id: String,
17 pub label: String,
18 pub score: f64,
19}
20
21#[derive(Debug, Clone)]
23pub struct CouplingNode {
24 pub node_id: String,
25 pub label: String,
26 pub coupling_score: usize,
27}
28
29#[derive(Debug, Clone)]
31pub struct GitSummary {
32 pub total_annotated_files: usize,
33 pub top_authors: Vec<String>,
34}
35
36#[derive(Debug, Clone)]
38pub struct ActivityInsights {
39 pub insights: Vec<MemoryNode>,
40 pub git_summary: GitSummary,
41}
42
43#[derive(Debug, Clone)]
45pub struct CodeHealthInsights {
46 pub insights: Vec<MemoryNode>,
47 pub file_hotspots: Vec<(String, usize, Vec<String>)>,
48 pub decision_chains: Vec<(String, usize, Vec<String>)>,
49 pub pagerank_leaders: Vec<PagerankEntry>,
50 pub community_count: usize,
51}
52
53#[derive(Debug, Clone)]
55pub struct SecurityInsights {
56 pub insights: Vec<MemoryNode>,
57 pub sensitive_file_count: usize,
58 pub endpoint_count: usize,
59 pub security_function_count: usize,
60}
61
62#[derive(Debug, Clone)]
64pub struct PerformanceInsights {
65 pub insights: Vec<MemoryNode>,
66 pub high_coupling_nodes: Vec<CouplingNode>,
67 pub max_depth: usize,
68 pub critical_path: Vec<PagerankEntry>,
69}
70
71impl CodememEngine {
74 pub fn activity_insights(
76 &self,
77 namespace: Option<&str>,
78 limit: usize,
79 ) -> Result<ActivityInsights, CodememError> {
80 let insights = self
81 .storage
82 .list_memories_by_tag("track:activity", namespace, limit)
83 .unwrap_or_default();
84
85 let git_summary = match self.lock_graph() {
86 Ok(graph) => {
87 let all_nodes = graph.get_all_nodes();
88 let mut annotated = 0;
89 let mut author_set: HashSet<String> = HashSet::new();
90 for node in &all_nodes {
91 if node.payload.contains_key("git_commit_count") {
92 annotated += 1;
93 if let Some(authors) =
94 node.payload.get("git_authors").and_then(|a| a.as_array())
95 {
96 for a in authors {
97 if let Some(name) = a.as_str() {
98 author_set.insert(name.to_string());
99 }
100 }
101 }
102 }
103 }
104 let mut top_authors: Vec<String> = author_set.into_iter().collect();
105 top_authors.sort();
106 top_authors.truncate(10);
107 GitSummary {
108 total_annotated_files: annotated,
109 top_authors,
110 }
111 }
112 Err(_) => GitSummary {
113 total_annotated_files: 0,
114 top_authors: Vec::new(),
115 },
116 };
117
118 Ok(ActivityInsights {
119 insights,
120 git_summary,
121 })
122 }
123
124 pub fn code_health_insights(
127 &self,
128 namespace: Option<&str>,
129 limit: usize,
130 ) -> Result<CodeHealthInsights, CodememError> {
131 let mut insights: Vec<MemoryNode> = self
132 .storage
133 .list_memories_by_tag("track:code-health", namespace, limit)
134 .unwrap_or_default();
135
136 if insights.is_empty() {
137 insights = self
138 .storage
139 .list_memories_by_tag("track:performance", namespace, limit)
140 .unwrap_or_default();
141 }
142
143 let file_hotspots = self
144 .storage
145 .get_file_hotspots(2, namespace)
146 .unwrap_or_default();
147
148 let decision_chains = self
149 .storage
150 .get_decision_chains(2, namespace)
151 .unwrap_or_default();
152
153 let (pagerank_leaders, community_count) = match self.lock_graph() {
154 Ok(graph) => {
155 let all_nodes = graph.get_all_nodes();
156 let mut file_pr: Vec<_> = all_nodes
157 .iter()
158 .filter(|n| n.kind == NodeKind::File)
159 .map(|n| PagerankEntry {
160 node_id: n.id.clone(),
161 label: n.label.clone(),
162 score: graph.get_pagerank(&n.id),
163 })
164 .filter(|e| e.score > 0.0)
165 .collect();
166 file_pr.sort_by(|a, b| {
167 b.score
168 .partial_cmp(&a.score)
169 .unwrap_or(std::cmp::Ordering::Equal)
170 });
171 file_pr.truncate(10);
172 let communities = graph.louvain_communities(1.0).len();
173 (file_pr, communities)
174 }
175 Err(_) => (Vec::new(), 0),
176 };
177
178 Ok(CodeHealthInsights {
179 insights,
180 file_hotspots,
181 decision_chains,
182 pagerank_leaders,
183 community_count,
184 })
185 }
186
187 pub fn security_insights(
189 &self,
190 namespace: Option<&str>,
191 limit: usize,
192 ) -> Result<SecurityInsights, CodememError> {
193 let insights = self
194 .storage
195 .list_memories_by_tag("track:security", namespace, limit)
196 .unwrap_or_default();
197
198 let (sensitive_file_count, endpoint_count, security_function_count) = match self
199 .lock_graph()
200 {
201 Ok(graph) => {
202 let all_nodes = graph.get_all_nodes();
203 let mut sensitive = 0;
204 let mut endpoints = 0;
205 let mut sec_fns = 0;
206 for node in &all_nodes {
207 if let Some(flags) = node
208 .payload
209 .get("security_flags")
210 .and_then(|f| f.as_array())
211 {
212 let flag_strs: Vec<&str> =
213 flags.iter().filter_map(|f| f.as_str()).collect();
214 if flag_strs.contains(&"sensitive") || flag_strs.contains(&"auth_related") {
215 sensitive += 1;
216 }
217 if flag_strs.contains(&"exposed_endpoint") {
218 endpoints += 1;
219 }
220 if flag_strs.contains(&"security_function") {
221 sec_fns += 1;
222 }
223 }
224 }
225 (sensitive, endpoints, sec_fns)
226 }
227 Err(_) => (0, 0, 0),
228 };
229
230 Ok(SecurityInsights {
231 insights,
232 sensitive_file_count,
233 endpoint_count,
234 security_function_count,
235 })
236 }
237
238 pub fn performance_insights(
241 &self,
242 namespace: Option<&str>,
243 limit: usize,
244 ) -> Result<PerformanceInsights, CodememError> {
245 let insights = self
246 .storage
247 .list_memories_by_tag("track:performance", namespace, limit)
248 .unwrap_or_default();
249
250 let (high_coupling_nodes, max_depth, critical_path) = match self.lock_graph() {
251 Ok(graph) => {
252 let all_nodes = graph.get_all_nodes();
253
254 let mut coupling_data: Vec<CouplingNode> = Vec::new();
256 for node in &all_nodes {
257 if let Some(score) = node.payload.get("coupling_score").and_then(|v| v.as_u64())
258 {
259 if score > 15 {
260 coupling_data.push(CouplingNode {
261 node_id: node.id.clone(),
262 label: node.label.clone(),
263 coupling_score: score as usize,
264 });
265 }
266 }
267 }
268 coupling_data.sort_by(|a, b| b.coupling_score.cmp(&a.coupling_score));
269 coupling_data.truncate(10);
270
271 let depth = graph.topological_layers().len();
273
274 let mut file_pr: Vec<_> = all_nodes
276 .iter()
277 .filter(|n| n.kind == NodeKind::File)
278 .map(|n| PagerankEntry {
279 node_id: n.id.clone(),
280 label: n.label.clone(),
281 score: graph.get_pagerank(&n.id),
282 })
283 .filter(|e| e.score > 0.0)
284 .collect();
285 file_pr.sort_by(|a, b| {
286 b.score
287 .partial_cmp(&a.score)
288 .unwrap_or(std::cmp::Ordering::Equal)
289 });
290 file_pr.truncate(10);
291
292 (coupling_data, depth, file_pr)
293 }
294 Err(_) => (Vec::new(), 0, Vec::new()),
295 };
296
297 Ok(PerformanceInsights {
298 insights,
299 high_coupling_nodes,
300 max_depth,
301 critical_path,
302 })
303 }
304}