scribe_core/
traits.rs

1//! Core traits for extensibility and plugin architecture.
2//!
3//! Defines the essential traits that enable customization and extension
4//! of Scribe's analysis pipeline, scoring system, and output formatting.
5
6use std::path::Path;
7use async_trait::async_trait;
8use serde::{Deserialize, Serialize};
9
10use crate::error::Result;
11use crate::file::FileInfo;
12use crate::types::{AnalysisResult, ScoreComponents, RepositoryInfo, CentralityScores};
13use crate::config::Config;
14
15/// Core trait for file analysis implementations
16#[async_trait]
17pub trait FileAnalyzer: Send + Sync {
18    /// Analyze a single file and return metadata
19    async fn analyze_file(&self, file_path: &Path, config: &Config) -> Result<FileInfo>;
20    
21    /// Load and analyze file content
22    async fn analyze_content(&self, file_info: &mut FileInfo, config: &Config) -> Result<()>;
23    
24    /// Batch analyze multiple files for efficiency
25    async fn analyze_batch(&self, files: Vec<&Path>, config: &Config) -> Result<Vec<FileInfo>> {
26        let mut results = Vec::new();
27        for file in files {
28            results.push(self.analyze_file(file, config).await?);
29        }
30        Ok(results)
31    }
32    
33    /// Get analyzer name/version for caching
34    fn name(&self) -> &'static str;
35    
36    /// Get analyzer version for cache invalidation
37    fn version(&self) -> &'static str;
38}
39
40/// Trait for heuristic scoring implementations
41pub trait HeuristicScorer: Send + Sync {
42    /// Compute heuristic scores for a file
43    fn score_file(&self, file_info: &FileInfo, repo_info: &RepositoryInfo) -> Result<ScoreComponents>;
44    
45    /// Batch score multiple files (can be optimized for cross-file analysis)
46    fn score_batch(&self, files: &[&FileInfo], repo_info: &RepositoryInfo) -> Result<Vec<ScoreComponents>> {
47        files.iter()
48            .map(|file| self.score_file(file, repo_info))
49            .collect()
50    }
51    
52    /// Get scorer name for identification
53    fn name(&self) -> &'static str;
54    
55    /// Get list of score components this scorer produces
56    fn score_components(&self) -> Vec<&'static str>;
57    
58    /// Check if scorer supports advanced features (V2)
59    fn supports_advanced_features(&self) -> bool {
60        false
61    }
62}
63
64/// Trait for repository analysis implementations
65#[async_trait]
66pub trait RepositoryAnalyzer: Send + Sync {
67    /// Analyze repository structure and metadata
68    async fn analyze_repository(&self, root_path: &Path, config: &Config) -> Result<RepositoryInfo>;
69    
70    /// Get repository statistics
71    async fn get_statistics(&self, root_path: &Path, files: &[FileInfo]) -> Result<RepositoryInfo>;
72    
73    /// Check if this analyzer can handle the given repository
74    fn can_analyze(&self, root_path: &Path) -> bool;
75    
76    /// Get analyzer priority (higher = preferred)
77    fn priority(&self) -> u8 {
78        0
79    }
80}
81
82/// Trait for git integration implementations
83#[async_trait]
84pub trait GitIntegration: Send + Sync {
85    /// Check if path is in a git repository
86    async fn is_git_repository(&self, path: &Path) -> Result<bool>;
87    
88    /// Get git status for files
89    async fn get_file_status(&self, files: &[&Path]) -> Result<Vec<crate::file::GitStatus>>;
90    
91    /// Get repository information
92    async fn get_repo_info(&self, root_path: &Path) -> Result<GitRepositoryInfo>;
93    
94    /// Analyze file churn (commit history)
95    async fn analyze_churn(&self, root_path: &Path, depth: usize) -> Result<Vec<crate::types::ChurnInfo>>;
96    
97    /// Check if file should be ignored by git
98    async fn should_ignore(&self, file_path: &Path, root_path: &Path) -> Result<bool>;
99}
100
101/// Git repository information
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct GitRepositoryInfo {
104    /// Repository root path
105    pub root: std::path::PathBuf,
106    /// Current branch
107    pub branch: Option<String>,
108    /// Remote URL
109    pub remote_url: Option<String>,
110    /// Last commit hash
111    pub last_commit: Option<String>,
112    /// Whether repository has uncommitted changes
113    pub has_changes: bool,
114}
115
116/// Trait for centrality computation implementations
117#[async_trait]
118pub trait CentralityComputer: Send + Sync {
119    /// Build dependency graph from files
120    async fn build_dependency_graph(&self, files: &[&FileInfo]) -> Result<DependencyGraph>;
121    
122    /// Compute PageRank centrality scores
123    async fn compute_centrality(&self, graph: &DependencyGraph) -> Result<CentralityScores>;
124    
125    /// Get supported languages for dependency analysis
126    fn supported_languages(&self) -> Vec<crate::file::Language>;
127    
128    /// Check if file can be analyzed for dependencies
129    fn can_analyze_file(&self, file_info: &FileInfo) -> bool;
130}
131
132/// Dependency graph representation
133#[derive(Debug, Clone)]
134pub struct DependencyGraph {
135    /// Node IDs (file paths)
136    pub nodes: Vec<String>,
137    
138    /// Adjacency list (node_id -> [dependent_node_ids])
139    pub edges: Vec<Vec<usize>>,
140    
141    /// Reverse adjacency list (node_id -> [dependency_node_ids])
142    pub reverse_edges: Vec<Vec<usize>>,
143    
144    /// Node metadata
145    pub node_metadata: Vec<DependencyNodeMetadata>,
146}
147
148/// Metadata for dependency graph nodes
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct DependencyNodeMetadata {
151    /// File path
152    pub path: String,
153    /// Programming language
154    pub language: crate::file::Language,
155    /// File size
156    pub size: u64,
157    /// Whether this is a test file
158    pub is_test: bool,
159    /// Whether this is an entrypoint
160    pub is_entrypoint: bool,
161}
162
163impl DependencyGraph {
164    /// Create empty dependency graph
165    pub fn new() -> Self {
166        Self {
167            nodes: Vec::new(),
168            edges: Vec::new(),
169            reverse_edges: Vec::new(),
170            node_metadata: Vec::new(),
171        }
172    }
173    
174    /// Add a node to the graph
175    pub fn add_node(&mut self, path: String, metadata: DependencyNodeMetadata) -> usize {
176        let node_id = self.nodes.len();
177        self.nodes.push(path);
178        self.edges.push(Vec::new());
179        self.reverse_edges.push(Vec::new());
180        self.node_metadata.push(metadata);
181        node_id
182    }
183    
184    /// Add an edge from source to target
185    pub fn add_edge(&mut self, source: usize, target: usize) {
186        if source < self.edges.len() && target < self.reverse_edges.len() {
187            self.edges[source].push(target);
188            self.reverse_edges[target].push(source);
189        }
190    }
191    
192    /// Get graph statistics
193    pub fn stats(&self) -> crate::types::GraphStats {
194        let total_nodes = self.nodes.len();
195        let total_edges: usize = self.edges.iter().map(|adj| adj.len()).sum();
196        
197        let in_degree_sum: usize = self.reverse_edges.iter().map(|adj| adj.len()).sum();
198        let out_degree_sum: usize = self.edges.iter().map(|adj| adj.len()).sum();
199        
200        let in_degree_avg = if total_nodes > 0 { in_degree_sum as f64 / total_nodes as f64 } else { 0.0 };
201        let out_degree_avg = if total_nodes > 0 { out_degree_sum as f64 / total_nodes as f64 } else { 0.0 };
202        
203        let in_degree_max = self.reverse_edges.iter().map(|adj| adj.len()).max().unwrap_or(0);
204        let out_degree_max = self.edges.iter().map(|adj| adj.len()).max().unwrap_or(0);
205        
206        let possible_edges = if total_nodes > 1 { total_nodes * (total_nodes - 1) } else { 0 };
207        let graph_density = if possible_edges > 0 { total_edges as f64 / possible_edges as f64 } else { 0.0 };
208        
209        crate::types::GraphStats {
210            total_nodes,
211            total_edges,
212            in_degree_avg,
213            in_degree_max,
214            out_degree_avg,
215            out_degree_max,
216            strongly_connected_components: 0, // TODO: Implement SCC computation
217            graph_density,
218        }
219    }
220}
221
222impl Default for DependencyGraph {
223    fn default() -> Self {
224        Self::new()
225    }
226}
227
228/// Trait for pattern matching implementations (glob, regex, etc.)
229pub trait PatternMatcher: Send + Sync {
230    /// Check if a path matches the pattern
231    fn matches(&self, path: &Path) -> bool;
232    
233    /// Get pattern string for debugging
234    fn pattern(&self) -> &str;
235    
236    /// Check if pattern is case sensitive
237    fn is_case_sensitive(&self) -> bool {
238        true
239    }
240}
241
242/// Trait for output formatting implementations
243pub trait OutputFormatter: Send + Sync {
244    /// Format analysis results
245    fn format_results(&self, results: &[AnalysisResult], config: &Config) -> Result<String>;
246    
247    /// Format repository information
248    fn format_repository_info(&self, repo_info: &RepositoryInfo, config: &Config) -> Result<String>;
249    
250    /// Get supported output format name
251    fn format_name(&self) -> &'static str;
252    
253    /// Get file extension for output format
254    fn file_extension(&self) -> &'static str;
255    
256    /// Check if format supports streaming output
257    fn supports_streaming(&self) -> bool {
258        false
259    }
260}
261
262/// Trait for caching implementations
263#[async_trait]
264pub trait CacheStorage: Send + Sync {
265    /// Get cached result
266    async fn get<T>(&self, key: &str) -> Result<Option<T>>
267    where
268        T: for<'de> Deserialize<'de> + Send;
269    
270    /// Store result in cache
271    async fn put<T>(&self, key: &str, value: &T, ttl: Option<std::time::Duration>) -> Result<()>
272    where
273        T: Serialize + Send + Sync;
274    
275    /// Remove item from cache
276    async fn remove(&self, key: &str) -> Result<()>;
277    
278    /// Clear entire cache
279    async fn clear(&self) -> Result<()>;
280    
281    /// Get cache statistics
282    async fn stats(&self) -> Result<CacheStats>;
283}
284
285/// Cache statistics
286#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct CacheStats {
288    /// Number of items in cache
289    pub item_count: usize,
290    /// Total cache size in bytes
291    pub size_bytes: u64,
292    /// Cache hit rate (0.0 - 1.0)
293    pub hit_rate: f64,
294    /// Number of cache hits
295    pub hits: u64,
296    /// Number of cache misses
297    pub misses: u64,
298}
299
300/// Trait for progress reporting implementations
301pub trait ProgressReporter: Send + Sync {
302    /// Start a new progress bar/indicator
303    fn start(&self, total: u64, message: &str);
304    
305    /// Update progress
306    fn update(&self, current: u64, message: Option<&str>);
307    
308    /// Finish progress reporting
309    fn finish(&self, message: &str);
310    
311    /// Report an error
312    fn error(&self, message: &str);
313    
314    /// Report a warning
315    fn warning(&self, message: &str);
316    
317    /// Check if progress reporting is enabled
318    fn is_enabled(&self) -> bool;
319}
320
321/// Trait for language-specific analysis extensions
322#[async_trait]
323pub trait LanguageExtension: Send + Sync {
324    /// Get supported languages
325    fn supported_languages(&self) -> Vec<crate::file::Language>;
326    
327    /// Extract dependencies from file content
328    async fn extract_dependencies(&self, content: &str, language: crate::file::Language) -> Result<Vec<String>>;
329    
330    /// Detect if file is an entrypoint (main, index, etc.)
331    async fn is_entrypoint(&self, file_info: &FileInfo) -> Result<bool>;
332    
333    /// Extract documentation/comments
334    async fn extract_documentation(&self, content: &str, language: crate::file::Language) -> Result<Vec<DocumentationBlock>>;
335    
336    /// Get extension priority (higher = preferred for language)
337    fn priority(&self) -> u8 {
338        0
339    }
340}
341
342/// Documentation block extracted from source code
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct DocumentationBlock {
345    /// Documentation text
346    pub text: String,
347    /// Position in file
348    pub position: crate::types::Range,
349    /// Type of documentation (comment, docstring, etc.)
350    pub doc_type: DocumentationType,
351}
352
353/// Types of documentation
354#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
355pub enum DocumentationType {
356    /// Single line comment (// or #)
357    LineComment,
358    /// Block comment (/* */ or """ """)
359    BlockComment,
360    /// Documentation comment (/// or /** */)
361    DocComment,
362    /// Docstring (Python, etc.)
363    Docstring,
364    /// Module-level documentation
365    ModuleDoc,
366    /// README or markdown documentation
367    Readme,
368}
369
370/// Trait for plugin registration and discovery
371pub trait PluginRegistry: Send + Sync {
372    /// Register a file analyzer
373    fn register_analyzer(&mut self, analyzer: Box<dyn FileAnalyzer>);
374    
375    /// Register a scorer
376    fn register_scorer(&mut self, scorer: Box<dyn HeuristicScorer>);
377    
378    /// Register a repository analyzer
379    fn register_repository_analyzer(&mut self, analyzer: Box<dyn RepositoryAnalyzer>);
380    
381    /// Register an output formatter
382    fn register_formatter(&mut self, formatter: Box<dyn OutputFormatter>);
383    
384    /// Register a language extension
385    fn register_language_extension(&mut self, extension: Box<dyn LanguageExtension>);
386    
387    /// Get registered analyzers
388    fn get_analyzers(&self) -> Vec<&dyn FileAnalyzer>;
389    
390    /// Get registered scorers
391    fn get_scorers(&self) -> Vec<&dyn HeuristicScorer>;
392    
393    /// Get registered formatters
394    fn get_formatters(&self) -> Vec<&dyn OutputFormatter>;
395    
396    /// Load plugins from directory
397    fn load_plugins_from_dir(&mut self, dir: &Path) -> Result<usize>;
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403
404    #[allow(dead_code)]
405    struct MockAnalyzer;
406    
407    #[async_trait]
408    impl FileAnalyzer for MockAnalyzer {
409        async fn analyze_file(&self, _file_path: &Path, _config: &Config) -> Result<FileInfo> {
410            unimplemented!()
411        }
412        
413        async fn analyze_content(&self, _file_info: &mut FileInfo, _config: &Config) -> Result<()> {
414            Ok(())
415        }
416        
417        fn name(&self) -> &'static str {
418            "mock"
419        }
420        
421        fn version(&self) -> &'static str {
422            "1.0.0"
423        }
424    }
425
426    #[test]
427    fn test_dependency_graph() {
428        let mut graph = DependencyGraph::new();
429        
430        let metadata = DependencyNodeMetadata {
431            path: "test.rs".to_string(),
432            language: crate::file::Language::Rust,
433            size: 100,
434            is_test: false,
435            is_entrypoint: false,
436        };
437        
438        let node1 = graph.add_node("file1.rs".to_string(), metadata.clone());
439        let node2 = graph.add_node("file2.rs".to_string(), metadata);
440        
441        graph.add_edge(node1, node2);
442        
443        assert_eq!(graph.nodes.len(), 2);
444        assert_eq!(graph.edges[node1].len(), 1);
445        assert_eq!(graph.reverse_edges[node2].len(), 1);
446        
447        let stats = graph.stats();
448        assert_eq!(stats.total_nodes, 2);
449        assert_eq!(stats.total_edges, 1);
450    }
451
452    #[test]
453    fn test_documentation_block() {
454        use crate::types::{Position, Range};
455        
456        let doc_block = DocumentationBlock {
457            text: "This is a test function".to_string(),
458            position: Range::new(Position::new(0, 0), Position::new(0, 23)),
459            doc_type: DocumentationType::DocComment,
460        };
461        
462        assert_eq!(doc_block.doc_type, DocumentationType::DocComment);
463        assert!(doc_block.text.contains("test function"));
464    }
465}