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