repopilot 0.17.0

Local-first CLI for reviewing Git changes, security boundaries, and blast radius before merge.
Documentation
use super::changed_git::collect_changed_scope;
use crate::audits::architecture::import_coupling::ImportCouplingAudit;
use crate::audits::pipeline::{run_framework_audits, run_project_audits};
use crate::findings::enrichment::enrich_findings_timed;
use crate::findings::quality::summarize_signal_quality_with_contract_violations;
use crate::findings::rule_config::{apply_rule_config, rule_config_diagnostics};
use crate::knowledge::decision::apply_project_decisions;
use crate::review::diff::ChangedFile;
use crate::scan::config::ScanConfig;
use crate::scan::types::ScanSummary;
use std::io;
use std::path::{Path, PathBuf};
use std::time::Instant;

pub fn scan_changed_with_config(
    path: &Path,
    config: &ScanConfig,
    base_ref: Option<&str>,
) -> io::Result<ScanSummary> {
    ChangedScanEngine::new(path, config, base_ref).run()
}

pub fn scan_resolved_changed_with_config(
    path: &Path,
    config: &ScanConfig,
    repo_root: PathBuf,
    changed_files: Vec<ChangedFile>,
    base_ref: Option<&str>,
) -> io::Result<ScanSummary> {
    ChangedScanEngine::resolved(path, config, repo_root, changed_files, base_ref).run()
}

struct ChangedScanEngine<'a> {
    path: &'a Path,
    config: &'a ScanConfig,
    base_ref: Option<&'a str>,
    resolved_scope: Option<ChangedDiscoveryStage>,
}

impl<'a> ChangedScanEngine<'a> {
    fn new(path: &'a Path, config: &'a ScanConfig, base_ref: Option<&'a str>) -> Self {
        Self {
            path,
            config,
            base_ref,
            resolved_scope: None,
        }
    }

    fn resolved(
        path: &'a Path,
        config: &'a ScanConfig,
        repo_root: PathBuf,
        changed_files: Vec<ChangedFile>,
        base_ref: Option<&'a str>,
    ) -> Self {
        Self {
            path,
            config,
            base_ref,
            resolved_scope: Some(ChangedDiscoveryStage {
                repo_root,
                changed_files,
                elapsed_us: 0,
            }),
        }
    }

    fn run(mut self) -> io::Result<ScanSummary> {
        let start = Instant::now();
        let discovery = match self.resolved_scope.take() {
            Some(discovery) => discovery,
            None => self.run_discovery()?,
        };
        if discovery.changed_files.is_empty() {
            return self.finalize_empty_changed(start, discovery);
        }
        let mut file_stage = self.run_file_analysis(&discovery)?;
        let repo_stage = self.run_repo_context(
            &discovery,
            &mut file_stage.facts,
            &file_stage.graph_patch_files,
        )?;
        let project_start = Instant::now();
        let ((project_findings, framework_findings), (coupling_findings, _, _)) = rayon::join(
            || {
                rayon::join(
                    || run_project_audits(&repo_stage.repo_context, self.config),
                    || run_framework_audits(&repo_stage.repo_context, self.config),
                )
            },
            || {
                ImportCouplingAudit.audit_with_graph(
                    &repo_stage.repo_context,
                    self.config,
                    &discovery.repo_root,
                )
            },
        );
        file_stage.findings.extend(project_findings);
        file_stage.findings.extend(framework_findings);
        file_stage.findings.extend(apply_project_decisions(
            &repo_stage.repo_context,
            coupling_findings,
        ));
        let post_scan_audits_us = project_start.elapsed().as_micros() as u64;
        let enrichment_us = enrich_findings_timed(&mut file_stage.findings, &discovery.repo_root);
        apply_rule_config(&mut file_stage.findings, self.config);
        let risk_scoring_us = self.score_findings(&repo_stage, &mut file_stage.findings);
        let contract_stage =
            super::contract_stage::validate_finding_contract_stage(&file_stage.findings);
        let signal_quality = summarize_signal_quality_with_contract_violations(
            &file_stage.findings,
            contract_stage.report.violations.len(),
        );
        let mut diagnostics = contract_stage.diagnostics;
        diagnostics.extend(rule_config_diagnostics(self.config));
        let finding_pipeline = ChangedFindingPipelineStage {
            post_scan_audits_us,
            enrichment_us,
            risk_scoring_us,
            contract_validation_us: contract_stage.elapsed_us,
            diagnostics,
            signal_quality,
        };

        self.finalize_report(start, discovery, file_stage, repo_stage, finding_pipeline)
    }

    fn run_discovery(&self) -> io::Result<ChangedDiscoveryStage> {
        let start = Instant::now();
        let changed_scope = collect_changed_scope(self.path, self.base_ref)?;
        Ok(ChangedDiscoveryStage {
            repo_root: changed_scope.repo_root,
            changed_files: changed_scope.changed_files,
            elapsed_us: start.elapsed().as_micros() as u64,
        })
    }
}

mod file_analysis;
mod finalize;
mod repo_context;
mod stages;
use stages::*;