repopilot 0.20.0

Local-first CLI for reviewing Git changes, security boundaries, and blast radius before merge.
Documentation
use super::super::collection;
use super::{ChangedDiscoveryStage, ChangedRepoContextStage, ChangedScanEngine};
use crate::findings::types::Finding;
use crate::frameworks::{
    DetectedFramework, detect_framework_projects, detect_frameworks,
    detect_react_native_architecture,
};
use crate::graph::context::{
    RepoContextGraph, context_graph_cache_miss, load_repo_context_graph, write_repo_context_graph,
};
use crate::graph::{CouplingGraph, build_coupling_graph};
use crate::risk::{apply_cluster_overlay, apply_graph_overlay, assess_findings};
use crate::scan::cache::{config_fingerprint, relative_cache_path};
use crate::scan::facts::{FileFacts, ScanFacts};
use crate::scan::parsed_cache::ParsedFactsCache;
use crate::scan::types::cache_diagnostic;
use std::collections::{BTreeMap, BTreeSet};
use std::io;
use std::path::{Path, PathBuf};
use std::time::Instant;

impl<'a> ChangedScanEngine<'a> {
    pub(super) fn run_repo_context(
        &self,
        discovery: &ChangedDiscoveryStage,
        facts: &mut ScanFacts,
        graph_patch_files: &[FileFacts],
        parsed_cache: &mut ParsedFactsCache,
    ) -> io::Result<ChangedRepoContextStage> {
        let start = Instant::now();
        let mut diagnostics = Vec::new();
        let repo_root = &discovery.repo_root;
        let fingerprint = config_fingerprint(self.config);

        if let Some(mut load) = load_repo_context_graph(repo_root, &fingerprint) {
            load.graph
                .apply_changed_facts(repo_root, &discovery.changed_files, graph_patch_files);
            if let Err(error) = write_repo_context_graph(repo_root, &fingerprint, &load.graph) {
                diagnostics.push(cache_diagnostic(&error));
            }

            let repo_context = load.graph.to_scan_facts();
            let coupling_graph = load.graph.coupling_graph();

            facts.detected_frameworks = repo_context.detected_frameworks.clone();
            facts.framework_projects = repo_context.framework_projects.clone();
            facts.react_native = repo_context.react_native.clone();

            return Ok(ChangedRepoContextStage {
                repo_context,
                coupling_graph,
                context_graph: load.graph,
                cache_info: load.cache_info,
                diagnostics,
                elapsed_us: start.elapsed().as_micros() as u64,
            });
        }

        let mut repo_context = collection::collect_scan_facts_without_content_with_parsed_cache(
            repo_root,
            self.config,
            parsed_cache,
        )?;
        parsed_cache.retain_referenced_current_scan();

        repo_context.detected_frameworks = detect_frameworks(repo_root);
        repo_context.framework_projects = detect_framework_projects(repo_root);
        repo_context.react_native = detect_react_native_profile(&repo_context);

        let coupling_graph =
            relative_coupling_graph(build_coupling_graph(&repo_context, repo_root), repo_root);
        let context_graph =
            RepoContextGraph::from_scan_facts(&repo_context, repo_root, coupling_graph.clone());
        let mut cache_info =
            context_graph_cache_miss(repo_root, "missing-or-invalid-context-graph-cache");
        match write_repo_context_graph(repo_root, &fingerprint, &context_graph) {
            Ok(_) => {
                cache_info.reason.push_str("; cache-updated");
            }
            Err(error) => diagnostics.push(cache_diagnostic(&error)),
        }

        facts.detected_frameworks = repo_context.detected_frameworks.clone();
        facts.framework_projects = repo_context.framework_projects.clone();
        facts.react_native = repo_context.react_native.clone();

        Ok(ChangedRepoContextStage {
            repo_context,
            coupling_graph,
            context_graph,
            cache_info,
            diagnostics,
            elapsed_us: start.elapsed().as_micros() as u64,
        })
    }

    pub(super) fn score_findings(
        &self,
        repo_stage: &ChangedRepoContextStage,
        findings: &mut [Finding],
    ) -> u64 {
        let start = Instant::now();
        assess_findings(findings, &repo_stage.repo_context);
        apply_graph_overlay(findings, &repo_stage.coupling_graph);
        apply_cluster_overlay(findings);
        start.elapsed().as_micros() as u64
    }
}

fn detect_react_native_profile(
    facts: &ScanFacts,
) -> Option<crate::frameworks::ReactNativeArchitectureProfile> {
    if facts
        .detected_frameworks
        .iter()
        .any(|framework| matches!(framework, DetectedFramework::ReactNative { .. }))
    {
        let profile = detect_react_native_architecture(&facts.root_path);
        if profile.detected {
            return Some(profile);
        }
    }
    None
}

fn relative_coupling_graph(graph: CouplingGraph, repo_root: &Path) -> CouplingGraph {
    let relativize_edges = |edges: BTreeMap<PathBuf, BTreeSet<PathBuf>>| {
        edges
            .into_iter()
            .map(|(source, targets)| {
                (
                    PathBuf::from(relative_cache_path(repo_root, &source)),
                    targets
                        .into_iter()
                        .map(|target| PathBuf::from(relative_cache_path(repo_root, &target)))
                        .collect(),
                )
            })
            .collect()
    };

    CouplingGraph {
        edges: relativize_edges(graph.edges),
        deferred_edges: relativize_edges(graph.deferred_edges),
        nodes: graph
            .nodes
            .into_iter()
            .map(|node| PathBuf::from(relative_cache_path(repo_root, &node)))
            .collect(),
    }
}