Skip to main content

driven/scale/
detector.rs

1//! Scale Detector
2//!
3//! Combines multiple analyzers to detect project scale.
4
5use super::{
6    ProjectContext, ProjectScale, ScaleRecommendation,
7    analyzers::{
8        ComplexityAnalyzer, DependencyAnalyzer, FileSizeAnalyzer, HistoryAnalyzer, ScaleAnalyzer,
9        TeamSizeAnalyzer,
10    },
11};
12use std::collections::HashMap;
13
14/// Detects project scale by combining multiple analyzers
15pub struct ScaleDetector {
16    analyzers: Vec<Box<dyn ScaleAnalyzer>>,
17}
18
19impl Default for ScaleDetector {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl ScaleDetector {
26    /// Create a new scale detector with default analyzers
27    pub fn new() -> Self {
28        Self {
29            analyzers: vec![
30                Box::new(FileSizeAnalyzer::new()),
31                Box::new(DependencyAnalyzer::new()),
32                Box::new(TeamSizeAnalyzer::new()),
33                Box::new(HistoryAnalyzer::new()),
34                Box::new(ComplexityAnalyzer::new()),
35            ],
36        }
37    }
38
39    /// Create a scale detector with custom analyzers
40    pub fn with_analyzers(analyzers: Vec<Box<dyn ScaleAnalyzer>>) -> Self {
41        Self { analyzers }
42    }
43
44    /// Add an analyzer to the detector
45    pub fn add_analyzer(&mut self, analyzer: Box<dyn ScaleAnalyzer>) {
46        self.analyzers.push(analyzer);
47    }
48
49    /// Detect the project scale from context
50    pub fn detect(&self, context: &ProjectContext) -> ProjectScale {
51        // Check for manual override first
52        if let Some(override_scale) = context.scale_override {
53            return override_scale;
54        }
55
56        // Collect votes from all analyzers
57        let mut votes: HashMap<ProjectScale, f32> = HashMap::new();
58
59        for analyzer in &self.analyzers {
60            if let Some((scale, confidence)) = analyzer.analyze(context) {
61                *votes.entry(scale).or_insert(0.0) += confidence;
62            }
63        }
64
65        // Return the scale with highest weighted votes
66        // In case of ties, prefer larger scale (more comprehensive workflow)
67        votes
68            .into_iter()
69            .max_by(|a, b| {
70                match a.1.partial_cmp(&b.1) {
71                    Some(std::cmp::Ordering::Equal) => {
72                        // Tie-breaker: prefer larger scale for determinism
73                        Self::scale_order(&a.0).cmp(&Self::scale_order(&b.0))
74                    }
75                    Some(ord) => ord,
76                    None => std::cmp::Ordering::Equal,
77                }
78            })
79            .map(|(scale, _)| scale)
80            .unwrap_or(ProjectScale::Feature) // Default to Feature if no data
81    }
82
83    /// Get a numeric order for scales (for deterministic tie-breaking)
84    fn scale_order(scale: &ProjectScale) -> u8 {
85        match scale {
86            ProjectScale::BugFix => 0,
87            ProjectScale::Feature => 1,
88            ProjectScale::Product => 2,
89            ProjectScale::Enterprise => 3,
90        }
91    }
92
93    /// Get a detailed recommendation with reasoning
94    pub fn recommend(&self, context: &ProjectContext) -> ScaleRecommendation {
95        // Check for manual override first
96        if let Some(override_scale) = context.scale_override {
97            return ScaleRecommendation::new(override_scale, 1.0)
98                .with_reason("Scale manually overridden by user".to_string());
99        }
100
101        // Collect votes and reasoning from all analyzers
102        let mut votes: HashMap<ProjectScale, f32> = HashMap::new();
103        let mut reasoning: Vec<String> = Vec::new();
104        let mut total_confidence = 0.0;
105        let mut analyzer_count = 0;
106
107        for analyzer in &self.analyzers {
108            if let Some((scale, confidence)) = analyzer.analyze(context) {
109                *votes.entry(scale).or_insert(0.0) += confidence;
110                total_confidence += confidence;
111                analyzer_count += 1;
112
113                reasoning.push(format!(
114                    "{} suggests {} (confidence: {:.0}%)",
115                    analyzer.name(),
116                    scale,
117                    confidence * 100.0
118                ));
119            }
120        }
121
122        // Determine winning scale
123        let (detected_scale, scale_confidence) = votes
124            .iter()
125            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
126            .map(|(scale, conf)| (*scale, *conf))
127            .unwrap_or((ProjectScale::Feature, 0.5));
128
129        // Calculate overall confidence
130        let overall_confidence = if analyzer_count > 0 {
131            (scale_confidence / total_confidence).min(1.0)
132        } else {
133            0.5
134        };
135
136        // Add context-based reasoning
137        if let Some(loc) = context.lines_of_code {
138            reasoning.push(format!("Project has {} lines of code", loc));
139        }
140        if let Some(files) = context.file_count {
141            reasoning.push(format!("Project has {} files", files));
142        }
143        if let Some(deps) = context.dependency_count {
144            reasoning.push(format!("Project has {} dependencies", deps));
145        }
146        if let Some(contributors) = context.contributor_count {
147            reasoning.push(format!("Project has {} contributors", contributors));
148        }
149
150        ScaleRecommendation::new(detected_scale, overall_confidence).with_reasons(reasoning)
151    }
152
153    /// Select appropriate workflows based on detected scale
154    pub fn select_workflows(&self, context: &ProjectContext) -> Vec<&'static str> {
155        let scale = self.detect(context);
156        scale.recommended_workflows()
157    }
158
159    /// Select appropriate agents based on detected scale
160    pub fn select_agents(&self, context: &ProjectContext) -> Vec<&'static str> {
161        let scale = self.detect(context);
162        scale.recommended_agents()
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_detect_bug_fix_scale() {
172        let detector = ScaleDetector::new();
173        let context = ProjectContext::new()
174            .with_lines_of_code(200)
175            .with_file_count(5)
176            .with_dependency_count(3);
177
178        let scale = detector.detect(&context);
179        assert_eq!(scale, ProjectScale::BugFix);
180    }
181
182    #[test]
183    fn test_detect_feature_scale() {
184        let detector = ScaleDetector::new();
185        let context = ProjectContext::new()
186            .with_lines_of_code(3000)
187            .with_file_count(30)
188            .with_dependency_count(15)
189            .with_contributor_count(2);
190
191        let scale = detector.detect(&context);
192        assert_eq!(scale, ProjectScale::Feature);
193    }
194
195    #[test]
196    fn test_detect_product_scale() {
197        let detector = ScaleDetector::new();
198        let context = ProjectContext::new()
199            .with_lines_of_code(25000)
200            .with_file_count(200)
201            .with_dependency_count(50)
202            .with_contributor_count(5)
203            .with_ci_cd(true)
204            .with_tests(true);
205
206        let scale = detector.detect(&context);
207        assert_eq!(scale, ProjectScale::Product);
208    }
209
210    #[test]
211    fn test_detect_enterprise_scale() {
212        let detector = ScaleDetector::new();
213        let context = ProjectContext::new()
214            .with_lines_of_code(200000)
215            .with_file_count(2000)
216            .with_dependency_count(200)
217            .with_contributor_count(50)
218            .with_commit_count(5000)
219            .with_ci_cd(true)
220            .with_tests(true)
221            .with_docs(true);
222
223        let scale = detector.detect(&context);
224        assert_eq!(scale, ProjectScale::Enterprise);
225    }
226
227    #[test]
228    fn test_manual_override() {
229        let detector = ScaleDetector::new();
230        let context = ProjectContext::new()
231            .with_lines_of_code(200000) // Would normally be Enterprise
232            .with_scale_override(ProjectScale::BugFix);
233
234        let scale = detector.detect(&context);
235        assert_eq!(scale, ProjectScale::BugFix);
236    }
237
238    #[test]
239    fn test_recommend_with_reasoning() {
240        let detector = ScaleDetector::new();
241        let context = ProjectContext::new()
242            .with_lines_of_code(10000)
243            .with_file_count(100)
244            .with_dependency_count(30);
245
246        let recommendation = detector.recommend(&context);
247        assert!(!recommendation.reasoning.is_empty());
248        assert!(recommendation.confidence > 0.0);
249        assert!(!recommendation.suggested_workflows.is_empty());
250    }
251
252    #[test]
253    fn test_select_workflows() {
254        let detector = ScaleDetector::new();
255        let context = ProjectContext::new()
256            .with_lines_of_code(100)
257            .with_file_count(3);
258
259        let workflows = detector.select_workflows(&context);
260        assert!(workflows.contains(&"quick-bug-fix"));
261    }
262
263    #[test]
264    fn test_select_agents() {
265        let detector = ScaleDetector::new();
266        let context = ProjectContext::new()
267            .with_lines_of_code(100000)
268            .with_contributor_count(20);
269
270        let agents = detector.select_agents(&context);
271        assert!(agents.contains(&"security"));
272        assert!(agents.contains(&"devops"));
273    }
274
275    #[test]
276    fn test_default_scale_without_data() {
277        let detector = ScaleDetector::new();
278        let context = ProjectContext::new();
279
280        let scale = detector.detect(&context);
281        assert_eq!(scale, ProjectScale::Feature); // Default
282    }
283}