1use super::{
6 ProjectContext, ProjectScale, ScaleRecommendation,
7 analyzers::{
8 ComplexityAnalyzer, DependencyAnalyzer, FileSizeAnalyzer, HistoryAnalyzer, ScaleAnalyzer,
9 TeamSizeAnalyzer,
10 },
11};
12use std::collections::HashMap;
13
14pub 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 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 pub fn with_analyzers(analyzers: Vec<Box<dyn ScaleAnalyzer>>) -> Self {
41 Self { analyzers }
42 }
43
44 pub fn add_analyzer(&mut self, analyzer: Box<dyn ScaleAnalyzer>) {
46 self.analyzers.push(analyzer);
47 }
48
49 pub fn detect(&self, context: &ProjectContext) -> ProjectScale {
51 if let Some(override_scale) = context.scale_override {
53 return override_scale;
54 }
55
56 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 votes
68 .into_iter()
69 .max_by(|a, b| {
70 match a.1.partial_cmp(&b.1) {
71 Some(std::cmp::Ordering::Equal) => {
72 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) }
82
83 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 pub fn recommend(&self, context: &ProjectContext) -> ScaleRecommendation {
95 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 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 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 let overall_confidence = if analyzer_count > 0 {
131 (scale_confidence / total_confidence).min(1.0)
132 } else {
133 0.5
134 };
135
136 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 pub fn select_workflows(&self, context: &ProjectContext) -> Vec<&'static str> {
155 let scale = self.detect(context);
156 scale.recommended_workflows()
157 }
158
159 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) .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); }
283}