1use crate::architectural_intent::ArchitecturalIntentTracker;
4use crate::cache_manager::CacheManager;
5use crate::codebase_scanner::CodebaseScanner;
6use crate::context_builder::ContextBuilder;
7use crate::dependency_analyzer::DependencyAnalyzer;
8use crate::error::ResearchError;
9use crate::models::{
10 ArchitecturalIntent, ArchitecturalStyle, CaseStyle, CodeContext, DocFormat, DocumentationStyle,
11 FormattingStyle, ImportGroup, ImportOrganization, IndentType, NamingConventions,
12 ProjectContext, SearchResult, StandardsProfile,
13};
14use crate::pattern_detector::PatternDetector;
15use crate::project_analyzer::ProjectAnalyzer;
16use crate::semantic_index::SemanticIndex;
17use crate::standards_detector::StandardsDetector;
18use std::path::Path;
19use std::sync::Arc;
20use tracing::{debug, info, warn};
21
22#[derive(Debug, Clone)]
27pub struct ResearchManager {
28 project_analyzer: Arc<ProjectAnalyzer>,
30 pattern_detector: Arc<PatternDetector>,
32 standards_detector: Arc<StandardsDetector>,
34 architectural_intent_tracker: Arc<ArchitecturalIntentTracker>,
36 context_builder: Arc<ContextBuilder>,
38 dependency_analyzer: Arc<DependencyAnalyzer>,
40 cache_manager: Arc<CacheManager>,
42}
43
44impl ResearchManager {
45 pub fn new() -> Self {
47 ResearchManager {
48 project_analyzer: Arc::new(ProjectAnalyzer::new()),
49 pattern_detector: Arc::new(PatternDetector::new()),
50 standards_detector: Arc::new(StandardsDetector::new()),
51 architectural_intent_tracker: Arc::new(ArchitecturalIntentTracker::new()),
52 context_builder: Arc::new(ContextBuilder::new(8000)), dependency_analyzer: Arc::new(DependencyAnalyzer::new()),
54 cache_manager: Arc::new(CacheManager::new()),
55 }
56 }
57
58 pub fn with_config(
60 project_analyzer: ProjectAnalyzer,
61 pattern_detector: PatternDetector,
62 standards_detector: StandardsDetector,
63 architectural_intent_tracker: ArchitecturalIntentTracker,
64 context_builder: ContextBuilder,
65 dependency_analyzer: DependencyAnalyzer,
66 cache_manager: CacheManager,
67 ) -> Self {
68 ResearchManager {
69 project_analyzer: Arc::new(project_analyzer),
70 pattern_detector: Arc::new(pattern_detector),
71 standards_detector: Arc::new(standards_detector),
72 architectural_intent_tracker: Arc::new(architectural_intent_tracker),
73 context_builder: Arc::new(context_builder),
74 dependency_analyzer: Arc::new(dependency_analyzer),
75 cache_manager: Arc::new(cache_manager),
76 }
77 }
78
79 pub async fn analyze_project(&self, root: &Path) -> Result<ProjectContext, ResearchError> {
97 if !root.exists() {
99 return Err(ResearchError::ProjectNotFound {
100 path: root.to_path_buf(),
101 reason: "Directory does not exist or is not accessible".to_string(),
102 });
103 }
104
105 debug!("Starting project analysis for {:?}", root);
106
107 use std::collections::HashMap;
109 use std::time::SystemTime;
110 let empty_mtimes: HashMap<std::path::PathBuf, SystemTime> = HashMap::new();
111 if let Ok(Some(cached)) = self.cache_manager.get(root, &empty_mtimes) {
112 info!("Using cached analysis for {:?}", root);
113 return Ok(cached);
114 }
115
116 debug!("Step 1: Detecting project type");
118 let project_type = self.project_analyzer.detect_type(root).map_err(|e| {
119 warn!("Failed to detect project type: {}", e);
120 e
121 })?;
122 debug!("Detected project type: {:?}", project_type);
123
124 debug!("Step 2: Analyzing project structure");
126 let structure = self.project_analyzer.analyze_structure(root).map_err(|e| {
127 warn!("Failed to analyze project structure: {}", e);
128 e
129 })?;
130 debug!("Found {} source directories", structure.source_dirs.len());
131
132 debug!("Step 3: Identifying frameworks");
134 let frameworks = self
135 .project_analyzer
136 .identify_frameworks(root)
137 .unwrap_or_default();
138 debug!("Identified {} frameworks", frameworks.len());
139
140 debug!("Step 4: Scanning codebase");
142 let scan_result = CodebaseScanner::scan(root).map_err(|e| {
143 warn!("Failed to scan codebase: {}", e);
144 e
145 })?;
146 debug!("Found {} files in codebase", scan_result.files.len());
147
148 debug!("Step 5: Building semantic index");
150 let mut semantic_index = SemanticIndex::new();
151
152 for file_meta in &scan_result.files {
154 if let Some(language) = &file_meta.language {
155 if let Ok(content) = std::fs::read_to_string(&file_meta.path) {
156 if let Ok(symbols) = crate::symbol_extractor::SymbolExtractor::extract_symbols(
157 &file_meta.path,
158 language,
159 &content,
160 ) {
161 for symbol in symbols {
162 semantic_index.add_symbol(symbol);
163 }
164 }
165 }
166 }
167 }
168 debug!("Built semantic index with symbols");
169
170 debug!("Step 6: Tracking cross-file references");
172 let mut known_symbols: std::collections::HashMap<String, String> =
173 std::collections::HashMap::new();
174 for symbol in semantic_index.all_symbols() {
175 known_symbols.insert(symbol.name.clone(), symbol.id.clone());
176 }
177
178 for file_meta in &scan_result.files {
179 if let Some(language) = &file_meta.language {
180 if let Ok(content) = std::fs::read_to_string(&file_meta.path) {
181 if let Ok(references) =
182 crate::reference_tracker::ReferenceTracker::track_references(
183 &file_meta.path,
184 language,
185 &content,
186 &known_symbols,
187 )
188 {
189 for reference in references {
190 semantic_index.add_reference(reference);
191 }
192 }
193 }
194 }
195 }
196 debug!("Tracked cross-file references");
197
198 debug!("Step 7: Detecting patterns");
200 let patterns = self
201 .pattern_detector
202 .detect(&scan_result)
203 .unwrap_or_default();
204 debug!("Detected {} patterns", patterns.len());
205
206 debug!("Step 8: Detecting standards and conventions");
208 let file_paths: Vec<&Path> = scan_result.files.iter().map(|f| f.path.as_path()).collect();
209 let standards = self
210 .standards_detector
211 .detect(&file_paths)
212 .unwrap_or_else(|_| StandardsProfile {
213 naming_conventions: NamingConventions {
214 function_case: CaseStyle::Mixed,
215 variable_case: CaseStyle::Mixed,
216 class_case: CaseStyle::Mixed,
217 constant_case: CaseStyle::Mixed,
218 },
219 formatting_style: FormattingStyle {
220 indent_size: 4,
221 indent_type: IndentType::Spaces,
222 line_length: 100,
223 },
224 import_organization: ImportOrganization {
225 order: vec![
226 ImportGroup::Standard,
227 ImportGroup::External,
228 ImportGroup::Internal,
229 ],
230 sort_within_group: false,
231 },
232 documentation_style: DocumentationStyle {
233 format: DocFormat::RustDoc,
234 required_for_public: false,
235 },
236 });
237 debug!("Detected standards and conventions");
238
239 debug!("Step 9: Analyzing dependencies");
241 let dependencies = self.dependency_analyzer.analyze(root).unwrap_or_default();
242 debug!("Found {} dependencies", dependencies.len());
243
244 debug!("Step 10: Tracking architectural intent");
246 let architectural_style = self
247 .architectural_intent_tracker
248 .infer_style(root)
249 .unwrap_or(ArchitecturalStyle::Unknown);
250 let architectural_intent = ArchitecturalIntent {
251 style: architectural_style,
252 principles: vec![],
253 constraints: vec![],
254 decisions: vec![],
255 };
256 debug!(
257 "Inferred architectural style: {:?}",
258 architectural_intent.style
259 );
260
261 debug!("Step 11: Building final project context");
263 let context = ProjectContext {
264 project_type,
265 languages: scan_result.languages,
266 frameworks,
267 structure,
268 patterns,
269 dependencies,
270 architectural_intent,
271 standards,
272 };
273
274 debug!("Caching analysis results");
276 let file_mtimes: HashMap<std::path::PathBuf, SystemTime> = scan_result
277 .files
278 .iter()
279 .filter_map(|f| {
280 std::fs::metadata(&f.path)
281 .ok()
282 .and_then(|m| m.modified().ok())
283 .map(|mtime| (f.path.clone(), mtime))
284 })
285 .collect();
286
287 if let Err(e) = self.cache_manager.set(root, &context, file_mtimes) {
288 warn!("Failed to cache analysis results: {}", e);
289 }
291
292 info!("Project analysis completed successfully for {:?}", root);
293 Ok(context)
294 }
295
296 pub async fn search_codebase(
310 &self,
311 query: &str,
312 semantic_index: &SemanticIndex,
313 ) -> Result<Vec<SearchResult>, ResearchError> {
314 if query.is_empty() {
315 return Err(ResearchError::SearchFailed {
316 query: query.to_string(),
317 reason: "Query string cannot be empty".to_string(),
318 });
319 }
320
321 debug!("Searching codebase for: {}", query);
322
323 let results = semantic_index.search_by_name(query);
325
326 debug!("Found {} search results", results.len());
327 Ok(results)
328 }
329
330 pub async fn get_context_for_query(
345 &self,
346 query: &str,
347 all_files: Vec<crate::models::FileContext>,
348 ) -> Result<CodeContext, ResearchError> {
349 if query.is_empty() {
350 return Err(ResearchError::AnalysisFailed {
351 reason: "Query string cannot be empty".to_string(),
352 context: "Context building requires a non-empty query to select relevant files"
353 .to_string(),
354 });
355 }
356
357 debug!("Building context for query: {}", query);
358
359 let relevant_files = self
361 .context_builder
362 .select_relevant_files(query, all_files)
363 .map_err(|e| {
364 warn!("Failed to select relevant files: {}", e);
365 e
366 })?;
367
368 debug!("Selected {} relevant files", relevant_files.len());
369
370 let context = self
372 .context_builder
373 .build_context(relevant_files)
374 .map_err(|e| {
375 warn!("Failed to build context: {}", e);
376 e
377 })?;
378
379 debug!("Built context with {} tokens", context.total_tokens);
380 Ok(context)
381 }
382
383 pub fn get_cache_statistics(
389 &self,
390 ) -> Result<crate::cache_manager::CacheStatistics, ResearchError> {
391 self.cache_manager.statistics()
392 }
393
394 pub fn clear_cache(&self) -> Result<(), ResearchError> {
400 self.cache_manager.clear()
401 }
402}
403
404impl Default for ResearchManager {
405 fn default() -> Self {
406 Self::new()
407 }
408}
409
410#[derive(Debug)]
412pub struct ResearchManagerBuilder {
413 project_analyzer: Option<ProjectAnalyzer>,
414 pattern_detector: Option<PatternDetector>,
415 standards_detector: Option<StandardsDetector>,
416 architectural_intent_tracker: Option<ArchitecturalIntentTracker>,
417 context_builder: Option<ContextBuilder>,
418 dependency_analyzer: Option<DependencyAnalyzer>,
419 cache_manager: Option<CacheManager>,
420}
421
422impl ResearchManagerBuilder {
423 pub fn new() -> Self {
425 ResearchManagerBuilder {
426 project_analyzer: None,
427 pattern_detector: None,
428 standards_detector: None,
429 architectural_intent_tracker: None,
430 context_builder: None,
431 dependency_analyzer: None,
432 cache_manager: None,
433 }
434 }
435
436 pub fn with_project_analyzer(mut self, analyzer: ProjectAnalyzer) -> Self {
438 self.project_analyzer = Some(analyzer);
439 self
440 }
441
442 pub fn with_pattern_detector(mut self, detector: PatternDetector) -> Self {
444 self.pattern_detector = Some(detector);
445 self
446 }
447
448 pub fn with_standards_detector(mut self, detector: StandardsDetector) -> Self {
450 self.standards_detector = Some(detector);
451 self
452 }
453
454 pub fn with_architectural_intent_tracker(
456 mut self,
457 tracker: ArchitecturalIntentTracker,
458 ) -> Self {
459 self.architectural_intent_tracker = Some(tracker);
460 self
461 }
462
463 pub fn with_context_builder(mut self, builder: ContextBuilder) -> Self {
465 self.context_builder = Some(builder);
466 self
467 }
468
469 pub fn with_dependency_analyzer(mut self, analyzer: DependencyAnalyzer) -> Self {
471 self.dependency_analyzer = Some(analyzer);
472 self
473 }
474
475 pub fn with_cache_manager(mut self, manager: CacheManager) -> Self {
477 self.cache_manager = Some(manager);
478 self
479 }
480
481 pub fn build(self) -> ResearchManager {
483 ResearchManager::with_config(
484 self.project_analyzer.unwrap_or_default(),
485 self.pattern_detector.unwrap_or_default(),
486 self.standards_detector.unwrap_or_default(),
487 self.architectural_intent_tracker.unwrap_or_default(),
488 self.context_builder
489 .unwrap_or_else(|| ContextBuilder::new(8000)),
490 self.dependency_analyzer.unwrap_or_default(),
491 self.cache_manager.unwrap_or_default(),
492 )
493 }
494}
495
496impl Default for ResearchManagerBuilder {
497 fn default() -> Self {
498 Self::new()
499 }
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505
506 #[test]
507 fn test_research_manager_creation() {
508 let manager = ResearchManager::new();
509 assert!(Arc::strong_count(&manager.project_analyzer) > 0);
511 }
512
513 #[test]
514 fn test_research_manager_default() {
515 let manager = ResearchManager::default();
516 assert!(Arc::strong_count(&manager.project_analyzer) > 0);
517 }
518
519 #[test]
520 fn test_research_manager_builder() {
521 let manager = ResearchManagerBuilder::new()
522 .with_project_analyzer(ProjectAnalyzer::new())
523 .with_pattern_detector(PatternDetector::new())
524 .build();
525 assert!(Arc::strong_count(&manager.project_analyzer) > 0);
526 }
527
528 #[test]
529 fn test_research_manager_builder_default() {
530 let manager = ResearchManagerBuilder::default().build();
531 assert!(Arc::strong_count(&manager.project_analyzer) > 0);
532 }
533
534 #[tokio::test]
535 async fn test_analyze_project_nonexistent_path() {
536 let manager = ResearchManager::new();
537 let result = manager
538 .analyze_project(Path::new("/nonexistent/path"))
539 .await;
540 assert!(result.is_err());
541 match result {
542 Err(ResearchError::ProjectNotFound { path: _, reason: _ }) => (),
543 _ => panic!("Expected ProjectNotFound error"),
544 }
545 }
546
547 #[tokio::test]
548 async fn test_search_codebase_empty_query() {
549 let manager = ResearchManager::new();
550 let semantic_index = SemanticIndex::new();
551 let result = manager.search_codebase("", &semantic_index).await;
552 assert!(result.is_err());
553 }
554
555 #[tokio::test]
556 async fn test_get_context_for_query_empty_query() {
557 let manager = ResearchManager::new();
558 let result = manager.get_context_for_query("", vec![]).await;
559 assert!(result.is_err());
560 }
561
562 #[test]
563 fn test_cache_statistics() {
564 let manager = ResearchManager::new();
565 let stats = manager.get_cache_statistics();
566 assert!(stats.is_ok());
567 if let Ok(s) = stats {
568 assert_eq!(s.hits, 0);
569 assert_eq!(s.misses, 0);
570 }
571 }
572
573 #[test]
574 fn test_clear_cache() {
575 let manager = ResearchManager::new();
576 let result = manager.clear_cache();
577 assert!(result.is_ok());
578 }
579}