Skip to main content

lean_ctx/core/graph_analysis/
mod.rs

1//! Static dependency-graph analyses (God-Nodes, import cycles).
2//!
3//! These run on the *real* directed dependency edges — `import` and `reexport` —
4//! and deliberately exclude the co-location heuristics (`sibling`, `cochange`)
5//! and the ambiguous `module` edges. That way the results reflect genuine code
6//! dependencies (graphify-style) instead of directory layout coincidences.
7
8mod centrality;
9mod cycles;
10mod god_nodes;
11mod surprising;
12
13pub use centrality::{
14    compute_bridge_centrality, compute_bridge_nodes, BridgeCentrality, BridgeNode,
15};
16pub use cycles::{find_import_cycles, ImportCycle};
17pub use god_nodes::{compute_god_nodes, GodNode};
18pub use surprising::{find_surprising_connections, SurprisingConnection};
19
20use crate::core::graph_provider::EdgeInfo;
21
22/// Edge kinds that represent a genuine *directed* code dependency
23/// (`from` depends on `to`).
24pub const DEP_EDGE_KINDS: [&str; 2] = ["import", "reexport"];
25
26/// True when an edge kind is a genuine directed dependency.
27pub fn is_dependency_kind(kind: &str) -> bool {
28    DEP_EDGE_KINDS.contains(&kind)
29}
30
31/// Directed dependency edges as `(from, to)` pairs, with self-loops removed.
32/// Borrows from `edges` to avoid allocating new strings.
33pub fn dependency_edges(edges: &[EdgeInfo]) -> Vec<(&str, &str)> {
34    edges
35        .iter()
36        .filter(|e| is_dependency_kind(&e.kind))
37        .filter(|e| e.from != e.to)
38        .map(|e| (e.from.as_str(), e.to.as_str()))
39        .collect()
40}
41
42/// Confidence that an edge represents a *real* relationship, in `0.0..=1.0`.
43///
44/// Explicit code references (`import`/`reexport`) are certain; structural
45/// `module` grouping is fairly reliable; the co-location/co-change heuristics
46/// are weaker and scale with their observed `weight` (e.g. how often two files
47/// changed together). The dashboard styles edges by this score so heuristic
48/// links read as faint/dashed while real dependencies stay solid (#273).
49pub fn edge_confidence(kind: &str, weight: f64) -> f64 {
50    match kind {
51        "import" => 1.0,
52        "reexport" => 0.95,
53        "module" => 0.6,
54        // More co-changes ⇒ more confidence, with diminishing returns.
55        "cochange" => (0.30 + weight.max(0.0).ln_1p() * 0.18).clamp(0.30, 0.85),
56        // Learned co-access (files opened together in real sessions, #289): a
57        // behavioural signal that strengthens with repeated reinforcement.
58        "co_access" => (0.35 + weight.max(0.0).ln_1p() * 0.16).clamp(0.35, 0.80),
59        "sibling" => 0.25,
60        _ => 0.5,
61    }
62}
63
64#[cfg(test)]
65mod confidence_tests {
66    use super::edge_confidence;
67
68    #[test]
69    fn explicit_refs_rank_above_heuristics() {
70        let import = edge_confidence("import", 0.0);
71        let reexport = edge_confidence("reexport", 0.0);
72        let module = edge_confidence("module", 0.0);
73        let sibling = edge_confidence("sibling", 0.0);
74        assert!(import >= reexport);
75        assert!(reexport > module);
76        assert!(module > sibling);
77        assert!((0.0..=1.0).contains(&sibling));
78    }
79
80    #[test]
81    fn cochange_scales_with_weight_and_is_bounded() {
82        let low = edge_confidence("cochange", 1.0);
83        let high = edge_confidence("cochange", 50.0);
84        assert!(high > low, "more co-changes should raise confidence");
85        assert!((0.30..=0.85).contains(&low));
86        assert!((0.30..=0.85).contains(&high));
87    }
88
89    #[test]
90    fn unknown_kind_is_neutral() {
91        assert_eq!(edge_confidence("mystery", 0.0), 0.5);
92    }
93
94    #[test]
95    fn co_access_scales_with_weight_and_is_bounded() {
96        let low = edge_confidence("co_access", 1.0);
97        let high = edge_confidence("co_access", 50.0);
98        assert!(high > low, "more reinforcement should raise confidence");
99        assert!((0.35..=0.80).contains(&low));
100        assert!((0.35..=0.80).contains(&high));
101        // Behavioural co-access sits above pure co-location (sibling) but below
102        // an explicit import.
103        assert!(low > edge_confidence("sibling", 0.0));
104        assert!(high < edge_confidence("import", 0.0));
105    }
106}