use crate::priority::semantic_classifier::FunctionRole;
use crate::priority::{DebtType, UnifiedDebtItem};
use serde::{Deserialize, Serialize};
pub fn calculate_debt_type_severity_multiplier(debt_type: &DebtType) -> f64 {
match debt_type {
DebtType::GodObject { .. } => 1.4,
DebtType::ComplexityHotspot { .. } | DebtType::Complexity { .. } => 1.2,
DebtType::TestComplexityHotspot { .. } | DebtType::TestComplexity { .. } => 1.2,
DebtType::FeatureEnvy { .. }
| DebtType::ScatteredType { .. }
| DebtType::CodeOrganization { .. } => 1.15,
DebtType::Dependency { .. }
| DebtType::OrphanedFunctions { .. }
| DebtType::UtilitiesSprawl { .. } => 1.1,
DebtType::ResourceLeak { .. }
| DebtType::ErrorSwallowing { .. }
| DebtType::AsyncMisuse { .. }
| DebtType::BlockingIO { .. } => 1.1,
DebtType::PrimitiveObsession { .. }
| DebtType::MagicValues { .. }
| DebtType::SuboptimalDataStructure { .. }
| DebtType::CollectionInefficiency { .. }
| DebtType::AllocationInefficiency { .. }
| DebtType::StringConcatenation { .. }
| DebtType::NestedLoops { .. } => 1.05,
DebtType::TestingGap { .. } => 1.0,
DebtType::DeadCode { .. } => 0.9,
DebtType::Todo { .. }
| DebtType::Fixme { .. }
| DebtType::CodeSmell { .. }
| DebtType::ResourceManagement { .. }
| DebtType::TestQuality { .. }
| DebtType::Duplication { .. }
| DebtType::TestDuplication { .. }
| DebtType::Risk { .. }
| DebtType::TestTodo { .. }
| DebtType::AssertionComplexity { .. }
| DebtType::FlakyTestPattern { .. } => 1.0,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScalingConfig {
pub god_object_exponent: f64,
pub god_module_exponent: f64,
pub high_complexity_exponent: f64, pub moderate_complexity_exponent: f64,
pub high_dependency_boost: f64, pub entry_point_boost: f64, pub complex_untested_boost: f64, pub error_swallowing_boost: f64, }
impl Default for ScalingConfig {
fn default() -> Self {
Self {
god_object_exponent: 1.4,
god_module_exponent: 1.4,
high_complexity_exponent: 1.2,
moderate_complexity_exponent: 1.1,
high_dependency_boost: 1.2,
entry_point_boost: 1.15,
complex_untested_boost: 1.25,
error_swallowing_boost: 1.15, }
}
}
fn apply_exponential_scaling(base_score: f64, debt_type: &DebtType, config: &ScalingConfig) -> f64 {
let safe_base = base_score.max(1.0);
let exponent = match debt_type {
DebtType::GodObject { .. } => config.god_object_exponent,
DebtType::ComplexityHotspot { cyclomatic, .. } => {
if *cyclomatic > 30 {
config.high_complexity_exponent
} else if *cyclomatic > 15 {
config.moderate_complexity_exponent
} else {
1.0
}
}
DebtType::TestingGap { cyclomatic, .. } if *cyclomatic > 20 => {
config.moderate_complexity_exponent
}
_ => 1.0,
};
safe_base.powf(exponent)
}
fn is_untested(item: &UnifiedDebtItem) -> bool {
matches!(
item.debt_type,
DebtType::TestingGap { coverage, .. } if coverage < 0.1
)
}
pub fn calculate_final_score(
base_score: f64,
debt_type: &DebtType,
item: &UnifiedDebtItem,
config: &ScalingConfig,
) -> (f64, f64, f64, f64) {
let exponent = match debt_type {
DebtType::GodObject { .. } => config.god_object_exponent,
DebtType::ComplexityHotspot { cyclomatic, .. } => {
if *cyclomatic > 30 {
config.high_complexity_exponent
} else if *cyclomatic > 15 {
config.moderate_complexity_exponent
} else {
1.0
}
}
DebtType::TestingGap { cyclomatic, .. } if *cyclomatic > 20 => {
config.moderate_complexity_exponent
}
_ => 1.0,
};
let scaled = apply_exponential_scaling(base_score, debt_type, config);
let debt_multiplier = calculate_debt_type_severity_multiplier(debt_type);
let after_debt_multiplier = scaled * debt_multiplier;
let mut boost = 1.0;
let total_deps = item.upstream_dependencies + item.downstream_dependencies;
if total_deps > 15 {
boost *= config.high_dependency_boost;
}
if matches!(item.function_role, FunctionRole::EntryPoint) {
boost *= config.entry_point_boost;
}
if is_untested(item) && item.cyclomatic_complexity > 20 {
boost *= config.complex_untested_boost;
}
if item.error_swallowing_count.unwrap_or(0) > 0 {
boost *= config.error_swallowing_boost;
}
let final_score = after_debt_multiplier * boost;
(final_score, exponent, boost, debt_multiplier)
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use std::path::PathBuf;
fn create_test_item(
base_score: f64,
debt_type: DebtType,
upstream_deps: usize,
downstream_deps: usize,
role: FunctionRole,
cyclomatic: u32,
) -> UnifiedDebtItem {
use crate::priority::unified_scorer::{Location, UnifiedScore};
use crate::priority::ActionableRecommendation;
use crate::priority::ImpactMetrics;
UnifiedDebtItem {
location: Location {
file: PathBuf::from("test.rs"),
function: "test_func".to_string(),
line: 1,
},
debt_type,
unified_score: UnifiedScore {
complexity_factor: 5.0,
coverage_factor: 5.0,
dependency_factor: 5.0,
role_multiplier: 1.0,
final_score: base_score.max(0.0),
base_score: Some(base_score),
exponential_factor: Some(1.0),
risk_boost: Some(1.0),
pre_adjustment_score: None,
adjustment_applied: None,
purity_factor: None,
refactorability_factor: None,
pattern_factor: None,
debt_adjustment: None,
pre_normalization_score: None,
structural_multiplier: Some(1.0),
has_coverage_data: false,
contextual_risk_multiplier: None,
pre_contextual_score: None,
debt_type_multiplier: None,
},
function_role: role,
recommendation: ActionableRecommendation {
primary_action: "Test".to_string(),
rationale: "Test".to_string(),
implementation_steps: vec![],
related_items: vec![],
steps: None,
estimated_effort_hours: None,
},
expected_impact: ImpactMetrics {
coverage_improvement: 0.0,
lines_reduction: 0,
complexity_reduction: 0.0,
risk_reduction: 0.0,
},
transitive_coverage: None,
upstream_dependencies: upstream_deps,
downstream_dependencies: downstream_deps,
upstream_callers: vec![],
downstream_callees: vec![],
upstream_production_callers: vec![],
upstream_test_callers: vec![],
production_blast_radius: 0,
nesting_depth: 1,
function_length: 10,
cyclomatic_complexity: cyclomatic,
cognitive_complexity: cyclomatic,
is_pure: None,
purity_confidence: None,
purity_level: None,
god_object_indicators: None,
tier: None,
function_context: None,
context_confidence: None,
contextual_recommendation: None,
pattern_analysis: None,
file_context: None,
context_multiplier: None,
context_type: None,
language_specific: None, detected_pattern: None,
contextual_risk: None, file_line_count: None,
responsibility_category: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
context_suggestion: None,
}
}
fn apply_risk_boosts(score: f64, item: &UnifiedDebtItem, config: &ScalingConfig) -> f64 {
let mut boost = 1.0;
let total_deps = item.upstream_dependencies + item.downstream_dependencies;
if total_deps > 15 {
boost *= config.high_dependency_boost;
}
if matches!(item.function_role, FunctionRole::EntryPoint) {
boost *= config.entry_point_boost;
}
if is_untested(item) && item.cyclomatic_complexity > 20 {
boost *= config.complex_untested_boost;
}
if item.error_swallowing_count.unwrap_or(0) > 0 {
boost *= config.error_swallowing_boost;
}
score * boost
}
#[test]
fn test_exponential_scaling_god_object() {
let config = ScalingConfig::default();
let base = 10.0;
let scaled = apply_exponential_scaling(
base,
&DebtType::GodObject {
methods: 50,
fields: Some(20),
responsibilities: 10,
god_object_score: 85.0,
lines: 400,
},
&config,
);
assert!(
(scaled - 25.1).abs() < 0.5,
"Expected ~25.1, got {}",
scaled
);
}
#[test]
fn test_exponential_scaling_creates_separation() {
let config = ScalingConfig::default();
let base = 20.0;
let god_object_scaled = apply_exponential_scaling(
base,
&DebtType::GodObject {
methods: 50,
fields: Some(20),
responsibilities: 10,
god_object_score: 85.0,
lines: 400,
},
&config,
);
let testing_gap_scaled = apply_exponential_scaling(
base,
&DebtType::TestingGap {
coverage: 0.0,
cyclomatic: 10,
cognitive: 10,
},
&config,
);
assert!(god_object_scaled > testing_gap_scaled * 2.0);
}
#[test]
fn test_risk_boosts_multiply() {
let config = ScalingConfig::default();
let item = create_test_item(
10.0,
DebtType::TestingGap {
coverage: 0.0,
cyclomatic: 25,
cognitive: 30,
},
10, 10, FunctionRole::EntryPoint,
25,
);
let boosted = apply_risk_boosts(10.0, &item, &config);
assert!(
(boosted - 17.25).abs() < 0.1,
"Expected ~17.25, got {}",
boosted
);
}
#[test]
fn test_calculate_final_score_integration() {
let config = ScalingConfig::default();
let item = create_test_item(
30.0,
DebtType::GodObject {
methods: 50,
fields: Some(1000),
responsibilities: 10,
god_object_score: 85.0,
lines: 600,
},
20, 10,
FunctionRole::EntryPoint,
35,
);
let (final_score, exponent, boost, debt_multiplier) =
calculate_final_score(30.0, &item.debt_type, &item, &config);
assert!(exponent == 1.4, "Expected exponent 1.4, got {}", exponent);
assert!(boost > 1.3, "Expected boost > 1.3, got {}", boost);
assert!(
debt_multiplier == 1.4,
"Expected debt_multiplier 1.4 for GodObject, got {}",
debt_multiplier
);
assert!(
final_score > 190.0,
"Expected score > 190, got {}",
final_score
);
}
#[test]
fn test_architectural_issues_naturally_surface() {
let config = ScalingConfig::default();
let god_object_item = create_test_item(
30.0,
DebtType::GodObject {
methods: 50,
fields: Some(1000),
responsibilities: 10,
god_object_score: 85.0,
lines: 600,
},
5,
5,
FunctionRole::PureLogic,
30,
);
let simple_gap_item = create_test_item(
50.0,
DebtType::TestingGap {
coverage: 0.0,
cyclomatic: 10,
cognitive: 10,
},
5,
5,
FunctionRole::PureLogic,
10,
);
let (god_score, _, _, god_debt_mult) =
calculate_final_score(30.0, &god_object_item.debt_type, &god_object_item, &config);
let (gap_score, _, _, gap_debt_mult) =
calculate_final_score(50.0, &simple_gap_item.debt_type, &simple_gap_item, &config);
assert!(
god_debt_mult == 1.4,
"Expected GodObject debt_multiplier 1.4, got {}",
god_debt_mult
);
assert!(
gap_debt_mult == 1.0,
"Expected TestingGap debt_multiplier 1.0, got {}",
gap_debt_mult
);
assert!(
god_score > gap_score,
"God object (score {}) should rank higher than simple testing gap (score {})",
god_score,
gap_score
);
}
#[test]
fn test_minimum_base_score_prevents_zero() {
let config = ScalingConfig::default();
let scaled = apply_exponential_scaling(
0.0,
&DebtType::GodObject {
methods: 50,
fields: Some(1000),
responsibilities: 10,
god_object_score: 85.0,
lines: 600,
},
&config,
);
assert!(
scaled >= 1.0,
"Scaled score should be at least 1.0, got {}",
scaled
);
}
#[test]
fn test_error_swallowing_boost() {
let config = ScalingConfig::default();
let item_no_swallowing = create_test_item(
10.0,
DebtType::TestingGap {
coverage: 0.5,
cyclomatic: 10,
cognitive: 12,
},
5,
5,
FunctionRole::PureLogic,
10,
);
let mut item_with_swallowing = create_test_item(
10.0,
DebtType::TestingGap {
coverage: 0.5,
cyclomatic: 10,
cognitive: 12,
},
5,
5,
FunctionRole::PureLogic,
10,
);
item_with_swallowing.error_swallowing_count = Some(3);
item_with_swallowing.error_swallowing_patterns =
Some(vec!["if_let_ok_without_else".to_string()]);
let boosted_no_swallowing = apply_risk_boosts(10.0, &item_no_swallowing, &config);
let boosted_with_swallowing = apply_risk_boosts(10.0, &item_with_swallowing, &config);
assert!(
boosted_with_swallowing > boosted_no_swallowing,
"Error swallowing should boost score. Without: {}, With: {}",
boosted_no_swallowing,
boosted_with_swallowing
);
let expected_boost = 10.0 * config.error_swallowing_boost;
assert!(
(boosted_with_swallowing - expected_boost).abs() < 0.1,
"Expected ~{}, got {}",
expected_boost,
boosted_with_swallowing
);
}
proptest! {
#[test]
fn prop_score_ordering_is_strict(
base_scores in prop::collection::vec(0.0f64..100.0f64, 2..50)
) {
let config = ScalingConfig::default();
let items: Vec<_> = base_scores.iter().enumerate().map(|(i, &score)| {
let debt_type = if i % 3 == 0 {
DebtType::GodObject {
methods: 50,
fields: Some(20),
responsibilities: 10,
god_object_score: 85.0,
lines: 400,
}
} else if i % 3 == 1 {
DebtType::ComplexityHotspot {
cyclomatic: 35,
cognitive: 40,
}
} else {
DebtType::TestingGap {
coverage: 0.0,
cyclomatic: 15,
cognitive: 20,
}
};
create_test_item(
score,
debt_type,
5,
5,
FunctionRole::PureLogic,
20,
)
}).collect();
let mut scored_items: Vec<_> = items.iter().enumerate().map(|(idx, item)| {
let (final_score, _, _, _) = calculate_final_score(
base_scores[idx],
&item.debt_type,
item,
&config
);
(final_score, idx)
}).collect();
scored_items.sort_by(|a, b| {
b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal)
});
for i in 0..scored_items.len().saturating_sub(1) {
let score_i = scored_items[i].0;
let score_next = scored_items[i + 1].0;
prop_assert!(
score_i >= score_next,
"Score inversion at index {}: {} < {} (items {} and {})",
i,
score_i,
score_next,
scored_items[i].1,
scored_items[i + 1].1
);
}
}
#[test]
fn prop_exponential_scaling_monotonic(
base_score in 1.0f64..100.0f64,
higher_score in 1.0f64..100.0f64,
) {
let (lower, higher) = if base_score < higher_score {
(base_score, higher_score)
} else {
(higher_score, base_score)
};
if (higher - lower).abs() < 0.01 {
return Ok(());
}
let config = ScalingConfig::default();
let debt_type = DebtType::GodObject {
methods: 50,
fields: Some(20),
responsibilities: 10,
god_object_score: 85.0,
lines: 400,
};
let scaled_lower = apply_exponential_scaling(lower, &debt_type, &config);
let scaled_higher = apply_exponential_scaling(higher, &debt_type, &config);
prop_assert!(
scaled_higher >= scaled_lower,
"Exponential scaling violated monotonicity: {}^1.4={} should be >= {}^1.4={}",
higher, scaled_higher, lower, scaled_lower
);
}
#[test]
fn prop_risk_boosts_non_negative(
base_score in 1.0f64..100.0f64,
upstream_deps in 0usize..30,
downstream_deps in 0usize..30,
) {
let config = ScalingConfig::default();
let role = if upstream_deps > 20 {
FunctionRole::EntryPoint
} else {
FunctionRole::PureLogic
};
let item = create_test_item(
base_score,
DebtType::TestingGap {
coverage: 0.0,
cyclomatic: 25,
cognitive: 30,
},
upstream_deps,
downstream_deps,
role,
25,
);
let boosted = apply_risk_boosts(base_score, &item, &config);
prop_assert!(
boosted >= base_score,
"Risk boost decreased score: {} -> {}",
base_score,
boosted
);
}
#[test]
fn prop_final_score_never_decreases_from_base(
base_score in 1.0f64..100.0f64,
) {
let config = ScalingConfig::default();
let item = create_test_item(
base_score,
DebtType::GodObject {
methods: 50,
fields: Some(20),
responsibilities: 10,
god_object_score: 85.0,
lines: 400,
},
10,
10,
FunctionRole::EntryPoint,
30,
);
let (final_score, _, _, _) = calculate_final_score(
base_score,
&item.debt_type,
&item,
&config,
);
prop_assert!(
final_score >= base_score * 0.99, "Final score {} is less than base score {}",
final_score,
base_score
);
}
}
#[test]
fn test_debt_type_severity_multiplier_god_object() {
let debt_type = DebtType::GodObject {
methods: 50,
fields: Some(20),
responsibilities: 10,
god_object_score: 85.0,
lines: 400,
};
assert_eq!(calculate_debt_type_severity_multiplier(&debt_type), 1.4);
}
#[test]
fn test_debt_type_severity_multiplier_complexity_hotspot() {
let debt_type = DebtType::ComplexityHotspot {
cyclomatic: 25,
cognitive: 30,
};
assert_eq!(calculate_debt_type_severity_multiplier(&debt_type), 1.2);
}
#[test]
fn test_debt_type_severity_multiplier_testing_gap() {
let debt_type = DebtType::TestingGap {
coverage: 0.0,
cyclomatic: 15,
cognitive: 20,
};
assert_eq!(calculate_debt_type_severity_multiplier(&debt_type), 1.0);
}
#[test]
fn test_debt_type_severity_multiplier_dead_code() {
let debt_type = DebtType::DeadCode {
visibility: crate::priority::FunctionVisibility::Private,
cyclomatic: 5,
cognitive: 3,
usage_hints: vec![],
};
assert_eq!(calculate_debt_type_severity_multiplier(&debt_type), 0.9);
}
#[test]
fn test_debt_type_severity_multiplier_differentiation() {
let config = ScalingConfig::default();
let base_score = 20.0;
let god_object = create_test_item(
base_score,
DebtType::GodObject {
methods: 50,
fields: Some(20),
responsibilities: 10,
god_object_score: 85.0,
lines: 400,
},
5,
5,
FunctionRole::PureLogic,
20,
);
let complexity = create_test_item(
base_score,
DebtType::ComplexityHotspot {
cyclomatic: 15,
cognitive: 20,
},
5,
5,
FunctionRole::PureLogic,
20,
);
let testing_gap = create_test_item(
base_score,
DebtType::TestingGap {
coverage: 0.0,
cyclomatic: 15,
cognitive: 20,
},
5,
5,
FunctionRole::PureLogic,
20,
);
let (god_score, _, _, god_mult) =
calculate_final_score(base_score, &god_object.debt_type, &god_object, &config);
let (complexity_score, _, _, complexity_mult) =
calculate_final_score(base_score, &complexity.debt_type, &complexity, &config);
let (gap_score, _, _, gap_mult) =
calculate_final_score(base_score, &testing_gap.debt_type, &testing_gap, &config);
assert!(
god_mult > complexity_mult,
"GodObject should have higher multiplier than ComplexityHotspot"
);
assert!(
complexity_mult > gap_mult,
"ComplexityHotspot should have higher multiplier than TestingGap"
);
assert!(
god_score > complexity_score,
"GodObject score {} should be > ComplexityHotspot score {}",
god_score,
complexity_score
);
assert!(
complexity_score > gap_score,
"ComplexityHotspot score {} should be > TestingGap score {}",
complexity_score,
gap_score
);
}
}