1use std::collections::{HashMap, HashSet};
2use std::hash::{Hash, Hasher};
3use std::io::Write;
4
5use flate2::Compression;
6use flate2::write::GzEncoder;
7
8use super::tokens::{count_tokens, encode_tokens};
9
10const BPE_ENTROPY_THRESHOLD: f64 = 1.0;
11
12#[derive(Debug)]
14pub struct EntropyResult {
15 pub output: String,
16 pub original_tokens: usize,
17 pub compressed_tokens: usize,
18 pub techniques: Vec<String>,
19}
20
21impl EntropyResult {
22 pub fn savings_percent(&self) -> f64 {
24 if self.original_tokens == 0 {
25 return 0.0;
26 }
27 let saved = self.original_tokens.saturating_sub(self.compressed_tokens);
28 (saved as f64 / self.original_tokens as f64) * 100.0
29 }
30}
31
32pub fn shannon_entropy(text: &str) -> f64 {
34 if text.is_empty() {
35 return 0.0;
36 }
37 let mut freq: HashMap<char, usize> = HashMap::new();
38 let total = text.chars().count();
39
40 for c in text.chars() {
41 *freq.entry(c).or_default() += 1;
42 }
43
44 freq.values().fold(0.0_f64, |acc, &count| {
45 let p = count as f64 / total as f64;
46 acc - p * p.log2()
47 })
48}
49
50pub fn token_entropy_from_ids(tokens: &[u32]) -> f64 {
52 if tokens.is_empty() {
53 return 0.0;
54 }
55 let total = tokens.len();
56 let mut freq: HashMap<u32, usize> = HashMap::new();
57 for &t in tokens {
58 *freq.entry(t).or_default() += 1;
59 }
60 let mut counts: Vec<usize> = freq.into_values().collect();
65 counts.sort_unstable();
66 counts.iter().fold(0.0_f64, |acc, &count| {
67 let p = count as f64 / total as f64;
68 acc - p * p.log2()
69 })
70}
71
72pub fn token_entropy(text: &str) -> f64 {
75 let tokens = encode_tokens(text);
76 token_entropy_from_ids(&tokens)
77}
78
79pub fn normalized_token_entropy_from_ids(tokens: &[u32]) -> f64 {
81 if tokens.is_empty() {
82 return 0.0;
83 }
84 let total = tokens.len();
85 let mut freq: HashMap<u32, usize> = HashMap::new();
86 for &t in tokens {
87 *freq.entry(t).or_default() += 1;
88 }
89 let n_unique = freq.len();
90 if n_unique <= 1 {
91 return 0.0;
92 }
93 let mut counts: Vec<usize> = freq.into_values().collect();
95 counts.sort_unstable();
96 let h = counts.iter().fold(0.0_f64, |acc, &count| {
97 let p = count as f64 / total as f64;
98 acc - p * p.log2()
99 });
100 let h_max = (n_unique as f64).log2();
101 h / h_max
102}
103
104pub fn normalized_token_entropy(text: &str) -> f64 {
108 let tokens = encode_tokens(text);
109 normalized_token_entropy_from_ids(&tokens)
110}
111
112pub fn jaccard_similarity(a: &str, b: &str) -> f64 {
114 let set_a: HashSet<&str> = a.split_whitespace().collect();
115 let set_b: HashSet<&str> = b.split_whitespace().collect();
116
117 let intersection = set_a.intersection(&set_b).count();
118 let union = set_a.union(&set_b).count();
119
120 if union == 0 {
121 return 0.0;
122 }
123 intersection as f64 / union as f64
124}
125
126pub fn ngram_jaccard(a: &str, b: &str, n: usize) -> f64 {
128 let set_a = ngram_set(a, n);
129 let set_b = ngram_set(b, n);
130
131 let intersection = set_a.intersection(&set_b).count();
132 let union = set_a.union(&set_b).count();
133
134 if union == 0 {
135 return 0.0;
136 }
137 intersection as f64 / union as f64
138}
139
140fn ngram_set(text: &str, n: usize) -> HashSet<Vec<String>> {
141 let words: Vec<&str> = text.split_whitespace().collect();
142 if words.len() < n {
143 let mut set = HashSet::new();
144 if !words.is_empty() {
145 set.insert(words.iter().map(std::string::ToString::to_string).collect());
146 }
147 return set;
148 }
149 words
150 .windows(n)
151 .map(|w| w.iter().map(std::string::ToString::to_string).collect())
152 .collect()
153}
154
155pub fn minhash_signature(text: &str, n: usize, k: usize) -> Vec<u64> {
158 let ngrams = ngram_set(text, n);
159 if ngrams.is_empty() {
160 return vec![u64::MAX; k];
161 }
162 let mut signature = vec![u64::MAX; k];
163 for ngram in &ngrams {
164 for (i, min) in signature.iter_mut().enumerate() {
165 let h = hash_with_seed(ngram, i as u64);
166 if h < *min {
167 *min = h;
168 }
169 }
170 }
171 signature
172}
173
174pub fn minhash_similarity(sig_a: &[u64], sig_b: &[u64]) -> f64 {
176 if sig_a.len() != sig_b.len() || sig_a.is_empty() {
177 return 0.0;
178 }
179 let matches = sig_a
180 .iter()
181 .zip(sig_b.iter())
182 .filter(|(a, b)| a == b)
183 .count();
184 matches as f64 / sig_a.len() as f64
185}
186
187fn hash_with_seed<T: Hash>(value: &T, seed: u64) -> u64 {
188 let mut hasher = std::collections::hash_map::DefaultHasher::new();
189 seed.hash(&mut hasher);
190 value.hash(&mut hasher);
191 hasher.finish()
192}
193
194pub fn kolmogorov_proxy(content: &str) -> f64 {
197 if content.is_empty() {
198 return 1.0;
199 }
200 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
201 encoder.write_all(content.as_bytes()).ok();
202 let compressed = encoder.finish().unwrap_or_default();
203 compressed.len() as f64 / content.len() as f64
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub enum CompressibilityClass {
209 High,
210 Medium,
211 Low,
212}
213
214impl CompressibilityClass {
215 pub fn label(&self) -> &'static str {
217 match self {
218 Self::High => "high (K<0.3)",
219 Self::Medium => "medium (0.3≤K<0.6)",
220 Self::Low => "low (K≥0.6)",
221 }
222 }
223}
224
225pub fn compressibility_class(content: &str) -> CompressibilityClass {
227 let k = kolmogorov_proxy(content);
228 if k < 0.3 {
229 CompressibilityClass::High
230 } else if k < 0.6 {
231 CompressibilityClass::Medium
232 } else {
233 CompressibilityClass::Low
234 }
235}
236
237pub fn entropy_compress(content: &str) -> EntropyResult {
239 entropy_compress_with_thresholds(content, BPE_ENTROPY_THRESHOLD, 0.7, &[])
240}
241
242pub fn entropy_compress_deterministic(content: &str) -> EntropyResult {
248 entropy_compress_inner(content, BPE_ENTROPY_THRESHOLD, 0.7, &[], false, &[])
249}
250
251pub fn entropy_compress_adaptive(
255 content: &str,
256 path: &str,
257 force_keep: &[String],
258) -> EntropyResult {
259 let thresholds = super::adaptive_thresholds::read_thresholds(path, content);
260 let before_lines = content.lines().count() as u32;
261 let result = entropy_compress_with_thresholds(
262 content,
263 thresholds.bpe_entropy,
264 thresholds.jaccard,
265 force_keep,
266 );
267 let after_lines = result.output.lines().count() as u32;
268
269 if before_lines != after_lines {
270 super::events::emit(super::events::EventKind::Compression {
271 path: path.to_string(),
272 before_lines,
273 after_lines,
274 strategy: "entropy_adaptive".to_string(),
275 kept_line_count: after_lines,
276 removed_line_count: before_lines.saturating_sub(after_lines),
277 });
278 }
279
280 result
281}
282
283pub fn entropy_compress_with_threshold(
288 content: &str,
289 path: &str,
290 bpe_entropy: f64,
291 force_keep: &[String],
292) -> EntropyResult {
293 let thresholds = super::adaptive_thresholds::read_thresholds(path, content);
294 entropy_compress_with_thresholds(content, bpe_entropy, thresholds.jaccard, force_keep)
295}
296
297pub fn entropy_compress_task_conditioned(
303 content: &str,
304 path: &str,
305 task_keywords: &[String],
306 force_keep: &[String],
307) -> EntropyResult {
308 let thresholds = super::adaptive_thresholds::read_thresholds(path, content);
309 let before_lines = content.lines().count() as u32;
310 let result = entropy_compress_with_task(
311 content,
312 thresholds.bpe_entropy,
313 thresholds.jaccard,
314 task_keywords,
315 force_keep,
316 );
317 let after_lines = result.output.lines().count() as u32;
318 if before_lines != after_lines {
319 super::events::emit(super::events::EventKind::Compression {
320 path: path.to_string(),
321 before_lines,
322 after_lines,
323 strategy: "entropy_task_conditioned".to_string(),
324 kept_line_count: after_lines,
325 removed_line_count: before_lines.saturating_sub(after_lines),
326 });
327 }
328 result
329}
330
331#[cfg(feature = "embeddings")]
337fn line_embedder(line_count: usize) -> impl Fn(&str) -> Option<Vec<f32>> {
338 use std::collections::HashMap;
339 use std::sync::Mutex;
340
341 const MAX_LINES_FOR_SEMANTIC: usize = 400;
342 const CACHE_CAP: usize = 8192;
343 static LINE_EMBED_CACHE: Mutex<Option<HashMap<u64, Vec<f32>>>> = Mutex::new(None);
344
345 let engine = if line_count <= MAX_LINES_FOR_SEMANTIC {
346 crate::core::embedding_access::embedding_engine_nonblocking()
347 } else {
348 None
349 };
350
351 move |line: &str| {
352 let engine = engine?;
353 if line.len() < 8 {
354 return None;
355 }
356 let mut hasher = std::collections::hash_map::DefaultHasher::new();
357 std::hash::Hash::hash(&line, &mut hasher);
358 let key = std::hash::Hasher::finish(&hasher);
359
360 if let Ok(mut guard) = LINE_EMBED_CACHE.lock()
361 && let Some(hit) = guard.get_or_insert_with(HashMap::new).get(&key)
362 {
363 return Some(hit.clone());
364 }
365 let emb = engine.embed(line).ok()?;
366 if let Ok(mut guard) = LINE_EMBED_CACHE.lock() {
367 let map = guard.get_or_insert_with(HashMap::new);
368 if map.len() >= CACHE_CAP {
369 map.clear();
370 }
371 map.insert(key, emb.clone());
372 }
373 Some(emb)
374 }
375}
376
377#[cfg(not(feature = "embeddings"))]
378fn line_embedder(_line_count: usize) -> impl Fn(&str) -> Option<Vec<f32>> {
379 |_: &str| None
380}
381
382fn entropy_compress_with_task(
388 content: &str,
389 entropy_threshold: f64,
390 jaccard_threshold: f64,
391 task_keywords: &[String],
392 force_keep: &[String],
393) -> EntropyResult {
394 entropy_compress_inner(
395 content,
396 entropy_threshold,
397 jaccard_threshold,
398 task_keywords,
399 false,
400 force_keep,
401 )
402}
403
404#[path = "entropy_omission.rs"]
405mod omission;
406use omission::{
407 LineFate, coverage_terms, gap_delta, omission_runs, render_with_omissions,
408 rescue_uncovered_lines,
409};
410
411fn entropy_compress_inner(
412 content: &str,
413 entropy_threshold: f64,
414 jaccard_threshold: f64,
415 task_keywords: &[String],
416 semantic: bool,
417 force_keep: &[String],
418) -> EntropyResult {
419 let original_tokens = count_tokens(content);
420 let lines: Vec<&str> = content.lines().collect();
421 let mut techniques = Vec::new();
422
423 let kw_lower: Vec<String> = task_keywords.iter().map(|k| k.to_lowercase()).collect();
424 let original_count = lines.len();
425 let mut task_rescued = 0usize;
426 let embed = line_embedder(if semantic { original_count } else { usize::MAX });
433 let mut scoring_ctx = super::surprise::ScoringCtx::new();
434 let floor = super::surprise::surprise_floor(lines.iter().copied(), entropy_threshold);
437 let mut fate: Vec<LineFate> = lines
438 .iter()
439 .map(|line| {
440 let trimmed = line.trim();
441 if super::protect::line_is_protected(line, force_keep)
444 || super::surprise::should_keep_line_semantic(
445 trimmed,
446 floor,
447 &embed,
448 &mut scoring_ctx,
449 )
450 {
451 return LineFate::Keep;
452 }
453 if !kw_lower.is_empty() {
455 let lower = trimmed.to_lowercase();
456 if kw_lower.iter().any(|kw| lower.contains(kw.as_str())) {
457 task_rescued += 1;
458 return LineFate::Keep;
459 }
460 }
461 LineFate::Predictable
462 })
463 .collect();
464
465 let kept: Vec<usize> = (0..lines.len())
467 .filter(|&i| fate[i] == LineFate::Keep)
468 .collect();
469 let kept_lines: Vec<&str> = kept.iter().map(|&i| lines[i]).collect();
470 let blocks = extract_blocks(&kept_lines);
471 for group in find_pattern_groups(&blocks, jaccard_threshold) {
472 for &idx in group.iter().skip(1) {
473 if !super::protect::line_is_protected(kept_lines[idx], force_keep) {
476 fate[kept[idx]] = LineFate::Duplicate;
477 }
478 }
479 }
480
481 let covered = rescue_uncovered_lines(&lines, &mut fate);
482 let (result, omitted_predictable, omitted_duplicate) = render_with_omissions(&lines, &fate);
483 if omitted_predictable > 0 || task_rescued > 0 {
484 let mut msg = format!("⊘ {omitted_predictable} predictable lines");
485 if task_rescued > 0 {
486 msg.push_str(&format!(" [+{task_rescued} task-rescued]"));
487 }
488 techniques.push(msg);
489 }
490 if omitted_duplicate > 0 {
491 techniques.push(format!("⊘ {omitted_duplicate} duplicate patterns (J≥0.7)"));
492 }
493 if covered > 0 {
494 techniques.push(format!("+{covered} kept for unique identifiers"));
495 }
496
497 let mut collapsed = Vec::new();
498 let mut blank_count = 0;
499 for line in &result {
500 if line.trim().is_empty() {
501 blank_count += 1;
502 if blank_count <= 1 {
503 collapsed.push(line.clone());
504 }
505 } else {
506 blank_count = 0;
507 collapsed.push(line.clone());
508 }
509 }
510 let output = collapsed.join("\n");
511 let compressed_tokens = count_tokens(&output);
512
513 let final_output = if compressed_tokens > original_tokens {
517 content.to_string()
518 } else {
519 output
520 };
521 let final_tokens = if compressed_tokens > original_tokens {
522 original_tokens
523 } else {
524 compressed_tokens
525 };
526
527 EntropyResult {
528 output: final_output,
529 original_tokens,
530 compressed_tokens: final_tokens,
531 techniques,
532 }
533}
534
535fn entropy_compress_with_thresholds(
536 content: &str,
537 entropy_threshold: f64,
538 jaccard_threshold: f64,
539 force_keep: &[String],
540) -> EntropyResult {
541 entropy_compress_with_task(
542 content,
543 entropy_threshold,
544 jaccard_threshold,
545 &[],
546 force_keep,
547 )
548}
549
550fn delimiter_balance_force_keep(
558 lines: &[&str],
559 scored: &[(usize, f64, usize)],
560 keep: &mut [bool],
561 used: &mut usize,
562 kept_count: &mut usize,
563) {
564 let tok_for =
565 |idx: usize| -> usize { scored.iter().find(|(i, _, _)| *i == idx).map_or(1, |s| s.2) };
566
567 let mut spans: Vec<(usize, usize)> = Vec::new();
569 let mut open_stack: Vec<usize> = Vec::new();
570 for (i, line) in lines.iter().enumerate() {
571 for ch in line.chars() {
572 match ch {
573 '(' | '[' => open_stack.push(i),
574 ')' | ']' => {
575 if let Some(opener) = open_stack.pop()
576 && opener != i
577 {
578 spans.push((opener, i));
579 }
580 }
581 _ => {}
582 }
583 }
584 }
585
586 for &(start, end) in &spans {
589 let any_kept = (start..=end).any(|i| keep[i]);
590 if any_kept {
591 for (i, kept) in keep
592 .iter_mut()
593 .enumerate()
594 .skip(start)
595 .take(end - start + 1)
596 {
597 if !*kept {
598 *kept = true;
599 *used += tok_for(i);
600 *kept_count += 1;
601 }
602 }
603 }
604 }
605}
606
607pub fn entropy_compress_to_density(content: &str, target: f64) -> EntropyResult {
615 entropy_compress_to_density_for(content, target, "")
616}
617
618fn structural_anchor_lines(content: &str, lines: &[&str], ext: &str) -> Vec<usize> {
622 if ext.is_empty() {
623 return Vec::new();
624 }
625 let mut anchors: Vec<usize> = super::signatures::extract_signatures(content, ext)
626 .iter()
627 .filter(|s| {
628 matches!(
629 s.kind,
630 "fn" | "method" | "class" | "struct" | "interface" | "trait" | "enum"
631 )
632 })
633 .filter_map(|s| s.start_line)
634 .filter_map(|line| line.checked_sub(1))
635 .filter(|&i| i < lines.len())
636 .collect();
637 for import in super::deps::extract_deps(content, ext).imports {
638 if let Some(i) = lines.iter().position(|l| l.contains(import.as_str())) {
639 anchors.push(i);
640 }
641 }
642 anchors.sort_unstable();
643 anchors.dedup();
644 anchors
645}
646
647pub fn entropy_compress_to_density_for(content: &str, target: f64, ext: &str) -> EntropyResult {
653 let target = target.clamp(0.05, 1.0);
654 let original_tokens = count_tokens(content);
655 if content.is_empty() || original_tokens == 0 {
656 return EntropyResult {
657 output: String::new(),
658 original_tokens,
659 compressed_tokens: 0,
660 techniques: vec![format!("density target={target:.2} (empty input)")],
661 };
662 }
663 if target >= 1.0 {
664 return EntropyResult {
667 output: content.to_string(),
668 original_tokens,
669 compressed_tokens: original_tokens,
670 techniques: vec![format!("density target={target:.2} (kept all)")],
671 };
672 }
673
674 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
675 let budget = ((original_tokens as f64) * target).ceil() as usize;
676
677 let lines: Vec<&str> = content.lines().collect();
678 let mut scored: Vec<(usize, f64, usize)> = lines
679 .iter()
680 .enumerate()
681 .map(|(i, l)| {
682 let trimmed = l.trim();
683 let toks = count_tokens(trimmed).max(1);
686 (i, token_entropy(trimmed), toks)
687 })
688 .collect();
689 scored.sort_by(|a, b| {
690 b.1.partial_cmp(&a.1)
691 .unwrap_or(std::cmp::Ordering::Equal)
692 .then(a.0.cmp(&b.0))
693 });
694
695 let mut keep = vec![false; lines.len()];
696 let mut used = 0usize;
697 let mut kept_count = 0usize;
698 let tokens_of = |idx: usize| -> usize { scored.iter().find(|s| s.0 == idx).map_or(1, |s| s.2) };
699 let anchors = structural_anchor_lines(content, &lines, ext);
705 for idx in scored.first().map(|s| s.0).into_iter().chain(anchors) {
706 if !keep[idx] {
707 keep[idx] = true;
708 used += tokens_of(idx);
709 kept_count += 1;
710 }
711 }
712 let marker_cost = count_tokens("[lean-ctx: omitted 10 lines]");
717 let mut gaps = omission_runs(&keep);
718 let mut vocab: std::collections::HashSet<&str> = (0..lines.len())
719 .filter(|&i| keep[i])
720 .flat_map(|i| coverage_terms(lines[i]))
721 .collect();
722 for coverage_pass in [true, false] {
723 for &(idx, _h, toks) in &scored {
724 if keep[idx] {
725 continue;
726 }
727 let new_gaps = gaps.saturating_add_signed(gap_delta(&keep, idx));
728 if used + toks + new_gaps * marker_cost > budget {
729 continue;
730 }
731 if coverage_pass && coverage_terms(lines[idx]).all(|t| vocab.contains(t)) {
732 continue;
733 }
734 keep[idx] = true;
735 used += toks;
736 gaps = new_gaps;
737 kept_count += 1;
738 vocab.extend(coverage_terms(lines[idx]));
739 }
740 }
741
742 delimiter_balance_force_keep(&lines, &scored, &mut keep, &mut used, &mut kept_count);
747
748 let fate: Vec<LineFate> = keep
749 .iter()
750 .map(|&k| {
751 if k {
752 LineFate::Keep
753 } else {
754 LineFate::Predictable
755 }
756 })
757 .collect();
758 let (out_lines, omitted, _) = render_with_omissions(&lines, &fate);
759 let output = out_lines.join("\n");
760 let compressed_tokens = count_tokens(&output);
761
762 EntropyResult {
763 output,
764 original_tokens,
765 compressed_tokens,
766 techniques: vec![format!(
767 "density target={target:.2} budget={budget} tok, kept {}/{} lines (⊘ {omitted})",
768 lines.len() - omitted,
769 lines.len()
770 )],
771 }
772}
773
774#[derive(Debug)]
776pub struct EntropyAnalysis {
777 pub avg_entropy: f64,
778 pub low_entropy_count: usize,
779 pub high_entropy_count: usize,
780 pub total_lines: usize,
781}
782
783pub fn analyze_entropy(content: &str) -> EntropyAnalysis {
785 let lines: Vec<&str> = content.lines().collect();
786 let total = lines.len();
787 let mut sum = 0.0;
788 let mut low = 0;
789 let mut high = 0;
790 let mut counted = 0;
791
792 for line in &lines {
793 let trimmed = line.trim();
794 if trimmed.is_empty() {
795 continue;
796 }
797 let h = token_entropy(trimmed);
798 sum += h;
799 counted += 1;
800 if h < BPE_ENTROPY_THRESHOLD {
801 low += 1;
802 }
803 if h > 3.0 {
804 high += 1;
805 }
806 }
807
808 EntropyAnalysis {
809 avg_entropy: if counted > 0 {
810 sum / counted as f64
811 } else {
812 0.0
813 },
814 low_entropy_count: low,
815 high_entropy_count: high,
816 total_lines: total,
817 }
818}
819
820struct Block {
821 content: String,
822}
823
824fn extract_blocks(lines: &[&str]) -> Vec<Block> {
825 let mut blocks = Vec::new();
826 let mut current = String::new();
827
828 for line in lines {
829 let trimmed = line.trim();
830 if trimmed.is_empty() && !current.is_empty() {
831 blocks.push(Block {
832 content: current.clone(),
833 });
834 current.clear();
835 } else if !trimmed.is_empty() {
836 current.push_str(trimmed);
837 current.push('\n');
838 }
839 }
840
841 if !current.is_empty() {
842 blocks.push(Block { content: current });
843 }
844
845 blocks
846}
847
848fn find_pattern_groups(blocks: &[Block], threshold: f64) -> Vec<Vec<usize>> {
849 let sets: Vec<HashSet<Vec<String>>> = blocks.iter().map(|b| ngram_set(&b.content, 2)).collect();
853 let sizes: Vec<usize> = sets.iter().map(std::collections::HashSet::len).collect();
854
855 let mut groups: Vec<Vec<usize>> = Vec::new();
856 let mut assigned: HashSet<usize> = HashSet::new();
857
858 for i in 0..blocks.len() {
859 if assigned.contains(&i) {
860 continue;
861 }
862 let mut group = vec![i];
863 for j in (i + 1)..blocks.len() {
864 if assigned.contains(&j) {
865 continue;
866 }
867 let size_i = sizes[i];
868 let size_j = sizes[j];
869 let min_sz = size_i.min(size_j);
870 let max_sz = size_i.max(size_j);
871 if max_sz > 0 && (min_sz as f64) < (threshold * max_sz as f64) {
872 continue;
873 }
874 let inter = sets[i].intersection(&sets[j]).count();
875 let union = size_i + size_j - inter;
876 if union > 0 && (inter as f64 / union as f64) >= threshold {
877 group.push(j);
878 assigned.insert(j);
879 }
880 }
881 if group.len() > 1 {
882 assigned.insert(i);
883 }
884 groups.push(group);
885 }
886
887 groups
888}
889
890#[cfg(test)]
891mod tests {
892 use super::*;
893
894 #[test]
895 fn protect_force_keeps_lines_in_entropy() {
896 let mut content = String::new();
899 for _ in 0..15 {
900 content.push_str("boilerplate noise line\n");
901 }
902 let baseline =
903 entropy_compress_inner(&content, BPE_ENTROPY_THRESHOLD, 0.7, &[], false, &[]);
904 let protected = entropy_compress_inner(
905 &content,
906 BPE_ENTROPY_THRESHOLD,
907 0.7,
908 &[],
909 false,
910 &["boilerplate".to_string()],
911 );
912 let baseline_hits = baseline.output.matches("boilerplate").count();
913 let protected_hits = protected.output.matches("boilerplate").count();
914 assert_eq!(
917 protected_hits, 15,
918 "all protected lines must survive: {}",
919 protected.output
920 );
921 assert!(
922 protected_hits >= baseline_hits,
923 "protect must keep at least as many lines as the baseline"
924 );
925 }
926
927 #[test]
928 fn empty_force_keep_is_byte_identical() {
929 let content = "fn a() {}\n// note\nlet x = 1;\nlet x = 1;\n// note\n";
931 let a = entropy_compress_inner(content, BPE_ENTROPY_THRESHOLD, 0.7, &[], false, &[]);
932 let b = entropy_compress_deterministic(content);
933 assert_eq!(a.output, b.output);
934 }
935
936 #[test]
937 fn shannon_entropy_empty_is_zero() {
938 assert_eq!(shannon_entropy(""), 0.0);
939 }
940
941 #[test]
942 fn shannon_entropy_single_char() {
943 assert_eq!(shannon_entropy("aaaa"), 0.0);
944 }
945
946 #[test]
947 fn shannon_entropy_high_for_varied_text() {
948 let varied = "abcdefghijklmnopqrstuvwxyz0123456789";
949 let uniform = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
950 assert!(
951 shannon_entropy(varied) > shannon_entropy(uniform),
952 "varied text should have higher entropy"
953 );
954 }
955
956 #[test]
957 fn jaccard_identical_is_one() {
958 let sim = jaccard_similarity("hello world", "hello world");
959 assert!((sim - 1.0).abs() < f64::EPSILON);
960 }
961
962 #[test]
963 fn jaccard_disjoint_is_zero() {
964 let sim = jaccard_similarity("abc", "xyz");
965 assert_eq!(sim, 0.0);
966 }
967
968 #[test]
969 fn jaccard_partial_overlap() {
970 let sim = jaccard_similarity("hello world", "hello rust");
971 assert!(sim > 0.0 && sim < 1.0);
972 }
973
974 #[test]
975 fn entropy_compress_produces_output() {
976 let content = "fn main() {\n println!(\"hello\");\n}\n\n// comment\n// another comment\n\nfn helper() {\n let x = 42;\n}\n";
977 let result = entropy_compress(content);
978 assert!(!result.output.is_empty(), "should produce non-empty output");
979 assert!(result.compressed_tokens <= result.original_tokens);
980 }
981
982 #[test]
983 fn entropy_result_savings() {
984 let r = EntropyResult {
985 output: "short".to_string(),
986 original_tokens: 100,
987 compressed_tokens: 60,
988 techniques: vec!["test".to_string()],
989 };
990 assert!((r.savings_percent() - 40.0).abs() < 0.1);
991 }
992
993 #[test]
994 fn entropy_result_zero_original() {
995 let r = EntropyResult {
996 output: String::new(),
997 original_tokens: 0,
998 compressed_tokens: 0,
999 techniques: vec![],
1000 };
1001 assert_eq!(r.savings_percent(), 0.0);
1002 }
1003
1004 #[test]
1005 fn token_entropy_empty_is_zero() {
1006 assert_eq!(token_entropy(""), 0.0);
1007 }
1008
1009 #[test]
1010 fn token_entropy_single_repeated_token() {
1011 assert_eq!(token_entropy("}"), 0.0);
1012 }
1013
1014 #[test]
1015 fn token_entropy_higher_for_diverse_code() {
1016 let diverse = "let result = compute_something(x, y, z);";
1017 let repetitive = "aaaa aaaa aaaa aaaa";
1018 assert!(
1019 token_entropy(diverse) > token_entropy(repetitive),
1020 "diverse code should have higher BPE token entropy"
1021 );
1022 }
1023
1024 #[test]
1025 fn token_entropy_vs_char_entropy_differ() {
1026 let code = "fn main() { println!(\"hello world\"); }";
1027 let te = token_entropy(code);
1028 let ce = shannon_entropy(code);
1029 assert!(te != ce, "BPE and char entropy should differ for code");
1030 }
1031
1032 #[test]
1033 fn ngram_jaccard_preserves_order() {
1034 let a = "a b c d";
1035 let b = "d c b a";
1036 let word_j = jaccard_similarity(a, b);
1037 let ngram_j = ngram_jaccard(a, b, 2);
1038 assert!(
1039 ngram_j < word_j,
1040 "reordered text should have lower bigram Jaccard ({ngram_j}) than word Jaccard ({word_j})"
1041 );
1042 }
1043
1044 #[test]
1045 fn ngram_jaccard_identical_is_one() {
1046 let text = "fn main() { println!(\"hello\"); }";
1047 let j = ngram_jaccard(text, text, 2);
1048 assert!((j - 1.0).abs() < f64::EPSILON);
1049 }
1050
1051 #[test]
1052 fn ngram_jaccard_disjoint_is_zero() {
1053 let j = ngram_jaccard("alpha beta gamma", "delta epsilon zeta", 2);
1054 assert_eq!(j, 0.0);
1055 }
1056
1057 #[test]
1058 fn minhash_approximates_jaccard() {
1059 let a = "fn main() { let x = 1; let y = 2; let z = x + y; println!(z); }";
1060 let b = "fn main() { let x = 1; let y = 2; let z = x + y; return z; }";
1061 let exact = ngram_jaccard(a, b, 2);
1062 let sig_a = minhash_signature(a, 2, 128);
1063 let sig_b = minhash_signature(b, 2, 128);
1064 let approx = minhash_similarity(&sig_a, &sig_b);
1065 assert!(
1066 (exact - approx).abs() < 0.2,
1067 "minhash ({approx}) should approximate exact ({exact}) within 0.2"
1068 );
1069 }
1070
1071 #[test]
1072 fn minhash_empty_text() {
1073 let sig = minhash_signature("", 2, 64);
1074 assert!(sig.iter().all(|&v| v == u64::MAX));
1075 }
1076
1077 #[test]
1078 fn kolmogorov_empty_is_one() {
1079 assert_eq!(kolmogorov_proxy(""), 1.0);
1080 }
1081
1082 #[test]
1083 fn kolmogorov_repetitive_is_low() {
1084 let repetitive = "aaa\n".repeat(1000);
1085 let k = kolmogorov_proxy(&repetitive);
1086 assert!(
1087 k < 0.1,
1088 "highly repetitive text should compress well: K={k}"
1089 );
1090 }
1091
1092 #[test]
1093 fn kolmogorov_diverse_is_higher() {
1094 let repetitive = "aaa\n".repeat(500);
1095 let diverse = (0..500)
1096 .map(|i| format!("line_{i}_unique_content_{}", i * 17 % 97))
1097 .collect::<Vec<_>>()
1098 .join("\n");
1099 assert!(
1100 kolmogorov_proxy(&diverse) > kolmogorov_proxy(&repetitive),
1101 "diverse content should have higher K than repetitive"
1102 );
1103 }
1104
1105 #[test]
1106 fn compressibility_class_repetitive_is_high() {
1107 let text = "use std::io;\n".repeat(200);
1108 assert_eq!(compressibility_class(&text), CompressibilityClass::High);
1109 }
1110
1111 #[test]
1112 fn kolmogorov_diverse_higher_than_repetitive() {
1113 let rep = "test\n".repeat(500);
1114 let diverse = (0..500)
1115 .map(|i| format!("unique_line_{i}_xk{}", i * 31 % 1000))
1116 .collect::<Vec<_>>()
1117 .join("\n");
1118 assert!(
1119 kolmogorov_proxy(&diverse) > kolmogorov_proxy(&rep),
1120 "diverse content should have higher K"
1121 );
1122 }
1123
1124 fn density_fixture() -> String {
1125 (0..120)
1126 .map(|i| {
1127 if i % 3 == 0 {
1128 format!(
1129 "fn compute_value_{i}(input: &str, flags: u32) -> Result<Output, Error> {{"
1130 )
1131 } else if i % 3 == 1 {
1132 format!(" let intermediate_{i} = transform(input, flags ^ {i});")
1133 } else {
1134 "}".to_string()
1135 }
1136 })
1137 .collect::<Vec<_>>()
1138 .join("\n")
1139 }
1140
1141 #[test]
1142 fn density_respects_token_budget() {
1143 let content = density_fixture();
1144 let orig = count_tokens(&content);
1145 for target in [0.3, 0.5, 0.7] {
1146 let r = entropy_compress_to_density(&content, target);
1147 let actual = r.compressed_tokens as f64 / orig as f64;
1148 assert!(
1151 actual <= target + 0.15,
1152 "target {target}: actual density {actual:.2} exceeds budget"
1153 );
1154 assert!(!r.output.is_empty(), "`r.output` is empty");
1155 }
1156 }
1157
1158 #[test]
1159 fn density_is_deterministic() {
1160 let content = density_fixture();
1161 let a = entropy_compress_to_density(&content, 0.4);
1162 let b = entropy_compress_to_density(&content, 0.4);
1163 assert_eq!(a.output, b.output);
1164 assert_eq!(a.compressed_tokens, b.compressed_tokens);
1165 }
1166
1167 fn registry_fixture() -> String {
1170 let mut rows = vec!["pub const REWRITE_COMMANDS: &[RewriteEntry] = &[".to_string()];
1171 for cmd in [
1172 "git",
1173 "gh",
1174 "cargo",
1175 "npm",
1176 "pnpm",
1177 "yarn",
1178 "bun",
1179 "deno",
1180 "vite",
1181 "tsc",
1182 "python",
1183 "pip",
1184 "pip3",
1185 "uv",
1186 "pytest",
1187 "mypy",
1188 "ruff",
1189 "go",
1190 "docker",
1191 "kubectl",
1192 "helm",
1193 "aws",
1194 "terraform",
1195 "tofu",
1196 "eslint",
1197 "prettier",
1198 "biome",
1199 "curl",
1200 "wget",
1201 "php",
1202 ] {
1203 rows.push(format!(" re(\"{cmd}\", Category::Build),"));
1204 }
1205 rows.push("];".to_string());
1206 rows.push(String::new());
1207 rows.push("pub enum Category {".to_string());
1208 for v in ["Vcs", "Build", "PackageManager", "Lint", "Infra", "Http"] {
1209 rows.push(format!(" {v},"));
1210 }
1211 rows.push("}".to_string());
1212 rows.join("\n")
1213 }
1214
1215 #[test]
1219 fn entropy_never_loses_a_literal_and_marks_every_gap() {
1220 let content = registry_fixture();
1221 for threshold in [0.8, 1.2, 1.9] {
1222 let r = entropy_compress_with_thresholds(&content, threshold, 0.7, &[]);
1223 for cmd in ["\"git\"", "\"gh\"", "\"php\"", "\"pip3\""] {
1224 assert!(r.output.contains(cmd), "t={threshold}: lost {cmd}");
1225 }
1226 let kept_lines = r
1227 .output
1228 .lines()
1229 .filter(|l| !l.contains("[lean-ctx: omitted"));
1230 let kept_source_lines = kept_lines.clone().count();
1231 let marked: usize = r
1232 .output
1233 .lines()
1234 .filter_map(|l| l.split("[lean-ctx: omitted ").nth(1))
1235 .filter_map(|rest| rest.split(' ').next()?.parse::<usize>().ok())
1236 .sum();
1237 assert_eq!(
1238 kept_source_lines + marked,
1239 content.lines().count(),
1240 "t={threshold}: every source line is either shown or counted in a marker"
1241 );
1242 for line in kept_lines {
1243 assert!(content.lines().any(|l| l == line), "invented line {line:?}");
1244 }
1245 }
1246 }
1247
1248 #[test]
1251 fn density_keeps_definitions_and_budgets_its_markers() {
1252 use std::fmt::Write as _;
1253 let mut content = String::new();
1254 for i in 0..40 {
1255 writeln!(content, "pub fn handler_{i}(input: &str) -> usize {{").unwrap();
1256 writeln!(content, " let parsed_{i} = input.len() + {i};").unwrap();
1257 for k in 0..6 {
1258 writeln!(
1259 content,
1260 " let step_{i}_{k} = parsed_{i}.wrapping_mul({k} + {i});"
1261 )
1262 .unwrap();
1263 }
1264 writeln!(content, " parsed_{i} * 2\n}}").unwrap();
1265 }
1266 let orig = count_tokens(&content);
1267 for target in [0.3, 0.5] {
1268 let r = entropy_compress_to_density_for(&content, target, "rs");
1269 for i in 0..40 {
1270 assert!(
1271 r.output.contains(&format!("pub fn handler_{i}(")),
1272 "t={target}: definition handler_{i} dropped"
1273 );
1274 }
1275 assert!(
1276 r.output.contains("[lean-ctx: omitted "),
1277 "t={target}: gaps must be marked"
1278 );
1279 let actual = r.compressed_tokens as f64 / orig as f64;
1280 assert!(
1281 actual <= target + 0.1,
1282 "t={target}: {actual:.2} — the omission markers must fit the budget"
1283 );
1284 }
1285 }
1286
1287 #[test]
1288 fn density_target_one_keeps_everything() {
1289 let content = density_fixture();
1290 let r = entropy_compress_to_density(&content, 1.0);
1291 assert_eq!(r.output, content);
1292 }
1293
1294 #[test]
1295 fn density_clamps_out_of_range_target() {
1296 let content = density_fixture();
1297 let low = entropy_compress_to_density(&content, 0.0);
1298 assert!(!low.output.is_empty(), "clamped to 0.05, never empty");
1299 let high = entropy_compress_to_density(&content, 5.0);
1300 assert_eq!(high.output, content, "clamped to 1.0 keeps all");
1301 }
1302
1303 #[test]
1304 fn density_empty_input() {
1305 let r = entropy_compress_to_density("", 0.5);
1306 assert_eq!(r.compressed_tokens, 0);
1307 assert!(r.output.is_empty(), "{:?}", r.output);
1308 }
1309
1310 #[test]
1311 fn density_delimiter_balance_guard() {
1312 let content = [
1315 "import logging",
1316 "logger = logging.getLogger(__name__)",
1317 "logger.info(",
1318 " 'Processing batch %d of %d',",
1319 " batch_idx,",
1320 " total_batches,",
1321 ")",
1322 "result = process(data)",
1323 ]
1324 .join("\n");
1325 let r = entropy_compress_to_density(&content, 0.5);
1326 if r.output.contains("logger.info(") {
1328 assert!(
1329 r.output.contains(')'),
1330 "delimiter guard must keep closing paren: {}",
1331 r.output
1332 );
1333 assert!(
1334 r.output.contains("batch_idx"),
1335 "delimiter guard must keep continuation args: {}",
1336 r.output
1337 );
1338 }
1339 }
1340
1341 #[test]
1342 fn density_delimiter_backward_propagation() {
1343 let mut lines: Vec<&str> = Vec::new();
1350 lines.push("import logging");
1351 lines.push("logger = logging.getLogger(__name__)");
1352 lines.push("");
1353 lines.extend(std::iter::repeat_n(" x = 1", 20));
1355 lines.push(" logger.info(");
1356 lines.push(" \"Processed item %s status=%s result=%s region=%s\",");
1357 lines.push(" item.id,");
1358 lines.push(" item.status,");
1359 lines.push(" result.summary,");
1360 lines.push(" region_name");
1361 lines.push(" )");
1362 lines.extend(std::iter::repeat_n(" y = 2", 10));
1364 let content = lines.join("\n");
1365 let r = entropy_compress_to_density(&content, 0.3);
1366 if r.output.contains("Processed item") {
1369 assert!(
1370 r.output.contains("logger.info("),
1371 "backward propagation: opener must be kept when format string is kept.\nGot: {}",
1372 r.output
1373 );
1374 assert!(
1375 r.output.contains(')'),
1376 "backward propagation: closer must also be kept.\nGot: {}",
1377 r.output
1378 );
1379 }
1380 if r.output.contains("logger.info(") {
1382 assert!(
1383 r.output.contains("item.id"),
1384 "forward propagation: args must be kept when opener is kept.\nGot: {}",
1385 r.output
1386 );
1387 }
1388 }
1389
1390 #[test]
1391 fn density_prefers_high_entropy_lines() {
1392 let content =
1393 "}\n}\n}\nlet complex_result = compute_unique_hash(seed, nonce, payload);\n}\n}";
1394 let r = entropy_compress_to_density(content, 0.6);
1395 assert!(
1396 r.output.contains("compute_unique_hash"),
1397 "high-entropy line must survive: {}",
1398 r.output
1399 );
1400 }
1401}