use crate::core::FunctionMetrics;
use crate::priority::call_graph::{CallGraph, FunctionId};
use crate::priority::semantic_classifier::FunctionRole;
use anyhow::{ensure, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct ReductionPercent(f64);
impl ReductionPercent {
pub fn new(value: f64) -> Result<Self> {
ensure!(
(0.0..=1.0).contains(&value),
"Reduction percent must be between 0.0 and 1.0, got {}",
value
);
Ok(Self(value))
}
pub fn value(&self) -> f64 {
self.0
}
pub fn as_percent(&self) -> f64 {
self.0 * 100.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrchestrationAdjustmentConfig {
pub enabled: bool,
pub base_orchestrator_reduction: f64,
pub max_quality_bonus: f64,
pub max_total_reduction: f64,
pub min_inherent_complexity_factor: f64,
pub min_composition_quality: f64,
}
impl Default for OrchestrationAdjustmentConfig {
fn default() -> Self {
Self {
enabled: true,
base_orchestrator_reduction: 0.20,
max_quality_bonus: 0.10,
max_total_reduction: 0.31, min_inherent_complexity_factor: 2.0,
min_composition_quality: 0.5,
}
}
}
impl OrchestrationAdjustmentConfig {
pub fn validate(&self) -> Result<()> {
ensure!(
(0.0..=1.0).contains(&self.base_orchestrator_reduction),
"base_orchestrator_reduction must be between 0.0 and 1.0, got {}",
self.base_orchestrator_reduction
);
ensure!(
(0.0..=1.0).contains(&self.max_quality_bonus),
"max_quality_bonus must be between 0.0 and 1.0, got {}",
self.max_quality_bonus
);
ensure!(
(0.0..=1.0).contains(&self.max_total_reduction),
"max_total_reduction must be between 0.0 and 1.0, got {}",
self.max_total_reduction
);
ensure!(
self.base_orchestrator_reduction + self.max_quality_bonus <= self.max_total_reduction,
"base_orchestrator_reduction ({}) + max_quality_bonus ({}) must be <= max_total_reduction ({})",
self.base_orchestrator_reduction,
self.max_quality_bonus,
self.max_total_reduction
);
ensure!(
self.min_inherent_complexity_factor > 0.0,
"min_inherent_complexity_factor must be positive, got {}",
self.min_inherent_complexity_factor
);
ensure!(
(0.0..=1.0).contains(&self.min_composition_quality),
"min_composition_quality must be between 0.0 and 1.0, got {}",
self.min_composition_quality
);
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScoreAdjustment {
pub original_score: f64,
pub adjusted_score: f64,
pub reduction_percent: f64,
pub adjustment_reason: String,
pub quality_score: f64,
}
impl ScoreAdjustment {
pub fn no_adjustment(score: f64) -> Self {
Self {
original_score: score,
adjusted_score: score,
reduction_percent: 0.0,
adjustment_reason: "No adjustment applied".to_string(),
quality_score: 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct CompositionMetrics {
pub callee_count: usize,
pub delegation_ratio: f64,
pub local_complexity: u32,
pub ast_composition_quality: Option<f64>,
}
pub fn extract_composition_metrics(
func_id: &FunctionId,
func: &FunctionMetrics,
call_graph: &CallGraph,
) -> CompositionMetrics {
let callees = call_graph.get_callees(func_id);
let callee_count = callees.len();
let delegation_ratio = if func.length > 0 {
callee_count as f64 / func.length as f64
} else {
0.0
};
let local_complexity = func.cyclomatic;
let ast_composition_quality = func
.composition_metrics
.as_ref()
.map(|cm| cm.composition_quality);
CompositionMetrics {
callee_count,
delegation_ratio,
local_complexity,
ast_composition_quality,
}
}
pub fn adjust_score(
config: &OrchestrationAdjustmentConfig,
base_score: f64,
role: &FunctionRole,
metrics: &CompositionMetrics,
) -> ScoreAdjustment {
if !config.enabled {
return ScoreAdjustment::no_adjustment(base_score);
}
match role {
FunctionRole::Orchestrator => adjust_orchestrator_score(config, base_score, metrics),
_ => ScoreAdjustment::no_adjustment(base_score),
}
}
fn adjust_orchestrator_score(
config: &OrchestrationAdjustmentConfig,
base_score: f64,
metrics: &CompositionMetrics,
) -> ScoreAdjustment {
if metrics.callee_count == 0 {
return ScoreAdjustment {
original_score: base_score,
adjusted_score: base_score,
reduction_percent: 0.0,
adjustment_reason: "Orchestrator with zero callees (no adjustment)".to_string(),
quality_score: 0.0,
};
}
let base_reduction = config.base_orchestrator_reduction;
let quality_score = calculate_composition_quality(config, metrics);
let quality_bonus = config.max_quality_bonus * quality_score;
let total_reduction = (base_reduction + quality_bonus).min(config.max_total_reduction);
let reduction_factor = 1.0 - total_reduction;
let adjusted = base_score * reduction_factor;
let min_complexity = metrics.callee_count as f64 * config.min_inherent_complexity_factor;
let final_score = adjusted.max(min_complexity);
let actual_reduction = (base_score - final_score) / base_score;
ScoreAdjustment {
original_score: base_score,
adjusted_score: final_score,
reduction_percent: actual_reduction * 100.0,
adjustment_reason: format!(
"Orchestrator (callees: {}, delegation: {:.1}%, quality: {:.2})",
metrics.callee_count,
metrics.delegation_ratio * 100.0,
quality_score
),
quality_score,
}
}
pub fn calculate_composition_quality(
config: &OrchestrationAdjustmentConfig,
metrics: &CompositionMetrics,
) -> f64 {
if let Some(ast_quality) = metrics.ast_composition_quality {
return ast_quality.min(1.0).max(config.min_composition_quality);
}
let callee_quality = match metrics.callee_count {
0..=1 => 0.0,
2 => 0.1,
3 => 0.2,
4 => 0.3,
5 => 0.35,
_ => 0.4, };
let delegation_quality = if metrics.delegation_ratio >= 0.5 {
0.4
} else if metrics.delegation_ratio >= 0.2 {
(metrics.delegation_ratio - 0.2) / 0.3 * 0.4
} else {
0.0
};
let complexity_quality = match metrics.local_complexity {
0..=2 => 0.2,
3 => 0.15,
4 => 0.1,
5 => 0.05,
_ => 0.0,
};
let quality = callee_quality + delegation_quality + complexity_quality;
quality.min(1.0).max(config.min_composition_quality)
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn test_default_config_is_valid() {
let config = OrchestrationAdjustmentConfig::default();
assert!(config.validate().is_ok());
}
#[test]
fn test_config_validation_invalid_base_reduction() {
let config = OrchestrationAdjustmentConfig {
base_orchestrator_reduction: 1.5,
..Default::default()
};
assert!(config.validate().is_err());
let config = OrchestrationAdjustmentConfig {
base_orchestrator_reduction: -0.1,
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_config_validation_base_plus_bonus_exceeds_max() {
let config = OrchestrationAdjustmentConfig {
base_orchestrator_reduction: 0.25,
max_quality_bonus: 0.15,
max_total_reduction: 0.30,
..Default::default()
};
assert!(config.validate().is_err(), "base + bonus must be <= max");
}
#[test]
fn test_config_validation_min_complexity_factor() {
let config = OrchestrationAdjustmentConfig {
min_inherent_complexity_factor: 0.0,
..Default::default()
};
assert!(config.validate().is_err());
let config = OrchestrationAdjustmentConfig {
min_inherent_complexity_factor: -1.0,
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_disabled_config() {
let config = OrchestrationAdjustmentConfig {
enabled: false,
..Default::default()
};
let role = FunctionRole::Orchestrator;
let metrics = CompositionMetrics {
callee_count: 8,
delegation_ratio: 0.5,
local_complexity: 2,
ast_composition_quality: None,
};
let adjustment = adjust_score(&config, 100.0, &role, &metrics);
assert_eq!(adjustment.reduction_percent, 0.0);
assert_eq!(adjustment.adjusted_score, 100.0);
}
#[test]
fn test_high_quality_orchestrator_max_reduction() {
let config = OrchestrationAdjustmentConfig::default();
let role = FunctionRole::Orchestrator;
let metrics = CompositionMetrics {
callee_count: 6,
delegation_ratio: 0.5,
local_complexity: 2,
ast_composition_quality: None,
};
let adjustment = adjust_score(&config, 100.0, &role, &metrics);
assert!(
adjustment.reduction_percent >= 25.0 && adjustment.reduction_percent <= 30.0,
"Expected reduction between 25-30%, got {}",
adjustment.reduction_percent
);
assert!(
adjustment.adjusted_score >= 70.0 && adjustment.adjusted_score <= 75.0,
"Expected adjusted score between 70-75, got {}",
adjustment.adjusted_score
);
assert!(
adjustment.quality_score >= 0.8,
"Expected high quality score, got {}",
adjustment.quality_score
);
}
#[test]
fn test_minimum_complexity_floor() {
let config = OrchestrationAdjustmentConfig::default();
let role = FunctionRole::Orchestrator;
let metrics = CompositionMetrics {
callee_count: 10,
delegation_ratio: 0.6,
local_complexity: 2,
ast_composition_quality: None,
};
let adjustment = adjust_score(&config, 30.0, &role, &metrics);
assert!(
adjustment.adjusted_score >= 20.0,
"Score should not go below 20 (10 callees × 2), got {}",
adjustment.adjusted_score
);
}
#[test]
fn test_zero_callee_edge_case() {
let config = OrchestrationAdjustmentConfig::default();
let role = FunctionRole::Orchestrator;
let metrics = CompositionMetrics {
callee_count: 0,
delegation_ratio: 0.0,
local_complexity: 5,
ast_composition_quality: None,
};
let adjustment = adjust_score(&config, 100.0, &role, &metrics);
assert_eq!(adjustment.reduction_percent, 0.0);
assert_eq!(adjustment.adjusted_score, 100.0);
assert!(adjustment.adjustment_reason.contains("zero callees"));
}
#[test]
fn test_composition_quality_excellent() {
let config = OrchestrationAdjustmentConfig::default();
let excellent = CompositionMetrics {
callee_count: 8,
delegation_ratio: 0.6,
local_complexity: 2,
ast_composition_quality: None,
};
let quality = calculate_composition_quality(&config, &excellent);
assert!(
quality >= 0.9,
"Excellent quality should be >= 0.9, got {}",
quality
);
}
#[test]
fn test_composition_quality_good() {
let config = OrchestrationAdjustmentConfig::default();
let good = CompositionMetrics {
callee_count: 4,
delegation_ratio: 0.3,
local_complexity: 3,
ast_composition_quality: None,
};
let quality = calculate_composition_quality(&config, &good);
assert!(
(0.5..0.9).contains(&quality),
"Good quality should be 0.5-0.9, got {}",
quality
);
}
#[test]
fn test_composition_quality_poor() {
let config = OrchestrationAdjustmentConfig::default();
let poor = CompositionMetrics {
callee_count: 2,
delegation_ratio: 0.1,
local_complexity: 8,
ast_composition_quality: None,
};
let quality = calculate_composition_quality(&config, &poor);
assert!(
quality >= config.min_composition_quality && quality <= 0.5,
"Poor quality should be between min and 0.5 (inclusive), got {}",
quality
);
}
#[test]
fn test_composition_quality_respects_config_minimum() {
let config = OrchestrationAdjustmentConfig {
min_composition_quality: 0.6,
..Default::default()
};
let worst = CompositionMetrics {
callee_count: 0,
delegation_ratio: 0.0,
local_complexity: 20,
ast_composition_quality: None,
};
let quality = calculate_composition_quality(&config, &worst);
assert_eq!(
quality, 0.6,
"Quality should never go below configured minimum"
);
}
#[test]
fn test_non_orchestrator_roles_no_adjustment() {
let config = OrchestrationAdjustmentConfig::default();
let metrics = CompositionMetrics {
callee_count: 5,
delegation_ratio: 0.3,
local_complexity: 3,
ast_composition_quality: None,
};
let pure_logic = FunctionRole::PureLogic;
let adj = adjust_score(&config, 100.0, &pure_logic, &metrics);
assert_eq!(adj.reduction_percent, 0.0);
assert_eq!(adj.adjusted_score, 100.0);
let io_wrapper = FunctionRole::IOWrapper;
let adj = adjust_score(&config, 100.0, &io_wrapper, &metrics);
assert_eq!(adj.reduction_percent, 0.0);
assert_eq!(adj.adjusted_score, 100.0);
let entry_point = FunctionRole::EntryPoint;
let adj = adjust_score(&config, 100.0, &entry_point, &metrics);
assert_eq!(adj.reduction_percent, 0.0);
assert_eq!(adj.adjusted_score, 100.0);
let pattern_match = FunctionRole::PatternMatch;
let adj = adjust_score(&config, 100.0, &pattern_match, &metrics);
assert_eq!(adj.reduction_percent, 0.0);
assert_eq!(adj.adjusted_score, 100.0);
let unknown = FunctionRole::Unknown;
let adj = adjust_score(&config, 100.0, &unknown, &metrics);
assert_eq!(adj.reduction_percent, 0.0);
assert_eq!(adj.adjusted_score, 100.0);
}
proptest! {
#[test]
fn prop_adjusted_score_never_exceeds_original(
base_score in 1.0f64..1000.0,
callee_count in 1usize..50,
delegation_ratio in 0.0f64..1.0,
complexity in 0u32..20
) {
let config = OrchestrationAdjustmentConfig::default();
let role = FunctionRole::Orchestrator;
let metrics = CompositionMetrics {
callee_count,
delegation_ratio,
local_complexity: complexity,
ast_composition_quality: None,
};
let adjustment = adjust_score(&config, base_score, &role, &metrics);
let min_floor = callee_count as f64 * config.min_inherent_complexity_factor;
let expected_max = base_score.max(min_floor);
prop_assert!(
adjustment.adjusted_score <= expected_max,
"Adjusted score {} exceeds max(base_score, min_floor) = max({}, {}) = {}",
adjustment.adjusted_score,
base_score,
min_floor,
expected_max
);
}
#[test]
fn prop_adjusted_score_respects_minimum_floor(
base_score in 10.0f64..1000.0,
callee_count in 1usize..50
) {
let config = OrchestrationAdjustmentConfig::default();
let role = FunctionRole::Orchestrator;
let metrics = CompositionMetrics {
callee_count,
delegation_ratio: 0.8,
local_complexity: 2,
ast_composition_quality: None,
};
let adjustment = adjust_score(&config, base_score, &role, &metrics);
let min_floor = callee_count as f64 * config.min_inherent_complexity_factor;
prop_assert!(
adjustment.adjusted_score >= min_floor,
"Adjusted score {} must be >= floor {}",
adjustment.adjusted_score,
min_floor
);
}
#[test]
fn prop_composition_quality_bounded(
callee_count in 0usize..20,
delegation_ratio in 0.0f64..1.0,
complexity in 0u32..20
) {
let config = OrchestrationAdjustmentConfig::default();
let metrics = CompositionMetrics {
callee_count,
delegation_ratio,
local_complexity: complexity,
ast_composition_quality: None,
};
let quality = calculate_composition_quality(&config, &metrics);
prop_assert!(
quality >= config.min_composition_quality && quality <= 1.0,
"Quality {} must be in [{}, 1.0]",
quality,
config.min_composition_quality
);
}
}
}