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/// `used_tokens` is a reported contract, so it is derived from the returned
19/// selection rather than reconstructed from budget arithmetic that can drift
20/// away from what was actually placed.
21fn selection_cost(selected: &[Fragment]) -> u32 {
22    selected.iter().map(|f| f.token_count).sum()
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum SelectionReason {
27    TopK,
28    NoCandidates,
29    BudgetExhausted,
30    NoUtility,
31    StoppedByTau,
32    BestSingleton,
33}
34
35impl SelectionReason {
36    pub fn as_str(&self) -> &'static str {
37        match self {
38            Self::TopK => "topk",
39            Self::NoCandidates => "no_candidates",
40            Self::BudgetExhausted => "budget_exhausted",
41            Self::NoUtility => "no_utility",
42            Self::StoppedByTau => "stopped_by_tau",
43            Self::BestSingleton => "best_singleton",
44        }
45    }
46}
47
48pub struct SelectionResult {
49    pub selected: Vec<Fragment>,
50    pub reason: SelectionReason,
51    pub used_tokens: u32,
52    pub utility: f64,
53    /// Greedy iterations actually executed (number of `apply_fragment`
54    /// calls in `run_greedy_loop_heap`). Diagnoses lazy-heap blowup:
55    /// expected ≈ output size, pathological ≫ output size when
56    /// stale-version rejections dominate.
57    pub greedy_iters: usize,
58    /// Additive certificate for adaptive stopping: an upper bound
59    /// (`tau * peak_density * remaining_budget`) on the utility that
60    /// continuing the same greedy to the feasibility frontier could
61    /// still have added. 0 when the loop ended for any other reason.
62    pub stopping_certificate: f64,
63}
64
65struct HeapEntry {
66    neg_density: f64,
67    frag_id: FragmentId,
68    version: u32,
69}
70
71impl PartialEq for HeapEntry {
72    fn eq(&self, other: &Self) -> bool {
73        self.neg_density.to_bits() == other.neg_density.to_bits() && self.frag_id == other.frag_id
74    }
75}
76
77impl Eq for HeapEntry {}
78
79impl PartialOrd for HeapEntry {
80    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
81        Some(self.cmp(other))
82    }
83}
84
85impl Ord for HeapEntry {
86    fn cmp(&self, other: &Self) -> Ordering {
87        other
88            .neg_density
89            .total_cmp(&self.neg_density)
90            .then_with(|| other.frag_id.cmp(&self.frag_id))
91    }
92}
93
94struct SelectionState {
95    selected: Vec<Fragment>,
96    selected_ids: IntervalIndex,
97    remaining_budget: u32,
98    utility_state: UtilityState,
99}
100
101fn drop_redundant_signatures(candidates: &[Fragment], budget: u32) -> Vec<Fragment> {
102    let mut full_token_by_loc: FxHashMap<(Arc<str>, u32), u32> = FxHashMap::default();
103    for f in candidates {
104        if !f.kind.is_signature() {
105            // Keep the LARGEST co-located full fragment, not the last one seen.
106            // Two non-signature fragments can share a start line (a class header
107            // `Definition` at [10,12] and the full class at [10,300]), and with a
108            // plain `insert` whichever came last in the candidate vec won the
109            // slot. When the small header won, the class's stub was filtered out
110            // as "redundant" precisely when the full class did not fit and the
111            // stub was its only affordable representation — and the outcome
112            // depended on vec order rather than on anything meaningful.
113            full_token_by_loc
114                .entry((f.id.path.clone(), f.start_line()))
115                .and_modify(|t| *t = (*t).max(f.token_count))
116                .or_insert(f.token_count);
117        }
118    }
119    candidates
120        .iter()
121        .filter(|f| {
122            if !f.kind.is_signature() {
123                return true;
124            }
125            let key = (f.id.path.clone(), f.start_line());
126            full_token_by_loc
127                .get(&key)
128                .copied()
129                .unwrap_or(SENTINEL_TOKEN_COUNT)
130                > budget
131        })
132        .cloned()
133        .collect()
134}
135
136fn compute_r_cap(
137    rel: &FxHashMap<FragmentId, f64>,
138    core_ids: Option<&FxHashSet<FragmentId>>,
139) -> f64 {
140    let values: Vec<f64> = rel
141        .iter()
142        .filter(|(fid, v)| **v > 0.0 && core_ids.map_or(true, |c| !c.contains(*fid)))
143        .map(|(_, v)| *v)
144        .collect();
145
146    if values.len() < 2 {
147        return if let Some(&v) = values.first() {
148            v.max(selection().r_cap_min)
149        } else {
150            1.0
151        };
152    }
153
154    let mut sorted = values.clone();
155    sorted.sort_by(|a, b| a.total_cmp(b));
156    let mid = sorted.len() / 2;
157    let med = if sorted.len() % 2 == 0 {
158        (sorted[mid - 1] + sorted[mid]) / 2.0
159    } else {
160        sorted[mid]
161    };
162
163    let mean: f64 = values.iter().sum::<f64>() / values.len() as f64;
164    let variance: f64 =
165        values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (values.len() - 1) as f64;
166    let std = variance.sqrt();
167
168    (med + UTILITY.r_cap_sigma * std).max(1e-9)
169}
170
171fn build_signature_lookup(
172    fragments: &[Fragment],
173    core_fragments: &[Fragment],
174    core_excerpts: Option<&FxHashMap<FragmentId, Fragment>>,
175) -> FxHashMap<FragmentId, Fragment> {
176    let mut sig_by_loc: FxHashMap<(Arc<str>, u32), Fragment> = FxHashMap::default();
177    for f in fragments {
178        if f.kind.is_signature() {
179            sig_by_loc.insert((f.id.path.clone(), f.start_line()), f.clone());
180        }
181    }
182    let mut sig_lookup = FxHashMap::default();
183    for cf in core_fragments {
184        let key = (cf.id.path.clone(), cf.start_line());
185        if let Some(sig) = sig_by_loc.get(&key) {
186            sig_lookup.insert(cf.id.clone(), sig.clone());
187            continue;
188        }
189        // Kinds without a signature (chunk, section) fall back to the excerpt
190        // around the hunk; without it an oversized core is skipped outright and
191        // the change signal disappears from the output (#103).
192        if let Some(excerpt) = core_excerpts.and_then(|e| e.get(&cf.id)) {
193            sig_lookup.insert(cf.id.clone(), excerpt.clone());
194        }
195    }
196    sig_lookup
197}
198
199fn select_core_fragments(
200    core_fragments: &[Fragment],
201    rel: &FxHashMap<FragmentId, f64>,
202    needs: &[InformationNeed],
203    state: &mut SelectionState,
204    budget_tokens: u32,
205    sig_lookup: &FxHashMap<FragmentId, Fragment>,
206    core_excerpts: Option<&FxHashMap<FragmentId, Fragment>>,
207) -> FxHashSet<FragmentId> {
208    // Which cores came out represented — by themselves, by a signature stub, or
209    // by a downshifted excerpt. A substitute has its own id, so membership in
210    // the selection cannot answer this, and treating a substituted core as
211    // "skipped" hands the full fragment straight back to the greedy.
212    let mut satisfied: FxHashSet<FragmentId> = FxHashSet::default();
213    let sel_cfg = selection();
214    let core_budget = (budget_tokens as f64 * sel_cfg.core_budget_fraction) as u32;
215    // #194: while other cores still wait, one file may not eat more than its
216    // share. The rescue sweep below runs ceiling-free — leftovers no other
217    // file claimed flow back — so a single-file change never strands budget.
218    let file_ceiling = (budget_tokens as f64 * sel_cfg.per_file_budget_fraction) as u32;
219    let mut file_spent: FxHashMap<Arc<str>, u32> = FxHashMap::default();
220    // Counter for cores placed; the first pass keeps `core_used <= core_budget`,
221    // but the rescue pass below intentionally allows it to exceed `core_budget`
222    // up to `budget_tokens`. Don't assume the tighter bound past this scope.
223    let mut core_used = 0u32;
224
225    let mut sorted_core: Vec<&Fragment> = core_fragments.iter().collect();
226    sorted_core.sort_by(|a, b| {
227        let ra = rel.get(&a.id).copied().unwrap_or(0.0);
228        let rb = rel.get(&b.id).copied().unwrap_or(0.0);
229        rb.total_cmp(&ra)
230    });
231
232    let place_fragment = |frag: &Fragment,
233                          core_used: &mut u32,
234                          state: &mut SelectionState,
235                          rel_score: f64,
236                          file_spent: &mut FxHashMap<Arc<str>, u32>| {
237        state.selected.push(frag.clone());
238        state.selected_ids.add_id(&frag.id);
239        state.remaining_budget = state.remaining_budget.saturating_sub(frag.token_count);
240        *core_used += frag.token_count;
241        *file_spent.entry(frag.id.path.clone()).or_insert(0) += frag.token_count;
242        apply_fragment(frag, rel_score, needs, &mut state.utility_state);
243    };
244
245    // (originating core id, the fragment actually offered for it — the core
246    // itself or its downshifted excerpt).
247    let mut skipped: Vec<(FragmentId, &Fragment)> = Vec::new();
248    for frag in &sorted_core {
249        // Downshift before the budget is consulted, not only when it forces the
250        // issue. A core whose hunk window covers a small share of it is mostly
251        // unchanged context, and emitting it whole is the over-dump behind
252        // #105/#107/#149 — behaviour that otherwise flips purely on how much
253        // budget happens to be left.
254        let core_id = frag.id.clone();
255        let frag: &Fragment = core_excerpts
256            .and_then(|e| e.get(&frag.id))
257            .filter(|excerpt| crate::excerpt::is_downshift_worthwhile(frag, excerpt))
258            .unwrap_or(frag);
259        if state.selected_ids.is_superset_of(frag) {
260            satisfied.insert(core_id);
261            continue;
262        }
263        let spent = file_spent.get(&frag.id.path).copied().unwrap_or(0);
264        let over_file_ceiling = spent > 0 && spent + frag.token_count > file_ceiling;
265        if core_used + frag.token_count > core_budget || over_file_ceiling {
266            if let Some(sig) = sig_lookup.get(&core_id) {
267                if !state.selected_ids.contains(&sig.id)
268                    && core_used + sig.token_count <= core_budget
269                {
270                    let rel_score = rel.get(&core_id).copied().unwrap_or(0.0);
271                    place_fragment(sig, &mut core_used, state, rel_score, &mut file_spent);
272                    satisfied.insert(core_id);
273                    continue;
274                }
275            }
276            skipped.push((core_id, frag));
277            continue;
278        }
279
280        let rel_score = rel.get(&core_id).copied().unwrap_or(0.0);
281        place_fragment(frag, &mut core_used, state, rel_score, &mut file_spent);
282        satisfied.insert(core_id);
283    }
284
285    // Bug #2 fix: cores that didn't fit the core_budget reservation must not be
286    // demoted to ordinary greedy candidates without a chance to be placed first.
287    // Sweep skipped cores cheapest-first against the *full* remaining budget
288    // (not just the core slice) so seeds aren't dropped purely because the
289    // highest-relevance core happened to be heavy.
290    if !skipped.is_empty() {
291        skipped.sort_by(|(_, a), (_, b)| a.token_count.cmp(&b.token_count));
292        for (core_id, frag) in skipped {
293            if state.remaining_budget == 0 {
294                break;
295            }
296            if state.selected_ids.is_superset_of(frag) {
297                satisfied.insert(core_id);
298                continue;
299            }
300            let rel_score = rel.get(&core_id).copied().unwrap_or(0.0);
301            if frag.token_count <= state.remaining_budget {
302                place_fragment(frag, &mut core_used, state, rel_score, &mut file_spent);
303                satisfied.insert(core_id);
304            } else if let Some(sig) = sig_lookup.get(&core_id) {
305                if !state.selected_ids.contains(&sig.id)
306                    && sig.token_count <= state.remaining_budget
307                {
308                    place_fragment(sig, &mut core_used, state, rel_score, &mut file_spent);
309                    satisfied.insert(core_id);
310                }
311            }
312        }
313    }
314
315    satisfied
316}
317
318fn build_initial_heap(
319    candidates: &[Fragment],
320    rel: &FxHashMap<FragmentId, f64>,
321    needs: &[InformationNeed],
322    state: &UtilityState,
323    id_to_frag: &mut FxHashMap<FragmentId, Fragment>,
324) -> BinaryHeap<HeapEntry> {
325    let mut heap = BinaryHeap::new();
326    for frag in candidates {
327        if frag.token_count > 0 {
328            let density = compute_density(
329                frag,
330                rel.get(&frag.id).copied().unwrap_or(0.0),
331                needs,
332                state,
333            );
334            heap.push(HeapEntry {
335                neg_density: -density,
336                frag_id: frag.id.clone(),
337                version: 0,
338            });
339            id_to_frag.insert(frag.id.clone(), frag.clone());
340        }
341    }
342    heap
343}
344
345fn find_best_candidate_heap(
346    heap: &mut BinaryHeap<HeapEntry>,
347    current_version: u32,
348    id_to_frag: &FxHashMap<FragmentId, Fragment>,
349    selected_ids: &IntervalIndex,
350    remaining_budget: u32,
351    rel: &FxHashMap<FragmentId, f64>,
352    needs: &[InformationNeed],
353    state: &UtilityState,
354) -> (Option<Fragment>, f64, u32) {
355    let cv = current_version;
356    while let Some(entry) = heap.pop() {
357        let frag = match id_to_frag.get(&entry.frag_id) {
358            Some(f) => f,
359            None => continue,
360        };
361        if frag.token_count > remaining_budget {
362            continue;
363        }
364        if selected_ids.overlaps(frag) {
365            continue;
366        }
367        if entry.version < cv {
368            let new_density = compute_density(
369                frag,
370                rel.get(&frag.id).copied().unwrap_or(0.0),
371                needs,
372                state,
373            );
374            heap.push(HeapEntry {
375                neg_density: -new_density,
376                frag_id: frag.id.clone(),
377                version: cv,
378            });
379            continue;
380        }
381        let actual_density = -entry.neg_density;
382        if actual_density <= 0.0 {
383            return (None, 0.0, cv);
384        }
385        return (Some(frag.clone()), actual_density, cv + 1);
386    }
387    (None, 0.0, cv)
388}
389
390fn find_best_singleton(
391    non_core: &[Fragment],
392    base_selected_ids: &IntervalIndex,
393    base_budget: u32,
394    rel: &FxHashMap<FragmentId, f64>,
395    needs: &[InformationNeed],
396    base_state: &UtilityState,
397) -> (Option<Fragment>, f64) {
398    let mut best_singleton = None;
399    let mut best_gain = 0.0;
400    for f in non_core {
401        if f.token_count > base_budget {
402            continue;
403        }
404        if base_selected_ids.overlaps(f) {
405            continue;
406        }
407        let gain = marginal_gain(f, rel.get(&f.id).copied().unwrap_or(0.0), needs, base_state);
408        if gain > best_gain {
409            best_gain = gain;
410            best_singleton = Some(f.clone());
411        }
412    }
413    (best_singleton, best_gain)
414}
415
416/// Paper-aligned strict Khuller H₁: `argmax_{f ∈ F, |f| ≤ B} U({f})`.
417/// Iterates the FULL ground set (including core), gates on the FULL
418/// budget B (not the residual after partial-core packing), and evaluates
419/// each candidate as a lone selection against an empty utility state.
420///
421/// This is the comparator that, together with the greedy chain, gives
422/// the `(1-1/e)/2` approximation guarantee from Khuller-Moss-Naor 1999
423/// for monotone submodular maximization under a knapsack constraint.
424/// Restricting H₁ to `non_core` with budget `B − cost(packed_core)`
425/// (the prior `find_best_singleton`) is a strict relaxation that
426/// excludes any single high-utility fragment with `|f| > B − β_core·B`.
427fn find_best_singleton_full_set(
428    fragments: &[Fragment],
429    budget_tokens: u32,
430    rel: &FxHashMap<FragmentId, f64>,
431    needs: &[InformationNeed],
432    empty_state: &UtilityState,
433) -> (Option<Fragment>, f64) {
434    let mut best = None;
435    let mut best_gain = 0.0;
436    for f in fragments {
437        if f.token_count == 0 || f.token_count > budget_tokens {
438            continue;
439        }
440        let gain = marginal_gain(
441            f,
442            rel.get(&f.id).copied().unwrap_or(0.0),
443            needs,
444            empty_state,
445        );
446        if gain > best_gain {
447            best_gain = gain;
448            best = Some(f.clone());
449        }
450    }
451    (best, best_gain)
452}
453
454fn init_selection_state(
455    core_ids: &FxHashSet<FragmentId>,
456    rel: &FxHashMap<FragmentId, f64>,
457    budget_tokens: u32,
458    file_importance: Option<&FxHashMap<Arc<str>, f64>>,
459) -> SelectionState {
460    let mut utility_state = UtilityState::default();
461    utility_state.r_cap = compute_r_cap(rel, Some(core_ids));
462    utility_state.changed_dirs = core_ids
463        .iter()
464        .filter_map(|cid| {
465            std::path::Path::new(cid.path.as_ref())
466                .parent()
467                .map(|p| p.to_path_buf())
468        })
469        .collect();
470    if let Some(fi) = file_importance {
471        utility_state.file_importance.clone_from(fi);
472    }
473    SelectionState {
474        selected: Vec::new(),
475        selected_ids: IntervalIndex::new(),
476        remaining_budget: budget_tokens,
477        utility_state,
478    }
479}
480
481#[allow(clippy::too_many_arguments)]
482fn run_greedy_loop_heap(
483    heap: &mut BinaryHeap<HeapEntry>,
484    id_to_frag: &FxHashMap<FragmentId, Fragment>,
485    state: &mut SelectionState,
486    rel: &FxHashMap<FragmentId, f64>,
487    needs: &[InformationNeed],
488    tau: f64,
489    _initial_budget: u32,
490    admissible_files: Option<&FxHashSet<Arc<str>>>,
491) -> (usize, f64, usize) {
492    let mut current_version = 0u32;
493    let mut peak_density: f64 = 0.0;
494    let mut loop_iters: usize = 0;
495    // Per-file admission (#65): opening a NEW file requires it to be
496    // naming-reachable from the changed set; fragments of files already
497    // opened (including the cores selected before this loop) compete on
498    // density as always. Inadmissible candidates are discarded, not
499    // deferred — admissibility is static within a run.
500    let mut open_files: FxHashSet<Arc<str>> =
501        state.selected.iter().map(|f| f.id.path.clone()).collect();
502    // #194 per-file ceiling: while the heap still holds other candidates, one
503    // file may not exceed its budget share. Blocked candidates are deferred,
504    // not dropped — once everyone else has had their chance the ceiling lifts
505    // (phase 2), so leftovers still flow to the highest density and a
506    // single-file run is unaffected.
507    let file_ceiling = (_initial_budget as f64 * selection().per_file_budget_fraction) as u32;
508    let mut file_spent: FxHashMap<Arc<str>, u32> = FxHashMap::default();
509    for f in &state.selected {
510        *file_spent.entry(f.id.path.clone()).or_insert(0) += f.token_count;
511    }
512    let mut deferred: Vec<HeapEntry> = Vec::new();
513    let mut ceiling_active = true;
514
515    loop {
516        while !heap.is_empty() && state.remaining_budget > 0 {
517            loop_iters += 1;
518            let (best_frag, best_density, new_version) = find_best_candidate_heap(
519                heap,
520                current_version,
521                id_to_frag,
522                &state.selected_ids,
523                state.remaining_budget,
524                rel,
525                needs,
526                &state.utility_state,
527            );
528            current_version = new_version;
529
530            let best_frag = match best_frag {
531                Some(f) => f,
532                None => break,
533            };
534            if best_density <= 0.0 {
535                break;
536            }
537
538            if let Some(admissible) = admissible_files {
539                if !open_files.contains(&best_frag.id.path)
540                    && !admissible.contains(&best_frag.id.path)
541                {
542                    continue;
543                }
544            }
545
546            if ceiling_active {
547                let spent = file_spent.get(&best_frag.id.path).copied().unwrap_or(0);
548                if spent > 0 && spent + best_frag.token_count > file_ceiling {
549                    deferred.push(HeapEntry {
550                        neg_density: -best_density,
551                        frag_id: best_frag.id.clone(),
552                        version: current_version,
553                    });
554                    continue;
555                }
556            }
557
558            if best_density > peak_density {
559                peak_density = best_density;
560            } else if peak_density > 0.0 && best_density < tau * peak_density {
561                break;
562            }
563
564            open_files.insert(best_frag.id.path.clone());
565            *file_spent.entry(best_frag.id.path.clone()).or_insert(0) += best_frag.token_count;
566            state.selected.push(best_frag.clone());
567            state.selected_ids.add_id(&best_frag.id);
568            state.remaining_budget = state.remaining_budget.saturating_sub(best_frag.token_count);
569            let rel_score = rel.get(&best_frag.id).copied().unwrap_or(0.0);
570            apply_fragment(&best_frag, rel_score, needs, &mut state.utility_state);
571        }
572
573        if ceiling_active && !deferred.is_empty() && state.remaining_budget > 0 {
574            ceiling_active = false;
575            for e in deferred.drain(..) {
576                heap.push(e);
577            }
578            continue;
579        }
580        break;
581    }
582
583    let threshold = tau * peak_density;
584    (state.selected.len(), threshold, loop_iters)
585}
586
587fn setup_and_select_core(
588    fragments: &[Fragment],
589    core_ids: &FxHashSet<FragmentId>,
590    rel: &FxHashMap<FragmentId, f64>,
591    needs: &[InformationNeed],
592    budget_tokens: u32,
593    file_importance: Option<&FxHashMap<Arc<str>, f64>>,
594    core_excerpts: Option<&FxHashMap<FragmentId, Fragment>>,
595) -> (SelectionState, Vec<Fragment>, Vec<Fragment>, bool) {
596    let mut core_fragments: Vec<Fragment> = fragments
597        .iter()
598        .filter(|f| core_ids.contains(&f.id))
599        .cloned()
600        .collect();
601    core_fragments.sort_by(|a, b| {
602        let ta = if a.token_count > 0 {
603            a.token_count
604        } else {
605            SENTINEL_TOKEN_COUNT
606        };
607        let tb = if b.token_count > 0 {
608            b.token_count
609        } else {
610            SENTINEL_TOKEN_COUNT
611        };
612        ta.cmp(&tb)
613            .then(a.line_count().cmp(&b.line_count()))
614            .then(a.start_line().cmp(&b.start_line()))
615    });
616
617    let non_core_fragments: Vec<Fragment> = fragments
618        .iter()
619        .filter(|f| !core_ids.contains(&f.id))
620        .cloned()
621        .collect();
622
623    let sig_lookup = build_signature_lookup(fragments, &core_fragments, core_excerpts);
624    let mut state = init_selection_state(core_ids, rel, budget_tokens, file_importance);
625    let satisfied_core_ids = select_core_fragments(
626        &core_fragments,
627        rel,
628        needs,
629        &mut state,
630        budget_tokens,
631        &sig_lookup,
632        core_excerpts,
633    );
634
635    // A core represented by a substitute (signature stub or downshifted
636    // excerpt) is satisfied even though its own id is absent from the
637    // selection — offering the full fragment back to the greedy would undo the
638    // substitution.
639    let skipped_core: Vec<FragmentId> = core_ids
640        .iter()
641        .filter(|id| !satisfied_core_ids.contains(*id))
642        .cloned()
643        .collect();
644
645    let mut non_core_with_skipped = non_core_fragments;
646    if !skipped_core.is_empty() {
647        let skipped_set: FxHashSet<FragmentId> = skipped_core.into_iter().collect();
648        for cf in &core_fragments {
649            if skipped_set.contains(&cf.id) {
650                non_core_with_skipped.push(cf.clone());
651            }
652        }
653    }
654
655    let should_return_early = state.remaining_budget == 0;
656    let selected_copy = state.selected.clone();
657    (
658        state,
659        non_core_with_skipped,
660        selected_copy,
661        should_return_early,
662    )
663}
664
665pub fn lazy_greedy_select(
666    fragments: Vec<Fragment>,
667    core_ids: &FxHashSet<FragmentId>,
668    rel: &FxHashMap<FragmentId, f64>,
669    needs: &[InformationNeed],
670    budget_tokens: u32,
671    tau: f64,
672    file_importance: Option<&FxHashMap<Arc<str>, f64>>,
673    core_excerpts: Option<&FxHashMap<FragmentId, Fragment>>,
674    admissible_files: Option<&FxHashSet<Arc<str>>>,
675) -> SelectionResult {
676    if fragments.is_empty() {
677        return SelectionResult {
678            selected: Vec::new(),
679            reason: SelectionReason::NoCandidates,
680            used_tokens: 0,
681            utility: 0.0,
682            greedy_iters: 0,
683            stopping_certificate: 0.0,
684        };
685    }
686
687    let (mut state, non_core_fragments, _selected_core, should_return_early) =
688        setup_and_select_core(
689            &fragments,
690            core_ids,
691            rel,
692            needs,
693            budget_tokens,
694            file_importance,
695            core_excerpts,
696        );
697
698    if should_return_early {
699        let used = budget_tokens - state.remaining_budget;
700        return SelectionResult {
701            selected: state.selected,
702            reason: SelectionReason::BudgetExhausted,
703            used_tokens: used,
704            utility: utility_value(&state.utility_state),
705            greedy_iters: 0,
706            stopping_certificate: 0.0,
707        };
708    }
709
710    let base_state = state.utility_state.copy();
711    let base_selected = state.selected.clone();
712    let base_budget = state.remaining_budget;
713
714    let candidates: Vec<Fragment> = non_core_fragments
715        .iter()
716        .filter(|f| !state.selected_ids.overlaps(f))
717        .cloned()
718        .collect();
719    let candidates = drop_redundant_signatures(&candidates, state.remaining_budget);
720
721    let mut id_to_frag: FxHashMap<FragmentId, Fragment> = FxHashMap::default();
722    let mut heap = build_initial_heap(
723        &candidates,
724        rel,
725        needs,
726        &state.utility_state,
727        &mut id_to_frag,
728    );
729
730    let (_, threshold, greedy_iters) = run_greedy_loop_heap(
731        &mut heap,
732        &id_to_frag,
733        &mut state,
734        rel,
735        needs,
736        tau,
737        budget_tokens,
738        admissible_files,
739    );
740
741    let greedy_utility = utility_value(&state.utility_state);
742
743    let mut base_selected_ids = IntervalIndex::new();
744    for f in &base_selected {
745        base_selected_ids.add_id(&f.id);
746    }
747
748    let (best_singleton, best_gain) = find_best_singleton(
749        &non_core_fragments,
750        &base_selected_ids,
751        base_budget,
752        rel,
753        needs,
754        &base_state,
755    );
756
757    let empty_state = init_selection_state(core_ids, rel, budget_tokens, file_importance);
758    let (full_singleton, full_singleton_gain) = find_best_singleton_full_set(
759        &fragments,
760        budget_tokens,
761        rel,
762        needs,
763        &empty_state.utility_state,
764    );
765
766    let mut best_alt_utility = greedy_utility;
767    let mut best_alt: Option<(u32, Vec<Fragment>)> = None;
768
769    if let Some(ref singleton) = best_singleton {
770        let u = utility_value(&base_state) + best_gain;
771        if u > best_alt_utility {
772            best_alt_utility = u;
773            let mut sel = base_selected.clone();
774            sel.push(singleton.clone());
775            best_alt = Some((selection_cost(&sel), sel));
776        }
777    }
778
779    if let Some(ref full) = full_singleton {
780        let u = utility_value(&empty_state.utility_state) + full_singleton_gain;
781        // Additive on top of the core selection, never a replacement for it.
782        // This branch used to return `vec![full]` outright, so a single heavy
783        // fragment whose standalone utility beat the greedy chain's discarded
784        // every changed-code fragment — the one thing the output exists to
785        // carry. `ensure_changed_files_represented` could not reliably undo it
786        // either: it only had `budget - full.token_count` left and only picks a
787        // fragment that fits. The two utilities are also measured from
788        // different baselines (this one from an empty state, `greedy_utility`
789        // from the core base), so the comparison can only ever be a heuristic
790        // nudge — not grounds for dropping the core.
791        //
792        // H₁ iterates the FULL ground set, so its winner can be a core the
793        // core pass already packed. Then this arm has nothing to add: its
794        // "alternative" is `base_selected` verbatim, a strict subset of the
795        // greedy result. Utility is monotone and `greedy_utility` already
796        // contains that core's contribution, so `u > best_alt_utility` should
797        // be unreachable in that case — this makes the reasoning a condition
798        // rather than an assumption, because the arm's cost accounting has no
799        // meaning when nothing is appended.
800        let already_selected = base_selected.iter().any(|f| f.id == full.id);
801        if u > best_alt_utility && full.token_count <= base_budget && !already_selected {
802            best_alt_utility = u;
803            let mut sel = base_selected.clone();
804            sel.push(full.clone());
805            best_alt = Some((selection_cost(&sel), sel));
806        }
807    }
808
809    if let Some((used, sel)) = best_alt {
810        return SelectionResult {
811            selected: sel,
812            reason: SelectionReason::BestSingleton,
813            used_tokens: used,
814            utility: best_alt_utility,
815            greedy_iters,
816            stopping_certificate: 0.0,
817        };
818    }
819
820    let used = budget_tokens - state.remaining_budget;
821    let reason = if state.remaining_budget == 0 {
822        SelectionReason::BudgetExhausted
823    } else if greedy_utility <= 0.0 {
824        SelectionReason::NoUtility
825    } else if state.selected.is_empty() || state.selected.len() == base_selected.len() {
826        SelectionReason::NoCandidates
827    } else if threshold > 0.0 && !heap.is_empty() {
828        SelectionReason::StoppedByTau
829    } else {
830        SelectionReason::NoCandidates
831    };
832
833    let stopping_certificate = if matches!(reason, SelectionReason::StoppedByTau) {
834        threshold * f64::from(state.remaining_budget)
835    } else {
836        0.0
837    };
838
839    SelectionResult {
840        selected: state.selected,
841        reason,
842        used_tokens: used,
843        utility: greedy_utility,
844        greedy_iters,
845        stopping_certificate,
846    }
847}
848
849#[cfg(test)]
850mod tests {
851    use super::*;
852    use crate::types::FragmentKind;
853
854    fn frag(path: &str, start: u32, end: u32, kind: FragmentKind, tokens: u32) -> Fragment {
855        let mut identifiers = FxHashSet::default();
856        identifiers.insert(format!("sym_{path}_{start}"));
857        Fragment {
858            id: FragmentId::new(Arc::from(path), start, end),
859            kind,
860            content: Arc::from(format!("// {path}:{start}-{end}\n")),
861            identifiers,
862            token_count: tokens,
863            symbol_name: Some(format!("sym_{start}")),
864        }
865    }
866
867    fn rel_map(frags: &[Fragment], score: f64) -> FxHashMap<FragmentId, f64> {
868        frags.iter().map(|f| (f.id.clone(), score)).collect()
869    }
870
871    fn cost_of(selected: &[Fragment]) -> u32 {
872        selected.iter().map(|f| f.token_count).sum()
873    }
874
875    /// The budget is a hard contract (`cost(C) <= B`); four separate call sites
876    /// gate on it and none of them was asserted. A `pick_smallest_fitting` that
877    /// returns a non-fitting candidate is one `if` away, and the oracle corpus
878    /// can never catch it because its budget is always >=2.5x the whole repo.
879    #[test]
880    fn selection_never_exceeds_the_budget() {
881        let frags: Vec<Fragment> = (0..12)
882            .map(|i| {
883                frag(
884                    "a.rs",
885                    1 + i * 50,
886                    40 + i * 50,
887                    FragmentKind::Function,
888                    30 + i * 17,
889                )
890            })
891            .collect();
892        let core: FxHashSet<FragmentId> = std::iter::once(frags[0].id.clone()).collect();
893        let rel = rel_map(&frags, 0.7);
894
895        for budget in [1u32, 7, 30, 31, 60, 200, 1_000] {
896            let result = lazy_greedy_select(
897                frags.clone(),
898                &core,
899                &rel,
900                &[],
901                budget,
902                0.12,
903                None,
904                None,
905                None,
906            );
907            assert!(
908                cost_of(&result.selected) <= budget,
909                "budget {budget} overrun: cost {} via {:?}",
910                cost_of(&result.selected),
911                result.reason
912            );
913            assert!(
914                result.used_tokens <= budget,
915                "reported used_tokens {} exceeds budget {budget}",
916                result.used_tokens
917            );
918        }
919    }
920
921    /// `used_tokens` is what every downstream budget report reads, and it was
922    /// only ever asserted as `<= budget`. Three code paths compute it by
923    /// different budget arithmetic; this pins the equality they all have to
924    /// satisfy, so a future path that reconstructs the figure instead of
925    /// measuring the selection fails here rather than in a results table.
926    #[test]
927    fn reported_used_tokens_always_equals_the_cost_of_the_returned_selection() {
928        let shapes: Vec<Vec<Fragment>> = vec![
929            vec![
930                frag("changed.rs", 1, 8, FragmentKind::Function, 20),
931                frag("other.rs", 1, 400, FragmentKind::Class, 900),
932                frag("other.rs", 500, 520, FragmentKind::Function, 40),
933            ],
934            // A lone, heavy core: H₁ over the full ground set can only win with
935            // a fragment the core pass already placed.
936            vec![frag("changed.rs", 1, 200, FragmentKind::Class, 400)],
937            (0..6)
938                .map(|i| {
939                    frag(
940                        "a.rs",
941                        1 + i * 20,
942                        10 + i * 20,
943                        FragmentKind::Function,
944                        20 + i * 60,
945                    )
946                })
947                .collect(),
948        ];
949
950        for frags in shapes {
951            let core: FxHashSet<FragmentId> = std::iter::once(frags[0].id.clone()).collect();
952            let rel = rel_map(&frags, 0.8);
953            for budget in [50u32, 120, 460, 1_000, 5_000] {
954                let result = lazy_greedy_select(
955                    frags.clone(),
956                    &core,
957                    &rel,
958                    &[],
959                    budget,
960                    0.12,
961                    None,
962                    None,
963                    None,
964                );
965                assert_eq!(
966                    result.used_tokens,
967                    cost_of(&result.selected),
968                    "reason {:?} at budget {budget}: reported {} but selection costs {}",
969                    result.reason,
970                    result.used_tokens,
971                    cost_of(&result.selected)
972                );
973            }
974        }
975    }
976
977    /// A core that is mostly unchanged must be placed as its hunk-window
978    /// excerpt, not in full — the over-dump behind #105/#107/#149. The excerpt
979    /// arrives through `core_excerpts`, keyed by the core it replaces.
980    #[test]
981    fn a_mostly_unchanged_core_is_placed_as_its_excerpt() {
982        let core = frag("script.sh", 1, 122, FragmentKind::Chunk, 600);
983        let excerpt = frag("script.sh", 58, 64, FragmentKind::Excerpt, 40);
984        let core_ids: FxHashSet<FragmentId> = std::iter::once(core.id.clone()).collect();
985        let rel = rel_map(&[core.clone()], 1.0);
986        let mut excerpts: FxHashMap<FragmentId, Fragment> = FxHashMap::default();
987        excerpts.insert(core.id.clone(), excerpt.clone());
988
989        let result = lazy_greedy_select(
990            vec![core.clone()],
991            &core_ids,
992            &rel,
993            &[],
994            8_000,
995            0.12,
996            None,
997            Some(&excerpts),
998            None,
999        );
1000
1001        let ids: Vec<String> = result
1002            .selected
1003            .iter()
1004            .map(|f| format!("{}:{}-{}", f.id.path, f.id.start_line, f.id.end_line))
1005            .collect();
1006        assert!(
1007            result.selected.iter().any(|f| f.id == excerpt.id),
1008            "core was not downshifted to its excerpt: {ids:?}"
1009        );
1010        assert!(
1011            !result.selected.iter().any(|f| f.id == core.id),
1012            "the full core was emitted alongside the excerpt: {ids:?}"
1013        );
1014    }
1015
1016    #[test]
1017    fn selected_fragments_never_overlap_and_are_never_duplicated() {
1018        let frags: Vec<Fragment> = (0..8)
1019            .map(|i| frag("a.rs", 1 + i * 10, 12 + i * 10, FragmentKind::Function, 25))
1020            .collect();
1021        let core: FxHashSet<FragmentId> = std::iter::once(frags[0].id.clone()).collect();
1022        let rel = rel_map(&frags, 0.9);
1023        let result = lazy_greedy_select(frags, &core, &rel, &[], 400, 0.12, None, None, None);
1024
1025        let ids: FxHashSet<FragmentId> = result.selected.iter().map(|f| f.id.clone()).collect();
1026        assert_eq!(
1027            ids.len(),
1028            result.selected.len(),
1029            "duplicate fragment selected"
1030        );
1031    }
1032
1033    /// `find_best_singleton_full_set` used to return `vec![full]`, discarding
1034    /// every core fragment. The core IS the changed code, so a selection that
1035    /// drops all of it answers a different question than the one asked.
1036    #[test]
1037    fn a_winning_singleton_never_evicts_the_core_selection() {
1038        // A heavy, highly relevant non-core fragment is the shape that makes the
1039        // full-set singleton win.
1040        let core_frag = frag("changed.rs", 1, 8, FragmentKind::Function, 20);
1041        let heavy = frag("other.rs", 1, 400, FragmentKind::Class, 900);
1042        let filler = frag("other.rs", 500, 520, FragmentKind::Function, 40);
1043        let frags = vec![core_frag.clone(), heavy.clone(), filler];
1044
1045        let core: FxHashSet<FragmentId> = std::iter::once(core_frag.id.clone()).collect();
1046        let mut rel = FxHashMap::default();
1047        rel.insert(core_frag.id.clone(), 0.05);
1048        rel.insert(heavy.id.clone(), 1.0);
1049        rel.insert(frags[2].id.clone(), 0.1);
1050
1051        let result = lazy_greedy_select(frags, &core, &rel, &[], 2_000, 0.12, None, None, None);
1052        assert!(
1053            result.selected.iter().any(|f| core.contains(&f.id)),
1054            "no core fragment survived; reason was {:?}",
1055            result.reason
1056        );
1057        assert!(cost_of(&result.selected) <= 2_000);
1058    }
1059
1060    #[test]
1061    fn empty_ground_set_reports_no_candidates() {
1062        let result = lazy_greedy_select(
1063            Vec::new(),
1064            &FxHashSet::default(),
1065            &FxHashMap::default(),
1066            &[],
1067            1_000,
1068            0.12,
1069            None,
1070            None,
1071            None,
1072        );
1073        assert!(result.selected.is_empty());
1074        assert_eq!(result.reason, SelectionReason::NoCandidates);
1075        assert_eq!(result.used_tokens, 0);
1076    }
1077
1078    /// Keyed on `(path, start_line)`, this used to be last-write-wins, so a
1079    /// small co-located sibling could delete the stub that was the only
1080    /// affordable representation of an oversized fragment.
1081    #[test]
1082    fn drop_redundant_signatures_is_independent_of_candidate_order() {
1083        let header = frag("a.rs", 10, 12, FragmentKind::Definition, 40);
1084        let whole = frag("a.rs", 10, 300, FragmentKind::Class, 4_000);
1085        let stub = frag("a.rs", 10, 11, FragmentKind::ClassSignature, 15);
1086
1087        let forward =
1088            drop_redundant_signatures(&[header.clone(), whole.clone(), stub.clone()], 500);
1089        let backward = drop_redundant_signatures(&[whole, header, stub], 500);
1090
1091        let kinds = |v: &[Fragment]| -> Vec<FragmentKind> { v.iter().map(|f| f.kind).collect() };
1092        assert!(
1093            kinds(&forward).contains(&FragmentKind::ClassSignature),
1094            "the stub for an unaffordable class was dropped: {:?}",
1095            kinds(&forward)
1096        );
1097        let mut a: Vec<FragmentKind> = kinds(&forward);
1098        let mut b: Vec<FragmentKind> = kinds(&backward);
1099        a.sort_by_key(|k| format!("{k:?}"));
1100        b.sort_by_key(|k| format!("{k:?}"));
1101        assert_eq!(a, b, "verdict depended on candidate order");
1102    }
1103
1104    #[test]
1105    fn drop_redundant_signatures_removes_a_stub_whose_full_fragment_fits() {
1106        let whole = frag("a.rs", 10, 40, FragmentKind::Class, 100);
1107        let stub = frag("a.rs", 10, 11, FragmentKind::ClassSignature, 15);
1108        let kept = drop_redundant_signatures(&[whole, stub], 500);
1109        assert!(
1110            !kept.iter().any(|f| f.kind.is_signature()),
1111            "stub survived even though the full fragment fits the budget"
1112        );
1113    }
1114    /// tau is the adaptive stop: once a candidate's density falls below
1115    /// `tau * peak_density` the loop stops instead of spending the rest of the
1116    /// budget. The oracle corpus used to run at tau=0.0, which made the
1117    /// predicate unreachable, so deleting the rule failed no test; the corpus
1118    /// now runs at the shipped default too (#175). This keeps a direct
1119    /// assertion on the rule that does not depend on corpus wiring.
1120    #[test]
1121    fn tau_stops_the_greedy_loop_before_the_budget_is_spent() {
1122        // Descending relevance against escalating cost gives sharply
1123        // descending density, which is what the stop rule reacts to.
1124        let frags: Vec<Fragment> = (0..6)
1125            .map(|i| {
1126                frag(
1127                    "a.rs",
1128                    1 + i * 20,
1129                    10 + i * 20,
1130                    FragmentKind::Function,
1131                    20 + i * i * 120,
1132                )
1133            })
1134            .collect();
1135        let core: FxHashSet<FragmentId> = std::iter::once(frags[0].id.clone()).collect();
1136        let mut rel = FxHashMap::default();
1137        // Geometric decay: with escalating cost the density ratio between
1138        // consecutive candidates falls below 5% by the tail, so the rule
1139        // fires at the shipped tau (0.05) and not only at looser settings.
1140        for (i, f) in frags.iter().enumerate() {
1141            rel.insert(f.id.clone(), 0.25f64.powi(i as i32).max(1e-6));
1142        }
1143
1144        let budget = 10_000;
1145        let default_tau = lazy_greedy_select(
1146            frags.clone(),
1147            &core,
1148            &rel,
1149            &[],
1150            budget,
1151            crate::config::limits::DEFAULT_STOPPING_THRESHOLD,
1152            None,
1153            None,
1154            None,
1155        );
1156        let no_tau = lazy_greedy_select(frags, &core, &rel, &[], budget, 0.0, None, None, None);
1157
1158        assert_eq!(
1159            default_tau.reason,
1160            SelectionReason::StoppedByTau,
1161            "the adaptive stop did not fire at the shipped default"
1162        );
1163        assert!(
1164            default_tau.selected.len() < no_tau.selected.len(),
1165            "tau={} selected {} fragments, same as tau=0.0 — the rule is inert",
1166            crate::config::limits::DEFAULT_STOPPING_THRESHOLD,
1167            default_tau.selected.len()
1168        );
1169        assert!(
1170            default_tau.used_tokens < no_tau.used_tokens,
1171            "the stop saved no budget: {} vs {}",
1172            default_tau.used_tokens,
1173            no_tau.used_tokens
1174        );
1175        assert!(
1176            default_tau.stopping_certificate > 0.0,
1177            "StoppedByTau must carry a positive certificate"
1178        );
1179        assert_eq!(
1180            no_tau.stopping_certificate, 0.0,
1181            "tau=0.0 cannot produce a stopping certificate"
1182        );
1183    }
1184
1185    /// #194: with competitors waiting, one file may not monopolize the budget
1186    /// — but once every other file has had its chance, the ceiling lifts and
1187    /// leftovers flow back, so a lone file still fills the budget.
1188    #[test]
1189    fn per_file_ceiling_blocks_monopoly_but_releases_leftovers() {
1190        // One "blob" file with many equal fragments vs two small files.
1191        let mut frags: Vec<Fragment> = (0..20)
1192            .map(|i| {
1193                frag(
1194                    "blob.json",
1195                    i * 10 + 1,
1196                    i * 10 + 9,
1197                    FragmentKind::Chunk,
1198                    100,
1199                )
1200            })
1201            .collect();
1202        frags.push(frag("a.rs", 1, 9, FragmentKind::Function, 100));
1203        frags.push(frag("b.rs", 1, 9, FragmentKind::Function, 100));
1204        let core: FxHashSet<FragmentId> = FxHashSet::default();
1205        let mut rel: FxHashMap<FragmentId, f64> = FxHashMap::default();
1206        // Blob fragments outrank the small files.
1207        for f in &frags {
1208            let w = if f.id.path.as_ref() == "blob.json" {
1209                1.0
1210            } else {
1211                0.5
1212            };
1213            rel.insert(f.id.clone(), w);
1214        }
1215        let budget = 1_000u32; // ceiling = 250 -> 2 blob frags in phase 1
1216        let result = lazy_greedy_select(
1217            frags.clone(),
1218            &core,
1219            &rel,
1220            &[],
1221            budget,
1222            0.0,
1223            None,
1224            None,
1225            None,
1226        );
1227        let by_file =
1228            |sel: &Vec<Fragment>, p: &str| sel.iter().filter(|f| f.id.path.as_ref() == p).count();
1229        assert!(
1230            by_file(&result.selected, "a.rs") == 1 && by_file(&result.selected, "b.rs") == 1,
1231            "small files must not be crowded out: {:?}",
1232            result
1233                .selected
1234                .iter()
1235                .map(|f| f.id.path.to_string())
1236                .collect::<Vec<_>>()
1237        );
1238        assert!(
1239            by_file(&result.selected, "blob.json") >= 5,
1240            "leftover budget must flow back to the blob once competitors are served"
1241        );
1242
1243        // Lone-file run: ceiling must not strand budget.
1244        let lone: Vec<Fragment> = (0..20)
1245            .map(|i| {
1246                frag(
1247                    "blob.json",
1248                    i * 10 + 1,
1249                    i * 10 + 9,
1250                    FragmentKind::Chunk,
1251                    100,
1252                )
1253            })
1254            .collect();
1255        let result = lazy_greedy_select(lone, &core, &rel, &[], budget, 0.0, None, None, None);
1256        assert!(
1257            result.selected.len() >= 9,
1258            "single-file selection stranded budget: {} fragments",
1259            result.selected.len()
1260        );
1261    }
1262}