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::adaptive_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::adaptive_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::adaptive_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::tools::ctx_knowledge::embeddings::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(
383 content: &str,
384 entropy_threshold: f64,
385 jaccard_threshold: f64,
386 task_keywords: &[String],
387 force_keep: &[String],
388) -> EntropyResult {
389 entropy_compress_inner(
390 content,
391 entropy_threshold,
392 jaccard_threshold,
393 task_keywords,
394 true,
395 force_keep,
396 )
397}
398
399fn entropy_compress_inner(
400 content: &str,
401 entropy_threshold: f64,
402 jaccard_threshold: f64,
403 task_keywords: &[String],
404 semantic: bool,
405 force_keep: &[String],
406) -> EntropyResult {
407 let original_tokens = count_tokens(content);
408 let mut lines: Vec<&str> = content.lines().collect();
409 let mut techniques = Vec::new();
410
411 let kw_lower: Vec<String> = task_keywords.iter().map(|k| k.to_lowercase()).collect();
412 let original_count = lines.len();
413 let mut task_rescued = 0usize;
414 let embed = line_embedder(if semantic { original_count } else { usize::MAX });
421 let mut scoring_ctx = super::surprise::ScoringCtx::new();
422 lines.retain(|line| {
423 let trimmed = line.trim();
424 if super::protect::line_is_protected(line, force_keep) {
427 return true;
428 }
429 if super::surprise::should_keep_line_semantic(
430 trimmed,
431 entropy_threshold,
432 &embed,
433 &mut scoring_ctx,
434 ) {
435 return true;
436 }
437 if !kw_lower.is_empty() {
439 let lower = trimmed.to_lowercase();
440 if kw_lower.iter().any(|kw| lower.contains(kw.as_str())) {
441 task_rescued += 1;
442 return true;
443 }
444 }
445 false
446 });
447 let removed = original_count - lines.len();
448 if removed > 0 || task_rescued > 0 {
449 let mut msg = format!("⊘ {removed} low-entropy lines (BPE H<{entropy_threshold:.2})");
450 if task_rescued > 0 {
451 msg.push_str(&format!(" [+{task_rescued} task-rescued]"));
452 }
453 techniques.push(msg);
454 }
455
456 let blocks = extract_blocks(&lines);
457 let groups = find_pattern_groups(&blocks, jaccard_threshold);
458 let mut dedup_count = 0;
459 for group in &groups {
460 if group.len() > 1 {
461 dedup_count += group.len() - 1;
462 }
463 }
464 if dedup_count > 0 {
465 techniques.push(format!("⊘ {dedup_count} duplicate patterns (J≥0.7)"));
466 }
467
468 let mut result: Vec<String> = Vec::new();
469 let mut skip_indices: std::collections::HashSet<usize> = std::collections::HashSet::new();
470 for group in &groups {
471 if group.len() > 1 {
472 for &idx in &group[1..] {
473 if !super::protect::line_is_protected(lines[idx], force_keep) {
476 skip_indices.insert(idx);
477 }
478 }
479 }
480 }
481 for (i, line) in lines.iter().enumerate() {
482 if !skip_indices.contains(&i) {
483 result.push(line.to_string());
484 }
485 }
486
487 let mut collapsed = Vec::new();
488 let mut blank_count = 0;
489 for line in &result {
490 if line.trim().is_empty() {
491 blank_count += 1;
492 if blank_count <= 1 {
493 collapsed.push(line.clone());
494 }
495 } else {
496 blank_count = 0;
497 collapsed.push(line.clone());
498 }
499 }
500 let output = collapsed.join("\n");
501 let compressed_tokens = count_tokens(&output);
502
503 let final_output = if compressed_tokens > original_tokens {
507 content.to_string()
508 } else {
509 output
510 };
511 let final_tokens = if compressed_tokens > original_tokens {
512 original_tokens
513 } else {
514 compressed_tokens
515 };
516
517 EntropyResult {
518 output: final_output,
519 original_tokens,
520 compressed_tokens: final_tokens,
521 techniques,
522 }
523}
524
525fn entropy_compress_with_thresholds(
526 content: &str,
527 entropy_threshold: f64,
528 jaccard_threshold: f64,
529 force_keep: &[String],
530) -> EntropyResult {
531 entropy_compress_with_task(
532 content,
533 entropy_threshold,
534 jaccard_threshold,
535 &[],
536 force_keep,
537 )
538}
539
540fn delimiter_balance_force_keep(
548 lines: &[&str],
549 scored: &[(usize, f64, usize)],
550 keep: &mut [bool],
551 used: &mut usize,
552 kept_count: &mut usize,
553) {
554 let tok_for =
555 |idx: usize| -> usize { scored.iter().find(|(i, _, _)| *i == idx).map_or(1, |s| s.2) };
556
557 let mut spans: Vec<(usize, usize)> = Vec::new();
559 let mut open_stack: Vec<usize> = Vec::new();
560 for (i, line) in lines.iter().enumerate() {
561 for ch in line.chars() {
562 match ch {
563 '(' | '[' => open_stack.push(i),
564 ')' | ']' => {
565 if let Some(opener) = open_stack.pop()
566 && opener != i
567 {
568 spans.push((opener, i));
569 }
570 }
571 _ => {}
572 }
573 }
574 }
575
576 for &(start, end) in &spans {
579 let any_kept = (start..=end).any(|i| keep[i]);
580 if any_kept {
581 for (i, kept) in keep
582 .iter_mut()
583 .enumerate()
584 .skip(start)
585 .take(end - start + 1)
586 {
587 if !*kept {
588 *kept = true;
589 *used += tok_for(i);
590 *kept_count += 1;
591 }
592 }
593 }
594 }
595}
596
597pub fn entropy_compress_to_density(content: &str, target: f64) -> EntropyResult {
605 let target = target.clamp(0.05, 1.0);
606 let original_tokens = count_tokens(content);
607 if content.is_empty() || original_tokens == 0 {
608 return EntropyResult {
609 output: String::new(),
610 original_tokens,
611 compressed_tokens: 0,
612 techniques: vec![format!("density target={target:.2} (empty input)")],
613 };
614 }
615
616 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
617 let budget = ((original_tokens as f64) * target).ceil() as usize;
618
619 let lines: Vec<&str> = content.lines().collect();
620 let mut scored: Vec<(usize, f64, usize)> = lines
621 .iter()
622 .enumerate()
623 .map(|(i, l)| {
624 let trimmed = l.trim();
625 let toks = count_tokens(trimmed).max(1);
628 (i, token_entropy(trimmed), toks)
629 })
630 .collect();
631 scored.sort_by(|a, b| {
632 b.1.partial_cmp(&a.1)
633 .unwrap_or(std::cmp::Ordering::Equal)
634 .then(a.0.cmp(&b.0))
635 });
636
637 let mut keep = vec![false; lines.len()];
638 let mut used = 0usize;
639 let mut kept_count = 0usize;
640 if let Some(&(idx, _, toks)) = scored.first() {
644 keep[idx] = true;
645 used += toks;
646 kept_count += 1;
647 }
648 for &(idx, _h, toks) in scored.iter().skip(1) {
649 if used + toks > budget {
650 continue;
651 }
652 keep[idx] = true;
653 used += toks;
654 kept_count += 1;
655 }
656
657 delimiter_balance_force_keep(&lines, &scored, &mut keep, &mut used, &mut kept_count);
662
663 let mut out_lines: Vec<&str> = Vec::with_capacity(kept_count);
664 for (i, line) in lines.iter().enumerate() {
665 if keep[i] {
666 out_lines.push(line);
667 }
668 }
669 let output = out_lines.join("\n");
670 let compressed_tokens = count_tokens(&output);
671
672 let dropped = lines.len() - kept_count;
673 EntropyResult {
674 output,
675 original_tokens,
676 compressed_tokens,
677 techniques: vec![format!(
678 "density target={target:.2} budget={budget} tok, kept {kept_count}/{} lines (⊘ {dropped})",
679 lines.len()
680 )],
681 }
682}
683
684#[derive(Debug)]
686pub struct EntropyAnalysis {
687 pub avg_entropy: f64,
688 pub low_entropy_count: usize,
689 pub high_entropy_count: usize,
690 pub total_lines: usize,
691}
692
693pub fn analyze_entropy(content: &str) -> EntropyAnalysis {
695 let lines: Vec<&str> = content.lines().collect();
696 let total = lines.len();
697 let mut sum = 0.0;
698 let mut low = 0;
699 let mut high = 0;
700 let mut counted = 0;
701
702 for line in &lines {
703 let trimmed = line.trim();
704 if trimmed.is_empty() {
705 continue;
706 }
707 let h = token_entropy(trimmed);
708 sum += h;
709 counted += 1;
710 if h < BPE_ENTROPY_THRESHOLD {
711 low += 1;
712 }
713 if h > 3.0 {
714 high += 1;
715 }
716 }
717
718 EntropyAnalysis {
719 avg_entropy: if counted > 0 {
720 sum / counted as f64
721 } else {
722 0.0
723 },
724 low_entropy_count: low,
725 high_entropy_count: high,
726 total_lines: total,
727 }
728}
729
730struct Block {
731 content: String,
732}
733
734fn extract_blocks(lines: &[&str]) -> Vec<Block> {
735 let mut blocks = Vec::new();
736 let mut current = String::new();
737
738 for line in lines {
739 let trimmed = line.trim();
740 if trimmed.is_empty() && !current.is_empty() {
741 blocks.push(Block {
742 content: current.clone(),
743 });
744 current.clear();
745 } else if !trimmed.is_empty() {
746 current.push_str(trimmed);
747 current.push('\n');
748 }
749 }
750
751 if !current.is_empty() {
752 blocks.push(Block { content: current });
753 }
754
755 blocks
756}
757
758fn find_pattern_groups(blocks: &[Block], threshold: f64) -> Vec<Vec<usize>> {
759 let sets: Vec<HashSet<Vec<String>>> = blocks.iter().map(|b| ngram_set(&b.content, 2)).collect();
763 let sizes: Vec<usize> = sets.iter().map(std::collections::HashSet::len).collect();
764
765 let mut groups: Vec<Vec<usize>> = Vec::new();
766 let mut assigned: HashSet<usize> = HashSet::new();
767
768 for i in 0..blocks.len() {
769 if assigned.contains(&i) {
770 continue;
771 }
772 let mut group = vec![i];
773 for j in (i + 1)..blocks.len() {
774 if assigned.contains(&j) {
775 continue;
776 }
777 let size_i = sizes[i];
778 let size_j = sizes[j];
779 let min_sz = size_i.min(size_j);
780 let max_sz = size_i.max(size_j);
781 if max_sz > 0 && (min_sz as f64) < (threshold * max_sz as f64) {
782 continue;
783 }
784 let inter = sets[i].intersection(&sets[j]).count();
785 let union = size_i + size_j - inter;
786 if union > 0 && (inter as f64 / union as f64) >= threshold {
787 group.push(j);
788 assigned.insert(j);
789 }
790 }
791 if group.len() > 1 {
792 assigned.insert(i);
793 }
794 groups.push(group);
795 }
796
797 groups
798}
799
800#[cfg(test)]
801mod tests {
802 use super::*;
803
804 #[test]
805 fn protect_force_keeps_lines_in_entropy() {
806 let mut content = String::new();
809 for _ in 0..15 {
810 content.push_str("boilerplate noise line\n");
811 }
812 let baseline =
813 entropy_compress_inner(&content, BPE_ENTROPY_THRESHOLD, 0.7, &[], false, &[]);
814 let protected = entropy_compress_inner(
815 &content,
816 BPE_ENTROPY_THRESHOLD,
817 0.7,
818 &[],
819 false,
820 &["boilerplate".to_string()],
821 );
822 let baseline_hits = baseline.output.matches("boilerplate").count();
823 let protected_hits = protected.output.matches("boilerplate").count();
824 assert_eq!(
827 protected_hits, 15,
828 "all protected lines must survive: {}",
829 protected.output
830 );
831 assert!(
832 protected_hits >= baseline_hits,
833 "protect must keep at least as many lines as the baseline"
834 );
835 }
836
837 #[test]
838 fn empty_force_keep_is_byte_identical() {
839 let content = "fn a() {}\n// note\nlet x = 1;\nlet x = 1;\n// note\n";
841 let a = entropy_compress_inner(content, BPE_ENTROPY_THRESHOLD, 0.7, &[], false, &[]);
842 let b = entropy_compress_deterministic(content);
843 assert_eq!(a.output, b.output);
844 }
845
846 #[test]
847 fn shannon_entropy_empty_is_zero() {
848 assert_eq!(shannon_entropy(""), 0.0);
849 }
850
851 #[test]
852 fn shannon_entropy_single_char() {
853 assert_eq!(shannon_entropy("aaaa"), 0.0);
854 }
855
856 #[test]
857 fn shannon_entropy_high_for_varied_text() {
858 let varied = "abcdefghijklmnopqrstuvwxyz0123456789";
859 let uniform = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
860 assert!(
861 shannon_entropy(varied) > shannon_entropy(uniform),
862 "varied text should have higher entropy"
863 );
864 }
865
866 #[test]
867 fn jaccard_identical_is_one() {
868 let sim = jaccard_similarity("hello world", "hello world");
869 assert!((sim - 1.0).abs() < f64::EPSILON);
870 }
871
872 #[test]
873 fn jaccard_disjoint_is_zero() {
874 let sim = jaccard_similarity("abc", "xyz");
875 assert_eq!(sim, 0.0);
876 }
877
878 #[test]
879 fn jaccard_partial_overlap() {
880 let sim = jaccard_similarity("hello world", "hello rust");
881 assert!(sim > 0.0 && sim < 1.0);
882 }
883
884 #[test]
885 fn entropy_compress_produces_output() {
886 let content = "fn main() {\n println!(\"hello\");\n}\n\n// comment\n// another comment\n\nfn helper() {\n let x = 42;\n}\n";
887 let result = entropy_compress(content);
888 assert!(!result.output.is_empty(), "should produce non-empty output");
889 assert!(result.compressed_tokens <= result.original_tokens);
890 }
891
892 #[test]
893 fn entropy_result_savings() {
894 let r = EntropyResult {
895 output: "short".to_string(),
896 original_tokens: 100,
897 compressed_tokens: 60,
898 techniques: vec!["test".to_string()],
899 };
900 assert!((r.savings_percent() - 40.0).abs() < 0.1);
901 }
902
903 #[test]
904 fn entropy_result_zero_original() {
905 let r = EntropyResult {
906 output: String::new(),
907 original_tokens: 0,
908 compressed_tokens: 0,
909 techniques: vec![],
910 };
911 assert_eq!(r.savings_percent(), 0.0);
912 }
913
914 #[test]
915 fn token_entropy_empty_is_zero() {
916 assert_eq!(token_entropy(""), 0.0);
917 }
918
919 #[test]
920 fn token_entropy_single_repeated_token() {
921 assert_eq!(token_entropy("}"), 0.0);
922 }
923
924 #[test]
925 fn token_entropy_higher_for_diverse_code() {
926 let diverse = "let result = compute_something(x, y, z);";
927 let repetitive = "aaaa aaaa aaaa aaaa";
928 assert!(
929 token_entropy(diverse) > token_entropy(repetitive),
930 "diverse code should have higher BPE token entropy"
931 );
932 }
933
934 #[test]
935 fn token_entropy_vs_char_entropy_differ() {
936 let code = "fn main() { println!(\"hello world\"); }";
937 let te = token_entropy(code);
938 let ce = shannon_entropy(code);
939 assert!(te != ce, "BPE and char entropy should differ for code");
940 }
941
942 #[test]
943 fn ngram_jaccard_preserves_order() {
944 let a = "a b c d";
945 let b = "d c b a";
946 let word_j = jaccard_similarity(a, b);
947 let ngram_j = ngram_jaccard(a, b, 2);
948 assert!(
949 ngram_j < word_j,
950 "reordered text should have lower bigram Jaccard ({ngram_j}) than word Jaccard ({word_j})"
951 );
952 }
953
954 #[test]
955 fn ngram_jaccard_identical_is_one() {
956 let text = "fn main() { println!(\"hello\"); }";
957 let j = ngram_jaccard(text, text, 2);
958 assert!((j - 1.0).abs() < f64::EPSILON);
959 }
960
961 #[test]
962 fn ngram_jaccard_disjoint_is_zero() {
963 let j = ngram_jaccard("alpha beta gamma", "delta epsilon zeta", 2);
964 assert_eq!(j, 0.0);
965 }
966
967 #[test]
968 fn minhash_approximates_jaccard() {
969 let a = "fn main() { let x = 1; let y = 2; let z = x + y; println!(z); }";
970 let b = "fn main() { let x = 1; let y = 2; let z = x + y; return z; }";
971 let exact = ngram_jaccard(a, b, 2);
972 let sig_a = minhash_signature(a, 2, 128);
973 let sig_b = minhash_signature(b, 2, 128);
974 let approx = minhash_similarity(&sig_a, &sig_b);
975 assert!(
976 (exact - approx).abs() < 0.2,
977 "minhash ({approx}) should approximate exact ({exact}) within 0.2"
978 );
979 }
980
981 #[test]
982 fn minhash_empty_text() {
983 let sig = minhash_signature("", 2, 64);
984 assert!(sig.iter().all(|&v| v == u64::MAX));
985 }
986
987 #[test]
988 fn kolmogorov_empty_is_one() {
989 assert_eq!(kolmogorov_proxy(""), 1.0);
990 }
991
992 #[test]
993 fn kolmogorov_repetitive_is_low() {
994 let repetitive = "aaa\n".repeat(1000);
995 let k = kolmogorov_proxy(&repetitive);
996 assert!(
997 k < 0.1,
998 "highly repetitive text should compress well: K={k}"
999 );
1000 }
1001
1002 #[test]
1003 fn kolmogorov_diverse_is_higher() {
1004 let repetitive = "aaa\n".repeat(500);
1005 let diverse = (0..500)
1006 .map(|i| format!("line_{i}_unique_content_{}", i * 17 % 97))
1007 .collect::<Vec<_>>()
1008 .join("\n");
1009 assert!(
1010 kolmogorov_proxy(&diverse) > kolmogorov_proxy(&repetitive),
1011 "diverse content should have higher K than repetitive"
1012 );
1013 }
1014
1015 #[test]
1016 fn compressibility_class_repetitive_is_high() {
1017 let text = "use std::io;\n".repeat(200);
1018 assert_eq!(compressibility_class(&text), CompressibilityClass::High);
1019 }
1020
1021 #[test]
1022 fn kolmogorov_diverse_higher_than_repetitive() {
1023 let rep = "test\n".repeat(500);
1024 let diverse = (0..500)
1025 .map(|i| format!("unique_line_{i}_xk{}", i * 31 % 1000))
1026 .collect::<Vec<_>>()
1027 .join("\n");
1028 assert!(
1029 kolmogorov_proxy(&diverse) > kolmogorov_proxy(&rep),
1030 "diverse content should have higher K"
1031 );
1032 }
1033
1034 fn density_fixture() -> String {
1035 (0..120)
1036 .map(|i| {
1037 if i % 3 == 0 {
1038 format!(
1039 "fn compute_value_{i}(input: &str, flags: u32) -> Result<Output, Error> {{"
1040 )
1041 } else if i % 3 == 1 {
1042 format!(" let intermediate_{i} = transform(input, flags ^ {i});")
1043 } else {
1044 "}".to_string()
1045 }
1046 })
1047 .collect::<Vec<_>>()
1048 .join("\n")
1049 }
1050
1051 #[test]
1052 fn density_respects_token_budget() {
1053 let content = density_fixture();
1054 let orig = count_tokens(&content);
1055 for target in [0.3, 0.5, 0.7] {
1056 let r = entropy_compress_to_density(&content, target);
1057 let actual = r.compressed_tokens as f64 / orig as f64;
1058 assert!(
1061 actual <= target + 0.15,
1062 "target {target}: actual density {actual:.2} exceeds budget"
1063 );
1064 assert!(!r.output.is_empty());
1065 }
1066 }
1067
1068 #[test]
1069 fn density_is_deterministic() {
1070 let content = density_fixture();
1071 let a = entropy_compress_to_density(&content, 0.4);
1072 let b = entropy_compress_to_density(&content, 0.4);
1073 assert_eq!(a.output, b.output);
1074 assert_eq!(a.compressed_tokens, b.compressed_tokens);
1075 }
1076
1077 #[test]
1078 fn density_target_one_keeps_everything() {
1079 let content = density_fixture();
1080 let r = entropy_compress_to_density(&content, 1.0);
1081 assert_eq!(r.output, content);
1082 }
1083
1084 #[test]
1085 fn density_clamps_out_of_range_target() {
1086 let content = density_fixture();
1087 let low = entropy_compress_to_density(&content, 0.0);
1088 assert!(!low.output.is_empty(), "clamped to 0.05, never empty");
1089 let high = entropy_compress_to_density(&content, 5.0);
1090 assert_eq!(high.output, content, "clamped to 1.0 keeps all");
1091 }
1092
1093 #[test]
1094 fn density_empty_input() {
1095 let r = entropy_compress_to_density("", 0.5);
1096 assert_eq!(r.compressed_tokens, 0);
1097 assert!(r.output.is_empty());
1098 }
1099
1100 #[test]
1101 fn density_delimiter_balance_guard() {
1102 let content = [
1105 "import logging",
1106 "logger = logging.getLogger(__name__)",
1107 "logger.info(",
1108 " 'Processing batch %d of %d',",
1109 " batch_idx,",
1110 " total_batches,",
1111 ")",
1112 "result = process(data)",
1113 ]
1114 .join("\n");
1115 let r = entropy_compress_to_density(&content, 0.5);
1116 if r.output.contains("logger.info(") {
1118 assert!(
1119 r.output.contains(')'),
1120 "delimiter guard must keep closing paren: {}",
1121 r.output
1122 );
1123 assert!(
1124 r.output.contains("batch_idx"),
1125 "delimiter guard must keep continuation args: {}",
1126 r.output
1127 );
1128 }
1129 }
1130
1131 #[test]
1132 fn density_delimiter_backward_propagation() {
1133 let mut lines: Vec<&str> = Vec::new();
1140 lines.push("import logging");
1141 lines.push("logger = logging.getLogger(__name__)");
1142 lines.push("");
1143 lines.extend(std::iter::repeat_n(" x = 1", 20));
1145 lines.push(" logger.info(");
1146 lines.push(" \"Processed item %s status=%s result=%s region=%s\",");
1147 lines.push(" item.id,");
1148 lines.push(" item.status,");
1149 lines.push(" result.summary,");
1150 lines.push(" region_name");
1151 lines.push(" )");
1152 lines.extend(std::iter::repeat_n(" y = 2", 10));
1154 let content = lines.join("\n");
1155 let r = entropy_compress_to_density(&content, 0.3);
1156 if r.output.contains("Processed item") {
1159 assert!(
1160 r.output.contains("logger.info("),
1161 "backward propagation: opener must be kept when format string is kept.\nGot: {}",
1162 r.output
1163 );
1164 assert!(
1165 r.output.contains(')'),
1166 "backward propagation: closer must also be kept.\nGot: {}",
1167 r.output
1168 );
1169 }
1170 if r.output.contains("logger.info(") {
1172 assert!(
1173 r.output.contains("item.id"),
1174 "forward propagation: args must be kept when opener is kept.\nGot: {}",
1175 r.output
1176 );
1177 }
1178 }
1179
1180 #[test]
1181 fn density_prefers_high_entropy_lines() {
1182 let content =
1183 "}\n}\n}\nlet complex_result = compute_unique_hash(seed, nonce, payload);\n}\n}";
1184 let r = entropy_compress_to_density(content, 0.6);
1185 assert!(
1186 r.output.contains("compute_unique_hash"),
1187 "high-entropy line must survive: {}",
1188 r.output
1189 );
1190 }
1191}