Skip to main content

tokenfold_core/transforms/
logs.rs

1//! `log_compaction` transform (canonical id `"log_compaction"`, v1.0.0).
2//!
3//! **Mode:** lossy-with-evidence. Ships behind `--experimental` until the fidelity gate
4//! (roadmap.md F-016) is green for this transform; see `crate::modes::ALL_ENTRIES`.
5//!
6//! Collapses runs of three or more **adjacent** identical lines into three output lines:
7//! the first occurrence, an evidence marker `[repeated Nx]` recording exactly how many
8//! copies were dropped, and the last occurrence. This preserves enough evidence to tell a
9//! reader "this line repeated N times here" without paying the token cost of every copy.
10//!
11//! Deliberate behavioral contract (see `port_spec` in `eval/transforms/log_compaction.py`
12//! and roadmap.md F-012):
13//! - **Threshold is 3, not 2.** Runs of exactly one or exactly two adjacent identical lines
14//!   are left completely untouched — no marker, every line kept. Only a run of three or
15//!   more collapses to the three-line evidence form above, regardless of how long the run
16//!   actually is (`[repeated 2000x]` is still just three output lines).
17//! - **Adjacent-only.** Only *consecutive* identical lines count as a run. Non-adjacent
18//!   duplicates (e.g. `[A, B, A]`) are never collapsed, even though `A` appears twice
19//!   overall. This is a documented, tested limitation, not a bug — interleaved log
20//!   deduplication is explicitly out of scope for this transform.
21//! - Relative line ordering is always preserved.
22//! - Empty input produces empty output; a single-line input is returned unchanged.
23//! - Timestamp removal is opt-in only (default off, via `remove_timestamps: bool`). When
24//!   off, two lines that differ only by timestamp are distinct strings and are never
25//!   collapsed. When on, a recognized leading timestamp is stripped from every line before
26//!   comparison, and the stripped form is what appears in the output — the timestamp is
27//!   genuinely removed from surviving lines, not just ignored for comparison purposes.
28//!
29//! Timestamp patterns run on Rust's `regex` crate, whose matching engine is provably
30//! linear-time in the length of the input (no catastrophic backtracking) — see `deny.toml`
31//! at the workspace root.
32
33use regex::Regex;
34use std::sync::OnceLock;
35
36/// Canonical transform id, as registered with the pipeline.
37pub const TRANSFORM_ID: &str = "log_compaction";
38
39/// Semantic version of this transform's output behavior.
40pub const TRANSFORM_VERSION: &str = "1.0.0";
41
42/// Minimum length of an adjacent run of identical lines that gets collapsed into the
43/// `[repeated Nx]` evidence form. Runs shorter than this are left completely unchanged.
44/// This is intentionally 3, not 2 — see the module-level doc comment.
45const MIN_RUN_LEN: usize = 3;
46
47/// Matches a leading ISO-8601 timestamp, e.g. `2026-07-11T10:22:03Z `,
48/// `2026-07-11T10:22:03.123456Z `, or `2026-07-11T10:22:03+00:00 `, including the
49/// trailing whitespace that separates it from the rest of the line.
50fn iso8601_timestamp_pattern() -> &'static Regex {
51    static RE: OnceLock<Regex> = OnceLock::new();
52    RE.get_or_init(|| {
53        Regex::new(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})[ \t]+")
54            .expect("iso8601_timestamp_pattern regex is a fixed valid literal")
55    })
56}
57
58/// Matches a leading syslog-style timestamp, e.g. `Jul 11 10:22:03 `, including the
59/// trailing whitespace that separates it from the rest of the line.
60fn syslog_timestamp_pattern() -> &'static Regex {
61    static RE: OnceLock<Regex> = OnceLock::new();
62    RE.get_or_init(|| {
63        Regex::new(r"^[A-Z][a-z]{2}[ \t]+\d{1,2}[ \t]+\d{2}:\d{2}:\d{2}[ \t]+")
64            .expect("syslog_timestamp_pattern regex is a fixed valid literal")
65    })
66}
67
68/// Strips a recognized leading timestamp from a single line, if present. Lines that don't
69/// match any recognized pattern are returned untouched.
70fn strip_timestamp(line: &str) -> &str {
71    if let Some(m) = iso8601_timestamp_pattern().find(line) {
72        return &line[m.end()..];
73    }
74    if let Some(m) = syslog_timestamp_pattern().find(line) {
75        return &line[m.end()..];
76    }
77    line
78}
79
80/// Compacts a log by collapsing runs of three or more adjacent identical lines into
81/// `first-occurrence` / `[repeated Nx]` / `last-occurrence` (three output lines total,
82/// regardless of run length). See the module-level doc comment for the full behavioral
83/// contract, including the adjacent-only limitation and the >=3 threshold.
84///
85/// # Parameters
86/// - `input`: raw log text as `\n`-separated lines (a trailing newline is not required).
87/// - `remove_timestamps`: when `true`, a recognized leading timestamp (ISO-8601 or
88///   syslog-style) is stripped from every line before run-detection, and the stripped form
89///   is what appears in the output. When `false` (the default), timestamps are never
90///   touched, so two lines identical except for their timestamp are treated as distinct.
91pub fn compact(input: &str, remove_timestamps: bool) -> String {
92    if input.is_empty() {
93        return String::new();
94    }
95
96    let lines: Vec<&str> = input.lines().collect();
97
98    if remove_timestamps {
99        let stripped: Vec<&str> = lines.iter().map(|line| strip_timestamp(line)).collect();
100        collapse_adjacent_runs(&stripped)
101    } else {
102        collapse_adjacent_runs(&lines)
103    }
104}
105
106/// Walks `lines` once, grouping adjacent identical lines into runs and handing each
107/// complete run to [`push_run`]. See [`compact`] for the full behavioral contract.
108fn collapse_adjacent_runs(lines: &[&str]) -> String {
109    let mut out: Vec<String> = Vec::new();
110    let mut iter = lines.iter();
111    let Some(&first) = iter.next() else {
112        return String::new();
113    };
114    let mut current = first;
115    let mut count = 1usize;
116
117    for &line in iter {
118        if line == current {
119            count += 1;
120        } else {
121            push_run(&mut out, current, count);
122            current = line;
123            count = 1;
124        }
125    }
126    push_run(&mut out, current, count);
127
128    out.join("\n")
129}
130
131/// Emits one run of `count` adjacent copies of `line` into `out`: collapsed into the
132/// `first` / `[repeated Nx]` / `last` evidence form when `count >= MIN_RUN_LEN`, or kept
133/// verbatim (every copy, unchanged) otherwise.
134fn push_run(out: &mut Vec<String>, line: &str, count: usize) {
135    if count >= MIN_RUN_LEN {
136        out.push(line.to_string());
137        out.push(format!("[repeated {count}x]"));
138        out.push(line.to_string());
139    } else {
140        for _ in 0..count {
141            out.push(line.to_string());
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn run_of_four_collapses_to_first_marker_last() {
152        let input = "A\nA\nA\nA";
153        assert_eq!(compact(input, false), "A\n[repeated 4x]\nA");
154    }
155
156    #[test]
157    fn run_of_exactly_three_collapses_boundary_case() {
158        // Boundary: the threshold is >= 3, so exactly three must collapse.
159        let input = "A\nA\nA";
160        assert_eq!(compact(input, false), "A\n[repeated 3x]\nA");
161    }
162
163    #[test]
164    fn run_of_exactly_two_does_not_collapse_boundary_case() {
165        // Boundary: the threshold is 3, NOT 2 - exactly two adjacent identical lines must
166        // be left completely unchanged, with no marker inserted.
167        let input = "A\nA";
168        assert_eq!(compact(input, false), "A\nA");
169    }
170
171    #[test]
172    fn non_adjacent_duplicates_are_never_collapsed() {
173        // Documented, intentional limitation: [A, B, A] has "A" appearing twice overall,
174        // but the two occurrences are not adjacent, so no collapse happens at all.
175        let input = "A\nB\nA";
176        assert_eq!(compact(input, false), "A\nB\nA");
177    }
178
179    #[test]
180    fn single_line_input_is_unchanged() {
181        let input = "only line";
182        assert_eq!(compact(input, false), "only line");
183    }
184
185    #[test]
186    fn empty_input_produces_empty_output() {
187        assert_eq!(compact("", false), "");
188    }
189
190    #[test]
191    fn mix_of_runs_and_singletons_preserves_relative_ordering() {
192        let input = "start\nA\nA\nA\nA\nA\nmiddle\nB\nB\nend";
193        let expected = "start\nA\n[repeated 5x]\nA\nmiddle\nB\nB\nend";
194        assert_eq!(compact(input, false), expected);
195    }
196
197    #[test]
198    fn remove_timestamps_true_collapses_lines_that_differ_only_by_timestamp() {
199        // Collapsing requires a run of >= 3 (MIN_RUN_LEN), so three lines are used here -
200        // not two - to actually trigger a collapse under the documented >=3 threshold,
201        // while still demonstrating that timestamp-only differences are ignored once
202        // remove_timestamps strips them.
203        let input = "2026-07-11T10:22:03Z connection reset\n\
204                     2026-07-11T10:22:04.123456Z connection reset\n\
205                     2026-07-11T10:22:05+00:00 connection reset";
206        let output = compact(input, true);
207        assert_eq!(output, "connection reset\n[repeated 3x]\nconnection reset");
208    }
209
210    #[test]
211    fn remove_timestamps_true_strips_syslog_style_prefix_too() {
212        let input = "Jul 11 10:22:03 connection reset\nJul 11 10:22:04 connection reset\nJul 11 10:22:05 connection reset";
213        let output = compact(input, true);
214        assert_eq!(output, "connection reset\n[repeated 3x]\nconnection reset");
215    }
216
217    #[test]
218    fn remove_timestamps_false_keeps_timestamp_differing_lines_distinct() {
219        // Default (off): timestamps are never touched, so these two lines - identical
220        // except for their timestamp - are treated as distinct and are not collapsed.
221        let input = "2026-07-11T10:22:03Z connection reset\n2026-07-11T10:22:04Z connection reset";
222        assert_eq!(compact(input, false), input);
223    }
224
225    #[test]
226    fn remove_timestamps_true_leaves_lines_without_a_recognized_timestamp_untouched() {
227        let input = "no timestamp here\nno timestamp here\nno timestamp here";
228        let output = compact(input, true);
229        assert_eq!(
230            output,
231            "no timestamp here\n[repeated 3x]\nno timestamp here"
232        );
233    }
234}