Skip to main content

lean_ctx/core/terse/
engine.rs

1//! Layer 1: Deterministic output compression.
2//!
3//! Replaces the legacy `compress_terse`/`compress_ultra` with a scoring-based
4//! approach that preserves information-dense lines and removes low-value content.
5
6use super::counter;
7use super::dictionaries::{self, DictLevel};
8use super::quality::{self, QualityConfig, QualityReport};
9use super::scoring;
10use crate::core::config::CompressionLevel;
11
12/// Threshold below which a line is considered low-information and may be removed.
13const LOW_SCORE_THRESHOLD: f32 = 2.5;
14
15const STANDARD_SCORE_THRESHOLD: f32 = 3.0;
16const MAX_SCORE_THRESHOLD: f32 = 3.5;
17
18/// Result of Layer 1 compression.
19#[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
31/// Runs Layer 1 deterministic compression on the input text.
32pub 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}