lean_ctx/core/community/
mod.rs1use std::collections::HashMap;
11
12use rusqlite::Connection;
13use serde::Serialize;
14
15use crate::core::graph_provider::GraphProvider;
16
17mod graph;
18mod hardening;
19mod leiden;
20mod stable_ids;
21#[cfg(test)]
22mod tests;
23
24use graph::{AdjGraph, edge_counts};
25
26#[derive(Debug, Clone, Serialize)]
27pub struct Community {
28 pub id: usize,
29 pub files: Vec<String>,
30 pub internal_edges: usize,
31 pub external_edges: usize,
32 pub cohesion: f64,
33}
34
35#[derive(Debug, Clone, Serialize)]
36pub struct CommunityResult {
37 pub communities: Vec<Community>,
38 pub modularity: f64,
39 pub node_count: usize,
40 pub edge_count: usize,
41}
42
43impl CommunityResult {
44 fn empty() -> Self {
45 Self {
46 communities: Vec::new(),
47 modularity: 0.0,
48 node_count: 0,
49 edge_count: 0,
50 }
51 }
52
53 pub fn assignment(&self) -> HashMap<String, usize> {
55 self.assignment_min_size(1)
56 }
57
58 pub fn assignment_min_size(&self, min_size: usize) -> HashMap<String, usize> {
63 let mut map = HashMap::new();
64 for community in &self.communities {
65 if community.files.len() < min_size {
66 continue;
67 }
68 for file in &community.files {
69 map.insert(file.clone(), community.id);
70 }
71 }
72 map
73 }
74}
75
76pub fn detect_communities(conn: &Connection) -> CommunityResult {
80 let graph = AdjGraph::from_property_graph(conn);
81 analyze(&graph, None).1
82}
83
84pub fn detect_communities_stable(conn: &Connection, project_root: &str) -> CommunityResult {
87 let graph = AdjGraph::from_property_graph(conn);
88 detect_stable(&graph, project_root)
89}
90
91pub fn detect_communities_for_provider(gp: &GraphProvider, project_root: &str) -> CommunityResult {
94 let graph = AdjGraph::from_provider(gp);
95 detect_stable(&graph, project_root)
96}
97
98fn detect_stable(graph: &AdjGraph, project_root: &str) -> CommunityResult {
99 if graph.node_count() == 0 {
100 return CommunityResult::empty();
101 }
102 let previous = stable_ids::load_previous(project_root);
103 let (assignment, result) = analyze(graph, previous.as_ref());
104 stable_ids::save_assignment(project_root, &graph.node_ids, &assignment);
105 result
106}
107
108fn analyze(
112 graph: &AdjGraph,
113 previous: Option<&HashMap<String, usize>>,
114) -> (Vec<usize>, CommunityResult) {
115 let n = graph.node_count();
116 if n == 0 {
117 return (Vec::new(), CommunityResult::empty());
118 }
119
120 let mut assignment = hardening::partition_with_hub_exclusion(graph);
121 hardening::split_oversized_and_incohesive(graph, &mut assignment);
122 let mut assignment = stable_ids::canonicalize(graph, &assignment);
123 if let Some(prev) = previous
124 && !prev.is_empty()
125 {
126 assignment = stable_ids::remap_to_previous(graph, &assignment, prev);
127 }
128
129 let result = build_result(graph, &assignment);
130 (assignment, result)
131}
132
133fn build_result(graph: &AdjGraph, assignment: &[usize]) -> CommunityResult {
134 let mut groups: HashMap<usize, Vec<usize>> = HashMap::new();
135 for (i, &c) in assignment.iter().enumerate() {
136 groups.entry(c).or_default().push(i);
137 }
138
139 let mut communities: Vec<Community> = groups
140 .into_iter()
141 .map(|(id, mut members)| {
142 members.sort_unstable();
143 let (internal, external) = edge_counts(graph, &members);
144 let total = (internal + external).max(1) as f64;
145 Community {
146 id,
147 files: members.iter().map(|&i| graph.node_ids[i].clone()).collect(),
148 internal_edges: internal,
149 external_edges: external,
150 cohesion: internal as f64 / total,
151 }
152 })
153 .collect();
154
155 communities.sort_by(|a, b| {
157 b.files
158 .len()
159 .cmp(&a.files.len())
160 .then(
161 b.cohesion
162 .partial_cmp(&a.cohesion)
163 .unwrap_or(std::cmp::Ordering::Equal),
164 )
165 .then(a.id.cmp(&b.id))
166 });
167
168 CommunityResult {
169 communities,
170 modularity: leiden::compute_modularity(graph, assignment),
171 node_count: graph.node_count(),
172 edge_count: graph.edge_count(),
173 }
174}