1pub mod base;
2pub mod config_edges;
3pub mod document;
4pub mod history;
5pub mod semantic;
6pub mod similarity;
7pub mod structural;
8
9use std::cmp::Reverse;
10use std::path::{Path, PathBuf};
11
12use rayon::prelude::*;
13use rustc_hash::{FxHashMap, FxHashSet};
14use tracing::debug;
15
16use crate::graph::{
17 CappedEdges, CompactEdge, EdgeCapStats, EdgeCategory, RankedCandidate, SourceTopK,
18 SuppressionFactors, cap_out_edges_per_source, dedup_compact_edges, intern_fragment_nodes,
19 push_bounded_top_k, read_max_out_edges_per_node,
20};
21use crate::types::FragmentId;
22
23pub type EdgeDict = FxHashMap<(FragmentId, FragmentId), f64>;
24pub type EdgeCategories = FxHashMap<(FragmentId, FragmentId), EdgeCategory>;
25
26use crate::types::Fragment;
27
28use self::base::EdgeBuilder;
29
30const EXPENSIVE_CATEGORIES: &[&str] = &["similarity", "history"];
31
32struct BuilderCategory {
33 name: &'static str,
34 builders: fn() -> Vec<Box<dyn EdgeBuilder>>,
35}
36
37fn builder_categories() -> Vec<BuilderCategory> {
38 vec![
39 BuilderCategory {
40 name: "semantic",
41 builders: || semantic::get_semantic_builders(),
42 },
43 BuilderCategory {
44 name: "structural",
45 builders: || structural::get_structural_builders(),
46 },
47 BuilderCategory {
48 name: "config",
49 builders: || config_edges::get_config_builders(),
50 },
51 BuilderCategory {
52 name: "document",
53 builders: || document::get_document_builders(),
54 },
55 BuilderCategory {
56 name: "similarity",
57 builders: || similarity::get_similarity_builders(),
58 },
59 BuilderCategory {
60 name: "history",
61 builders: || history::get_history_builders(),
62 },
63 ]
64}
65
66pub fn get_all_builders() -> Vec<Box<dyn EdgeBuilder>> {
67 let mut all = Vec::new();
68 for cat in builder_categories() {
69 all.extend((cat.builders)());
70 }
71 all
72}
73
74fn pack_pair(src: u32, dst: u32) -> u64 {
75 ((src as u64) << 32) | dst as u64
76}
77
78struct LoggedEmission {
79 src: u32,
80 dst: u32,
81 weight: f64,
82}
83
84pub fn collect_capped_edges(
104 fragments: &[Fragment],
105 repo_root: Option<&Path>,
106 skip_expensive: bool,
107) -> CappedEdges {
108 let mut all_builders: Vec<(&str, Box<dyn EdgeBuilder>)> = Vec::new();
109 for cat in builder_categories() {
110 if skip_expensive && EXPENSIVE_CATEGORIES.contains(&cat.name) {
111 debug!("skipping {} edge builders (skip_expensive=true)", cat.name);
112 continue;
113 }
114 for builder in (cat.builders)() {
115 all_builders.push((cat.name, builder));
116 }
117 }
118
119 let (node_to_idx, idx_to_node) = intern_fragment_nodes(fragments);
120 let category_weights = *crate::config::category_weights::CATEGORY_WEIGHTS;
121 let builder_meta: Vec<(EdgeCategory, f64)> = all_builders
122 .iter()
123 .map(|(cat_name, builder)| {
124 let category = EdgeCategory::from_str(builder.category_label().unwrap_or(cat_name));
125 (category, category_weights.multiplier(category))
126 })
127 .collect();
128
129 let per_builder_log: Vec<Vec<LoggedEmission>> = all_builders
130 .par_iter()
131 .map(|(_, builder)| {
132 let edges = builder.build(fragments, repo_root);
133 let mut log = Vec::with_capacity(edges.len());
134 for ((src, dst), weight) in edges {
135 let (Some(&s), Some(&d)) = (node_to_idx.get(&src), node_to_idx.get(&dst)) else {
136 continue;
137 };
138 log.push(LoggedEmission {
139 src: s,
140 dst: d,
141 weight,
142 });
143 }
144 log
145 })
146 .collect();
147 drop(all_builders);
148
149 let n_nodes = idx_to_node.len();
150 let mut in_degree = vec![0u32; n_nodes];
151 let mut out_degree = vec![0u32; n_nodes];
152 let mut category_entries: Vec<(u32, u32, EdgeCategory)> = Vec::new();
153 let mut sem_out_files: FxHashMap<u32, FxHashSet<&str>> = FxHashMap::default();
154 let mut raw_by_category: FxHashMap<EdgeCategory, u64> = FxHashMap::default();
155 let mut deduped_by_category: FxHashMap<EdgeCategory, u64> = FxHashMap::default();
156 let mut seen: FxHashSet<u64> = FxHashSet::default();
157 for (builder_idx, log) in per_builder_log.iter().enumerate() {
158 let (category, _) = builder_meta[builder_idx];
159 *raw_by_category.entry(category).or_default() += log.len() as u64;
160 for e in log {
161 if !seen.insert(pack_pair(e.src, e.dst)) {
162 continue;
163 }
164 *deduped_by_category.entry(category).or_default() += 1;
165 in_degree[e.dst as usize] += 1;
166 out_degree[e.src as usize] += 1;
167 category_entries.push((e.src, e.dst, category));
168 if category == EdgeCategory::Semantic {
169 sem_out_files
170 .entry(e.src)
171 .or_default()
172 .insert(idx_to_node[e.dst as usize].path.as_ref());
173 }
174 }
175 }
176 drop(seen);
177 let mut emissions_by_category: Vec<(EdgeCategory, u64, u64)> = raw_by_category
178 .iter()
179 .map(|(&category, &raw)| {
180 let deduped = deduped_by_category.get(&category).copied().unwrap_or(0);
181 (category, raw, deduped)
182 })
183 .collect();
184 emissions_by_category.sort_unstable_by_key(|e| e.0.as_str());
185 category_entries.sort_unstable_by_key(|e| (e.0, e.1));
186
187 let mut sem_file_deg = vec![0u32; n_nodes];
188 for (&src, files) in &sem_out_files {
189 sem_file_deg[src as usize] = files.len() as u32;
190 }
191 drop(sem_out_files);
192
193 let deduped_edge_count = category_entries.len();
194 let factors = SuppressionFactors::from_counters(in_degree, sem_file_deg);
195 let max_per_node = read_max_out_edges_per_node();
196
197 let capped_per_builder: Vec<Vec<CompactEdge>> = per_builder_log
198 .into_par_iter()
199 .enumerate()
200 .map(|(builder_idx, log)| {
201 let (builder_category, multiplier) = builder_meta[builder_idx];
202 let mut per_source: FxHashMap<u32, SourceTopK> = FxHashMap::default();
203 for e in log {
204 let category = category_entries
205 .binary_search_by_key(&(e.src, e.dst), |c| (c.0, c.1))
206 .map(|k| category_entries[k].2)
207 .unwrap_or(builder_category);
208 let damped = factors.damp(e.weight * multiplier, category, e.src, e.dst);
209 push_bounded_top_k(
210 per_source.entry(e.src).or_default(),
211 RankedCandidate {
212 weight: damped,
213 dst: e.dst,
214 category,
215 },
216 max_per_node,
217 );
218 }
219 let mut survivors =
220 Vec::with_capacity(per_source.values().map(|h| h.len()).sum::<usize>());
221 for (src, heap) in per_source {
222 for Reverse(c) in heap {
223 survivors.push(CompactEdge {
224 src,
225 dst: c.dst,
226 weight: c.weight,
227 category: c.category,
228 });
229 }
230 }
231 survivors
232 })
233 .collect();
234
235 let total: usize = capped_per_builder.iter().map(|v| v.len()).sum();
236 let mut edges: Vec<CompactEdge> = Vec::with_capacity(total);
237 for v in capped_per_builder {
238 edges.extend(v);
239 }
240 dedup_compact_edges(&mut edges);
241 cap_out_edges_per_source(&mut edges, max_per_node);
242
243 let nodes_capped = out_degree
244 .iter()
245 .filter(|&&d| d as usize > max_per_node)
246 .count();
247 let cap_stats = EdgeCapStats {
248 edges_before_cap: deduped_edge_count,
249 edges_after_cap: edges.len(),
250 edges_dropped_by_cap: deduped_edge_count - edges.len(),
251 nodes_capped,
252 max_out_edges_per_node: max_per_node,
253 emissions_by_category,
254 };
255
256 CappedEdges {
257 node_to_idx,
258 idx_to_node,
259 edges,
260 category_entries,
261 cap_stats,
262 }
263}
264
265pub fn discover_all_related_files(
266 changed_files: &[PathBuf],
267 all_candidates: &[PathBuf],
268 repo_root: Option<&Path>,
269 file_cache: Option<&FxHashMap<PathBuf, String>>,
270) -> Vec<PathBuf> {
271 let mut discovered: FxHashMap<PathBuf, ()> = FxHashMap::default();
272 for builder in get_all_builders() {
273 for f in
274 builder.discover_related_files(changed_files, all_candidates, repo_root, file_cache)
275 {
276 discovered.entry(f).or_insert(());
277 }
278 }
279 let mut result: Vec<PathBuf> = discovered.into_keys().collect();
280 result.sort();
281 result
282}