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 if let Some(ref root) = session.project_root {
79 begin_lines.push(format!("Root: {root}"));
80 }
81
82 if let Some(ref task) = session.task {
83 let pct = task
84 .progress_pct
85 .map_or(String::new(), |p| format!(" [{p}%]"));
86 begin_lines.push(format!("Task: {}{pct}", task.description));
87 }
88
89 if !session.decisions.is_empty() {
90 let items: Vec<&str> = session
91 .decisions
92 .iter()
93 .rev()
94 .take(5)
95 .map(|d| d.summary.as_str())
96 .collect();
97 begin_lines.push(format!("Decisions: {}", items.join(" | ")));
98 }
99
100 if !session.files_touched.is_empty() {
101 let items: Vec<String> = session
102 .files_touched
103 .iter()
104 .rev()
105 .take(15)
106 .map(|f| {
107 let r = f.file_ref.as_deref().unwrap_or("?");
108 let status = if f.modified { "mod" } else { &f.last_mode };
109 let summary_hint = f
110 .summary
111 .as_deref()
112 .map_or(String::new(), |s| format!(", \"{s}\""));
113 format!("{r}={} [{status}{summary_hint}]", short_path(&f.path))
114 })
115 .collect();
116 begin_lines.push(format!("Files: {}", items.join(" ")));
117 }
118
119 if !session.progress.is_empty() {
121 let items: Vec<String> = session
122 .progress
123 .iter()
124 .rev()
125 .take(5)
126 .map(|p| {
127 p.detail
128 .as_deref()
129 .map_or_else(|| p.action.clone(), |d| format!("{}: {d}", p.action))
130 })
131 .collect();
132 begin_lines.push(format!("Progress: {}", items.join(" | ")));
133 }
134
135 if !session.findings.is_empty() {
136 let items: Vec<String> = session
137 .findings
138 .iter()
139 .rev()
140 .take(8)
141 .map(|f| f.summary.clone())
142 .collect();
143 end_lines.push(format!("Findings: {}", items.join(" | ")));
144 }
145
146 if let Some(ref tests) = session.test_results {
147 let status = if tests.failed > 0 { "FAIL" } else { "PASS" };
148 end_lines.push(format!(
149 "Tests [{status}]: {}/{} ({})",
150 tests.passed, tests.total, tests.command
151 ));
152 }
153
154 if !session.next_steps.is_empty() {
155 end_lines.push(format!("Next: {}", session.next_steps.join(" → ")));
156 }
157
158 end_lines.push(format!(
160 "ACTIVE SESSION v{} | {} calls | {} tok saved",
161 session.version, session.stats.total_tool_calls, session.stats.total_tokens_saved
162 ));
163
164 PositionedOutput {
165 begin_block: begin_lines.join("\n"),
166 end_block: end_lines.join("\n"),
167 }
168}
169
170#[cfg(test)]
171pub fn compute_litm_efficiency(
172 begin_tokens: usize,
173 middle_tokens: usize,
174 end_tokens: usize,
175 ccp_begin_tokens: usize,
176 ccp_end_tokens: usize,
177) -> (f64, f64) {
178 let total_without = (begin_tokens + middle_tokens + end_tokens) as f64;
179 let effective_without =
180 _ALPHA * begin_tokens as f64 + _BETA * middle_tokens as f64 + _GAMMA * end_tokens as f64;
181
182 let total_with = (ccp_begin_tokens + ccp_end_tokens) as f64;
183 let effective_with = _ALPHA * ccp_begin_tokens as f64 + _GAMMA * ccp_end_tokens as f64;
184
185 let eff_without = if total_without > 0.0 {
186 effective_without / total_without * 100.0
187 } else {
188 0.0
189 };
190 let eff_with = if total_with > 0.0 {
191 effective_with / total_with * 100.0
192 } else {
193 0.0
194 };
195
196 (eff_without, eff_with)
197}
198
199#[cfg(test)]
200pub fn compute_litm_efficiency_for_profile(
201 begin_tokens: usize,
202 middle_tokens: usize,
203 end_tokens: usize,
204 ccp_begin_tokens: usize,
205 ccp_end_tokens: usize,
206 profile: &LitmProfile,
207) -> (f64, f64) {
208 let total_without = (begin_tokens + middle_tokens + end_tokens) as f64;
209 let effective_without = profile.alpha * begin_tokens as f64
210 + profile.beta * middle_tokens as f64
211 + profile.gamma * end_tokens as f64;
212
213 let total_with = (ccp_begin_tokens + ccp_end_tokens) as f64;
214 let effective_with =
215 profile.alpha * ccp_begin_tokens as f64 + profile.gamma * ccp_end_tokens as f64;
216
217 let eff_without = if total_without > 0.0 {
218 effective_without / total_without * 100.0
219 } else {
220 0.0
221 };
222 let eff_with = if total_with > 0.0 {
223 effective_with / total_with * 100.0
224 } else {
225 0.0
226 };
227
228 (eff_without, eff_with)
229}
230
231#[cfg(test)]
232pub fn content_attention_efficiency(content: &str, profile: &LitmProfile) -> f64 {
233 use crate::core::attention_model;
234
235 let lines: Vec<&str> = content.lines().collect();
236 if lines.is_empty() {
237 return 0.0;
238 }
239
240 let importances: Vec<f64> = lines
241 .iter()
242 .enumerate()
243 .map(|(i, line)| {
244 let pos = i as f64 / (lines.len() - 1).max(1) as f64;
245 attention_model::combined_attention(
246 line,
247 pos,
248 profile.alpha,
249 profile.beta,
250 profile.gamma,
251 )
252 })
253 .collect();
254
255 attention_model::attention_efficiency(&importances, profile.alpha, profile.beta, profile.gamma)
256}
257
258fn short_path(path: &str) -> String {
259 let parts: Vec<&str> = path.split('/').collect();
260 if parts.len() <= 2 {
261 return path.to_string();
262 }
263 parts.last().copied().unwrap_or(path).to_string()
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn litm_efficiency_without_ccp_lower() {
272 let (eff_without, eff_with) = compute_litm_efficiency(100, 500, 100, 300, 200);
273 assert!(
274 eff_with > eff_without,
275 "CCP should improve LITM efficiency: without={eff_without:.1}%, with={eff_with:.1}%"
276 );
277 }
278
279 #[test]
280 fn litm_efficiency_zero_tokens() {
281 let (eff_without, eff_with) = compute_litm_efficiency(0, 0, 0, 0, 0);
282 assert_eq!(eff_without, 0.0);
283 assert_eq!(eff_with, 0.0);
284 }
285
286 #[test]
287 fn litm_all_at_begin_is_alpha() {
288 let (_, eff_with) = compute_litm_efficiency(0, 0, 0, 100, 0);
289 assert!((eff_with - 90.0).abs() < 0.1, "all begin should be ~90%");
290 }
291
292 #[test]
293 fn litm_all_at_end_is_gamma() {
294 let (_, eff_with) = compute_litm_efficiency(0, 0, 0, 0, 100);
295 assert!((eff_with - 85.0).abs() < 0.1, "all end should be ~85%");
296 }
297
298 #[test]
299 fn litm_middle_heavy_is_worst() {
300 let (eff_middle, _) = compute_litm_efficiency(10, 1000, 10, 0, 0);
301 let (eff_balanced, _) = compute_litm_efficiency(500, 20, 500, 0, 0);
302 assert!(
303 eff_balanced > eff_middle,
304 "middle-heavy should be less efficient"
305 );
306 }
307
308 #[test]
309 fn short_path_simple() {
310 assert_eq!(short_path("file.rs"), "file.rs");
311 assert_eq!(short_path("src/file.rs"), "src/file.rs");
312 assert_eq!(short_path("a/b/c/file.rs"), "file.rs");
313 }
314
315 #[test]
316 fn litm_profile_from_client_claude() {
317 let p = LitmProfile::from_client_name("Claude Desktop");
318 assert_eq!(p.name, "claude");
319 assert!((p.alpha - 0.92).abs() < f64::EPSILON);
320 }
321
322 #[test]
323 fn litm_profile_from_client_cursor() {
324 let p = LitmProfile::from_client_name("Cursor");
325 assert_eq!(p.name, "claude");
326 }
327
328 #[test]
329 fn litm_profile_from_client_gemini() {
330 let p = LitmProfile::from_client_name("Gemini CLI");
331 assert_eq!(p.name, "gemini");
332 assert!((p.beta - 0.60).abs() < f64::EPSILON);
333 }
334
335 #[test]
336 fn litm_profile_unknown_defaults_to_gpt() {
337 let p = LitmProfile::from_client_name("unknown-tool");
338 assert_eq!(p.name, "gpt");
339 }
340
341 #[test]
342 fn litm_profile_efficiency_differs_by_model() {
343 let (_, claude_eff) =
344 compute_litm_efficiency_for_profile(200, 0, 100, 200, 100, &LitmProfile::CLAUDE);
345 let (_, gemini_eff) =
346 compute_litm_efficiency_for_profile(200, 0, 100, 200, 100, &LitmProfile::GEMINI);
347 assert!(
348 (claude_eff - gemini_eff).abs() > 0.1,
349 "different profiles should yield different efficiencies"
350 );
351 }
352}