use crate::model::{AuditReport, RiskFinding};
pub fn render_markdown(report: &AuditReport) -> String {
let mut out = String::new();
out.push_str("# Rust Technical Audit Report\n\n");
out.push_str("## Executive Summary\n\n");
out.push_str(&format!(
"Repository `{}` received an overall technical due diligence score of **{}/100**.\n\n",
report.repository_path, report.overall_score
));
out.push_str(&format!("{}\n\n", report.overview.summary));
out.push_str("### Weighted Scoring Model\n\n");
out.push_str("| Area | Weight | Score |\n| --- | ---: | ---: |\n");
out.push_str(&format!(
"| Dependency Health | 20% | {} |\n",
report.dependencies.score
));
out.push_str(&format!(
"| Code Quality | 25% | {} |\n",
report.code_quality.score
));
out.push_str(&format!(
"| Architecture | 25% | {} |\n",
report.architecture.score
));
out.push_str(&format!("| Testing | 15% | {} |\n", report.testing.score));
out.push_str(&format!(
"| Risk Posture | 15% | {} |\n\n",
report.risks.score
));
out.push_str("## Architecture\n\n");
out.push_str(&format!(
"- Style: {}\n- Separation of concerns: {}\n- Detected layers: {}\n- Domain boundaries: {}\n\n",
report.architecture.architecture_style,
report.architecture.separation_of_concerns,
list_or_none(&report.architecture.detected_layers),
list_or_none(&report.architecture.domain_boundaries)
));
out.push_str("## Dependency Health\n\n");
out.push_str(&format!(
"- Total direct dependencies: {}\n- Critical dependencies: {}\n- Maintenance risks: {}\n- Outdated indicators: {}\n\n",
report.dependencies.direct_dependencies,
list_or_none(&report.dependencies.critical_dependencies),
list_or_none(&report.dependencies.maintenance_risks),
list_or_none(&report.dependencies.outdated_indicators)
));
out.push_str("## Code Quality\n\n");
out.push_str(&format!(
"- Lines of Rust code: {}\n- Rust modules: {}\n- Function count: {}\n- Average function size: {:.1} lines\n- Large modules: {}\n- Potential God modules: {}\n\n",
report.code_quality.lines_of_code,
report.code_quality.module_count,
report.code_quality.function_count,
report.code_quality.average_function_size,
list_or_none(&report.code_quality.large_modules),
list_or_none(&report.code_quality.god_module_candidates)
));
out.push_str("## Testing\n\n");
out.push_str(&format!(
"- {}\n- Unit test files: {}\n- Integration test files: {}\n- Test functions: {}\n\n",
report.testing.testing_structure,
report.testing.unit_test_files,
report.testing.integration_test_files,
report.testing.test_function_count
));
out.push_str("## Risks\n\n");
if report.risks.findings.is_empty() {
out.push_str(
"No material engineering risk findings were generated by the current rule set.\n\n",
);
} else {
for finding in &report.risks.findings {
push_finding(&mut out, finding);
}
}
out.push_str("## Recommendations\n\n");
out.push_str(recommendations(report).as_str());
out.push('\n');
out
}
fn push_finding(out: &mut String, finding: &RiskFinding) {
out.push_str(&format!(
"### {} ({})\n\nEvidence: {}\n\nRecommendation: {}\n\n",
finding.title,
finding.severity.as_str(),
finding.evidence,
finding.recommendation
));
}
fn recommendations(report: &AuditReport) -> String {
let mut recommendations = Vec::new();
if report.testing.score < 70 {
recommendations.push("Increase automated test coverage around critical business flows.");
}
if report.architecture.score < 75 {
recommendations.push("Document intended architectural boundaries and enforce them through crate or module ownership.");
}
if report.dependencies.score < 80 {
recommendations.push("Create a dependency ownership and upgrade policy.");
}
if report.code_quality.score < 80 {
recommendations
.push("Prioritize decomposition of large modules and high-branching functions.");
}
if recommendations.is_empty() {
recommendations.push(
"Maintain current engineering practices and use periodic audits to watch trend lines.",
);
}
recommendations
.into_iter()
.map(|item| format!("- {item}"))
.collect::<Vec<_>>()
.join("\n")
}
fn list_or_none(values: &[String]) -> String {
if values.is_empty() {
"none detected".to_string()
} else {
values.join(", ")
}
}