1use std::collections::{HashMap, HashSet};
2use std::hash::{Hash, Hasher};
3use std::io::Write;
4
5use flate2::write::GzEncoder;
6use flate2::Compression;
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 freq.values().fold(0.0_f64, |acc, &count| {
61 let p = count as f64 / total as f64;
62 acc - p * p.log2()
63 })
64}
65
66pub fn token_entropy(text: &str) -> f64 {
69 let tokens = encode_tokens(text);
70 token_entropy_from_ids(&tokens)
71}
72
73pub fn normalized_token_entropy_from_ids(tokens: &[u32]) -> f64 {
75 if tokens.is_empty() {
76 return 0.0;
77 }
78 let total = tokens.len();
79 let mut freq: HashMap<u32, usize> = HashMap::new();
80 for &t in tokens {
81 *freq.entry(t).or_default() += 1;
82 }
83 let n_unique = freq.len();
84 if n_unique <= 1 {
85 return 0.0;
86 }
87 let h = freq.values().fold(0.0_f64, |acc, &count| {
88 let p = count as f64 / total as f64;
89 acc - p * p.log2()
90 });
91 let h_max = (n_unique as f64).log2();
92 h / h_max
93}
94
95pub fn normalized_token_entropy(text: &str) -> f64 {
99 let tokens = encode_tokens(text);
100 normalized_token_entropy_from_ids(&tokens)
101}
102
103pub fn jaccard_similarity(a: &str, b: &str) -> f64 {
105 let set_a: HashSet<&str> = a.split_whitespace().collect();
106 let set_b: HashSet<&str> = b.split_whitespace().collect();
107
108 let intersection = set_a.intersection(&set_b).count();
109 let union = set_a.union(&set_b).count();
110
111 if union == 0 {
112 return 0.0;
113 }
114 intersection as f64 / union as f64
115}
116
117pub fn ngram_jaccard(a: &str, b: &str, n: usize) -> f64 {
119 let set_a = ngram_set(a, n);
120 let set_b = ngram_set(b, n);
121
122 let intersection = set_a.intersection(&set_b).count();
123 let union = set_a.union(&set_b).count();
124
125 if union == 0 {
126 return 0.0;
127 }
128 intersection as f64 / union as f64
129}
130
131fn ngram_set(text: &str, n: usize) -> HashSet<Vec<String>> {
132 let words: Vec<&str> = text.split_whitespace().collect();
133 if words.len() < n {
134 let mut set = HashSet::new();
135 if !words.is_empty() {
136 set.insert(words.iter().map(std::string::ToString::to_string).collect());
137 }
138 return set;
139 }
140 words
141 .windows(n)
142 .map(|w| w.iter().map(std::string::ToString::to_string).collect())
143 .collect()
144}
145
146pub fn minhash_signature(text: &str, n: usize, k: usize) -> Vec<u64> {
149 let ngrams = ngram_set(text, n);
150 if ngrams.is_empty() {
151 return vec![u64::MAX; k];
152 }
153 let mut signature = vec![u64::MAX; k];
154 for ngram in &ngrams {
155 for (i, min) in signature.iter_mut().enumerate() {
156 let h = hash_with_seed(ngram, i as u64);
157 if h < *min {
158 *min = h;
159 }
160 }
161 }
162 signature
163}
164
165pub fn minhash_similarity(sig_a: &[u64], sig_b: &[u64]) -> f64 {
167 if sig_a.len() != sig_b.len() || sig_a.is_empty() {
168 return 0.0;
169 }
170 let matches = sig_a
171 .iter()
172 .zip(sig_b.iter())
173 .filter(|(a, b)| a == b)
174 .count();
175 matches as f64 / sig_a.len() as f64
176}
177
178fn hash_with_seed<T: Hash>(value: &T, seed: u64) -> u64 {
179 let mut hasher = std::collections::hash_map::DefaultHasher::new();
180 seed.hash(&mut hasher);
181 value.hash(&mut hasher);
182 hasher.finish()
183}
184
185pub fn kolmogorov_proxy(content: &str) -> f64 {
188 if content.is_empty() {
189 return 1.0;
190 }
191 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
192 encoder.write_all(content.as_bytes()).ok();
193 let compressed = encoder.finish().unwrap_or_default();
194 compressed.len() as f64 / content.len() as f64
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub enum CompressibilityClass {
200 High,
201 Medium,
202 Low,
203}
204
205impl CompressibilityClass {
206 pub fn label(&self) -> &'static str {
208 match self {
209 Self::High => "high (K<0.3)",
210 Self::Medium => "medium (0.3≤K<0.6)",
211 Self::Low => "low (K≥0.6)",
212 }
213 }
214}
215
216pub fn compressibility_class(content: &str) -> CompressibilityClass {
218 let k = kolmogorov_proxy(content);
219 if k < 0.3 {
220 CompressibilityClass::High
221 } else if k < 0.6 {
222 CompressibilityClass::Medium
223 } else {
224 CompressibilityClass::Low
225 }
226}
227
228pub fn entropy_compress(content: &str) -> EntropyResult {
230 entropy_compress_with_thresholds(content, BPE_ENTROPY_THRESHOLD, 0.7)
231}
232
233pub fn entropy_compress_deterministic(content: &str) -> EntropyResult {
239 entropy_compress_inner(content, BPE_ENTROPY_THRESHOLD, 0.7, &[], false)
240}
241
242pub fn entropy_compress_adaptive(content: &str, path: &str) -> EntropyResult {
244 let thresholds = super::adaptive_thresholds::adaptive_thresholds(path, content);
245 let before_lines = content.lines().count() as u32;
246 let result =
247 entropy_compress_with_thresholds(content, thresholds.bpe_entropy, thresholds.jaccard);
248 let after_lines = result.output.lines().count() as u32;
249
250 if before_lines != after_lines {
251 super::events::emit(super::events::EventKind::Compression {
252 path: path.to_string(),
253 before_lines,
254 after_lines,
255 strategy: "entropy_adaptive".to_string(),
256 kept_line_count: after_lines,
257 removed_line_count: before_lines.saturating_sub(after_lines),
258 });
259 }
260
261 result
262}
263
264pub fn entropy_compress_task_conditioned(
270 content: &str,
271 path: &str,
272 task_keywords: &[String],
273) -> EntropyResult {
274 let thresholds = super::adaptive_thresholds::adaptive_thresholds(path, content);
275 let before_lines = content.lines().count() as u32;
276 let result = entropy_compress_with_task(
277 content,
278 thresholds.bpe_entropy,
279 thresholds.jaccard,
280 task_keywords,
281 );
282 let after_lines = result.output.lines().count() as u32;
283 if before_lines != after_lines {
284 super::events::emit(super::events::EventKind::Compression {
285 path: path.to_string(),
286 before_lines,
287 after_lines,
288 strategy: "entropy_task_conditioned".to_string(),
289 kept_line_count: after_lines,
290 removed_line_count: before_lines.saturating_sub(after_lines),
291 });
292 }
293 result
294}
295
296#[cfg(feature = "embeddings")]
302fn line_embedder(line_count: usize) -> impl Fn(&str) -> Option<Vec<f32>> {
303 use std::collections::HashMap;
304 use std::sync::Mutex;
305
306 const MAX_LINES_FOR_SEMANTIC: usize = 400;
307 const CACHE_CAP: usize = 8192;
308 static LINE_EMBED_CACHE: Mutex<Option<HashMap<u64, Vec<f32>>>> = Mutex::new(None);
309
310 let engine = if line_count <= MAX_LINES_FOR_SEMANTIC {
311 crate::tools::ctx_knowledge::embeddings::embedding_engine_nonblocking()
312 } else {
313 None
314 };
315
316 move |line: &str| {
317 let engine = engine?;
318 if line.len() < 8 {
319 return None;
320 }
321 let mut hasher = std::collections::hash_map::DefaultHasher::new();
322 std::hash::Hash::hash(&line, &mut hasher);
323 let key = std::hash::Hasher::finish(&hasher);
324
325 if let Ok(mut guard) = LINE_EMBED_CACHE.lock() {
326 if let Some(hit) = guard.get_or_insert_with(HashMap::new).get(&key) {
327 return Some(hit.clone());
328 }
329 }
330 let emb = engine.embed(line).ok()?;
331 if let Ok(mut guard) = LINE_EMBED_CACHE.lock() {
332 let map = guard.get_or_insert_with(HashMap::new);
333 if map.len() >= CACHE_CAP {
334 map.clear();
335 }
336 map.insert(key, emb.clone());
337 }
338 Some(emb)
339 }
340}
341
342#[cfg(not(feature = "embeddings"))]
343fn line_embedder(_line_count: usize) -> impl Fn(&str) -> Option<Vec<f32>> {
344 |_: &str| None
345}
346
347fn entropy_compress_with_task(
348 content: &str,
349 entropy_threshold: f64,
350 jaccard_threshold: f64,
351 task_keywords: &[String],
352) -> EntropyResult {
353 entropy_compress_inner(
354 content,
355 entropy_threshold,
356 jaccard_threshold,
357 task_keywords,
358 true,
359 )
360}
361
362fn entropy_compress_inner(
363 content: &str,
364 entropy_threshold: f64,
365 jaccard_threshold: f64,
366 task_keywords: &[String],
367 semantic: bool,
368) -> EntropyResult {
369 let original_tokens = count_tokens(content);
370 let mut lines: Vec<&str> = content.lines().collect();
371 let mut techniques = Vec::new();
372
373 let kw_lower: Vec<String> = task_keywords.iter().map(|k| k.to_lowercase()).collect();
374 let original_count = lines.len();
375 let mut task_rescued = 0usize;
376 let embed = line_embedder(if semantic { original_count } else { usize::MAX });
383 let mut scoring_ctx = super::surprise::ScoringCtx::new();
384 lines.retain(|line| {
385 let trimmed = line.trim();
386 if super::surprise::should_keep_line_semantic(
387 trimmed,
388 entropy_threshold,
389 &embed,
390 &mut scoring_ctx,
391 ) {
392 return true;
393 }
394 if !kw_lower.is_empty() {
396 let lower = trimmed.to_lowercase();
397 if kw_lower.iter().any(|kw| lower.contains(kw.as_str())) {
398 task_rescued += 1;
399 return true;
400 }
401 }
402 false
403 });
404 let removed = original_count - lines.len();
405 if removed > 0 || task_rescued > 0 {
406 let mut msg = format!("⊘ {removed} low-entropy lines (BPE H<{entropy_threshold:.2})");
407 if task_rescued > 0 {
408 msg.push_str(&format!(" [+{task_rescued} task-rescued]"));
409 }
410 techniques.push(msg);
411 }
412
413 let blocks = extract_blocks(&lines);
414 let groups = find_pattern_groups(&blocks, jaccard_threshold);
415 let mut dedup_count = 0;
416 for group in &groups {
417 if group.len() > 1 {
418 dedup_count += group.len() - 1;
419 }
420 }
421 if dedup_count > 0 {
422 techniques.push(format!("⊘ {dedup_count} duplicate patterns (J≥0.7)"));
423 }
424
425 let mut result: Vec<String> = Vec::new();
426 let mut skip_indices: std::collections::HashSet<usize> = std::collections::HashSet::new();
427 for group in &groups {
428 if group.len() > 1 {
429 for &idx in &group[1..] {
430 skip_indices.insert(idx);
431 }
432 }
433 }
434 for (i, line) in lines.iter().enumerate() {
435 if !skip_indices.contains(&i) {
436 result.push(line.to_string());
437 }
438 }
439
440 let mut collapsed = Vec::new();
441 let mut blank_count = 0;
442 for line in &result {
443 if line.trim().is_empty() {
444 blank_count += 1;
445 if blank_count <= 1 {
446 collapsed.push(line.clone());
447 }
448 } else {
449 blank_count = 0;
450 collapsed.push(line.clone());
451 }
452 }
453 let output = collapsed.join("\n");
454 let compressed_tokens = count_tokens(&output);
455
456 let final_output = if compressed_tokens > original_tokens {
460 content.to_string()
461 } else {
462 output
463 };
464 let final_tokens = if compressed_tokens > original_tokens {
465 original_tokens
466 } else {
467 compressed_tokens
468 };
469
470 EntropyResult {
471 output: final_output,
472 original_tokens,
473 compressed_tokens: final_tokens,
474 techniques,
475 }
476}
477
478fn entropy_compress_with_thresholds(
479 content: &str,
480 entropy_threshold: f64,
481 jaccard_threshold: f64,
482) -> EntropyResult {
483 entropy_compress_with_task(content, entropy_threshold, jaccard_threshold, &[])
484}
485
486pub fn entropy_compress_to_density(content: &str, target: f64) -> EntropyResult {
494 let target = target.clamp(0.05, 1.0);
495 let original_tokens = count_tokens(content);
496 if content.is_empty() || original_tokens == 0 {
497 return EntropyResult {
498 output: String::new(),
499 original_tokens,
500 compressed_tokens: 0,
501 techniques: vec![format!("density target={target:.2} (empty input)")],
502 };
503 }
504
505 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
506 let budget = ((original_tokens as f64) * target).ceil() as usize;
507
508 let lines: Vec<&str> = content.lines().collect();
509 let mut scored: Vec<(usize, f64, usize)> = lines
510 .iter()
511 .enumerate()
512 .map(|(i, l)| {
513 let trimmed = l.trim();
514 let toks = count_tokens(trimmed).max(1);
517 (i, token_entropy(trimmed), toks)
518 })
519 .collect();
520 scored.sort_by(|a, b| {
521 b.1.partial_cmp(&a.1)
522 .unwrap_or(std::cmp::Ordering::Equal)
523 .then(a.0.cmp(&b.0))
524 });
525
526 let mut keep = vec![false; lines.len()];
527 let mut used = 0usize;
528 let mut kept_count = 0usize;
529 if let Some(&(idx, _, toks)) = scored.first() {
533 keep[idx] = true;
534 used += toks;
535 kept_count += 1;
536 }
537 for &(idx, _h, toks) in scored.iter().skip(1) {
538 if used + toks > budget {
539 continue;
541 }
542 keep[idx] = true;
543 used += toks;
544 kept_count += 1;
545 }
546
547 let mut out_lines: Vec<&str> = Vec::with_capacity(kept_count);
548 for (i, line) in lines.iter().enumerate() {
549 if keep[i] {
550 out_lines.push(line);
551 }
552 }
553 let output = out_lines.join("\n");
554 let compressed_tokens = count_tokens(&output);
555
556 let dropped = lines.len() - kept_count;
557 EntropyResult {
558 output,
559 original_tokens,
560 compressed_tokens,
561 techniques: vec![format!(
562 "density target={target:.2} budget={budget} tok, kept {kept_count}/{} lines (⊘ {dropped})",
563 lines.len()
564 )],
565 }
566}
567
568#[derive(Debug)]
570pub struct EntropyAnalysis {
571 pub avg_entropy: f64,
572 pub low_entropy_count: usize,
573 pub high_entropy_count: usize,
574 pub total_lines: usize,
575}
576
577pub fn analyze_entropy(content: &str) -> EntropyAnalysis {
579 let lines: Vec<&str> = content.lines().collect();
580 let total = lines.len();
581 let mut sum = 0.0;
582 let mut low = 0;
583 let mut high = 0;
584 let mut counted = 0;
585
586 for line in &lines {
587 let trimmed = line.trim();
588 if trimmed.is_empty() {
589 continue;
590 }
591 let h = token_entropy(trimmed);
592 sum += h;
593 counted += 1;
594 if h < BPE_ENTROPY_THRESHOLD {
595 low += 1;
596 }
597 if h > 3.0 {
598 high += 1;
599 }
600 }
601
602 EntropyAnalysis {
603 avg_entropy: if counted > 0 {
604 sum / counted as f64
605 } else {
606 0.0
607 },
608 low_entropy_count: low,
609 high_entropy_count: high,
610 total_lines: total,
611 }
612}
613
614struct Block {
615 content: String,
616}
617
618fn extract_blocks(lines: &[&str]) -> Vec<Block> {
619 let mut blocks = Vec::new();
620 let mut current = String::new();
621
622 for line in lines {
623 let trimmed = line.trim();
624 if trimmed.is_empty() && !current.is_empty() {
625 blocks.push(Block {
626 content: current.clone(),
627 });
628 current.clear();
629 } else if !trimmed.is_empty() {
630 current.push_str(trimmed);
631 current.push('\n');
632 }
633 }
634
635 if !current.is_empty() {
636 blocks.push(Block { content: current });
637 }
638
639 blocks
640}
641
642fn find_pattern_groups(blocks: &[Block], threshold: f64) -> Vec<Vec<usize>> {
643 let sets: Vec<HashSet<Vec<String>>> = blocks.iter().map(|b| ngram_set(&b.content, 2)).collect();
647 let sizes: Vec<usize> = sets.iter().map(std::collections::HashSet::len).collect();
648
649 let mut groups: Vec<Vec<usize>> = Vec::new();
650 let mut assigned: HashSet<usize> = HashSet::new();
651
652 for i in 0..blocks.len() {
653 if assigned.contains(&i) {
654 continue;
655 }
656 let mut group = vec![i];
657 for j in (i + 1)..blocks.len() {
658 if assigned.contains(&j) {
659 continue;
660 }
661 let size_i = sizes[i];
662 let size_j = sizes[j];
663 let min_sz = size_i.min(size_j);
664 let max_sz = size_i.max(size_j);
665 if max_sz > 0 && (min_sz as f64) < (threshold * max_sz as f64) {
666 continue;
667 }
668 let inter = sets[i].intersection(&sets[j]).count();
669 let union = size_i + size_j - inter;
670 if union > 0 && (inter as f64 / union as f64) >= threshold {
671 group.push(j);
672 assigned.insert(j);
673 }
674 }
675 if group.len() > 1 {
676 assigned.insert(i);
677 }
678 groups.push(group);
679 }
680
681 groups
682}
683
684#[cfg(test)]
685mod tests {
686 use super::*;
687
688 #[test]
689 fn shannon_entropy_empty_is_zero() {
690 assert_eq!(shannon_entropy(""), 0.0);
691 }
692
693 #[test]
694 fn shannon_entropy_single_char() {
695 assert_eq!(shannon_entropy("aaaa"), 0.0);
696 }
697
698 #[test]
699 fn shannon_entropy_high_for_varied_text() {
700 let varied = "abcdefghijklmnopqrstuvwxyz0123456789";
701 let uniform = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
702 assert!(
703 shannon_entropy(varied) > shannon_entropy(uniform),
704 "varied text should have higher entropy"
705 );
706 }
707
708 #[test]
709 fn jaccard_identical_is_one() {
710 let sim = jaccard_similarity("hello world", "hello world");
711 assert!((sim - 1.0).abs() < f64::EPSILON);
712 }
713
714 #[test]
715 fn jaccard_disjoint_is_zero() {
716 let sim = jaccard_similarity("abc", "xyz");
717 assert_eq!(sim, 0.0);
718 }
719
720 #[test]
721 fn jaccard_partial_overlap() {
722 let sim = jaccard_similarity("hello world", "hello rust");
723 assert!(sim > 0.0 && sim < 1.0);
724 }
725
726 #[test]
727 fn entropy_compress_produces_output() {
728 let content = "fn main() {\n println!(\"hello\");\n}\n\n// comment\n// another comment\n\nfn helper() {\n let x = 42;\n}\n";
729 let result = entropy_compress(content);
730 assert!(!result.output.is_empty(), "should produce non-empty output");
731 assert!(result.compressed_tokens <= result.original_tokens);
732 }
733
734 #[test]
735 fn entropy_result_savings() {
736 let r = EntropyResult {
737 output: "short".to_string(),
738 original_tokens: 100,
739 compressed_tokens: 60,
740 techniques: vec!["test".to_string()],
741 };
742 assert!((r.savings_percent() - 40.0).abs() < 0.1);
743 }
744
745 #[test]
746 fn entropy_result_zero_original() {
747 let r = EntropyResult {
748 output: String::new(),
749 original_tokens: 0,
750 compressed_tokens: 0,
751 techniques: vec![],
752 };
753 assert_eq!(r.savings_percent(), 0.0);
754 }
755
756 #[test]
757 fn token_entropy_empty_is_zero() {
758 assert_eq!(token_entropy(""), 0.0);
759 }
760
761 #[test]
762 fn token_entropy_single_repeated_token() {
763 assert_eq!(token_entropy("}"), 0.0);
764 }
765
766 #[test]
767 fn token_entropy_higher_for_diverse_code() {
768 let diverse = "let result = compute_something(x, y, z);";
769 let repetitive = "aaaa aaaa aaaa aaaa";
770 assert!(
771 token_entropy(diverse) > token_entropy(repetitive),
772 "diverse code should have higher BPE token entropy"
773 );
774 }
775
776 #[test]
777 fn token_entropy_vs_char_entropy_differ() {
778 let code = "fn main() { println!(\"hello world\"); }";
779 let te = token_entropy(code);
780 let ce = shannon_entropy(code);
781 assert!(te != ce, "BPE and char entropy should differ for code");
782 }
783
784 #[test]
785 fn ngram_jaccard_preserves_order() {
786 let a = "a b c d";
787 let b = "d c b a";
788 let word_j = jaccard_similarity(a, b);
789 let ngram_j = ngram_jaccard(a, b, 2);
790 assert!(
791 ngram_j < word_j,
792 "reordered text should have lower bigram Jaccard ({ngram_j}) than word Jaccard ({word_j})"
793 );
794 }
795
796 #[test]
797 fn ngram_jaccard_identical_is_one() {
798 let text = "fn main() { println!(\"hello\"); }";
799 let j = ngram_jaccard(text, text, 2);
800 assert!((j - 1.0).abs() < f64::EPSILON);
801 }
802
803 #[test]
804 fn ngram_jaccard_disjoint_is_zero() {
805 let j = ngram_jaccard("alpha beta gamma", "delta epsilon zeta", 2);
806 assert_eq!(j, 0.0);
807 }
808
809 #[test]
810 fn minhash_approximates_jaccard() {
811 let a = "fn main() { let x = 1; let y = 2; let z = x + y; println!(z); }";
812 let b = "fn main() { let x = 1; let y = 2; let z = x + y; return z; }";
813 let exact = ngram_jaccard(a, b, 2);
814 let sig_a = minhash_signature(a, 2, 128);
815 let sig_b = minhash_signature(b, 2, 128);
816 let approx = minhash_similarity(&sig_a, &sig_b);
817 assert!(
818 (exact - approx).abs() < 0.2,
819 "minhash ({approx}) should approximate exact ({exact}) within 0.2"
820 );
821 }
822
823 #[test]
824 fn minhash_empty_text() {
825 let sig = minhash_signature("", 2, 64);
826 assert!(sig.iter().all(|&v| v == u64::MAX));
827 }
828
829 #[test]
830 fn kolmogorov_empty_is_one() {
831 assert_eq!(kolmogorov_proxy(""), 1.0);
832 }
833
834 #[test]
835 fn kolmogorov_repetitive_is_low() {
836 let repetitive = "aaa\n".repeat(1000);
837 let k = kolmogorov_proxy(&repetitive);
838 assert!(
839 k < 0.1,
840 "highly repetitive text should compress well: K={k}"
841 );
842 }
843
844 #[test]
845 fn kolmogorov_diverse_is_higher() {
846 let repetitive = "aaa\n".repeat(500);
847 let diverse = (0..500)
848 .map(|i| format!("line_{i}_unique_content_{}", i * 17 % 97))
849 .collect::<Vec<_>>()
850 .join("\n");
851 assert!(
852 kolmogorov_proxy(&diverse) > kolmogorov_proxy(&repetitive),
853 "diverse content should have higher K than repetitive"
854 );
855 }
856
857 #[test]
858 fn compressibility_class_repetitive_is_high() {
859 let text = "use std::io;\n".repeat(200);
860 assert_eq!(compressibility_class(&text), CompressibilityClass::High);
861 }
862
863 #[test]
864 fn kolmogorov_diverse_higher_than_repetitive() {
865 let rep = "test\n".repeat(500);
866 let diverse = (0..500)
867 .map(|i| format!("unique_line_{i}_xk{}", i * 31 % 1000))
868 .collect::<Vec<_>>()
869 .join("\n");
870 assert!(
871 kolmogorov_proxy(&diverse) > kolmogorov_proxy(&rep),
872 "diverse content should have higher K"
873 );
874 }
875
876 fn density_fixture() -> String {
877 (0..120)
878 .map(|i| {
879 if i % 3 == 0 {
880 format!(
881 "fn compute_value_{i}(input: &str, flags: u32) -> Result<Output, Error> {{"
882 )
883 } else if i % 3 == 1 {
884 format!(" let intermediate_{i} = transform(input, flags ^ {i});")
885 } else {
886 "}".to_string()
887 }
888 })
889 .collect::<Vec<_>>()
890 .join("\n")
891 }
892
893 #[test]
894 fn density_respects_token_budget() {
895 let content = density_fixture();
896 let orig = count_tokens(&content);
897 for target in [0.3, 0.5, 0.7] {
898 let r = entropy_compress_to_density(&content, target);
899 let actual = r.compressed_tokens as f64 / orig as f64;
900 assert!(
901 actual <= target + 0.10,
902 "target {target}: actual density {actual:.2} exceeds budget"
903 );
904 assert!(!r.output.is_empty());
905 }
906 }
907
908 #[test]
909 fn density_is_deterministic() {
910 let content = density_fixture();
911 let a = entropy_compress_to_density(&content, 0.4);
912 let b = entropy_compress_to_density(&content, 0.4);
913 assert_eq!(a.output, b.output);
914 assert_eq!(a.compressed_tokens, b.compressed_tokens);
915 }
916
917 #[test]
918 fn density_target_one_keeps_everything() {
919 let content = density_fixture();
920 let r = entropy_compress_to_density(&content, 1.0);
921 assert_eq!(r.output, content);
922 }
923
924 #[test]
925 fn density_clamps_out_of_range_target() {
926 let content = density_fixture();
927 let low = entropy_compress_to_density(&content, 0.0);
928 assert!(!low.output.is_empty(), "clamped to 0.05, never empty");
929 let high = entropy_compress_to_density(&content, 5.0);
930 assert_eq!(high.output, content, "clamped to 1.0 keeps all");
931 }
932
933 #[test]
934 fn density_empty_input() {
935 let r = entropy_compress_to_density("", 0.5);
936 assert_eq!(r.compressed_tokens, 0);
937 assert!(r.output.is_empty());
938 }
939
940 #[test]
941 fn density_prefers_high_entropy_lines() {
942 let content =
943 "}\n}\n}\nlet complex_result = compute_unique_hash(seed, nonce, payload);\n}\n}";
944 let r = entropy_compress_to_density(content, 0.6);
945 assert!(
946 r.output.contains("compute_unique_hash"),
947 "high-entropy line must survive: {}",
948 r.output
949 );
950 }
951}