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 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, 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, Serialize)]
94pub struct ExcludedItem {
95 pub id: String,
96 pub path: String,
97 pub reason: String,
98}
99
100pub fn compile(
105 candidates: &[CompileCandidate],
106 budget: TokenBudget,
107 mode: CompileMode,
108) -> CompileResult {
109 let run_id = format!(
110 "run_{}_{}",
111 chrono::Utc::now().format("%Y%m%d_%H%M%S"),
112 std::process::id() % 1000
113 );
114
115 let mut selected: Vec<SelectedItem> = Vec::new();
116 let mut excluded: Vec<ExcludedItem> = Vec::new();
117 let mut warnings: Vec<String> = Vec::new();
118 let mut tokens_used: usize = 0;
119 let remaining = budget.remaining();
120
121 let (pinned, unpinned): (Vec<_>, Vec<_>) = candidates
122 .iter()
123 .partition(|c| c.pinned || c.state == ContextState::Pinned);
124
125 for c in &pinned {
126 if c.state == ContextState::Excluded {
127 excluded.push(ExcludedItem {
128 id: c.id.to_string(),
129 path: c.path.clone(),
130 reason: "excluded by overlay".to_string(),
131 });
132 continue;
133 }
134 let (view, tokens) =
135 best_affordable_view(&c.view_costs, remaining.saturating_sub(tokens_used));
136 tokens_used = tokens_used.saturating_add(tokens);
137 selected.push(SelectedItem {
138 id: c.id.to_string(),
139 path: c.path.clone(),
140 view: view.as_str().to_string(),
141 tokens,
142 phi: c.phi,
143 pinned: true,
144 });
145 }
146
147 let pickable: Vec<usize> = unpinned
153 .iter()
154 .enumerate()
155 .filter(|(_, c)| c.state != ContextState::Excluded)
156 .map(|(i, _)| i)
157 .collect();
158
159 let effs: Vec<f64> = unpinned
160 .iter()
161 .map(|c| {
162 let best_tokens = c
163 .view_costs
164 .cheapest_content_view()
165 .map_or(c.selected_tokens, |(_, t)| t);
166 efficiency(c.phi, best_tokens.max(1))
167 })
168 .collect();
169 let max_eff = effs
170 .iter()
171 .copied()
172 .fold(0.0_f64, f64::max)
173 .max(f64::MIN_POSITIVE);
174
175 let mut selected_sketches: Vec<String> = selected
177 .iter()
178 .map(|s| sketch_of(candidates, &s.id, &s.path))
179 .collect();
180 let mut redundancy_applied = false;
181 let mut remaining_idx: Vec<usize> = pickable;
182
183 while !remaining_idx.is_empty() {
184 let budget_left = remaining.saturating_sub(tokens_used);
185 if budget_left == 0 {
186 break;
187 }
188 let mut best: Option<(usize, usize, f64, usize)> = None; for (pos, &idx) in remaining_idx.iter().enumerate() {
191 let c = &unpinned[idx];
192 let (_, tokens) = best_affordable_view(&c.view_costs, budget_left);
193 if tokens == 0 || tokens > budget_left {
194 continue;
195 }
196 let sketch = candidate_sketch(c);
197 let max_sim = selected_sketches
198 .iter()
199 .map(|s| jaccard_similarity(s, &sketch))
200 .fold(0.0_f64, f64::max);
201 if max_sim > 0.0 {
202 redundancy_applied = true;
203 }
204 let norm_eff = effs[idx] / max_eff;
205 let mmr = crate::core::context_field::mmr_score(
206 norm_eff,
207 max_sim,
208 crate::core::context_field::MMR_LAMBDA,
209 );
210 let better = match best {
211 None => true,
212 Some((_, best_idx, best_mmr, _)) => {
213 mmr > best_mmr
214 || (mmr == best_mmr && effs[idx] > effs[best_idx])
215 || (mmr == best_mmr
216 && (effs[idx] - effs[best_idx]).abs() < f64::EPSILON
217 && c.id.to_string() < unpinned[best_idx].id.to_string())
218 }
219 };
220 if better {
221 best = Some((pos, idx, mmr, tokens));
222 }
223 }
224
225 let Some((pos, idx, _mmr, _)) = best else {
226 break;
228 };
229 remaining_idx.remove(pos);
230 let c = &unpinned[idx];
231 let (view, tokens) = best_affordable_view(&c.view_costs, budget_left);
232 tokens_used = tokens_used.saturating_add(tokens);
233 selected_sketches.push(candidate_sketch(c));
234 selected.push(SelectedItem {
235 id: c.id.to_string(),
236 path: c.path.clone(),
237 view: view.as_str().to_string(),
238 tokens,
239 phi: c.phi,
240 pinned: false,
241 });
242 }
243
244 for idx in remaining_idx {
246 let c = &unpinned[idx];
247 excluded.push(ExcludedItem {
248 id: c.id.to_string(),
249 path: c.path.clone(),
250 reason: "budget exhausted".to_string(),
251 });
252 }
253 if redundancy_applied {
254 crate::core::introspect::tick("integration_phi");
255 }
256
257 for c in candidates
258 .iter()
259 .filter(|c| c.state == ContextState::Excluded)
260 {
261 if !excluded.iter().any(|e| e.id == c.id.to_string()) {
262 excluded.push(ExcludedItem {
263 id: c.id.to_string(),
264 path: c.path.clone(),
265 reason: "excluded by overlay/policy".to_string(),
266 });
267 }
268 }
269
270 let mut deduped: Vec<SelectedItem> = Vec::with_capacity(selected.len());
275 let mut kept_sketches: Vec<String> = Vec::with_capacity(selected.len());
276 let mut dedup_tokens = 0usize;
277 for item in &selected {
278 let sketch_i = sketch_of(candidates, &item.id, &item.path);
279 let dominated = deduped
280 .iter()
281 .zip(&kept_sketches)
282 .any(|(existing, sketch_j)| {
283 if sketch_i.is_empty() || sketch_j.is_empty() {
284 return false;
285 }
286 jaccard_similarity(sketch_j, &sketch_i) > DEDUP_JACCARD_THRESHOLD
287 && existing.phi >= item.phi
288 });
289 if dominated {
290 excluded.push(ExcludedItem {
291 id: item.id.clone(),
292 path: item.path.clone(),
293 reason: "dedup: >70% content overlap with higher-Phi item".to_string(),
294 });
295 } else {
296 dedup_tokens += item.tokens;
297 kept_sketches.push(sketch_i);
298 deduped.push(item.clone());
299 }
300 }
301 selected = deduped;
302 tokens_used = dedup_tokens;
303
304 if selected.len() >= 3 {
307 selected.sort_by(|a, b| {
308 b.phi
309 .partial_cmp(&a.phi)
310 .unwrap_or(std::cmp::Ordering::Equal)
311 });
312 let n = selected.len();
313 let mut reordered = Vec::with_capacity(n);
314 let mut left = Vec::new();
315 let mut right = Vec::new();
316 for (i, item) in selected.into_iter().enumerate() {
317 if i % 2 == 0 {
318 left.push(item);
319 } else {
320 right.push(item);
321 }
322 }
323 right.reverse();
324 reordered.extend(left);
325 reordered.extend(right);
326 selected = reordered;
327 }
328
329 if tokens_used as f64 / budget.total.max(1) as f64 > 0.9 {
330 warnings.push(format!(
331 "Context budget >90% utilized ({tokens_used}/{} tokens)",
332 budget.total
333 ));
334 }
335
336 CompileResult {
337 run_id,
338 mode: mode.as_str().to_string(),
339 budget_total: budget.total,
340 budget_used: tokens_used,
341 items_considered: candidates.len(),
342 items_selected: selected.len(),
343 items_excluded: excluded.len(),
344 items_pinned: pinned.len(),
345 selected,
346 excluded_reasons: excluded,
347 warnings,
348 }
349}
350
351const DEDUP_JACCARD_THRESHOLD: f64 = 0.7;
354
355fn candidate_sketch(c: &CompileCandidate) -> String {
359 c.content_sketch.clone().unwrap_or_else(|| c.path.clone())
360}
361
362fn sketch_of(candidates: &[CompileCandidate], id: &str, fallback_path: &str) -> String {
365 candidates
366 .iter()
367 .find(|c| c.id.to_string() == id)
368 .and_then(|c| c.content_sketch.clone())
369 .unwrap_or_else(|| fallback_path.to_string())
370}
371
372fn best_affordable_view(costs: &ViewCosts, budget_left: usize) -> (ViewKind, usize) {
374 let mut options: Vec<(ViewKind, usize)> = costs
375 .estimates
376 .iter()
377 .map(|(&v, &t)| (v, t))
378 .filter(|(_, t)| *t <= budget_left && *t > 0)
379 .collect();
380
381 options.sort_by_key(|(v, _)| v.density_rank());
382
383 options
384 .first()
385 .copied()
386 .unwrap_or((ViewKind::Handle, 25.min(budget_left)))
387}
388
389pub fn format_compile_result(result: &CompileResult) -> String {
391 let mut out = String::new();
392 out.push_str(&format!(
393 "[compiled] {} mode, {}/{} tokens\n",
394 result.mode, result.budget_used, result.budget_total
395 ));
396 out.push_str(&format!(
397 "Selected: {} items, Excluded: {}, Pinned: {}\n\n",
398 result.items_selected, result.items_excluded, result.items_pinned
399 ));
400
401 if !result.selected.is_empty() {
402 out.push_str("Included:\n");
403 for item in &result.selected {
404 let pin_tag = if item.pinned { " [pinned]" } else { "" };
405 out.push_str(&format!(
406 " {} {} {}t phi={:.2}{}\n",
407 item.path, item.view, item.tokens, item.phi, pin_tag
408 ));
409 }
410 }
411
412 if !result.excluded_reasons.is_empty() {
413 out.push('\n');
414 out.push_str("Excluded:\n");
415 for item in &result.excluded_reasons {
416 out.push_str(&format!(" {} — {}\n", item.path, item.reason));
417 }
418 }
419
420 if !result.warnings.is_empty() {
421 out.push('\n');
422 for w in &result.warnings {
423 out.push_str(&format!("WARNING: {w}\n"));
424 }
425 }
426
427 out
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 fn make_candidate(path: &str, phi: f64, full_tokens: usize, pinned: bool) -> CompileCandidate {
435 CompileCandidate {
436 id: ContextItemId::from_file(path),
437 kind: ContextKind::File,
438 path: path.to_string(),
439 state: if pinned {
440 ContextState::Pinned
441 } else {
442 ContextState::Included
443 },
444 phi,
445 view_costs: ViewCosts::from_full_tokens(full_tokens),
446 selected_view: ViewKind::Full,
447 selected_tokens: full_tokens,
448 pinned,
449 content_sketch: None,
450 }
451 }
452
453 fn make_candidate_with_sketch(
454 path: &str,
455 phi: f64,
456 full_tokens: usize,
457 sketch: &str,
458 ) -> CompileCandidate {
459 CompileCandidate {
460 content_sketch: Some(sketch.to_string()),
461 ..make_candidate(path, phi, full_tokens, false)
462 }
463 }
464
465 #[test]
466 fn compile_selects_highest_efficiency_first() {
467 let candidates = vec![
468 make_candidate("low_eff.rs", 0.1, 5000, false),
469 make_candidate("high_eff.rs", 0.9, 200, false),
470 ];
471 let budget = TokenBudget {
472 total: 10000,
473 used: 0,
474 };
475 let result = compile(&candidates, budget, CompileMode::HandleManifest);
476 assert_eq!(result.items_selected, 2);
477 assert_eq!(result.selected[0].path, "high_eff.rs");
478 }
479
480 #[test]
481 fn compile_respects_budget() {
482 let candidates = vec![
483 make_candidate("big.rs", 0.5, 8000, false),
484 make_candidate("small.rs", 0.5, 200, false),
485 ];
486 let budget = TokenBudget {
487 total: 2000,
488 used: 0,
489 };
490 let result = compile(&candidates, budget, CompileMode::Compressed);
491 let total_tokens: usize = result.selected.iter().map(|s| s.tokens).sum();
492 assert!(
493 total_tokens <= 2000,
494 "selected tokens {total_tokens} should fit in budget 2000"
495 );
496 }
497
498 #[test]
499 fn compile_includes_pinned_first() {
500 let candidates = vec![
501 make_candidate("normal.rs", 0.9, 200, false),
502 make_candidate("pinned.rs", 0.1, 300, true),
503 ];
504 let budget = TokenBudget {
505 total: 10000,
506 used: 0,
507 };
508 let result = compile(&candidates, budget, CompileMode::HandleManifest);
509 assert!(result.selected[0].pinned, "pinned item should come first");
510 }
511
512 #[test]
513 fn compile_excludes_excluded_state() {
514 let candidates = vec![CompileCandidate {
515 state: ContextState::Excluded,
516 ..make_candidate("excluded.rs", 0.9, 100, false)
517 }];
518 let budget = TokenBudget {
519 total: 10000,
520 used: 0,
521 };
522 let result = compile(&candidates, budget, CompileMode::HandleManifest);
523 assert_eq!(result.items_selected, 0);
524 assert_eq!(result.items_excluded, 1);
525 }
526
527 #[test]
528 fn compile_downgrades_view_when_budget_tight() {
529 let candidates = vec![make_candidate("big.rs", 0.9, 5000, false)];
530 let budget = TokenBudget {
531 total: 800,
532 used: 0,
533 };
534 let result = compile(&candidates, budget, CompileMode::Compressed);
535 if let Some(item) = result.selected.first() {
536 assert_ne!(item.view, "full", "should downgrade from full under budget");
537 assert!(item.tokens <= 800);
538 }
539 }
540
541 #[test]
542 fn compile_warns_at_high_utilization() {
543 let candidates = vec![make_candidate("a.rs", 0.9, 950, false)];
544 let budget = TokenBudget {
545 total: 1000,
546 used: 0,
547 };
548 let result = compile(&candidates, budget, CompileMode::HandleManifest);
549 assert!(
550 !result.warnings.is_empty(),
551 "should warn when >90% utilized"
552 );
553 }
554
555 #[test]
556 fn dedup_drops_content_duplicate_keeps_higher_phi() {
557 let candidates = vec![
560 make_candidate_with_sketch("a.rs", 0.9, 300, "same content fingerprint here"),
561 make_candidate_with_sketch("b.rs", 0.4, 300, "same content fingerprint here"),
562 ];
563 let budget = TokenBudget {
564 total: 10000,
565 used: 0,
566 };
567 let result = compile(&candidates, budget, CompileMode::HandleManifest);
568 assert_eq!(
569 result.items_selected, 1,
570 "content duplicate should be deduped to one item"
571 );
572 assert_eq!(
573 result.selected[0].path, "a.rs",
574 "the higher-Phi duplicate must survive"
575 );
576 }
577
578 #[test]
579 fn distinct_content_is_not_deduped() {
580 let candidates = vec![
582 make_candidate_with_sketch("a.rs", 0.9, 300, "alpha beta gamma"),
583 make_candidate_with_sketch("b.rs", 0.8, 300, "delta epsilon zeta"),
584 ];
585 let budget = TokenBudget {
586 total: 10000,
587 used: 0,
588 };
589 let result = compile(&candidates, budget, CompileMode::HandleManifest);
590 assert_eq!(
591 result.items_selected, 2,
592 "distinct content must both survive"
593 );
594 }
595
596 #[test]
597 fn compile_is_deterministic() {
598 let candidates = vec![
600 make_candidate("a.rs", 0.7, 400, false),
601 make_candidate("b.rs", 0.6, 300, false),
602 make_candidate("c.rs", 0.8, 500, false),
603 ];
604 let budget = TokenBudget {
605 total: 5000,
606 used: 0,
607 };
608 let r1 = compile(&candidates, budget, CompileMode::Compressed);
609 let r2 = compile(&candidates, budget, CompileMode::Compressed);
610 let paths1: Vec<&str> = r1.selected.iter().map(|s| s.path.as_str()).collect();
611 let paths2: Vec<&str> = r2.selected.iter().map(|s| s.path.as_str()).collect();
612 assert_eq!(paths1, paths2, "selection order must be deterministic");
613 }
614
615 #[test]
616 fn format_compile_result_includes_key_info() {
617 let candidates = vec![
618 make_candidate("a.rs", 0.8, 500, false),
619 make_candidate("b.rs", 0.3, 200, true),
620 ];
621 let budget = TokenBudget {
622 total: 10000,
623 used: 0,
624 };
625 let result = compile(&candidates, budget, CompileMode::HandleManifest);
626 let text = format_compile_result(&result);
627 assert!(text.contains("a.rs"));
628 assert!(text.contains("b.rs"));
629 assert!(text.contains("[pinned]"));
630 }
631}