1use crate::core::session::SessionState;
2
3#[derive(Debug, Clone, Copy)]
4pub struct LitmProfile {
5 pub alpha: f64,
6 pub beta: f64,
7 pub gamma: f64,
8 pub name: &'static str,
9}
10
11impl LitmProfile {
12 pub const CLAUDE: Self = Self {
13 alpha: 0.92,
14 beta: 0.50,
15 gamma: 0.88,
16 name: "claude",
17 };
18 pub const GPT: Self = Self {
19 alpha: 0.90,
20 beta: 0.55,
21 gamma: 0.85,
22 name: "gpt",
23 };
24 pub const GEMINI: Self = Self {
25 alpha: 0.88,
26 beta: 0.60,
27 gamma: 0.82,
28 name: "gemini",
29 };
30 pub const DEFAULT: Self = Self::GPT;
31
32 pub fn from_client_name(client: &str) -> Self {
33 if let Ok(override_val) = std::env::var("LEAN_CTX_LITM_PROFILE") {
34 return Self::from_name(&override_val);
35 }
36 let lower = client.to_lowercase();
37 if lower.contains("claude") || lower.contains("cursor") {
38 Self::CLAUDE
39 } else if lower.contains("gemini") {
40 Self::GEMINI
41 } else {
42 Self::GPT
43 }
44 }
45
46 pub fn from_name(name: &str) -> Self {
47 match name.to_lowercase().as_str() {
48 "claude" | "cursor" => Self::CLAUDE,
49 "gemini" => Self::GEMINI,
50 "gpt" | "openai" | "codex" => Self::GPT,
51 _ => Self::DEFAULT,
52 }
53 }
54}
55
56#[cfg(test)]
57const _ALPHA: f64 = 0.9;
58#[cfg(test)]
59const _BETA: f64 = 0.55;
60#[cfg(test)]
61const _GAMMA: f64 = 0.85;
62
63pub struct PositionedOutput {
64 pub begin_block: String,
65 pub end_block: String,
66}
67
68pub fn position_optimize(session: &SessionState) -> PositionedOutput {
73 let mut begin_lines = Vec::new();
74 let mut end_lines = Vec::new();
75
76 begin_lines.push(format!(
77 "ACTIVE SESSION v{} | {} calls | {} tok saved",
78 session.version, session.stats.total_tool_calls, session.stats.total_tokens_saved
79 ));
80
81 if let Some(ref task) = session.task {
82 let pct = task
83 .progress_pct
84 .map_or(String::new(), |p| format!(" [{p}%]"));
85 begin_lines.push(format!("Task: {}{pct}", task.description));
86 }
87
88 if let Some(ref root) = session.project_root {
89 begin_lines.push(format!("Root: {root}"));
90 }
91
92 if !session.decisions.is_empty() {
93 let items: Vec<&str> = session
94 .decisions
95 .iter()
96 .rev()
97 .take(5)
98 .map(|d| d.summary.as_str())
99 .collect();
100 begin_lines.push(format!("Decisions: {}", items.join(" | ")));
101 }
102
103 if !session.files_touched.is_empty() {
104 let items: Vec<String> = session
105 .files_touched
106 .iter()
107 .rev()
108 .take(15)
109 .map(|f| {
110 let r = f.file_ref.as_deref().unwrap_or("?");
111 let status = if f.modified { "mod" } else { &f.last_mode };
112 format!("{r}={} [{status}]", short_path(&f.path))
113 })
114 .collect();
115 begin_lines.push(format!("Files: {}", items.join(" ")));
116 }
117
118 if !session.findings.is_empty() {
119 let items: Vec<String> = session
120 .findings
121 .iter()
122 .rev()
123 .take(5)
124 .map(|f| match (&f.file, f.line) {
125 (Some(file), Some(line)) => format!("{}:{line} — {}", short_path(file), f.summary),
126 (Some(file), None) => format!("{} — {}", short_path(file), f.summary),
127 _ => f.summary.clone(),
128 })
129 .collect();
130 end_lines.push(format!("Findings: {}", items.join(" | ")));
131 }
132
133 if let Some(ref tests) = session.test_results {
134 let status = if tests.failed > 0 { "FAIL" } else { "PASS" };
135 end_lines.push(format!(
136 "Tests [{status}]: {}/{} ({})",
137 tests.passed, tests.total, tests.command
138 ));
139 }
140
141 if !session.next_steps.is_empty() {
142 end_lines.push(format!("Next: {}", session.next_steps.join(" → ")));
143 }
144
145 if !end_lines.is_empty() || session.stats.total_tool_calls > 3 {
146 end_lines.push(
147 "ctx_read>Read ctx_shell>Shell ctx_search>Grep ctx_tree>ls | Edit/Write/Glob=native"
148 .to_string(),
149 );
150 }
151
152 PositionedOutput {
153 begin_block: begin_lines.join("\n"),
154 end_block: end_lines.join("\n"),
155 }
156}
157
158#[cfg(test)]
159pub fn compute_litm_efficiency(
160 begin_tokens: usize,
161 middle_tokens: usize,
162 end_tokens: usize,
163 ccp_begin_tokens: usize,
164 ccp_end_tokens: usize,
165) -> (f64, f64) {
166 let total_without = (begin_tokens + middle_tokens + end_tokens) as f64;
167 let effective_without =
168 _ALPHA * begin_tokens as f64 + _BETA * middle_tokens as f64 + _GAMMA * end_tokens as f64;
169
170 let total_with = (ccp_begin_tokens + ccp_end_tokens) as f64;
171 let effective_with = _ALPHA * ccp_begin_tokens as f64 + _GAMMA * ccp_end_tokens as f64;
172
173 let eff_without = if total_without > 0.0 {
174 effective_without / total_without * 100.0
175 } else {
176 0.0
177 };
178 let eff_with = if total_with > 0.0 {
179 effective_with / total_with * 100.0
180 } else {
181 0.0
182 };
183
184 (eff_without, eff_with)
185}
186
187#[cfg(test)]
188pub fn compute_litm_efficiency_for_profile(
189 begin_tokens: usize,
190 middle_tokens: usize,
191 end_tokens: usize,
192 ccp_begin_tokens: usize,
193 ccp_end_tokens: usize,
194 profile: &LitmProfile,
195) -> (f64, f64) {
196 let total_without = (begin_tokens + middle_tokens + end_tokens) as f64;
197 let effective_without = profile.alpha * begin_tokens as f64
198 + profile.beta * middle_tokens as f64
199 + profile.gamma * end_tokens as f64;
200
201 let total_with = (ccp_begin_tokens + ccp_end_tokens) as f64;
202 let effective_with =
203 profile.alpha * ccp_begin_tokens as f64 + profile.gamma * ccp_end_tokens as f64;
204
205 let eff_without = if total_without > 0.0 {
206 effective_without / total_without * 100.0
207 } else {
208 0.0
209 };
210 let eff_with = if total_with > 0.0 {
211 effective_with / total_with * 100.0
212 } else {
213 0.0
214 };
215
216 (eff_without, eff_with)
217}
218
219#[cfg(test)]
220pub fn content_attention_efficiency(content: &str, profile: &LitmProfile) -> f64 {
221 use crate::core::attention_model;
222
223 let lines: Vec<&str> = content.lines().collect();
224 if lines.is_empty() {
225 return 0.0;
226 }
227
228 let importances: Vec<f64> = lines
229 .iter()
230 .enumerate()
231 .map(|(i, line)| {
232 let pos = i as f64 / (lines.len() - 1).max(1) as f64;
233 attention_model::combined_attention(
234 line,
235 pos,
236 profile.alpha,
237 profile.beta,
238 profile.gamma,
239 )
240 })
241 .collect();
242
243 attention_model::attention_efficiency(&importances, profile.alpha, profile.beta, profile.gamma)
244}
245
246fn short_path(path: &str) -> String {
247 let parts: Vec<&str> = path.split('/').collect();
248 if parts.len() <= 2 {
249 return path.to_string();
250 }
251 parts.last().copied().unwrap_or(path).to_string()
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn litm_efficiency_without_ccp_lower() {
260 let (eff_without, eff_with) = compute_litm_efficiency(100, 500, 100, 300, 200);
261 assert!(
262 eff_with > eff_without,
263 "CCP should improve LITM efficiency: without={eff_without:.1}%, with={eff_with:.1}%"
264 );
265 }
266
267 #[test]
268 fn litm_efficiency_zero_tokens() {
269 let (eff_without, eff_with) = compute_litm_efficiency(0, 0, 0, 0, 0);
270 assert_eq!(eff_without, 0.0);
271 assert_eq!(eff_with, 0.0);
272 }
273
274 #[test]
275 fn litm_all_at_begin_is_alpha() {
276 let (_, eff_with) = compute_litm_efficiency(0, 0, 0, 100, 0);
277 assert!((eff_with - 90.0).abs() < 0.1, "all begin should be ~90%");
278 }
279
280 #[test]
281 fn litm_all_at_end_is_gamma() {
282 let (_, eff_with) = compute_litm_efficiency(0, 0, 0, 0, 100);
283 assert!((eff_with - 85.0).abs() < 0.1, "all end should be ~85%");
284 }
285
286 #[test]
287 fn litm_middle_heavy_is_worst() {
288 let (eff_middle, _) = compute_litm_efficiency(10, 1000, 10, 0, 0);
289 let (eff_balanced, _) = compute_litm_efficiency(500, 20, 500, 0, 0);
290 assert!(
291 eff_balanced > eff_middle,
292 "middle-heavy should be less efficient"
293 );
294 }
295
296 #[test]
297 fn short_path_simple() {
298 assert_eq!(short_path("file.rs"), "file.rs");
299 assert_eq!(short_path("src/file.rs"), "src/file.rs");
300 assert_eq!(short_path("a/b/c/file.rs"), "file.rs");
301 }
302
303 #[test]
304 fn litm_profile_from_client_claude() {
305 let p = LitmProfile::from_client_name("Claude Desktop");
306 assert_eq!(p.name, "claude");
307 assert!((p.alpha - 0.92).abs() < f64::EPSILON);
308 }
309
310 #[test]
311 fn litm_profile_from_client_cursor() {
312 let p = LitmProfile::from_client_name("Cursor");
313 assert_eq!(p.name, "claude");
314 }
315
316 #[test]
317 fn litm_profile_from_client_gemini() {
318 let p = LitmProfile::from_client_name("Gemini CLI");
319 assert_eq!(p.name, "gemini");
320 assert!((p.beta - 0.60).abs() < f64::EPSILON);
321 }
322
323 #[test]
324 fn litm_profile_unknown_defaults_to_gpt() {
325 let p = LitmProfile::from_client_name("unknown-tool");
326 assert_eq!(p.name, "gpt");
327 }
328
329 #[test]
330 fn litm_profile_efficiency_differs_by_model() {
331 let (_, claude_eff) =
332 compute_litm_efficiency_for_profile(200, 0, 100, 200, 100, &LitmProfile::CLAUDE);
333 let (_, gemini_eff) =
334 compute_litm_efficiency_for_profile(200, 0, 100, 200, 100, &LitmProfile::GEMINI);
335 assert!(
336 (claude_eff - gemini_eff).abs() > 0.1,
337 "different profiles should yield different efficiencies"
338 );
339 }
340}