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 PositionedOutput {
146 begin_block: begin_lines.join("\n"),
147 end_block: end_lines.join("\n"),
148 }
149}
150
151#[cfg(test)]
152pub fn compute_litm_efficiency(
153 begin_tokens: usize,
154 middle_tokens: usize,
155 end_tokens: usize,
156 ccp_begin_tokens: usize,
157 ccp_end_tokens: usize,
158) -> (f64, f64) {
159 let total_without = (begin_tokens + middle_tokens + end_tokens) as f64;
160 let effective_without =
161 _ALPHA * begin_tokens as f64 + _BETA * middle_tokens as f64 + _GAMMA * end_tokens as f64;
162
163 let total_with = (ccp_begin_tokens + ccp_end_tokens) as f64;
164 let effective_with = _ALPHA * ccp_begin_tokens as f64 + _GAMMA * ccp_end_tokens as f64;
165
166 let eff_without = if total_without > 0.0 {
167 effective_without / total_without * 100.0
168 } else {
169 0.0
170 };
171 let eff_with = if total_with > 0.0 {
172 effective_with / total_with * 100.0
173 } else {
174 0.0
175 };
176
177 (eff_without, eff_with)
178}
179
180#[cfg(test)]
181pub fn compute_litm_efficiency_for_profile(
182 begin_tokens: usize,
183 middle_tokens: usize,
184 end_tokens: usize,
185 ccp_begin_tokens: usize,
186 ccp_end_tokens: usize,
187 profile: &LitmProfile,
188) -> (f64, f64) {
189 let total_without = (begin_tokens + middle_tokens + end_tokens) as f64;
190 let effective_without = profile.alpha * begin_tokens as f64
191 + profile.beta * middle_tokens as f64
192 + profile.gamma * end_tokens as f64;
193
194 let total_with = (ccp_begin_tokens + ccp_end_tokens) as f64;
195 let effective_with =
196 profile.alpha * ccp_begin_tokens as f64 + profile.gamma * ccp_end_tokens as f64;
197
198 let eff_without = if total_without > 0.0 {
199 effective_without / total_without * 100.0
200 } else {
201 0.0
202 };
203 let eff_with = if total_with > 0.0 {
204 effective_with / total_with * 100.0
205 } else {
206 0.0
207 };
208
209 (eff_without, eff_with)
210}
211
212#[cfg(test)]
213pub fn content_attention_efficiency(content: &str, profile: &LitmProfile) -> f64 {
214 use crate::core::attention_model;
215
216 let lines: Vec<&str> = content.lines().collect();
217 if lines.is_empty() {
218 return 0.0;
219 }
220
221 let importances: Vec<f64> = lines
222 .iter()
223 .enumerate()
224 .map(|(i, line)| {
225 let pos = i as f64 / (lines.len() - 1).max(1) as f64;
226 attention_model::combined_attention(
227 line,
228 pos,
229 profile.alpha,
230 profile.beta,
231 profile.gamma,
232 )
233 })
234 .collect();
235
236 attention_model::attention_efficiency(&importances, profile.alpha, profile.beta, profile.gamma)
237}
238
239fn short_path(path: &str) -> String {
240 let parts: Vec<&str> = path.split('/').collect();
241 if parts.len() <= 2 {
242 return path.to_string();
243 }
244 parts.last().copied().unwrap_or(path).to_string()
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn litm_efficiency_without_ccp_lower() {
253 let (eff_without, eff_with) = compute_litm_efficiency(100, 500, 100, 300, 200);
254 assert!(
255 eff_with > eff_without,
256 "CCP should improve LITM efficiency: without={eff_without:.1}%, with={eff_with:.1}%"
257 );
258 }
259
260 #[test]
261 fn litm_efficiency_zero_tokens() {
262 let (eff_without, eff_with) = compute_litm_efficiency(0, 0, 0, 0, 0);
263 assert_eq!(eff_without, 0.0);
264 assert_eq!(eff_with, 0.0);
265 }
266
267 #[test]
268 fn litm_all_at_begin_is_alpha() {
269 let (_, eff_with) = compute_litm_efficiency(0, 0, 0, 100, 0);
270 assert!((eff_with - 90.0).abs() < 0.1, "all begin should be ~90%");
271 }
272
273 #[test]
274 fn litm_all_at_end_is_gamma() {
275 let (_, eff_with) = compute_litm_efficiency(0, 0, 0, 0, 100);
276 assert!((eff_with - 85.0).abs() < 0.1, "all end should be ~85%");
277 }
278
279 #[test]
280 fn litm_middle_heavy_is_worst() {
281 let (eff_middle, _) = compute_litm_efficiency(10, 1000, 10, 0, 0);
282 let (eff_balanced, _) = compute_litm_efficiency(500, 20, 500, 0, 0);
283 assert!(
284 eff_balanced > eff_middle,
285 "middle-heavy should be less efficient"
286 );
287 }
288
289 #[test]
290 fn short_path_simple() {
291 assert_eq!(short_path("file.rs"), "file.rs");
292 assert_eq!(short_path("src/file.rs"), "src/file.rs");
293 assert_eq!(short_path("a/b/c/file.rs"), "file.rs");
294 }
295
296 #[test]
297 fn litm_profile_from_client_claude() {
298 let p = LitmProfile::from_client_name("Claude Desktop");
299 assert_eq!(p.name, "claude");
300 assert!((p.alpha - 0.92).abs() < f64::EPSILON);
301 }
302
303 #[test]
304 fn litm_profile_from_client_cursor() {
305 let p = LitmProfile::from_client_name("Cursor");
306 assert_eq!(p.name, "claude");
307 }
308
309 #[test]
310 fn litm_profile_from_client_gemini() {
311 let p = LitmProfile::from_client_name("Gemini CLI");
312 assert_eq!(p.name, "gemini");
313 assert!((p.beta - 0.60).abs() < f64::EPSILON);
314 }
315
316 #[test]
317 fn litm_profile_unknown_defaults_to_gpt() {
318 let p = LitmProfile::from_client_name("unknown-tool");
319 assert_eq!(p.name, "gpt");
320 }
321
322 #[test]
323 fn litm_profile_efficiency_differs_by_model() {
324 let (_, claude_eff) =
325 compute_litm_efficiency_for_profile(200, 0, 100, 200, 100, &LitmProfile::CLAUDE);
326 let (_, gemini_eff) =
327 compute_litm_efficiency_for_profile(200, 0, 100, 200, 100, &LitmProfile::GEMINI);
328 assert!(
329 (claude_eff - gemini_eff).abs() > 0.1,
330 "different profiles should yield different efficiencies"
331 );
332 }
333}