repopilot 0.11.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
use crate::cli::CompareOutputFormatArg;
use repopilot::compare::diff::diff_summaries;
use repopilot::compare::render::render;
use repopilot::report::writer::write_report;
use repopilot::scan::types::ScanSummary;
use std::error::Error;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

pub fn run(
    before: PathBuf,
    after: PathBuf,
    format: CompareOutputFormatArg,
    output: Option<PathBuf>,
) -> Result<(), Box<dyn std::error::Error>> {
    let before_summary = read_summary(&before)?;
    let after_summary = read_summary(&after)?;

    let diff = diff_summaries(&before_summary, &after_summary);
    let rendered = render(&diff, format.into())?;

    write_report(&rendered, output.as_deref())?;

    Ok(())
}

fn read_summary(path: &Path) -> Result<ScanSummary, CompareInputError> {
    let content = fs::read_to_string(path).map_err(|source| CompareInputError::Read {
        path: path.to_path_buf(),
        source,
    })?;

    serde_json::from_str(&content).map_err(|source| CompareInputError::Parse {
        path: path.to_path_buf(),
        source,
    })
}

#[derive(Debug)]
enum CompareInputError {
    Read {
        path: PathBuf,
        source: io::Error,
    },
    Parse {
        path: PathBuf,
        source: serde_json::Error,
    },
}

impl std::fmt::Display for CompareInputError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Read { path, .. } => {
                write!(f, "Failed to read scan summary {}", path.display())
            }
            Self::Parse { path, .. } => {
                write!(f, "Failed to parse scan summary {}", path.display())
            }
        }
    }
}

impl Error for CompareInputError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            Self::Read { source, .. } => Some(source),
            Self::Parse { source, .. } => Some(source),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::read_summary;
    use repopilot::output::json;
    use repopilot::scan::types::ScanSummary;
    use std::fs;
    use std::path::PathBuf;
    use tempfile::tempdir;

    #[test]
    fn read_summary_accepts_versioned_json_report() {
        let summary = ScanSummary {
            root_path: PathBuf::from("."),
            files_count: 7,
            lines_of_code: 42,
            ..ScanSummary::default()
        };
        let rendered = json::render(&summary).expect("json render should succeed");
        let dir = tempdir().expect("tempdir should be created");
        let path = dir.path().join("report.json");
        fs::write(&path, rendered).expect("report should be written");

        let parsed = read_summary(&path).expect("versioned report should parse");

        assert_eq!(parsed.files_count, 7);
        assert_eq!(parsed.lines_of_code, 42);
    }

    #[test]
    fn read_summary_still_accepts_legacy_scan_summary_json() {
        let summary = ScanSummary {
            root_path: PathBuf::from("."),
            files_count: 3,
            lines_of_code: 21,
            ..ScanSummary::default()
        };
        let rendered =
            serde_json::to_string_pretty(&summary).expect("legacy json render should succeed");
        let dir = tempdir().expect("tempdir should be created");
        let path = dir.path().join("legacy-report.json");
        fs::write(&path, rendered).expect("report should be written");

        let parsed = read_summary(&path).expect("legacy report should parse");

        assert_eq!(parsed.files_count, 3);
        assert_eq!(parsed.lines_of_code, 21);
    }
}