repopilot 0.12.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
use crate::findings::types::{Confidence, Finding, Severity};
use crate::risk::RiskPriority;
use crate::scan::types::ScanSummary;

#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct FindingFilter {
    pub min_severity: Option<Severity>,
    pub min_confidence: Option<Confidence>,
    pub min_priority: Option<RiskPriority>,
    pub rule_ids: Vec<String>,
}

impl FindingFilter {
    pub fn is_empty(&self) -> bool {
        self.min_severity.is_none()
            && self.min_confidence.is_none()
            && self.min_priority.is_none()
            && self.rule_ids.is_empty()
    }

    pub fn matches(&self, finding: &Finding) -> bool {
        if let Some(min) = self.min_severity
            && !finding.severity.is_at_least(&min)
        {
            return false;
        }

        if let Some(min) = self.min_confidence
            && finding.confidence < min
        {
            return false;
        }

        if let Some(min) = self.min_priority
            && !finding.risk.priority.is_at_least(min)
        {
            return false;
        }

        if !self.rule_ids.is_empty()
            && !self
                .rule_ids
                .iter()
                .any(|rule_id| rule_id == &finding.rule_id)
        {
            return false;
        }

        true
    }

    pub fn apply_to_summary(&self, summary: &mut ScanSummary) {
        if !self.is_empty() {
            summary.findings.retain(|finding| self.matches(finding));
        }
        recompute_summary_metrics(summary);
    }
}

pub fn recompute_summary_metrics(summary: &mut ScanSummary) {
    summary.visible_findings_count = summary.findings.len();
    summary.health_score =
        ScanSummary::compute_health_score(&summary.findings, summary.non_empty_lines);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::risk::RiskAssessment;
    use std::path::PathBuf;

    fn finding(
        rule_id: &str,
        severity: Severity,
        confidence: Confidence,
        priority: RiskPriority,
    ) -> Finding {
        Finding {
            rule_id: rule_id.to_string(),
            severity,
            confidence,
            risk: RiskAssessment {
                priority,
                ..RiskAssessment::default()
            },
            ..Finding::default()
        }
    }

    #[test]
    fn severity_threshold_matches_at_or_above_minimum() {
        let filter = FindingFilter {
            min_severity: Some(Severity::High),
            ..FindingFilter::default()
        };

        assert!(filter.matches(&finding(
            "rule.high",
            Severity::High,
            Confidence::Medium,
            RiskPriority::P3
        )));
        assert!(!filter.matches(&finding(
            "rule.medium",
            Severity::Medium,
            Confidence::Medium,
            RiskPriority::P3
        )));
    }

    #[test]
    fn confidence_threshold_matches_at_or_above_minimum() {
        let filter = FindingFilter {
            min_confidence: Some(Confidence::Medium),
            ..FindingFilter::default()
        };

        assert!(filter.matches(&finding(
            "rule.medium",
            Severity::Low,
            Confidence::Medium,
            RiskPriority::P3
        )));
        assert!(!filter.matches(&finding(
            "rule.low",
            Severity::Low,
            Confidence::Low,
            RiskPriority::P3
        )));
    }

    #[test]
    fn priority_threshold_matches_at_or_above_minimum() {
        let filter = FindingFilter {
            min_priority: Some(RiskPriority::P1),
            ..FindingFilter::default()
        };

        assert!(filter.matches(&finding(
            "rule.p1",
            Severity::Low,
            Confidence::Medium,
            RiskPriority::P1
        )));
        assert!(!filter.matches(&finding(
            "rule.p2",
            Severity::Low,
            Confidence::Medium,
            RiskPriority::P2
        )));
    }

    #[test]
    fn repeated_rule_filter_matches_any_rule_id() {
        let filter = FindingFilter {
            rule_ids: vec!["rule.one".to_string(), "rule.two".to_string()],
            ..FindingFilter::default()
        };

        assert!(filter.matches(&finding(
            "rule.two",
            Severity::Low,
            Confidence::Medium,
            RiskPriority::P3
        )));
        assert!(!filter.matches(&finding(
            "rule.three",
            Severity::Low,
            Confidence::Medium,
            RiskPriority::P3
        )));
    }

    #[test]
    fn combined_filter_requires_every_selected_threshold() {
        let filter = FindingFilter {
            min_severity: Some(Severity::Medium),
            min_confidence: Some(Confidence::High),
            min_priority: Some(RiskPriority::P2),
            rule_ids: vec!["rule.keep".to_string()],
        };

        assert!(filter.matches(&finding(
            "rule.keep",
            Severity::High,
            Confidence::High,
            RiskPriority::P2
        )));
        assert!(!filter.matches(&finding(
            "rule.keep",
            Severity::High,
            Confidence::Medium,
            RiskPriority::P2
        )));
        assert!(!filter.matches(&finding(
            "rule.drop",
            Severity::High,
            Confidence::High,
            RiskPriority::P2
        )));
    }

    #[test]
    fn apply_to_summary_recomputes_visible_count_and_health_score() {
        let filter = FindingFilter {
            min_severity: Some(Severity::High),
            ..FindingFilter::default()
        };
        let mut summary = ScanSummary {
            root_path: PathBuf::from("."),
            non_empty_lines: 1_000,
            findings: vec![
                finding(
                    "rule.keep",
                    Severity::High,
                    Confidence::Medium,
                    RiskPriority::P3,
                ),
                finding(
                    "rule.drop",
                    Severity::Low,
                    Confidence::Medium,
                    RiskPriority::P3,
                ),
            ],
            ..ScanSummary::default()
        };

        filter.apply_to_summary(&mut summary);

        assert_eq!(summary.findings.len(), 1);
        assert_eq!(summary.visible_findings_count, 1);
        assert_eq!(summary.health_score, 95);
    }
}