lean_ctx_server/tools/
ctx_smart_read.rs1use lean_ctx::core::auto_mode_resolver::{self, AutoModeContext};
3use lean_ctx::core::cache::SessionCache;
4use lean_ctx::core::tokens::count_tokens;
5
6pub fn select_mode(cache: &SessionCache, path: &str) -> String {
7 select_mode_with_task(cache, path, None)
8}
9
10pub fn select_mode_with_task(cache: &SessionCache, path: &str, task: Option<&str>) -> String {
12 if let Ok(meta) = std::fs::metadata(path) {
13 let cap = lean_ctx::core::limits::max_read_bytes() as u64;
14 if meta.len() > cap {
15 return "full".to_string();
16 }
17 }
18
19 let token_count = cache
24 .get(path)
25 .filter(|e| !lean_ctx::core::cache::is_cache_entry_stale(path, e.stored_mtime))
26 .map(|e| e.original_tokens)
27 .filter(|t| *t > 0)
28 .unwrap_or_else(|| {
33 crate::tools::ctx_read::read_file_lossy(path).map_or(0, |c| count_tokens(&c))
34 });
35
36 let ctx = AutoModeContext {
37 path,
38 token_count,
39 line_count: None,
40 task,
41 cache: Some(cache),
42 };
43 auto_mode_resolver::resolve(&ctx).mode
44}
45
46pub fn select_unranged_mode(cache: &SessionCache, path: &str, task: Option<&str>) -> String {
52 if std::fs::metadata(path)
54 .is_ok_and(|meta| meta.len() > lean_ctx::core::limits::max_read_bytes() as u64)
55 {
56 return "full".to_string();
57 }
58 let (token_count, line_count) = cache
59 .get(path)
60 .filter(|e| !lean_ctx::core::cache::is_cache_entry_stale(path, e.stored_mtime))
61 .filter(|e| e.original_tokens > 0)
62 .map(|e| (e.original_tokens, e.line_count))
63 .unwrap_or_else(|| {
64 crate::tools::ctx_read::read_file_lossy(path)
65 .map_or((0, 0), |c| (count_tokens(&c), c.lines().count()))
66 });
67 let ctx = AutoModeContext {
68 path,
69 token_count,
70 line_count: Some(line_count),
71 task,
72 cache: Some(cache),
73 };
74 auto_mode_resolver::unranged_view(auto_mode_resolver::resolve(&ctx), path, line_count).mode
75}
76
77pub fn is_code_ext(ext: &str) -> bool {
78 matches!(
79 ext,
80 "rs" | "ts"
81 | "tsx"
82 | "js"
83 | "jsx"
84 | "py"
85 | "go"
86 | "java"
87 | "c"
88 | "cpp"
89 | "cc"
90 | "h"
91 | "hpp"
92 | "rb"
93 | "cs"
94 | "kt"
95 | "swift"
96 | "php"
97 | "zig"
98 | "ex"
99 | "exs"
100 | "scala"
101 | "sc"
102 | "dart"
103 | "sh"
104 | "bash"
105 | "svelte"
106 | "vue"
107 )
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn test_code_detection() {
116 assert!(is_code_ext("rs"));
117 assert!(is_code_ext("py"));
118 assert!(is_code_ext("tsx"));
119 assert!(!is_code_ext("json"));
120 }
121
122 #[test]
127 fn a_withheld_source_does_not_steer_the_mode() {
128 let _data = lean_ctx::core::data_dir::isolated_data_dir();
129 let policy = lean_ctx::core::policy::load(
130 "name = \"ctx-smart-read-test\"\nversion = \"1.0.0\"\ndescription = \"test\"\n\
131 [filters]\nclassification = \"block\"\nblocked_labels = [\"CONFIDENTIAL\"]\n",
132 )
133 .unwrap();
134 let _policy = lean_ctx::core::policy::runtime::TestPolicyOverride::set(Some(policy));
135 let project = tempfile::tempdir().unwrap();
136 let root = project.path().to_string_lossy().into_owned();
137 let body = "pub fn generated_handler(input: &str) -> usize { input.len() }\n".repeat(4000);
138 let blocked = project.path().join("blocked.rs");
139 let open = project.path().join("open.rs");
140 std::fs::write(&blocked, format!("CONFIDENTIAL\n{body}")).unwrap();
141 std::fs::write(&open, &body).unwrap();
142 let missing = project.path().join("missing.rs");
143 let cache = SessionCache::new();
144 let mode = |path: &std::path::Path| {
145 crate::tools::admitted_sources::as_agent_acquisition(&root, || {
146 select_mode(&cache, &path.to_string_lossy())
147 })
148 };
149 assert_eq!(mode(&blocked), mode(&missing));
150 let counted = crate::tools::admitted_sources::as_agent_acquisition(&root, || {
152 crate::tools::ctx_read::read_file_lossy(&open.to_string_lossy())
153 .map_or(0, |text| count_tokens(&text))
154 });
155 assert!(counted > 10_000, "{counted}");
156 }
157}