Skip to main content

scc_context/
selector.rs

1//! Token-budget selection over ranked context candidates (Wave 14B/15.1).
2//!
3//! Four deterministic, no-panic selectors:
4//! - [`select_with_budget`]: greedy value/token knapsack over
5//!   [`ContextItem`]s with a soft budget + hard maximum; required items
6//!   are always kept (the caller compresses them when they alone exceed
7//!   the hard max).
8//! - [`select_in_order`]: the `optimizer`-off variant — rank-order cut,
9//!   no value/token reordering.
10//! - [`mmr_diversify`]: Maximal Marginal Relevance — penalizes candidates
11//!   similar to already-selected ones so one component/owner group cannot
12//!   crowd out the rest.
13//! - [`enforce_quotas`]: TOKEN-aware per-kind caps (fraction × available
14//!   tokens, adapted to the pool's running average token cost) over a
15//!   ranked list, preserving rank order.
16
17use scc_core::ContextItem;
18use std::collections::HashMap;
19
20/// Select indices into `items` within a token `budget`. Required items come
21/// first and are never dropped (even when they alone exceed the budget —
22/// required context is never silently cut); the remaining budget is filled
23/// greedily by value/token, highest first. Deterministic: ties break on
24/// index order.
25///
26/// `hard_max` is the absolute token ceiling (soft target + 20% by default):
27/// required items may push the total over `budget` but the caller
28/// structurally compresses them when they alone exceed `hard_max` (the
29/// item costs then already reflect the compressed render); pool items are
30/// only added while the total stays within both the soft budget and the
31/// hard maximum.
32// trace:v1 id=impl.scc.selector work=WORK-SCC-015 satisfies=REQ-SCC-IR
33pub fn select_with_budget(items: &[ContextItem], budget: usize, hard_max: usize) -> Vec<usize> {
34    let mut selected: Vec<usize> = Vec::new();
35    let mut spent: usize = 0;
36    for (i, item) in items.iter().enumerate() {
37        if item.required {
38            selected.push(i);
39            spent = spent.saturating_add(item.token_cost);
40        }
41    }
42    let mut rest: Vec<usize> = (0..items.len()).filter(|i| !items[*i].required).collect();
43    rest.sort_by(|a, b| {
44        let va = items[*a].value / items[*a].token_cost.max(1) as f64;
45        let vb = items[*b].value / items[*b].token_cost.max(1) as f64;
46        vb.partial_cmp(&va)
47            .unwrap_or(std::cmp::Ordering::Equal)
48            .then_with(|| a.cmp(b))
49    });
50    for i in rest {
51        let cost = items[i].token_cost;
52        let next = spent.saturating_add(cost);
53        if next <= budget && next <= hard_max {
54            selected.push(i);
55            spent = next;
56        }
57    }
58    selected
59}
60
61/// Budget selection in list order (the `optimizer`-off ablation of
62/// [`select_with_budget`]): walk items in importance order, keep required
63/// items unconditionally, and accept pool items while the total token
64/// spend stays within both the soft `budget` and the `hard_max` ceiling.
65/// No value/token reordering — a candidate's rank order decides.
66// trace:v1 id=impl.scc.selector.select-in-order work=WORK-SCC-015 satisfies=REQ-SCC-IR
67pub fn select_in_order(items: &[ContextItem], budget: usize, hard_max: usize) -> Vec<usize> {
68    let mut selected: Vec<usize> = Vec::new();
69    let mut spent: usize = 0;
70    for (i, item) in items.iter().enumerate() {
71        if item.required {
72            selected.push(i);
73            spent = spent.saturating_add(item.token_cost);
74            continue;
75        }
76        let next = spent.saturating_add(item.token_cost);
77        if next <= budget && next <= hard_max {
78            selected.push(i);
79            spent = next;
80        }
81    }
82    selected
83}
84
85/// Maximal Marginal Relevance selection: repeatedly pick the candidate
86/// maximizing `lambda * score - (1 - lambda) * max_similarity(selected)`,
87/// up to `budget` items. `lambda` is clamped to [0, 1]; 0.5 is the default
88/// (see [`mmr_diversify_default`]). Deterministic: ties keep the earlier
89/// candidate in `ranked` order.
90// trace:exempt reason=internal-detail
91pub fn mmr_diversify(
92    ranked: &[(String, f64)],
93    similarity: impl Fn(&str, &str) -> f64,
94    lambda: f64,
95    budget: usize,
96) -> Vec<String> {
97    let n = ranked.len();
98    let budget = budget.min(n);
99    let lambda = lambda.clamp(0.0, 1.0);
100    let mut selected: Vec<usize> = Vec::with_capacity(budget);
101    let mut picked = vec![false; n];
102    // Incremental max-similarity (profiler receipt 2026-09-11: the naive
103    // re-scan over all selected per candidate was ~98% of the surface
104    // render). max() is commutative, so carrying each candidate's running
105    // max forward and comparing only against the newly selected item
106    // yields bit-identical values — and therefore identical picks — in
107    // O(budget*n) similarity calls instead of O(budget*n*selected).
108    let mut max_sim = vec![0.0_f64; n];
109    while selected.len() < budget {
110        if let Some(&last) = selected.last() {
111            for i in 0..n {
112                if picked[i] {
113                    continue;
114                }
115                let s = similarity(&ranked[i].0, &ranked[last].0);
116                if s > max_sim[i] {
117                    max_sim[i] = s;
118                }
119            }
120        }
121        let mut best: Option<(usize, f64)> = None;
122        for i in 0..n {
123            if picked[i] {
124                continue;
125            }
126            let v = lambda * ranked[i].1 - (1.0 - lambda) * max_sim[i];
127            if best.is_none_or(|(_, bv)| v > bv) {
128                best = Some((i, v));
129            }
130        }
131        match best {
132            Some((i, _)) => {
133                selected.push(i);
134                picked[i] = true;
135            }
136            None => break,
137        }
138    }
139    selected.into_iter().map(|i| ranked[i].0.clone()).collect()
140}
141
142/// [`mmr_diversify`] with the spec's default `lambda = 0.5`.
143// trace:exempt reason=internal-detail
144pub fn mmr_diversify_default(
145    ranked: &[(String, f64)],
146    similarity: impl Fn(&str, &str) -> f64,
147    budget: usize,
148) -> Vec<String> {
149    mmr_diversify(ranked, similarity, 0.5, budget)
150}
151
152/// Enforce per-kind quotas over a ranked list, preserving rank order —
153/// TOKEN-aware (reviewer item 6): each kind's cap is a token budget
154/// `fraction * available_tokens`; a candidate is accepted only while
155/// accepting it would not push its kind's accumulated spend over that
156/// cap. Because the cap is a token budget applied over actual per-
157/// candidate costs, the effective entry allowance adapts to the selected
158/// pool's running average token cost (expensive candidates exhaust a
159/// kind's allocation faster, cheap ones admit more entries). A candidate
160/// that would exceed its group cap is skipped — the next candidate of
161/// another group takes its place, so the remaining groups rebalance.
162///
163/// This implements the spec's global surface budget — 30% public/
164/// entrypoint, 25% core impl, 15% types/interfaces, 10% state owners,
165/// 10% contract APIs, 10% flow-critical (quota keys `public`, `core`,
166/// `types`, `state`, `contract`, `flow`) — scaled to the tokens actually
167/// available to the pool (`available_tokens` = budget minus required
168/// spend; required entries are partitioned out before this stage and may
169/// exceed their group allocation).
170// trace:exempt reason=internal-detail
171pub fn enforce_quotas(
172    ranked: &[(String, f64)],
173    kind_of: impl Fn(&str) -> &str,
174    quotas: &[(String, f64)],
175    available_tokens: usize,
176    token_cost: impl Fn(&str) -> usize,
177) -> Vec<String> {
178    let mut caps: HashMap<&str, usize> = HashMap::new();
179    for (kind, frac) in quotas {
180        let cap = (frac.clamp(0.0, 1.0) * available_tokens as f64).round() as usize;
181        caps.insert(kind.as_str(), cap);
182    }
183    let mut spent: HashMap<&str, usize> = HashMap::new();
184    let mut out: Vec<String> = Vec::new();
185    for (id, _) in ranked {
186        let k = kind_of(id);
187        let take = match caps.get(k) {
188            None => true,
189            Some(&cap) => {
190                let s = spent.entry(k).or_insert(0);
191                let next = s.saturating_add(token_cost(id));
192                if next <= cap {
193                    *s = next;
194                    true
195                } else {
196                    false
197                }
198            }
199        };
200        if take {
201            out.push(id.clone());
202        }
203    }
204    out
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211// trace:exempt reason=internal-detail
212    fn item(id: &str, value: f64, token_cost: usize, required: bool) -> ContextItem {
213        ContextItem {
214            id: id.to_string(),
215            value,
216            token_cost,
217            required,
218            group: None,
219        }
220    }
221
222    // ---- (g) budget selection: drops low-value, never drops required ----
223
224    #[test]
225// trace:exempt reason=internal-detail
226    fn budget_selection_drops_low_value_and_keeps_required() {
227        let items = vec![
228            item("required", 0.1, 120, true),
229            item("high", 0.9, 50, false),
230            item("mid", 0.5, 50, false),
231            item("low", 0.1, 50, false),
232        ];
233        // Budget 170: required (120) + highest value/token (high, 50).
234        let sel = select_with_budget(&items, 170, 204);
235        assert_eq!(sel, vec![0, 1]);
236        // Mid/low are dropped; required is present.
237        assert!(sel.contains(&0));
238        assert!(!sel.contains(&2) && !sel.contains(&3));
239
240        // Budget exhausted by required alone: still never drops required.
241        let sel = select_with_budget(&items, 50, 204);
242        assert_eq!(sel, vec![0]);
243
244        // Budget 0: only required.
245        let sel = select_with_budget(&items, 0, 204);
246        assert_eq!(sel, vec![0]);
247
248        // Empty input.
249        assert!(select_with_budget(&[], 100, 100).is_empty());
250
251        // No required: pure value/token greedy.
252        let no_req = vec![item("a", 0.1, 100, false), item("b", 0.9, 10, false)];
253        let sel = select_with_budget(&no_req, 100, 100);
254        assert_eq!(sel, vec![1]);
255
256        // Hard max never drops required — a ceiling below the required
257        // spend still selects it (the caller compresses the render
258        // instead); pool items must fit the soft budget AND the hard max.
259        let sel = select_with_budget(&items, 50, 60);
260        assert_eq!(sel, vec![0]);
261        let sel = select_with_budget(&items, 10, 5);
262        assert_eq!(sel, vec![0]);
263    }
264
265    // ---- optimizer-off: rank-order selection ----
266
267    #[test]
268// trace:exempt reason=internal-detail
269    fn select_in_order_keeps_rank_order() {
270        let items = vec![
271            item("required", 0.1, 120, true),
272            item("high", 0.9, 50, false),
273            item("mid", 0.5, 50, false),
274            item("low", 0.1, 50, false),
275        ];
276        // Rank-order cut: required first, then items in list order while
277        // the total fits — no value/token reordering.
278        let sel = select_in_order(&items, 170, 204);
279        assert_eq!(sel, vec![0, 1]);
280        // One token less: required alone; mid/low are cut by budget.
281        let sel = select_in_order(&items, 169, 204);
282        assert_eq!(sel, vec![0]);
283        // Required is never dropped, even at a zero budget.
284        let sel = select_in_order(&items, 0, 204);
285        assert_eq!(sel, vec![0]);
286    }
287
288    // ---- (e) MMR diversity: same-owner DTOs are capped ----
289
290    #[test]
291// trace:exempt reason=internal-detail
292    fn mmr_caps_same_owner_dtos() {
293        let ranked: Vec<(String, f64)> = (0..5)
294            .map(|i| (format!("Order.dto{i}"), 0.9 - 0.1 * i as f64))
295            .collect();
296        // All five share the same owner group → similarity 1.0 between any
297        // pair. With a small budget, at most 2 are selected.
298        let same_owner = |a: &str, b: &str| -> f64 {
299            if a.starts_with("Order.") && b.starts_with("Order.") {
300                1.0
301            } else {
302                0.0
303            }
304        };
305        let sel = mmr_diversify_default(&ranked, same_owner, 2);
306        assert_eq!(sel.len(), 2);
307        // The two highest-scoring survive (identical penalty).
308        assert_eq!(sel[0], "Order.dto0");
309        assert_eq!(sel[1], "Order.dto1");
310
311        // Budget larger than the list returns everything.
312        let all = mmr_diversify_default(&ranked, same_owner, 10);
313        assert_eq!(all.len(), 5);
314    }
315
316    #[test]
317// trace:exempt reason=internal-detail
318    fn mmr_keeps_diverse_items() {
319        let ranked = vec![
320            ("a".to_string(), 0.9),
321            ("b".to_string(), 0.8),
322            ("c".to_string(), 0.7),
323        ];
324        // Distinct groups → similarity 0 → pure score order.
325        let distinct = |_: &str, _: &str| 0.0;
326        let sel = mmr_diversify_default(&ranked, distinct, 3);
327        assert_eq!(sel, vec!["a".to_string(), "b".to_string(), "c".to_string()]);
328
329        // Lambda 0 = pure diversity: second pick is the least similar to
330        // the first (score order with all-zero similarity ties breaks to
331        // the earliest).
332        let sel = mmr_diversify(&ranked, distinct, 0.0, 3);
333        assert_eq!(sel.len(), 3);
334
335        // Empty ranked / zero budget.
336        assert!(mmr_diversify_default(&[], distinct, 5).is_empty());
337        assert!(mmr_diversify_default(&ranked, distinct, 0).is_empty());
338    }
339
340    // ---- (f) token-aware quota enforcement ----
341
342    #[test]
343// trace:exempt reason=internal-detail
344    fn quotas_are_token_aware() {
345        // One kind dominates the candidate pool (100 of 140 candidates);
346        // caps derive from available TOKENS, not candidate counts.
347        let mut ranked: Vec<(String, f64)> = Vec::new();
348        for i in 0..100 {
349            ranked.push((format!("pub:{i}"), 1.0 - i as f64 / 200.0));
350        }
351        for i in 0..20 {
352            ranked.push((format!("core:{i}"), 1.0 - i as f64 / 200.0));
353        }
354        for i in 0..20 {
355            ranked.push((format!("types:{i}"), 1.0 - i as f64 / 200.0));
356        }
357
358        fn kind_of(id: &str) -> &str {
359            if id.starts_with("pub:") {
360                "public"
361            } else if id.starts_with("core:") {
362                "core"
363            } else {
364                "types"
365            }
366        }
367        fn other_kind(_: &str) -> &str {
368            "other"
369        }
370        let quotas = vec![
371            ("public".to_string(), 0.50),
372            ("core".to_string(), 0.25),
373            ("types".to_string(), 0.25),
374        ];
375        // 1400 tokens available: caps 700/350/350. At 10 tokens each the
376        // dominant kind is capped at 70 entries (not its 100-candidate
377        // count share); the under-represented kinds keep all 20 entries.
378        let cost10 = |_: &str| 10usize;
379        let sel = enforce_quotas(&ranked, kind_of, &quotas, 1400, cost10);
380        let count = |k: &str| sel.iter().filter(|id| kind_of(id.as_str()) == k).count();
381        assert_eq!(count("public"), 70);
382        assert_eq!(count("core"), 20);
383        assert_eq!(count("types"), 20);
384        // Rank order is preserved: the first accepted per kind is the
385        // first candidate of that kind in the ranked list.
386        assert_eq!(sel[0], "pub:0");
387        assert_eq!(sel[70], "core:0");
388
389        // Expensive candidates exhaust a kind's allocation faster: at 50
390        // tokens each the public cap (700) admits 14 entries, not 70.
391        let cost50 = |_: &str| 50usize;
392        let sel50 = enforce_quotas(&ranked, kind_of, &quotas, 1400, cost50);
393        assert_eq!(
394            sel50.iter().filter(|id| kind_of(id.as_str()) == "public").count(),
395            14
396        );
397
398        // A capped-out dominant kind rebalances to the next group: the
399        // first core candidate follows the last accepted public one.
400        let sel = enforce_quotas(&ranked, kind_of, &quotas, 1400, cost10);
401        let core_first = sel.iter().position(|id| kind_of(id.as_str()) == "core").unwrap();
402        assert_eq!(sel[core_first], "core:0");
403
404        // Kinds without a quota entry are uncapped.
405        let sel_other = enforce_quotas(
406            &[("x".to_string(), 0.5), ("y".to_string(), 0.5)],
407            other_kind,
408            &[],
409            100,
410            |_: &str| 10,
411        );
412        assert_eq!(sel_other.len(), 2);
413    }
414}