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 let fallback_flags: Vec<bool> = all_builders
133 .iter()
134 .map(|(_, builder)| builder.is_fallback())
135 .collect();
136
137 let per_builder_log: Vec<Vec<LoggedEmission>> = all_builders
138 .par_iter()
139 .map(|(name, builder)| {
140 crate::deadline::check_compute_deadline("edge construction");
141 let t = std::time::Instant::now();
142 let edges = builder.build(fragments, repo_root);
143 if std::env::var_os("DIFFCTX_TRACE_BUILDERS").is_some() {
144 eprintln!(
145 "builder {name}: {:.1}s, {} edges",
146 t.elapsed().as_secs_f64(),
147 edges.len()
148 );
149 }
150 let mut log = Vec::with_capacity(edges.len());
151 for ((src, dst), weight) in edges {
152 let (Some(&s), Some(&d)) = (node_to_idx.get(&src), node_to_idx.get(&dst)) else {
153 continue;
154 };
155 log.push(LoggedEmission {
156 src: s,
157 dst: d,
158 weight,
159 });
160 }
161 log
162 })
163 .collect();
164 drop(all_builders);
165
166 let mut per_builder_log = per_builder_log;
172 if fallback_flags.iter().any(|&f| f) {
173 let mut dedicated_files: FxHashSet<&str> = FxHashSet::default();
174 for (builder_idx, log) in per_builder_log.iter().enumerate() {
175 if fallback_flags[builder_idx] || builder_meta[builder_idx].0 != EdgeCategory::Semantic
176 {
177 continue;
178 }
179 for e in log {
180 dedicated_files.insert(idx_to_node[e.src as usize].path.as_ref());
181 dedicated_files.insert(idx_to_node[e.dst as usize].path.as_ref());
182 }
183 }
184 for (builder_idx, log) in per_builder_log.iter_mut().enumerate() {
185 if !fallback_flags[builder_idx] {
186 continue;
187 }
188 log.retain(|e| {
189 !dedicated_files.contains(idx_to_node[e.src as usize].path.as_ref())
190 || !dedicated_files.contains(idx_to_node[e.dst as usize].path.as_ref())
191 });
192 }
193 }
194
195 let n_nodes = idx_to_node.len();
196 let mut in_degree = vec![0u32; n_nodes];
197 let mut out_degree = vec![0u32; n_nodes];
198 let mut category_entries: Vec<(u32, u32, EdgeCategory)> = Vec::new();
199 let mut sem_out_files: FxHashMap<u32, FxHashSet<&str>> = FxHashMap::default();
200 let mut raw_by_category: FxHashMap<EdgeCategory, u64> = FxHashMap::default();
201 let mut deduped_by_category: FxHashMap<EdgeCategory, u64> = FxHashMap::default();
202 let mut seen: FxHashSet<u64> = FxHashSet::default();
203 for (builder_idx, log) in per_builder_log.iter().enumerate() {
204 let (category, _) = builder_meta[builder_idx];
205 *raw_by_category.entry(category).or_default() += log.len() as u64;
206 for e in log {
207 if !seen.insert(pack_pair(e.src, e.dst)) {
208 continue;
209 }
210 *deduped_by_category.entry(category).or_default() += 1;
211 in_degree[e.dst as usize] += 1;
212 out_degree[e.src as usize] += 1;
213 category_entries.push((e.src, e.dst, category));
214 if category == EdgeCategory::Semantic {
215 sem_out_files
216 .entry(e.src)
217 .or_default()
218 .insert(idx_to_node[e.dst as usize].path.as_ref());
219 }
220 }
221 }
222 drop(seen);
223 let mut emissions_by_category: Vec<(EdgeCategory, u64, u64)> = raw_by_category
224 .iter()
225 .map(|(&category, &raw)| {
226 let deduped = deduped_by_category.get(&category).copied().unwrap_or(0);
227 (category, raw, deduped)
228 })
229 .collect();
230 emissions_by_category.sort_unstable_by_key(|e| e.0.as_str());
231 category_entries.sort_unstable_by_key(|e| (e.0, e.1));
232
233 let mut sem_file_deg = vec![0u32; n_nodes];
234 for (&src, files) in &sem_out_files {
235 sem_file_deg[src as usize] = files.len() as u32;
236 }
237 drop(sem_out_files);
238
239 let deduped_edge_count = category_entries.len();
240 let factors = SuppressionFactors::from_counters(in_degree, sem_file_deg);
241 let max_per_node = read_max_out_edges_per_node();
242
243 let capped_per_builder: Vec<Vec<CompactEdge>> = per_builder_log
244 .into_par_iter()
245 .enumerate()
246 .map(|(builder_idx, log)| {
247 let (builder_category, multiplier) = builder_meta[builder_idx];
248 let mut per_source: FxHashMap<u32, SourceTopK> = FxHashMap::default();
249 for e in log {
250 let category = category_entries
251 .binary_search_by_key(&(e.src, e.dst), |c| (c.0, c.1))
252 .map(|k| category_entries[k].2)
253 .unwrap_or(builder_category);
254 let damped = factors.damp(e.weight * multiplier, category, e.src, e.dst);
255 push_bounded_top_k(
256 per_source.entry(e.src).or_default(),
257 RankedCandidate {
258 weight: damped,
259 dst: e.dst,
260 category,
261 },
262 max_per_node,
263 );
264 }
265 let mut survivors =
266 Vec::with_capacity(per_source.values().map(|h| h.len()).sum::<usize>());
267 for (src, heap) in per_source {
268 for Reverse(c) in heap {
269 survivors.push(CompactEdge {
270 src,
271 dst: c.dst,
272 weight: c.weight,
273 category: c.category,
274 });
275 }
276 }
277 survivors
278 })
279 .collect();
280
281 let total: usize = capped_per_builder.iter().map(|v| v.len()).sum();
282 let mut edges: Vec<CompactEdge> = Vec::with_capacity(total);
283 for v in capped_per_builder {
284 edges.extend(v);
285 }
286 dedup_compact_edges(&mut edges);
287 cap_out_edges_per_source(&mut edges, max_per_node);
288
289 let nodes_capped = out_degree
290 .iter()
291 .filter(|&&d| d as usize > max_per_node)
292 .count();
293 let cap_stats = EdgeCapStats {
294 edges_before_cap: deduped_edge_count,
295 edges_after_cap: edges.len(),
296 edges_dropped_by_cap: deduped_edge_count - edges.len(),
297 nodes_capped,
298 max_out_edges_per_node: max_per_node,
299 emissions_by_category,
300 };
301
302 CappedEdges {
303 node_to_idx,
304 idx_to_node,
305 edges,
306 cap_stats,
307 }
308}
309
310pub fn discover_all_related_files(
311 changed_files: &[PathBuf],
312 all_candidates: &[PathBuf],
313 repo_root: Option<&Path>,
314 file_cache: Option<&FxHashMap<PathBuf, String>>,
315) -> Vec<PathBuf> {
316 let mut discovered: FxHashMap<PathBuf, ()> = FxHashMap::default();
317 for builder in get_all_builders() {
318 for f in
319 builder.discover_related_files(changed_files, all_candidates, repo_root, file_cache)
320 {
321 discovered.entry(f).or_insert(());
322 }
323 }
324 let mut result: Vec<PathBuf> = discovered.into_keys().collect();
325 result.sort();
326 result
327}
328
329#[cfg(test)]
330mod fallback_gate_tests {
331 use super::*;
332 use rustc_hash::FxHashSet as Set;
333 use std::sync::Arc;
334
335 fn frag(path: &str, content: &str, idents: &[&str]) -> Fragment {
336 Fragment {
337 id: crate::types::FragmentId::new(Arc::from(path), 1, 10),
338 kind: crate::types::FragmentKind::Function,
339 content: Arc::from(content),
340 identifiers: idents.iter().map(|s| s.to_string()).collect::<Set<_>>(),
341 token_count: 10,
342 symbol_name: None,
343 }
344 }
345
346 #[test]
347 fn tags_edges_survive_only_where_dedicated_builders_came_back_empty() {
348 let fragments = vec![
352 frag(
353 "proj/a.c",
354 "#include \"bdep.h\"\nint zzcommonzz;\n",
355 &["zzcommonzz"],
356 ),
357 frag("proj/bdep.h", "int bdecl(void);\n", &["bdecl"]),
358 frag(
359 "proj/c.c",
360 "#include \"ddep.h\"\nint zzcommonzz;\n",
361 &["zzcommonzz"],
362 ),
363 frag("proj/ddep.h", "int ddecl(void);\n", &["ddecl"]),
364 frag("proj/u1.xyz", "zzcommonzz here\n", &["zzcommonzz"]),
365 frag("proj/u2.xyz", "zzcommonzz there\n", &["zzcommonzz"]),
366 ];
367 let capped = collect_capped_edges(&fragments, None, false);
368 let node_path = |idx: u32| capped.idx_to_node[idx as usize].path.clone();
369 let has = |a: &str, b: &str| {
372 capped.edges.iter().any(|e| {
373 if e.category != EdgeCategory::Semantic {
374 return false;
375 }
376 let s = node_path(e.src);
377 let d = node_path(e.dst);
378 (s.ends_with(a) && d.ends_with(b)) || (s.ends_with(b) && d.ends_with(a))
379 })
380 };
381 assert!(
382 has("u1.xyz", "u2.xyz"),
383 "fallback must still connect files no dedicated builder covers"
384 );
385 assert!(
386 !has("a.c", "c.c"),
387 "a tags-only link between two dedicated-covered files is the measured noise class (#131)"
388 );
389 }
390}