use crate::explain::model::{ExplainDecision, ExplainReport};
use crate::output::OutputFormat;
pub fn render_explain_report(
report: &ExplainReport,
format: OutputFormat,
) -> Result<String, serde_json::Error> {
match format {
OutputFormat::Console => Ok(render_console(report)),
OutputFormat::Markdown => Ok(render_markdown(report)),
OutputFormat::Json | OutputFormat::Html | OutputFormat::Sarif => {
serde_json::to_string_pretty(report)
}
}
}
fn render_console(report: &ExplainReport) -> String {
let mut output = String::new();
output.push_str("RepoPilot Explain\n\n");
output.push_str(&format!("File: {}\n", report.path));
output.push_str(&format!(
"Language: {}\n",
report.source.language_name.as_deref().unwrap_or("unknown")
));
output.push_str(&format!("Lines of code: {}\n", report.source.lines_of_code));
output.push_str(&format!(
"Inline tests: {}\n\n",
yes_no(report.source.has_inline_tests)
));
output.push_str("Audit context:\n");
output.push_str(&format!(" Language: {}\n", report.context.language));
output.push_str(&format!(
" Language support: {}\n",
report
.context
.language_support
.as_deref()
.unwrap_or("unknown")
));
output.push_str(&format!(
" Frameworks: {}\n",
comma_or_none(&report.context.frameworks)
));
output.push_str(&format!(
" Roles: {}\n",
comma_or_none(&report.context.roles)
));
output.push_str(&format!(
" Paradigms: {}\n",
comma_or_none(&report.context.paradigms)
));
output.push_str(&format!(
" Runtimes: {}\n",
comma_or_none(&report.context.runtimes)
));
output.push_str(&format!(" Test code: {}\n", yes_no(report.context.is_test)));
output.push_str(&format!(
" Production code: {}\n",
yes_no(report.context.is_production_code)
));
output.push_str("\nRule decision:\n");
match &report.decision {
Some(decision) => output.push_str(&render_console_decision(decision)),
None => output.push_str(" not requested; pass --rule <RULE_ID> to evaluate one\n"),
}
output
}
fn render_console_decision(decision: &ExplainDecision) -> String {
let mut output = String::new();
output.push_str(&format!(" Rule: {}\n", decision.rule_id));
output.push_str(&format!(
" Signal: {}\n",
decision.signal.as_deref().unwrap_or("none")
));
output.push_str(&format!(
" Base severity: {}\n",
decision.base_severity.label()
));
output.push_str(&format!(" Action: {}\n", decision.action));
output.push_str(&format!(
" Final severity: {}\n",
decision.final_severity.label()
));
if let Some(reason) = &decision.reason {
output.push_str(&format!(" Reason: {reason}\n"));
}
output
}
fn render_markdown(report: &ExplainReport) -> String {
let mut output = String::new();
output.push_str("# RepoPilot Explain\n\n");
output.push_str(&format!("- **File:** `{}`\n", report.path));
output.push_str(&format!(
"- **Language:** `{}`\n",
report.source.language_name.as_deref().unwrap_or("unknown")
));
output.push_str(&format!(
"- **Lines of code:** {}\n",
report.source.lines_of_code
));
output.push_str(&format!(
"- **Inline tests:** {}\n\n",
yes_no(report.source.has_inline_tests)
));
output.push_str("## Audit context\n\n");
output.push_str(&format!("- **Language:** `{}`\n", report.context.language));
output.push_str(&format!(
"- **Language support:** `{}`\n",
report
.context
.language_support
.as_deref()
.unwrap_or("unknown")
));
output.push_str(&format!(
"- **Frameworks:** {}\n",
markdown_list(&report.context.frameworks)
));
output.push_str(&format!(
"- **Roles:** {}\n",
markdown_list(&report.context.roles)
));
output.push_str(&format!(
"- **Paradigms:** {}\n",
markdown_list(&report.context.paradigms)
));
output.push_str(&format!(
"- **Runtimes:** {}\n",
markdown_list(&report.context.runtimes)
));
output.push_str(&format!(
"- **Test code:** {}\n",
yes_no(report.context.is_test)
));
output.push_str(&format!(
"- **Production code:** {}\n\n",
yes_no(report.context.is_production_code)
));
output.push_str("## Rule decision\n\n");
match &report.decision {
Some(decision) => {
output.push_str(&format!("- **Rule:** `{}`\n", decision.rule_id));
output.push_str(&format!(
"- **Signal:** `{}`\n",
decision.signal.as_deref().unwrap_or("none")
));
output.push_str(&format!(
"- **Base severity:** `{}`\n",
decision.base_severity.label()
));
output.push_str(&format!("- **Action:** `{}`\n", decision.action));
output.push_str(&format!(
"- **Final severity:** `{}`\n",
decision.final_severity.label()
));
if let Some(reason) = &decision.reason {
output.push_str(&format!("- **Reason:** {reason}\n"));
}
}
None => {
output.push_str("No rule was requested. Pass `--rule <RULE_ID>` to evaluate one.\n");
}
}
output
}
fn yes_no(value: bool) -> &'static str {
if value { "yes" } else { "no" }
}
fn comma_or_none(values: &[String]) -> String {
if values.is_empty() {
"none".to_string()
} else {
values.join(", ")
}
}
fn markdown_list(values: &[String]) -> String {
if values.is_empty() {
"none".to_string()
} else {
values
.iter()
.map(|value| format!("`{value}`"))
.collect::<Vec<_>>()
.join(", ")
}
}