repopilot 0.3.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
use crate::baseline::diff::{BaselineScanReport, BaselineStatus};
use crate::baseline::gate::CiGateResult;
use crate::findings::types::Finding;
use crate::scan::types::ScanSummary;

pub fn render(summary: &ScanSummary) -> String {
    let mut output = String::new();

    output.push_str("RepoPilot Scan\n");
    output.push_str(&format!("Path: {}\n\n", summary.root_path.display()));

    output.push_str(&format!("Files analyzed: {}\n", summary.files_count));
    output.push_str(&format!(
        "Directories analyzed: {}\n",
        summary.directories_count
    ));
    output.push_str(&format!("Lines of code: {}\n\n", summary.lines_of_code));

    output.push_str("Languages:\n");

    if summary.languages.is_empty() {
        output.push_str("  No languages detected\n");
    } else {
        for language in &summary.languages {
            output.push_str(&format!(
                "  {}: {} files\n",
                language.name, language.files_count
            ));
        }
    }

    output.push_str("\nFindings:\n");

    if summary.findings.is_empty() {
        output.push_str("  No findings found\n");
    } else {
        for finding in &summary.findings {
            output.push_str(&format!(
                "  [{}] {}{}\n",
                finding.severity_label(),
                finding.rule_id,
                finding.title
            ));

            for evidence in &finding.evidence {
                output.push_str(&format!(
                    "    Evidence: {}:{}{}\n",
                    evidence.path.display(),
                    evidence.line_start,
                    evidence.snippet.trim()
                ));
            }
        }
    }

    output
}

pub fn render_with_baseline(report: &BaselineScanReport, ci_gate: Option<&CiGateResult>) -> String {
    let summary = &report.summary;
    let mut output = String::new();

    output.push_str("RepoPilot Scan\n");
    output.push_str(&format!("Path: {}\n", summary.root_path.display()));
    match &report.baseline_path {
        Some(path) => output.push_str(&format!("Baseline: {}\n", path.display())),
        None => output.push_str("Baseline: none (all findings treated as new)\n"),
    }
    output.push('\n');

    output.push_str(&format!("Files analyzed: {}\n", summary.files_count));
    output.push_str(&format!(
        "Directories analyzed: {}\n",
        summary.directories_count
    ));
    output.push_str(&format!("Lines of code: {}\n\n", summary.lines_of_code));

    output.push_str(&format!("New findings: {}\n", report.new_count()));
    output.push_str(&format!("Existing findings: {}\n", report.existing_count()));

    if let Some(ci_gate) = ci_gate {
        let status = if ci_gate.passed() { "passed" } else { "failed" };
        output.push_str(&format!("CI gate: {status} ({})\n", ci_gate.label()));
    }

    output.push_str("\nLanguages:\n");

    if summary.languages.is_empty() {
        output.push_str("  No languages detected\n");
    } else {
        for language in &summary.languages {
            output.push_str(&format!(
                "  {}: {} files\n",
                language.name, language.files_count
            ));
        }
    }

    output.push_str("\nFindings:\n");

    if summary.findings.is_empty() {
        output.push_str("  No findings found\n");
        return output;
    }

    render_findings_group(
        &mut output,
        "New findings",
        &report.findings_with_status(BaselineStatus::New),
    );
    render_findings_group(
        &mut output,
        "Existing findings",
        &report.findings_with_status(BaselineStatus::Existing),
    );

    output
}

fn render_findings_group(output: &mut String, label: &str, findings: &[&Finding]) {
    output.push_str(&format!("  {label}: {}\n", findings.len()));

    for finding in findings {
        output.push_str(&format!(
            "    [{}] {}{}\n",
            finding.severity_label(),
            finding.rule_id,
            finding.title
        ));

        for evidence in &finding.evidence {
            output.push_str(&format!(
                "      Evidence: {}:{}{}\n",
                evidence.path.display(),
                evidence.line_start,
                evidence.snippet.trim()
            ));
        }
    }
}