use crate::models::tdg::{TDGHotspot, TDGSummary};
use anyhow::Result;
use std::fmt::Write;
use std::path::Path;
#[must_use]
pub fn filter_tdg_hotspots(
mut hotspots: Vec<TDGHotspot>,
threshold: f64,
top: usize,
critical_only: bool,
) -> Vec<TDGHotspot> {
if threshold > 0.0 {
hotspots.retain(|h| h.tdg_score >= threshold);
}
if critical_only {
hotspots.retain(|h| h.tdg_score > 2.5);
}
if top > 0 && hotspots.len() > top {
hotspots.truncate(top);
}
hotspots
}
pub fn format_tdg_json(
summary: &TDGSummary,
hotspots: &[TDGHotspot],
include_components: bool,
) -> Result<String> {
let mut json_data = serde_json::json!({
"summary": {
"total_files": summary.total_files,
"critical_files": summary.critical_files,
"warning_files": summary.warning_files,
"average_tdg": summary.average_tdg,
"p95_tdg": summary.p95_tdg,
"p99_tdg": summary.p99_tdg,
"estimated_debt_hours": summary.estimated_debt_hours,
},
"hotspots": hotspots,
});
if include_components {
json_data["components"] = serde_json::json!({
"complexity_weight": 0.4,
"churn_weight": 0.3,
"duplication_weight": 0.2,
"coupling_weight": 0.1,
});
}
serde_json::to_string_pretty(&json_data).map_err(Into::into)
}
pub fn format_tdg_table(hotspots: &[TDGHotspot], verbose: bool) -> Result<String> {
let mut output = String::new();
writeln!(
&mut output,
"| File | TDG Score | Primary Factor | Est. Hours |"
)?;
writeln!(
&mut output,
"|------|-----------|----------------|-----------|"
)?;
for hotspot in hotspots {
writeln!(
&mut output,
"| {} | {:.2} | {} | {:.1} |",
std::path::Path::new(&hotspot.path)
.file_name()
.unwrap_or_default()
.to_string_lossy(),
hotspot.tdg_score,
hotspot.primary_factor,
hotspot.estimated_hours
)?;
if verbose {
writeln!(
&mut output,
"| | Components: C={:.2} Ch={:.2} D={:.2} Co={:.2} |",
hotspot.tdg_score * 0.4, hotspot.tdg_score * 0.3, hotspot.tdg_score * 0.2, hotspot.tdg_score * 0.1, )?;
}
}
Ok(output)
}
pub fn format_tdg_markdown(
summary: &TDGSummary,
hotspots: &[TDGHotspot],
include_components: bool,
) -> Result<String> {
let mut output = String::new();
write_tdg_header(&mut output)?;
write_tdg_summary(&mut output, summary)?;
if !hotspots.is_empty() {
write_tdg_hotspots(&mut output, hotspots, include_components)?;
}
Ok(output)
}
fn write_tdg_header(output: &mut String) -> Result<()> {
use std::fmt::Write;
writeln!(output, "# Technical Debt Gradient Analysis\n")?;
Ok(())
}
fn write_tdg_summary(output: &mut String, summary: &TDGSummary) -> Result<()> {
use std::fmt::Write;
writeln!(output, "## Summary\n")?;
writeln!(output, "- **Total Files**: {}", summary.total_files)?;
writeln!(
output,
"- **Critical Files**: {} (TDG > 2.5)",
summary.critical_files
)?;
writeln!(
output,
"- **Warning Files**: {} (TDG > 1.5)",
summary.warning_files
)?;
writeln!(output, "- **Average TDG**: {:.3}", summary.average_tdg)?;
writeln!(output, "- **95th Percentile**: {:.3}", summary.p95_tdg)?;
writeln!(
output,
"- **Estimated Debt**: {:.1} hours\n",
summary.estimated_debt_hours
)?;
Ok(())
}
fn write_tdg_hotspots(
output: &mut String,
hotspots: &[TDGHotspot],
include_components: bool,
) -> Result<()> {
use std::fmt::Write;
writeln!(output, "## Top Hotspots\n")?;
for (i, hotspot) in hotspots.iter().enumerate() {
write_single_hotspot(output, i + 1, hotspot, include_components)?;
}
Ok(())
}
fn write_single_hotspot(
output: &mut String,
index: usize,
hotspot: &TDGHotspot,
include_components: bool,
) -> Result<()> {
use std::fmt::Write;
writeln!(output, "### {}. {}\n", index, hotspot.path)?;
write_hotspot_basic_info(output, hotspot)?;
if include_components {
write_component_breakdown(output, hotspot)?;
}
Ok(())
}
fn write_hotspot_basic_info(output: &mut String, hotspot: &TDGHotspot) -> Result<()> {
use std::fmt::Write;
writeln!(output, "- **TDG Score**: {:.3}", hotspot.tdg_score)?;
writeln!(output, "- **Primary Factor**: {}", hotspot.primary_factor)?;
writeln!(
output,
"- **Estimated Hours**: {:.1}\n",
hotspot.estimated_hours
)?;
Ok(())
}
fn write_component_breakdown(output: &mut String, hotspot: &TDGHotspot) -> Result<()> {
use std::fmt::Write;
writeln!(output, "#### Component Breakdown:")?;
writeln!(
output,
"- Complexity: {:.3}",
calculate_component_score(hotspot.tdg_score, 0.4)
)?;
writeln!(
output,
"- Churn: {:.3}",
calculate_component_score(hotspot.tdg_score, 0.3)
)?;
writeln!(
output,
"- Duplication: {:.3}",
calculate_component_score(hotspot.tdg_score, 0.2)
)?;
writeln!(
output,
"- Coupling: {:.3}\n",
calculate_component_score(hotspot.tdg_score, 0.1)
)?;
Ok(())
}
fn calculate_component_score(tdg_score: f64, weight: f64) -> f64 {
tdg_score * weight
}
pub fn format_tdg_sarif(hotspots: &[TDGHotspot], project_path: &Path) -> Result<String> {
let mut results = Vec::new();
for hotspot in hotspots {
let level = if hotspot.tdg_score > 2.5 {
"error"
} else if hotspot.tdg_score > 1.5 {
"warning"
} else {
"note"
};
let rule_id = if hotspot.tdg_score > 2.5 {
"critical-tdg"
} else if hotspot.tdg_score > 1.5 {
"high-tdg"
} else {
"moderate-tdg"
};
results.push(serde_json::json!({
"ruleId": rule_id,
"level": level,
"message": {
"text": format!(
"File has TDG score of {:.2} ({}). Estimated refactoring time: {:.1} hours",
hotspot.tdg_score,
hotspot.primary_factor,
hotspot.estimated_hours
)
},
"locations": [{
"physicalLocation": {
"artifactLocation": {
"uri": std::path::Path::new(&hotspot.path)
.strip_prefix(project_path)
.unwrap_or(std::path::Path::new(&hotspot.path))
.to_string_lossy()
}
}
}]
}));
}
let sarif = serde_json::json!({
"version": "2.1.0",
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"runs": [{
"tool": {
"driver": {
"name": "paiml-tdg-analyzer",
"version": env!("CARGO_PKG_VERSION"),
"informationUri": "https://github.com/paiml/paiml-mcp-agent-toolkit",
"rules": generate_tdg_rules(),
}
},
"results": results
}]
});
serde_json::to_string_pretty(&sarif).map_err(Into::into)
}
fn generate_tdg_rules() -> Vec<serde_json::Value> {
vec![
serde_json::json!({
"id": "critical-tdg",
"name": "Critical Technical Debt",
"shortDescription": {
"text": "File has critical technical debt gradient"
},
"fullDescription": {
"text": "Files with TDG > 2.5 require immediate refactoring"
},
"defaultConfiguration": {
"level": "error"
}
}),
serde_json::json!({
"id": "high-tdg",
"name": "High Technical Debt",
"shortDescription": {
"text": "File has high technical debt gradient"
},
"fullDescription": {
"text": "Files with TDG > 1.5 should be refactored soon"
},
"defaultConfiguration": {
"level": "warning"
}
}),
serde_json::json!({
"id": "moderate-tdg",
"name": "Moderate Technical Debt",
"shortDescription": {
"text": "File has moderate technical debt gradient"
},
"fullDescription": {
"text": "Files with TDG > 1.0 should be monitored"
},
"defaultConfiguration": {
"level": "note"
}
}),
]
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}