Skip to main content

codemem_engine/enrichment/
architecture.rs

1//! Architecture inference: layering, patterns, circular dependencies.
2
3use 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    /// Infer architectural layers and patterns from the module dependency graph.
11    ///
12    /// Analyzes IMPORTS/CALLS/DEPENDS_ON edges between modules to detect layering
13    /// (e.g., api -> service -> storage) and recognizes common directory patterns
14    /// (controllers/, models/, views/, handlers/).
15    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            // Build module dependency graph from IMPORTS/CALLS edges
27            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        // Detect architectural layers by analyzing dependency direction
58        // Extract top-level directory from node IDs
59        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        // Build directory-level dependency counts
73        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        // Detect layers: directories with no incoming deps are "top" layers
87        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        // Detect common architectural patterns from directory names
146        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        // Detect circular dependencies between directories
180        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}