aster/map/server/services/
dependency.rs1use regex::Regex;
6use std::collections::{HashMap, HashSet};
7
8use crate::map::server::types::DependencyTreeNode;
9use crate::map::types_enhanced::{EnhancedCodeBlueprint, ModuleDependency};
10
11static ENTRY_PATTERNS: &[&str] = &[
13 r"cli\.(ts|js)$",
14 r"index\.(ts|js)$",
15 r"main\.(ts|js)$",
16 r"app\.(ts|js)$",
17 r"server\.(ts|js)$",
18 r"entry\.(ts|js)$",
19];
20
21pub fn detect_entry_points(blueprint: &EnhancedCodeBlueprint) -> Vec<String> {
23 let entry_patterns: Vec<Regex> = ENTRY_PATTERNS
24 .iter()
25 .filter_map(|p| Regex::new(p).ok())
26 .collect();
27
28 let mut import_counts: HashMap<String, usize> = HashMap::new();
30 for dep in &blueprint.references.module_deps {
31 *import_counts.entry(dep.target.clone()).or_insert(0) += 1;
32 }
33
34 let mut candidates: Vec<(String, i32)> = Vec::new();
35
36 for module in blueprint.modules.values() {
37 use once_cell::sync::Lazy;
38 static ROOT_PATTERN: Lazy<Regex> =
39 Lazy::new(|| Regex::new(r"^(src/)?[^/]+\.(ts|js)$").unwrap());
40
41 let mut score: i32 = 0;
42
43 for (i, pattern) in entry_patterns.iter().enumerate() {
45 if pattern.is_match(&module.id) {
46 score += ((entry_patterns.len() - i) * 10) as i32;
47 break;
48 }
49 }
50
51 if ROOT_PATTERN.is_match(&module.id) {
53 score += 5;
54 }
55
56 let import_count = import_counts.get(&module.id).copied().unwrap_or(0);
58 if import_count == 0 {
59 score += 20;
60 }
61
62 if !module.imports.is_empty() {
64 score += module.imports.len().min(10) as i32;
65 }
66
67 if score > 0 {
68 candidates.push((module.id.clone(), score));
69 }
70 }
71
72 candidates.sort_by(|a, b| b.1.cmp(&a.1));
73 candidates.into_iter().take(5).map(|(id, _)| id).collect()
74}
75
76pub fn build_dependency_tree(
78 blueprint: &EnhancedCodeBlueprint,
79 entry_id: &str,
80 max_depth: usize,
81) -> Option<DependencyTreeNode> {
82 let _module = blueprint.modules.get(entry_id)?;
83
84 let mut deps_by_source: HashMap<String, Vec<&ModuleDependency>> = HashMap::new();
86 for dep in &blueprint.references.module_deps {
87 deps_by_source
88 .entry(dep.source.clone())
89 .or_default()
90 .push(dep);
91 }
92
93 fn build_node(
94 blueprint: &EnhancedCodeBlueprint,
95 deps_by_source: &HashMap<String, Vec<&ModuleDependency>>,
96 module_id: &str,
97 depth: usize,
98 max_depth: usize,
99 visited: &mut HashSet<String>,
100 ) -> Option<DependencyTreeNode> {
101 let module = blueprint.modules.get(module_id)?;
102 let is_circular = visited.contains(module_id);
103
104 let mut node = DependencyTreeNode {
105 id: module_id.to_string(),
106 name: module.name.clone(),
107 path: module.path.clone(),
108 language: Some(module.language.clone()),
109 lines: Some(module.lines),
110 semantic: module
111 .semantic
112 .as_ref()
113 .map(|s| serde_json::to_value(s).unwrap_or_default()),
114 children: Vec::new(),
115 depth,
116 is_circular: if is_circular { Some(true) } else { None },
117 };
118
119 if is_circular || depth >= max_depth {
120 return Some(node);
121 }
122
123 visited.insert(module_id.to_string());
124
125 if let Some(deps) = deps_by_source.get(module_id) {
126 let mut sorted_deps: Vec<_> = deps.iter().collect();
127 sorted_deps.sort_by(|a, b| a.target.cmp(&b.target));
128
129 for dep in sorted_deps {
130 if blueprint.modules.contains_key(&dep.target) {
131 if let Some(child) = build_node(
132 blueprint,
133 deps_by_source,
134 &dep.target,
135 depth + 1,
136 max_depth,
137 visited,
138 ) {
139 node.children.push(child);
140 }
141 }
142 }
143 }
144
145 visited.remove(module_id);
146 Some(node)
147 }
148
149 let mut visited = HashSet::new();
150 build_node(
151 blueprint,
152 &deps_by_source,
153 entry_id,
154 0,
155 max_depth,
156 &mut visited,
157 )
158}