use std::collections::HashSet;
use crate::core::context_ledger::ContextLedger;
const MIN_PREFETCH_WEIGHT: f64 = 0.15;
const MAX_PREFETCH: usize = 3;
pub fn prefetch_candidates(
project_root: &str,
path: &str,
ledger: &ContextLedger,
) -> Vec<(String, f64)> {
let norm = crate::core::pathutil::normalize_tool_path(path);
let loaded: HashSet<String> = ledger
.entries
.iter()
.map(|e| crate::core::pathutil::normalize_tool_path(&e.path))
.collect();
crate::core::cooccurrence::related(project_root, &norm, MAX_PREFETCH * 2)
.into_iter()
.filter(|(p, w)| {
*w >= MIN_PREFETCH_WEIGHT
&& p != &norm
&& !loaded.contains(&crate::core::pathutil::normalize_tool_path(p))
})
.take(MAX_PREFETCH)
.collect()
}
pub fn prefetch_hint(project_root: &str, path: &str, ledger: &ContextLedger) -> Option<String> {
let candidates = prefetch_candidates(project_root, path, ledger);
if candidates.is_empty() {
return None;
}
crate::core::introspect::tick("fep_prefetch");
let names: Vec<String> = candidates
.iter()
.map(|(p, _)| crate::core::protocol::shorten_path(p))
.collect();
Some(format!("Likely next (co-accessed): {}", names.join(", ")))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_graph_yields_no_prefetch() {
let _env = crate::core::data_dir::test_env_lock();
let dir = tempfile::tempdir().unwrap();
crate::test_env::set_var("LEAN_CTX_DATA_DIR", dir.path());
let project = tempfile::tempdir().unwrap();
let root = project.path().to_string_lossy().to_string();
let ledger = ContextLedger::new();
assert!(prefetch_candidates(&root, "src/a.rs", &ledger).is_empty());
crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
}
#[test]
fn co_accessed_file_is_suggested() {
let _env = crate::core::data_dir::test_env_lock();
let dir = tempfile::tempdir().unwrap();
crate::test_env::set_var("LEAN_CTX_DATA_DIR", dir.path());
let project = tempfile::tempdir().unwrap();
let root = project.path().to_string_lossy().to_string();
for _ in 0..5 {
crate::core::cooccurrence::record_access(
&root,
&["src/a.rs".to_string(), "src/b.rs".to_string()],
);
}
let mut ledger = ContextLedger::new();
ledger.record("src/a.rs", "full", 100, 100);
let cands = prefetch_candidates(&root, "src/a.rs", &ledger);
assert!(
cands.iter().any(|(p, _)| p.contains("b.rs")),
"co-accessed B should be suggested, got {cands:?}"
);
crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
}
#[test]
fn already_loaded_file_is_not_suggested() {
let _env = crate::core::data_dir::test_env_lock();
let dir = tempfile::tempdir().unwrap();
crate::test_env::set_var("LEAN_CTX_DATA_DIR", dir.path());
let project = tempfile::tempdir().unwrap();
let root = project.path().to_string_lossy().to_string();
for _ in 0..5 {
crate::core::cooccurrence::record_access(
&root,
&["src/a.rs".to_string(), "src/b.rs".to_string()],
);
}
let mut ledger = ContextLedger::new();
ledger.record("src/a.rs", "full", 100, 100);
ledger.record("src/b.rs", "full", 100, 100);
let cands = prefetch_candidates(&root, "src/a.rs", &ledger);
assert!(
!cands.iter().any(|(p, _)| p.contains("b.rs")),
"already-loaded B must not be prefetched"
);
crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
}
}