lean_ctx/core/
compress_preview.rs1use crate::core::compressor;
12use crate::core::tokens::count_tokens;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Pipeline {
17 Read,
20 Shell,
22}
23
24impl Pipeline {
25 pub fn label(self) -> &'static str {
28 match self {
29 Pipeline::Read => "read/aggressive",
30 Pipeline::Shell => "shell",
31 }
32 }
33}
34
35pub struct Preview {
38 pub pipeline: Pipeline,
39 pub original: String,
40 pub compressed: String,
41 pub original_tokens: usize,
42 pub compressed_tokens: usize,
43}
44
45impl Preview {
46 pub fn original_bytes(&self) -> usize {
47 self.original.len()
48 }
49
50 pub fn compressed_bytes(&self) -> usize {
51 self.compressed.len()
52 }
53
54 pub fn saved_tokens(&self) -> usize {
57 self.original_tokens.saturating_sub(self.compressed_tokens)
58 }
59
60 pub fn token_ratio(&self) -> f64 {
63 if self.original_tokens == 0 {
64 return 1.0;
65 }
66 self.compressed_tokens as f64 / self.original_tokens as f64
67 }
68
69 pub fn saved_pct(&self) -> f64 {
71 ((1.0 - self.token_ratio()) * 1000.0).round() / 10.0
72 }
73
74 pub fn diff(&self) -> String {
76 compressor::diff_content(&self.original, &self.compressed)
77 }
78
79 pub fn render(&self) -> String {
82 let mut out = String::new();
83 out.push_str("compress preview — pipeline: ");
84 out.push_str(self.pipeline.label());
85 out.push('\n');
86 out.push_str(&format!(
87 "tokens: {} -> {} (-{}, {:.1}% saved)\n",
88 self.original_tokens,
89 self.compressed_tokens,
90 self.saved_tokens(),
91 self.saved_pct(),
92 ));
93 out.push_str(&format!(
94 "bytes: {} -> {}\n",
95 self.original_bytes(),
96 self.compressed_bytes(),
97 ));
98 out.push_str("-- diff (original -> compressed) --\n");
99 out.push_str(&self.diff());
100 if !out.ends_with('\n') {
101 out.push('\n');
102 }
103 out
104 }
105}
106
107pub fn preview_read(content: &str, ext: Option<&str>) -> Preview {
111 let compressed = compressor::aggressive_compress(content, ext);
112 finish(Pipeline::Read, content, compressed)
113}
114
115pub fn preview_shell(command: &str, output: &str) -> Preview {
118 let compressed = crate::shell::compress::engine::compress_if_beneficial_pub(command, output);
119 finish(Pipeline::Shell, output, compressed)
120}
121
122fn finish(pipeline: Pipeline, original: &str, compressed: String) -> Preview {
123 Preview {
124 pipeline,
125 original_tokens: count_tokens(original),
126 compressed_tokens: count_tokens(&compressed),
127 original: original.to_string(),
128 compressed,
129 }
130}
131
132pub fn ext_of(path: &str) -> Option<String> {
134 std::path::Path::new(path)
135 .extension()
136 .and_then(|e| e.to_str())
137 .map(str::to_ascii_lowercase)
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn read_preview_strips_comments_and_saves_tokens() {
146 let src = "// a leading comment\nfn main() {\n // inner note\n let x = 1;\n}\n";
147 let p = preview_read(src, Some("rs"));
148 assert_eq!(p.pipeline, Pipeline::Read);
149 assert!(
150 !p.compressed.contains("leading comment") && !p.compressed.contains("inner note"),
151 "comments must be stripped: {}",
152 p.compressed
153 );
154 assert!(p.compressed_tokens < p.original_tokens, "tokens must drop");
155 assert!(p.saved_tokens() > 0);
156 assert!(p.token_ratio() < 1.0);
157 assert!(p.saved_pct() > 0.0);
158 }
159
160 #[test]
161 fn read_preview_compacts_pretty_json() {
162 let json =
163 "{\n \"name\": \"lean-ctx\",\n \"nested\": {\n \"a\": 1,\n \"b\": 2\n }\n}\n";
164 let p = preview_read(json, Some("json"));
165 assert!(!p.compressed.contains("\n "), "json must be compacted");
167 assert!(p.compressed_tokens < p.original_tokens);
168 }
169
170 #[test]
171 fn no_change_input_reports_no_savings_and_clean_diff() {
172 let src = "let x = 1;";
173 let p = preview_read(src, Some("rs"));
174 assert_eq!(
175 p.compressed, p.original,
176 "already-minimal input is unchanged"
177 );
178 assert_eq!(p.saved_tokens(), 0);
179 assert!((p.token_ratio() - 1.0).abs() < f64::EPSILON);
180 assert_eq!(p.saved_pct(), 0.0);
181 assert_eq!(p.diff(), "(no changes)");
182 }
183
184 #[test]
185 fn shell_preview_compresses_repetitive_output() {
186 let output = "Compiling foo\n".repeat(40);
187 let p = preview_shell("cargo build", &output);
188 assert_eq!(p.pipeline, Pipeline::Shell);
189 assert!(
190 p.compressed_tokens <= p.original_tokens,
191 "shell compression never inflates"
192 );
193 }
194
195 #[test]
196 fn render_is_deterministic_and_self_describing() {
197 let src = "// c\nfn a() {}\n";
198 let p = preview_read(src, Some("rs"));
199 let a = p.render();
200 let b = p.render();
201 assert_eq!(a, b, "render must be deterministic (#498)");
202 assert!(a.contains("pipeline: read/aggressive"));
203 assert!(a.contains("tokens:"));
204 assert!(a.contains("bytes:"));
205 }
206
207 #[test]
208 fn ext_of_extracts_lowercased_extension() {
209 assert_eq!(ext_of("/a/b/File.RS").as_deref(), Some("rs"));
210 assert_eq!(ext_of("data.JSON").as_deref(), Some("json"));
211 assert_eq!(ext_of("Makefile"), None);
212 assert_eq!(ext_of("-"), None);
213 }
214}