Skip to main content

codemem_engine/
insights.rs

1//! Insight aggregation domain logic.
2//!
3//! These methods compute graph-derived insight summaries (PageRank leaders,
4//! Louvain communities, topology depth, security flags, coupling scores)
5//! so the API/transport layer only formats results.
6
7use crate::CodememEngine;
8use codemem_core::{CodememError, MemoryNode, NodeKind};
9use std::collections::HashSet;
10
11// ── Result Types ─────────────────────────────────────────────────────────────
12
13/// PageRank entry for a graph node.
14#[derive(Debug, Clone)]
15pub struct PagerankEntry {
16    pub node_id: String,
17    pub label: String,
18    pub score: f64,
19}
20
21/// High-coupling node with its coupling score.
22#[derive(Debug, Clone)]
23pub struct CouplingNode {
24    pub node_id: String,
25    pub label: String,
26    pub coupling_score: usize,
27}
28
29/// Git annotation summary from graph node payloads.
30#[derive(Debug, Clone)]
31pub struct GitSummary {
32    pub total_annotated_files: usize,
33    pub top_authors: Vec<String>,
34}
35
36/// Aggregated activity insights.
37#[derive(Debug, Clone)]
38pub struct ActivityInsights {
39    pub insights: Vec<MemoryNode>,
40    pub git_summary: GitSummary,
41}
42
43/// Aggregated code health insights.
44#[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/// Aggregated security insights.
54#[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/// Aggregated performance insights.
63#[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
71// ── Engine Methods ───────────────────────────────────────────────────────────
72
73impl CodememEngine {
74    /// Aggregate activity insights: stored track:activity memories + git annotation summary.
75    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    /// Aggregate code health insights: stored memories, file hotspots, decision chains,
125    /// PageRank leaders, and Louvain community count.
126    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    /// Aggregate security insights: stored memories + security flag counts from graph nodes.
188    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    /// Aggregate performance insights: stored memories, coupling scores,
239    /// topology depth, and PageRank critical path.
240    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                // Coupling scores from annotations
255                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                // Dependency depth from topological layers
272                let depth = graph.topological_layers().len();
273
274                // Critical path from PageRank
275                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}