lean_ctx/core/code_health/
coupling.rs1use serde::Serialize;
11use std::collections::{BTreeMap, BTreeSet};
12
13#[derive(Debug, Clone, PartialEq, Serialize)]
15pub struct ModuleCoupling {
16 pub module: String,
17 pub afferent: usize,
19 pub efferent: usize,
21 pub instability: f64,
23}
24
25pub fn module_coupling(edges: &[(String, String)]) -> Vec<ModuleCoupling> {
29 let mut unique: BTreeSet<(&str, &str)> = BTreeSet::new();
30 for (from, to) in edges {
31 if from != to {
32 unique.insert((from.as_str(), to.as_str()));
33 }
34 }
35
36 let mut efferent: BTreeMap<&str, usize> = BTreeMap::new();
37 let mut afferent: BTreeMap<&str, usize> = BTreeMap::new();
38 let mut modules: BTreeSet<&str> = BTreeSet::new();
39 for (from, to) in &unique {
40 *efferent.entry(from).or_default() += 1;
41 *afferent.entry(to).or_default() += 1;
42 modules.insert(from);
43 modules.insert(to);
44 }
45
46 modules
47 .into_iter()
48 .map(|m| {
49 let ce = efferent.get(m).copied().unwrap_or(0);
50 let ca = afferent.get(m).copied().unwrap_or(0);
51 let instability = if ca + ce == 0 {
52 0.0
53 } else {
54 ce as f64 / (ca + ce) as f64
55 };
56 ModuleCoupling {
57 module: m.to_string(),
58 afferent: ca,
59 efferent: ce,
60 instability,
61 }
62 })
63 .collect()
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 fn edge(a: &str, b: &str) -> (String, String) {
71 (a.to_string(), b.to_string())
72 }
73
74 #[test]
75 fn computes_ca_ce_and_instability() {
76 let edges = [edge("a", "b"), edge("a", "c"), edge("d", "a")];
78 let cps = module_coupling(&edges);
79 let a = cps.iter().find(|c| c.module == "a").unwrap();
80 assert_eq!(a.efferent, 2, "a depends on b and c");
81 assert_eq!(a.afferent, 1, "d depends on a");
82 assert!((a.instability - (2.0 / 3.0)).abs() < 1e-9);
83 }
84
85 #[test]
86 fn dedups_and_ignores_self_edges() {
87 let edges = [edge("a", "b"), edge("a", "b"), edge("a", "a")];
88 let cps = module_coupling(&edges);
89 let a = cps.iter().find(|c| c.module == "a").unwrap();
90 assert_eq!(a.efferent, 1);
91 assert!(
92 (a.instability - 1.0).abs() < 1e-9,
93 "only outgoing => unstable"
94 );
95 }
96
97 #[test]
98 fn stable_module_has_zero_instability() {
99 let edges = [edge("x", "core"), edge("y", "core")];
100 let cps = module_coupling(&edges);
101 let core = cps.iter().find(|c| c.module == "core").unwrap();
102 assert_eq!(core.afferent, 2);
103 assert_eq!(core.efferent, 0);
104 assert!((core.instability - 0.0).abs() < 1e-9);
105 }
106
107 #[test]
108 fn deterministic_order() {
109 let edges = [edge("b", "a"), edge("c", "a")];
110 assert_eq!(module_coupling(&edges), module_coupling(&edges));
111 }
112}