ricecoder_research/
manager.rs

1//! Research manager - central coordinator for analysis operations
2
3use 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/// Central coordinator for research operations
23///
24/// The ResearchManager orchestrates all analysis components and manages the research lifecycle.
25/// It provides the main public API for research queries.
26#[derive(Debug, Clone)]
27pub struct ResearchManager {
28    /// Project analyzer for detecting project type and structure
29    project_analyzer: Arc<ProjectAnalyzer>,
30    /// Pattern detector for identifying patterns
31    pattern_detector: Arc<PatternDetector>,
32    /// Standards detector for extracting conventions
33    standards_detector: Arc<StandardsDetector>,
34    /// Architectural intent tracker for understanding architecture
35    architectural_intent_tracker: Arc<ArchitecturalIntentTracker>,
36    /// Context builder for selecting relevant files
37    context_builder: Arc<ContextBuilder>,
38    /// Dependency analyzer for multi-language support
39    dependency_analyzer: Arc<DependencyAnalyzer>,
40    /// Cache manager for caching analysis results
41    cache_manager: Arc<CacheManager>,
42}
43
44impl ResearchManager {
45    /// Create a new ResearchManager with default configuration
46    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)), // Default 8000 token limit
53            dependency_analyzer: Arc::new(DependencyAnalyzer::new()),
54            cache_manager: Arc::new(CacheManager::new()),
55        }
56    }
57
58    /// Create a new ResearchManager with custom configuration
59    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    /// Analyze a project and gather comprehensive context
80    ///
81    /// This method performs a multi-stage analysis:
82    /// 1. Detect project type
83    /// 2. Scan codebase and build semantic index
84    /// 3. Detect patterns
85    /// 4. Detect standards and conventions
86    /// 5. Analyze dependencies
87    /// 6. Track architectural intent
88    ///
89    /// # Arguments
90    ///
91    /// * `root` - Root path of the project to analyze
92    ///
93    /// # Returns
94    ///
95    /// A `ProjectContext` containing all gathered information, or a `ResearchError`
96    pub async fn analyze_project(&self, root: &Path) -> Result<ProjectContext, ResearchError> {
97        // Verify project exists
98        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        // Check cache first (with empty file mtimes for initial check)
108        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        // 1. Detect project type
117        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        // 2. Analyze project structure
125        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        // 3. Identify frameworks
133        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        // 4. Scan codebase and build semantic index
141        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        // 5. Build semantic index
149        debug!("Step 5: Building semantic index");
150        let mut semantic_index = SemanticIndex::new();
151
152        // Extract symbols from scanned files
153        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        // 6. Track references
171        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        // 7. Detect patterns
199        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        // 8. Detect standards and conventions
207        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        // 9. Analyze dependencies (multi-language)
240        debug!("Step 9: Analyzing dependencies");
241        let dependencies = self.dependency_analyzer.analyze(root).unwrap_or_default();
242        debug!("Found {} dependencies", dependencies.len());
243
244        // 10. Track architectural intent
245        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        // 11. Build final context
262        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        // Cache results
275        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            // Don't fail the analysis if caching fails
290        }
291
292        info!("Project analysis completed successfully for {:?}", root);
293        Ok(context)
294    }
295
296    /// Search the codebase for symbols and patterns
297    ///
298    /// Performs semantic search across the indexed codebase to find symbols,
299    /// references, and patterns matching the query.
300    ///
301    /// # Arguments
302    ///
303    /// * `query` - Search query string
304    /// * `semantic_index` - The semantic index to search
305    ///
306    /// # Returns
307    ///
308    /// A vector of search results, or a `ResearchError`
309    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        // Use semantic index to search by name
324        let results = semantic_index.search_by_name(query);
325
326        debug!("Found {} search results", results.len());
327        Ok(results)
328    }
329
330    /// Get context for a specific query
331    ///
332    /// Automatically selects and prioritizes relevant files based on the query.
333    /// This method combines semantic search with relevance scoring to provide
334    /// the most relevant context for AI providers.
335    ///
336    /// # Arguments
337    ///
338    /// * `query` - Query string describing what context is needed
339    /// * `all_files` - All available files to select from
340    ///
341    /// # Returns
342    ///
343    /// A `CodeContext` with relevant files and symbols, or a `ResearchError`
344    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        // Select relevant files based on query
360        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        // Build context from selected files
371        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    /// Get cache statistics
384    ///
385    /// # Returns
386    ///
387    /// Cache statistics including hit rate, miss rate, and size, or a `ResearchError`
388    pub fn get_cache_statistics(
389        &self,
390    ) -> Result<crate::cache_manager::CacheStatistics, ResearchError> {
391        self.cache_manager.statistics()
392    }
393
394    /// Clear the cache
395    ///
396    /// # Returns
397    ///
398    /// A `ResearchError` if clearing fails
399    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/// Builder for ResearchManager with custom configuration
411#[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    /// Create a new builder with default components
424    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    /// Set the project analyzer
437    pub fn with_project_analyzer(mut self, analyzer: ProjectAnalyzer) -> Self {
438        self.project_analyzer = Some(analyzer);
439        self
440    }
441
442    /// Set the pattern detector
443    pub fn with_pattern_detector(mut self, detector: PatternDetector) -> Self {
444        self.pattern_detector = Some(detector);
445        self
446    }
447
448    /// Set the standards detector
449    pub fn with_standards_detector(mut self, detector: StandardsDetector) -> Self {
450        self.standards_detector = Some(detector);
451        self
452    }
453
454    /// Set the architectural intent tracker
455    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    /// Set the context builder
464    pub fn with_context_builder(mut self, builder: ContextBuilder) -> Self {
465        self.context_builder = Some(builder);
466        self
467    }
468
469    /// Set the dependency analyzer
470    pub fn with_dependency_analyzer(mut self, analyzer: DependencyAnalyzer) -> Self {
471        self.dependency_analyzer = Some(analyzer);
472        self
473    }
474
475    /// Set the cache manager
476    pub fn with_cache_manager(mut self, manager: CacheManager) -> Self {
477        self.cache_manager = Some(manager);
478        self
479    }
480
481    /// Build the ResearchManager
482    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        // Manager should be created successfully
510        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}