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