bctx-weave 0.1.25

bctx-weave — FilterMesh lens pipeline, CLI interception, domain compression
Documentation
use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;

// INFO output section headers: "# Server", "# Clients", etc.
static INFO_SECTION_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^# \w+\s*$").unwrap());
// INFO fields to keep (signal-rich)
const INFO_KEEP: &[&str] = &[
    "redis_version",
    "uptime_in_seconds",
    "connected_clients",
    "used_memory_human",
    "used_memory_peak_human",
    "mem_fragmentation_ratio",
    "rdb_last_bgsave_status",
    "aof_enabled",
    "role",
    "connected_slaves",
    "master_replid",
    "db0:",
    "db1:",
    "db2:",
    "db3:",
    "db4:",
    "db5:",
    "db6:",
    "db7:",
    "db8:",
    "db9:",
    "total_commands_processed",
    "instantaneous_ops_per_sec",
    "total_net_input_bytes",
    "keyspace_hits",
    "keyspace_misses",
];

// ── KEYS * / SCAN output ──────────────────────────────────────────────────────

pub fn compress_keys(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let lines: Vec<&str> = cleaned.lines().filter(|l| !l.trim().is_empty()).collect();
    if lines.len() > 50 {
        return format!(
            "{}\n… [{} more keys — use SCAN with MATCH/COUNT or KEYS pattern:*] …",
            lines[..50].join("\n"),
            lines.len() - 50
        );
    }
    lines.join("\n")
}

// ── INFO output ───────────────────────────────────────────────────────────────

pub fn compress_info(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let mut out: Vec<&str> = Vec::new();
    let mut current_section = "";

    for line in cleaned.lines() {
        let t = line.trim();
        if t.is_empty() {
            continue;
        }
        // Track section headers
        if INFO_SECTION_RE.is_match(t) {
            current_section = t;
            // Only emit headers for sections that have kept fields
            out.push(line);
            continue;
        }
        let _ = current_section;
        // Keep signal-rich fields; skip the rest
        if INFO_KEEP.iter().any(|k| t.starts_with(k)) {
            out.push(line);
        }
    }
    if out.is_empty() {
        return compress_generic(raw);
    }
    out.join("\n")
}

// ── DEBUG OBJECT / TYPE output ────────────────────────────────────────────────

pub fn compress_debug(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    // Strip encoding/serialization noise, keep key type and size
    let useful: Vec<&str> = cleaned
        .lines()
        .filter(|l| {
            let t = l.trim();
            !t.is_empty()
                && !t.contains("ql-nodes")
                && !t.contains("ziplist")
                && !t.contains("listpack")
                && !t.contains("refcount")
                && (t.contains("encoding")
                    || t.contains("serializedlength")
                    || t.contains("lru_seconds")
                    || t.contains("type:")
                    || t.starts_with("Value at:")
                    || t.contains("error")
                    || t.contains("Error"))
        })
        .collect();
    if useful.is_empty() {
        return compress_generic(raw);
    }
    useful.join("\n")
}

// ── generic redis-cli output ──────────────────────────────────────────────────

pub fn compress_generic(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let lines: Vec<&str> = cleaned.lines().filter(|l| !l.trim().is_empty()).collect();
    if lines.len() > 40 {
        return format!(
            "{}\n… [{} more lines] …",
            lines[..40].join("\n"),
            lines.len() - 40
        );
    }
    lines.join("\n")
}

// ── top-level (called from db/mod.rs) ────────────────────────────────────────

pub fn compress_redis(raw: &str) -> String {
    // Heuristic: detect which kind of output this is
    let t = raw.trim();
    if t.contains("redis_version:") || t.contains("# Server") {
        return compress_info(raw);
    }
    // KEYS/SCAN: numbered list or plain key names
    let lines: Vec<&str> = t.lines().collect();
    let looks_like_keys = lines.len() > 5
        && lines
            .iter()
            .take(5)
            .all(|l| !l.contains(':') || l.trim().starts_with('"'));
    if looks_like_keys {
        return compress_keys(raw);
    }
    compress_generic(raw)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn keys_truncates_large_keyspace() {
        let keys: Vec<String> = (0..60).map(|i| format!("user:session:{i}")).collect();
        let out = compress_keys(&keys.join("\n"));
        assert!(out.contains("more keys"), "{out}");
        assert!(out.contains("SCAN"), "{out}");
    }

    #[test]
    fn info_keeps_signal_fields() {
        let raw = "# Server\nredis_version:7.2.0\nos:Linux 5.15.0\narch_bits:64\n# Clients\nconnected_clients:42\nblocked_clients:0\n# Memory\nused_memory_human:1.23M\nused_memory_peak_human:2.00M\nmem_fragmentation_ratio:1.05\ntotal_system_memory_human:16.00G\n";
        let out = compress_info(raw);
        assert!(out.contains("redis_version"), "{out}");
        assert!(out.contains("connected_clients"), "{out}");
        assert!(out.contains("used_memory_human"), "{out}");
        assert!(!out.contains("arch_bits"), "{out}");
        assert!(!out.contains("total_system_memory_human"), "{out}");
    }

    #[test]
    fn info_keeps_keyspace_db_lines() {
        let raw = "# Keyspace\ndb0:keys=1234,expires=56,avg_ttl=78900\n";
        let out = compress_info(raw);
        assert!(out.contains("db0:"), "{out}");
    }

    #[test]
    fn generic_truncates_large_output() {
        let lines: Vec<String> = (0..50).map(|i| format!("line {i}: value")).collect();
        let out = compress_generic(&lines.join("\n"));
        assert!(out.contains("more lines"), "{out}");
    }
}