use super::components::{add_blank_line, add_label_value, add_section_header};
use crate::priority::classification::Severity;
use crate::priority::{DebtType, UnifiedDebtItem};
use crate::tui::results::app::ResultsApp;
use crate::tui::theme::Theme;
use ratatui::{
layout::Rect,
style::{Color, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Wrap},
Frame,
};
const INDENT: usize = 2;
const LABEL_WIDTH: usize = 24; const GAP: usize = 4;
fn debt_type_severity_label(multiplier: f64) -> &'static str {
if multiplier >= 1.35 {
"architectural"
} else if multiplier >= 1.15 {
"code quality"
} else if multiplier >= 1.05 {
"minor issue"
} else if multiplier < 0.95 {
"low priority"
} else {
"standard"
}
}
pub fn build_final_score_section(
item: &UnifiedDebtItem,
theme: &Theme,
_width: u16,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
add_section_header(&mut lines, "final score", theme);
let score = item.unified_score.final_score;
let severity = Severity::from_score_100(score);
let severity_color = match severity {
Severity::Critical => Color::Red,
Severity::High => Color::LightRed,
Severity::Medium => Color::Yellow,
Severity::Low => Color::Green,
};
let label = format!(
"{:width$}",
format!("{}total", " ".repeat(INDENT)),
width = LABEL_WIDTH
);
let gap = " ".repeat(GAP);
lines.push(Line::from(vec![
Span::raw(label),
Span::raw(gap),
Span::styled(format!("{:.1}", score), Style::default().fg(severity_color)),
Span::raw(" "),
Span::styled(
format!("[{}]", severity.as_str().to_lowercase()),
Style::default().fg(severity_color),
),
]));
add_blank_line(&mut lines);
lines
}
pub fn build_raw_inputs_section(
item: &UnifiedDebtItem,
theme: &Theme,
width: u16,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
add_section_header(&mut lines, "raw inputs", theme);
add_label_value(
&mut lines,
"cyclomatic",
item.cyclomatic_complexity.to_string(),
theme,
width,
);
let entropy_adjusted = item
.entropy_analysis
.as_ref()
.map(|e| e.adjusted_complexity)
.or_else(|| {
item.god_object_indicators
.as_ref()
.and_then(|g| g.aggregated_entropy.as_ref())
.map(|e| e.adjusted_complexity)
});
if let Some(adjusted) = entropy_adjusted {
if adjusted != item.cognitive_complexity {
add_label_value(
&mut lines,
"cognitive",
format!(
"{} → {} (entropy-adjusted)",
item.cognitive_complexity, adjusted
),
theme,
width,
);
} else {
add_label_value(
&mut lines,
"cognitive",
item.cognitive_complexity.to_string(),
theme,
width,
);
}
} else {
add_label_value(
&mut lines,
"cognitive",
item.cognitive_complexity.to_string(),
theme,
width,
);
}
add_label_value(
&mut lines,
"nesting",
item.nesting_depth.to_string(),
theme,
width,
);
add_label_value(
&mut lines,
"loc",
item.function_length.to_string(),
theme,
width,
);
add_label_value(
&mut lines,
"upstream deps",
item.upstream_dependencies.to_string(),
theme,
width,
);
add_label_value(
&mut lines,
"downstream deps",
item.downstream_dependencies.to_string(),
theme,
width,
);
if let Some(coverage) = &item.transitive_coverage {
add_label_value(
&mut lines,
"direct coverage",
format!("{:.1}%", coverage.direct * 100.0),
theme,
width,
);
if (coverage.transitive - coverage.direct).abs() > 0.01 {
add_label_value(
&mut lines,
"transitive coverage",
format!("{:.1}%", coverage.transitive * 100.0),
theme,
width,
);
}
}
add_blank_line(&mut lines);
lines
}
pub fn build_score_factors_section(
item: &UnifiedDebtItem,
theme: &Theme,
width: u16,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
add_section_header(&mut lines, "score factors (0-10 scale)", theme);
let has_god_object = matches!(&item.debt_type, DebtType::GodObject { .. })
|| item
.god_object_indicators
.as_ref()
.map(|g| g.is_god_object)
.unwrap_or(false);
let cyc = item.cyclomatic_complexity;
let cog_adjusted = item
.entropy_analysis
.as_ref()
.map(|e| e.adjusted_complexity)
.unwrap_or(item.cognitive_complexity);
let purity_bonus = item.purity_level.map(|level| {
let conf = item.purity_confidence.unwrap_or(0.0);
match level {
crate::core::PurityLevel::StrictlyPure if conf > 0.8 => 0.70,
crate::core::PurityLevel::StrictlyPure => 0.80,
crate::core::PurityLevel::LocallyPure if conf > 0.8 => 0.75,
crate::core::PurityLevel::LocallyPure => 0.85,
crate::core::PurityLevel::ReadOnly if conf > 0.8 => 0.90,
crate::core::PurityLevel::ReadOnly => 0.95,
crate::core::PurityLevel::Impure => 1.0,
}
});
let complexity_formula = if let Some(bonus) = purity_bonus {
if (bonus - 1.0_f64).abs() > 0.01 {
let adj_cyc = (cyc as f64 * bonus) as u32;
let adj_cog = (cog_adjusted as f64 * bonus) as u32;
if has_god_object {
format!(
"({}×0.4 + {}×0.6) / 2 × god [purity ×{:.2}]",
adj_cyc, adj_cog, bonus
)
} else {
format!(
"({}×0.4 + {}×0.6) / 2 [purity ×{:.2}]",
adj_cyc, adj_cog, bonus
)
}
} else if has_god_object {
format!("({}×0.4 + {}×0.6) / 2 × god", cyc, cog_adjusted)
} else {
format!("({}×0.4 + {}×0.6) / 2", cyc, cog_adjusted)
}
} else if has_god_object {
format!("({}×0.4 + {}×0.6) / 2 × god", cyc, cog_adjusted)
} else {
format!("({}×0.4 + {}×0.6) / 2", cyc, cog_adjusted)
};
add_factor_line(
&mut lines,
"complexity",
item.unified_score.complexity_factor,
&complexity_formula,
theme,
width,
);
add_factor_line(
&mut lines,
"coverage gap",
item.unified_score.coverage_factor,
"(1.0 - coverage%) * 10",
theme,
width,
);
add_factor_line(
&mut lines,
"dependencies",
item.unified_score.dependency_factor,
"upstream / 2.0",
theme,
width,
);
add_blank_line(&mut lines);
lines
}
pub fn build_multipliers_section(
item: &UnifiedDebtItem,
theme: &Theme,
width: u16,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
add_section_header(&mut lines, "multipliers applied", theme);
let role_desc = format!("{:?}", item.function_role);
add_multiplier_line(
&mut lines,
"role",
item.unified_score.role_multiplier,
&role_desc,
theme,
width,
);
if let Some(purity) = item.unified_score.purity_factor {
let purity_desc = if let Some(level) = &item.purity_level {
format!("{:?}", level)
} else if let Some(is_pure) = item.is_pure {
if is_pure {
"pure".to_string()
} else {
"impure".to_string()
}
} else {
"data flow analysis".to_string()
};
let desc = if let Some(conf) = item.purity_confidence {
format!("{} ({:.0}% conf)", purity_desc, conf * 100.0)
} else {
purity_desc
};
add_multiplier_line(&mut lines, "purity", purity, &desc, theme, width);
}
if let Some(pattern) = item.unified_score.pattern_factor {
add_multiplier_line(
&mut lines,
"pattern",
pattern,
"data flow vs logic",
theme,
width,
);
}
if let Some(refactor) = item.unified_score.refactorability_factor {
add_multiplier_line(
&mut lines,
"refactorability",
refactor,
"dead stores/escape analysis",
theme,
width,
);
}
if let Some(context) = item.context_multiplier {
let context_type = item
.context_type
.as_ref()
.map(|t| format!("{:?}", t))
.unwrap_or_else(|| "unknown".to_string());
add_multiplier_line(&mut lines, "context", context, &context_type, theme, width);
}
if let Some(struct_mult) = item.unified_score.structural_multiplier {
if (struct_mult - 1.0).abs() > 0.01 {
let desc = if struct_mult > 1.2 {
"deeply nested"
} else if struct_mult > 1.0 {
"moderate nesting"
} else if struct_mult < 0.85 {
"flat structure"
} else {
"good structure"
};
add_multiplier_line(&mut lines, "structural", struct_mult, desc, theme, width);
}
}
let is_god_object = matches!(&item.debt_type, DebtType::GodObject { .. })
|| item
.god_object_indicators
.as_ref()
.map(|g| g.is_god_object)
.unwrap_or(false);
if is_god_object {
let dampening_factor = item
.entropy_analysis
.as_ref()
.map(|e| e.dampening_factor)
.or_else(|| {
item.god_object_indicators
.as_ref()
.and_then(|g| g.aggregated_entropy.as_ref())
.map(|e| e.dampening_factor)
});
if let Some(dampening) = dampening_factor {
let desc = if item.entropy_analysis.is_some() {
"repetitive patterns"
} else {
"aggregated repetition"
};
add_multiplier_line(
&mut lines,
"entropy dampening",
dampening,
desc,
theme,
width,
);
}
}
add_blank_line(&mut lines);
lines
}
pub fn build_scaling_pipeline_section(
item: &UnifiedDebtItem,
theme: &Theme,
width: u16,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
let has_scaling_data = item.unified_score.base_score.is_some()
|| item.unified_score.exponential_factor.is_some()
|| item.unified_score.debt_type_multiplier.is_some()
|| item.unified_score.risk_boost.is_some()
|| item.unified_score.pre_adjustment_score.is_some();
if !has_scaling_data {
return lines;
}
add_section_header(&mut lines, "score scaling pipeline", theme);
if let Some(base) = item.unified_score.base_score {
add_label_value(
&mut lines,
"base score",
format!("{:.2}", base),
theme,
width,
);
}
if let Some(exp) = item.unified_score.exponential_factor {
let exp_desc = if (exp - 1.0).abs() < 0.01 {
"no scaling".to_string()
} else {
format!("^{:.2} applied", exp)
};
add_label_value(&mut lines, "exponential factor", exp_desc, theme, width);
}
if let Some(mult) = item.unified_score.debt_type_multiplier {
let mult_desc = if (mult - 1.0).abs() < 0.01 {
"none".to_string()
} else {
format!("{:.2}x ({})", mult, debt_type_severity_label(mult))
};
add_label_value(&mut lines, "debt type multiplier", mult_desc, theme, width);
}
if let Some(boost) = item.unified_score.risk_boost {
let boost_desc = if (boost - 1.0).abs() < 0.01 {
"none".to_string()
} else {
format!("{:.2}x", boost)
};
add_label_value(&mut lines, "risk boost", boost_desc, theme, width);
}
if let Some(pre_adj) = item.unified_score.pre_adjustment_score {
add_label_value(
&mut lines,
"pre-adjustment score",
format!("{:.2}", pre_adj),
theme,
width,
);
}
add_blank_line(&mut lines);
lines
}
pub fn build_god_object_impact_section(
item: &UnifiedDebtItem,
theme: &Theme,
width: u16,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
let god_object_score = match &item.debt_type {
DebtType::GodObject {
god_object_score, ..
} => Some(*god_object_score),
_ => item
.god_object_indicators
.as_ref()
.filter(|g| g.is_god_object)
.map(|g| g.god_object_score),
};
let Some(go_score) = god_object_score else {
return lines;
};
add_section_header(&mut lines, "god object impact (MAJOR)", theme);
if let Some(indicators) = &item.god_object_indicators {
add_label_value(
&mut lines,
"detection type",
format!("{:?}", indicators.detection_type),
theme,
width,
);
}
add_label_value(
&mut lines,
"god object score",
format!("{:.1}", go_score),
theme,
width,
);
let multiplier = 3.0 + (go_score / 50.0);
let label = format!(
"{:width$}",
format!("{}multiplier", " ".repeat(INDENT)),
width = LABEL_WIDTH
);
let gap = " ".repeat(GAP);
lines.push(Line::from(vec![
Span::raw(label),
Span::raw(gap),
Span::styled(
format!("{:.2}x", multiplier),
Style::default().fg(Color::Red),
),
Span::raw(" "),
Span::styled("(3.0 + score/50)", Style::default().fg(theme.muted)),
]));
if let Some(indicators) = &item.god_object_indicators {
add_label_value(
&mut lines,
"methods",
indicators.method_count.to_string(),
theme,
width,
);
add_label_value(
&mut lines,
"fields",
indicators.field_count.to_string(),
theme,
width,
);
add_label_value(
&mut lines,
"responsibilities",
indicators.responsibility_count.to_string(),
theme,
width,
);
if let Some(weighted) = indicators.weighted_method_count {
add_label_value(
&mut lines,
"weighted methods",
format!("{:.1} (pure-adjusted)", weighted),
theme,
width,
);
}
if let Some(trait_summary) = &indicators.trait_method_summary {
add_label_value(
&mut lines,
"trait methods",
format!(
"{} mandated, {} extractable",
trait_summary.mandated_count, trait_summary.extractable_count
),
theme,
width,
);
}
if indicators.domain_count > 0 {
add_label_value(
&mut lines,
"domains",
format!(
"{} (diversity: {:.2})",
indicators.domain_count, indicators.domain_diversity
),
theme,
width,
);
}
if let Some(metrics) = &indicators.complexity_metrics {
add_label_value(
&mut lines,
"avg complexity",
format!("{:.1}", metrics.avg_cyclomatic),
theme,
width,
);
add_label_value(
&mut lines,
"max complexity",
format!("{}", metrics.max_cyclomatic),
theme,
width,
);
}
}
add_blank_line(&mut lines);
lines
}
pub fn build_orchestration_section(
item: &UnifiedDebtItem,
theme: &Theme,
width: u16,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
let Some(adjustment) = &item.unified_score.adjustment_applied else {
return lines;
};
add_section_header(&mut lines, "orchestration adjustment", theme);
add_label_value(
&mut lines,
"original score",
format!("{:.2}", adjustment.original_score),
theme,
width,
);
add_label_value(
&mut lines,
"adjusted score",
format!("{:.2}", adjustment.adjusted_score),
theme,
width,
);
add_label_value(
&mut lines,
"reduction",
format!("{:.1}%", adjustment.reduction_percent),
theme,
width,
);
add_label_value(
&mut lines,
"reason",
adjustment.adjustment_reason.clone(),
theme,
width,
);
add_blank_line(&mut lines);
lines
}
struct CalculationContext {
is_god_object_item: bool,
has_god_object: bool,
god_multiplier: Option<f64>,
role: f64,
purity: f64,
pattern: f64,
refactor: f64,
context: f64,
struct_mult: f64,
has_coverage_data: bool,
base_score: f64,
exponent: f64,
risk_boost: f64,
final_score: f64,
}
fn collect_calculation_context(item: &UnifiedDebtItem) -> CalculationContext {
let has_god_object = matches!(&item.debt_type, DebtType::GodObject { .. })
|| item
.god_object_indicators
.as_ref()
.map(|g| g.is_god_object)
.unwrap_or(false);
let god_multiplier = if has_god_object {
let go_score = match &item.debt_type {
DebtType::GodObject {
god_object_score, ..
} => *god_object_score,
_ => item
.god_object_indicators
.as_ref()
.map(|g| g.god_object_score)
.unwrap_or(0.0),
};
Some(3.0 + (go_score / 50.0))
} else {
None
};
let role = item.unified_score.role_multiplier;
let purity = item.unified_score.purity_factor.unwrap_or(1.0);
let pattern = item.unified_score.pattern_factor.unwrap_or(1.0);
let refactor = item.unified_score.refactorability_factor.unwrap_or(1.0);
let context = item.context_multiplier.unwrap_or(1.0);
let struct_mult = item.unified_score.structural_multiplier.unwrap_or_else(|| {
if item.cyclomatic_complexity == 0 {
1.0
} else {
let ratio = item.nesting_depth as f64 / item.cyclomatic_complexity as f64;
match ratio {
r if r >= 0.6 => 1.5,
r if r >= 0.5 => 1.3,
r if r >= 0.4 => 1.15,
r if r >= 0.2 => 1.0,
r if r >= 0.1 => 0.85,
_ => 0.7,
}
}
});
CalculationContext {
is_god_object_item: matches!(item.debt_type, DebtType::GodObject { .. }),
has_god_object,
god_multiplier,
role,
purity,
pattern,
refactor,
context,
struct_mult,
has_coverage_data: item.unified_score.has_coverage_data,
base_score: item.unified_score.base_score.unwrap_or(0.0),
exponent: item.unified_score.exponential_factor.unwrap_or(1.0),
risk_boost: item.unified_score.risk_boost.unwrap_or(1.0),
final_score: item.unified_score.final_score,
}
}
fn build_god_object_formula_lines(theme: &Theme, width: u16) -> Vec<Line<'static>> {
let mut lines = Vec::new();
add_label_value(
&mut lines,
"formula",
"M × F × R × S × 20 × violations × adjustments".to_string(),
theme,
width,
);
add_label_value(
&mut lines,
"where",
"M=methods/20, F=fields/15, R=resp/3, S=lines/1000".to_string(),
theme,
width,
);
add_label_value(
&mut lines,
"adjustments",
"entropy dampening, complexity weight, functional bonus".to_string(),
theme,
width,
);
add_label_value(
&mut lines,
"note",
"factors capped at 3.0, uses weighted methods if available".to_string(),
theme,
width,
);
lines
}
fn build_regular_formula_lines(
ctx: &CalculationContext,
item: &UnifiedDebtItem,
theme: &Theme,
width: u16,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
let formula = if ctx.has_coverage_data {
if ctx.has_god_object {
"(C×5 + D×2.5) × cov × role × struct × god"
} else {
"(C×5 + D×2.5) × cov × role × struct"
}
} else if ctx.has_god_object {
"(C×5 + D×2.5) × role × struct × god"
} else {
"(C×5 + D×2.5) × role × struct"
};
add_label_value(&mut lines, "formula", formula.to_string(), theme, width);
let c_display = if let Some(gm) = ctx.god_multiplier {
item.unified_score.complexity_factor / gm
} else {
item.unified_score.complexity_factor
};
if ctx.has_coverage_data {
add_label_value(
&mut lines,
"where",
format!(
"C={:.1}, D={:.1}, cov={:.2}, role={:.2}, struct={:.2}",
c_display,
item.unified_score.dependency_factor,
item.unified_score.coverage_factor / 10.0,
ctx.role,
ctx.struct_mult
),
theme,
width,
);
} else {
add_label_value(
&mut lines,
"where",
format!(
"C={:.1}, D={:.1}, role={:.2}, struct={:.2}",
c_display, item.unified_score.dependency_factor, ctx.role, ctx.struct_mult
),
theme,
width,
);
}
let other_mults: Vec<String> = [
(ctx.purity, "purity"),
(ctx.pattern, "pattern"),
(ctx.refactor, "refactor"),
(ctx.context, "context"),
]
.iter()
.filter(|(v, _)| (*v - 1.0).abs() > 0.01)
.map(|(v, name)| format!("{}={:.2}", name, v))
.collect();
if !other_mults.is_empty() {
add_label_value(
&mut lines,
"other mults",
other_mults.join(", "),
theme,
width,
);
}
if let Some(gm) = ctx.god_multiplier {
add_label_value(
&mut lines,
"god_mult",
format!("{:.2} (3.0 + score/50)", gm),
theme,
width,
);
}
lines
}
#[derive(Debug, Clone)]
struct GodObjectMetrics {
methods: usize,
fields: usize,
responsibilities: usize,
lines_of_code: usize,
weighted_methods: Option<f64>,
go_score: f64,
entropy_damp: Option<f64>,
}
#[derive(Debug, Clone, Copy)]
struct GodObjectThresholds {
max_methods: usize,
max_fields: usize,
max_lines: usize,
max_responsibilities: usize,
}
impl Default for GodObjectThresholds {
fn default() -> Self {
Self {
max_methods: 20,
max_fields: 15,
max_lines: 1000,
max_responsibilities: 5,
}
}
}
#[derive(Debug, Clone, Copy)]
struct GodObjectFactors {
method_factor: f64,
field_factor: f64,
resp_factor: f64,
size_factor: f64,
}
impl GodObjectFactors {
fn base_product(&self) -> f64 {
self.method_factor * self.field_factor * self.resp_factor * self.size_factor
}
}
fn extract_god_object_metrics(item: &UnifiedDebtItem) -> GodObjectMetrics {
let stored_base = item.unified_score.base_score.unwrap_or(0.0);
let go_score = match &item.debt_type {
DebtType::GodObject {
god_object_score, ..
} => *god_object_score,
_ => stored_base,
};
let (methods, fields, responsibilities, lines_of_code) = match &item.debt_type {
DebtType::GodObject {
methods,
fields,
responsibilities,
lines: loc,
..
} => (
*methods as usize,
fields.unwrap_or(0) as usize,
*responsibilities as usize,
*loc as usize,
),
_ => item
.god_object_indicators
.as_ref()
.map(|ind| {
(
ind.method_count,
ind.field_count,
ind.responsibility_count,
ind.lines_of_code,
)
})
.unwrap_or((0, 0, 0, 0)),
};
let weighted_methods = item
.god_object_indicators
.as_ref()
.and_then(|g| g.weighted_method_count);
let entropy_damp = item.entropy_analysis.as_ref().map(|e| e.dampening_factor);
GodObjectMetrics {
methods,
fields,
responsibilities,
lines_of_code,
weighted_methods,
go_score,
entropy_damp,
}
}
fn calculate_god_object_factors(
metrics: &GodObjectMetrics,
thresholds: &GodObjectThresholds,
) -> GodObjectFactors {
let effective_methods = metrics.weighted_methods.unwrap_or(metrics.methods as f64);
GodObjectFactors {
method_factor: (effective_methods / thresholds.max_methods as f64).min(3.0),
field_factor: (metrics.fields as f64 / thresholds.max_fields as f64).min(3.0),
resp_factor: (metrics.responsibilities as f64 / 3.0).min(3.0),
size_factor: (metrics.lines_of_code as f64 / thresholds.max_lines as f64).min(3.0),
}
}
fn build_god_object_violations(
metrics: &GodObjectMetrics,
thresholds: &GodObjectThresholds,
) -> Vec<String> {
let effective_methods = metrics.weighted_methods.unwrap_or(metrics.methods as f64);
[
(
effective_methods > thresholds.max_methods as f64,
format!(
"methods {:.0} > {}",
effective_methods, thresholds.max_methods
),
),
(
metrics.fields > thresholds.max_fields,
format!("fields {} > {}", metrics.fields, thresholds.max_fields),
),
(
metrics.responsibilities > thresholds.max_responsibilities,
format!(
"responsibilities {} > {}",
metrics.responsibilities, thresholds.max_responsibilities
),
),
(
metrics.lines_of_code > thresholds.max_lines,
format!("lines {} > {}", metrics.lines_of_code, thresholds.max_lines),
),
]
.into_iter()
.filter(|(violated, _)| *violated)
.map(|(_, msg)| msg)
.collect()
}
fn build_god_object_factor_lines(
metrics: &GodObjectMetrics,
factors: &GodObjectFactors,
thresholds: &GodObjectThresholds,
theme: &Theme,
width: u16,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
let method_display = match metrics.weighted_methods {
Some(w) if (w - metrics.methods as f64).abs() > 0.1 => format!(
"{:.2} = {:.1} / {} (raw {} → weighted)",
factors.method_factor, w, thresholds.max_methods, metrics.methods
),
_ => format!(
"{:.2} = {} / {} (max 3.0)",
factors.method_factor, metrics.methods, thresholds.max_methods
),
};
add_label_value(&mut lines, "1. method factor", method_display, theme, width);
add_label_value(
&mut lines,
"2. field factor",
format!(
"{:.2} = {} / {} (max 3.0)",
factors.field_factor, metrics.fields, thresholds.max_fields
),
theme,
width,
);
add_label_value(
&mut lines,
"3. responsibility factor",
format!(
"{:.2} = {} / 3 (max 3.0)",
factors.resp_factor, metrics.responsibilities
),
theme,
width,
);
add_label_value(
&mut lines,
"4. size factor",
format!(
"{:.2} = {} / {} (max 3.0)",
factors.size_factor, metrics.lines_of_code, thresholds.max_lines
),
theme,
width,
);
let base_product = factors.base_product();
add_label_value(
&mut lines,
"5. base product",
format!(
"{:.2} = {:.2} × {:.2} × {:.2} × {:.2}",
base_product,
factors.method_factor,
factors.field_factor,
factors.resp_factor,
factors.size_factor
),
theme,
width,
);
lines
}
fn build_god_object_scaling_lines(
factors: &GodObjectFactors,
violations: &[String],
theme: &Theme,
width: u16,
) -> (f64, usize, Vec<Line<'static>>) {
let mut lines = Vec::new();
let base_product = factors.base_product();
let violation_count = violations.len();
let violation_detail = if violations.is_empty() {
"none".to_string()
} else {
violations.join(", ")
};
add_label_value(
&mut lines,
"6. violations",
format!("{} ({})", violation_count, violation_detail),
theme,
width,
);
let scaled_score = if violation_count > 0 {
base_product * 20.0 * violation_count as f64
} else {
base_product * 10.0
};
let scaling_detail = if violation_count > 0 {
format!(
"{:.2} × 20 × {} = {:.2}",
base_product, violation_count, scaled_score
)
} else {
format!("{:.2} × 10 = {:.2}", base_product, scaled_score)
};
add_label_value(&mut lines, "7. scaling", scaling_detail, theme, width);
(scaled_score, 8, lines)
}
fn build_god_object_adjustment_lines(
metrics: &GodObjectMetrics,
mut current: f64,
mut step_num: usize,
theme: &Theme,
width: u16,
) -> (usize, f64, Vec<Line<'static>>) {
let mut lines = Vec::new();
if let Some(entropy_damp) = metrics.entropy_damp {
if (entropy_damp - 1.0).abs() > 0.01 {
let after_entropy = current * entropy_damp;
add_label_value(
&mut lines,
&format!("{}. × entropy", step_num),
format!(
"{:.2} × {:.2} = {:.2} (dampening)",
current, entropy_damp, after_entropy
),
theme,
width,
);
current = after_entropy;
step_num += 1;
}
}
let diff = (current - metrics.go_score).abs();
if diff > 1.0 && current > 0.0 {
let adjustment_factor = metrics.go_score / current;
if (adjustment_factor - 1.0).abs() > 0.01 {
let adjustment_desc = if adjustment_factor > 1.0 {
"complexity weight"
} else {
"functional bonus"
};
add_label_value(
&mut lines,
&format!("{}. × {}", step_num, adjustment_desc),
format!(
"{:.2} × {:.2} = {:.2}",
current, adjustment_factor, metrics.go_score
),
theme,
width,
);
step_num += 1;
}
}
add_label_value(
&mut lines,
&format!("{}. god object score", step_num),
format!("{:.2}", metrics.go_score),
theme,
width,
);
(step_num + 1, metrics.go_score, lines)
}
fn build_god_object_calculation_steps(
item: &UnifiedDebtItem,
theme: &Theme,
width: u16,
) -> (usize, f64, Vec<Line<'static>>) {
let metrics = extract_god_object_metrics(item);
let thresholds = GodObjectThresholds::default();
let factors = calculate_god_object_factors(&metrics, &thresholds);
let violations = build_god_object_violations(&metrics, &thresholds);
let mut lines = build_god_object_factor_lines(&metrics, &factors, &thresholds, theme, width);
let (scaled_score, step_num, scaling_lines) =
build_god_object_scaling_lines(&factors, &violations, theme, width);
lines.extend(scaling_lines);
let (final_step, final_value, adjustment_lines) =
build_god_object_adjustment_lines(&metrics, scaled_score, step_num, theme, width);
lines.extend(adjustment_lines);
(final_step, final_value, lines)
}
fn build_regular_calculation_steps(
ctx: &CalculationContext,
item: &UnifiedDebtItem,
theme: &Theme,
width: u16,
) -> (usize, f64, Vec<Line<'static>>) {
let mut lines = Vec::new();
let c = item.unified_score.complexity_factor;
let d = item.unified_score.dependency_factor;
let weighted_base = (c * 5.0) + (d * 2.5);
let (displayed_base, formula_detail) = if ctx.has_coverage_data {
let cov_mult = item.unified_score.coverage_factor / 10.0;
let base_with_cov = weighted_base * cov_mult;
(
base_with_cov,
format!(
"{:.2} = (C×5 + D×2.5) × cov = ({:.1}×5 + {:.1}×2.5) × {:.2}",
base_with_cov, c, d, cov_mult
),
)
} else {
(
weighted_base,
format!(
"{:.2} = C×5 + D×2.5 = {:.1}×5 + {:.1}×2.5",
weighted_base, c, d
),
)
};
add_label_value(&mut lines, "1. weighted base", formula_detail, theme, width);
let after_role = displayed_base * ctx.role;
let mut next_step = 2_usize;
if (ctx.role - 1.0).abs() > 0.01 {
add_label_value(
&mut lines,
"2. × role",
format!(
"{:.2} × {:.2} = {:.2}",
displayed_base, ctx.role, after_role
),
theme,
width,
);
next_step = 3;
}
let after_struct = after_role * ctx.struct_mult;
if (ctx.struct_mult - 1.0).abs() > 0.01 {
add_label_value(
&mut lines,
&format!("{}. × struct", next_step),
format!(
"{:.2} × {:.2} = {:.2}",
after_role, ctx.struct_mult, after_struct
),
theme,
width,
);
next_step += 1;
}
(next_step, after_struct, lines)
}
fn build_adjustment_steps(
item: &UnifiedDebtItem,
ctx: &CalculationContext,
mut step_num: usize,
mut current_value: f64,
theme: &Theme,
width: u16,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
if let Some(adj) = &item.unified_score.adjustment_applied {
if adj.reduction_percent.abs() > 0.1 {
let after_orch = current_value * (1.0 - adj.reduction_percent / 100.0);
add_label_value(
&mut lines,
&format!("{}. orchestration", step_num),
format!(
"{:.2} × {:.2} = {:.2} ({})",
current_value,
1.0 - adj.reduction_percent / 100.0,
after_orch,
adj.adjustment_reason
),
theme,
width,
);
current_value = after_orch;
step_num += 1;
}
}
if let Some(ctx_mult) = item.context_multiplier {
if (ctx_mult - 1.0).abs() > 0.01 {
let ctx_type_name = item
.context_type
.as_ref()
.map(|t| format!("{:?}", t).to_lowercase())
.unwrap_or_else(|| "context".to_string());
let after_ctx = current_value * ctx_mult;
add_label_value(
&mut lines,
&format!("{}. × context", step_num),
format!(
"{:.2} × {:.2} = {:.2} ({} dampening)",
current_value, ctx_mult, after_ctx, ctx_type_name
),
theme,
width,
);
current_value = after_ctx;
step_num += 1;
}
}
if let Some(debt) = &item.unified_score.debt_adjustment {
if debt.total.abs() > 0.01 {
let after_debt = current_value + debt.total;
let components: Vec<String> = [
(debt.testing, "test"),
(debt.resource, "res"),
(debt.duplication, "dup"),
]
.iter()
.filter(|(v, _)| v.abs() > 0.01)
.map(|(v, name)| format!("{}:{:+.2}", name, v))
.collect();
let breakdown = if components.is_empty() {
String::new()
} else {
format!(" ({})", components.join(", "))
};
add_label_value(
&mut lines,
&format!("{}. + debt", step_num),
format!(
"{:.2} + {:.2} = {:.2}{}",
current_value, debt.total, after_debt, breakdown
),
theme,
width,
);
current_value = after_debt;
step_num += 1;
}
}
if let Some(risk_mult) = item.unified_score.contextual_risk_multiplier {
if (risk_mult - 1.0).abs() > 0.001 {
let pre_ctx = item
.unified_score
.pre_contextual_score
.unwrap_or(current_value);
let after_risk = pre_ctx * risk_mult;
add_label_value(
&mut lines,
&format!("{}. × risk", step_num),
format!(
"{:.2} × {:.2} = {:.2} (git history risk)",
pre_ctx, risk_mult, after_risk
),
theme,
width,
);
current_value = after_risk;
step_num += 1;
}
}
let gap_to_base = (ctx.base_score - current_value).abs();
let gap_to_final = (ctx.final_score - current_value).abs();
let base_close_to_final = (ctx.base_score - ctx.final_score).abs() < ctx.final_score * 0.05;
if gap_to_base > 0.5 && base_close_to_final {
let pre_norm = item.unified_score.pre_normalization_score;
if ctx.base_score > current_value && current_value > 0.0 {
let diff = ctx.base_score - current_value;
let explanation = if let Some(pn) = pre_norm {
format!("{:.2} → {:.2} → {:.2}", current_value, pn, ctx.base_score)
} else {
format!(
"{:.2} + {:.2} = {:.2} (combined adjustments)",
current_value, diff, ctx.base_score
)
};
add_label_value(
&mut lines,
&format!("{}. adjusted", step_num),
explanation,
theme,
width,
);
} else if ctx.base_score < current_value {
add_label_value(
&mut lines,
&format!("{}. adjusted", step_num),
format!("{:.2} → {:.2}", current_value, ctx.base_score),
theme,
width,
);
} else {
add_label_value(
&mut lines,
&format!("{}. normalized", step_num),
format!("{:.2} → {:.2}", current_value, ctx.base_score),
theme,
width,
);
}
} else if gap_to_final > 0.5 && gap_to_base > 0.5 {
add_label_value(
&mut lines,
&format!("{}. final adjustments", step_num),
format!("{:.2} → {:.2}", current_value, ctx.final_score),
theme,
width,
);
}
let mut current = ctx.base_score;
if (ctx.exponent - 1.0).abs() > 0.01 {
let after_exp = current.powf(ctx.exponent);
add_label_value(
&mut lines,
"exponential",
format!("{:.2}^{:.2} = {:.2}", current, ctx.exponent, after_exp),
theme,
width,
);
current = after_exp;
}
if (ctx.risk_boost - 1.0).abs() > 0.01 {
let after_boost = current * ctx.risk_boost;
add_label_value(
&mut lines,
"risk boost",
format!(
"{:.2} × {:.2} = {:.2}",
current, ctx.risk_boost, after_boost
),
theme,
width,
);
}
add_label_value(
&mut lines,
"final",
format!("{:.1}", ctx.final_score),
theme,
width,
);
lines
}
pub fn build_calculation_summary_section(
item: &UnifiedDebtItem,
theme: &Theme,
width: u16,
) -> Vec<Line<'static>> {
let ctx = collect_calculation_context(item);
let mut lines = Vec::new();
add_section_header(&mut lines, "score formula (simplified)", theme);
if ctx.is_god_object_item {
lines.extend(build_god_object_formula_lines(theme, width));
} else {
lines.extend(build_regular_formula_lines(&ctx, item, theme, width));
}
add_blank_line(&mut lines);
add_section_header(&mut lines, "calculation steps", theme);
let (step_num, current_value, step_lines) = if ctx.is_god_object_item {
build_god_object_calculation_steps(item, theme, width)
} else {
build_regular_calculation_steps(&ctx, item, theme, width)
};
lines.extend(step_lines);
lines.extend(build_adjustment_steps(
item,
&ctx,
step_num,
current_value,
theme,
width,
));
add_blank_line(&mut lines);
lines
}
fn add_factor_line(
lines: &mut Vec<Line<'static>>,
label: &str,
value: f64,
formula: &str,
theme: &Theme,
_width: u16,
) {
let label_formatted = format!(
"{:width$}",
format!("{}{}", " ".repeat(INDENT), label),
width = LABEL_WIDTH
);
let gap = " ".repeat(GAP);
lines.push(Line::from(vec![
Span::raw(label_formatted),
Span::raw(gap),
Span::styled(format!("{:.2}", value), Style::default().fg(theme.primary)),
Span::raw(" "),
Span::styled(format!("({})", formula), Style::default().fg(theme.muted)),
]));
}
fn add_multiplier_line(
lines: &mut Vec<Line<'static>>,
label: &str,
value: f64,
reason: &str,
theme: &Theme,
_width: u16,
) {
let color = if value < 0.8 {
Color::Green } else if value > 1.2 {
Color::Red } else {
theme.primary };
let effect = if value < 0.95 {
"reduces"
} else if value > 1.05 {
"increases"
} else {
"neutral"
};
let label_formatted = format!(
"{:width$}",
format!("{}{}", " ".repeat(INDENT), label),
width = LABEL_WIDTH
);
let gap = " ".repeat(GAP);
lines.push(Line::from(vec![
Span::raw(label_formatted),
Span::raw(gap),
Span::styled(format!("{:.2}x", value), Style::default().fg(color)),
Span::raw(" "),
Span::styled(
format!("[{}] {}", effect, reason),
Style::default().fg(theme.muted),
),
]));
}
pub fn build_item_indicator_section(
current_index: usize,
total_items: usize,
debt_type: &DebtType,
theme: &Theme,
_width: u16,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
if total_items > 1 {
let debt_name = super::overview::format_debt_type_name(debt_type);
lines.push(Line::from(vec![Span::styled(
format!(
"viewing item {} of {}: {}",
current_index + 1,
total_items,
debt_name
),
Style::default().fg(theme.muted),
)]));
lines.push(Line::from("")); }
lines
}
pub fn build_combined_reference_line(
item_score: f64,
combined_score: f64,
other_item_count: usize,
theme: &Theme,
_width: u16,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
if other_item_count > 0 {
let other_text = if other_item_count == 1 {
"1 other item".to_string()
} else {
format!("{} other items", other_item_count)
};
const INDENT: usize = 2;
const LABEL_WIDTH: usize = 24;
const GAP: usize = 4;
let label = format!(
"{:width$}",
format!("{}location combined", " ".repeat(INDENT)),
width = LABEL_WIDTH
);
let gap = " ".repeat(GAP);
lines.push(Line::from(vec![
Span::raw(label),
Span::raw(gap),
Span::styled(
format!("{:.1}", combined_score),
Style::default().fg(theme.primary),
),
Span::raw(" "),
Span::styled(
format!("(this + {})", other_text),
Style::default().fg(theme.muted),
),
]));
lines.push(Line::from(""));
let _ = item_score; }
lines
}
pub fn build_page_lines(item: &UnifiedDebtItem, theme: &Theme, width: u16) -> Vec<Line<'static>> {
[
build_final_score_section(item, theme, width),
build_raw_inputs_section(item, theme, width),
build_score_factors_section(item, theme, width),
build_multipliers_section(item, theme, width),
build_scaling_pipeline_section(item, theme, width),
build_god_object_impact_section(item, theme, width),
build_orchestration_section(item, theme, width),
build_calculation_summary_section(item, theme, width),
]
.into_iter()
.flatten()
.collect()
}
pub fn build_page_lines_with_context(
item: &UnifiedDebtItem,
location_items: &[&UnifiedDebtItem],
current_item_index: usize,
theme: &Theme,
width: u16,
) -> Vec<Line<'static>> {
let mut result = Vec::new();
if location_items.len() > 1 {
result.extend(build_item_indicator_section(
current_item_index,
location_items.len(),
&item.debt_type,
theme,
width,
));
}
result.extend(build_final_score_section(item, theme, width));
result.extend(build_raw_inputs_section(item, theme, width));
result.extend(build_score_factors_section(item, theme, width));
result.extend(build_multipliers_section(item, theme, width));
result.extend(build_scaling_pipeline_section(item, theme, width));
result.extend(build_god_object_impact_section(item, theme, width));
result.extend(build_orchestration_section(item, theme, width));
result.extend(build_calculation_summary_section(item, theme, width));
if location_items.len() > 1 {
let combined_score: f64 = location_items
.iter()
.map(|i| i.unified_score.final_score)
.sum();
result.extend(build_combined_reference_line(
item.unified_score.final_score,
combined_score,
location_items.len() - 1,
theme,
width,
));
}
result
}
pub fn render(
frame: &mut Frame,
app: &ResultsApp,
item: &UnifiedDebtItem,
area: Rect,
theme: &Theme,
) {
let location_items = super::overview::get_items_at_location(app, item);
let lines = build_page_lines_with_context(item, &location_items, 0, theme, area.width);
let paragraph = Paragraph::new(lines)
.block(Block::default().borders(Borders::NONE))
.wrap(Wrap { trim: false })
.scroll(app.detail_scroll_offset());
frame.render_widget(paragraph, area);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::priority::unified_scorer::{Location, UnifiedScore};
use crate::priority::{ActionableRecommendation, FunctionRole, ImpactMetrics};
fn create_test_item(final_score: f64, debt_type: DebtType) -> UnifiedDebtItem {
UnifiedDebtItem {
location: Location {
file: std::path::PathBuf::from("test.rs"),
line: 10,
function: "test_func".to_string(),
},
unified_score: UnifiedScore {
final_score: final_score.max(0.0),
complexity_factor: 5.0,
coverage_factor: 7.0,
dependency_factor: 3.0,
role_multiplier: 1.2,
base_score: Some(25.0),
exponential_factor: None,
risk_boost: None,
pre_adjustment_score: None,
adjustment_applied: None,
purity_factor: Some(0.7),
refactorability_factor: Some(1.0),
pattern_factor: Some(0.85),
debt_adjustment: None,
pre_normalization_score: None,
structural_multiplier: Some(1.15),
has_coverage_data: false,
contextual_risk_multiplier: None,
pre_contextual_score: None,
debt_type_multiplier: None,
},
debt_type,
function_role: FunctionRole::PureLogic,
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 {
complexity_reduction: 5.0,
coverage_improvement: 0.1,
lines_reduction: 10,
risk_reduction: 0.2,
},
transitive_coverage: None,
file_context: None,
upstream_dependencies: 5,
downstream_dependencies: 10,
upstream_callers: vec![],
downstream_callees: vec![],
upstream_production_callers: vec![],
upstream_test_callers: vec![],
production_blast_radius: 0,
nesting_depth: 3,
function_length: 100,
cyclomatic_complexity: 15,
cognitive_complexity: 25,
is_pure: Some(true),
purity_confidence: Some(0.9),
purity_level: None,
god_object_indicators: None,
tier: None,
function_context: None,
context_confidence: None,
contextual_recommendation: None,
pattern_analysis: None,
context_multiplier: Some(0.9),
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,
}
}
#[test]
fn test_build_final_score_section() {
let item = create_test_item(
65.0,
DebtType::ComplexityHotspot {
cyclomatic: 15,
cognitive: 25,
},
);
let theme = Theme::default();
let lines = build_final_score_section(&item, &theme, 80);
assert!(!lines.is_empty());
let content: String = lines
.iter()
.flat_map(|l| l.spans.iter())
.map(|s| s.content.as_ref())
.collect();
assert!(content.contains("65.0"));
assert!(content.contains("high")); }
#[test]
fn test_build_raw_inputs_section() {
let item = create_test_item(
50.0,
DebtType::ComplexityHotspot {
cyclomatic: 15,
cognitive: 25,
},
);
let theme = Theme::default();
let lines = build_raw_inputs_section(&item, &theme, 80);
let content: String = lines
.iter()
.flat_map(|l| l.spans.iter())
.map(|s| s.content.as_ref())
.collect();
assert!(content.contains("cyclomatic"));
assert!(content.contains("15"));
assert!(content.contains("cognitive"));
assert!(content.contains("25"));
}
#[test]
fn test_build_score_factors_section() {
let item = create_test_item(
50.0,
DebtType::ComplexityHotspot {
cyclomatic: 15,
cognitive: 25,
},
);
let theme = Theme::default();
let lines = build_score_factors_section(&item, &theme, 80);
let content: String = lines
.iter()
.flat_map(|l| l.spans.iter())
.map(|s| s.content.as_ref())
.collect();
assert!(content.contains("complexity"));
assert!(content.contains("5.00")); }
#[test]
fn test_build_multipliers_section() {
let item = create_test_item(
50.0,
DebtType::ComplexityHotspot {
cyclomatic: 15,
cognitive: 25,
},
);
let theme = Theme::default();
let lines = build_multipliers_section(&item, &theme, 80);
let content: String = lines
.iter()
.flat_map(|l| l.spans.iter())
.map(|s| s.content.as_ref())
.collect();
assert!(content.contains("role"));
assert!(content.contains("1.20")); assert!(content.contains("purity"));
assert!(content.contains("0.70")); }
#[test]
fn test_god_object_section_shown_for_god_objects() {
let item = create_test_item(
90.0,
DebtType::GodObject {
methods: 50,
fields: Some(20),
responsibilities: 8,
lines: 1000,
god_object_score: 75.0,
},
);
let theme = Theme::default();
let lines = build_god_object_impact_section(&item, &theme, 80);
assert!(!lines.is_empty());
let content: String = lines
.iter()
.flat_map(|l| l.spans.iter())
.map(|s| s.content.as_ref())
.collect();
assert!(content.contains("god object"));
assert!(content.contains("75.0"));
}
#[test]
fn test_god_object_section_empty_for_non_god_objects() {
let item = create_test_item(
50.0,
DebtType::ComplexityHotspot {
cyclomatic: 15,
cognitive: 25,
},
);
let theme = Theme::default();
let lines = build_god_object_impact_section(&item, &theme, 80);
assert!(lines.is_empty());
}
#[test]
fn test_build_page_lines_combines_all_sections() {
let item = create_test_item(
75.0,
DebtType::ComplexityHotspot {
cyclomatic: 15,
cognitive: 25,
},
);
let theme = Theme::default();
let lines = build_page_lines(&item, &theme, 80);
assert!(lines.len() > 10);
}
#[test]
fn test_column_alignment_consistent() {
let item = create_test_item(
50.0,
DebtType::ComplexityHotspot {
cyclomatic: 15,
cognitive: 25,
},
);
let theme = Theme::default();
let factor_lines = build_score_factors_section(&item, &theme, 80);
let mult_lines = build_multipliers_section(&item, &theme, 80);
for line in factor_lines.iter().skip(1) {
if !line.spans.is_empty() && !line.spans[0].content.is_empty() {
let first_span = &line.spans[0].content;
if !first_span.trim().is_empty() {
assert!(
first_span.len() >= INDENT,
"Line should have at least {} indent: {:?}",
INDENT,
first_span
);
}
}
}
for line in mult_lines.iter().skip(1) {
if !line.spans.is_empty() && !line.spans[0].content.is_empty() {
let first_span = &line.spans[0].content;
if !first_span.trim().is_empty() {
assert!(
first_span.len() >= INDENT,
"Line should have at least {} indent: {:?}",
INDENT,
first_span
);
}
}
}
}
}