use std::collections::HashMap;
use gaoya::minhash::{MinHasher, MinHasher32};
use once_cell::sync::Lazy;
use regex::Regex;
const MIN_RUN: usize = 3;
const MIN_GLOBAL_REPEAT: usize = 3;
const MAX_BUCKETS: usize = 512;
const MINHASH_NUM_HASHES: usize = 64;
const MINHASH_SEED: u64 = 1;
const JACCARD_THRESHOLD: f64 = 0.9;
const VOTE_SAMPLE: usize = 10;
const VOTE_SHARE: f64 = 0.5;
static VARIABLE: Lazy<Regex> = Lazy::new(|| {
Regex::new(concat!(
r#""[^"]*""#, r#"|\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?"#, r"|\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b", r"|\b0x[0-9a-fA-F]+\b", r"|\b[0-9a-fA-F]{12,}\b", r"|\b\d{1,3}(?:\.\d{1,3}){3}\b", r"|\d+(?:\.\d+)?", ))
.unwrap()
});
pub fn collapse(text: &str) -> (String, bool) {
let lines: Vec<&str> = text.lines().collect();
if lines.len() < MIN_RUN {
return (text.to_string(), false);
}
let mut out: Vec<String> = Vec::with_capacity(lines.len());
let mut folded = false;
let mut i = 0;
while i < lines.len() {
let tpl = template_of(lines[i]).0;
let mut j = i + 1;
while j < lines.len() && template_of(lines[j]).0 == tpl {
j += 1;
}
let run = &lines[i..j];
if run.len() >= MIN_RUN && tpl.contains("{}") {
let collapsed = render_run(&tpl, run);
let original_len: usize = run.iter().map(|l| l.len() + 1).sum();
if collapsed.len() < original_len {
out.push(collapsed);
folded = true;
i = j;
continue;
}
}
out.extend(run.iter().map(|l| (*l).to_string()));
i = j;
}
(out.join("\n"), folded)
}
fn template_of(line: &str) -> (String, Vec<String>) {
let mut tpl = String::with_capacity(line.len());
let mut vars = Vec::new();
let mut last = 0;
for m in VARIABLE.find_iter(line) {
push_collapsed(&mut tpl, &line[last..m.start()]);
tpl.push_str("{}");
vars.push(m.as_str().to_string());
last = m.end();
}
push_collapsed(&mut tpl, &line[last..]);
(tpl, vars)
}
fn push_collapsed(out: &mut String, s: &str) {
let mut prev_ws = out.ends_with(' ');
for c in s.chars() {
if c.is_whitespace() {
if !prev_ws {
out.push(' ');
prev_ws = true;
}
} else {
out.push(c);
prev_ws = false;
}
}
}
fn render_run(tpl: &str, run: &[&str]) -> String {
let tuples: Vec<String> = run
.iter()
.map(|l| format!("({})", template_of(l).1.join(",")))
.collect();
format!("{tpl} [×{}: {}]", run.len(), tuples.join(" "))
}
pub fn collapse_global(text: &str) -> (String, bool) {
let (consecutive, folded_consec) = collapse(text);
let lines: Vec<&str> = consecutive.lines().collect();
if lines.len() < MIN_GLOBAL_REPEAT {
return (consecutive, folded_consec);
}
let mut buckets: HashMap<BucketKey, Vec<usize>> = HashMap::new();
let mut order: Vec<BucketKey> = Vec::new(); for (i, line) in lines.iter().enumerate() {
if line.trim().is_empty() || line.contains("[×") {
continue;
}
let key = bucket_key(line);
let entry = buckets.entry(key.clone()).or_insert_with(|| {
order.push(key.clone());
Vec::new()
});
entry.push(i);
}
let work: Vec<BucketKey> = order.into_iter().take(MAX_BUCKETS).collect();
let hasher = MinHasher32::new_with_hasher_and_seed(
MINHASH_NUM_HASHES,
gaoya::minhash::SipHasher24BuildHasher::default(),
MINHASH_SEED,
);
let sigs: Vec<Vec<u32>> = work
.iter()
.map(|k| {
let rep = lines[buckets[k][0]];
hasher.create_signature(masked_tokens(rep).into_iter())
})
.collect();
let groups = merge_buckets(&work, &sigs);
let mut fold_at: HashMap<usize, String> = HashMap::new(); let mut drop_line = vec![false; lines.len()];
let mut folded_global = false;
for group in &groups {
let mut members: Vec<usize> = group
.iter()
.flat_map(|k| buckets[k].iter().copied())
.collect();
members.sort_unstable();
if members.len() < MIN_GLOBAL_REPEAT {
continue;
}
let member_lines: Vec<&str> = members.iter().map(|&i| lines[i]).collect();
let tpl = vote_template(&member_lines);
if !tpl.contains("{}") {
continue;
}
if !has_value_shaped_slot(&tpl, &member_lines) {
continue;
}
let rendered = render_voted(&tpl, &member_lines);
let original_len: usize = member_lines.iter().map(|l| l.len() + 1).sum();
if rendered.len() >= original_len {
continue;
}
let first = members[0];
for &i in &members[1..] {
drop_line[i] = true;
}
fold_at.insert(first, rendered);
folded_global = true;
}
if !folded_global {
return (consecutive, folded_consec);
}
let mut out: Vec<String> = Vec::with_capacity(lines.len());
for (i, line) in lines.iter().enumerate() {
if let Some(rep) = fold_at.remove(&i) {
out.push(rep);
} else if !drop_line[i] {
out.push((*line).to_string());
}
}
(out.join("\n"), true)
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
struct BucketKey {
token_count: usize,
anchors: [Option<char>; 5],
}
fn bucket_key(line: &str) -> BucketKey {
let chars: Vec<char> = line.chars().collect();
let n = chars.len();
let at = |num: usize, den: usize| -> Option<char> {
if n == 0 {
return None;
}
let idx = num * (n - 1) / den;
chars.get(idx).copied()
};
BucketKey {
token_count: tokens(line).len(),
anchors: [at(0, 4), at(1, 4), at(2, 4), at(3, 4), at(4, 4)],
}
}
fn tokens(line: &str) -> Vec<&str> {
line.split_whitespace().collect()
}
fn masked_tokens(line: &str) -> Vec<String> {
template_of(line)
.0
.split_whitespace()
.map(str::to_string)
.collect()
}
fn merge_buckets(work: &[BucketKey], sigs: &[Vec<u32>]) -> Vec<Vec<BucketKey>> {
let n = work.len();
let mut parent: Vec<usize> = (0..n).collect();
fn find(parent: &mut [usize], mut x: usize) -> usize {
while parent[x] != x {
parent[x] = parent[parent[x]]; x = parent[x];
}
x
}
for i in 0..n {
for j in (i + 1)..n {
if gaoya::minhash::compute_minhash_similarity(&sigs[i], &sigs[j]) >= JACCARD_THRESHOLD {
let (ri, rj) = (find(&mut parent, i), find(&mut parent, j));
if ri != rj {
parent[ri.max(rj)] = ri.min(rj); }
}
}
}
let mut by_root: HashMap<usize, Vec<BucketKey>> = HashMap::new();
let mut root_order: Vec<usize> = Vec::new();
for (i, key) in work.iter().enumerate() {
let r = find(&mut parent, i);
by_root
.entry(r)
.or_insert_with(|| {
root_order.push(r);
Vec::new()
})
.push(key.clone());
}
root_order
.into_iter()
.map(|r| by_root.remove(&r).unwrap_or_default())
.collect()
}
fn vote_template(members: &[&str]) -> String {
let toks: Vec<Vec<&str>> = members
.iter()
.take(VOTE_SAMPLE)
.map(|l| tokens(l))
.collect();
let width = toks.iter().map(Vec::len).max().unwrap_or(0);
let voters = toks.len();
let need = ((voters as f64) * VOTE_SHARE).ceil().max(1.0) as usize;
let mut out = String::new();
let mut prev_var = false;
for pos in 0..width {
let mut counts: HashMap<&str, usize> = HashMap::new();
for row in &toks {
if let Some(t) = row.get(pos) {
*counts.entry(t).or_insert(0) += 1;
}
}
let constant = dominant_token(&counts, need);
match constant {
Some(t) => {
if !out.is_empty() {
out.push(' ');
}
out.push_str(t);
prev_var = false;
}
None => {
if !prev_var {
if !out.is_empty() {
out.push(' ');
}
out.push_str("{}");
prev_var = true;
}
}
}
}
out
}
fn dominant_token<'a>(counts: &HashMap<&'a str, usize>, need: usize) -> Option<&'a str> {
let max = counts.values().copied().max().unwrap_or(0);
if max < need {
return None;
}
let mut at_max = counts.iter().filter(|&(_, &c)| c == max);
let (&winner, _) = at_max.next()?;
if at_max.next().is_some() {
return None;
}
Some(winner)
}
fn render_voted(tpl: &str, members: &[&str]) -> String {
let tuples: Vec<String> = members
.iter()
.map(|l| format!("({})", variable_values(tpl, l).join(",")))
.collect();
format!("{tpl} [×{}: {}]", members.len(), tuples.join(" "))
}
fn variable_values(tpl: &str, line: &str) -> Vec<String> {
let tpl_toks: Vec<&str> = tpl.split(' ').filter(|t| !t.is_empty()).collect();
let line_toks: Vec<&str> = tokens(line);
let mut vals: Vec<String> = Vec::new();
let mut li = 0;
for (ti, &tt) in tpl_toks.iter().enumerate() {
if tt == "{}" {
let next_const = tpl_toks[ti + 1..].iter().find(|&&t| t != "{}").copied();
let mut slot: Vec<&str> = Vec::new();
while li < line_toks.len() && Some(line_toks[li]) != next_const {
slot.push(line_toks[li]);
li += 1;
}
vals.push(slot.join(" "));
} else {
if li < line_toks.len() && line_toks[li] == tt {
li += 1;
}
}
}
vals
}
fn has_value_shaped_slot(tpl: &str, members: &[&str]) -> bool {
let per_member: Vec<Vec<String>> = members.iter().map(|l| variable_values(tpl, l)).collect();
let slots = per_member.iter().map(Vec::len).max().unwrap_or(0);
(0..slots).any(|s| {
let (mut shaped, mut total) = (0usize, 0usize);
for vals in &per_member {
if let Some(v) = vals.get(s) {
total += 1;
if value_shaped(v) {
shaped += 1;
}
}
}
total > 0 && shaped * 2 >= total })
}
fn value_shaped(v: &str) -> bool {
if v.is_empty() {
return false;
}
v.chars().any(|c| c.is_ascii_digit()) || VARIABLE.is_match(v)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn templates_mask_variables() {
let (tpl, vars) = template_of("Connection to db-01 timed out after 30ms");
assert_eq!(tpl, "Connection to db-{} timed out after {}ms");
assert_eq!(vars, vec!["01", "30"]);
}
#[test]
fn timestamp_and_uuid_are_single_tokens() {
let (tpl, vars) =
template_of("2023-01-02T10:00:00Z req 1f2e3d4c-5b6a-7980-1234-567890abcdef done");
assert_eq!(tpl, "{} req {} done");
assert_eq!(vars.len(), 2, "timestamp and UUID each mask as one slot");
}
#[test]
fn collapses_a_run_losslessly_and_shorter() {
let text = "Connection to db-01 timed out after 30ms\n\
Connection to db-02 timed out after 12ms\n\
Connection to db-07 timed out after 5ms";
let (out, folded) = collapse(text);
assert!(folded, "a real run folded");
assert_eq!(
out.lines().count(),
1,
"three same-template lines fold to one"
);
assert!(out.starts_with("Connection to db-{} timed out after {}ms [×3:"));
for tuple in ["(01,30)", "(02,12)", "(07,5)"] {
assert!(out.contains(tuple), "missing {tuple} in {out}");
}
assert!(out.len() < text.len(), "collapse must shrink the run");
}
#[test]
fn leaves_short_runs_untouched() {
let text = "host a failed 1 time\nhost b failed 2 times";
let (out, folded) = collapse(text);
assert_eq!(out, text, "a 2-line run is below MIN_RUN");
assert!(!folded, "nothing folded");
}
#[test]
fn aligned_columns_fold_despite_padding() {
let text = "drwxr-xr-x 2 u g 0 Apr 01 file_0.log\n\
drwxr-xr-x 2 u g 1024 Apr 02 file_1.log\n\
drwxr-xr-x 2 u g 524288 Apr 03 file_2.log";
let (out, _) = collapse(text);
assert_eq!(
out.lines().count(),
1,
"aligned rows fold to one despite padding: {out}"
);
assert!(out.contains("[×3:"), "one run of 3");
assert!(
out.contains("524288") && out.contains("1024"),
"every value preserved"
);
}
#[test]
fn does_not_collapse_slot_free_identical_lines() {
let text = "starting up\nstarting up\nstarting up";
let (out, folded) = collapse(text);
assert_eq!(out, text);
assert!(!folded, "slot-free identical lines do not fold");
}
#[test]
fn only_consecutive_runs_collapse() {
let text = "value is 1\ndifferent entirely here\nvalue is 2";
let (out, folded) = collapse(text);
assert_eq!(out, text);
assert!(!folded, "non-consecutive runs below MIN_RUN do not fold");
}
#[test]
fn trailing_newline_with_no_fold_reports_not_folded() {
let text = "alpha beta gamma\ndelta epsilon zeta\neta theta iota\n";
let (out, folded) = collapse(text);
assert!(
!folded,
"nothing folded → false despite the stripped trailing newline"
);
assert_ne!(
out, text,
"join() drops the trailing newline (would fool a string compare)"
);
}
fn count_tokens(s: &str) -> usize {
s.split_whitespace().count()
}
fn interleaved_build(pairs: usize) -> String {
let mut lines = Vec::with_capacity(pairs * 2);
for i in 0..pairs {
lines.push(format!(
"[build] compiled object file number {i} successfully, no warnings"
));
lines.push(format!(
"[test] executed checks for shard {i} of pool, all green ok"
));
}
lines.join("\n")
}
#[test]
fn global_folds_interleaved_where_consecutive_cannot() {
let text = interleaved_build(12);
let (consec, folded_consec) = collapse(&text);
assert!(
!folded_consec,
"consecutive pass can't fold interleaved lines"
);
assert_eq!(
consec.lines().count(),
24,
"consecutive leaves all 24 lines"
);
let (out, folded) = collapse_global(&text);
assert!(folded, "global pass folds the interleaved templates");
assert_eq!(
out.lines().count(),
2,
"two distinct templates fold to two representatives: {out}"
);
assert_eq!(
out.matches("[×12:").count(),
2,
"each of the two templates folds 12 occurrences: {out}"
);
let saved = 100.0 - (count_tokens(&out) as f64 / count_tokens(&text) as f64 * 100.0);
assert!(
saved >= 60.0,
"expected ≥60% token savings, got {saved:.1}%"
);
}
#[test]
fn global_fold_is_information_preserving() {
let text = interleaved_build(4);
let (out, _) = collapse_global(&text);
for id in ["0", "1", "2", "3"] {
let hits = out.matches(id).count();
assert!(
hits >= 2,
"value {id} preserved for both templates (got {hits}): {out}"
);
}
assert!(
out.contains("compiled object file"),
"template 1 kept: {out}"
);
assert!(
out.contains("executed checks for shard"),
"template 2 kept: {out}"
);
}
#[test]
fn global_collapse_is_deterministic() {
let mut lines = Vec::new();
for i in 0..15 {
lines.push(format!("worker A processed job {i} in {i}ms"));
lines.push(format!("worker B fetched record {i} size {i}kb"));
lines.push(format!("worker C indexed shard {i} ok"));
}
let text = lines.join("\n");
let (a, fa) = collapse_global(&text);
let (b, fb) = collapse_global(&text);
assert_eq!(a, b, "same input twice ⇒ identical output");
assert_eq!(fa, fb, "fold flag stable too");
assert!(fa, "and it actually folded");
}
#[test]
fn error_codes_survive_global_collapse() {
let mut lines = Vec::new();
for (i, code) in ["E0308", "E0277", "E0425", "E0599"].iter().enumerate() {
lines.push(format!("error[{code}]: mismatch in module_{i}"));
lines.push(format!("note: build step {i} continued"));
}
let text = lines.join("\n");
let (out, folded) = collapse_global(&text);
assert!(folded, "the interleaved error/note templates fold");
for code in ["E0308", "E0277", "E0425", "E0599"] {
assert!(
out.contains(code),
"{code} must survive (not dropped): {out}"
);
}
}
#[test]
fn error_code_constant_stays_in_template() {
let mut lines = Vec::new();
for i in 0..6 {
lines.push(format!("error[E0308]: mismatched types at site {i}"));
lines.push(format!("info: checked candidate {i}"));
}
let text = lines.join("\n");
let (out, folded) = collapse_global(&text);
assert!(folded);
assert!(
out.contains("E0308"),
"constant error code kept in template: {out}"
);
}
#[test]
fn global_collapse_handles_cjk_lines() {
let mut lines = Vec::new();
for i in 0..10 {
lines.push(format!("处理 任务 {i} 完成 用时 {i} 毫秒"));
lines.push(format!("读取 文件 {i} 大小 {i} 字节"));
}
let text = lines.join("\n");
let (out, folded) = collapse_global(&text);
assert!(folded, "CJK templates fold: {out}");
assert!(
out.contains('处'),
"CJK content preserved in the template: {out}"
);
assert!(out.lines().count() < text.lines().count());
}
#[test]
fn global_collapse_handles_very_long_lines() {
let blob = "x".repeat(4000);
let mut lines = Vec::new();
for i in 0..5 {
lines.push(format!("record {i} payload {blob} tail {i}"));
lines.push(format!("checksum {i} verified ok ({i} bytes)"));
}
let text = lines.join("\n");
let (out, folded) = collapse_global(&text);
assert!(folded, "long interleaved lines fold without panicking");
assert!(out.lines().count() < text.lines().count());
}
#[test]
fn global_collapse_leaves_single_line_segment() {
let (out, folded) = collapse_global("just one line here 42");
assert_eq!(out, "just one line here 42");
assert!(!folded);
}
#[test]
fn global_declines_diverse_prose() {
let prose = "The committee reviewed the annual budget on Tuesday morning.\n\
A sudden storm delayed the harvest across three northern counties.\n\
Engineers traced the outage to a misconfigured load balancer.\n\
The museum unveiled a restored fresco after two years of work.";
let (out, folded) = collapse_global(prose);
assert!(!folded, "diverse prose must not fold: {out}");
}
#[test]
fn global_declines_word_only_sentence_frame() {
const W: &[&str] = &[
"alpha", "bravo", "cobalt", "dune", "ember", "flint", "granite", "harbor",
];
let prose: String = (0..8)
.map(|i| {
format!(
"The {} review of {} examined {} thoroughly today.",
W[i % W.len()],
W[(i + 3) % W.len()],
W[(i + 5) % W.len()]
)
})
.collect::<Vec<_>>()
.join("\n");
let (out, folded) = collapse_global(&prose);
assert!(!folded, "word-only sentence frame must not fold: {out}");
}
#[test]
fn global_folds_value_bearing_frame() {
let logs: String = (0..8)
.map(|i| format!("The job for shard {i} completed in {i} seconds flat."))
.collect::<Vec<_>>()
.join("\n");
let (out, folded) = collapse_global(&logs);
assert!(folded, "a value-bearing frame folds: {out}");
assert!(out.contains("[×8:"), "all eight occurrences fold: {out}");
}
#[test]
fn global_preserves_consecutive_fold_then_adds_global() {
let mut lines = Vec::new();
for i in 0..3 {
lines.push(format!("loading shard shard-{i} ready"));
}
for i in 0..6 {
lines.push(format!("alpha worker {i} done"));
lines.push(format!("beta worker {i} done"));
}
let text = lines.join("\n");
let (out, folded) = collapse_global(&text);
assert!(folded);
assert!(out.contains("[×3:"), "consecutive run folded: {out}");
assert!(out.contains("[×6:"), "interleaved set folded: {out}");
assert_eq!(out.matches("[×3:").count(), 1, "no double-fold: {out}");
}
#[test]
fn vote_template_marks_disagreeing_positions_variable() {
let members = ["GET /api/v1/users 200 fast", "GET /api/v1/posts 200 fast"];
assert_eq!(vote_template(&members), "GET {} 200 fast");
let members2 = ["job alpha one done", "job beta two done"];
assert_eq!(vote_template(&members2), "job {} done");
let members3 = ["x A z", "x A z", "x B z"]; assert_eq!(vote_template(&members3), "x A z");
let members4 = ["x A z", "x B z"]; assert_eq!(vote_template(&members4), "x {} z");
}
#[test]
fn bucket_key_is_char_boundary_safe() {
let line = "café münster 北京 处理 42";
let k1 = bucket_key(line);
let k2 = bucket_key(line);
assert_eq!(k1, k2, "identical multibyte line ⇒ identical key");
assert_eq!(k1.token_count, 5, "token count counts whitespace tokens");
let _ = bucket_key("");
let _ = bucket_key("北");
}
}
#[cfg(test)]
mod global_grep_records {
use super::*;
#[test]
fn interleaved_grep_records_fold_globally() {
let mut lines = Vec::new();
for i in 0..70 {
lines.push(format!(
"src/parser/config.rs:{}: let v = parse_config(input, {});",
i + 10,
i
));
if i % 2 == 1 {
lines.push("--".to_string());
}
}
let text = lines.join("\n");
let (out, folded) = collapse_global(&text);
assert!(folded, "global pass must fold the 70-member group");
assert!(
out.lines().count() < 40,
"70 records + seps collapse far below input size"
);
for i in 0..70 {
assert!(
out.contains(&format!("{}", i + 10)),
"line number {} lost",
i + 10
);
}
}
}