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(
108 fragments: &[Fragment],
109 repo_root: Option<&Path>,
110 skip_expensive: bool,
111) -> CappedEdges {
112 let mut all_builders: Vec<(&str, Box<dyn EdgeBuilder>)> = Vec::new();
113 for cat in builder_categories() {
114 if skip_expensive && EXPENSIVE_CATEGORIES.contains(&cat.name) {
115 debug!("skipping {} edge builders (skip_expensive=true)", cat.name);
116 continue;
117 }
118 for builder in (cat.builders)() {
119 all_builders.push((cat.name, builder));
120 }
121 }
122
123 let (node_to_idx, idx_to_node) = intern_fragment_nodes(fragments);
124 let category_weights = *crate::config::category_weights::CATEGORY_WEIGHTS;
125 let builder_meta: Vec<(EdgeCategory, f64)> = all_builders
126 .iter()
127 .map(|(cat_name, builder)| {
128 let category = EdgeCategory::from_str(builder.category_label().unwrap_or(cat_name));
129 (category, category_weights.multiplier(category))
130 })
131 .collect();
132
133 let per_builder_log: Vec<Vec<LoggedEmission>> = all_builders
134 .par_iter()
135 .map(|(_, builder)| {
136 let edges = builder.build(fragments, repo_root);
137 let mut log = Vec::with_capacity(edges.len());
138 for ((src, dst), weight) in edges {
139 let (Some(&s), Some(&d)) = (node_to_idx.get(&src), node_to_idx.get(&dst)) else {
140 continue;
141 };
142 log.push(LoggedEmission {
143 src: s,
144 dst: d,
145 weight,
146 });
147 }
148 log
149 })
150 .collect();
151 drop(all_builders);
152
153 let n_nodes = idx_to_node.len();
154 let mut in_degree = vec![0u32; n_nodes];
155 let mut out_degree = vec![0u32; n_nodes];
156 let mut category_entries: Vec<(u32, u32, EdgeCategory)> = Vec::new();
157 let mut sem_out_files: FxHashMap<u32, FxHashSet<&str>> = FxHashMap::default();
158 let mut raw_by_category: FxHashMap<EdgeCategory, u64> = FxHashMap::default();
159 let mut deduped_by_category: FxHashMap<EdgeCategory, u64> = FxHashMap::default();
160 let mut seen: FxHashSet<u64> = FxHashSet::default();
161 for (builder_idx, log) in per_builder_log.iter().enumerate() {
162 let (category, _) = builder_meta[builder_idx];
163 *raw_by_category.entry(category).or_default() += log.len() as u64;
164 for e in log {
165 if !seen.insert(pack_pair(e.src, e.dst)) {
166 continue;
167 }
168 *deduped_by_category.entry(category).or_default() += 1;
169 in_degree[e.dst as usize] += 1;
170 out_degree[e.src as usize] += 1;
171 category_entries.push((e.src, e.dst, category));
172 if category == EdgeCategory::Semantic {
173 sem_out_files
174 .entry(e.src)
175 .or_default()
176 .insert(idx_to_node[e.dst as usize].path.as_ref());
177 }
178 }
179 }
180 drop(seen);
181 let mut emissions_by_category: Vec<(EdgeCategory, u64, u64)> = raw_by_category
182 .iter()
183 .map(|(&category, &raw)| {
184 let deduped = deduped_by_category.get(&category).copied().unwrap_or(0);
185 (category, raw, deduped)
186 })
187 .collect();
188 emissions_by_category.sort_unstable_by_key(|e| e.0.as_str());
189 category_entries.sort_unstable_by_key(|e| (e.0, e.1));
190
191 let mut sem_file_deg = vec![0u32; n_nodes];
192 for (&src, files) in &sem_out_files {
193 sem_file_deg[src as usize] = files.len() as u32;
194 }
195 drop(sem_out_files);
196
197 let deduped_edge_count = category_entries.len();
198 let factors = SuppressionFactors::from_counters(in_degree, sem_file_deg);
199 let max_per_node = read_max_out_edges_per_node();
200
201 let capped_per_builder: Vec<Vec<CompactEdge>> = per_builder_log
202 .into_par_iter()
203 .enumerate()
204 .map(|(builder_idx, log)| {
205 let (builder_category, multiplier) = builder_meta[builder_idx];
206 let mut per_source: FxHashMap<u32, SourceTopK> = FxHashMap::default();
207 for e in log {
208 let category = category_entries
209 .binary_search_by_key(&(e.src, e.dst), |c| (c.0, c.1))
210 .map(|k| category_entries[k].2)
211 .unwrap_or(builder_category);
212 let damped = factors.damp(e.weight * multiplier, category, e.src, e.dst);
213 push_bounded_top_k(
214 per_source.entry(e.src).or_default(),
215 RankedCandidate {
216 weight: damped,
217 dst: e.dst,
218 category,
219 },
220 max_per_node,
221 );
222 }
223 let mut survivors =
224 Vec::with_capacity(per_source.values().map(|h| h.len()).sum::<usize>());
225 for (src, heap) in per_source {
226 for Reverse(c) in heap {
227 survivors.push(CompactEdge {
228 src,
229 dst: c.dst,
230 weight: c.weight,
231 category: c.category,
232 });
233 }
234 }
235 survivors
236 })
237 .collect();
238
239 let total: usize = capped_per_builder.iter().map(|v| v.len()).sum();
240 let mut edges: Vec<CompactEdge> = Vec::with_capacity(total);
241 for v in capped_per_builder {
242 edges.extend(v);
243 }
244 dedup_compact_edges(&mut edges);
245 cap_out_edges_per_source(&mut edges, max_per_node);
246
247 let nodes_capped = out_degree
248 .iter()
249 .filter(|&&d| d as usize > max_per_node)
250 .count();
251 let cap_stats = EdgeCapStats {
252 edges_before_cap: deduped_edge_count,
253 edges_after_cap: edges.len(),
254 edges_dropped_by_cap: deduped_edge_count - edges.len(),
255 nodes_capped,
256 max_out_edges_per_node: max_per_node,
257 emissions_by_category,
258 };
259
260 CappedEdges {
261 node_to_idx,
262 idx_to_node,
263 edges,
264 cap_stats,
265 }
266}
267
268pub fn discover_all_related_files(
269 changed_files: &[PathBuf],
270 all_candidates: &[PathBuf],
271 repo_root: Option<&Path>,
272 file_cache: Option<&FxHashMap<PathBuf, String>>,
273) -> Vec<PathBuf> {
274 let mut discovered: FxHashMap<PathBuf, ()> = FxHashMap::default();
275 for builder in get_all_builders() {
276 for f in
277 builder.discover_related_files(changed_files, all_candidates, repo_root, file_cache)
278 {
279 discovered.entry(f).or_insert(());
280 }
281 }
282 let mut result: Vec<PathBuf> = discovered.into_keys().collect();
283 result.sort();
284 result
285}