codemem_engine/enrichment/
architecture.rs1use super::EnrichResult;
4use crate::CodememEngine;
5use codemem_core::{CodememError, GraphBackend, NodeKind, RelationshipType};
6use serde_json::json;
7use std::collections::{HashMap, HashSet};
8
9impl CodememEngine {
10 pub fn enrich_architecture(
16 &self,
17 namespace: Option<&str>,
18 ) -> Result<EnrichResult, CodememError> {
19 let all_nodes;
20 let mut module_deps: HashMap<String, HashSet<String>> = HashMap::new();
21
22 {
23 let graph = self.lock_graph()?;
24 all_nodes = graph.get_all_nodes();
25
26 for node in &all_nodes {
28 if !matches!(
29 node.kind,
30 NodeKind::File | NodeKind::Module | NodeKind::Package
31 ) {
32 continue;
33 }
34 if let Ok(edges) = graph.get_edges(&node.id) {
35 for edge in &edges {
36 if !matches!(
37 edge.relationship,
38 RelationshipType::Imports
39 | RelationshipType::Calls
40 | RelationshipType::DependsOn
41 ) {
42 continue;
43 }
44 if edge.src == node.id {
45 module_deps
46 .entry(node.id.clone())
47 .or_default()
48 .insert(edge.dst.clone());
49 }
50 }
51 }
52 }
53 }
54
55 let mut insights_stored = 0;
56
57 fn top_dir(node_id: &str) -> Option<String> {
60 let path = node_id
61 .strip_prefix("file:")
62 .or_else(|| node_id.strip_prefix("pkg:"))
63 .unwrap_or(node_id);
64 let parts: Vec<&str> = path.split('/').collect();
65 if parts.len() >= 2 {
66 Some(parts[0].to_string())
67 } else {
68 None
69 }
70 }
71
72 let mut dir_deps: HashMap<String, HashSet<String>> = HashMap::new();
74 for (src, dsts) in &module_deps {
75 if let Some(src_dir) = top_dir(src) {
76 for dst in dsts {
77 if let Some(dst_dir) = top_dir(dst) {
78 if src_dir != dst_dir {
79 dir_deps.entry(src_dir.clone()).or_default().insert(dst_dir);
80 }
81 }
82 }
83 }
84 }
85
86 let all_dirs: HashSet<String> = dir_deps
88 .keys()
89 .chain(dir_deps.values().flat_map(|v| v.iter()))
90 .cloned()
91 .collect();
92 let dirs_with_incoming: HashSet<String> =
93 dir_deps.values().flat_map(|v| v.iter()).cloned().collect();
94 let top_layers: Vec<&String> = all_dirs
95 .iter()
96 .filter(|d| !dirs_with_incoming.contains(*d))
97 .collect();
98 let bottom_layers: Vec<&String> = all_dirs
99 .iter()
100 .filter(|d| !dir_deps.contains_key(*d))
101 .collect();
102
103 if !dir_deps.is_empty() {
104 let mut layer_desc = String::new();
105 if !top_layers.is_empty() {
106 let mut sorted_top: Vec<&&String> = top_layers.iter().collect();
107 sorted_top.sort();
108 layer_desc.push_str(&format!(
109 "Top-level (entry points): {}",
110 sorted_top
111 .iter()
112 .map(|s| s.as_str())
113 .collect::<Vec<_>>()
114 .join(", ")
115 ));
116 }
117 if !bottom_layers.is_empty() {
118 if !layer_desc.is_empty() {
119 layer_desc.push_str("; ");
120 }
121 let mut sorted_bottom: Vec<&&String> = bottom_layers.iter().collect();
122 sorted_bottom.sort();
123 layer_desc.push_str(&format!(
124 "Foundation (no outbound deps): {}",
125 sorted_bottom
126 .iter()
127 .map(|s| s.as_str())
128 .collect::<Vec<_>>()
129 .join(", ")
130 ));
131 }
132 let content = format!(
133 "Architecture: {} module groups with layered dependencies. {}",
134 all_dirs.len(),
135 layer_desc
136 );
137 if self
138 .store_insight(&content, "architecture", &[], 0.9, namespace, &[])
139 .is_some()
140 {
141 insights_stored += 1;
142 }
143 }
144
145 let known_patterns = [
147 ("controllers", "MVC Controller layer"),
148 ("handlers", "Handler/Controller layer"),
149 ("models", "Data model layer"),
150 ("views", "View/Template layer"),
151 ("services", "Service/Business logic layer"),
152 ("api", "API layer"),
153 ("routes", "Routing layer"),
154 ("middleware", "Middleware layer"),
155 ("utils", "Utility/Helper layer"),
156 ("lib", "Library/Core layer"),
157 ];
158
159 let detected: Vec<&str> = known_patterns
160 .iter()
161 .filter(|(name, _)| {
162 all_nodes
163 .iter()
164 .any(|n| n.kind == NodeKind::Package && n.label.contains(name))
165 })
166 .map(|(_, desc)| *desc)
167 .collect();
168
169 if !detected.is_empty() {
170 let content = format!("Architecture patterns detected: {}", detected.join(", "));
171 if self
172 .store_insight(&content, "architecture", &[], 0.7, namespace, &[])
173 .is_some()
174 {
175 insights_stored += 1;
176 }
177 }
178
179 for (dir, deps) in &dir_deps {
181 for dep in deps {
182 if let Some(back_deps) = dir_deps.get(dep) {
183 if back_deps.contains(dir) && dir < dep {
184 let content = format!(
185 "Circular dependency: {} and {} depend on each other — consider refactoring",
186 dir, dep
187 );
188 if self
189 .store_insight(
190 &content,
191 "architecture",
192 &["circular-dep"],
193 0.8,
194 namespace,
195 &[],
196 )
197 .is_some()
198 {
199 insights_stored += 1;
200 }
201 }
202 }
203 }
204 }
205
206 self.save_index();
207
208 Ok(EnrichResult {
209 insights_stored,
210 details: json!({
211 "module_count": all_dirs.len(),
212 "dependency_edges": module_deps.values().map(|v| v.len()).sum::<usize>(),
213 "top_layers": top_layers.len(),
214 "bottom_layers": bottom_layers.len(),
215 "patterns_detected": detected.len(),
216 "insights_stored": insights_stored,
217 }),
218 })
219 }
220}