1use serde::Serialize;
16
17use super::context_field::{
18 ContextItemId, ContextKind, ContextState, TokenBudget, ViewCosts, ViewKind, efficiency,
19};
20use super::entropy::jaccard_similarity;
21
22#[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#[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 pub content_sketch: Option<String>,
65}
66
67#[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#[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
119pub 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
153pub(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 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 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 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 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 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 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 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
395const DEDUP_JACCARD_THRESHOLD: f64 = 0.7;
398
399fn candidate_sketch(c: &CompileCandidate) -> String {
403 c.content_sketch.clone().unwrap_or_else(|| c.path.clone())
404}
405
406fn 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
416fn 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 options.first().copied()
429}
430
431pub 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 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 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 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}