use crate::audit::{AuditIssue, IssueCategory, IssueSeverity};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct HealthScoreWeights {
pub critical_weight: f64,
pub warning_weight: f64,
pub info_weight: f64,
pub security_multiplier: f64,
pub breaking_changes_multiplier: f64,
pub dependencies_multiplier: f64,
pub version_consistency_multiplier: f64,
pub upgrades_multiplier: f64,
pub other_multiplier: f64,
}
impl Default for HealthScoreWeights {
fn default() -> Self {
Self {
critical_weight: 15.0,
warning_weight: 5.0,
info_weight: 1.0,
security_multiplier: 1.5,
breaking_changes_multiplier: 1.3,
dependencies_multiplier: 1.2,
version_consistency_multiplier: 1.0,
upgrades_multiplier: 0.8,
other_multiplier: 1.0,
}
}
}
impl HealthScoreWeights {
#[must_use]
pub fn strict() -> Self {
Self {
critical_weight: 20.0,
warning_weight: 8.0,
info_weight: 2.0,
security_multiplier: 2.0,
breaking_changes_multiplier: 1.5,
dependencies_multiplier: 1.3,
version_consistency_multiplier: 1.2,
upgrades_multiplier: 1.0,
other_multiplier: 1.0,
}
}
#[must_use]
pub fn lenient() -> Self {
Self {
critical_weight: 10.0,
warning_weight: 3.0,
info_weight: 0.5,
security_multiplier: 1.5,
breaking_changes_multiplier: 1.2,
dependencies_multiplier: 1.0,
version_consistency_multiplier: 0.8,
upgrades_multiplier: 0.5,
other_multiplier: 0.8,
}
}
#[must_use]
pub fn category_multiplier(&self, category: IssueCategory) -> f64 {
match category {
IssueCategory::Security => self.security_multiplier,
IssueCategory::BreakingChanges => self.breaking_changes_multiplier,
IssueCategory::Dependencies => self.dependencies_multiplier,
IssueCategory::VersionConsistency => self.version_consistency_multiplier,
IssueCategory::Upgrades => self.upgrades_multiplier,
IssueCategory::Other => self.other_multiplier,
}
}
#[must_use]
pub fn severity_weight(&self, severity: IssueSeverity) -> f64 {
match severity {
IssueSeverity::Critical => self.critical_weight,
IssueSeverity::Warning => self.warning_weight,
IssueSeverity::Info => self.info_weight,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct HealthScoreBreakdown {
pub score: u8,
pub total_issues: usize,
pub total_deduction: f64,
pub deductions_by_severity: HashMap<String, f64>,
pub deductions_by_category: HashMap<String, f64>,
pub issues_by_severity: HashMap<String, usize>,
pub issues_by_category: HashMap<String, usize>,
}
impl HealthScoreBreakdown {
#[must_use]
pub fn summary(&self) -> String {
let mut lines = vec![
format!("Health Score: {}/100", self.score),
format!("Total Issues: {}", self.total_issues),
format!("Total Deduction: {:.1} points", self.total_deduction),
String::new(),
];
if !self.issues_by_severity.is_empty() {
lines.push("Issues by Severity:".to_string());
for severity in ["critical", "warning", "info"] {
if let Some(count) = self.issues_by_severity.get(severity)
&& *count > 0
{
let deduction =
self.deductions_by_severity.get(severity).copied().unwrap_or(0.0);
lines.push(format!(" {}: {} (-{:.1} points)", severity, count, deduction));
}
}
lines.push(String::new());
}
if !self.issues_by_category.is_empty() {
lines.push("Issues by Category:".to_string());
let mut categories: Vec<_> = self.issues_by_category.iter().collect();
categories.sort_by_key(|(_, count)| std::cmp::Reverse(**count));
for (category, count) in categories {
if *count > 0 {
let deduction =
self.deductions_by_category.get(category.as_str()).copied().unwrap_or(0.0);
lines.push(format!(" {}: {} (-{:.1} points)", category, count, deduction));
}
}
}
lines.join("\n")
}
}
#[must_use]
pub fn calculate_health_score(issues: &[AuditIssue], weights: &HealthScoreWeights) -> u8 {
let breakdown = calculate_health_score_detailed(issues, weights);
breakdown.score
}
#[must_use]
pub fn calculate_health_score_detailed(
issues: &[AuditIssue],
weights: &HealthScoreWeights,
) -> HealthScoreBreakdown {
let mut deductions_by_severity: HashMap<String, f64> = HashMap::new();
let mut deductions_by_category: HashMap<String, f64> = HashMap::new();
let mut issues_by_severity: HashMap<String, usize> = HashMap::new();
let mut issues_by_category: HashMap<String, usize> = HashMap::new();
let mut category_counts: HashMap<IssueCategory, usize> = HashMap::new();
let mut total_deduction = 0.0;
for issue in issues {
let severity_key = issue.severity.as_str().to_string();
let category_key = issue.category.as_str().to_string();
*issues_by_severity.entry(severity_key.clone()).or_insert(0) += 1;
*issues_by_category.entry(category_key.clone()).or_insert(0) += 1;
let base_weight = weights.severity_weight(issue.severity);
let category_multiplier = weights.category_multiplier(issue.category);
let category_count = category_counts.entry(issue.category).or_insert(0);
let diminishing_factor = calculate_diminishing_factor(*category_count);
*category_count += 1;
let deduction = base_weight * category_multiplier * diminishing_factor;
*deductions_by_severity.entry(severity_key).or_insert(0.0) += deduction;
*deductions_by_category.entry(category_key).or_insert(0.0) += deduction;
total_deduction += deduction;
}
let raw_score = 100.0 - total_deduction;
let score = raw_score.clamp(0.0, 100.0).round() as u8;
HealthScoreBreakdown {
score,
total_issues: issues.len(),
total_deduction,
deductions_by_severity,
deductions_by_category,
issues_by_severity,
issues_by_category,
}
}
#[must_use]
pub fn calculate_diminishing_factor(count: usize) -> f64 {
match count {
0 => 1.0,
1 => 0.9,
2 => 0.8,
_ => 0.7,
}
}