use super::{
ProjectContext, ProjectScale, ScaleRecommendation,
analyzers::{
ComplexityAnalyzer, DependencyAnalyzer, FileSizeAnalyzer, HistoryAnalyzer, ScaleAnalyzer,
TeamSizeAnalyzer,
},
};
use std::collections::HashMap;
pub struct ScaleDetector {
analyzers: Vec<Box<dyn ScaleAnalyzer>>,
}
impl Default for ScaleDetector {
fn default() -> Self {
Self::new()
}
}
impl ScaleDetector {
pub fn new() -> Self {
Self {
analyzers: vec![
Box::new(FileSizeAnalyzer::new()),
Box::new(DependencyAnalyzer::new()),
Box::new(TeamSizeAnalyzer::new()),
Box::new(HistoryAnalyzer::new()),
Box::new(ComplexityAnalyzer::new()),
],
}
}
pub fn with_analyzers(analyzers: Vec<Box<dyn ScaleAnalyzer>>) -> Self {
Self { analyzers }
}
pub fn add_analyzer(&mut self, analyzer: Box<dyn ScaleAnalyzer>) {
self.analyzers.push(analyzer);
}
pub fn detect(&self, context: &ProjectContext) -> ProjectScale {
if let Some(override_scale) = context.scale_override {
return override_scale;
}
let mut votes: HashMap<ProjectScale, f32> = HashMap::new();
for analyzer in &self.analyzers {
if let Some((scale, confidence)) = analyzer.analyze(context) {
*votes.entry(scale).or_insert(0.0) += confidence;
}
}
votes
.into_iter()
.max_by(|a, b| {
match a.1.partial_cmp(&b.1) {
Some(std::cmp::Ordering::Equal) => {
Self::scale_order(&a.0).cmp(&Self::scale_order(&b.0))
}
Some(ord) => ord,
None => std::cmp::Ordering::Equal,
}
})
.map(|(scale, _)| scale)
.unwrap_or(ProjectScale::Feature) }
fn scale_order(scale: &ProjectScale) -> u8 {
match scale {
ProjectScale::BugFix => 0,
ProjectScale::Feature => 1,
ProjectScale::Product => 2,
ProjectScale::Enterprise => 3,
}
}
pub fn recommend(&self, context: &ProjectContext) -> ScaleRecommendation {
if let Some(override_scale) = context.scale_override {
return ScaleRecommendation::new(override_scale, 1.0)
.with_reason("Scale manually overridden by user".to_string());
}
let mut votes: HashMap<ProjectScale, f32> = HashMap::new();
let mut reasoning: Vec<String> = Vec::new();
let mut total_confidence = 0.0;
let mut analyzer_count = 0;
for analyzer in &self.analyzers {
if let Some((scale, confidence)) = analyzer.analyze(context) {
*votes.entry(scale).or_insert(0.0) += confidence;
total_confidence += confidence;
analyzer_count += 1;
reasoning.push(format!(
"{} suggests {} (confidence: {:.0}%)",
analyzer.name(),
scale,
confidence * 100.0
));
}
}
let (detected_scale, scale_confidence) = votes
.iter()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(scale, conf)| (*scale, *conf))
.unwrap_or((ProjectScale::Feature, 0.5));
let overall_confidence = if analyzer_count > 0 {
(scale_confidence / total_confidence).min(1.0)
} else {
0.5
};
if let Some(loc) = context.lines_of_code {
reasoning.push(format!("Project has {} lines of code", loc));
}
if let Some(files) = context.file_count {
reasoning.push(format!("Project has {} files", files));
}
if let Some(deps) = context.dependency_count {
reasoning.push(format!("Project has {} dependencies", deps));
}
if let Some(contributors) = context.contributor_count {
reasoning.push(format!("Project has {} contributors", contributors));
}
ScaleRecommendation::new(detected_scale, overall_confidence).with_reasons(reasoning)
}
pub fn select_workflows(&self, context: &ProjectContext) -> Vec<&'static str> {
let scale = self.detect(context);
scale.recommended_workflows()
}
pub fn select_agents(&self, context: &ProjectContext) -> Vec<&'static str> {
let scale = self.detect(context);
scale.recommended_agents()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_bug_fix_scale() {
let detector = ScaleDetector::new();
let context = ProjectContext::new()
.with_lines_of_code(200)
.with_file_count(5)
.with_dependency_count(3);
let scale = detector.detect(&context);
assert_eq!(scale, ProjectScale::BugFix);
}
#[test]
fn test_detect_feature_scale() {
let detector = ScaleDetector::new();
let context = ProjectContext::new()
.with_lines_of_code(3000)
.with_file_count(30)
.with_dependency_count(15)
.with_contributor_count(2);
let scale = detector.detect(&context);
assert_eq!(scale, ProjectScale::Feature);
}
#[test]
fn test_detect_product_scale() {
let detector = ScaleDetector::new();
let context = ProjectContext::new()
.with_lines_of_code(25000)
.with_file_count(200)
.with_dependency_count(50)
.with_contributor_count(5)
.with_ci_cd(true)
.with_tests(true);
let scale = detector.detect(&context);
assert_eq!(scale, ProjectScale::Product);
}
#[test]
fn test_detect_enterprise_scale() {
let detector = ScaleDetector::new();
let context = ProjectContext::new()
.with_lines_of_code(200000)
.with_file_count(2000)
.with_dependency_count(200)
.with_contributor_count(50)
.with_commit_count(5000)
.with_ci_cd(true)
.with_tests(true)
.with_docs(true);
let scale = detector.detect(&context);
assert_eq!(scale, ProjectScale::Enterprise);
}
#[test]
fn test_manual_override() {
let detector = ScaleDetector::new();
let context = ProjectContext::new()
.with_lines_of_code(200000) .with_scale_override(ProjectScale::BugFix);
let scale = detector.detect(&context);
assert_eq!(scale, ProjectScale::BugFix);
}
#[test]
fn test_recommend_with_reasoning() {
let detector = ScaleDetector::new();
let context = ProjectContext::new()
.with_lines_of_code(10000)
.with_file_count(100)
.with_dependency_count(30);
let recommendation = detector.recommend(&context);
assert!(!recommendation.reasoning.is_empty());
assert!(recommendation.confidence > 0.0);
assert!(!recommendation.suggested_workflows.is_empty());
}
#[test]
fn test_select_workflows() {
let detector = ScaleDetector::new();
let context = ProjectContext::new()
.with_lines_of_code(100)
.with_file_count(3);
let workflows = detector.select_workflows(&context);
assert!(workflows.contains(&"quick-bug-fix"));
}
#[test]
fn test_select_agents() {
let detector = ScaleDetector::new();
let context = ProjectContext::new()
.with_lines_of_code(100000)
.with_contributor_count(20);
let agents = detector.select_agents(&context);
assert!(agents.contains(&"security"));
assert!(agents.contains(&"devops"));
}
#[test]
fn test_default_scale_without_data() {
let detector = ScaleDetector::new();
let context = ProjectContext::new();
let scale = detector.detect(&context);
assert_eq!(scale, ProjectScale::Feature); }
}