supercode-reduce 0.5.45

Optional lossless, reversible session reduction for Volter Harness
Documentation
//! TR-6 (T16) — superseded-output eviction: deterministic same-tool/
//! canonicalized-args supersession behind [`super::ReductionKind::Superseded`].
//!
//! **The rule.** Two (or more) tool results in the same slice were produced
//! by the SAME tool, called with the SAME canonicalized arguments (a re-run
//! of `cargo test`, a re-run of `git diff`, a re-listed directory). Only the
//! chronologically LATEST such result is ever worth keeping in full — an
//! older run of the identical command is superseded information, exactly the
//! way an old failing `cargo test` run becomes noise once the fixed re-run
//! lands. Unlike [`super::ReductionKind::DuplicateOutput`] (TR-2), the
//! superseded and superseding contents are NOT required to be byte-identical
//! — that is the whole point (a stale failing run vs. a later passing one).
//!
//! **v1 canonicalization ([`canonical_key`]).** Deliberately narrow: EXACT
//! match after only the most trivial normalization (trim leading/trailing
//! whitespace; for a tool with a configured "command" field —
//! [`ReductionPolicy::supersede_command_fields`] — collapse internal
//! whitespace RUNS in that one field to a single space). No semantic
//! equivalence guessing of any kind: `ls -la` and `ls -al` list the same
//! information to a human but are DIFFERENT commands here, and stay that way
//! — TR-6.md's frozen spec is explicit that a false supersession (silently
//! hiding a result that was NOT actually superseded) is worse than a missed
//! one (an old result that could have been evicted stays visible instead).
//! Every detection here is a pure function of `(tool_name, arguments)` — no
//! disk I/O, no ambient state — so re-running it against the same transcript
//! always yields the same key for the same call (SPEC.md TR-6 dev/05:
//! determinism).
//!
//! [`ReductionPolicy::supersede_command_fields`]: super::ReductionPolicy::supersede_command_fields

use std::collections::{HashMap, HashSet};

use supercode_interchange::{ChatMessage, Role};

/// One non-read tool-result occurrence in a slice, paired back to the
/// assistant `tool_calls` entry that produced it (by `tool_call_id`, searched
/// BACKWARD from the result — the same direction [`super::detect_reads`]
/// searches in, for the same reason: the result is what [`super::project_messages`]
/// actually reduces, and the call is only consulted to learn what produced
/// it). `key` is this occurrence's [`canonical_key`] — two occurrences with
/// the same key are supersession candidates against each other.
#[derive(Debug, Clone)]
pub(crate) struct SupersedeCandidate {
    /// Index of the `Role::Tool` result in the slice this was detected
    /// against.
    pub(crate) index: usize,
    /// The tool name from the paired assistant call (for the stub summary).
    pub(crate) tool_name: String,
    /// This occurrence's canonicalization key ([`canonical_key`]).
    pub(crate) key: String,
}

/// Find every candidate tool-result occurrence in `msgs`: a `Role::Tool`
/// result (excluding `read_indices` — the read-family passes, A8/TR-3, own
/// that address space exclusively, with strictly richer path-aware
/// redundancy handling than a flat "same tool+args" key could express, the
/// same carve-out [`super::project_messages`]'s TR-2 pass makes) whose
/// `tool_call_id` resolves to a paired assistant `tool_calls` entry. A result
/// with no resolvable pairing (e.g. an imported transcript that never
/// recorded the originating call) is never a candidate — there is no
/// `(tool, args)` identity to key it by.
pub(crate) fn detect(
    msgs: &[ChatMessage],
    read_indices: &HashSet<usize>,
    command_fields: &HashMap<String, String>,
) -> Vec<SupersedeCandidate> {
    let mut out = Vec::new();
    for (i, msg) in msgs.iter().enumerate() {
        if msg.role != Role::Tool || read_indices.contains(&i) {
            continue;
        }
        let Some(call_id) = msg.tool_call_id.as_deref() else {
            continue;
        };
        let Some((tool_name, arguments)) = msgs[..i].iter().rev().find_map(|m| {
            if m.role != Role::Assistant {
                return None;
            }
            m.tool_calls()
                .iter()
                .find(|c| c.id == call_id)
                .map(|c| (c.function.name.clone(), c.function.arguments.clone()))
        }) else {
            continue;
        };
        let key = canonical_key(&tool_name, &arguments, command_fields);
        out.push(SupersedeCandidate {
            index: i,
            tool_name,
            key,
        });
    }
    out
}

/// v1 supersession canonicalization key for one `(tool_name, arguments)` call
/// — see the module doc comment for the exact-match-after-trivial-
/// normalization contract this upholds. `arguments` is the tool call's raw
/// serialized JSON argument string (`FunctionCall::arguments`).
///
/// When `tool_name` has a configured command-bearing field
/// (`command_fields`, e.g. `bash`/`shell`/`exec_command` -> `"command"`) and
/// `arguments` parses as a JSON object with that field present as a string,
/// the key is built from the WHOLE-STRING-trimmed, internal-whitespace-
/// collapsed value of just that field — so `"cargo   test\n"` and
/// `"cargo test"` key identically, but `"cargo test"` and `"cargo test --lib"`
/// never do (no token-level or flag-level equivalence reasoning). For every
/// other tool (no configured field, or the field is absent/non-string/the
/// arguments don't parse as an object), the key falls back to the entire
/// `arguments` string, trimmed only — never whitespace-collapsed internally,
/// since a multi-argument JSON object's internal whitespace is not safely
/// collapsible the way one shell command line's is.
pub(crate) fn canonical_key(
    tool_name: &str,
    arguments: &str,
    command_fields: &HashMap<String, String>,
) -> String {
    let normalized_args = command_fields
        .get(tool_name)
        .and_then(|field| {
            serde_json::from_str::<serde_json::Value>(arguments)
                .ok()
                .and_then(|v| v.get(field).and_then(|f| f.as_str()).map(str::to_string))
        })
        .map(|command| collapse_whitespace(command.trim()))
        .unwrap_or_else(|| arguments.trim().to_string());
    // NUL never appears in a tool name or a trimmed JSON/command string, so
    // this is a safe, unambiguous separator between the two halves of the key
    // (no tool name can ever collide with another tool name + a differently
    // split argument string).
    format!("{tool_name}\u{0}{normalized_args}")
}

/// Collapse every run of Unicode whitespace in `s` to a single ASCII space —
/// QUOTE-AWARE: whitespace runs are only ever collapsed OUTSIDE a `"..."` or
/// `'...'` shell literal. Whitespace (and everything else) INSIDE a quoted
/// literal is preserved byte-for-byte, because it is part of the literal
/// VALUE the shell would pass to the command, not inter-token separator
/// whitespace — collapsing it would make `echo "a  b"` and `echo "a b"`
/// (two textually DIFFERENT commands whose quoted arguments differ) key
/// identically, a false supersession (TR-6.md: worse than a missed one).
///
/// Quote handling follows POSIX shell lexing close enough for this narrow
/// purpose:
/// - a `'...'` (single-quoted) literal has NO escaping at all — a `\` inside
///   it is a literal backslash, and only a following `'` closes it;
/// - OUTSIDE single-quotes (i.e. both unquoted and inside `"..."`), a `\`
///   escapes the very next character: the pair is copied through verbatim
///   and, critically, an escaped quote character (`\"`) does NOT toggle
///   quote state — `"a\"b"` is one continuous double-quoted literal, not two
///   adjacent ones.
///
/// CONSERVATIVE FALLBACK: if `s` ends still inside an open quote (unbalanced
/// — e.g. a truncated/malformed capture), the lex is ambiguous about which
/// bytes are "inside a literal" at all, so this returns `s` completely
/// UNCHANGED (trim was already applied by the caller before this is called;
/// no internal collapsing is attempted) rather than guess — an ambiguous
/// command must never risk being falsely equated with another.
///
/// Pure, allocation-only — never touches anything outside `s` itself.
fn collapse_whitespace(s: &str) -> String {
    #[derive(PartialEq)]
    enum Quote {
        None,
        Single,
        Double,
    }

    let mut out = String::with_capacity(s.len());
    let mut chars = s.chars().peekable();
    let mut quote = Quote::None;
    let mut last_was_space = false;

    while let Some(c) = chars.next() {
        match quote {
            Quote::Single => {
                // POSIX: no escaping inside single quotes; only a closing
                // `'` ends the literal.
                out.push(c);
                if c == '\'' {
                    quote = Quote::None;
                    last_was_space = false;
                }
            }
            Quote::Double => {
                if c == '\\' {
                    out.push(c);
                    if let Some(next) = chars.next() {
                        out.push(next);
                    }
                    // An escaped char (including an escaped `"`) never
                    // toggles quote state.
                } else if c == '"' {
                    out.push(c);
                    quote = Quote::None;
                    last_was_space = false;
                } else {
                    // Verbatim: whitespace inside a quoted literal is part
                    // of the value, never collapsed.
                    out.push(c);
                }
            }
            Quote::None => {
                if c == '\\' {
                    out.push(c);
                    if let Some(next) = chars.next() {
                        out.push(next);
                    }
                    last_was_space = false;
                } else if c == '"' {
                    out.push(c);
                    quote = Quote::Double;
                    last_was_space = false;
                } else if c == '\'' {
                    out.push(c);
                    quote = Quote::Single;
                    last_was_space = false;
                } else if c.is_whitespace() {
                    if !last_was_space {
                        out.push(' ');
                    }
                    last_was_space = true;
                } else {
                    out.push(c);
                    last_was_space = false;
                }
            }
        }
    }

    if quote == Quote::None {
        out
    } else {
        // Unbalanced quote: the lex is ambiguous end-to-end. Conservative
        // fallback — trim-only (already done by the caller), no internal
        // collapse at all — so an ambiguous command can never be falsely
        // equated with a differently-spaced variant.
        s.to_string()
    }
}

/// The built-in default for [`ReductionPolicy::supersede_command_fields`]:
/// the same command-bearing tool identities T30/TR-4's
/// [`super::normalize::NORMALIZE_TOOLS`] normalizes (`bash`, `shell`,
/// `exec_command`), each keyed to their shared `"command"` argument field —
/// the one argument whose value is a literal shell command line, the case
/// TR-6.md's frozen spec calls out by name (`git diff` vs `git diff --stat`,
/// `ls a/` vs `ls b/`).
///
/// [`ReductionPolicy::supersede_command_fields`]: super::ReductionPolicy::supersede_command_fields
pub(crate) fn default_command_fields() -> HashMap<String, String> {
    let mut m = HashMap::new();
    m.insert("bash".to_string(), "command".to_string());
    m.insert("shell".to_string(), "command".to_string());
    m.insert("exec_command".to_string(), "command".to_string());
    m
}