repopilot 0.11.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
use crate::baseline::diff::BaselineStatus;
use crate::baseline::gate::CiGateResult;
use crate::findings::types::Finding;
use crate::review::diff::ChangedFile;
use crate::review::model::{ReviewReport, SeverityCounts};
use serde::Serialize;

pub fn render_json(
    report: &ReviewReport,
    ci_gate: Option<&CiGateResult>,
) -> Result<String, serde_json::Error> {
    let findings = report
        .summary
        .findings
        .iter()
        .enumerate()
        .map(|(index, finding)| ReviewJsonFinding {
            finding,
            in_diff: report
                .finding_status(index)
                .map(|status| status.in_diff)
                .unwrap_or(false),
            baseline_status: report
                .finding_status(index)
                .and_then(|status| status.baseline_status),
        })
        .collect::<Vec<_>>();

    let output = ReviewJsonReport {
        root_path: report.summary.root_path.to_string_lossy().to_string(),
        git_root: report.repo_root.to_string_lossy().to_string(),
        files_count: report.summary.files_count,
        directories_count: report.summary.directories_count,
        lines_of_code: report.summary.lines_of_code,
        changed_files: &report.changed_files,
        blast_radius: report
            .blast_radius
            .iter()
            .map(|path| path.to_string_lossy().to_string())
            .collect(),
        review: ReviewJsonMetadata {
            in_diff_findings: report.in_diff_count(),
            out_of_diff_findings: report.out_of_diff_count(),
            new_in_diff_findings: report.new_in_diff_count(),
            existing_in_diff_findings: report.existing_in_diff_count(),
            severity_counts: report.severity_counts(),
        },
        baseline: ReviewBaselineJsonMetadata {
            path: report
                .baseline_path
                .as_ref()
                .map(|path| path.to_string_lossy().to_string()),
        },
        ci_gate: ci_gate.map(ReviewCiGateJsonMetadata::from),
        findings,
    };

    serde_json::to_string_pretty(&output)
}

#[derive(Serialize)]
struct ReviewJsonReport<'a> {
    root_path: String,
    git_root: String,
    files_count: usize,
    directories_count: usize,
    lines_of_code: usize,
    changed_files: &'a [ChangedFile],
    blast_radius: Vec<String>,
    review: ReviewJsonMetadata,
    baseline: ReviewBaselineJsonMetadata,
    #[serde(skip_serializing_if = "Option::is_none")]
    ci_gate: Option<ReviewCiGateJsonMetadata>,
    findings: Vec<ReviewJsonFinding<'a>>,
}

#[derive(Serialize)]
struct ReviewJsonMetadata {
    in_diff_findings: usize,
    out_of_diff_findings: usize,
    new_in_diff_findings: usize,
    existing_in_diff_findings: usize,
    severity_counts: SeverityCounts,
}

#[derive(Serialize)]
struct ReviewBaselineJsonMetadata {
    path: Option<String>,
}

#[derive(Serialize)]
struct ReviewCiGateJsonMetadata {
    fail_on: String,
    status: &'static str,
    failed_findings: usize,
}

impl From<&CiGateResult> for ReviewCiGateJsonMetadata {
    fn from(result: &CiGateResult) -> Self {
        Self {
            fail_on: result.label(),
            status: if result.passed() { "passed" } else { "failed" },
            failed_findings: result.failed_findings,
        }
    }
}

#[derive(Serialize)]
struct ReviewJsonFinding<'a> {
    #[serde(flatten)]
    finding: &'a Finding,
    in_diff: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    baseline_status: Option<BaselineStatus>,
}