lean_ctx/core/
fep_prefetch.rs1use std::collections::HashSet;
16
17use crate::core::context_ledger::ContextLedger;
18
19const MIN_PREFETCH_WEIGHT: f64 = 0.15;
22const MAX_PREFETCH: usize = 3;
24
25pub fn prefetch_candidates(
30 project_root: &str,
31 path: &str,
32 ledger: &ContextLedger,
33) -> Vec<(String, f64)> {
34 let norm = crate::core::pathutil::normalize_tool_path(path);
35 let loaded: HashSet<String> = ledger
36 .entries
37 .iter()
38 .map(|e| crate::core::pathutil::normalize_tool_path(&e.path))
39 .collect();
40
41 crate::core::cooccurrence::related(project_root, &norm, MAX_PREFETCH * 2)
42 .into_iter()
43 .filter(|(p, w)| {
44 *w >= MIN_PREFETCH_WEIGHT
45 && p != &norm
46 && !loaded.contains(&crate::core::pathutil::normalize_tool_path(p))
47 })
48 .take(MAX_PREFETCH)
49 .collect()
50}
51
52pub fn prefetch_hint(project_root: &str, path: &str, ledger: &ContextLedger) -> Option<String> {
56 let candidates = prefetch_candidates(project_root, path, ledger);
57 if candidates.is_empty() {
58 return None;
59 }
60 crate::core::introspect::tick("fep_prefetch");
61 let names: Vec<String> = candidates
62 .iter()
63 .map(|(p, _)| crate::core::protocol::shorten_path(p))
64 .collect();
65 Some(format!("Likely next (co-accessed): {}", names.join(", ")))
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn empty_graph_yields_no_prefetch() {
74 let _env = crate::core::data_dir::test_env_lock();
75 let dir = tempfile::tempdir().unwrap();
76 crate::test_env::set_var("LEAN_CTX_DATA_DIR", dir.path());
77 let project = tempfile::tempdir().unwrap();
78 let root = project.path().to_string_lossy().to_string();
79
80 let ledger = ContextLedger::new();
81 assert!(prefetch_candidates(&root, "src/a.rs", &ledger).is_empty());
82 crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
83 }
84
85 #[test]
86 fn co_accessed_file_is_suggested() {
87 let _env = crate::core::data_dir::test_env_lock();
89 let dir = tempfile::tempdir().unwrap();
90 crate::test_env::set_var("LEAN_CTX_DATA_DIR", dir.path());
91 let project = tempfile::tempdir().unwrap();
92 let root = project.path().to_string_lossy().to_string();
93
94 for _ in 0..5 {
96 crate::core::cooccurrence::record_access(
97 &root,
98 &["src/a.rs".to_string(), "src/b.rs".to_string()],
99 );
100 }
101
102 let mut ledger = ContextLedger::new();
104 ledger.record("src/a.rs", "full", 100, 100);
105 let cands = prefetch_candidates(&root, "src/a.rs", &ledger);
106 assert!(
107 cands.iter().any(|(p, _)| p.contains("b.rs")),
108 "co-accessed B should be suggested, got {cands:?}"
109 );
110 crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
111 }
112
113 #[test]
114 fn already_loaded_file_is_not_suggested() {
115 let _env = crate::core::data_dir::test_env_lock();
117 let dir = tempfile::tempdir().unwrap();
118 crate::test_env::set_var("LEAN_CTX_DATA_DIR", dir.path());
119 let project = tempfile::tempdir().unwrap();
120 let root = project.path().to_string_lossy().to_string();
121
122 for _ in 0..5 {
123 crate::core::cooccurrence::record_access(
124 &root,
125 &["src/a.rs".to_string(), "src/b.rs".to_string()],
126 );
127 }
128 let mut ledger = ContextLedger::new();
129 ledger.record("src/a.rs", "full", 100, 100);
130 ledger.record("src/b.rs", "full", 100, 100);
131 let cands = prefetch_candidates(&root, "src/a.rs", &ledger);
132 assert!(
133 !cands.iter().any(|(p, _)| p.contains("b.rs")),
134 "already-loaded B must not be prefetched"
135 );
136 crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
137 }
138}