project_map_cli_rust/core/
orchestrator.rs1use std::path::Path;
2use std::collections::HashMap;
3use ignore::WalkBuilder;
4use crate::core::parser::CodeParser;
5use crate::core::graph::{ProjectGraph, NodeData, NodeType, EdgeType};
6use crate::core::utils::{path_to_fqn, resolve_import_path};
7use crate::error::Result;
8
9pub struct Orchestrator {
10 parser: CodeParser,
11 graph: ProjectGraph,
12}
13
14impl Orchestrator {
15 pub fn new() -> Self {
16 Self {
17 parser: CodeParser::new(),
18 graph: ProjectGraph::new(),
19 }
20 }
21
22 pub fn build_index(&mut self, root: &Path) -> Result<()> {
23 let mut outlines = Vec::new();
24 let mut fqn_to_node = HashMap::new();
25 let mut path_to_node = HashMap::new();
26
27 let walk = WalkBuilder::new(root)
30 .filter_entry(|e| e.file_name() != ".project-map")
31 .build();
32
33 for result in walk {
34 let entry = match result {
35 Ok(e) => e,
36 Err(_) => continue,
37 };
38
39 let path = entry.path();
40 if entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) {
41 let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("");
42 if extension == "py" || extension == "rs" || extension == "ts" || extension == "tsx" || extension == "kt" || extension == "sql" || extension == "vue" {
43 match self.parser.parse_file(path) {
44 Ok(outline) => {
45 let fqn = path_to_fqn(root, path);
46 let file_node = self.graph.add_node(NodeData {
47 path: outline.path.clone(),
48 name: fqn.clone(),
49 kind: "file".to_string(),
50 line: 0,
51 start_byte: 0,
52 end_byte: 0,
53 node_type: NodeType::File,
54 docstring: None,
55 });
56 fqn_to_node.insert(fqn, file_node);
57 path_to_node.insert(outline.path.clone(), file_node);
58
59 for symbol in &outline.symbols {
60 let symbol_node = self.graph.add_node(NodeData {
61 path: outline.path.clone(),
62 name: symbol.name.clone(),
63 kind: symbol.kind.clone(),
64 line: symbol.line,
65 start_byte: symbol.start_byte,
66 end_byte: symbol.end_byte,
67 node_type: NodeType::Symbol,
68 docstring: symbol.docstring.clone(),
69 });
70 self.graph.add_edge(file_node, symbol_node, EdgeType::Contains);
71 }
72 outlines.push(outline);
73 }
74 Err(e) => {
75 if !e.to_string().contains("valid UTF-8") {
77 eprintln!("Error parsing {}: {}", path.display(), e);
78 }
79 }
80 }
81 }
82 }
83 }
84
85 for outline in outlines {
87 if let Some(&from_node) = path_to_node.get(&outline.path) {
88 for imp in outline.imports {
89 if let Some(&to_node) = fqn_to_node.get(&imp) {
91 self.graph.add_edge(from_node, to_node, EdgeType::Imports);
92 } else {
93 let resolved_rel = resolve_import_path(&outline.path, &imp);
95
96 let mut found = false;
98 for ext in &["", ".ts", ".tsx", "/index.ts", "/index.tsx"] {
99 let candidate = format!("{}{}", resolved_rel, ext);
100 if let Some(&to_node) = path_to_node.get(&candidate) {
101 self.graph.add_edge(from_node, to_node, EdgeType::Imports);
102 found = true;
103 break;
104 }
105 }
106
107 if !found {
109 let matching_fqn = fqn_to_node.keys()
110 .find(|&k| k.ends_with(&imp));
111 if let Some(key) = matching_fqn {
112 let &to_node = fqn_to_node.get(key).unwrap();
113 self.graph.add_edge(from_node, to_node, EdgeType::Imports);
114 }
115 }
116 }
117 }
118 }
119 }
120
121 Ok(())
122 }
123
124 pub fn save_index(&self, path: &Path) -> Result<()> {
125 self.graph.save(path)
126 }
127
128 pub fn save_index_versioned(&self, base_dir: &Path) -> Result<()> {
129 use chrono::Local;
130 use std::fs;
131 use std::os::unix::fs::symlink;
132
133 let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string();
134 let backups_dir = base_dir.join("backups");
135 let current_backup_dir = backups_dir.join(×tamp);
136
137 fs::create_dir_all(¤t_backup_dir)?;
138
139 let index_path = current_backup_dir.join(".project-map.json");
140 self.graph.save(&index_path)?;
141
142 if let Ok(entries) = fs::read_dir(&backups_dir) {
144 let mut dirs: Vec<_> = entries.filter_map(|e| e.ok()).collect();
145 dirs.sort_by_key(|e| e.metadata().and_then(|m| m.modified()).unwrap_or_else(|_| std::time::SystemTime::UNIX_EPOCH));
146
147 while dirs.len() > 5 {
148 let oldest = dirs.remove(0);
149 fs::remove_dir_all(oldest.path()).ok();
150 }
151 }
152
153 let latest_link = base_dir.join("latest");
154 if latest_link.exists() {
155 fs::remove_file(&latest_link).ok();
156 fs::remove_dir_all(&latest_link).ok();
157 }
158
159 #[cfg(unix)]
161 {
162 let rel_target = Path::new("backups").join(×tamp);
164 symlink(rel_target, &latest_link)?;
165 }
166
167 #[cfg(not(unix))]
169 {
170 fs::create_dir_all(&latest_link)?;
171 fs::copy(&index_path, latest_link.join(".project-map.json"))?;
172 }
173
174 Ok(())
175 }
176}