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_pure_decoration(trimmed) {
99 lines_removed += 1;
100 continue;
101 }
102
103 if is_filler_line(trimmed) && !score.has_structural_marker {
104 lines_removed += 1;
105 continue;
106 }
107
108 if score.combined < threshold && !score.has_structural_marker {
109 lines_removed += 1;
110 continue;
111 }
112
113 kept_lines.push(*line);
114 }
115
116 let filtered = kept_lines.join("\n");
117
118 let quality_config = match level {
119 CompressionLevel::Max | CompressionLevel::Raw => QualityConfig {
120 min_identifier_preservation: 0.80,
121 ..QualityConfig::default()
122 },
123 _ => QualityConfig::default(),
124 };
125
126 let filtered_tokens = counter::count(&filtered);
127 let quality_report = quality::check(
128 text,
129 &filtered,
130 tokens_before,
131 filtered_tokens,
132 &quality_config,
133 );
134
135 if !quality_report.passed {
136 return EngineResult {
137 output: text.to_string(),
138 tokens_before,
139 tokens_after: tokens_before,
140 quality: quality_report,
141 lines_removed: 0,
142 lines_total,
143 };
144 }
145
146 let dict_level = match level {
147 CompressionLevel::Max | CompressionLevel::Raw | CompressionLevel::Standard => {
148 DictLevel::Full
149 }
150 CompressionLevel::Lite | CompressionLevel::Off => DictLevel::General,
151 };
152 let compressed = dictionaries::apply_dictionaries(&filtered, dict_level);
153 let compressed = match dict_level {
154 DictLevel::Full => super::auto_dict::apply(&compressed).unwrap_or(compressed),
155 DictLevel::General => compressed,
156 };
157 let tokens_after = counter::count(&compressed);
158
159 EngineResult {
160 output: compressed,
161 tokens_before,
162 tokens_after,
163 quality: quality_report,
164 lines_removed,
165 lines_total,
166 }
167}
168
169fn is_filler_line(line: &str) -> bool {
170 let trimmed = line.trim();
171
172 if trimmed == "|" || trimmed == "| " {
173 return true;
174 }
175
176 let lower = line.to_lowercase();
177 const FILLER_PATTERNS: &[&str] = &[
178 "use \"git add",
179 "use \"git restore",
180 "(use \"git",
181 "run with `rust_backtrace",
182 "for more information about this error",
183 "try `rustc --explain",
184 "run `npm fund`",
185 "run `npm audit`",
186 "to address all issues",
187 "sending build context",
188 "using cache",
189 "packages are looking for funding",
190 "no changes added to commit",
191 "help: ",
192 "= note: ",
193 "---> running in",
194 ];
195 FILLER_PATTERNS.iter().any(|p| lower.contains(p))
196}
197
198fn is_pure_decoration(line: &str) -> bool {
199 let trimmed = line.trim();
200
201 if trimmed.is_empty() {
202 return true;
203 }
204
205 if trimmed.chars().all(|c| c == '|' || c.is_whitespace()) {
206 return true;
207 }
208
209 if line.len() < 3 {
210 return false;
211 }
212
213 if line.starts_with("//") || line.starts_with('#') || line.starts_with("--") {
214 let content = line
215 .trim_start_matches('/')
216 .trim_start_matches('#')
217 .trim_start_matches('-')
218 .trim();
219 return content.is_empty() || is_banner_chars(content);
220 }
221
222 is_banner_chars(line)
223}
224
225fn is_banner_chars(line: &str) -> bool {
226 let chars: Vec<char> = line.chars().collect();
227 if chars.len() < 4 {
228 return false;
229 }
230 let first = chars[0];
231 if matches!(
232 first,
233 '=' | '-' | '*' | '─' | '━' | '▀' | '▄' | '╔' | '╚' | '║' | '░' | '█' | '═'
234 ) {
235 let same_count = chars.iter().filter(|c| **c == first).count();
236 return same_count as f64 / chars.len() as f64 > 0.6;
237 }
238 false
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn compress_off_returns_original() {
247 let text = "hello world\n\nsome blank lines\n\n";
248 let result = compress(text, &CompressionLevel::Off);
249 assert_eq!(result.output, text);
250 assert_eq!(result.lines_removed, 0);
251 }
252
253 #[test]
254 fn compress_lite_removes_blank_lines() {
255 let text = "line one\n\n\nline two\n\n";
256 let result = compress(text, &CompressionLevel::Lite);
257 assert!(
258 !result.output.contains("\n\n"),
259 "blank lines should be removed"
260 );
261 }
262
263 #[test]
264 fn compress_preserves_paths() {
265 let text = "error in src/main.rs at line 42\n\nsome blank\n\n";
266 let result = compress(text, &CompressionLevel::Standard);
267 assert!(
268 result.output.contains("src/main.rs"),
269 "path must be preserved"
270 );
271 }
272
273 #[test]
274 fn decoration_detection() {
275 assert!(is_pure_decoration("════════════════════"));
276 assert!(is_pure_decoration("--------------------"));
277 assert!(is_pure_decoration("// ================"));
278 assert!(!is_pure_decoration("error: mismatched types"));
279 }
280
281 #[test]
282 fn compress_returns_token_counts() {
283 let text = "Hello world from the compression engine test";
284 let result = compress(text, &CompressionLevel::Lite);
285 assert!(result.tokens_before > 0);
286 }
287}