use std::fmt;
pub fn calculate_coverage_multiplier(coverage_pct: f64) -> f64 {
calculate_coverage_multiplier_with_test_flag(coverage_pct, false)
}
pub fn calculate_coverage_multiplier_with_test_flag(coverage_pct: f64, is_test_code: bool) -> f64 {
if is_test_code {
return 0.0; }
1.0 - coverage_pct
}
pub fn calculate_coverage_factor(coverage_pct: f64) -> f64 {
calculate_coverage_factor_with_test_flag(coverage_pct, false)
}
pub fn calculate_coverage_factor_with_test_flag(coverage_pct: f64, is_test_code: bool) -> f64 {
if is_test_code {
return 0.1;
}
let coverage_gap = 1.0 - coverage_pct;
match coverage_pct {
0.0 => 10.0,
c if c < 0.2 => 5.0 + (coverage_gap * 3.0),
c if c < 0.5 => 2.0 + (coverage_gap * 2.0),
_ => (coverage_gap.powf(1.5) + 0.1).max(0.1),
}
}
pub fn calculate_complexity_factor(raw_complexity: f64) -> f64 {
(raw_complexity / 2.0).clamp(0.0, 10.0)
}
pub fn calculate_dependency_factor(upstream_count: usize) -> f64 {
((upstream_count as f64) / 2.0).min(10.0)
}
#[derive(Debug, Clone)]
pub struct InstabilityAwareDependencyResult {
pub adjusted_factor: f64,
pub base_factor: f64,
pub multiplier: f64,
pub classification: &'static str,
pub is_stable_by_design: bool,
pub is_architectural_concern: bool,
}
pub fn calculate_instability_aware_dependency_factor(
production_caller_count: usize,
test_caller_count: usize,
efferent_count: usize,
) -> InstabilityAwareDependencyResult {
use crate::output::unified::{
calculate_instability, classify_coupling_pattern, CouplingClassification,
};
let afferent_count = production_caller_count + test_caller_count;
let instability = calculate_instability(afferent_count, efferent_count);
let classification = classify_coupling_pattern(
instability,
production_caller_count,
test_caller_count,
efferent_count,
);
let base_factor = calculate_dependency_factor(production_caller_count);
let multiplier = classification.score_multiplier();
let adjusted_factor = (base_factor * multiplier).min(10.0);
let classification_name = match classification {
CouplingClassification::WellTestedCore => "Well-Tested Core",
CouplingClassification::StableFoundation => "Stable Foundation",
CouplingClassification::UnstableHighCoupling => "Unstable High Coupling",
CouplingClassification::StableCore => "Stable Core",
CouplingClassification::UtilityModule => "Utility Module",
CouplingClassification::ArchitecturalHub => "Architectural Hub",
CouplingClassification::LeafModule => "Leaf Module",
CouplingClassification::Isolated => "Isolated",
CouplingClassification::HighlyCoupled => "Highly Coupled",
};
InstabilityAwareDependencyResult {
adjusted_factor,
base_factor,
multiplier,
classification: classification_name,
is_stable_by_design: classification.is_stable_by_design(),
is_architectural_concern: classification.is_architectural_concern(),
}
}
pub fn calculate_base_score_with_coverage_multiplier(
coverage_multiplier: f64,
complexity_factor: f64,
dependency_factor: f64,
) -> f64 {
let base = calculate_base_score_no_coverage(complexity_factor, dependency_factor);
base * coverage_multiplier
}
pub fn calculate_base_score(
coverage_factor: f64,
complexity_factor: f64,
dependency_factor: f64,
) -> f64 {
let coverage_weight = 0.40; let complexity_weight = 0.40; let dependency_weight = 0.20;
let coverage_score = coverage_factor * 10.0; let complexity_score = complexity_factor * 10.0; let dependency_score = dependency_factor * 10.0;
(coverage_score * coverage_weight)
+ (complexity_score * complexity_weight)
+ (dependency_score * dependency_weight)
}
pub fn calculate_base_score_no_coverage(complexity_factor: f64, dependency_factor: f64) -> f64 {
let complexity_weight = 0.50; let dependency_weight = 0.25;
let complexity_score = complexity_factor * 10.0;
let dependency_score = dependency_factor * 10.0;
(complexity_score * complexity_weight) + (dependency_score * dependency_weight)
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct NormalizedScore {
pub raw: f64,
pub normalized: f64,
pub scaling_method: ScalingMethod,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum ScalingMethod {
Linear, SquareRoot, Logarithmic, }
impl fmt::Display for NormalizedScore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let indicator = match self.scaling_method {
ScalingMethod::Linear => "▁", ScalingMethod::SquareRoot => "▃", ScalingMethod::Logarithmic => "▅", };
let severity = if self.normalized < 3.0 {
"low"
} else if self.normalized < 7.0 {
"medium"
} else if self.normalized < 15.0 {
"high"
} else {
"critical"
};
write!(
f,
"{:.1} (raw: {:.1}, {}, {})",
self.normalized, self.raw, severity, indicator
)
}
}
fn determine_scaling_method(score: f64) -> ScalingMethod {
if score <= 10.0 {
ScalingMethod::Linear
} else if score <= 100.0 {
ScalingMethod::SquareRoot
} else {
ScalingMethod::Logarithmic
}
}
pub fn normalize_final_score_with_metadata(raw_score: f64) -> NormalizedScore {
let normalized = if raw_score <= 0.0 {
0.0
} else if raw_score < 10.0 {
raw_score
} else if raw_score < 100.0 {
10.0 + (raw_score - 10.0).sqrt() * 3.33
} else {
41.59 + (raw_score / 100.0).ln() * 10.0
};
NormalizedScore {
raw: raw_score,
normalized,
scaling_method: determine_scaling_method(raw_score),
}
}
pub fn denormalize_score(normalized: f64) -> f64 {
if normalized <= 0.0 {
0.0
} else if normalized < 10.0 {
normalized
} else if normalized < 41.59 {
let adjusted = (normalized - 10.0) / 3.33;
10.0 + adjusted.powf(2.0)
} else {
let log_component = (normalized - 41.59) / 10.0;
100.0 * log_component.exp()
}
}
pub fn normalize_complexity(cyclomatic: u32, cognitive: u32) -> f64 {
let combined = (cyclomatic + cognitive) as f64 / 2.0;
if combined <= 5.0 {
combined * 0.6
} else if combined <= 10.0 {
3.0 + (combined - 5.0) * 0.6
} else {
6.0 + ((combined - 10.0) * 0.2).min(4.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComplexityDistributionType {
Concentrated,
Mixed,
Distributed,
}
pub fn calculate_file_scope_complexity_factor(
total_complexity: u32,
distribution: ComplexityDistributionType,
max_function_complexity: u32,
) -> f64 {
let base_factor = calculate_complexity_factor(total_complexity as f64);
let distribution_dampening = match distribution {
ComplexityDistributionType::Distributed => 0.4, ComplexityDistributionType::Mixed => 0.7, ComplexityDistributionType::Concentrated => 1.0, };
let max_function_dampening = if max_function_complexity < 15 {
0.5 } else if max_function_complexity < 30 {
0.75 } else {
1.0 };
base_factor * distribution_dampening * max_function_dampening
}
pub fn generate_normalization_curve() -> Vec<(f64, f64, &'static str)> {
let mut curve_data = Vec::new();
let sample_points = [0, 5, 10, 20, 30, 50, 75, 100, 150, 200, 500, 1000];
for raw in sample_points {
let raw = raw as f64;
let normalized = raw.max(0.0);
curve_data.push((raw, normalized, "Linear"));
}
curve_data
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_coverage_multiplier() {
assert_eq!(calculate_coverage_multiplier(0.0), 1.0); assert_eq!(calculate_coverage_multiplier(0.5), 0.5); assert!((calculate_coverage_multiplier(0.8) - 0.2).abs() < 0.01); assert_eq!(calculate_coverage_multiplier(1.0), 0.0); }
#[test]
fn test_coverage_multiplier_with_test_flag() {
assert_eq!(calculate_coverage_multiplier_with_test_flag(0.0, true), 0.0);
assert_eq!(calculate_coverage_multiplier_with_test_flag(0.5, true), 0.0);
assert_eq!(calculate_coverage_multiplier_with_test_flag(1.0, true), 0.0);
assert_eq!(
calculate_coverage_multiplier_with_test_flag(0.0, false),
1.0
);
assert_eq!(
calculate_coverage_multiplier_with_test_flag(0.5, false),
0.5
);
assert_eq!(
calculate_coverage_multiplier_with_test_flag(1.0, false),
0.0
);
}
#[test]
fn test_base_score_with_coverage_multiplier() {
let complexity_factor = 5.0;
let dependency_factor = 2.0;
let score_no_coverage = calculate_base_score_with_coverage_multiplier(
1.0,
complexity_factor,
dependency_factor,
);
let base = calculate_base_score_no_coverage(complexity_factor, dependency_factor);
assert_eq!(score_no_coverage, base);
let score_half_coverage = calculate_base_score_with_coverage_multiplier(
0.5,
complexity_factor,
dependency_factor,
);
assert!((score_half_coverage - base * 0.5).abs() < 0.01);
let score_high_coverage = calculate_base_score_with_coverage_multiplier(
0.2,
complexity_factor,
dependency_factor,
);
assert!((score_high_coverage - base * 0.2).abs() < 0.01);
let score_full_coverage = calculate_base_score_with_coverage_multiplier(
0.0,
complexity_factor,
dependency_factor,
);
assert_eq!(score_full_coverage, 0.0);
}
#[test]
fn test_coverage_reduces_score_monotonicity() {
let complexity_factor = 8.0;
let dependency_factor = 3.0;
let score_0_coverage = calculate_base_score_with_coverage_multiplier(
1.0,
complexity_factor,
dependency_factor,
);
let score_50_coverage = calculate_base_score_with_coverage_multiplier(
0.5,
complexity_factor,
dependency_factor,
);
let score_80_coverage = calculate_base_score_with_coverage_multiplier(
0.2,
complexity_factor,
dependency_factor,
);
let score_100_coverage = calculate_base_score_with_coverage_multiplier(
0.0,
complexity_factor,
dependency_factor,
);
assert!(score_0_coverage > score_50_coverage);
assert!(score_50_coverage > score_80_coverage);
assert!(score_80_coverage > score_100_coverage);
assert_eq!(score_100_coverage, 0.0);
}
#[test]
fn test_calculate_coverage_factor() {
assert_eq!(calculate_coverage_factor(0.0), 10.0);
assert!((calculate_coverage_factor(0.1) - 7.7).abs() < 0.01); assert!((calculate_coverage_factor(0.19) - 7.43).abs() < 0.01);
assert!((calculate_coverage_factor(0.2) - 3.6).abs() < 0.01); assert!((calculate_coverage_factor(0.49) - 3.02).abs() < 0.01);
assert!((calculate_coverage_factor(0.5) - 0.453).abs() < 0.01); assert!((calculate_coverage_factor(1.0) - 0.1).abs() < 0.01); }
#[test]
fn test_coverage_factor_with_test_flag() {
assert_eq!(calculate_coverage_factor_with_test_flag(0.0, true), 0.1);
assert_eq!(calculate_coverage_factor_with_test_flag(0.5, true), 0.1);
assert_eq!(calculate_coverage_factor_with_test_flag(1.0, true), 0.1);
assert_eq!(calculate_coverage_factor_with_test_flag(0.0, false), 10.0);
assert!((calculate_coverage_factor_with_test_flag(0.5, false) - 0.453).abs() < 0.01);
}
#[test]
fn test_calculate_complexity_factor() {
assert_eq!(calculate_complexity_factor(0.0), 0.0);
assert_eq!(calculate_complexity_factor(10.0), 5.0);
assert_eq!(calculate_complexity_factor(20.0), 10.0);
assert_eq!(calculate_complexity_factor(30.0), 10.0); }
#[test]
fn test_calculate_dependency_factor() {
assert_eq!(calculate_dependency_factor(0), 0.0);
assert_eq!(calculate_dependency_factor(10), 5.0);
assert_eq!(calculate_dependency_factor(20), 10.0);
assert_eq!(calculate_dependency_factor(30), 10.0); }
#[test]
fn test_calculate_base_score() {
let score = calculate_base_score(1.0, 0.5, 0.1);
assert!((score - 6.2).abs() < 0.01);
}
#[test]
fn test_weights_sum_to_one() {
const COVERAGE_WEIGHT: f64 = 0.40;
const COMPLEXITY_WEIGHT: f64 = 0.40;
const DEPENDENCY_WEIGHT: f64 = 0.20;
let sum = COVERAGE_WEIGHT + COMPLEXITY_WEIGHT + DEPENDENCY_WEIGHT;
assert!((sum - 1.0).abs() < 0.001);
}
#[test]
fn test_complexity_coverage_balance() {
const COVERAGE_WEIGHT: f64 = 0.40;
const COMPLEXITY_WEIGHT: f64 = 0.40;
assert_eq!(COVERAGE_WEIGHT, COMPLEXITY_WEIGHT);
}
#[test]
fn test_god_object_ranks_higher_than_simple_untested() {
let simple_score = calculate_base_score(
11.0, 7.5, 10.0, );
let god_score = calculate_base_score(
5.0, 10.0, 10.0, );
let coverage_coverage_delta = (simple_score - god_score).abs();
assert!(
coverage_coverage_delta < 20.0,
"Scores should be reasonably close with balanced weights. Simple: {}, God: {}, Delta: {}",
simple_score,
god_score,
coverage_coverage_delta
);
const COVERAGE_WEIGHT: f64 = 0.40;
const COMPLEXITY_WEIGHT: f64 = 0.40;
assert_eq!(COVERAGE_WEIGHT, COMPLEXITY_WEIGHT);
}
#[test]
fn test_calculate_base_score_no_coverage() {
let score1 = calculate_base_score_no_coverage(5.0, 1.0);
assert!(
(score1 - 27.5).abs() < 0.01,
"High complexity should dominate score. Expected 27.5, got {}",
score1
);
let score2 = calculate_base_score_no_coverage(1.0, 5.0);
assert!(
(score2 - 17.5).abs() < 0.01,
"High dependencies should contribute. Expected 17.5, got {}",
score2
);
let score3 = calculate_base_score_no_coverage(8.0, 6.0);
assert!(
(score3 - 55.0).abs() < 0.01,
"Both high should yield high score. Expected 55.0, got {}",
score3
);
let score4 = calculate_base_score_no_coverage(0.0, 0.0);
assert_eq!(score4, 0.0, "Zero factors should yield zero score");
let complexity_contribution = 10.0 * 10.0 * 0.5; let dependency_contribution = 10.0 * 10.0 * 0.25; assert_eq!(
complexity_contribution / dependency_contribution,
2.0,
"Complexity should have 2x weight of dependency"
);
let total_weight: f64 = 0.50 + 0.25;
let reserved_weight: f64 = 1.0 - total_weight;
assert!(
(reserved_weight - 0.25).abs() < 0.01,
"Should reserve 25% for debt patterns"
);
}
#[test]
fn test_scaling_method_detection() {
let score1 = normalize_final_score_with_metadata(5.0);
assert_eq!(score1.scaling_method, ScalingMethod::Linear);
let score2 = normalize_final_score_with_metadata(50.0);
assert_eq!(score2.scaling_method, ScalingMethod::SquareRoot);
let score3 = normalize_final_score_with_metadata(200.0);
assert_eq!(score3.scaling_method, ScalingMethod::Logarithmic);
}
#[test]
fn test_generate_normalization_curve() {
let curve = generate_normalization_curve();
assert!(!curve.is_empty());
let linear_count = curve
.iter()
.filter(|&(_, _, region)| *region == "Linear")
.count();
assert_eq!(linear_count, curve.len());
for i in 1..curve.len() {
assert!(
curve[i].0 >= curve[i - 1].0,
"Raw scores should be monotonic"
);
assert!(
curve[i].1 >= curve[i - 1].1,
"Normalized scores should be monotonic"
);
}
for (raw, normalized, _) in curve {
assert_eq!(raw, normalized, "Scores should be identity");
}
}
#[test]
fn test_normalized_score_display() {
let score = NormalizedScore {
raw: 45.0,
normalized: 16.7,
scaling_method: ScalingMethod::SquareRoot,
};
let display = format!("{}", score);
assert!(display.contains("16.7"));
assert!(display.contains("45.0"));
assert!(display.contains("critical")); }
#[test]
fn test_file_scope_complexity_factor_concentrated() {
let factor = calculate_file_scope_complexity_factor(
100,
ComplexityDistributionType::Concentrated,
60,
);
let base_factor = calculate_complexity_factor(100.0);
assert!((factor - base_factor).abs() < 0.01);
}
#[test]
fn test_file_scope_complexity_factor_distributed() {
let factor = calculate_file_scope_complexity_factor(
100,
ComplexityDistributionType::Distributed,
8, );
let base_factor = calculate_complexity_factor(100.0);
let expected = base_factor * 0.4 * 0.5;
assert!((factor - expected).abs() < 0.01);
}
#[test]
fn test_file_scope_complexity_factor_mixed() {
let factor = calculate_file_scope_complexity_factor(
100,
ComplexityDistributionType::Mixed,
25, );
let base_factor = calculate_complexity_factor(100.0);
let expected = base_factor * 0.7 * 0.75;
assert!((factor - expected).abs() < 0.01);
}
#[test]
fn test_distributed_file_gets_significantly_lower_score() {
let distributed_factor = calculate_file_scope_complexity_factor(
182,
ComplexityDistributionType::Distributed,
12, );
let concentrated_factor = calculate_file_scope_complexity_factor(
182,
ComplexityDistributionType::Concentrated,
182, );
assert!(
distributed_factor < concentrated_factor * 0.5,
"Distributed score ({}) should be less than half of concentrated score ({})",
distributed_factor,
concentrated_factor
);
}
#[test]
fn test_max_function_complexity_affects_dampening() {
let low_max = calculate_file_scope_complexity_factor(
100,
ComplexityDistributionType::Mixed,
10, );
let high_max = calculate_file_scope_complexity_factor(
100,
ComplexityDistributionType::Mixed,
50, );
assert!(
high_max > low_max,
"High max ({}) should be higher than low max ({})",
high_max,
low_max
);
}
#[test]
fn test_instability_aware_well_tested_core() {
let result = calculate_instability_aware_dependency_factor(
5, 85, 10, );
assert_eq!(result.classification, "Well-Tested Core");
assert!(result.is_stable_by_design);
assert!(!result.is_architectural_concern);
assert!(result.multiplier < 0.5);
assert!(result.adjusted_factor < result.base_factor);
}
#[test]
fn test_instability_aware_unstable_high_coupling() {
let result = calculate_instability_aware_dependency_factor(
15, 2, 60, );
assert_eq!(result.classification, "Unstable High Coupling");
assert!(!result.is_stable_by_design);
assert!(result.is_architectural_concern);
assert!(result.multiplier > 1.0);
assert!(result.adjusted_factor > result.base_factor);
}
#[test]
fn test_instability_aware_stable_foundation() {
let result = calculate_instability_aware_dependency_factor(
15, 3, 5, );
assert_eq!(result.classification, "Stable Foundation");
assert!(result.is_stable_by_design);
assert!(!result.is_architectural_concern);
assert!(result.multiplier < 1.0);
}
#[test]
fn test_instability_aware_leaf_module() {
let result = calculate_instability_aware_dependency_factor(
1, 0, 10, );
assert_eq!(result.classification, "Leaf Module");
assert!(!result.is_stable_by_design);
assert!(!result.is_architectural_concern);
}
#[test]
fn test_instability_aware_overflow_rs_scenario() {
let result = calculate_instability_aware_dependency_factor(
5, 85, 35, );
assert_eq!(result.classification, "Well-Tested Core");
assert!(result.is_stable_by_design);
assert!(result.multiplier < 0.5);
assert!(
result.adjusted_factor < 1.5,
"Score should be heavily reduced"
);
}
}