repopilot 0.9.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::findings::types::{Finding, Severity};
use serde::Serialize;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum FailOn {
    New(Severity),
    Any(Severity),
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct CiGateResult {
    pub fail_on: FailOn,
    pub failed_findings: usize,
}

impl CiGateResult {
    pub fn passed(&self) -> bool {
        self.failed_findings == 0
    }

    pub fn label(&self) -> String {
        match self.fail_on {
            FailOn::New(severity) => format!("new-{}", severity.lowercase_label()),
            FailOn::Any(severity) => severity.lowercase_label().to_string(),
        }
    }

    pub fn failure_message(&self) -> Option<String> {
        if self.passed() {
            return None;
        }

        let scope = match self.fail_on {
            FailOn::New(_) => "new findings",
            FailOn::Any(_) => "findings",
        };
        let severity = match self.fail_on {
            FailOn::New(severity) | FailOn::Any(severity) => severity.lowercase_label(),
        };

        Some(format!(
            "RepoPilot CI Gate failed\n\nReason:\nFound {} {scope} at or above {severity} severity.\n\nUse `repopilot baseline create . --force` only if these findings are accepted technical debt.",
            self.failed_findings
        ))
    }
}

pub fn evaluate_ci_gate(report: &BaselineScanReport, fail_on: FailOn) -> CiGateResult {
    let failed_findings = report
        .summary
        .findings
        .iter()
        .enumerate()
        .filter(|(index, finding)| finding_matches(report, *index, finding, fail_on))
        .count();

    CiGateResult {
        fail_on,
        failed_findings,
    }
}

fn finding_matches(
    report: &BaselineScanReport,
    index: usize,
    finding: &Finding,
    fail_on: FailOn,
) -> bool {
    match fail_on {
        FailOn::New(threshold) => {
            report.finding_status(index) == BaselineStatus::New
                && finding.severity.is_at_least(&threshold)
        }
        FailOn::Any(threshold) => finding.severity.is_at_least(&threshold),
    }
}