Skip to main content

lean_ctx/core/
savings_footer.rs

1use std::cell::RefCell;
2use std::sync::atomic::{AtomicUsize, Ordering};
3
4static SESSION_ORIGINAL: AtomicUsize = AtomicUsize::new(0);
5static SESSION_SAVED: AtomicUsize = AtomicUsize::new(0);
6static SESSION_CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
7
8thread_local! {
9    static CURRENT_MODE: RefCell<Option<String>> = const { RefCell::new(None) };
10    static CURRENT_DETAIL: RefCell<Option<String>> = const { RefCell::new(None) };
11}
12
13pub struct SavingsInfo<'a> {
14    pub original: usize,
15    pub compressed: usize,
16    pub mode: Option<&'a str>,
17    pub detail: Option<&'a str>,
18}
19
20pub struct ModeGuard;
21
22impl ModeGuard {
23    pub fn new(mode: &str) -> Self {
24        CURRENT_MODE.with(|m| *m.borrow_mut() = Some(mode.to_string()));
25        Self
26    }
27
28    pub fn with_detail(mode: &str, detail: &str) -> Self {
29        CURRENT_MODE.with(|m| *m.borrow_mut() = Some(mode.to_string()));
30        CURRENT_DETAIL.with(|d| *d.borrow_mut() = Some(detail.to_string()));
31        Self
32    }
33}
34
35impl Drop for ModeGuard {
36    fn drop(&mut self) {
37        // Must be panic-free: a `borrow_mut` panic while the thread is already
38        // unwinding another panic would escalate to a process abort (#378). Use
39        // `try_borrow_mut` and silently skip if the slot is somehow in use.
40        CURRENT_MODE.with(|m| {
41            if let Ok(mut slot) = m.try_borrow_mut() {
42                *slot = None;
43            }
44        });
45        CURRENT_DETAIL.with(|d| {
46            if let Ok(mut slot) = d.try_borrow_mut() {
47                *slot = None;
48            }
49        });
50    }
51}
52
53fn current_mode() -> Option<String> {
54    CURRENT_MODE.with(|m| m.borrow().clone())
55}
56
57fn current_detail() -> Option<String> {
58    CURRENT_DETAIL.with(|d| d.borrow().clone())
59}
60
61pub fn record_savings(original: usize, saved: usize) {
62    SESSION_ORIGINAL.fetch_add(original, Ordering::Relaxed);
63    SESSION_SAVED.fetch_add(saved, Ordering::Relaxed);
64    SESSION_CALL_COUNT.fetch_add(1, Ordering::Relaxed);
65}
66
67pub fn session_totals() -> (usize, usize, usize) {
68    (
69        SESSION_ORIGINAL.load(Ordering::Relaxed),
70        SESSION_SAVED.load(Ordering::Relaxed),
71        SESSION_CALL_COUNT.load(Ordering::Relaxed),
72    )
73}
74
75pub fn reset_session() {
76    SESSION_ORIGINAL.store(0, Ordering::Relaxed);
77    SESSION_SAVED.store(0, Ordering::Relaxed);
78    SESSION_CALL_COUNT.store(0, Ordering::Relaxed);
79}
80
81fn format_number(n: usize) -> String {
82    if n >= 1_000_000 {
83        let m = n as f64 / 1_000_000.0;
84        format!("{m:.1}M")
85    } else if n >= 10_000 {
86        let k = n as f64 / 1_000.0;
87        format!("{k:.1}k")
88    } else if n >= 1_000 {
89        let whole = n / 1_000;
90        format!("{whole},{:03}", n % 1_000)
91    } else {
92        n.to_string()
93    }
94}
95
96fn is_explicitly_enabled() -> bool {
97    matches!(std::env::var("LEAN_CTX_SHOW_SAVINGS"), Ok(v) if v.trim() == "1")
98}
99
100fn is_ultra_suppressed() -> bool {
101    if is_explicitly_enabled() {
102        return false;
103    }
104    let level = super::config::CompressionLevel::effective(&super::config::Config::load());
105    matches!(level, super::config::CompressionLevel::Max)
106}
107
108pub fn format_footer(info: &SavingsInfo<'_>) -> String {
109    if !super::protocol::savings_footer_visible() {
110        return String::new();
111    }
112    if is_ultra_suppressed() {
113        return String::new();
114    }
115    format_footer_inner(info)
116}
117
118fn format_footer_inner(info: &SavingsInfo<'_>) -> String {
119    if info.original == 0 {
120        return String::new();
121    }
122    let saved = info.original.saturating_sub(info.compressed);
123    if saved == 0 {
124        return String::new();
125    }
126    let pct = (saved as f64 / info.original as f64 * 100.0).round() as usize;
127
128    let annotation = super::config::CompressionAnnotation::effective();
129    let threshold = super::config::Config::load().annotation_threshold_pct as usize;
130
131    if matches!(annotation, super::config::CompressionAnnotation::None) {
132        record_savings(info.original, saved);
133        return String::new();
134    }
135
136    if pct < threshold {
137        record_savings(info.original, saved);
138        return String::new();
139    }
140
141    let orig_str = format_number(info.original);
142    let comp_str = format_number(info.compressed);
143
144    let pct_display = match annotation {
145        super::config::CompressionAnnotation::Quantized => {
146            let quantized = ((pct + 5) / 10) * 10;
147            format!("~{quantized}")
148        }
149        _ => pct.to_string(),
150    };
151
152    let mut parts = vec![format!(
153        "{orig_str} \u{2192} {comp_str} tok (\u{2193}{pct_display}%)"
154    )];
155
156    if let Some(mode) = info.mode {
157        parts.push(format!("mode: {mode}"));
158    }
159    if let Some(detail) = info.detail {
160        parts.push(detail.to_string());
161    }
162
163    record_savings(info.original, saved);
164
165    let body = parts.join(" | ");
166    format!("\u{2500}\u{2500}\u{2500} {body} \u{2500}\u{2500}\u{2500}")
167}
168
169pub fn format_footer_basic(original: usize, compressed: usize) -> String {
170    let mode = current_mode();
171    let detail = current_detail();
172    format_footer(&SavingsInfo {
173        original,
174        compressed,
175        mode: mode.as_deref(),
176        detail: detail.as_deref(),
177    })
178}
179
180pub fn append_footer(output: &str, info: &SavingsInfo<'_>) -> String {
181    let footer = format_footer(info);
182    if footer.is_empty() {
183        output.to_string()
184    } else {
185        format!("{output}\n{footer}")
186    }
187}
188
189pub fn append_footer_basic(output: &str, original: usize, compressed: usize) -> String {
190    let footer = format_footer_basic(original, compressed);
191    if footer.is_empty() {
192        output.to_string()
193    } else {
194        format!("{output}\n{footer}")
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn format_number_small() {
204        assert_eq!(format_number(42), "42");
205        assert_eq!(format_number(999), "999");
206    }
207
208    #[test]
209    fn format_number_thousands() {
210        assert_eq!(format_number(1_000), "1,000");
211        assert_eq!(format_number(4_200), "4,200");
212        assert_eq!(format_number(9_999), "9,999");
213    }
214
215    #[test]
216    fn format_number_large() {
217        assert_eq!(format_number(12_300), "12.3k");
218        assert_eq!(format_number(45_200), "45.2k");
219    }
220
221    #[test]
222    fn format_number_millions() {
223        assert_eq!(format_number(1_500_000), "1.5M");
224    }
225
226    #[test]
227    fn basic_footer_format() {
228        let _lock = crate::core::data_dir::test_env_lock();
229        crate::test_env::set_var("LEAN_CTX_COMPRESSION_ANNOTATION", "full");
230
231        let info = SavingsInfo {
232            original: 4200,
233            compressed: 840,
234            mode: Some("map"),
235            detail: None,
236        };
237        let result = format_footer_inner(&info);
238        assert!(
239            result.starts_with("\u{2500}\u{2500}\u{2500} "),
240            "should start with box-drawing: {result}"
241        );
242        assert!(
243            result.ends_with(" \u{2500}\u{2500}\u{2500}"),
244            "should end with box-drawing: {result}"
245        );
246        assert!(
247            result.contains("4,200"),
248            "should contain formatted original: {result}"
249        );
250        assert!(
251            result.contains("840"),
252            "should contain compressed: {result}"
253        );
254        assert!(
255            result.contains("\u{2193}80%"),
256            "should contain percentage: {result}"
257        );
258        assert!(
259            result.contains("mode: map"),
260            "should contain mode: {result}"
261        );
262
263        crate::test_env::remove_var("LEAN_CTX_COMPRESSION_ANNOTATION");
264    }
265
266    #[test]
267    fn footer_with_detail() {
268        let _lock = crate::core::data_dir::test_env_lock();
269        crate::test_env::set_var("LEAN_CTX_COMPRESSION_ANNOTATION", "full");
270
271        let info = SavingsInfo {
272            original: 12300,
273            compressed: 620,
274            mode: None,
275            detail: Some("3 patterns matched"),
276        };
277        let result = format_footer_inner(&info);
278        assert!(
279            result.contains("3 patterns matched"),
280            "detail missing: {result}"
281        );
282        assert!(
283            result.contains("12.3k"),
284            "should format large numbers: {result}"
285        );
286
287        crate::test_env::remove_var("LEAN_CTX_COMPRESSION_ANNOTATION");
288    }
289
290    #[test]
291    fn footer_returns_empty_when_no_savings() {
292        let result = format_footer_inner(&SavingsInfo {
293            original: 100,
294            compressed: 100,
295            mode: None,
296            detail: None,
297        });
298        assert!(
299            result.is_empty(),
300            "should be empty with 0 savings: {result}"
301        );
302    }
303
304    #[test]
305    fn footer_returns_empty_when_zero_original() {
306        let result = format_footer_inner(&SavingsInfo {
307            original: 0,
308            compressed: 0,
309            mode: None,
310            detail: None,
311        });
312        assert!(
313            result.is_empty(),
314            "should be empty with 0 original: {result}"
315        );
316    }
317
318    #[test]
319    fn visibility_gated_tests() {
320        let _lock = crate::core::data_dir::test_env_lock();
321
322        crate::test_env::set_var("LEAN_CTX_SHOW_SAVINGS", "0");
323        crate::test_env::set_var("LEAN_CTX_SAVINGS_FOOTER", "never");
324        let result = format_footer_basic(100, 50);
325        assert!(
326            result.is_empty(),
327            "should be empty with never mode: {result}"
328        );
329
330        let result = append_footer_basic("hello", 100, 50);
331        assert_eq!(result, "hello");
332
333        crate::test_env::set_var("LEAN_CTX_SHOW_SAVINGS", "1");
334        crate::test_env::set_var("LEAN_CTX_SAVINGS_FOOTER", "always");
335        crate::test_env::remove_var("LEAN_CTX_QUIET");
336        super::super::protocol::set_mcp_context(false);
337
338        let result = append_footer_basic("hello", 100, 50);
339        assert!(
340            result.starts_with("hello\n"),
341            "should start with original: {result}"
342        );
343        assert!(
344            result.contains("\u{2500}\u{2500}\u{2500}"),
345            "should contain box-drawing: {result}"
346        );
347
348        // Restore ALL touched env — leaking LEAN_CTX_SAVINGS_FOOTER=always
349        // made footers visible in unrelated tests (GL #556 flakiness).
350        crate::test_env::remove_var("LEAN_CTX_SHOW_SAVINGS");
351        crate::test_env::remove_var("LEAN_CTX_SAVINGS_FOOTER");
352    }
353
354    #[test]
355    fn session_accumulator_tracks() {
356        reset_session();
357        record_savings(100, 50);
358        record_savings(200, 80);
359        let (orig, saved, calls) = session_totals();
360        assert_eq!(orig, 300);
361        assert_eq!(saved, 130);
362        assert_eq!(calls, 2);
363        reset_session();
364    }
365
366    #[test]
367    fn session_counter_removed_from_footer() {
368        let _lock = crate::core::data_dir::test_env_lock();
369        crate::test_env::set_var("LEAN_CTX_COMPRESSION_ANNOTATION", "full");
370
371        reset_session();
372        for _ in 0..20 {
373            record_savings(100, 50);
374        }
375        let info = SavingsInfo {
376            original: 100,
377            compressed: 50,
378            mode: None,
379            detail: None,
380        };
381        let result = format_footer_inner(&info);
382        assert!(
383            !result.contains("session:"),
384            "session counter must not appear in footer (breaks prefix stability): {result}"
385        );
386        reset_session();
387        crate::test_env::remove_var("LEAN_CTX_COMPRESSION_ANNOTATION");
388    }
389
390    #[test]
391    fn quantized_mode_rounds_to_nearest_10() {
392        let _lock = crate::core::data_dir::test_env_lock();
393        crate::test_env::set_var("LEAN_CTX_COMPRESSION_ANNOTATION", "quantized");
394
395        let info = SavingsInfo {
396            original: 100,
397            compressed: 58,
398            mode: None,
399            detail: None,
400        };
401        let result = format_footer_inner(&info);
402        assert!(
403            result.contains("~40%"),
404            "42% should quantize to ~40%: {result}"
405        );
406
407        let info = SavingsInfo {
408            original: 100,
409            compressed: 13,
410            mode: None,
411            detail: None,
412        };
413        let result = format_footer_inner(&info);
414        assert!(
415            result.contains("~90%"),
416            "87% should quantize to ~90%: {result}"
417        );
418
419        crate::test_env::remove_var("LEAN_CTX_COMPRESSION_ANNOTATION");
420    }
421
422    #[test]
423    fn none_mode_suppresses_all_annotations() {
424        let _lock = crate::core::data_dir::test_env_lock();
425        crate::test_env::set_var("LEAN_CTX_COMPRESSION_ANNOTATION", "none");
426
427        let info = SavingsInfo {
428            original: 100,
429            compressed: 20,
430            mode: Some("map"),
431            detail: None,
432        };
433        let result = format_footer_inner(&info);
434        assert!(result.is_empty(), "none mode should suppress all: {result}");
435
436        crate::test_env::remove_var("LEAN_CTX_COMPRESSION_ANNOTATION");
437    }
438
439    #[test]
440    fn threshold_suppresses_small_savings() {
441        let _lock = crate::core::data_dir::test_env_lock();
442        crate::test_env::set_var("LEAN_CTX_COMPRESSION_ANNOTATION", "full");
443
444        let info = SavingsInfo {
445            original: 100,
446            compressed: 97,
447            mode: None,
448            detail: None,
449        };
450        let result = format_footer_inner(&info);
451        assert!(
452            result.is_empty(),
453            "3% savings (below default 5% threshold) should be suppressed: {result}"
454        );
455
456        let info = SavingsInfo {
457            original: 100,
458            compressed: 90,
459            mode: None,
460            detail: None,
461        };
462        let result = format_footer_inner(&info);
463        assert!(
464            result.contains("10%"),
465            "10% savings (above 5% threshold) should appear: {result}"
466        );
467
468        crate::test_env::remove_var("LEAN_CTX_COMPRESSION_ANNOTATION");
469    }
470
471    #[test]
472    fn mode_guard_sets_and_clears() {
473        assert!(current_mode().is_none());
474        {
475            let _guard = ModeGuard::new("map");
476            assert_eq!(current_mode().as_deref(), Some("map"));
477        }
478        assert!(current_mode().is_none());
479    }
480
481    #[test]
482    fn mode_guard_with_detail() {
483        {
484            let _guard = ModeGuard::with_detail("shell", "3 patterns");
485            assert_eq!(current_mode().as_deref(), Some("shell"));
486            assert_eq!(current_detail().as_deref(), Some("3 patterns"));
487        }
488        assert!(current_mode().is_none());
489        assert!(current_detail().is_none());
490    }
491}