Skip to main content

_diffctx/
select.rs

1use std::cmp::Ordering;
2use std::collections::BinaryHeap;
3use std::sync::Arc;
4
5use rustc_hash::{FxHashMap, FxHashSet};
6
7use crate::config::limits::UTILITY;
8use crate::config::selection::selection;
9use crate::interval::IntervalIndex;
10use crate::types::{Fragment, FragmentId};
11use crate::utility::needs::InformationNeed;
12use crate::utility::scoring::{
13    UtilityState, apply_fragment, compute_density, marginal_gain, utility_value,
14};
15
16const SENTINEL_TOKEN_COUNT: u32 = 1_000_000_000;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum SelectionReason {
20    TopK,
21    NoCandidates,
22    BudgetExhausted,
23    NoUtility,
24    StoppedByTau,
25    BestSingleton,
26}
27
28impl SelectionReason {
29    pub fn as_str(&self) -> &'static str {
30        match self {
31            Self::TopK => "topk",
32            Self::NoCandidates => "no_candidates",
33            Self::BudgetExhausted => "budget_exhausted",
34            Self::NoUtility => "no_utility",
35            Self::StoppedByTau => "stopped_by_tau",
36            Self::BestSingleton => "best_singleton",
37        }
38    }
39}
40
41pub struct SelectionResult {
42    pub selected: Vec<Fragment>,
43    pub reason: SelectionReason,
44    pub used_tokens: u32,
45    pub utility: f64,
46    /// Greedy iterations actually executed (number of `apply_fragment`
47    /// calls in `run_greedy_loop_heap`). Diagnoses lazy-heap blowup:
48    /// expected ≈ output size, pathological ≫ output size when
49    /// stale-version rejections dominate.
50    pub greedy_iters: usize,
51    /// Additive certificate for adaptive stopping: an upper bound
52    /// (`tau * peak_density * remaining_budget`) on the utility that
53    /// continuing the same greedy to the feasibility frontier could
54    /// still have added. 0 when the loop ended for any other reason.
55    pub stopping_certificate: f64,
56}
57
58struct HeapEntry {
59    neg_density: f64,
60    frag_id: FragmentId,
61    version: u32,
62}
63
64impl PartialEq for HeapEntry {
65    fn eq(&self, other: &Self) -> bool {
66        self.neg_density.to_bits() == other.neg_density.to_bits() && self.frag_id == other.frag_id
67    }
68}
69
70impl Eq for HeapEntry {}
71
72impl PartialOrd for HeapEntry {
73    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
74        Some(self.cmp(other))
75    }
76}
77
78impl Ord for HeapEntry {
79    fn cmp(&self, other: &Self) -> Ordering {
80        other
81            .neg_density
82            .total_cmp(&self.neg_density)
83            .then_with(|| other.frag_id.cmp(&self.frag_id))
84    }
85}
86
87struct SelectionState {
88    selected: Vec<Fragment>,
89    selected_ids: IntervalIndex,
90    remaining_budget: u32,
91    utility_state: UtilityState,
92}
93
94fn drop_redundant_signatures(candidates: &[Fragment], budget: u32) -> Vec<Fragment> {
95    let mut full_token_by_loc: FxHashMap<(Arc<str>, u32), u32> = FxHashMap::default();
96    for f in candidates {
97        if !f.kind.is_signature() {
98            full_token_by_loc.insert((f.id.path.clone(), f.start_line()), f.token_count);
99        }
100    }
101    candidates
102        .iter()
103        .filter(|f| {
104            if !f.kind.is_signature() {
105                return true;
106            }
107            let key = (f.id.path.clone(), f.start_line());
108            full_token_by_loc
109                .get(&key)
110                .copied()
111                .unwrap_or(SENTINEL_TOKEN_COUNT)
112                > budget
113        })
114        .cloned()
115        .collect()
116}
117
118fn compute_r_cap(
119    rel: &FxHashMap<FragmentId, f64>,
120    core_ids: Option<&FxHashSet<FragmentId>>,
121) -> f64 {
122    let values: Vec<f64> = rel
123        .iter()
124        .filter(|(fid, v)| **v > 0.0 && core_ids.map_or(true, |c| !c.contains(*fid)))
125        .map(|(_, v)| *v)
126        .collect();
127
128    if values.len() < 2 {
129        return if let Some(&v) = values.first() {
130            v.max(selection().r_cap_min)
131        } else {
132            1.0
133        };
134    }
135
136    let mut sorted = values.clone();
137    sorted.sort_by(|a, b| a.total_cmp(b));
138    let mid = sorted.len() / 2;
139    let med = if sorted.len() % 2 == 0 {
140        (sorted[mid - 1] + sorted[mid]) / 2.0
141    } else {
142        sorted[mid]
143    };
144
145    let mean: f64 = values.iter().sum::<f64>() / values.len() as f64;
146    let variance: f64 =
147        values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (values.len() - 1) as f64;
148    let std = variance.sqrt();
149
150    (med + UTILITY.r_cap_sigma * std).max(1e-9)
151}
152
153fn build_signature_lookup(
154    fragments: &[Fragment],
155    core_fragments: &[Fragment],
156) -> FxHashMap<FragmentId, Fragment> {
157    let mut sig_by_loc: FxHashMap<(Arc<str>, u32), Fragment> = FxHashMap::default();
158    for f in fragments {
159        if f.kind.is_signature() {
160            sig_by_loc.insert((f.id.path.clone(), f.start_line()), f.clone());
161        }
162    }
163    let mut sig_lookup = FxHashMap::default();
164    for cf in core_fragments {
165        let key = (cf.id.path.clone(), cf.start_line());
166        if let Some(sig) = sig_by_loc.get(&key) {
167            sig_lookup.insert(cf.id.clone(), sig.clone());
168        }
169    }
170    sig_lookup
171}
172
173fn select_core_fragments(
174    core_fragments: &[Fragment],
175    rel: &FxHashMap<FragmentId, f64>,
176    needs: &[InformationNeed],
177    state: &mut SelectionState,
178    budget_tokens: u32,
179    sig_lookup: &FxHashMap<FragmentId, Fragment>,
180) {
181    let core_budget = (budget_tokens as f64 * selection().core_budget_fraction) as u32;
182    // Counter for cores placed; the first pass keeps `core_used <= core_budget`,
183    // but the rescue pass below intentionally allows it to exceed `core_budget`
184    // up to `budget_tokens`. Don't assume the tighter bound past this scope.
185    let mut core_used = 0u32;
186
187    let mut sorted_core: Vec<&Fragment> = core_fragments.iter().collect();
188    sorted_core.sort_by(|a, b| {
189        let ra = rel.get(&a.id).copied().unwrap_or(0.0);
190        let rb = rel.get(&b.id).copied().unwrap_or(0.0);
191        rb.total_cmp(&ra)
192    });
193
194    let place_fragment =
195        |frag: &Fragment, core_used: &mut u32, state: &mut SelectionState, rel_score: f64| {
196            state.selected.push(frag.clone());
197            state.selected_ids.add_id(&frag.id);
198            state.remaining_budget = state.remaining_budget.saturating_sub(frag.token_count);
199            *core_used += frag.token_count;
200            apply_fragment(frag, rel_score, needs, &mut state.utility_state);
201        };
202
203    let mut skipped: Vec<&Fragment> = Vec::new();
204    for frag in &sorted_core {
205        if state.selected_ids.is_superset_of(frag) {
206            continue;
207        }
208        if core_used + frag.token_count > core_budget {
209            if let Some(sig) = sig_lookup.get(&frag.id) {
210                if !state.selected_ids.contains(&sig.id)
211                    && core_used + sig.token_count <= core_budget
212                {
213                    let rel_score = rel.get(&frag.id).copied().unwrap_or(0.0);
214                    place_fragment(sig, &mut core_used, state, rel_score);
215                    continue;
216                }
217            }
218            skipped.push(frag);
219            continue;
220        }
221
222        let rel_score = rel.get(&frag.id).copied().unwrap_or(0.0);
223        place_fragment(frag, &mut core_used, state, rel_score);
224    }
225
226    // Bug #2 fix: cores that didn't fit the core_budget reservation must not be
227    // demoted to ordinary greedy candidates without a chance to be placed first.
228    // Sweep skipped cores cheapest-first against the *full* remaining budget
229    // (not just the core slice) so seeds aren't dropped purely because the
230    // highest-relevance core happened to be heavy.
231    if !skipped.is_empty() {
232        skipped.sort_by(|a, b| a.token_count.cmp(&b.token_count));
233        for frag in skipped {
234            if state.remaining_budget == 0 {
235                break;
236            }
237            if state.selected_ids.is_superset_of(frag) {
238                continue;
239            }
240            if frag.token_count <= state.remaining_budget {
241                let rel_score = rel.get(&frag.id).copied().unwrap_or(0.0);
242                place_fragment(frag, &mut core_used, state, rel_score);
243            } else if let Some(sig) = sig_lookup.get(&frag.id) {
244                if !state.selected_ids.contains(&sig.id)
245                    && sig.token_count <= state.remaining_budget
246                {
247                    let rel_score = rel.get(&frag.id).copied().unwrap_or(0.0);
248                    place_fragment(sig, &mut core_used, state, rel_score);
249                }
250            }
251        }
252    }
253}
254
255fn build_initial_heap(
256    candidates: &[Fragment],
257    rel: &FxHashMap<FragmentId, f64>,
258    needs: &[InformationNeed],
259    state: &UtilityState,
260    id_to_frag: &mut FxHashMap<FragmentId, Fragment>,
261) -> BinaryHeap<HeapEntry> {
262    let mut heap = BinaryHeap::new();
263    for frag in candidates {
264        if frag.token_count > 0 {
265            let density = compute_density(
266                frag,
267                rel.get(&frag.id).copied().unwrap_or(0.0),
268                needs,
269                state,
270            );
271            heap.push(HeapEntry {
272                neg_density: -density,
273                frag_id: frag.id.clone(),
274                version: 0,
275            });
276            id_to_frag.insert(frag.id.clone(), frag.clone());
277        }
278    }
279    heap
280}
281
282fn find_best_candidate_heap(
283    heap: &mut BinaryHeap<HeapEntry>,
284    current_version: u32,
285    id_to_frag: &FxHashMap<FragmentId, Fragment>,
286    selected_ids: &IntervalIndex,
287    remaining_budget: u32,
288    rel: &FxHashMap<FragmentId, f64>,
289    needs: &[InformationNeed],
290    state: &UtilityState,
291) -> (Option<Fragment>, f64, u32) {
292    let cv = current_version;
293    while let Some(entry) = heap.pop() {
294        let frag = match id_to_frag.get(&entry.frag_id) {
295            Some(f) => f,
296            None => continue,
297        };
298        if frag.token_count > remaining_budget {
299            continue;
300        }
301        if selected_ids.overlaps(frag) {
302            continue;
303        }
304        if entry.version < cv {
305            let new_density = compute_density(
306                frag,
307                rel.get(&frag.id).copied().unwrap_or(0.0),
308                needs,
309                state,
310            );
311            heap.push(HeapEntry {
312                neg_density: -new_density,
313                frag_id: frag.id.clone(),
314                version: cv,
315            });
316            continue;
317        }
318        let actual_density = -entry.neg_density;
319        if actual_density <= 0.0 {
320            return (None, 0.0, cv);
321        }
322        return (Some(frag.clone()), actual_density, cv + 1);
323    }
324    (None, 0.0, cv)
325}
326
327fn find_best_singleton(
328    non_core: &[Fragment],
329    base_selected_ids: &IntervalIndex,
330    base_budget: u32,
331    rel: &FxHashMap<FragmentId, f64>,
332    needs: &[InformationNeed],
333    base_state: &UtilityState,
334) -> (Option<Fragment>, f64) {
335    let mut best_singleton = None;
336    let mut best_gain = 0.0;
337    for f in non_core {
338        if f.token_count > base_budget {
339            continue;
340        }
341        if base_selected_ids.overlaps(f) {
342            continue;
343        }
344        let gain = marginal_gain(f, rel.get(&f.id).copied().unwrap_or(0.0), needs, base_state);
345        if gain > best_gain {
346            best_gain = gain;
347            best_singleton = Some(f.clone());
348        }
349    }
350    (best_singleton, best_gain)
351}
352
353/// Paper-aligned strict Khuller H₁: `argmax_{f ∈ F, |f| ≤ B} U({f})`.
354/// Iterates the FULL ground set (including core), gates on the FULL
355/// budget B (not the residual after partial-core packing), and evaluates
356/// each candidate as a lone selection against an empty utility state.
357///
358/// This is the comparator that, together with the greedy chain, gives
359/// the `(1-1/e)/2` approximation guarantee from Khuller-Moss-Naor 1999
360/// for monotone submodular maximization under a knapsack constraint.
361/// Restricting H₁ to `non_core` with budget `B − cost(packed_core)`
362/// (the prior `find_best_singleton`) is a strict relaxation that
363/// excludes any single high-utility fragment with `|f| > B − β_core·B`.
364fn find_best_singleton_full_set(
365    fragments: &[Fragment],
366    budget_tokens: u32,
367    rel: &FxHashMap<FragmentId, f64>,
368    needs: &[InformationNeed],
369    empty_state: &UtilityState,
370) -> (Option<Fragment>, f64) {
371    let mut best = None;
372    let mut best_gain = 0.0;
373    for f in fragments {
374        if f.token_count == 0 || f.token_count > budget_tokens {
375            continue;
376        }
377        let gain = marginal_gain(
378            f,
379            rel.get(&f.id).copied().unwrap_or(0.0),
380            needs,
381            empty_state,
382        );
383        if gain > best_gain {
384            best_gain = gain;
385            best = Some(f.clone());
386        }
387    }
388    (best, best_gain)
389}
390
391fn init_selection_state(
392    core_ids: &FxHashSet<FragmentId>,
393    rel: &FxHashMap<FragmentId, f64>,
394    budget_tokens: u32,
395    file_importance: Option<&FxHashMap<Arc<str>, f64>>,
396) -> SelectionState {
397    let mut utility_state = UtilityState::default();
398    utility_state.r_cap = compute_r_cap(rel, Some(core_ids));
399    utility_state.changed_dirs = core_ids
400        .iter()
401        .filter_map(|cid| {
402            std::path::Path::new(cid.path.as_ref())
403                .parent()
404                .map(|p| p.to_path_buf())
405        })
406        .collect();
407    if let Some(fi) = file_importance {
408        utility_state.file_importance.clone_from(fi);
409    }
410    SelectionState {
411        selected: Vec::new(),
412        selected_ids: IntervalIndex::new(),
413        remaining_budget: budget_tokens,
414        utility_state,
415    }
416}
417
418fn run_greedy_loop_heap(
419    heap: &mut BinaryHeap<HeapEntry>,
420    id_to_frag: &FxHashMap<FragmentId, Fragment>,
421    state: &mut SelectionState,
422    rel: &FxHashMap<FragmentId, f64>,
423    needs: &[InformationNeed],
424    tau: f64,
425    _initial_budget: u32,
426) -> (usize, f64, usize) {
427    let mut current_version = 0u32;
428    let mut peak_density: f64 = 0.0;
429    let mut loop_iters: usize = 0;
430
431    while !heap.is_empty() && state.remaining_budget > 0 {
432        loop_iters += 1;
433        let (best_frag, best_density, new_version) = find_best_candidate_heap(
434            heap,
435            current_version,
436            id_to_frag,
437            &state.selected_ids,
438            state.remaining_budget,
439            rel,
440            needs,
441            &state.utility_state,
442        );
443        current_version = new_version;
444
445        let best_frag = match best_frag {
446            Some(f) => f,
447            None => break,
448        };
449        if best_density <= 0.0 {
450            break;
451        }
452
453        if best_density > peak_density {
454            peak_density = best_density;
455        } else if peak_density > 0.0 && best_density < tau * peak_density {
456            break;
457        }
458
459        state.selected.push(best_frag.clone());
460        state.selected_ids.add_id(&best_frag.id);
461        state.remaining_budget = state.remaining_budget.saturating_sub(best_frag.token_count);
462        let rel_score = rel.get(&best_frag.id).copied().unwrap_or(0.0);
463        apply_fragment(&best_frag, rel_score, needs, &mut state.utility_state);
464    }
465
466    let threshold = tau * peak_density;
467    (state.selected.len(), threshold, loop_iters)
468}
469
470fn setup_and_select_core(
471    fragments: &[Fragment],
472    core_ids: &FxHashSet<FragmentId>,
473    rel: &FxHashMap<FragmentId, f64>,
474    needs: &[InformationNeed],
475    budget_tokens: u32,
476    file_importance: Option<&FxHashMap<Arc<str>, f64>>,
477) -> (SelectionState, Vec<Fragment>, Vec<Fragment>, bool) {
478    let mut core_fragments: Vec<Fragment> = fragments
479        .iter()
480        .filter(|f| core_ids.contains(&f.id))
481        .cloned()
482        .collect();
483    core_fragments.sort_by(|a, b| {
484        let ta = if a.token_count > 0 {
485            a.token_count
486        } else {
487            SENTINEL_TOKEN_COUNT
488        };
489        let tb = if b.token_count > 0 {
490            b.token_count
491        } else {
492            SENTINEL_TOKEN_COUNT
493        };
494        ta.cmp(&tb)
495            .then(a.line_count().cmp(&b.line_count()))
496            .then(a.start_line().cmp(&b.start_line()))
497    });
498
499    let non_core_fragments: Vec<Fragment> = fragments
500        .iter()
501        .filter(|f| !core_ids.contains(&f.id))
502        .cloned()
503        .collect();
504
505    let sig_lookup = build_signature_lookup(fragments, &core_fragments);
506    let mut state = init_selection_state(core_ids, rel, budget_tokens, file_importance);
507    select_core_fragments(
508        &core_fragments,
509        rel,
510        needs,
511        &mut state,
512        budget_tokens,
513        &sig_lookup,
514    );
515
516    let selected_core_ids: FxHashSet<FragmentId> =
517        state.selected.iter().map(|f| f.id.clone()).collect();
518    let skipped_core: Vec<FragmentId> = core_ids
519        .iter()
520        .filter(|id| !selected_core_ids.contains(*id))
521        .cloned()
522        .collect();
523
524    let mut non_core_with_skipped = non_core_fragments;
525    if !skipped_core.is_empty() {
526        let skipped_set: FxHashSet<FragmentId> = skipped_core.into_iter().collect();
527        for cf in &core_fragments {
528            if skipped_set.contains(&cf.id) {
529                non_core_with_skipped.push(cf.clone());
530            }
531        }
532    }
533
534    let should_return_early = state.remaining_budget == 0;
535    let selected_copy = state.selected.clone();
536    (
537        state,
538        non_core_with_skipped,
539        selected_copy,
540        should_return_early,
541    )
542}
543
544pub fn lazy_greedy_select(
545    fragments: Vec<Fragment>,
546    core_ids: &FxHashSet<FragmentId>,
547    rel: &FxHashMap<FragmentId, f64>,
548    needs: &[InformationNeed],
549    budget_tokens: u32,
550    tau: f64,
551    file_importance: Option<&FxHashMap<Arc<str>, f64>>,
552) -> SelectionResult {
553    if fragments.is_empty() {
554        return SelectionResult {
555            selected: Vec::new(),
556            reason: SelectionReason::NoCandidates,
557            used_tokens: 0,
558            utility: 0.0,
559            greedy_iters: 0,
560            stopping_certificate: 0.0,
561        };
562    }
563
564    let (mut state, non_core_fragments, _selected_core, should_return_early) =
565        setup_and_select_core(
566            &fragments,
567            core_ids,
568            rel,
569            needs,
570            budget_tokens,
571            file_importance,
572        );
573
574    if should_return_early {
575        let used = budget_tokens - state.remaining_budget;
576        return SelectionResult {
577            selected: state.selected,
578            reason: SelectionReason::BudgetExhausted,
579            used_tokens: used,
580            utility: utility_value(&state.utility_state),
581            greedy_iters: 0,
582            stopping_certificate: 0.0,
583        };
584    }
585
586    let base_state = state.utility_state.copy();
587    let base_selected = state.selected.clone();
588    let base_budget = state.remaining_budget;
589
590    let candidates: Vec<Fragment> = non_core_fragments
591        .iter()
592        .filter(|f| !state.selected_ids.overlaps(f))
593        .cloned()
594        .collect();
595    let candidates = drop_redundant_signatures(&candidates, state.remaining_budget);
596
597    let mut id_to_frag: FxHashMap<FragmentId, Fragment> = FxHashMap::default();
598    let mut heap = build_initial_heap(
599        &candidates,
600        rel,
601        needs,
602        &state.utility_state,
603        &mut id_to_frag,
604    );
605
606    let (_, threshold, greedy_iters) = run_greedy_loop_heap(
607        &mut heap,
608        &id_to_frag,
609        &mut state,
610        rel,
611        needs,
612        tau,
613        budget_tokens,
614    );
615
616    let greedy_utility = utility_value(&state.utility_state);
617
618    let mut base_selected_ids = IntervalIndex::new();
619    for f in &base_selected {
620        base_selected_ids.add_id(&f.id);
621    }
622
623    let (best_singleton, best_gain) = find_best_singleton(
624        &non_core_fragments,
625        &base_selected_ids,
626        base_budget,
627        rel,
628        needs,
629        &base_state,
630    );
631
632    let empty_state = init_selection_state(core_ids, rel, budget_tokens, file_importance);
633    let (full_singleton, full_singleton_gain) = find_best_singleton_full_set(
634        &fragments,
635        budget_tokens,
636        rel,
637        needs,
638        &empty_state.utility_state,
639    );
640
641    let mut best_alt_utility = greedy_utility;
642    let mut best_alt: Option<(Vec<Fragment>, u32)> = None;
643
644    if let Some(ref singleton) = best_singleton {
645        let u = utility_value(&base_state) + best_gain;
646        if u > best_alt_utility {
647            best_alt_utility = u;
648            let used = budget_tokens - (base_budget - singleton.token_count);
649            let mut sel = base_selected.clone();
650            sel.push(singleton.clone());
651            best_alt = Some((sel, used));
652        }
653    }
654
655    if let Some(ref full) = full_singleton {
656        let u = utility_value(&empty_state.utility_state) + full_singleton_gain;
657        if u > best_alt_utility {
658            best_alt_utility = u;
659            best_alt = Some((vec![full.clone()], full.token_count));
660        }
661    }
662
663    if let Some((sel, used)) = best_alt {
664        return SelectionResult {
665            selected: sel,
666            reason: SelectionReason::BestSingleton,
667            used_tokens: used,
668            utility: best_alt_utility,
669            greedy_iters,
670            stopping_certificate: 0.0,
671        };
672    }
673
674    let used = budget_tokens - state.remaining_budget;
675    let reason = if state.remaining_budget == 0 {
676        SelectionReason::BudgetExhausted
677    } else if greedy_utility <= 0.0 {
678        SelectionReason::NoUtility
679    } else if state.selected.is_empty() || state.selected.len() == base_selected.len() {
680        SelectionReason::NoCandidates
681    } else if threshold > 0.0 && !heap.is_empty() {
682        SelectionReason::StoppedByTau
683    } else {
684        SelectionReason::NoCandidates
685    };
686
687    let stopping_certificate = if matches!(reason, SelectionReason::StoppedByTau) {
688        threshold * f64::from(state.remaining_budget)
689    } else {
690        0.0
691    };
692
693    SelectionResult {
694        selected: state.selected,
695        reason,
696        used_tokens: used,
697        utility: greedy_utility,
698        greedy_iters,
699        stopping_certificate,
700    }
701}