Skip to main content

_diffctx/edges/
mod.rs

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
84/// Two-pass edge construction that never retains the raw edge universe
85/// as keyed dictionaries and runs every builder exactly once.
86///
87/// Pass 1 runs every builder and records its emissions into a compact
88/// per-builder log of (src, dst, weight) triples (16 bytes/edge, builder
89/// tag implicit in the outer index); a first-seen scan in builder
90/// registration order reproduces `dedup_compact_edges` semantics exactly
91/// (each pair counted once, category from the first builder that
92/// produced it) and yields per-node in-degree, per-source out-degree,
93/// the semantic distinct-file fan counts, and the sorted category table.
94///
95/// Pass 2 replays the log instead of rerunning the builders, damps each
96/// emission on the fly with the pass-1 hub-suppression factors — always
97/// under the pair's canonical first-builder category — and keeps at most
98/// K candidates per source per builder in a bounded min-heap, freeing
99/// each builder's log shard as it is consumed. Any edge evicted from a
100/// per-builder heap is outranked by K surviving same-source edges, so
101/// the final merge + dedup + cap over the survivors is bit-identical to
102/// capping the full materialized universe.
103pub 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}