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