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