Skip to main content

lean_ctx/core/context_prefetch/
preloader.rs

1//! Context prefetch planning from file-access trajectory predictions.
2//!
3//! Filters low-confidence and already-loaded files to build a bounded
4//! preload plan for proactive context warming.
5
6use super::trajectory::FileTrajectory;
7
8/// A prefetch plan: files to preload and their predicted relevance.
9#[derive(Debug, Clone)]
10pub struct PrefetchPlan {
11    /// Files selected for prefetch, highest confidence first.
12    pub files: Vec<PrefetchEntry>,
13    /// Sum of estimated token sizes; 0 until size integration is wired.
14    pub total_predicted_tokens: usize,
15}
16
17/// One file candidate in a prefetch plan.
18#[derive(Debug, Clone)]
19pub struct PrefetchEntry {
20    /// File path to preload.
21    pub path: String,
22    /// Transition probability in `(0.0, 1.0]`.
23    pub confidence: f64,
24    /// Human-readable selection rationale.
25    pub reason: &'static str,
26}
27
28/// Build a prefetch plan from trajectory predictions and co-access data.
29///
30/// Predictions at or below `min_confidence` and files already present in the
31/// current context are excluded.
32pub(crate) fn build_prefetch_plan(
33    trajectory: &FileTrajectory,
34    loaded_files: &[&str],
35    max_files: usize,
36    min_confidence: f64,
37) -> PrefetchPlan {
38    let files: Vec<PrefetchEntry> = trajectory
39        .predict(max_files.saturating_add(loaded_files.len()))
40        .into_iter()
41        .filter(|(path, confidence)| {
42            *confidence > min_confidence && !loaded_files.contains(&path.as_str())
43        })
44        .take(max_files)
45        .map(|(path, confidence)| PrefetchEntry {
46            path,
47            confidence,
48            reason: "trajectory transition",
49        })
50        .collect();
51    // Estimate unavailable without reading files; set to 0 until integration wiring provides cached sizes.
52    let total_predicted_tokens = 0;
53
54    PrefetchPlan {
55        files,
56        total_predicted_tokens,
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn empty_trajectory_gives_empty_plan() {
66        let plan = build_prefetch_plan(&FileTrajectory::new(10), &[], 3, 0.2);
67        assert!(plan.files.is_empty());
68        assert_eq!(plan.total_predicted_tokens, 0);
69    }
70
71    #[test]
72    fn loaded_files_are_excluded() {
73        let mut trajectory = FileTrajectory::new(10);
74        for path in ["src/a.rs", "src/b.rs", "src/a.rs"] {
75            trajectory.record(path);
76        }
77
78        let plan = build_prefetch_plan(&trajectory, &["src/b.rs"], 3, 0.2);
79        assert!(plan.files.is_empty());
80    }
81
82    #[test]
83    fn low_confidence_filtered() {
84        let mut trajectory = FileTrajectory::new(10);
85        for path in ["src/a.rs", "src/b.rs", "src/a.rs", "src/c.rs", "src/a.rs"] {
86            trajectory.record(path);
87        }
88
89        let plan = build_prefetch_plan(&trajectory, &[], 3, 0.5);
90        assert!(plan.files.is_empty());
91    }
92
93    #[test]
94    fn selected_files_have_zero_token_estimate_until_wired() {
95        let mut trajectory = FileTrajectory::new(10);
96        for path in ["src/a.rs", "src/b.rs", "src/a.rs"] {
97            trajectory.record(path);
98        }
99
100        let plan = build_prefetch_plan(&trajectory, &[], 1, 0.2);
101        assert_eq!(plan.files.len(), 1);
102        assert_eq!(plan.total_predicted_tokens, 0);
103    }
104}