use super::*;
use crate::storage::profile::Proficiency;
use tracing::info;
pub struct TeamAggregator;
impl TeamAggregator {
pub fn new() -> Self {
Self
}
pub fn aggregate(
&self,
config: &TeamConfig,
profiles: &[DeveloperProfile],
) -> Result<TeamProfile> {
info!("Aggregating team profile for: {}", config.name);
let language_coverage = self.aggregate_language_coverage(profiles);
let technology_coverage = self.aggregate_technology_coverage(profiles);
let member_stats = self.aggregate_member_stats(config, profiles);
let collaboration_patterns = self.analyze_collaboration_patterns(profiles);
let knowledge_distribution = self.analyze_knowledge_distribution(profiles, &language_coverage);
let recommendations = self.generate_recommendations(
profiles,
&language_coverage,
&knowledge_distribution,
);
Ok(TeamProfile {
config: config.clone(),
aggregated_at: chrono::Utc::now(),
language_coverage,
technology_coverage,
member_stats,
collaboration_patterns,
knowledge_distribution,
recommendations,
})
}
fn aggregate_language_coverage(
&self,
profiles: &[DeveloperProfile],
) -> HashMap<String, TeamLanguageCoverage> {
let mut coverage: HashMap<String, TeamLanguageCoverage> = HashMap::new();
for profile in profiles {
let username = &profile.identity.github_username;
for (lang, _) in &profile.github.primary_languages {
let entry = coverage.entry(lang.clone()).or_insert_with(|| TeamLanguageCoverage {
language: lang.clone(),
total_experience_years: 0.0,
expert_count: 0,
advanced_count: 0,
intermediate_count: 0,
beginner_count: 0,
primary_experts: Vec::new(),
});
if let Some(skill) = profile.skills.languages.iter().find(|s| &s.name == lang) {
entry.total_experience_years += skill.years_experience;
match skill.proficiency {
Proficiency::Expert => {
entry.expert_count += 1;
if let Some(username) = username {
entry.primary_experts.push(username.clone());
}
}
Proficiency::Advanced => entry.advanced_count += 1,
Proficiency::Intermediate => entry.intermediate_count += 1,
Proficiency::Beginner => entry.beginner_count += 1,
}
} else {
entry.intermediate_count += 1;
}
}
}
coverage
}
fn aggregate_technology_coverage(
&self,
profiles: &[DeveloperProfile],
) -> HashMap<String, TechnologyCoverage> {
let mut coverage: HashMap<String, TechnologyCoverage> = HashMap::new();
for profile in profiles {
let username = profile.identity.github_username.clone().unwrap_or_default();
for framework in &profile.skills.frameworks {
let entry = coverage.entry(framework.name.clone()).or_insert_with(|| TechnologyCoverage {
name: framework.name.clone(),
category: "framework".to_string(),
users: Vec::new(),
expertise_level: TeamExpertiseLevel::None,
});
entry.users.push(username.clone());
}
for tool in &profile.skills.tools {
let entry = coverage.entry(tool.name.clone()).or_insert_with(|| TechnologyCoverage {
name: tool.name.clone(),
category: "tool".to_string(),
users: Vec::new(),
expertise_level: TeamExpertiseLevel::None,
});
if !entry.users.contains(&username) {
entry.users.push(username.clone());
}
}
for (lang, libs) in &profile.coding_patterns.common_libraries {
for lib in libs {
let key = format!("{}:{}", lang, lib);
let entry = coverage.entry(key.clone()).or_insert_with(|| TechnologyCoverage {
name: lib.clone(),
category: "library".to_string(),
users: Vec::new(),
expertise_level: TeamExpertiseLevel::None,
});
if !entry.users.contains(&username) {
entry.users.push(username.clone());
}
}
}
}
let total_members = profiles.len();
for entry in coverage.values_mut() {
let user_count = entry.users.len();
let ratio = user_count as f64 / total_members as f64;
entry.expertise_level = if ratio >= 0.5 {
TeamExpertiseLevel::Deep
} else if ratio >= 0.25 {
TeamExpertiseLevel::Moderate
} else if user_count > 0 {
TeamExpertiseLevel::Limited
} else {
TeamExpertiseLevel::None
};
}
coverage
}
fn aggregate_member_stats(
&self,
config: &TeamConfig,
profiles: &[DeveloperProfile],
) -> Vec<MemberStats> {
profiles.iter()
.map(|profile| {
let username = profile.identity.github_username.clone().unwrap_or_default();
let role = config.members.iter()
.find(|m| m.github_username == username)
.map(|m| m.role.clone())
.unwrap_or(TeamRole::Contributor);
MemberStats {
github_username: username.clone(),
role,
primary_languages: profile.github.primary_languages.iter()
.take(3)
.map(|(l, _)| l.clone())
.collect(),
contribution_score: self.calculate_contribution_score(profile),
code_review_activity: CodeReviewStats {
reviews_given: 0, reviews_received: 0,
avg_review_time_hours: None,
},
knowledge_areas: profile.skills.languages.iter()
.filter(|s| matches!(s.proficiency, Proficiency::Advanced | Proficiency::Expert))
.map(|s| s.name.clone())
.collect(),
}
})
.collect()
}
fn calculate_contribution_score(&self, profile: &DeveloperProfile) -> f64 {
let mut score = 0.0;
score += profile.github.total_contributions_30d as f64 * 0.1;
score += profile.github.total_repositories as f64 * 0.5;
score += profile.github.primary_languages.len() as f64 * 0.3;
let expert_skills = profile.skills.languages.iter()
.filter(|s| matches!(s.proficiency, Proficiency::Expert))
.count();
score += expert_skills as f64 * 1.0;
score
}
fn analyze_collaboration_patterns(
&self,
_profiles: &[DeveloperProfile],
) -> CollaborationPatterns {
CollaborationPatterns {
most_active_pairs: Vec::new(),
code_review_network: Vec::new(),
knowledge_sharing_score: 0.0,
}
}
fn analyze_knowledge_distribution(
&self,
profiles: &[DeveloperProfile],
language_coverage: &HashMap<String, TeamLanguageCoverage>,
) -> KnowledgeDistribution {
let mut bus_factor: HashMap<String, usize> = HashMap::new();
let mut knowledge_silos: Vec<KnowledgeSilo> = Vec::new();
let mut cross_training_opportunities: Vec<CrossTrainingOpportunity> = Vec::new();
for (lang, coverage) in language_coverage {
let experts = coverage.expert_count + coverage.advanced_count;
bus_factor.insert(lang.clone(), experts.max(1));
if coverage.expert_count <= 1 && coverage.advanced_count <= 2 {
let risk_level = if coverage.expert_count == 0 {
RiskLevel::High
} else {
RiskLevel::Medium
};
knowledge_silos.push(KnowledgeSilo {
technology: lang.clone(),
primary_experts: coverage.primary_experts.clone(),
risk_level,
});
if coverage.expert_count > 0 {
let trainees: Vec<String> = profiles.iter()
.filter(|p| {
p.github.primary_languages.iter().any(|(l, _)| l == lang) &&
!coverage.primary_experts.iter().any(|e| {
p.identity.github_username.as_ref() == Some(e)
})
})
.filter_map(|p| p.identity.github_username.clone())
.collect();
if !trainees.is_empty() {
cross_training_opportunities.push(CrossTrainingOpportunity {
technology: lang.clone(),
potential_mentors: coverage.primary_experts.clone(),
recommended_trainees: trainees,
priority: if coverage.expert_count == 1 {
Priority::High
} else {
Priority::Medium
},
});
}
}
}
}
KnowledgeDistribution {
bus_factor,
knowledge_silos,
cross_training_opportunities,
}
}
fn generate_recommendations(
&self,
profiles: &[DeveloperProfile],
language_coverage: &HashMap<String, TeamLanguageCoverage>,
knowledge_distribution: &KnowledgeDistribution,
) -> Vec<TeamRecommendation> {
let mut recommendations = Vec::new();
for silo in &knowledge_distribution.knowledge_silos {
if matches!(silo.risk_level, RiskLevel::Critical | RiskLevel::High) {
recommendations.push(TeamRecommendation {
category: RecommendationCategory::KnowledgeSharing,
title: format!("Address Knowledge Silo: {}", silo.technology),
description: format!(
"Only {} expert(s) in {}. Risk of knowledge loss if they leave.",
silo.primary_experts.len(),
silo.technology
),
action_items: vec![
format!("Pair program with {} on {}", silo.primary_experts.join(", "), silo.technology),
format!("Document {} best practices", silo.technology),
"Schedule knowledge sharing session".to_string(),
],
priority: if silo.primary_experts.len() == 1 {
Priority::Critical
} else {
Priority::High
},
});
}
}
let has_frontend = language_coverage.contains_key("JavaScript")
|| language_coverage.contains_key("TypeScript");
let has_backend = language_coverage.contains_key("Rust")
|| language_coverage.contains_key("Go")
|| language_coverage.contains_key("Python")
|| language_coverage.contains_key("Java");
if has_backend && !has_frontend {
recommendations.push(TeamRecommendation {
category: RecommendationCategory::SkillGap,
title: "Consider Frontend Expertise".to_string(),
description: "Team has strong backend skills but limited frontend coverage.".to_string(),
action_items: vec![
"Evaluate need for frontend development".to_string(),
"Consider hiring frontend specialist".to_string(),
"Cross-train backend developers in basic frontend".to_string(),
],
priority: Priority::Medium,
});
}
if profiles.len() < 3 && language_coverage.len() > 5 {
recommendations.push(TeamRecommendation {
category: RecommendationCategory::Hiring,
title: "Team May Be Spread Too Thin".to_string(),
description: format!(
"Small team ({}) maintaining expertise in {} different technologies.",
profiles.len(),
language_coverage.len()
),
action_items: vec![
"Prioritize core technologies".to_string(),
"Consider consolidating tech stack".to_string(),
"Plan strategic hiring".to_string(),
],
priority: Priority::High,
});
}
recommendations
}
}
impl Default for TeamAggregator {
fn default() -> Self {
Self::new()
}
}