repopilot 0.19.0

Local-first CLI for reviewing Git changes, security boundaries, and blast radius before merge.
Documentation
mod algorithms;
mod builder;
mod capabilities;
mod coupling_snapshot;
mod diagnostic;
mod edge;
mod node;
mod snapshot;

pub use algorithms::{
    DirectoryDependency, GraphBlastRadius, GraphCycle, GraphDegreeSummary,
    GraphDirectoryDependencies, GraphNeighborhood, GraphV2Summary, NodeDegree, blast_radius,
    compute_degrees, direct_dependents, directory_dependencies, find_cycles, neighborhood,
    shortest_cycle, summarize_graph, top_fan_in, top_fan_out,
};
pub use builder::graph_snapshot_from_scan;
pub use capabilities::{GraphCapabilities, graph_capabilities};
pub use coupling_snapshot::build_coupling_graph_snapshot;
pub use diagnostic::GraphDiagnostic;
pub use edge::{GraphEdge, GraphEdgeConfidence, GraphEdgeKind, GraphEdgeProvenance};
pub use node::{GraphNode, GraphNodeId, GraphNodeKind};
pub use snapshot::GraphSnapshot;

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

    #[test]
    fn node_id_stores_value_and_sorts_deterministically() {
        let first = GraphNodeId::new("file:src/a.rs");
        let second = GraphNodeId::new("file:src/b.rs");
        let mut ids = [second, first.clone()];

        ids.sort();

        assert_eq!(first.as_str(), "file:src/a.rs");
        assert_eq!(ids[0], first);
    }

    #[test]
    fn new_snapshot_is_empty() {
        let snapshot = GraphSnapshot::new();

        assert_eq!(snapshot.node_count(), 0);
        assert_eq!(snapshot.edge_count(), 0);
        assert_eq!(snapshot.diagnostic_count(), 0);
    }

    #[test]
    fn snapshot_adds_nodes_edges_and_diagnostics() {
        let source_id = GraphNodeId::new("file:src/lib.rs");
        let target_id = GraphNodeId::new("external:serde");
        let mut snapshot = GraphSnapshot::new();

        snapshot.add_node(GraphNode {
            id: source_id.clone(),
            kind: GraphNodeKind::File,
            label: "src/lib.rs".to_string(),
            path: Some(PathBuf::from("src/lib.rs")),
        });
        snapshot.add_node(GraphNode {
            id: target_id.clone(),
            kind: GraphNodeKind::ExternalDependency,
            label: "serde".to_string(),
            path: None,
        });
        snapshot.add_edge(GraphEdge {
            from: source_id.clone(),
            to: target_id.clone(),
            kind: GraphEdgeKind::DependsOn,
            provenance: GraphEdgeProvenance::Import,
            confidence: GraphEdgeConfidence::High,
        });
        snapshot.add_diagnostic(GraphDiagnostic {
            code: "graph.unresolved-import".to_string(),
            message: "could not resolve one import".to_string(),
            path: Some(PathBuf::from("src/lib.rs")),
        });

        assert_eq!(snapshot.node_count(), 2);
        assert_eq!(snapshot.edge_count(), 1);
        assert_eq!(snapshot.diagnostic_count(), 1);
        assert_eq!(
            snapshot.nodes[0].path.as_deref(),
            Some(std::path::Path::new("src/lib.rs"))
        );
        assert!(snapshot.nodes[1].path.is_none());
        assert_eq!(snapshot.edges[0].from, source_id);
        assert_eq!(snapshot.edges[0].to, target_id);
        assert_eq!(snapshot.edges[0].kind, GraphEdgeKind::DependsOn);
    }
}