Skip to main content

Module normalize

Module normalize 

Source
Expand description

T30/TR-4 — terminal-noise normalization (ReductionKind::OutputNormalized): a small, deterministic line-buffer terminal simulator that collapses ANSI color/style codes and carriage-return/erase-line/cursor-up redraws down to the FINAL rendered content of each line — the same content a human watching the build would actually see, without the hundreds of intermediate redraws a captured progress bar otherwise leaves in the transcript.

Not a full vte emulation (per SPEC.md TR-4’s approach sketch): this supports exactly the sequences that dominate real cargo/npm/pip/ docker output —

  • SGR (ESC[...m, colors/styles) — stripped; it never prints or moves.
  • CR (\r) — cursor to column 0 of the current row.
  • LF (\n) — cursor to column 0 of the NEXT row (a deliberate simplification: real LF preserves column, but every real capture in this codebase’s fixtures pairs LF with either a preceding CR or content that starts a fresh line anyway, so this never diverges from the fixtures’ actual rendering and keeps the model trivial to reason about).
  • EL (ESC[K, ESC[0K, ESC[1K, ESC[2K) — erase to end / to start / whole line.
  • CUU / CUD (ESC[<n>A / ESC[<n>B) — cursor up/down n rows (the multi-line redraw idiom docker pull uses for concurrent layers).
  • CHA (ESC[<n>G) — cursor to absolute column n (1-based); the idiom modern npm’s spinner uses instead of \r.
  • DEC private mode set/reset (ESC[?...h / ESC[?...l) — e.g. ?25l/ ?25h (cursor hide/show around a spinner): dropped silently. This is a deliberately narrow carve-out (real DEC private modes can do much more, e.g. the alternate screen buffer) but build-tool output never uses those — see the module-level safety note below.

Everything else — including a CSI sequence with a final byte this module doesn’t recognize (device status report, cursor-position, scroll, etc.), an OSC sequence, or any escape truncated by an upstream byte cap before its terminator — is passed through verbatim, as literal printable text, landing in the rendered output unchanged. “Never guess”: an unrecognized sequence is never assumed to be a no-op, so its bytes are never silently dropped, and the reduction is content-preserving even for escape vocabulary this module has never seen.

§Safety / no-panic guarantee

normalize never panics on any input, including a &str truncated mid-escape-sequence (the TR-1 gotcha this module was warned about: Agent::cap_tool_output’s 100 KB history cap can slice a raw tool output anywhere, including through the middle of a CSI/OSC sequence, before this module ever sees it). A truncated sequence at the end of the input is detected (no terminator found before the string ends) and copied through as literal text, same as any other unrecognized sequence — never a panic, never an out-of-bounds slice. See tests::never_panics_on_malformed_input for a sweep over adversarial byte patterns (including sequences chopped at every possible byte boundary).

All scanning here is on &str byte offsets, but every control byte this module inspects (ESC 0x1B, CR 0x0D, LF 0x0A, CSI param/final bytes 0x20-0x7E) is ASCII — and ASCII bytes are never a continuation byte (0x80-0xBF) or a lead byte (0xC0-0xFF) of a multi-byte UTF-8 sequence, so every position this module treats as a slice boundary is guaranteed to already be a valid char boundary in a well-formed &str. Regular (non-control) runs between control bytes are therefore always safe to slice directly.

Constants§

DEFAULT_MIN_SAVINGS
Minimum byte savings (original.len() - normalized.len()) for project_messages to accept a normalization candidate — SPEC.md TR-4’s “savings floor” knob, mirrored as ReductionPolicy::terminal_output_min_savings. Exposed here as the documented default; the policy field is what callers actually tune.
NORMALIZE_TOOLS
Tool names T30/TR-4’s candidate rule treats as “terminal/exec” — a result from one of these is eligible for super::ReductionKind::OutputNormalized. Compared against the INVOKING tool call’s function name (see detect_normalize_candidates), the same pattern the A8 read-tool list uses for read-type tools:

Functions§

normalize
Normalize input: strip ANSI SGR, simulate CR/EL/CUU/CUD/CHA redraws, and return the final rendered text. Deterministic and pure — same bytes in, byte-identical text out, every call (SPEC.md TR-4’s determinism requirement; see tests::deterministic_across_repeated_runs).
summary
Format the honesty trailer’s summary text (SPEC.md TR-4: “normalized text must remain honest”): plain ASCII, one line, no ] — same constraints the reduction-stub formatter already enforces on every summary, so this is folded into the shared [sc-reduced output-normalized <id>: ...] grammar (see reduce.rs’s OutputNormalized candidate pass) rather than a bespoke sentinel — that keeps the existing leak-guard (A11), stub::parse (sessions show-reductions), and Kind::from dispatch all working for this kind with no special-casing.