use aho_corasick::{AhoCorasick, AhoCorasickBuilder, MatchKind};
use crate::ir::{DocNode, FidelityLevel};
use crate::stream::estimate_tokens;
#[derive(Debug, Clone)]
pub struct CompressionConfig {
pub budget: usize,
pub current_tokens: usize,
pub fidelity: FidelityLevel,
}
impl CompressionConfig {
pub fn usage_ratio(&self) -> f64 {
if self.budget == 0 {
return 1.0;
}
self.current_tokens as f64 / self.budget as f64
}
pub fn stage(&self) -> CompressionStage {
match self.usage_ratio() {
r if r < 0.60 => CompressionStage::StopwordOnly,
r if r < 0.80 => CompressionStage::PruneLowImportance,
r if r < 0.95 => CompressionStage::DeduplicateAndLinearize,
_ => CompressionStage::MaxCompression,
}
}
pub fn min_stage(&self) -> CompressionStage {
match self.fidelity {
FidelityLevel::Compressed => CompressionStage::PruneLowImportance,
_ => CompressionStage::StopwordOnly,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum CompressionStage {
StopwordOnly,
PruneLowImportance,
DeduplicateAndLinearize,
MaxCompression,
}
pub struct AdaptiveCompressor {
ascii_ac: Option<AhoCorasick>,
nonascii_stopwords: Vec<String>,
}
impl Default for AdaptiveCompressor {
fn default() -> Self {
Self::new()
}
}
impl AdaptiveCompressor {
pub fn new() -> Self {
Self::with_stopwords(default_stopwords())
}
pub fn with_stopwords(stopwords: Vec<String>) -> Self {
let mut ascii_stopwords: Vec<String> = Vec::new();
let mut nonascii_stopwords = Vec::new();
for sw in &stopwords {
if estimate_tokens(sw) <= 1 {
continue;
}
if sw.is_ascii() {
ascii_stopwords.push(sw.to_ascii_lowercase());
} else {
nonascii_stopwords.push(sw.clone());
}
}
let ascii_ac = if ascii_stopwords.is_empty() {
None
} else {
AhoCorasickBuilder::new()
.ascii_case_insensitive(true)
.match_kind(MatchKind::LeftmostFirst)
.build(&ascii_stopwords)
.ok()
};
Self {
ascii_ac,
nonascii_stopwords,
}
}
pub fn has_stopwords(&self) -> bool {
self.ascii_ac.is_some() || !self.nonascii_stopwords.is_empty()
}
pub fn compress(&self, mut nodes: Vec<DocNode>, cfg: &CompressionConfig) -> Vec<DocNode> {
if cfg.fidelity == FidelityLevel::Lossless {
return nodes; }
let stage = cfg.stage().max(cfg.min_stage());
nodes = self.remove_stopwords(nodes);
if stage >= CompressionStage::PruneLowImportance {
nodes = prune_low_importance(nodes, 0.20);
}
if stage >= CompressionStage::DeduplicateAndLinearize {
nodes = deduplicate_paras(nodes);
}
if stage >= CompressionStage::MaxCompression {
nodes = truncate_to_first_sentence(nodes);
}
if cfg.fidelity == FidelityLevel::Compressed && stage >= CompressionStage::MaxCompression {
nodes = collapse_lists_to_inline(nodes);
nodes = summarize_code_blocks(nodes);
nodes = prune_low_importance(nodes, 0.20);
}
nodes
}
fn remove_stopwords(&self, nodes: Vec<DocNode>) -> Vec<DocNode> {
if !self.has_stopwords() {
return nodes;
}
nodes
.into_iter()
.map(|node| match node {
DocNode::Para { text, importance } => DocNode::Para {
text: self.strip_stopwords(&text),
importance,
},
DocNode::Header { level, text } => DocNode::Header {
level,
text: self.strip_stopwords(&text),
},
other => other,
})
.collect()
}
fn strip_stopwords(&self, text: &str) -> String {
let result: String = if let Some(ac) = &self.ascii_ac {
let bytes = text.as_bytes();
let mut out = String::with_capacity(text.len());
let mut last = 0usize;
for mat in ac.find_iter(text) {
let start = mat.start();
let end = mat.end();
let before_ok = start == 0 || !is_word_byte(bytes[start - 1]);
let after_ok = end == bytes.len() || !is_word_byte(bytes[end]);
if before_ok && after_ok {
out.push_str(&text[last..start]);
let skip_end = skip_trailing_space(bytes, end);
last = skip_end;
}
}
out.push_str(&text[last..]);
out
} else {
text.to_string()
};
let mut out2 = String::with_capacity(result.len());
if !self.nonascii_stopwords.is_empty() {
for token in result.split_whitespace().filter(|token| {
!self
.nonascii_stopwords
.iter()
.any(|sw| sw.as_str() == *token)
}) {
if !out2.is_empty() {
out2.push(' ');
}
out2.push_str(token);
}
} else {
for token in result.split_whitespace() {
if !out2.is_empty() {
out2.push(' ');
}
out2.push_str(token);
}
}
out2
}
}
#[inline]
fn is_word_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_'
}
#[inline]
fn skip_trailing_space(bytes: &[u8], mut pos: usize) -> usize {
while pos < bytes.len() && (bytes[pos] == b' ' || bytes[pos] == b'\t') {
pos += 1;
}
pos
}
const MAX_PRUNE_RATIO: f32 = 0.40;
fn prune_low_importance(nodes: Vec<DocNode>, threshold: f32) -> Vec<DocNode> {
let para_importances: Vec<f32> = nodes
.iter()
.filter_map(|n| {
if let DocNode::Para { importance, .. } = n {
Some(*importance)
} else {
None
}
})
.collect();
if para_importances.len() <= 1 {
return nodes;
}
let mut sorted = para_importances.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
let cutoff_idx = ((sorted.len() as f32 * threshold) as usize).min(sorted.len() - 1);
let cutoff = sorted[cutoff_idx];
if cutoff >= sorted[sorted.len() - 1] {
return nodes;
}
let total_paras = para_importances.len();
let max_removals = ((total_paras as f32 * MAX_PRUNE_RATIO) as usize).max(1);
let mut removed = 0usize;
let filtered: Vec<DocNode> = nodes
.iter()
.filter(|n| {
if let DocNode::Para { importance, .. } = n {
if *importance > cutoff {
true
} else if removed < max_removals {
removed += 1;
false
} else {
true
}
} else {
true
}
})
.cloned()
.collect();
let filtered_has_para = filtered.iter().any(|n| matches!(n, DocNode::Para { .. }));
let input_had_para = nodes.iter().any(|n| matches!(n, DocNode::Para { .. }));
if input_had_para && !filtered_has_para {
nodes
} else {
filtered
}
}
fn deduplicate_paras(nodes: Vec<DocNode>) -> Vec<DocNode> {
use std::collections::HashSet;
const SIMILARITY_THRESHOLD: f64 = 0.85;
let mut signatures: Vec<HashSet<String>> = Vec::new();
nodes
.into_iter()
.filter(|n| {
if let DocNode::Para { text, .. } = n {
let tokens: HashSet<String> =
text.split_whitespace().map(|t| t.to_lowercase()).collect();
if tokens.is_empty() {
return true;
}
for existing in &signatures {
let sim = jaccard_similarity(&tokens, existing);
if sim >= SIMILARITY_THRESHOLD {
return false;
}
}
signatures.push(tokens);
}
true
})
.collect()
}
fn jaccard_similarity(
a: &std::collections::HashSet<String>,
b: &std::collections::HashSet<String>,
) -> f64 {
if a.is_empty() && b.is_empty() {
return 1.0;
}
let intersection = a.intersection(b).count();
let union = a.union(b).count();
intersection as f64 / union as f64
}
fn collapse_lists_to_inline(nodes: Vec<DocNode>) -> Vec<DocNode> {
nodes
.into_iter()
.map(|node| match node {
DocNode::List { items, .. } if !items.is_empty() => {
let joined = items.join(", ");
DocNode::Para {
text: joined,
importance: 0.5, }
}
other => other,
})
.collect()
}
fn summarize_code_blocks(nodes: Vec<DocNode>) -> Vec<DocNode> {
nodes
.into_iter()
.map(|node| match node {
DocNode::Code { ref lang, ref body } => {
let line_count = body.lines().filter(|l| !l.trim().is_empty()).count();
if line_count == 0 {
return DocNode::Para {
text: String::new(),
importance: 0.0,
};
}
let lang_tag = lang.as_deref().unwrap_or("code");
DocNode::Para {
text: format!("[{lang_tag} {line_count}L]"),
importance: 0.3, }
}
other => other,
})
.collect()
}
fn truncate_to_first_sentence(nodes: Vec<DocNode>) -> Vec<DocNode> {
nodes
.into_iter()
.map(|node| match node {
DocNode::Para { text, importance } => {
let first = first_sentence(&text);
DocNode::Para {
text: first,
importance,
}
}
other => other,
})
.collect()
}
fn first_sentence(text: &str) -> String {
for (i, c) in text.char_indices() {
if c == '.' {
if is_abbreviation_or_decimal(text, i) {
continue;
}
return text[..i + c.len_utf8()].trim().to_string();
}
if matches!(
c,
'!' | '?'
| '。' | '!' | '?' | '।' | '॥' | '۔' | '።' | '᙮' | '꓿' | '︒' | '﹒' | '.' ) {
return text[..i + c.len_utf8()].trim().to_string();
}
}
text.trim().to_string() }
const ABBREVIATIONS: &[&str] = &[
"dr", "mr", "mrs", "ms", "prof", "sr", "jr", "vs", "etc", "eg", "ie", "fig", "eq", "no", "vol",
"us", "am", "pm", "gen", "sgt", "capt", "lt", "col", "inc", "ltd", "corp", "dept", "est",
"govt", "assn", "al", "approx", "appt", "apt", "ave", "blvd", "cot", "def", "div", "fx", "min",
"max", "misc", "mon", "tue", "wed", "thu", "fri", "sat", "sun", "jan", "feb", "mar", "apr",
"jun", "jul", "aug", "sep", "sept", "oct", "nov", "dec", "st", "nd", "rd", "th",
];
fn is_abbreviation_or_decimal(text: &str, pos: usize) -> bool {
let bytes = text.as_bytes();
let preceded_by_digit = pos > 0 && bytes[pos - 1].is_ascii_digit();
let followed_by_digit = pos + 1 < bytes.len() && bytes[pos + 1].is_ascii_digit();
if followed_by_digit {
return true;
}
if preceded_by_digit && !followed_by_digit {
return false;
}
let before = &text[..pos];
let word_start = before
.char_indices()
.rfind(|(_, c)| !c.is_alphanumeric() && *c != '.')
.map(|(i, c)| i + c.len_utf8())
.unwrap_or(0);
let word_before = &before[word_start..];
let after = &text[pos + 1..]; let mut extended = word_before.to_string();
let mut rest = after.chars().peekable();
while let Some(c) = rest.peek() {
if c.is_ascii_alphabetic() {
extended.push(*c);
rest.next();
if let Some('.') = rest.peek() {
extended.push('.');
rest.next();
} else {
extended.pop();
break;
}
} else {
break;
}
}
let cleaned: String = extended
.chars()
.filter(|c| c.is_alphabetic())
.collect::<String>()
.to_lowercase();
if cleaned.is_empty() {
return false;
}
if !ABBREVIATIONS.contains(&cleaned.as_str()) {
return false;
}
if cleaned == "us" && !extended.contains('.') {
return false;
}
true
}
fn default_stopwords() -> Vec<String> {
let articles = ["a", "an", "the"];
let conjunctions = ["and", "or", "but", "nor", "yet", "so", "for"];
let prepositions = [
"in", "on", "at", "to", "of", "by", "as", "up", "via", "into", "from", "with", "than",
"about", "over", "after", "before", "between", "through", "during", "within", "without",
];
let auxiliaries = [
"is", "are", "was", "were", "be", "been", "being", "have", "has", "had", "do", "does",
"did", "will", "would", "shall", "should", "may", "might", "must", "can", "could",
];
let pronouns = [
"it", "its", "this", "that", "these", "those", "not", "no", "also", "too", "very", "just",
"such",
];
let korean_connectives = [
"그리고",
"하지만",
"그러나",
"따라서",
"또한",
"즉",
"및",
"또는",
"그래서",
"그런데",
"게다가",
"다만",
"단지",
"특히",
"주로",
"왜냐하면",
"그러므로",
"한편",
"반면",
"이처럼",
"이렇게",
"이에",
"이후",
"이전",
];
articles
.iter()
.chain(conjunctions.iter())
.chain(prepositions.iter())
.chain(auxiliaries.iter())
.chain(pronouns.iter())
.map(|s| s.to_string())
.chain(korean_connectives.iter().map(|s| s.to_string()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn make_para(text: &str, importance: f32) -> DocNode {
DocNode::Para {
text: text.into(),
importance,
}
}
#[test]
fn lossless_skips_all_compression() {
let nodes = vec![make_para("the quick brown fox", 0.1)];
let cfg = CompressionConfig {
budget: 100,
current_tokens: 99,
fidelity: FidelityLevel::Lossless,
};
let compressor = AdaptiveCompressor::new();
let result = compressor.compress(nodes.clone(), &cfg);
if let (DocNode::Para { text: t1, .. }, DocNode::Para { text: t2, .. }) =
(&nodes[0], &result[0])
{
assert_eq!(t1, t2);
}
}
#[test]
fn new_compressor_has_stopwords() {
let compressor = AdaptiveCompressor::new();
assert!(
compressor.has_stopwords(),
"default compressor must have a non-empty stopword list"
);
}
#[test]
fn empty_compressor_has_no_stopwords() {
let compressor = AdaptiveCompressor::with_stopwords(vec![]);
assert!(
!compressor.has_stopwords(),
"compressor built with empty list must report no stopwords"
);
}
#[test]
fn stopword_removal_ascii_works() {
#[cfg(not(feature = "tiktoken"))]
{
let compressor = AdaptiveCompressor::new();
let nodes = vec![make_para("all about performance", 1.0)];
let cfg = CompressionConfig {
budget: 1000,
current_tokens: 100, fidelity: FidelityLevel::Semantic,
};
let result = compressor.compress(nodes, &cfg);
if let DocNode::Para { text, .. } = &result[0] {
assert!(
!text.to_lowercase().contains("about"),
"stopword 'about' (2-token word) must be removed: got '{}'",
text
);
}
}
}
#[cfg(feature = "tiktoken")]
#[test]
fn stopword_removal_multitoken_word_works() {
assert_eq!(crate::bpe_token_count("nevertheless"), 2);
assert_eq!(crate::bpe_token_count("about"), 1);
let compressor = AdaptiveCompressor::with_stopwords(vec!["nevertheless".into()]);
let nodes = vec![make_para("nevertheless it works", 1.0)];
let cfg = CompressionConfig {
budget: 1000,
current_tokens: 100,
fidelity: FidelityLevel::Semantic,
};
let result = compressor.compress(nodes, &cfg);
if let DocNode::Para { text, .. } = &result[0] {
assert!(
!text.to_lowercase().contains("nevertheless"),
"2-token stopword 'nevertheless' must be removed: got '{}'",
text
);
}
}
#[test]
fn single_token_stopword_not_removed() {
let compressor = AdaptiveCompressor::new();
let nodes = vec![make_para("the cat is in the house", 1.0)];
let cfg = CompressionConfig {
budget: 1000,
current_tokens: 100,
fidelity: FidelityLevel::Semantic,
};
let result = compressor.compress(nodes, &cfg);
if let DocNode::Para { text, .. } = &result[0] {
assert!(
text.to_lowercase().contains("the"),
"1-token stopword 'the' must NOT be removed by ROI filter: got '{}'",
text
);
}
}
#[test]
fn with_stopwords_removes_specified_ascii_words() {
#[cfg(not(feature = "tiktoken"))]
{
let compressor =
AdaptiveCompressor::with_stopwords(vec!["hello".into(), "world".into()]);
let nodes = vec![make_para("hello world foo", 1.0)];
let cfg = CompressionConfig {
budget: 1000,
current_tokens: 100,
fidelity: FidelityLevel::Semantic,
};
let result = compressor.compress(nodes, &cfg);
if let DocNode::Para { text, .. } = &result[0] {
assert!(
!text.to_lowercase().contains("hello"),
"'hello' must be removed: got '{}'",
text
);
assert!(
!text.to_lowercase().contains("world"),
"'world' must be removed: got '{}'",
text
);
assert!(text.contains("foo"), "'foo' must remain: got '{}'", text);
}
}
#[cfg(feature = "tiktoken")]
{
let compressor =
AdaptiveCompressor::with_stopwords(vec!["hello".into(), "world".into()]);
let nodes = vec![make_para("hello world foo", 1.0)];
let cfg = CompressionConfig {
budget: 1000,
current_tokens: 100,
fidelity: FidelityLevel::Semantic,
};
let result = compressor.compress(nodes, &cfg);
if let DocNode::Para { text, .. } = &result[0] {
assert!(
text.contains("hello"),
"1-token 'hello' is ROI-negative, kept: got '{text}'"
);
}
}
}
#[test]
fn nonascii_stopword_removal_works() {
let compressor = AdaptiveCompressor::new();
let nodes = vec![make_para("사과 그리고 바나나", 1.0)];
let cfg = CompressionConfig {
budget: 1000,
current_tokens: 100,
fidelity: FidelityLevel::Semantic,
};
let result = compressor.compress(nodes, &cfg);
if let DocNode::Para { text, .. } = &result[0] {
assert!(
!text.contains("그리고"),
"Korean connective '그리고' must be removed: got '{}'",
text
);
assert!(text.contains("사과"), "'사과' must remain: got '{}'", text);
assert!(
text.contains("바나나"),
"'바나나' must remain: got '{}'",
text
);
}
}
#[test]
fn nonascii_stopword_partial_match_not_removed() {
let compressor = AdaptiveCompressor::with_stopwords(vec!["그리고".into()]);
let nodes = vec![make_para("그리고나서 확인", 1.0)];
let cfg = CompressionConfig {
budget: 1000,
current_tokens: 100,
fidelity: FidelityLevel::Semantic,
};
let result = compressor.compress(nodes, &cfg);
if let DocNode::Para { text, .. } = &result[0] {
assert!(
text.contains("그리고나서"),
"'그리고나서' must NOT be removed (not an exact token): got '{}'",
text
);
}
}
#[test]
fn prune_low_importance_removes_bottom_20_pct() {
let nodes = vec![
make_para("중요 단락", 0.9),
make_para("보통 단락", 0.5),
make_para("낮은 단락", 0.1),
make_para("낮은 단락2", 0.05),
make_para("낮은 단락3", 0.02),
];
let result = prune_low_importance(nodes, 0.20);
assert!(result.len() < 5, "some nodes must be removed");
}
#[test]
fn deduplicate_removes_duplicates() {
let nodes = vec![
make_para("동일한 내용입니다.", 1.0),
make_para("다른 내용입니다.", 1.0),
make_para("동일한 내용입니다.", 0.9),
];
let result = deduplicate_paras(nodes);
assert_eq!(result.len(), 2, "one duplicate paragraph must be removed");
}
#[test]
fn first_sentence_extraction() {
assert_eq!(first_sentence("안녕하세요. 반갑습니다."), "안녕하세요.");
assert_eq!(
first_sentence("문장 부호 없는 텍스트"),
"문장 부호 없는 텍스트"
);
assert_eq!(first_sentence("Hello world! Bye."), "Hello world!");
}
#[test]
fn first_sentence_multilingual() {
assert_eq!(
first_sentence("यह पहला वाक्य है। यह दूसरा है।"),
"यह पहला वाक्य है।"
);
assert_eq!(
first_sentence("هذه الجملة الأولى۔ هذه الثانية۔"),
"هذه الجملة الأولى۔"
);
assert_eq!(
first_sentence("ይህ የመጀመሪያ ዓረፍተ ነገር ነው። ሁለተኛ።"),
"ይህ የመጀመሪያ ዓረፍተ ነገር ነው።"
);
assert_eq!(
first_sentence("これが最初の文です.これが二番目です."),
"これが最初の文です."
);
}
#[test]
fn prune_keeps_single_paragraph() {
let compressor = AdaptiveCompressor::with_stopwords(vec![]);
let nodes = vec![make_para("only paragraph", 0.1)]; let cfg = CompressionConfig {
budget: 100,
current_tokens: 65,
fidelity: FidelityLevel::Semantic,
};
let result = compressor.compress(nodes, &cfg);
assert_eq!(
result.len(),
1,
"the sole paragraph in a single-paragraph document must not be removed"
);
}
#[test]
fn prune_keeps_all_equal_importance_paragraphs() {
let compressor = AdaptiveCompressor::with_stopwords(vec![]);
let nodes = vec![
make_para("first", 0.5),
make_para("second", 0.5),
make_para("third", 0.5),
];
let cfg = CompressionConfig {
budget: 100,
current_tokens: 65,
fidelity: FidelityLevel::Semantic,
};
let result = compressor.compress(nodes, &cfg);
assert_eq!(
result.len(),
3,
"paragraphs with equal importance must not all be removed"
);
}
#[test]
fn ascii_stopword_respects_word_boundaries() {
let compressor = AdaptiveCompressor::with_stopwords(vec!["through".into()]);
let cfg = CompressionConfig {
budget: 1000,
current_tokens: 100,
fidelity: FidelityLevel::Semantic,
};
#[cfg(not(feature = "tiktoken"))]
{
let nodes = vec![make_para("pass through carefully", 1.0)];
let result = compressor.compress(nodes, &cfg);
if let DocNode::Para { text, .. } = &result[0] {
assert!(
!text.to_lowercase().contains(" through "),
"standalone 'through' must be removed: got '{}'",
text
);
assert!(
text.contains("pass") && text.contains("carefully"),
"non-stopword tokens must remain: got '{}'",
text
);
}
}
let nodes2 = vec![make_para("system throughput matters", 1.0)];
let result2 = compressor.compress(nodes2, &cfg);
if let DocNode::Para { text, .. } = &result2[0] {
assert!(
text.contains("throughput"),
"'throughput' must not be modified by stopword 'through': got '{}'",
text
);
}
}
#[test]
fn default_stopwords_no_duplicates() {
let stopwords = default_stopwords();
let mut seen = std::collections::HashSet::new();
let mut duplicates = Vec::new();
for sw in &stopwords {
if !seen.insert(sw.as_str()) {
duplicates.push(sw.clone());
}
}
assert!(
duplicates.is_empty(),
"duplicate stopwords found: {:?}",
duplicates
);
}
#[test]
fn first_sentence_skips_abbreviation_dr() {
assert_eq!(
first_sentence("Dr. Smith confirmed the results. The experiment was successful."),
"Dr. Smith confirmed the results."
);
}
#[test]
fn first_sentence_skips_abbreviation_us() {
assert_eq!(
first_sentence("U.S. patent no. 1234. Filed in 2024."),
"U.S. patent no. 1234."
);
}
#[test]
fn first_sentence_skips_abbreviation_eg() {
assert_eq!(
first_sentence("e.g. machine learning. Used widely."),
"e.g. machine learning."
);
}
#[test]
fn first_sentence_skips_abbreviation_ie() {
assert_eq!(
first_sentence("i.e. the model output. This is correct."),
"i.e. the model output."
);
}
#[test]
fn first_sentence_skips_abbreviation_fig() {
assert_eq!(
first_sentence("See Fig. 3 for details. The chart shows growth."),
"See Fig. 3 for details."
);
}
#[test]
fn first_sentence_preserves_normal_period() {
assert_eq!(
first_sentence("This is a normal sentence. And another one."),
"This is a normal sentence."
);
}
#[test]
fn first_sentence_handles_price() {
assert_eq!(
first_sentence("This costs $3.50 per unit. Total is $350."),
"This costs $3.50 per unit."
);
}
#[test]
fn first_sentence_us_pronoun_not_abbreviation() {
assert_eq!(first_sentence("Tell us. We will respond."), "Tell us.");
}
#[test]
fn stage_thresholds() {
let base = CompressionConfig {
budget: 100,
current_tokens: 0,
fidelity: FidelityLevel::Semantic,
};
let at = |tokens| CompressionConfig {
current_tokens: tokens,
..base.clone()
};
assert_eq!(at(50).stage(), CompressionStage::StopwordOnly);
assert_eq!(at(70).stage(), CompressionStage::PruneLowImportance);
assert_eq!(at(85).stage(), CompressionStage::DeduplicateAndLinearize);
assert_eq!(at(96).stage(), CompressionStage::MaxCompression);
}
#[test]
fn fuzzy_dedup_removes_near_duplicates() {
let nodes = vec![
make_para("The system processes requests", 1.0),
make_para("The system processes the requests", 0.9),
make_para("Completely different content here", 1.0),
];
let result = deduplicate_paras(nodes);
assert_eq!(
result.len(),
2,
"near-duplicate paragraphs should be deduped: {:?}",
result
.iter()
.filter_map(|n| if let DocNode::Para { text, .. } = n {
Some(text.clone())
} else {
None
})
.collect::<Vec<_>>()
);
}
#[test]
fn fuzzy_dedup_keeps_dissimilar() {
let nodes = vec![
make_para("The quick brown fox jumps", 1.0),
make_para("Completely different content here", 1.0),
];
let result = deduplicate_paras(nodes);
assert_eq!(result.len(), 2, "dissimilar paragraphs must be kept");
}
#[test]
fn prune_caps_at_40_percent() {
let mut nodes = vec![];
for _ in 0..8 {
nodes.push(make_para("medium importance", 0.5));
}
for _ in 0..2 {
nodes.push(make_para("high importance", 0.6));
}
let result = prune_low_importance(nodes, 0.20);
let remaining = result.len();
assert!(
remaining >= 6,
"at least 60% of paragraphs must survive the cap, got {remaining}/10"
);
}
#[test]
fn collapse_lists_to_inline_converts_list_to_para() {
let nodes = vec![DocNode::List {
ordered: false,
items: vec!["Alpha".into(), "Beta".into(), "Gamma".into()],
}];
let result = collapse_lists_to_inline(nodes);
assert_eq!(result.len(), 1);
if let DocNode::Para { text, .. } = &result[0] {
assert!(
text.contains("Alpha"),
"Alpha must appear in collapsed text"
);
assert!(text.contains("Beta"), "Beta must appear in collapsed text");
assert!(
text.contains("Gamma"),
"Gamma must appear in collapsed text"
);
} else {
panic!("expected Para node after list collapse");
}
}
#[test]
fn summarize_code_blocks_replaces_with_summary() {
let nodes = vec![DocNode::Code {
lang: Some("rust".into()),
body: "fn main() {}\nfn helper() {}\n".into(),
}];
let result = summarize_code_blocks(nodes);
assert_eq!(result.len(), 1);
if let DocNode::Para { text, .. } = &result[0] {
assert!(
text.contains("rust"),
"summary must mention language: got '{text}'"
);
assert!(
text.contains('L'),
"summary must mention line count: got '{text}'"
);
} else {
panic!("expected Para node after code summarization");
}
}
#[test]
fn compressed_mode_differs_from_semantic_at_max_stage() {
let compressor = AdaptiveCompressor::with_stopwords(vec![]);
let nodes = vec![
make_para("First important paragraph with content.", 0.9),
DocNode::List {
ordered: false,
items: vec!["item one".into(), "item two".into(), "item three".into()],
},
DocNode::Code {
lang: Some("python".into()),
body: "def foo():\n pass\ndef bar():\n return 1\n".into(),
},
];
let semantic_cfg = CompressionConfig {
budget: 100,
current_tokens: 98, fidelity: FidelityLevel::Semantic,
};
let compressed_cfg = CompressionConfig {
budget: 100,
current_tokens: 98,
fidelity: FidelityLevel::Compressed,
};
let semantic_result = compressor.compress(nodes.clone(), &semantic_cfg);
let compressed_result = compressor.compress(nodes, &compressed_cfg);
assert!(
compressed_result.len() <= semantic_result.len(),
"Compressed ({}) must produce ≤ nodes than Semantic ({})",
compressed_result.len(),
semantic_result.len()
);
let has_code_block = compressed_result
.iter()
.any(|n| matches!(n, DocNode::Code { .. }));
assert!(
!has_code_block,
"Compressed mode must replace Code nodes with summaries"
);
}
}