fn generate_markdown_context(
toolchain: &str,
project_path: &Path,
context: &crate::services::deep_context::DeepContext,
) -> Result<String> {
let mut builder = MarkdownBuilder::new();
builder.content.push_str("# Project Context\n\n");
builder
.content
.push_str(&format!("**Language**: {}\n", toolchain));
builder
.content
.push_str(&format!("**Project Path**: {}\n\n", project_path.display()));
builder.content.push_str("## Project Structure\n\n");
let (total_files, total_functions) = count_emitted_body(context);
builder
.content
.push_str(&format!("- **Total Files**: {}\n", total_files));
builder
.content
.push_str(&format!("- **Total Functions**: {}\n", total_functions));
if let Some(complexity_report) = &context.analyses.complexity_report {
builder.content.push_str(&format!(
"- **Median Cyclomatic**: {:.2}\n",
complexity_report.summary.median_cyclomatic
));
builder.content.push_str(&format!(
"- **Median Cognitive**: {:.2}\n\n",
complexity_report.summary.median_cognitive
));
} else {
builder.content.push('\n');
}
builder.content.push_str("## Quality Scorecard\n\n");
use crate::services::deep_context::QualityScorecard;
let tdg_score = context
.quality_scorecard
.overall_health
.map(|h| h.clamp(0.0, 100.0));
builder.content.push_str(&format!(
"- **Overall Health**: {}\n",
QualityScorecard::render(tdg_score, "%")
));
builder.content.push_str(&format!(
"- **Maintainability Index**: {}\n",
QualityScorecard::render(context.quality_scorecard.maintainability_index, "")
));
builder.content.push_str(&format!(
"- **Complexity Score**: {}\n",
QualityScorecard::render(context.quality_scorecard.complexity_score, "")
));
if let Some(coverage) = context.quality_scorecard.test_coverage {
let normalized_coverage = coverage.min(100.0).max(0.0);
builder.content.push_str(&format!(
"- **Test Coverage**: {:.1}%\n",
normalized_coverage
));
} else {
builder.content.push_str("- **Test Coverage**: N/A\n");
}
builder.content.push('\n');
builder.content.push_str("## Files\n\n");
for enhanced_context in &context.analyses.ast_contexts {
add_simple_file_section(&mut builder, &enhanced_context.base, &context.analyses);
}
Ok(builder.content)
}
fn count_emitted_body(context: &crate::services::deep_context::DeepContext) -> (usize, usize) {
let files = context.analyses.ast_contexts.len();
let functions = context
.analyses
.ast_contexts
.iter()
.map(|c| count_listed_functions(&c.base))
.sum();
(files, functions)
}
fn count_listed_functions(file: &crate::services::context::FileContext) -> usize {
file.items
.iter()
.filter(|item| matches!(item, crate::services::context::AstItem::Function { .. }))
.count()
}
fn add_simple_file_section(
builder: &mut MarkdownBuilder,
file: &crate::services::context::FileContext,
analyses: &crate::services::deep_context::AnalysisResults,
) {
builder.content.push_str(&format!("### {}\n\n", file.path));
let file_metrics = find_file_metrics(file, analyses);
let function_count = count_listed_functions(file);
if let Some(file_metrics) = file_metrics {
builder.content.push_str(&format!(
"**File Complexity**: {} | **Functions**: {}\n\n",
file_metrics.total_complexity.cyclomatic, function_count
));
} else {
if function_count > 0 || !file.items.is_empty() {
builder.content.push_str(&format!(
"**File Complexity**: N/A | **Functions**: {}\n\n",
function_count
));
}
}
for item in &file.items {
builder.content.push_str(&format_ast_item_line(item, file, analyses));
}
builder.content.push('\n');
}
fn format_ast_item_line(
item: &crate::services::context::AstItem,
file: &crate::services::context::FileContext,
analyses: &crate::services::deep_context::AnalysisResults,
) -> String {
match item {
crate::services::context::AstItem::Function { name, .. } => {
let annotations = get_simple_function_annotations(name, file, analyses);
format!("- **Function**: `{}`{}\n", name, annotations)
}
crate::services::context::AstItem::Struct { name, fields_count, .. } => {
format!("- **Struct**: `{}` [fields: {}]\n", name, fields_count)
}
crate::services::context::AstItem::Trait { name, .. } => {
format!("- **Trait**: `{}`\n", name)
}
crate::services::context::AstItem::Enum { name, variants_count, .. } => {
format!("- **Enum**: `{}` [variants: {}]\n", name, variants_count)
}
crate::services::context::AstItem::Impl { type_name, trait_name, .. } => {
if let Some(trait_name) = trait_name {
format!("- **Impl**: `{}` for `{}`\n", trait_name, type_name)
} else {
format!("- **Impl**: `{}`\n", type_name)
}
}
_ => String::new(),
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod header_body_agreement_tests {
use super::*;
use crate::services::complexity::{ComplexityReport, ComplexitySummary};
use crate::services::context::{AstItem, FileContext};
use crate::services::deep_context::{DeepContext, DefectAnnotations, EnhancedFileContext};
fn file_with_functions(path: &str, names: &[&str]) -> EnhancedFileContext {
EnhancedFileContext {
base: FileContext {
path: path.to_string(),
language: "rust".to_string(),
items: names
.iter()
.map(|n| AstItem::Function {
name: (*n).to_string(),
visibility: "pub".to_string(),
is_async: false,
line: 1,
})
.collect(),
complexity_metrics: None,
},
complexity_metrics: None,
churn_metrics: None,
defects: DefectAnnotations {
dead_code: None,
technical_debt: vec![],
complexity_violations: vec![],
tdg_score: None,
},
symbol_id: path.to_string(),
}
}
#[test]
fn header_totals_match_the_body_they_head() {
let mut context = DeepContext::default();
context.analyses.ast_contexts = vec![
file_with_functions("src/a.rs", &["one", "two"]),
file_with_functions("src/b.rs", &["three"]),
];
context.analyses.complexity_report = Some(ComplexityReport {
summary: ComplexitySummary {
total_files: 7,
total_functions: 999,
..ComplexitySummary::default()
},
violations: vec![],
hotspots: vec![],
files: vec![],
});
let md =
generate_markdown_context("rust", std::path::Path::new("/repo"), &context).unwrap();
assert!(
md.contains("- **Total Files**: 2\n"),
"header must count the emitted file sections:\n{md}"
);
assert!(
md.contains("- **Total Functions**: 3\n"),
"header must count the emitted function entries:\n{md}"
);
assert!(
!md.contains("999"),
"header must not report a count from a different analysis pass:\n{md}"
);
let sections = md.matches("\n### ").count();
let functions = md.matches("- **Function**").count();
assert_eq!(sections, 2);
assert_eq!(functions, 3);
}
#[test]
fn json_header_totals_match_the_body_they_head() {
let mut context = DeepContext::default();
context.analyses.ast_contexts = vec![
file_with_functions("src/a.rs", &["one", "two"]),
file_with_functions("src/b.rs", &["three"]),
];
context.analyses.complexity_report = Some(ComplexityReport {
summary: ComplexitySummary {
total_files: 7,
total_functions: 999,
..ComplexitySummary::default()
},
violations: vec![],
hotspots: vec![],
files: vec![],
});
let json = generate_json_context("rust", std::path::Path::new("/repo"), &context).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).expect("parseable");
assert_eq!(parsed["project"]["total_files"], 2, "{json}");
assert_eq!(parsed["project"]["total_functions"], 3, "{json}");
assert_eq!(
parsed["files"].as_array().expect("files array").len(),
2,
"header must count the array it heads:\n{json}"
);
}
#[test]
fn markdown_and_json_headers_agree() {
let mut context = DeepContext::default();
context.analyses.ast_contexts = vec![
file_with_functions("src/a.rs", &["one", "two"]),
file_with_functions("src/b.rs", &["three"]),
file_with_functions("src/c.rs", &["four", "five", "six"]),
];
context.analyses.complexity_report = Some(ComplexityReport {
summary: ComplexitySummary {
total_files: 7,
total_functions: 999,
..ComplexitySummary::default()
},
violations: vec![],
hotspots: vec![],
files: vec![],
});
let path = std::path::Path::new("/repo");
let md = generate_markdown_context("rust", path, &context).unwrap();
let json = generate_json_context("rust", path, &context).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).expect("parseable");
assert!(md.contains("- **Total Files**: 3\n"), "{md}");
assert!(md.contains("- **Total Functions**: 6\n"), "{md}");
assert_eq!(parsed["project"]["total_files"], 3, "{json}");
assert_eq!(parsed["project"]["total_functions"], 6, "{json}");
}
}