Skip to main content

lean_ctx/core/
quality.rs

1use crate::core::preservation;
2use crate::core::tokens::count_tokens;
3
4macro_rules! static_regex {
5    ($pattern:expr_2021) => {{
6        static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
7        RE.get_or_init(|| {
8            regex::Regex::new($pattern).expect(concat!("BUG: invalid static regex: ", $pattern))
9        })
10    }};
11}
12
13const QUALITY_THRESHOLD: f64 = 0.95;
14const MIN_DENSITY: f64 = 0.15;
15
16#[derive(Debug, Clone)]
17pub struct QualityScore {
18    pub ast_score: f64,
19    pub identifier_score: f64,
20    pub line_score: f64,
21    pub density: f64,
22    pub composite: f64,
23    pub passed: bool,
24}
25
26impl QualityScore {
27    pub fn format_compact(&self) -> String {
28        if self.passed {
29            format!(
30                "Q:{:.0}% (ast:{:.0} id:{:.0} ln:{:.0} ρ:{:.0}) ✓",
31                self.composite * 100.0,
32                self.ast_score * 100.0,
33                self.identifier_score * 100.0,
34                self.line_score * 100.0,
35                self.density * 100.0,
36            )
37        } else {
38            format!(
39                "Q:{:.0}% (ast:{:.0} id:{:.0} ln:{:.0} ρ:{:.0}) ✗ BELOW THRESHOLD",
40                self.composite * 100.0,
41                self.ast_score * 100.0,
42                self.identifier_score * 100.0,
43                self.line_score * 100.0,
44                self.density * 100.0,
45            )
46        }
47    }
48}
49
50pub fn score(original: &str, compressed: &str, ext: &str) -> QualityScore {
51    let pres = preservation::measure(original, compressed, ext);
52    let ast_score = pres.overall();
53
54    let identifier_score = measure_identifier_preservation(original, compressed);
55    let line_score = measure_line_preservation(original, compressed);
56    let density = information_density(original, compressed, ext);
57
58    let composite = ast_score * 0.5 + identifier_score * 0.3 + line_score * 0.2;
59
60    let compression_ratio = measure_line_preservation(original, compressed);
61    let adaptive_threshold = QUALITY_THRESHOLD - 0.05 * (1.0 - compression_ratio);
62    let passed = composite >= adaptive_threshold && density >= MIN_DENSITY;
63
64    QualityScore {
65        ast_score,
66        identifier_score,
67        line_score,
68        density,
69        composite,
70        passed,
71    }
72}
73
74/// Information density: ratio of semantic tokens to total output tokens.
75/// Measures how much "meaning" per output token is preserved.
76pub fn information_density(original: &str, compressed: &str, ext: &str) -> f64 {
77    let output_tokens = count_tokens(compressed);
78    if output_tokens == 0 {
79        return 1.0;
80    }
81
82    let pres = preservation::measure(original, compressed, ext);
83    let semantic_items = pres.functions_preserved + pres.exports_preserved + pres.imports_preserved;
84    let ident_re = static_regex!(r"\b[a-zA-Z_][a-zA-Z0-9_]{3,}\b");
85    let unique_idents: std::collections::HashSet<&str> =
86        ident_re.find_iter(compressed).map(|m| m.as_str()).collect();
87    let semantic_token_estimate = semantic_items + unique_idents.len();
88
89    (semantic_token_estimate as f64 / output_tokens as f64).min(1.0)
90}
91
92/// Guard: returns compressed if quality passes, original otherwise
93pub fn guard(original: &str, compressed: &str, ext: &str) -> (String, QualityScore) {
94    let q = score(original, compressed, ext);
95    if q.passed {
96        (compressed.to_string(), q)
97    } else {
98        (original.to_string(), q)
99    }
100}
101
102/// #940: density-specific guard that does NOT penalise line reduction.
103///
104/// The standard `score()` composite weights `line_score` at 20% and the
105/// adaptive threshold rises as `line_score` drops — so a density target of
106/// 0.4 (which *intentionally* discards ~60% of lines) always fails by
107/// construction. Density mode needs a structure-only gate: AST + identifiers.
108pub fn guard_density(
109    original: &str,
110    compressed: &str,
111    ext: &str,
112    target: f64,
113) -> (String, QualityScore) {
114    let pres = preservation::measure(original, compressed, ext);
115    let ast_score = pres.overall();
116    let identifier_score = measure_identifier_preservation(original, compressed);
117    let line_score = measure_line_preservation(original, compressed);
118    let density = information_density(original, compressed, ext);
119
120    // Structure-only composite: ignore line_score for density mode.
121    let structure_composite = ast_score * 0.6 + identifier_score * 0.4;
122
123    // Structure must survive intact: definitions, exports and imports. Losing
124    // fewer lines is the *point* of density mode, never a reason to reject.
125    let structure_threshold = 0.85;
126    // Identifiers are judged against the requested budget: a view holding 30%
127    // of the tokens cannot hold 62% of the identifiers (what a fixed composite
128    // bar demanded, so only targets ≥ 0.8 ever passed), but it must hold at
129    // least as large a share of them as of the tokens — no dilution.
130    let identifier_floor = target.clamp(0.05, structure_threshold);
131    // Information density likewise against the file itself: source code sits
132    // at ρ≈0.07–0.14 *uncompressed*, so the absolute floor rejected every
133    // density view of real code, including ones denser than the original.
134    let density_floor = MIN_DENSITY.min(information_density(original, original, ext));
135    let passed = ast_score >= structure_threshold
136        && identifier_score >= identifier_floor
137        && density >= density_floor;
138
139    let q = QualityScore {
140        ast_score,
141        identifier_score,
142        line_score,
143        density,
144        composite: structure_composite,
145        passed,
146    };
147
148    if passed {
149        (compressed.to_string(), q)
150    } else {
151        tracing::debug!(
152            target = target,
153            composite = structure_composite,
154            threshold = structure_threshold,
155            "density quality gate rejected → full fallback"
156        );
157        (original.to_string(), q)
158    }
159}
160
161fn measure_identifier_preservation(original: &str, compressed: &str) -> f64 {
162    let ident_re = static_regex!(r"\b[a-zA-Z_][a-zA-Z0-9_]{3,}\b");
163
164    let original_idents: std::collections::HashSet<&str> =
165        ident_re.find_iter(original).map(|m| m.as_str()).collect();
166
167    if original_idents.is_empty() {
168        return 1.0;
169    }
170
171    let preserved = original_idents
172        .iter()
173        .filter(|id| compressed.contains(*id))
174        .count();
175
176    preserved as f64 / original_idents.len() as f64
177}
178
179fn measure_line_preservation(original: &str, compressed: &str) -> f64 {
180    let original_lines: usize = original.lines().filter(|l| !l.trim().is_empty()).count();
181    if original_lines == 0 {
182        return 1.0;
183    }
184
185    let compressed_lines: usize = compressed.lines().filter(|l| !l.trim().is_empty()).count();
186    let ratio = compressed_lines as f64 / original_lines as f64;
187
188    ratio.min(1.0)
189}
190
191#[cfg(test)]
192pub mod tests {
193    use super::*;
194
195    #[test]
196    fn test_perfect_score_identity() {
197        let code = "fn main() {\n    println!(\"hello\");\n}\n";
198        let q = score(code, code, "rs");
199        assert!(q.composite >= 0.99);
200        assert!(q.passed);
201    }
202
203    #[test]
204    fn test_score_below_threshold_returns_original() {
205        let original = "fn validate_token() {\n    let result = check();\n    return result;\n}\n";
206        let bad_compressed = "removed everything";
207        let (output, q) = guard(original, bad_compressed, "rs");
208        assert!(!q.passed);
209        assert_eq!(output, original);
210    }
211
212    #[test]
213    fn test_good_compression_passes() {
214        let original = "fn validate_token() {\n    let result = check();\n    return result;\n}\n";
215        let compressed = "fn validate_token() { let result = check(); return result; }";
216        let q = score(original, compressed, "rs");
217        assert!(q.ast_score >= 0.9);
218        assert!(q.identifier_score >= 0.9);
219    }
220
221    #[test]
222    fn test_score_format_compact() {
223        let code = "fn main() {}\n";
224        let q = score(code, code, "rs");
225        let formatted = q.format_compact();
226        assert!(formatted.contains("Q:"));
227        assert!(formatted.contains("✓"));
228    }
229
230    #[test]
231    fn test_empty_content_scores_perfect() {
232        let q = score("", "", "rs");
233        assert!(q.passed);
234        assert!(q.composite >= 0.99);
235    }
236
237    #[test]
238    fn test_rust_file_with_structs() {
239        let original = "pub struct Config {\n    pub name: String,\n    pub value: usize,\n}\n\nimpl Config {\n    pub fn new() -> Self {\n        Self { name: String::new(), value: 0 }\n    }\n}\n";
240        let compressed = "pub struct Config { pub name: String, pub value: usize }\nimpl Config { pub fn new() -> Self { Self { name: String::new(), value: 0 } } }";
241        let q = score(original, compressed, "rs");
242        assert!(q.identifier_score >= 0.9);
243    }
244
245    #[test]
246    fn test_typescript_file() {
247        let original = "export function fetchData(url: string): Promise<Response> {\n  return fetch(url);\n}\n\nexport const API_URL = 'https://api.example.com';\n";
248        let compressed = "export function fetchData(url: string): Promise<Response> { return fetch(url); }\nexport const API_URL = 'https://api.example.com';";
249        let q = score(original, compressed, "ts");
250        assert!(q.identifier_score >= 0.9);
251    }
252
253    #[test]
254    fn test_python_file() {
255        let original = "def validate_credentials(username: str, password: str) -> bool:\n    user = find_user(username)\n    return verify_hash(user.password_hash, password)\n";
256        let compressed = "def validate_credentials(username, password): user = find_user(username); return verify_hash(user.password_hash, password)";
257        let q = score(original, compressed, "py");
258        assert!(q.identifier_score >= 0.8);
259    }
260
261    #[test]
262    fn test_density_high_for_meaningful_compression() {
263        let original = "pub fn calculate_total(items: Vec<Item>) -> f64 {\n    items.iter().map(|i| i.price * i.quantity as f64).sum()\n}\n";
264        let d = information_density(original, original, "rs");
265        assert!(d > 0.15, "identity should have high density: {d}");
266    }
267
268    #[test]
269    fn test_density_low_for_garbage() {
270        let original = "pub fn calculate_total(items: Vec<Item>) -> f64 {\n    items.iter().map(|i| i.price).sum()\n}\n";
271        let garbage = "xxx xxx xxx xxx xxx xxx xxx xxx xxx xxx xxx xxx xxx xxx xxx";
272        let d = information_density(original, garbage, "rs");
273        assert!(d < 0.5, "garbage output should have low density: {d}");
274    }
275
276    #[test]
277    fn test_density_in_quality_score() {
278        let code = "fn main() {\n    println!(\"hello\");\n}\n";
279        let q = score(code, code, "rs");
280        assert!(q.density > 0.0, "density should be computed");
281    }
282
283    #[test]
284    fn test_adaptive_threshold_looser_for_heavy_compression() {
285        let original = "fn validate_token() {\n    let result = check();\n    return result;\n}\nfn other() {\n    let x = 1;\n}\n";
286        let compressed = "fn validate_token() { let result = check(); return result; }";
287        let q = score(original, compressed, "rs");
288        assert!(
289            q.density >= MIN_DENSITY,
290            "compressed code should maintain minimum density"
291        );
292    }
293}