Skip to main content

lean_ctx/core/
context_compiler.rs

1//! Context Compiler -- builds minimal context packages under budget constraints.
2//!
3//! Physical metaphor: Free Energy minimization.
4//! F = E - TS, where E = token cost, T = budget pressure, S = information (Phi).
5//!
6//! Algorithm:
7//!   1. LOAD    ledger items + active overlays -> candidates
8//!   2. SCORE   Phi(i,t) for each candidate (Context Field)
9//!   3. SELECT  greedy knapsack with view selection
10//!   4. DEDUP   redundancy removal via Jaccard
11//!   5. ORDER   Lost-in-the-Middle reorder (LiTM profile)
12//!   6. RENDER  output in the requested mode
13//!   7. PROVE   record provenance in evidence ledger
14
15use serde::Serialize;
16
17use super::context_field::{
18    ContextItemId, ContextKind, ContextState, TokenBudget, ViewCosts, ViewKind, efficiency,
19};
20use super::entropy::jaccard_similarity;
21
22/// Compilation output mode.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum CompileMode {
25    HandleManifest,
26    Compressed,
27    FullPrompt,
28}
29
30impl CompileMode {
31    pub fn parse(s: &str) -> Self {
32        match s.trim().to_lowercase().as_str() {
33            "compressed" => Self::Compressed,
34            "full" | "full_prompt" => Self::FullPrompt,
35            _ => Self::HandleManifest,
36        }
37    }
38
39    pub(crate) fn as_str(&self) -> &'static str {
40        match self {
41            Self::HandleManifest => "handle_manifest",
42            Self::Compressed => "compressed",
43            Self::FullPrompt => "full_prompt",
44        }
45    }
46}
47
48/// A candidate item ready for selection.
49#[derive(Debug, Clone)]
50pub struct CompileCandidate {
51    pub id: ContextItemId,
52    pub kind: ContextKind,
53    pub path: String,
54    pub state: ContextState,
55    pub phi: f64,
56    pub view_costs: ViewCosts,
57    pub selected_view: ViewKind,
58    pub selected_tokens: usize,
59    pub pinned: bool,
60    /// Content fingerprint used for redundancy/MMR comparison (#5). A short
61    /// signature or token sketch of the item's content; when `None`, selection
62    /// falls back to the path (so callers that don't provide content behave as
63    /// before). Comparing content — not paths — is what makes dedup correct.
64    pub content_sketch: Option<String>,
65}
66
67/// Result of a compilation run.
68#[derive(Debug, Clone, Serialize)]
69pub struct CompileResult {
70    pub run_id: String,
71    pub mode: String,
72    pub budget_total: usize,
73    pub budget_used: usize,
74    pub items_considered: usize,
75    pub items_selected: usize,
76    pub items_excluded: usize,
77    pub items_pinned: usize,
78    pub selected: Vec<SelectedItem>,
79    pub excluded_reasons: Vec<ExcludedItem>,
80    pub warnings: Vec<String>,
81}
82
83#[derive(Debug, Clone, PartialEq, Serialize)]
84pub struct SelectedItem {
85    pub id: String,
86    pub path: String,
87    pub view: String,
88    pub tokens: usize,
89    pub phi: f64,
90    pub pinned: bool,
91}
92
93#[derive(Debug, Clone, PartialEq, Serialize)]
94pub struct ExcludedItem {
95    pub id: String,
96    pub path: String,
97    pub reason: String,
98}
99
100/// Pure output of explicit candidate selection.
101///
102/// This boundary intentionally excludes clocks, process identity, providers,
103/// persistence, global policy state, and instrumentation. Callers supply all
104/// selection inputs through `candidates` and `budget`.
105#[derive(Debug, Clone, PartialEq)]
106pub(crate) struct CompileSelection {
107    pub budget_total: usize,
108    pub budget_used: usize,
109    pub items_considered: usize,
110    pub items_selected: usize,
111    pub items_excluded: usize,
112    pub items_pinned: usize,
113    pub selected: Vec<SelectedItem>,
114    pub excluded_reasons: Vec<ExcludedItem>,
115    pub warnings: Vec<String>,
116    redundancy_applied: bool,
117}
118
119/// Compile a minimal context package from candidates under budget constraints.
120///
121/// This implements a greedy knapsack: pinned items first, then by efficiency
122/// (Phi/token), with automatic view downgrade under budget pressure.
123pub fn compile(
124    candidates: &[CompileCandidate],
125    budget: TokenBudget,
126    mode: CompileMode,
127) -> CompileResult {
128    let run_id = format!(
129        "run_{}_{}",
130        chrono::Utc::now().format("%Y%m%d_%H%M%S"),
131        std::process::id() % 1000
132    );
133    let selection = select_explicit(candidates, budget);
134    if selection.redundancy_applied {
135        crate::core::introspect::tick("integration_phi");
136    }
137
138    CompileResult {
139        run_id,
140        mode: mode.as_str().to_string(),
141        budget_total: selection.budget_total,
142        budget_used: selection.budget_used,
143        items_considered: selection.items_considered,
144        items_selected: selection.items_selected,
145        items_excluded: selection.items_excluded,
146        items_pinned: selection.items_pinned,
147        selected: selection.selected,
148        excluded_reasons: selection.excluded_reasons,
149        warnings: selection.warnings,
150    }
151}
152
153/// Select a bounded context package from fully explicit inputs.
154pub(crate) fn select_explicit(
155    candidates: &[CompileCandidate],
156    budget: TokenBudget,
157) -> CompileSelection {
158    let mut selected: Vec<SelectedItem> = Vec::new();
159    let mut excluded: Vec<ExcludedItem> = Vec::new();
160    let mut warnings: Vec<String> = Vec::new();
161    let mut tokens_used: usize = 0;
162    let remaining = budget.remaining();
163
164    let (pinned, unpinned): (Vec<_>, Vec<_>) = candidates
165        .iter()
166        .partition(|c| c.pinned || c.state == ContextState::Pinned);
167
168    for c in &pinned {
169        if c.state == ContextState::Excluded {
170            excluded.push(ExcludedItem {
171                id: c.id.to_string(),
172                path: c.path.clone(),
173                reason: "excluded by overlay".to_string(),
174            });
175            continue;
176        }
177        let Some((view, tokens)) =
178            best_affordable_view(&c.view_costs, remaining.saturating_sub(tokens_used))
179        else {
180            excluded.push(ExcludedItem {
181                id: c.id.to_string(),
182                path: c.path.clone(),
183                reason: "budget cannot fit a declared pinned view".to_owned(),
184            });
185            continue;
186        };
187        tokens_used = tokens_used.saturating_add(tokens);
188        selected.push(SelectedItem {
189            id: c.id.to_string(),
190            path: c.path.clone(),
191            view: view.as_str().to_string(),
192            tokens,
193            phi: c.phi,
194            pinned: true,
195        });
196    }
197
198    // Steps 2+3: SCORE + SELECT via greedy Maximal Marginal Relevance (#5).
199    // Relevance = normalized efficiency (Phi/token, keeps the knapsack budget-
200    // aware); penalty = max content similarity to the already-selected set, so a
201    // near-duplicate of something already chosen loses to a complementary item.
202    // Deterministic: fixed λ, fixed tie-break (efficiency, then id).
203    let pickable: Vec<usize> = unpinned
204        .iter()
205        .enumerate()
206        .filter(|(_, c)| c.state != ContextState::Excluded)
207        .map(|(i, _)| i)
208        .collect();
209
210    let effs: Vec<f64> = unpinned
211        .iter()
212        .map(|c| {
213            let best_tokens = c
214                .view_costs
215                .cheapest_content_view()
216                .map_or(c.selected_tokens, |(_, t)| t);
217            efficiency(c.phi, best_tokens.max(1))
218        })
219        .collect();
220    let max_eff = effs
221        .iter()
222        .copied()
223        .fold(0.0_f64, f64::max)
224        .max(f64::MIN_POSITIVE);
225
226    // Sketches of already-selected items (pinned first) drive the redundancy term.
227    let mut selected_sketches: Vec<String> = selected
228        .iter()
229        .map(|s| sketch_of(candidates, &s.id, &s.path))
230        .collect();
231    let mut redundancy_applied = false;
232    let mut remaining_idx: Vec<usize> = pickable;
233
234    while !remaining_idx.is_empty() {
235        let budget_left = remaining.saturating_sub(tokens_used);
236        if budget_left == 0 {
237            break;
238        }
239        // Pick the highest-MMR candidate that still fits the budget.
240        let mut best: Option<(usize, usize, f64, ViewKind, usize)> = None;
241        for (pos, &idx) in remaining_idx.iter().enumerate() {
242            let c = &unpinned[idx];
243            let Some((view, tokens)) = best_affordable_view(&c.view_costs, budget_left) else {
244                continue;
245            };
246            let sketch = candidate_sketch(c);
247            let max_sim = selected_sketches
248                .iter()
249                .map(|s| jaccard_similarity(s, &sketch))
250                .fold(0.0_f64, f64::max);
251            if max_sim > 0.0 {
252                redundancy_applied = true;
253            }
254            let norm_eff = effs[idx] / max_eff;
255            let mmr = crate::core::context_field::mmr_score(
256                norm_eff,
257                max_sim,
258                crate::core::context_field::MMR_LAMBDA,
259            );
260            let better = match best {
261                None => true,
262                Some((_, best_idx, best_mmr, _, _)) => {
263                    mmr > best_mmr
264                        || (mmr == best_mmr && effs[idx] > effs[best_idx])
265                        || (mmr == best_mmr
266                            && (effs[idx] - effs[best_idx]).abs() < f64::EPSILON
267                            && c.id.to_string() < unpinned[best_idx].id.to_string())
268                }
269            };
270            if better {
271                best = Some((pos, idx, mmr, view, tokens));
272            }
273        }
274
275        let Some((pos, idx, _mmr, view, tokens)) = best else {
276            // Nothing else fits the remaining budget.
277            break;
278        };
279        remaining_idx.remove(pos);
280        let c = &unpinned[idx];
281        tokens_used = tokens_used.saturating_add(tokens);
282        selected_sketches.push(candidate_sketch(c));
283        selected.push(SelectedItem {
284            id: c.id.to_string(),
285            path: c.path.clone(),
286            view: view.as_str().to_string(),
287            tokens,
288            phi: c.phi,
289            pinned: false,
290        });
291    }
292
293    // Anything still unpicked didn't fit the budget.
294    for idx in remaining_idx {
295        let c = &unpinned[idx];
296        excluded.push(ExcludedItem {
297            id: c.id.to_string(),
298            path: c.path.clone(),
299            reason: "budget exhausted".to_string(),
300        });
301    }
302    for c in candidates
303        .iter()
304        .filter(|c| c.state == ContextState::Excluded)
305    {
306        if !excluded.iter().any(|e| e.id == c.id.to_string()) {
307            excluded.push(ExcludedItem {
308                id: c.id.to_string(),
309                path: c.path.clone(),
310                reason: "excluded by overlay/policy".to_string(),
311            });
312        }
313    }
314
315    // Step 4: DEDUP — drop items whose CONTENT is >70% redundant with an
316    // already-kept item of equal-or-higher Phi (IIT non-redundancy). The old
317    // code compared file *paths* and mis-indexed the kept list; we now compare
318    // each item's content sketch against the sketches we actually kept.
319    let mut deduped: Vec<SelectedItem> = Vec::with_capacity(selected.len());
320    let mut kept_sketches: Vec<String> = Vec::with_capacity(selected.len());
321    let mut dedup_tokens = 0usize;
322    for item in &selected {
323        let sketch_i = sketch_of(candidates, &item.id, &item.path);
324        let dominated = deduped
325            .iter()
326            .zip(&kept_sketches)
327            .any(|(existing, sketch_j)| {
328                if sketch_i.is_empty() || sketch_j.is_empty() {
329                    return false;
330                }
331                jaccard_similarity(sketch_j, &sketch_i) > DEDUP_JACCARD_THRESHOLD
332                    && existing.phi >= item.phi
333            });
334        if dominated {
335            excluded.push(ExcludedItem {
336                id: item.id.clone(),
337                path: item.path.clone(),
338                reason: "dedup: >70% content overlap with higher-Phi item".to_string(),
339            });
340        } else {
341            dedup_tokens += item.tokens;
342            kept_sketches.push(sketch_i);
343            deduped.push(item.clone());
344        }
345    }
346    selected = deduped;
347    tokens_used = dedup_tokens;
348
349    // Step 5: ORDER — Lost-in-the-Middle (LiTM) reorder.
350    // High-Phi items at the beginning and end; medium-Phi in the middle.
351    if selected.len() >= 3 {
352        selected.sort_by(|a, b| {
353            b.phi
354                .partial_cmp(&a.phi)
355                .unwrap_or(std::cmp::Ordering::Equal)
356        });
357        let n = selected.len();
358        let mut reordered = Vec::with_capacity(n);
359        let mut left = Vec::new();
360        let mut right = Vec::new();
361        for (i, item) in selected.into_iter().enumerate() {
362            if i % 2 == 0 {
363                left.push(item);
364            } else {
365                right.push(item);
366            }
367        }
368        right.reverse();
369        reordered.extend(left);
370        reordered.extend(right);
371        selected = reordered;
372    }
373
374    if tokens_used as f64 / budget.total.max(1) as f64 > 0.9 {
375        warnings.push(format!(
376            "Context budget >90% utilized ({tokens_used}/{} tokens)",
377            budget.total
378        ));
379    }
380
381    CompileSelection {
382        budget_total: budget.total,
383        budget_used: tokens_used,
384        items_considered: candidates.len(),
385        items_selected: selected.len(),
386        items_excluded: excluded.len(),
387        items_pinned: pinned.len(),
388        selected,
389        excluded_reasons: excluded,
390        warnings,
391        redundancy_applied,
392    }
393}
394
395/// Above this content-Jaccard, a lower-or-equal-Phi item is treated as a
396/// redundant duplicate and dropped during DEDUP (#5).
397const DEDUP_JACCARD_THRESHOLD: f64 = 0.7;
398
399/// Content fingerprint of a candidate for redundancy comparison (#5): its
400/// explicit `content_sketch` when provided, else the path as a degraded fallback
401/// (so callers that supply no content behave exactly as before).
402fn candidate_sketch(c: &CompileCandidate) -> String {
403    c.content_sketch.clone().unwrap_or_else(|| c.path.clone())
404}
405
406/// Look up a candidate by its id string and return its content sketch, falling
407/// back to `fallback_path` when the candidate or its sketch is missing.
408fn sketch_of(candidates: &[CompileCandidate], id: &str, fallback_path: &str) -> String {
409    candidates
410        .iter()
411        .find(|c| c.id.to_string() == id)
412        .and_then(|c| c.content_sketch.clone())
413        .unwrap_or_else(|| fallback_path.to_string())
414}
415
416/// Select the best view that fits within the budget, preferring denser views.
417fn best_affordable_view(costs: &ViewCosts, budget_left: usize) -> Option<(ViewKind, usize)> {
418    let mut options: Vec<(ViewKind, usize)> = costs
419        .estimates
420        .iter()
421        .map(|(&v, &t)| (v, t))
422        .filter(|(_, t)| *t <= budget_left && *t > 0)
423        .collect();
424
425    options.sort_by_key(|(v, _)| v.density_rank());
426
427    // A missing or unaffordable view cannot become a fabricated short handle.
428    options.first().copied()
429}
430
431/// Format the compilation result for display.
432pub fn format_compile_result(result: &CompileResult) -> String {
433    let mut out = String::new();
434    out.push_str(&format!(
435        "[compiled] {} mode, {}/{} tokens\n",
436        result.mode, result.budget_used, result.budget_total
437    ));
438    out.push_str(&format!(
439        "Selected: {} items, Excluded: {}, Pinned: {}\n\n",
440        result.items_selected, result.items_excluded, result.items_pinned
441    ));
442
443    if !result.selected.is_empty() {
444        out.push_str("Included:\n");
445        for item in &result.selected {
446            let pin_tag = if item.pinned { " [pinned]" } else { "" };
447            out.push_str(&format!(
448                "  {} {} {}t phi={:.2}{}\n",
449                item.path, item.view, item.tokens, item.phi, pin_tag
450            ));
451        }
452    }
453
454    if !result.excluded_reasons.is_empty() {
455        out.push('\n');
456        out.push_str("Excluded:\n");
457        for item in &result.excluded_reasons {
458            out.push_str(&format!("  {} — {}\n", item.path, item.reason));
459        }
460    }
461
462    if !result.warnings.is_empty() {
463        out.push('\n');
464        for w in &result.warnings {
465            out.push_str(&format!("WARNING: {w}\n"));
466        }
467    }
468
469    out
470}
471
472#[cfg(test)]
473mod tests {
474    use super::*;
475
476    fn make_candidate(path: &str, phi: f64, full_tokens: usize, pinned: bool) -> CompileCandidate {
477        CompileCandidate {
478            id: ContextItemId::from_file(path),
479            kind: ContextKind::File,
480            path: path.to_string(),
481            state: if pinned {
482                ContextState::Pinned
483            } else {
484                ContextState::Included
485            },
486            phi,
487            view_costs: ViewCosts::from_full_tokens(full_tokens),
488            selected_view: ViewKind::Full,
489            selected_tokens: full_tokens,
490            pinned,
491            content_sketch: None,
492        }
493    }
494
495    fn make_candidate_with_sketch(
496        path: &str,
497        phi: f64,
498        full_tokens: usize,
499        sketch: &str,
500    ) -> CompileCandidate {
501        CompileCandidate {
502            content_sketch: Some(sketch.to_string()),
503            ..make_candidate(path, phi, full_tokens, false)
504        }
505    }
506
507    #[test]
508    fn compile_selects_highest_efficiency_first() {
509        let candidates = vec![
510            make_candidate("low_eff.rs", 0.1, 5000, false),
511            make_candidate("high_eff.rs", 0.9, 200, false),
512        ];
513        let budget = TokenBudget {
514            total: 10000,
515            used: 0,
516        };
517        let result = compile(&candidates, budget, CompileMode::HandleManifest);
518        assert_eq!(result.items_selected, 2);
519        assert_eq!(result.selected[0].path, "high_eff.rs");
520    }
521
522    #[test]
523    fn compile_respects_budget() {
524        let candidates = vec![
525            make_candidate("big.rs", 0.5, 8000, false),
526            make_candidate("small.rs", 0.5, 200, false),
527        ];
528        let budget = TokenBudget {
529            total: 2000,
530            used: 0,
531        };
532        let result = compile(&candidates, budget, CompileMode::Compressed);
533        let total_tokens: usize = result.selected.iter().map(|s| s.tokens).sum();
534        assert!(
535            total_tokens <= 2000,
536            "selected tokens {total_tokens} should fit in budget 2000"
537        );
538    }
539
540    #[test]
541    fn compile_includes_pinned_first() {
542        let candidates = vec![
543            make_candidate("normal.rs", 0.9, 200, false),
544            make_candidate("pinned.rs", 0.1, 300, true),
545        ];
546        let budget = TokenBudget {
547            total: 10000,
548            used: 0,
549        };
550        let result = compile(&candidates, budget, CompileMode::HandleManifest);
551        assert!(result.selected[0].pinned, "pinned item should come first");
552    }
553
554    #[test]
555    fn compile_excludes_excluded_state() {
556        let candidates = vec![CompileCandidate {
557            state: ContextState::Excluded,
558            ..make_candidate("excluded.rs", 0.9, 100, false)
559        }];
560        let budget = TokenBudget {
561            total: 10000,
562            used: 0,
563        };
564        let result = compile(&candidates, budget, CompileMode::HandleManifest);
565        assert_eq!(result.items_selected, 0);
566        assert_eq!(result.items_excluded, 1);
567    }
568
569    #[test]
570    fn compile_downgrades_view_when_budget_tight() {
571        let candidates = vec![make_candidate("big.rs", 0.9, 5000, false)];
572        let budget = TokenBudget {
573            total: 800,
574            used: 0,
575        };
576        let result = compile(&candidates, budget, CompileMode::Compressed);
577        if let Some(item) = result.selected.first() {
578            assert_ne!(item.view, "full", "should downgrade from full under budget");
579            assert!(item.tokens <= 800);
580        }
581    }
582
583    #[test]
584    fn compile_warns_at_high_utilization() {
585        let candidates = vec![make_candidate("a.rs", 0.9, 950, false)];
586        let budget = TokenBudget {
587            total: 1000,
588            used: 0,
589        };
590        let result = compile(&candidates, budget, CompileMode::HandleManifest);
591        assert!(
592            !result.warnings.is_empty(),
593            "should warn when >90% utilized"
594        );
595    }
596
597    #[test]
598    fn dedup_drops_content_duplicate_keeps_higher_phi() {
599        // #5: two items with identical content sketches but different paths —
600        // the lower-Phi one must be dropped (content-based, not path-based).
601        let candidates = vec![
602            make_candidate_with_sketch("a.rs", 0.9, 300, "same content fingerprint here"),
603            make_candidate_with_sketch("b.rs", 0.4, 300, "same content fingerprint here"),
604        ];
605        let budget = TokenBudget {
606            total: 10000,
607            used: 0,
608        };
609        let result = compile(&candidates, budget, CompileMode::HandleManifest);
610        assert_eq!(
611            result.items_selected, 1,
612            "content duplicate should be deduped to one item"
613        );
614        assert_eq!(
615            result.selected[0].path, "a.rs",
616            "the higher-Phi duplicate must survive"
617        );
618    }
619
620    #[test]
621    fn distinct_content_is_not_deduped() {
622        // #5 regression: different content must NOT be treated as duplicate.
623        let candidates = vec![
624            make_candidate_with_sketch("a.rs", 0.9, 300, "alpha beta gamma"),
625            make_candidate_with_sketch("b.rs", 0.8, 300, "delta epsilon zeta"),
626        ];
627        let budget = TokenBudget {
628            total: 10000,
629            used: 0,
630        };
631        let result = compile(&candidates, budget, CompileMode::HandleManifest);
632        assert_eq!(
633            result.items_selected, 2,
634            "distinct content must both survive"
635        );
636    }
637
638    #[test]
639    fn compile_is_deterministic() {
640        // Determinism contract (#498): identical input → identical selection.
641        let candidates = vec![
642            make_candidate("a.rs", 0.7, 400, false),
643            make_candidate("b.rs", 0.6, 300, false),
644            make_candidate("c.rs", 0.8, 500, false),
645        ];
646        let budget = TokenBudget {
647            total: 5000,
648            used: 0,
649        };
650        let r1 = compile(&candidates, budget, CompileMode::Compressed);
651        let r2 = compile(&candidates, budget, CompileMode::Compressed);
652        let paths1: Vec<&str> = r1.selected.iter().map(|s| s.path.as_str()).collect();
653        let paths2: Vec<&str> = r2.selected.iter().map(|s| s.path.as_str()).collect();
654        assert_eq!(paths1, paths2, "selection order must be deterministic");
655    }
656
657    #[test]
658    fn select_explicit_golden_selection() {
659        let candidates = vec![
660            make_candidate("a.rs", 0.7, 400, false),
661            make_candidate("b.rs", 0.6, 300, true),
662            make_candidate("c.rs", 0.8, 500, false),
663        ];
664        let budget = TokenBudget {
665            total: 5000,
666            used: 250,
667        };
668
669        let result = select_explicit(&candidates, budget);
670
671        assert_eq!(
672            result
673                .selected
674                .iter()
675                .map(|item| {
676                    (
677                        item.path.as_str(),
678                        item.view.as_str(),
679                        item.tokens,
680                        item.phi,
681                        item.pinned,
682                    )
683                })
684                .collect::<Vec<_>>(),
685            vec![
686                ("c.rs", "full", 500, 0.8, false),
687                ("b.rs", "full", 300, 0.6, true),
688                ("a.rs", "full", 400, 0.7, false),
689            ]
690        );
691        assert_eq!(result.budget_total, 5000);
692        assert_eq!(result.budget_used, 1200);
693        assert_eq!(result.items_considered, 3);
694        assert_eq!(result.items_selected, 3);
695        assert_eq!(result.items_excluded, 0);
696        assert_eq!(result.items_pinned, 1);
697        assert!(
698            result.excluded_reasons.is_empty(),
699            "{:?}",
700            result.excluded_reasons
701        );
702        assert!(result.warnings.is_empty(), "{:?}", result.warnings);
703        assert!(!result.redundancy_applied);
704    }
705
706    #[test]
707    fn select_explicit_is_pure_and_repeatable() {
708        let candidates = vec![
709            make_candidate("a.rs", 0.7, 400, false),
710            make_candidate("b.rs", 0.6, 300, true),
711            make_candidate("c.rs", 0.8, 500, false),
712        ];
713        let budget = TokenBudget {
714            total: 5000,
715            used: 250,
716        };
717
718        let expected = select_explicit(&candidates, budget);
719        for _mode in [
720            CompileMode::HandleManifest,
721            CompileMode::Compressed,
722            CompileMode::FullPrompt,
723        ] {
724            assert_eq!(select_explicit(&candidates, budget), expected);
725        }
726    }
727
728    #[test]
729    fn compile_preserves_legacy_run_id_shape() {
730        let result = compile(
731            &[make_candidate("a.rs", 0.7, 400, false)],
732            TokenBudget {
733                total: 5000,
734                used: 0,
735            },
736            CompileMode::HandleManifest,
737        );
738        let parts: Vec<&str> = result.run_id.split('_').collect();
739        assert_eq!(parts.len(), 4);
740        assert_eq!(parts[0], "run");
741        assert_eq!(parts[1].len(), 8);
742        assert_eq!(parts[2].len(), 6);
743        assert!((1..=3).contains(&parts[3].len()));
744        assert!(
745            parts[1..]
746                .iter()
747                .all(|part| part.bytes().all(|byte| byte.is_ascii_digit()))
748        );
749    }
750
751    #[test]
752    fn format_compile_result_includes_key_info() {
753        let candidates = vec![
754            make_candidate("a.rs", 0.8, 500, false),
755            make_candidate("b.rs", 0.3, 200, true),
756        ];
757        let budget = TokenBudget {
758            total: 10000,
759            used: 0,
760        };
761        let result = compile(&candidates, budget, CompileMode::HandleManifest);
762        let text = format_compile_result(&result);
763        assert!(text.contains("a.rs"));
764        assert!(text.contains("b.rs"));
765        assert!(text.contains("[pinned]"));
766    }
767}