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    core_excerpts: Option<&FxHashMap<FragmentId, Fragment>>,
157) -> FxHashMap<FragmentId, Fragment> {
158    let mut sig_by_loc: FxHashMap<(Arc<str>, u32), Fragment> = FxHashMap::default();
159    for f in fragments {
160        if f.kind.is_signature() {
161            sig_by_loc.insert((f.id.path.clone(), f.start_line()), f.clone());
162        }
163    }
164    let mut sig_lookup = FxHashMap::default();
165    for cf in core_fragments {
166        let key = (cf.id.path.clone(), cf.start_line());
167        if let Some(sig) = sig_by_loc.get(&key) {
168            sig_lookup.insert(cf.id.clone(), sig.clone());
169            continue;
170        }
171        // Kinds without a signature (chunk, section) fall back to the excerpt
172        // around the hunk; without it an oversized core is skipped outright and
173        // the change signal disappears from the output (#103).
174        if let Some(excerpt) = core_excerpts.and_then(|e| e.get(&cf.id)) {
175            sig_lookup.insert(cf.id.clone(), excerpt.clone());
176        }
177    }
178    sig_lookup
179}
180
181fn select_core_fragments(
182    core_fragments: &[Fragment],
183    rel: &FxHashMap<FragmentId, f64>,
184    needs: &[InformationNeed],
185    state: &mut SelectionState,
186    budget_tokens: u32,
187    sig_lookup: &FxHashMap<FragmentId, Fragment>,
188) {
189    let core_budget = (budget_tokens as f64 * selection().core_budget_fraction) as u32;
190    // Counter for cores placed; the first pass keeps `core_used <= core_budget`,
191    // but the rescue pass below intentionally allows it to exceed `core_budget`
192    // up to `budget_tokens`. Don't assume the tighter bound past this scope.
193    let mut core_used = 0u32;
194
195    let mut sorted_core: Vec<&Fragment> = core_fragments.iter().collect();
196    sorted_core.sort_by(|a, b| {
197        let ra = rel.get(&a.id).copied().unwrap_or(0.0);
198        let rb = rel.get(&b.id).copied().unwrap_or(0.0);
199        rb.total_cmp(&ra)
200    });
201
202    let place_fragment =
203        |frag: &Fragment, core_used: &mut u32, state: &mut SelectionState, rel_score: f64| {
204            state.selected.push(frag.clone());
205            state.selected_ids.add_id(&frag.id);
206            state.remaining_budget = state.remaining_budget.saturating_sub(frag.token_count);
207            *core_used += frag.token_count;
208            apply_fragment(frag, rel_score, needs, &mut state.utility_state);
209        };
210
211    let mut skipped: Vec<&Fragment> = Vec::new();
212    for frag in &sorted_core {
213        if state.selected_ids.is_superset_of(frag) {
214            continue;
215        }
216        if core_used + frag.token_count > core_budget {
217            if let Some(sig) = sig_lookup.get(&frag.id) {
218                if !state.selected_ids.contains(&sig.id)
219                    && core_used + sig.token_count <= core_budget
220                {
221                    let rel_score = rel.get(&frag.id).copied().unwrap_or(0.0);
222                    place_fragment(sig, &mut core_used, state, rel_score);
223                    continue;
224                }
225            }
226            skipped.push(frag);
227            continue;
228        }
229
230        let rel_score = rel.get(&frag.id).copied().unwrap_or(0.0);
231        place_fragment(frag, &mut core_used, state, rel_score);
232    }
233
234    // Bug #2 fix: cores that didn't fit the core_budget reservation must not be
235    // demoted to ordinary greedy candidates without a chance to be placed first.
236    // Sweep skipped cores cheapest-first against the *full* remaining budget
237    // (not just the core slice) so seeds aren't dropped purely because the
238    // highest-relevance core happened to be heavy.
239    if !skipped.is_empty() {
240        skipped.sort_by(|a, b| a.token_count.cmp(&b.token_count));
241        for frag in skipped {
242            if state.remaining_budget == 0 {
243                break;
244            }
245            if state.selected_ids.is_superset_of(frag) {
246                continue;
247            }
248            if frag.token_count <= state.remaining_budget {
249                let rel_score = rel.get(&frag.id).copied().unwrap_or(0.0);
250                place_fragment(frag, &mut core_used, state, rel_score);
251            } else if let Some(sig) = sig_lookup.get(&frag.id) {
252                if !state.selected_ids.contains(&sig.id)
253                    && sig.token_count <= state.remaining_budget
254                {
255                    let rel_score = rel.get(&frag.id).copied().unwrap_or(0.0);
256                    place_fragment(sig, &mut core_used, state, rel_score);
257                }
258            }
259        }
260    }
261}
262
263fn build_initial_heap(
264    candidates: &[Fragment],
265    rel: &FxHashMap<FragmentId, f64>,
266    needs: &[InformationNeed],
267    state: &UtilityState,
268    id_to_frag: &mut FxHashMap<FragmentId, Fragment>,
269) -> BinaryHeap<HeapEntry> {
270    let mut heap = BinaryHeap::new();
271    for frag in candidates {
272        if frag.token_count > 0 {
273            let density = compute_density(
274                frag,
275                rel.get(&frag.id).copied().unwrap_or(0.0),
276                needs,
277                state,
278            );
279            heap.push(HeapEntry {
280                neg_density: -density,
281                frag_id: frag.id.clone(),
282                version: 0,
283            });
284            id_to_frag.insert(frag.id.clone(), frag.clone());
285        }
286    }
287    heap
288}
289
290fn find_best_candidate_heap(
291    heap: &mut BinaryHeap<HeapEntry>,
292    current_version: u32,
293    id_to_frag: &FxHashMap<FragmentId, Fragment>,
294    selected_ids: &IntervalIndex,
295    remaining_budget: u32,
296    rel: &FxHashMap<FragmentId, f64>,
297    needs: &[InformationNeed],
298    state: &UtilityState,
299) -> (Option<Fragment>, f64, u32) {
300    let cv = current_version;
301    while let Some(entry) = heap.pop() {
302        let frag = match id_to_frag.get(&entry.frag_id) {
303            Some(f) => f,
304            None => continue,
305        };
306        if frag.token_count > remaining_budget {
307            continue;
308        }
309        if selected_ids.overlaps(frag) {
310            continue;
311        }
312        if entry.version < cv {
313            let new_density = compute_density(
314                frag,
315                rel.get(&frag.id).copied().unwrap_or(0.0),
316                needs,
317                state,
318            );
319            heap.push(HeapEntry {
320                neg_density: -new_density,
321                frag_id: frag.id.clone(),
322                version: cv,
323            });
324            continue;
325        }
326        let actual_density = -entry.neg_density;
327        if actual_density <= 0.0 {
328            return (None, 0.0, cv);
329        }
330        return (Some(frag.clone()), actual_density, cv + 1);
331    }
332    (None, 0.0, cv)
333}
334
335fn find_best_singleton(
336    non_core: &[Fragment],
337    base_selected_ids: &IntervalIndex,
338    base_budget: u32,
339    rel: &FxHashMap<FragmentId, f64>,
340    needs: &[InformationNeed],
341    base_state: &UtilityState,
342) -> (Option<Fragment>, f64) {
343    let mut best_singleton = None;
344    let mut best_gain = 0.0;
345    for f in non_core {
346        if f.token_count > base_budget {
347            continue;
348        }
349        if base_selected_ids.overlaps(f) {
350            continue;
351        }
352        let gain = marginal_gain(f, rel.get(&f.id).copied().unwrap_or(0.0), needs, base_state);
353        if gain > best_gain {
354            best_gain = gain;
355            best_singleton = Some(f.clone());
356        }
357    }
358    (best_singleton, best_gain)
359}
360
361/// Paper-aligned strict Khuller H₁: `argmax_{f ∈ F, |f| ≤ B} U({f})`.
362/// Iterates the FULL ground set (including core), gates on the FULL
363/// budget B (not the residual after partial-core packing), and evaluates
364/// each candidate as a lone selection against an empty utility state.
365///
366/// This is the comparator that, together with the greedy chain, gives
367/// the `(1-1/e)/2` approximation guarantee from Khuller-Moss-Naor 1999
368/// for monotone submodular maximization under a knapsack constraint.
369/// Restricting H₁ to `non_core` with budget `B − cost(packed_core)`
370/// (the prior `find_best_singleton`) is a strict relaxation that
371/// excludes any single high-utility fragment with `|f| > B − β_core·B`.
372fn find_best_singleton_full_set(
373    fragments: &[Fragment],
374    budget_tokens: u32,
375    rel: &FxHashMap<FragmentId, f64>,
376    needs: &[InformationNeed],
377    empty_state: &UtilityState,
378) -> (Option<Fragment>, f64) {
379    let mut best = None;
380    let mut best_gain = 0.0;
381    for f in fragments {
382        if f.token_count == 0 || f.token_count > budget_tokens {
383            continue;
384        }
385        let gain = marginal_gain(
386            f,
387            rel.get(&f.id).copied().unwrap_or(0.0),
388            needs,
389            empty_state,
390        );
391        if gain > best_gain {
392            best_gain = gain;
393            best = Some(f.clone());
394        }
395    }
396    (best, best_gain)
397}
398
399fn init_selection_state(
400    core_ids: &FxHashSet<FragmentId>,
401    rel: &FxHashMap<FragmentId, f64>,
402    budget_tokens: u32,
403    file_importance: Option<&FxHashMap<Arc<str>, f64>>,
404) -> SelectionState {
405    let mut utility_state = UtilityState::default();
406    utility_state.r_cap = compute_r_cap(rel, Some(core_ids));
407    utility_state.changed_dirs = core_ids
408        .iter()
409        .filter_map(|cid| {
410            std::path::Path::new(cid.path.as_ref())
411                .parent()
412                .map(|p| p.to_path_buf())
413        })
414        .collect();
415    if let Some(fi) = file_importance {
416        utility_state.file_importance.clone_from(fi);
417    }
418    SelectionState {
419        selected: Vec::new(),
420        selected_ids: IntervalIndex::new(),
421        remaining_budget: budget_tokens,
422        utility_state,
423    }
424}
425
426fn run_greedy_loop_heap(
427    heap: &mut BinaryHeap<HeapEntry>,
428    id_to_frag: &FxHashMap<FragmentId, Fragment>,
429    state: &mut SelectionState,
430    rel: &FxHashMap<FragmentId, f64>,
431    needs: &[InformationNeed],
432    tau: f64,
433    _initial_budget: u32,
434) -> (usize, f64, usize) {
435    let mut current_version = 0u32;
436    let mut peak_density: f64 = 0.0;
437    let mut loop_iters: usize = 0;
438
439    while !heap.is_empty() && state.remaining_budget > 0 {
440        loop_iters += 1;
441        let (best_frag, best_density, new_version) = find_best_candidate_heap(
442            heap,
443            current_version,
444            id_to_frag,
445            &state.selected_ids,
446            state.remaining_budget,
447            rel,
448            needs,
449            &state.utility_state,
450        );
451        current_version = new_version;
452
453        let best_frag = match best_frag {
454            Some(f) => f,
455            None => break,
456        };
457        if best_density <= 0.0 {
458            break;
459        }
460
461        if best_density > peak_density {
462            peak_density = best_density;
463        } else if peak_density > 0.0 && best_density < tau * peak_density {
464            break;
465        }
466
467        state.selected.push(best_frag.clone());
468        state.selected_ids.add_id(&best_frag.id);
469        state.remaining_budget = state.remaining_budget.saturating_sub(best_frag.token_count);
470        let rel_score = rel.get(&best_frag.id).copied().unwrap_or(0.0);
471        apply_fragment(&best_frag, rel_score, needs, &mut state.utility_state);
472    }
473
474    let threshold = tau * peak_density;
475    (state.selected.len(), threshold, loop_iters)
476}
477
478fn setup_and_select_core(
479    fragments: &[Fragment],
480    core_ids: &FxHashSet<FragmentId>,
481    rel: &FxHashMap<FragmentId, f64>,
482    needs: &[InformationNeed],
483    budget_tokens: u32,
484    file_importance: Option<&FxHashMap<Arc<str>, f64>>,
485    core_excerpts: Option<&FxHashMap<FragmentId, Fragment>>,
486) -> (SelectionState, Vec<Fragment>, Vec<Fragment>, bool) {
487    let mut core_fragments: Vec<Fragment> = fragments
488        .iter()
489        .filter(|f| core_ids.contains(&f.id))
490        .cloned()
491        .collect();
492    core_fragments.sort_by(|a, b| {
493        let ta = if a.token_count > 0 {
494            a.token_count
495        } else {
496            SENTINEL_TOKEN_COUNT
497        };
498        let tb = if b.token_count > 0 {
499            b.token_count
500        } else {
501            SENTINEL_TOKEN_COUNT
502        };
503        ta.cmp(&tb)
504            .then(a.line_count().cmp(&b.line_count()))
505            .then(a.start_line().cmp(&b.start_line()))
506    });
507
508    let non_core_fragments: Vec<Fragment> = fragments
509        .iter()
510        .filter(|f| !core_ids.contains(&f.id))
511        .cloned()
512        .collect();
513
514    let sig_lookup = build_signature_lookup(fragments, &core_fragments, core_excerpts);
515    let mut state = init_selection_state(core_ids, rel, budget_tokens, file_importance);
516    select_core_fragments(
517        &core_fragments,
518        rel,
519        needs,
520        &mut state,
521        budget_tokens,
522        &sig_lookup,
523    );
524
525    let selected_core_ids: FxHashSet<FragmentId> =
526        state.selected.iter().map(|f| f.id.clone()).collect();
527    let skipped_core: Vec<FragmentId> = core_ids
528        .iter()
529        .filter(|id| !selected_core_ids.contains(*id))
530        .cloned()
531        .collect();
532
533    let mut non_core_with_skipped = non_core_fragments;
534    if !skipped_core.is_empty() {
535        let skipped_set: FxHashSet<FragmentId> = skipped_core.into_iter().collect();
536        for cf in &core_fragments {
537            if skipped_set.contains(&cf.id) {
538                non_core_with_skipped.push(cf.clone());
539            }
540        }
541    }
542
543    let should_return_early = state.remaining_budget == 0;
544    let selected_copy = state.selected.clone();
545    (
546        state,
547        non_core_with_skipped,
548        selected_copy,
549        should_return_early,
550    )
551}
552
553pub fn lazy_greedy_select(
554    fragments: Vec<Fragment>,
555    core_ids: &FxHashSet<FragmentId>,
556    rel: &FxHashMap<FragmentId, f64>,
557    needs: &[InformationNeed],
558    budget_tokens: u32,
559    tau: f64,
560    file_importance: Option<&FxHashMap<Arc<str>, f64>>,
561    core_excerpts: Option<&FxHashMap<FragmentId, Fragment>>,
562) -> SelectionResult {
563    if fragments.is_empty() {
564        return SelectionResult {
565            selected: Vec::new(),
566            reason: SelectionReason::NoCandidates,
567            used_tokens: 0,
568            utility: 0.0,
569            greedy_iters: 0,
570            stopping_certificate: 0.0,
571        };
572    }
573
574    let (mut state, non_core_fragments, _selected_core, should_return_early) =
575        setup_and_select_core(
576            &fragments,
577            core_ids,
578            rel,
579            needs,
580            budget_tokens,
581            file_importance,
582            core_excerpts,
583        );
584
585    if should_return_early {
586        let used = budget_tokens - state.remaining_budget;
587        return SelectionResult {
588            selected: state.selected,
589            reason: SelectionReason::BudgetExhausted,
590            used_tokens: used,
591            utility: utility_value(&state.utility_state),
592            greedy_iters: 0,
593            stopping_certificate: 0.0,
594        };
595    }
596
597    let base_state = state.utility_state.copy();
598    let base_selected = state.selected.clone();
599    let base_budget = state.remaining_budget;
600
601    let candidates: Vec<Fragment> = non_core_fragments
602        .iter()
603        .filter(|f| !state.selected_ids.overlaps(f))
604        .cloned()
605        .collect();
606    let candidates = drop_redundant_signatures(&candidates, state.remaining_budget);
607
608    let mut id_to_frag: FxHashMap<FragmentId, Fragment> = FxHashMap::default();
609    let mut heap = build_initial_heap(
610        &candidates,
611        rel,
612        needs,
613        &state.utility_state,
614        &mut id_to_frag,
615    );
616
617    let (_, threshold, greedy_iters) = run_greedy_loop_heap(
618        &mut heap,
619        &id_to_frag,
620        &mut state,
621        rel,
622        needs,
623        tau,
624        budget_tokens,
625    );
626
627    let greedy_utility = utility_value(&state.utility_state);
628
629    let mut base_selected_ids = IntervalIndex::new();
630    for f in &base_selected {
631        base_selected_ids.add_id(&f.id);
632    }
633
634    let (best_singleton, best_gain) = find_best_singleton(
635        &non_core_fragments,
636        &base_selected_ids,
637        base_budget,
638        rel,
639        needs,
640        &base_state,
641    );
642
643    let empty_state = init_selection_state(core_ids, rel, budget_tokens, file_importance);
644    let (full_singleton, full_singleton_gain) = find_best_singleton_full_set(
645        &fragments,
646        budget_tokens,
647        rel,
648        needs,
649        &empty_state.utility_state,
650    );
651
652    let mut best_alt_utility = greedy_utility;
653    let mut best_alt: Option<(Vec<Fragment>, u32)> = None;
654
655    if let Some(ref singleton) = best_singleton {
656        let u = utility_value(&base_state) + best_gain;
657        if u > best_alt_utility {
658            best_alt_utility = u;
659            let used = budget_tokens - (base_budget - singleton.token_count);
660            let mut sel = base_selected.clone();
661            sel.push(singleton.clone());
662            best_alt = Some((sel, used));
663        }
664    }
665
666    if let Some(ref full) = full_singleton {
667        let u = utility_value(&empty_state.utility_state) + full_singleton_gain;
668        if u > best_alt_utility {
669            best_alt_utility = u;
670            best_alt = Some((vec![full.clone()], full.token_count));
671        }
672    }
673
674    if let Some((sel, used)) = best_alt {
675        return SelectionResult {
676            selected: sel,
677            reason: SelectionReason::BestSingleton,
678            used_tokens: used,
679            utility: best_alt_utility,
680            greedy_iters,
681            stopping_certificate: 0.0,
682        };
683    }
684
685    let used = budget_tokens - state.remaining_budget;
686    let reason = if state.remaining_budget == 0 {
687        SelectionReason::BudgetExhausted
688    } else if greedy_utility <= 0.0 {
689        SelectionReason::NoUtility
690    } else if state.selected.is_empty() || state.selected.len() == base_selected.len() {
691        SelectionReason::NoCandidates
692    } else if threshold > 0.0 && !heap.is_empty() {
693        SelectionReason::StoppedByTau
694    } else {
695        SelectionReason::NoCandidates
696    };
697
698    let stopping_certificate = if matches!(reason, SelectionReason::StoppedByTau) {
699        threshold * f64::from(state.remaining_budget)
700    } else {
701        0.0
702    };
703
704    SelectionResult {
705        selected: state.selected,
706        reason,
707        used_tokens: used,
708        utility: greedy_utility,
709        greedy_iters,
710        stopping_certificate,
711    }
712}