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                // The substitute honors the same per-file ceiling (#212): a
268                // file at its share must not keep placing signature stubs —
269                // its remaining cores defer to the ceiling-free rescue sweep
270                // below, after other files had their turn.
271                let sig_over_ceiling = spent > 0 && spent + sig.token_count > file_ceiling;
272                if !state.selected_ids.contains(&sig.id)
273                    && core_used + sig.token_count <= core_budget
274                    && !sig_over_ceiling
275                {
276                    let rel_score = rel.get(&core_id).copied().unwrap_or(0.0);
277                    place_fragment(sig, &mut core_used, state, rel_score, &mut file_spent);
278                    satisfied.insert(core_id);
279                    continue;
280                }
281            }
282            skipped.push((core_id, frag));
283            continue;
284        }
285
286        let rel_score = rel.get(&core_id).copied().unwrap_or(0.0);
287        place_fragment(frag, &mut core_used, state, rel_score, &mut file_spent);
288        satisfied.insert(core_id);
289    }
290
291    // Bug #2 fix: cores that didn't fit the core_budget reservation must not be
292    // demoted to ordinary greedy candidates without a chance to be placed first.
293    // Sweep skipped cores cheapest-first against the *full* remaining budget
294    // (not just the core slice) so seeds aren't dropped purely because the
295    // highest-relevance core happened to be heavy.
296    if !skipped.is_empty() {
297        skipped.sort_by(|(_, a), (_, b)| a.token_count.cmp(&b.token_count));
298        for (core_id, frag) in skipped {
299            if state.remaining_budget == 0 {
300                break;
301            }
302            if state.selected_ids.is_superset_of(frag) {
303                satisfied.insert(core_id);
304                continue;
305            }
306            let rel_score = rel.get(&core_id).copied().unwrap_or(0.0);
307            if frag.token_count <= state.remaining_budget {
308                place_fragment(frag, &mut core_used, state, rel_score, &mut file_spent);
309                satisfied.insert(core_id);
310            } else if let Some(sig) = sig_lookup.get(&core_id) {
311                if !state.selected_ids.contains(&sig.id)
312                    && sig.token_count <= state.remaining_budget
313                {
314                    place_fragment(sig, &mut core_used, state, rel_score, &mut file_spent);
315                    satisfied.insert(core_id);
316                }
317            }
318        }
319    }
320
321    satisfied
322}
323
324fn build_initial_heap(
325    candidates: &[Fragment],
326    rel: &FxHashMap<FragmentId, f64>,
327    needs: &[InformationNeed],
328    state: &UtilityState,
329    id_to_frag: &mut FxHashMap<FragmentId, Fragment>,
330) -> BinaryHeap<HeapEntry> {
331    let mut heap = BinaryHeap::new();
332    for frag in candidates {
333        if frag.token_count > 0 {
334            let density = compute_density(
335                frag,
336                rel.get(&frag.id).copied().unwrap_or(0.0),
337                needs,
338                state,
339            );
340            heap.push(HeapEntry {
341                neg_density: -density,
342                frag_id: frag.id.clone(),
343                version: 0,
344            });
345            id_to_frag.insert(frag.id.clone(), frag.clone());
346        }
347    }
348    heap
349}
350
351fn find_best_candidate_heap(
352    heap: &mut BinaryHeap<HeapEntry>,
353    current_version: u32,
354    id_to_frag: &FxHashMap<FragmentId, Fragment>,
355    selected_ids: &IntervalIndex,
356    remaining_budget: u32,
357    rel: &FxHashMap<FragmentId, f64>,
358    needs: &[InformationNeed],
359    state: &UtilityState,
360) -> (Option<Fragment>, f64, u32) {
361    let cv = current_version;
362    while let Some(entry) = heap.pop() {
363        let frag = match id_to_frag.get(&entry.frag_id) {
364            Some(f) => f,
365            None => continue,
366        };
367        if frag.token_count > remaining_budget {
368            continue;
369        }
370        if selected_ids.overlaps(frag) {
371            continue;
372        }
373        if entry.version < cv {
374            let new_density = compute_density(
375                frag,
376                rel.get(&frag.id).copied().unwrap_or(0.0),
377                needs,
378                state,
379            );
380            heap.push(HeapEntry {
381                neg_density: -new_density,
382                frag_id: frag.id.clone(),
383                version: cv,
384            });
385            continue;
386        }
387        let actual_density = -entry.neg_density;
388        if actual_density <= 0.0 {
389            return (None, 0.0, cv);
390        }
391        return (Some(frag.clone()), actual_density, cv + 1);
392    }
393    (None, 0.0, cv)
394}
395
396fn find_best_singleton(
397    non_core: &[Fragment],
398    base_selected_ids: &IntervalIndex,
399    base_budget: u32,
400    rel: &FxHashMap<FragmentId, f64>,
401    needs: &[InformationNeed],
402    base_state: &UtilityState,
403) -> (Option<Fragment>, f64) {
404    let mut best_singleton = None;
405    let mut best_gain = 0.0;
406    for f in non_core {
407        if f.token_count > base_budget {
408            continue;
409        }
410        if base_selected_ids.overlaps(f) {
411            continue;
412        }
413        let gain = marginal_gain(f, rel.get(&f.id).copied().unwrap_or(0.0), needs, base_state);
414        if gain > best_gain {
415            best_gain = gain;
416            best_singleton = Some(f.clone());
417        }
418    }
419    (best_singleton, best_gain)
420}
421
422/// Paper-aligned strict Khuller H₁: `argmax_{f ∈ F, |f| ≤ B} U({f})`.
423/// Iterates the FULL ground set (including core), gates on the FULL
424/// budget B (not the residual after partial-core packing), and evaluates
425/// each candidate as a lone selection against an empty utility state.
426///
427/// This is the comparator that, together with the greedy chain, gives
428/// the `(1-1/e)/2` approximation guarantee from Khuller-Moss-Naor 1999
429/// for monotone submodular maximization under a knapsack constraint.
430/// Restricting H₁ to `non_core` with budget `B − cost(packed_core)`
431/// (the prior `find_best_singleton`) is a strict relaxation that
432/// excludes any single high-utility fragment with `|f| > B − β_core·B`.
433fn find_best_singleton_full_set(
434    fragments: &[Fragment],
435    budget_tokens: u32,
436    rel: &FxHashMap<FragmentId, f64>,
437    needs: &[InformationNeed],
438    empty_state: &UtilityState,
439) -> (Option<Fragment>, f64) {
440    let mut best = None;
441    let mut best_gain = 0.0;
442    for f in fragments {
443        if f.token_count == 0 || f.token_count > budget_tokens {
444            continue;
445        }
446        let gain = marginal_gain(
447            f,
448            rel.get(&f.id).copied().unwrap_or(0.0),
449            needs,
450            empty_state,
451        );
452        if gain > best_gain {
453            best_gain = gain;
454            best = Some(f.clone());
455        }
456    }
457    (best, best_gain)
458}
459
460fn init_selection_state(
461    core_ids: &FxHashSet<FragmentId>,
462    rel: &FxHashMap<FragmentId, f64>,
463    budget_tokens: u32,
464    file_importance: Option<&FxHashMap<Arc<str>, f64>>,
465) -> SelectionState {
466    let mut utility_state = UtilityState::default();
467    utility_state.r_cap = compute_r_cap(rel, Some(core_ids));
468    utility_state.changed_dirs = core_ids
469        .iter()
470        .filter_map(|cid| {
471            std::path::Path::new(cid.path.as_ref())
472                .parent()
473                .map(|p| p.to_path_buf())
474        })
475        .collect();
476    if let Some(fi) = file_importance {
477        utility_state.file_importance.clone_from(fi);
478    }
479    SelectionState {
480        selected: Vec::new(),
481        selected_ids: IntervalIndex::new(),
482        remaining_budget: budget_tokens,
483        utility_state,
484    }
485}
486
487#[allow(clippy::too_many_arguments)]
488fn run_greedy_loop_heap(
489    heap: &mut BinaryHeap<HeapEntry>,
490    id_to_frag: &FxHashMap<FragmentId, Fragment>,
491    state: &mut SelectionState,
492    rel: &FxHashMap<FragmentId, f64>,
493    needs: &[InformationNeed],
494    tau: f64,
495    _initial_budget: u32,
496    admissible_files: Option<&FxHashSet<Arc<str>>>,
497) -> (usize, f64, usize) {
498    let mut current_version = 0u32;
499    let mut peak_density: f64 = 0.0;
500    let mut loop_iters: usize = 0;
501    // Per-file admission (#65): opening a NEW file requires it to be
502    // naming-reachable from the changed set; fragments of files already
503    // opened (including the cores selected before this loop) compete on
504    // density as always. Inadmissible candidates are discarded, not
505    // deferred — admissibility is static within a run.
506    let mut open_files: FxHashSet<Arc<str>> =
507        state.selected.iter().map(|f| f.id.path.clone()).collect();
508    // #194 per-file ceiling: while the heap still holds other candidates, one
509    // file may not exceed its budget share. Blocked candidates are deferred,
510    // not dropped — once everyone else has had their chance the ceiling lifts
511    // (phase 2), so leftovers still flow to the highest density and a
512    // single-file run is unaffected.
513    let file_ceiling = (_initial_budget as f64 * selection().per_file_budget_fraction) as u32;
514    let mut file_spent: FxHashMap<Arc<str>, u32> = FxHashMap::default();
515    for f in &state.selected {
516        *file_spent.entry(f.id.path.clone()).or_insert(0) += f.token_count;
517    }
518    let mut deferred: Vec<HeapEntry> = Vec::new();
519    let mut ceiling_active = true;
520
521    loop {
522        while !heap.is_empty() && state.remaining_budget > 0 {
523            loop_iters += 1;
524            let (best_frag, best_density, new_version) = find_best_candidate_heap(
525                heap,
526                current_version,
527                id_to_frag,
528                &state.selected_ids,
529                state.remaining_budget,
530                rel,
531                needs,
532                &state.utility_state,
533            );
534            current_version = new_version;
535
536            let best_frag = match best_frag {
537                Some(f) => f,
538                None => break,
539            };
540            if best_density <= 0.0 {
541                break;
542            }
543
544            if let Some(admissible) = admissible_files {
545                if !open_files.contains(&best_frag.id.path)
546                    && !admissible.contains(&best_frag.id.path)
547                {
548                    continue;
549                }
550            }
551
552            if ceiling_active {
553                let spent = file_spent.get(&best_frag.id.path).copied().unwrap_or(0);
554                if spent > 0 && spent + best_frag.token_count > file_ceiling {
555                    deferred.push(HeapEntry {
556                        neg_density: -best_density,
557                        frag_id: best_frag.id.clone(),
558                        version: current_version,
559                    });
560                    continue;
561                }
562            }
563
564            if best_density > peak_density {
565                peak_density = best_density;
566            } else if peak_density > 0.0 && best_density < tau * peak_density {
567                break;
568            }
569
570            open_files.insert(best_frag.id.path.clone());
571            *file_spent.entry(best_frag.id.path.clone()).or_insert(0) += best_frag.token_count;
572            state.selected.push(best_frag.clone());
573            state.selected_ids.add_id(&best_frag.id);
574            state.remaining_budget = state.remaining_budget.saturating_sub(best_frag.token_count);
575            let rel_score = rel.get(&best_frag.id).copied().unwrap_or(0.0);
576            apply_fragment(&best_frag, rel_score, needs, &mut state.utility_state);
577        }
578
579        if ceiling_active && !deferred.is_empty() && state.remaining_budget > 0 {
580            ceiling_active = false;
581            for e in deferred.drain(..) {
582                heap.push(e);
583            }
584            continue;
585        }
586        break;
587    }
588
589    let threshold = tau * peak_density;
590    (state.selected.len(), threshold, loop_iters)
591}
592
593fn setup_and_select_core(
594    fragments: &[Fragment],
595    core_ids: &FxHashSet<FragmentId>,
596    rel: &FxHashMap<FragmentId, f64>,
597    needs: &[InformationNeed],
598    budget_tokens: u32,
599    file_importance: Option<&FxHashMap<Arc<str>, f64>>,
600    core_excerpts: Option<&FxHashMap<FragmentId, Fragment>>,
601) -> (SelectionState, Vec<Fragment>, Vec<Fragment>, bool) {
602    let mut core_fragments: Vec<Fragment> = fragments
603        .iter()
604        .filter(|f| core_ids.contains(&f.id))
605        .cloned()
606        .collect();
607    core_fragments.sort_by(|a, b| {
608        let ta = if a.token_count > 0 {
609            a.token_count
610        } else {
611            SENTINEL_TOKEN_COUNT
612        };
613        let tb = if b.token_count > 0 {
614            b.token_count
615        } else {
616            SENTINEL_TOKEN_COUNT
617        };
618        ta.cmp(&tb)
619            .then(a.line_count().cmp(&b.line_count()))
620            .then(a.start_line().cmp(&b.start_line()))
621    });
622
623    let non_core_fragments: Vec<Fragment> = fragments
624        .iter()
625        .filter(|f| !core_ids.contains(&f.id))
626        .cloned()
627        .collect();
628
629    let sig_lookup = build_signature_lookup(fragments, &core_fragments, core_excerpts);
630    let mut state = init_selection_state(core_ids, rel, budget_tokens, file_importance);
631    let satisfied_core_ids = select_core_fragments(
632        &core_fragments,
633        rel,
634        needs,
635        &mut state,
636        budget_tokens,
637        &sig_lookup,
638        core_excerpts,
639    );
640
641    // A core represented by a substitute (signature stub or downshifted
642    // excerpt) is satisfied even though its own id is absent from the
643    // selection — offering the full fragment back to the greedy would undo the
644    // substitution.
645    let skipped_core: Vec<FragmentId> = core_ids
646        .iter()
647        .filter(|id| !satisfied_core_ids.contains(*id))
648        .cloned()
649        .collect();
650
651    let mut non_core_with_skipped = non_core_fragments;
652    if !skipped_core.is_empty() {
653        let skipped_set: FxHashSet<FragmentId> = skipped_core.into_iter().collect();
654        for cf in &core_fragments {
655            if skipped_set.contains(&cf.id) {
656                non_core_with_skipped.push(cf.clone());
657            }
658        }
659    }
660
661    let should_return_early = state.remaining_budget == 0;
662    let selected_copy = state.selected.clone();
663    (
664        state,
665        non_core_with_skipped,
666        selected_copy,
667        should_return_early,
668    )
669}
670
671pub fn lazy_greedy_select(
672    fragments: Vec<Fragment>,
673    core_ids: &FxHashSet<FragmentId>,
674    rel: &FxHashMap<FragmentId, f64>,
675    needs: &[InformationNeed],
676    budget_tokens: u32,
677    tau: f64,
678    file_importance: Option<&FxHashMap<Arc<str>, f64>>,
679    core_excerpts: Option<&FxHashMap<FragmentId, Fragment>>,
680    admissible_files: Option<&FxHashSet<Arc<str>>>,
681) -> SelectionResult {
682    if fragments.is_empty() {
683        return SelectionResult {
684            selected: Vec::new(),
685            reason: SelectionReason::NoCandidates,
686            used_tokens: 0,
687            utility: 0.0,
688            greedy_iters: 0,
689            stopping_certificate: 0.0,
690        };
691    }
692
693    let (mut state, non_core_fragments, _selected_core, should_return_early) =
694        setup_and_select_core(
695            &fragments,
696            core_ids,
697            rel,
698            needs,
699            budget_tokens,
700            file_importance,
701            core_excerpts,
702        );
703
704    if should_return_early {
705        let used = budget_tokens - state.remaining_budget;
706        return SelectionResult {
707            selected: state.selected,
708            reason: SelectionReason::BudgetExhausted,
709            used_tokens: used,
710            utility: utility_value(&state.utility_state),
711            greedy_iters: 0,
712            stopping_certificate: 0.0,
713        };
714    }
715
716    let base_state = state.utility_state.copy();
717    let base_selected = state.selected.clone();
718    let base_budget = state.remaining_budget;
719
720    let candidates: Vec<Fragment> = non_core_fragments
721        .iter()
722        .filter(|f| !state.selected_ids.overlaps(f))
723        .cloned()
724        .collect();
725    let candidates = drop_redundant_signatures(&candidates, state.remaining_budget);
726
727    let mut id_to_frag: FxHashMap<FragmentId, Fragment> = FxHashMap::default();
728    let mut heap = build_initial_heap(
729        &candidates,
730        rel,
731        needs,
732        &state.utility_state,
733        &mut id_to_frag,
734    );
735
736    let (_, threshold, greedy_iters) = run_greedy_loop_heap(
737        &mut heap,
738        &id_to_frag,
739        &mut state,
740        rel,
741        needs,
742        tau,
743        budget_tokens,
744        admissible_files,
745    );
746
747    let greedy_utility = utility_value(&state.utility_state);
748
749    let mut base_selected_ids = IntervalIndex::new();
750    for f in &base_selected {
751        base_selected_ids.add_id(&f.id);
752    }
753
754    let (best_singleton, best_gain) = find_best_singleton(
755        &non_core_fragments,
756        &base_selected_ids,
757        base_budget,
758        rel,
759        needs,
760        &base_state,
761    );
762
763    let empty_state = init_selection_state(core_ids, rel, budget_tokens, file_importance);
764    let (full_singleton, full_singleton_gain) = find_best_singleton_full_set(
765        &fragments,
766        budget_tokens,
767        rel,
768        needs,
769        &empty_state.utility_state,
770    );
771
772    let mut best_alt_utility = greedy_utility;
773    let mut best_alt: Option<(u32, Vec<Fragment>)> = None;
774
775    if let Some(ref singleton) = best_singleton {
776        let u = utility_value(&base_state) + best_gain;
777        if u > best_alt_utility {
778            best_alt_utility = u;
779            let mut sel = base_selected.clone();
780            sel.push(singleton.clone());
781            best_alt = Some((selection_cost(&sel), sel));
782        }
783    }
784
785    if let Some(ref full) = full_singleton {
786        let u = utility_value(&empty_state.utility_state) + full_singleton_gain;
787        // Additive on top of the core selection, never a replacement for it.
788        // This branch used to return `vec![full]` outright, so a single heavy
789        // fragment whose standalone utility beat the greedy chain's discarded
790        // every changed-code fragment — the one thing the output exists to
791        // carry. `ensure_changed_files_represented` could not reliably undo it
792        // either: it only had `budget - full.token_count` left and only picks a
793        // fragment that fits. The two utilities are also measured from
794        // different baselines (this one from an empty state, `greedy_utility`
795        // from the core base), so the comparison can only ever be a heuristic
796        // nudge — not grounds for dropping the core.
797        //
798        // H₁ iterates the FULL ground set, so its winner can be a core the
799        // core pass already packed. Then this arm has nothing to add: its
800        // "alternative" is `base_selected` verbatim, a strict subset of the
801        // greedy result. Utility is monotone and `greedy_utility` already
802        // contains that core's contribution, so `u > best_alt_utility` should
803        // be unreachable in that case — this makes the reasoning a condition
804        // rather than an assumption, because the arm's cost accounting has no
805        // meaning when nothing is appended.
806        let already_selected = base_selected.iter().any(|f| f.id == full.id);
807        if u > best_alt_utility && full.token_count <= base_budget && !already_selected {
808            best_alt_utility = u;
809            let mut sel = base_selected.clone();
810            sel.push(full.clone());
811            best_alt = Some((selection_cost(&sel), sel));
812        }
813    }
814
815    if let Some((used, sel)) = best_alt {
816        return SelectionResult {
817            selected: sel,
818            reason: SelectionReason::BestSingleton,
819            used_tokens: used,
820            utility: best_alt_utility,
821            greedy_iters,
822            stopping_certificate: 0.0,
823        };
824    }
825
826    let used = budget_tokens - state.remaining_budget;
827    let reason = if state.remaining_budget == 0 {
828        SelectionReason::BudgetExhausted
829    } else if greedy_utility <= 0.0 {
830        SelectionReason::NoUtility
831    } else if state.selected.is_empty() || state.selected.len() == base_selected.len() {
832        SelectionReason::NoCandidates
833    } else if threshold > 0.0 && !heap.is_empty() {
834        SelectionReason::StoppedByTau
835    } else {
836        SelectionReason::NoCandidates
837    };
838
839    let stopping_certificate = if matches!(reason, SelectionReason::StoppedByTau) {
840        threshold * f64::from(state.remaining_budget)
841    } else {
842        0.0
843    };
844
845    SelectionResult {
846        selected: state.selected,
847        reason,
848        used_tokens: used,
849        utility: greedy_utility,
850        greedy_iters,
851        stopping_certificate,
852    }
853}
854
855#[cfg(test)]
856mod tests {
857    use super::*;
858    use crate::types::FragmentKind;
859
860    fn frag(path: &str, start: u32, end: u32, kind: FragmentKind, tokens: u32) -> Fragment {
861        let mut identifiers = FxHashSet::default();
862        identifiers.insert(format!("sym_{path}_{start}"));
863        Fragment {
864            id: FragmentId::new(Arc::from(path), start, end),
865            kind,
866            content: Arc::from(format!("// {path}:{start}-{end}\n")),
867            identifiers,
868            token_count: tokens,
869            symbol_name: Some(format!("sym_{start}")),
870        }
871    }
872
873    fn rel_map(frags: &[Fragment], score: f64) -> FxHashMap<FragmentId, f64> {
874        frags.iter().map(|f| (f.id.clone(), score)).collect()
875    }
876
877    fn cost_of(selected: &[Fragment]) -> u32 {
878        selected.iter().map(|f| f.token_count).sum()
879    }
880
881    /// The budget is a hard contract (`cost(C) <= B`); four separate call sites
882    /// gate on it and none of them was asserted. A `pick_smallest_fitting` that
883    /// returns a non-fitting candidate is one `if` away, and the oracle corpus
884    /// can never catch it because its budget is always >=2.5x the whole repo.
885    #[test]
886    fn selection_never_exceeds_the_budget() {
887        let frags: Vec<Fragment> = (0..12)
888            .map(|i| {
889                frag(
890                    "a.rs",
891                    1 + i * 50,
892                    40 + i * 50,
893                    FragmentKind::Function,
894                    30 + i * 17,
895                )
896            })
897            .collect();
898        let core: FxHashSet<FragmentId> = std::iter::once(frags[0].id.clone()).collect();
899        let rel = rel_map(&frags, 0.7);
900
901        for budget in [1u32, 7, 30, 31, 60, 200, 1_000] {
902            let result = lazy_greedy_select(
903                frags.clone(),
904                &core,
905                &rel,
906                &[],
907                budget,
908                0.12,
909                None,
910                None,
911                None,
912            );
913            assert!(
914                cost_of(&result.selected) <= budget,
915                "budget {budget} overrun: cost {} via {:?}",
916                cost_of(&result.selected),
917                result.reason
918            );
919            assert!(
920                result.used_tokens <= budget,
921                "reported used_tokens {} exceeds budget {budget}",
922                result.used_tokens
923            );
924        }
925    }
926
927    /// `used_tokens` is what every downstream budget report reads, and it was
928    /// only ever asserted as `<= budget`. Three code paths compute it by
929    /// different budget arithmetic; this pins the equality they all have to
930    /// satisfy, so a future path that reconstructs the figure instead of
931    /// measuring the selection fails here rather than in a results table.
932    #[test]
933    fn reported_used_tokens_always_equals_the_cost_of_the_returned_selection() {
934        let shapes: Vec<Vec<Fragment>> = vec![
935            vec![
936                frag("changed.rs", 1, 8, FragmentKind::Function, 20),
937                frag("other.rs", 1, 400, FragmentKind::Class, 900),
938                frag("other.rs", 500, 520, FragmentKind::Function, 40),
939            ],
940            // A lone, heavy core: H₁ over the full ground set can only win with
941            // a fragment the core pass already placed.
942            vec![frag("changed.rs", 1, 200, FragmentKind::Class, 400)],
943            (0..6)
944                .map(|i| {
945                    frag(
946                        "a.rs",
947                        1 + i * 20,
948                        10 + i * 20,
949                        FragmentKind::Function,
950                        20 + i * 60,
951                    )
952                })
953                .collect(),
954        ];
955
956        for frags in shapes {
957            let core: FxHashSet<FragmentId> = std::iter::once(frags[0].id.clone()).collect();
958            let rel = rel_map(&frags, 0.8);
959            for budget in [50u32, 120, 460, 1_000, 5_000] {
960                let result = lazy_greedy_select(
961                    frags.clone(),
962                    &core,
963                    &rel,
964                    &[],
965                    budget,
966                    0.12,
967                    None,
968                    None,
969                    None,
970                );
971                assert_eq!(
972                    result.used_tokens,
973                    cost_of(&result.selected),
974                    "reason {:?} at budget {budget}: reported {} but selection costs {}",
975                    result.reason,
976                    result.used_tokens,
977                    cost_of(&result.selected)
978                );
979            }
980        }
981    }
982
983    /// A core that is mostly unchanged must be placed as its hunk-window
984    /// excerpt, not in full — the over-dump behind #105/#107/#149. The excerpt
985    /// arrives through `core_excerpts`, keyed by the core it replaces.
986    #[test]
987    fn a_mostly_unchanged_core_is_placed_as_its_excerpt() {
988        let core = frag("script.sh", 1, 122, FragmentKind::Chunk, 600);
989        let excerpt = frag("script.sh", 58, 64, FragmentKind::Excerpt, 40);
990        let core_ids: FxHashSet<FragmentId> = std::iter::once(core.id.clone()).collect();
991        let rel = rel_map(&[core.clone()], 1.0);
992        let mut excerpts: FxHashMap<FragmentId, Fragment> = FxHashMap::default();
993        excerpts.insert(core.id.clone(), excerpt.clone());
994
995        let result = lazy_greedy_select(
996            vec![core.clone()],
997            &core_ids,
998            &rel,
999            &[],
1000            8_000,
1001            0.12,
1002            None,
1003            Some(&excerpts),
1004            None,
1005        );
1006
1007        let ids: Vec<String> = result
1008            .selected
1009            .iter()
1010            .map(|f| format!("{}:{}-{}", f.id.path, f.id.start_line, f.id.end_line))
1011            .collect();
1012        assert!(
1013            result.selected.iter().any(|f| f.id == excerpt.id),
1014            "core was not downshifted to its excerpt: {ids:?}"
1015        );
1016        assert!(
1017            !result.selected.iter().any(|f| f.id == core.id),
1018            "the full core was emitted alongside the excerpt: {ids:?}"
1019        );
1020    }
1021
1022    #[test]
1023    fn selected_fragments_never_overlap_and_are_never_duplicated() {
1024        let frags: Vec<Fragment> = (0..8)
1025            .map(|i| frag("a.rs", 1 + i * 10, 12 + i * 10, FragmentKind::Function, 25))
1026            .collect();
1027        let core: FxHashSet<FragmentId> = std::iter::once(frags[0].id.clone()).collect();
1028        let rel = rel_map(&frags, 0.9);
1029        let result = lazy_greedy_select(frags, &core, &rel, &[], 400, 0.12, None, None, None);
1030
1031        let ids: FxHashSet<FragmentId> = result.selected.iter().map(|f| f.id.clone()).collect();
1032        assert_eq!(
1033            ids.len(),
1034            result.selected.len(),
1035            "duplicate fragment selected"
1036        );
1037    }
1038
1039    /// `find_best_singleton_full_set` used to return `vec![full]`, discarding
1040    /// every core fragment. The core IS the changed code, so a selection that
1041    /// drops all of it answers a different question than the one asked.
1042    #[test]
1043    fn a_winning_singleton_never_evicts_the_core_selection() {
1044        // A heavy, highly relevant non-core fragment is the shape that makes the
1045        // full-set singleton win.
1046        let core_frag = frag("changed.rs", 1, 8, FragmentKind::Function, 20);
1047        let heavy = frag("other.rs", 1, 400, FragmentKind::Class, 900);
1048        let filler = frag("other.rs", 500, 520, FragmentKind::Function, 40);
1049        let frags = vec![core_frag.clone(), heavy.clone(), filler];
1050
1051        let core: FxHashSet<FragmentId> = std::iter::once(core_frag.id.clone()).collect();
1052        let mut rel = FxHashMap::default();
1053        rel.insert(core_frag.id.clone(), 0.05);
1054        rel.insert(heavy.id.clone(), 1.0);
1055        rel.insert(frags[2].id.clone(), 0.1);
1056
1057        let result = lazy_greedy_select(frags, &core, &rel, &[], 2_000, 0.12, None, None, None);
1058        assert!(
1059            result.selected.iter().any(|f| core.contains(&f.id)),
1060            "no core fragment survived; reason was {:?}",
1061            result.reason
1062        );
1063        assert!(cost_of(&result.selected) <= 2_000);
1064    }
1065
1066    #[test]
1067    fn empty_ground_set_reports_no_candidates() {
1068        let result = lazy_greedy_select(
1069            Vec::new(),
1070            &FxHashSet::default(),
1071            &FxHashMap::default(),
1072            &[],
1073            1_000,
1074            0.12,
1075            None,
1076            None,
1077            None,
1078        );
1079        assert!(result.selected.is_empty());
1080        assert_eq!(result.reason, SelectionReason::NoCandidates);
1081        assert_eq!(result.used_tokens, 0);
1082    }
1083
1084    /// Keyed on `(path, start_line)`, this used to be last-write-wins, so a
1085    /// small co-located sibling could delete the stub that was the only
1086    /// affordable representation of an oversized fragment.
1087    #[test]
1088    fn drop_redundant_signatures_is_independent_of_candidate_order() {
1089        let header = frag("a.rs", 10, 12, FragmentKind::Definition, 40);
1090        let whole = frag("a.rs", 10, 300, FragmentKind::Class, 4_000);
1091        let stub = frag("a.rs", 10, 11, FragmentKind::ClassSignature, 15);
1092
1093        let forward =
1094            drop_redundant_signatures(&[header.clone(), whole.clone(), stub.clone()], 500);
1095        let backward = drop_redundant_signatures(&[whole, header, stub], 500);
1096
1097        let kinds = |v: &[Fragment]| -> Vec<FragmentKind> { v.iter().map(|f| f.kind).collect() };
1098        assert!(
1099            kinds(&forward).contains(&FragmentKind::ClassSignature),
1100            "the stub for an unaffordable class was dropped: {:?}",
1101            kinds(&forward)
1102        );
1103        let mut a: Vec<FragmentKind> = kinds(&forward);
1104        let mut b: Vec<FragmentKind> = kinds(&backward);
1105        a.sort_by_key(|k| format!("{k:?}"));
1106        b.sort_by_key(|k| format!("{k:?}"));
1107        assert_eq!(a, b, "verdict depended on candidate order");
1108    }
1109
1110    #[test]
1111    fn drop_redundant_signatures_removes_a_stub_whose_full_fragment_fits() {
1112        let whole = frag("a.rs", 10, 40, FragmentKind::Class, 100);
1113        let stub = frag("a.rs", 10, 11, FragmentKind::ClassSignature, 15);
1114        let kept = drop_redundant_signatures(&[whole, stub], 500);
1115        assert!(
1116            !kept.iter().any(|f| f.kind.is_signature()),
1117            "stub survived even though the full fragment fits the budget"
1118        );
1119    }
1120    /// tau is the adaptive stop: once a candidate's density falls below
1121    /// `tau * peak_density` the loop stops instead of spending the rest of the
1122    /// budget. The oracle corpus used to run at tau=0.0, which made the
1123    /// predicate unreachable, so deleting the rule failed no test; the corpus
1124    /// now runs at the shipped default too (#175). This keeps a direct
1125    /// assertion on the rule that does not depend on corpus wiring.
1126    #[test]
1127    fn tau_stops_the_greedy_loop_before_the_budget_is_spent() {
1128        // Descending relevance against escalating cost gives sharply
1129        // descending density, which is what the stop rule reacts to.
1130        let frags: Vec<Fragment> = (0..6)
1131            .map(|i| {
1132                frag(
1133                    "a.rs",
1134                    1 + i * 20,
1135                    10 + i * 20,
1136                    FragmentKind::Function,
1137                    20 + i * i * 120,
1138                )
1139            })
1140            .collect();
1141        let core: FxHashSet<FragmentId> = std::iter::once(frags[0].id.clone()).collect();
1142        let mut rel = FxHashMap::default();
1143        // Geometric decay: with escalating cost the density ratio between
1144        // consecutive candidates falls below 5% by the tail, so the rule
1145        // fires at the shipped tau (0.05) and not only at looser settings.
1146        for (i, f) in frags.iter().enumerate() {
1147            rel.insert(f.id.clone(), 0.25f64.powi(i as i32).max(1e-6));
1148        }
1149
1150        let budget = 10_000;
1151        let default_tau = lazy_greedy_select(
1152            frags.clone(),
1153            &core,
1154            &rel,
1155            &[],
1156            budget,
1157            crate::config::limits::DEFAULT_STOPPING_THRESHOLD,
1158            None,
1159            None,
1160            None,
1161        );
1162        let no_tau = lazy_greedy_select(frags, &core, &rel, &[], budget, 0.0, None, None, None);
1163
1164        assert_eq!(
1165            default_tau.reason,
1166            SelectionReason::StoppedByTau,
1167            "the adaptive stop did not fire at the shipped default"
1168        );
1169        assert!(
1170            default_tau.selected.len() < no_tau.selected.len(),
1171            "tau={} selected {} fragments, same as tau=0.0 — the rule is inert",
1172            crate::config::limits::DEFAULT_STOPPING_THRESHOLD,
1173            default_tau.selected.len()
1174        );
1175        assert!(
1176            default_tau.used_tokens < no_tau.used_tokens,
1177            "the stop saved no budget: {} vs {}",
1178            default_tau.used_tokens,
1179            no_tau.used_tokens
1180        );
1181        assert!(
1182            default_tau.stopping_certificate > 0.0,
1183            "StoppedByTau must carry a positive certificate"
1184        );
1185        assert_eq!(
1186            no_tau.stopping_certificate, 0.0,
1187            "tau=0.0 cannot produce a stopping certificate"
1188        );
1189    }
1190
1191    /// #194: with competitors waiting, one file may not monopolize the budget
1192    /// — but once every other file has had its chance, the ceiling lifts and
1193    /// leftovers flow back, so a lone file still fills the budget.
1194    #[test]
1195    fn per_file_ceiling_blocks_monopoly_but_releases_leftovers() {
1196        // One "blob" file with many equal fragments vs two small files.
1197        let mut frags: Vec<Fragment> = (0..20)
1198            .map(|i| {
1199                frag(
1200                    "blob.json",
1201                    i * 10 + 1,
1202                    i * 10 + 9,
1203                    FragmentKind::Chunk,
1204                    100,
1205                )
1206            })
1207            .collect();
1208        frags.push(frag("a.rs", 1, 9, FragmentKind::Function, 100));
1209        frags.push(frag("b.rs", 1, 9, FragmentKind::Function, 100));
1210        let core: FxHashSet<FragmentId> = FxHashSet::default();
1211        let mut rel: FxHashMap<FragmentId, f64> = FxHashMap::default();
1212        // Blob fragments outrank the small files.
1213        for f in &frags {
1214            let w = if f.id.path.as_ref() == "blob.json" {
1215                1.0
1216            } else {
1217                0.5
1218            };
1219            rel.insert(f.id.clone(), w);
1220        }
1221        let budget = 1_000u32; // ceiling = 250 -> 2 blob frags in phase 1
1222        let result = lazy_greedy_select(
1223            frags.clone(),
1224            &core,
1225            &rel,
1226            &[],
1227            budget,
1228            0.0,
1229            None,
1230            None,
1231            None,
1232        );
1233        let by_file =
1234            |sel: &Vec<Fragment>, p: &str| sel.iter().filter(|f| f.id.path.as_ref() == p).count();
1235        assert!(
1236            by_file(&result.selected, "a.rs") == 1 && by_file(&result.selected, "b.rs") == 1,
1237            "small files must not be crowded out: {:?}",
1238            result
1239                .selected
1240                .iter()
1241                .map(|f| f.id.path.to_string())
1242                .collect::<Vec<_>>()
1243        );
1244        assert!(
1245            by_file(&result.selected, "blob.json") >= 5,
1246            "leftover budget must flow back to the blob once competitors are served"
1247        );
1248
1249        run_lone_file_check(budget, &rel);
1250    }
1251
1252    /// #212: once a file is at its ceiling, its remaining cores may not keep
1253    /// placing signature stubs either — competitor cores get their phase-1
1254    /// seat first, and the file's tail waits for the ceiling-free sweep.
1255    #[test]
1256    fn signature_stubs_honor_the_per_file_ceiling() {
1257        let budget = 1_000u32;
1258        let mut cores: Vec<Fragment> = vec![frag("blob.rs", 1, 90, FragmentKind::Function, 240)];
1259        for i in 1..8 {
1260            cores.push(frag(
1261                "blob.rs",
1262                1 + i * 100,
1263                90 + i * 100,
1264                FragmentKind::Function,
1265                400,
1266            ));
1267        }
1268        cores.push(frag("b.rs", 1, 50, FragmentKind::Function, 200));
1269        let sig_lookup: FxHashMap<FragmentId, Fragment> = cores
1270            .iter()
1271            .filter(|c| c.id.path.as_ref() == "blob.rs")
1272            .map(|c| {
1273                let mut sig = frag(
1274                    "blob.rs",
1275                    c.start_line(),
1276                    c.start_line(),
1277                    FragmentKind::FunctionSignature,
1278                    30,
1279                );
1280                sig.id = FragmentId::new(c.id.path.clone(), c.start_line(), c.start_line());
1281                (c.id.clone(), sig)
1282            })
1283            .collect();
1284        let mut rel: FxHashMap<FragmentId, f64> = FxHashMap::default();
1285        for c in &cores {
1286            let w = if c.id.path.as_ref() == "blob.rs" {
1287                1.0
1288            } else {
1289                0.5
1290            };
1291            rel.insert(c.id.clone(), w);
1292        }
1293        let core_ids: FxHashSet<FragmentId> = cores.iter().map(|c| c.id.clone()).collect();
1294        let mut state = init_selection_state(&core_ids, &rel, budget, None);
1295        select_core_fragments(&cores, &rel, &[], &mut state, budget, &sig_lookup, None);
1296
1297        let first_b = state
1298            .selected
1299            .iter()
1300            .position(|f| f.id.path.as_ref() == "b.rs")
1301            .expect("the competitor core must be selected");
1302        let stubs_before_b = state.selected[..first_b]
1303            .iter()
1304            .filter(|f| f.kind.is_signature())
1305            .count();
1306        assert_eq!(
1307            stubs_before_b, 0,
1308            "a file at its ceiling placed {stubs_before_b} signature stubs before the competitor got its seat"
1309        );
1310    }
1311
1312    fn run_lone_file_check(budget: u32, rel: &FxHashMap<FragmentId, f64>) {
1313        // Lone-file run: ceiling must not strand budget.
1314        let lone: Vec<Fragment> = (0..20)
1315            .map(|i| {
1316                frag(
1317                    "blob.json",
1318                    i * 10 + 1,
1319                    i * 10 + 9,
1320                    FragmentKind::Chunk,
1321                    100,
1322                )
1323            })
1324            .collect();
1325        let core: FxHashSet<FragmentId> = FxHashSet::default();
1326        let result = lazy_greedy_select(lone, &core, rel, &[], budget, 0.0, None, None, None);
1327        assert!(
1328            result.selected.len() >= 9,
1329            "single-file selection stranded budget: {} fragments",
1330            result.selected.len()
1331        );
1332    }
1333}