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