1use super::counter;
7use super::dictionaries::{self, DictLevel};
8use super::quality::{self, QualityConfig, QualityReport};
9use super::scoring;
10use crate::core::config::CompressionLevel;
11
12const LOW_SCORE_THRESHOLD: f32 = 2.5;
14
15const STANDARD_SCORE_THRESHOLD: f32 = 3.0;
16const MAX_SCORE_THRESHOLD: f32 = 3.5;
17
18#[derive(Debug)]
20pub struct EngineResult {
21 pub output: String,
22 pub tokens_before: u32,
23 pub tokens_after: u32,
24 pub quality: QualityReport,
25 pub lines_removed: usize,
26 pub lines_total: usize,
27}
28
29const MIN_LINES_FOR_COMPRESSION: usize = 5;
30
31pub fn compress(text: &str, level: &CompressionLevel) -> EngineResult {
33 let tokens_before = counter::count(text);
34 let lines_total = text.lines().count();
35
36 if !level.is_active() || text.is_empty() || lines_total < MIN_LINES_FOR_COMPRESSION {
37 return EngineResult {
38 output: text.to_string(),
39 tokens_before,
40 tokens_after: tokens_before,
41 quality: quality::check(
42 text,
43 text,
44 tokens_before,
45 tokens_before,
46 &QualityConfig::default(),
47 ),
48 lines_removed: 0,
49 lines_total,
50 };
51 }
52
53 let result = compress_at_level(text, tokens_before, level);
54
55 if result.quality.passed {
56 return result;
57 }
58
59 if matches!(level, CompressionLevel::Max | CompressionLevel::Raw) {
60 let fallback = compress_at_level(text, tokens_before, &CompressionLevel::Standard);
61 if fallback.quality.passed {
62 return fallback;
63 }
64 }
65
66 EngineResult {
67 output: text.to_string(),
68 tokens_before,
69 tokens_after: tokens_before,
70 quality: result.quality,
71 lines_removed: 0,
72 lines_total: text.lines().count(),
73 }
74}
75
76fn compress_at_level(text: &str, tokens_before: u32, level: &CompressionLevel) -> EngineResult {
77 let scores = scoring::score_lines(text);
78 let lines: Vec<&str> = text.lines().collect();
79 let lines_total = lines.len();
80
81 let threshold = match level {
82 CompressionLevel::Max | CompressionLevel::Raw => MAX_SCORE_THRESHOLD,
83 CompressionLevel::Standard => STANDARD_SCORE_THRESHOLD,
84 CompressionLevel::Lite | CompressionLevel::Off => LOW_SCORE_THRESHOLD,
85 };
86
87 let mut kept_lines = Vec::new();
88 let mut lines_removed = 0;
89
90 for (score, line) in scores.iter().zip(lines.iter()) {
91 let trimmed = line.trim();
92
93 if trimmed.is_empty() {
94 lines_removed += 1;
95 continue;
96 }
97
98 if is_section_boundary(trimmed) {
102 kept_lines.push(*line);
103 continue;
104 }
105
106 if is_pure_decoration(trimmed) {
107 lines_removed += 1;
108 continue;
109 }
110
111 if is_filler_line(trimmed) && !score.has_structural_marker {
112 lines_removed += 1;
113 continue;
114 }
115
116 if score.combined < threshold && !score.has_structural_marker {
117 lines_removed += 1;
118 continue;
119 }
120
121 kept_lines.push(*line);
122 }
123
124 let filtered = kept_lines.join("\n");
125
126 let quality_config = match level {
127 CompressionLevel::Max | CompressionLevel::Raw => QualityConfig {
128 min_identifier_preservation: 0.80,
129 ..QualityConfig::default()
130 },
131 _ => QualityConfig::default(),
132 };
133
134 let filtered_tokens = counter::count(&filtered);
135 let quality_report = quality::check(
136 text,
137 &filtered,
138 tokens_before,
139 filtered_tokens,
140 &quality_config,
141 );
142
143 if !quality_report.passed {
144 return EngineResult {
145 output: text.to_string(),
146 tokens_before,
147 tokens_after: tokens_before,
148 quality: quality_report,
149 lines_removed: 0,
150 lines_total,
151 };
152 }
153
154 let dict_level = match level {
155 CompressionLevel::Max | CompressionLevel::Raw | CompressionLevel::Standard => {
156 DictLevel::Full
157 }
158 CompressionLevel::Lite | CompressionLevel::Off => DictLevel::General,
159 };
160 let compressed = dictionaries::apply_dictionaries(&filtered, dict_level);
161 let compressed = match dict_level {
162 DictLevel::Full => super::auto_dict::apply(&compressed).unwrap_or(compressed),
163 DictLevel::General => compressed,
164 };
165 let critical_lost = quality::critical_facts_lost(text, &compressed);
168 if critical_lost > 0 {
169 return EngineResult {
170 output: text.to_string(),
171 tokens_before,
172 tokens_after: tokens_before,
173 quality: QualityReport {
174 passed: false,
175 critical_lost,
176 ..quality_report
177 },
178 lines_removed: 0,
179 lines_total,
180 };
181 }
182 let tokens_after = counter::count(&compressed);
183
184 EngineResult {
185 output: compressed,
186 tokens_before,
187 tokens_after,
188 quality: quality_report,
189 lines_removed,
190 lines_total,
191 }
192}
193
194fn is_filler_line(line: &str) -> bool {
195 let trimmed = line.trim();
196
197 if trimmed == "|" || trimmed == "| " {
198 return true;
199 }
200
201 let lower = line.to_lowercase();
202 const FILLER_PATTERNS: &[&str] = &[
203 "use \"git add",
204 "use \"git restore",
205 "(use \"git",
206 "run with `rust_backtrace",
207 "for more information about this error",
208 "try `rustc --explain",
209 "run `npm fund`",
210 "run `npm audit`",
211 "to address all issues",
212 "sending build context",
213 "using cache",
214 "packages are looking for funding",
215 "no changes added to commit",
216 "help: ",
217 "= note: ",
218 "---> running in",
219 ];
220 FILLER_PATTERNS.iter().any(|p| lower.contains(p))
221}
222
223fn is_pure_decoration(line: &str) -> bool {
224 let trimmed = line.trim();
225
226 if trimmed.is_empty() {
227 return true;
228 }
229
230 if trimmed.chars().all(|c| c == '|' || c.is_whitespace()) {
231 return true;
232 }
233
234 if line.len() < 3 {
235 return false;
236 }
237
238 if line.starts_with("//") || line.starts_with('#') || line.starts_with("--") {
239 let content = line
240 .trim_start_matches('/')
241 .trim_start_matches('#')
242 .trim_start_matches('-')
243 .trim();
244 return content.is_empty() || is_banner_chars(content);
245 }
246
247 is_banner_chars(line)
248}
249
250fn is_section_boundary(line: &str) -> bool {
253 let trimmed = line.trim_start();
254 ["===", "---", "###", "***", "___", ":::"]
255 .iter()
256 .any(|prefix| trimmed.starts_with(prefix))
257}
258
259fn is_banner_chars(line: &str) -> bool {
260 let chars: Vec<char> = line.chars().collect();
261 if chars.len() < 4 {
262 return false;
263 }
264 let first = chars[0];
265 if matches!(
266 first,
267 '=' | '-' | '*' | '─' | '━' | '▀' | '▄' | '╔' | '╚' | '║' | '░' | '█' | '═'
268 ) {
269 let same_count = chars.iter().filter(|c| **c == first).count();
270 return same_count as f64 / chars.len() as f64 > 0.6;
271 }
272 false
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn compress_off_returns_original() {
281 let text = "hello world\n\nsome blank lines\n\n";
282 let result = compress(text, &CompressionLevel::Off);
283 assert_eq!(result.output, text);
284 assert_eq!(result.lines_removed, 0);
285 }
286
287 #[test]
291 fn dictionary_rewritten_test_summary_keeps_compression() {
292 let text = "running 3 tests\n\n\
293 test orders::totals ... FAILED\n\n\
294 test orders::refund ... ok\n\n\
295 failures:\n\n\
296 test result: FAILED. 2 passed; 1 failed; 0 ignored\n\n";
297 let result = compress(text, &CompressionLevel::Standard);
298 assert_eq!(result.quality.critical_lost, 0, "{}", result.output);
299 assert_ne!(result.output, text, "compression must not fall back");
300 assert!(result.output.contains("FAIL"), "{}", result.output);
301 assert!(result.output.contains("1 failed"), "{}", result.output);
302 }
303
304 #[test]
308 fn unittest_verdict_line_survives_every_level() {
309 let run = "........\n\
310 ----------------------------------------------------------------------\n\
311 Ran 8 tests in 0.001s\n\nOK\n";
312 let failed = "..F.....\n\
313 ----------------------------------------------------------------------\n\
314 Ran 8 tests in 0.001s\n\nFAILED\n";
315 for level in [
316 CompressionLevel::Lite,
317 CompressionLevel::Standard,
318 CompressionLevel::Max,
319 ] {
320 let ok = compress(run, &level).output;
321 assert!(ok.lines().any(|l| l.trim() == "OK"), "{level:?}: {ok}");
322 assert!(ok.contains("Ran 8 tests"), "{level:?}: {ok}");
323 let bad = compress(failed, &level).output;
324 assert!(bad.contains("FAIL"), "{level:?}: {bad}");
325 }
326 }
327
328 #[test]
329 fn compress_lite_removes_blank_lines() {
330 let text = "line one\n\n\nline two\n\n";
331 let result = compress(text, &CompressionLevel::Lite);
332 assert!(
333 !result.output.contains("\n\n"),
334 "blank lines should be removed"
335 );
336 }
337
338 #[test]
339 fn compress_preserves_paths() {
340 let text = "error in src/main.rs at line 42\n\nsome blank\n\n";
341 let result = compress(text, &CompressionLevel::Standard);
342 assert!(
343 result.output.contains("src/main.rs"),
344 "path must be preserved"
345 );
346 }
347
348 #[test]
349 fn decoration_detection() {
350 assert!(is_pure_decoration("════════════════════"));
351 assert!(is_pure_decoration("--------------------"));
352 assert!(is_pure_decoration("// ================"));
353 assert!(!is_pure_decoration("error: mismatched types"));
354 }
355
356 #[test]
357 fn compress_preserves_multi_section_record_boundaries() {
358 let text = "record alpha payload remains associated with alpha\n\
359 additional alpha detail for the first record\n\
360 ===\n\
361 record beta payload remains associated with beta\n\
362 additional beta detail for the second record\n\
363 ---\n\
364 record gamma payload remains associated with gamma\n\
365 ###\n\
366 record delta payload remains associated with delta";
367
368 let result = compress(text, &CompressionLevel::Standard);
369
370 assert!(
371 result.output.contains("==="),
372 "first boundary missing: {}",
373 result.output
374 );
375 assert!(
376 result.output.contains("---"),
377 "second boundary missing: {}",
378 result.output
379 );
380 assert!(
381 result.output.contains("###"),
382 "third boundary missing: {}",
383 result.output
384 );
385 let alpha = result.output.find("record alpha").unwrap();
386 let first_boundary = result.output.find("===").unwrap();
387 let beta = result.output.find("record beta").unwrap();
388 let second_boundary = result.output.find("---").unwrap();
389 let gamma = result.output.find("record gamma").unwrap();
390 let third_boundary = result.output.find("###").unwrap();
391 let delta = result.output.find("record delta").unwrap();
392 assert!(alpha < first_boundary && first_boundary < beta);
393 assert!(beta < second_boundary && second_boundary < gamma);
394 assert!(gamma < third_boundary && third_boundary < delta);
395 }
396
397 #[test]
398 fn compress_returns_token_counts() {
399 let text = "Hello world from the compression engine test";
400 let result = compress(text, &CompressionLevel::Lite);
401 assert!(result.tokens_before > 0);
402 }
403}