bctx-weave 0.1.29

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

// "Container foo-bar-1  Created" / "Started" / "Healthy" / "Removed"
static CONTAINER_EVENT_RE: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"(?m)^ Container [^\s]+ +(Created|Recreated|Started|Stopped|Removed)\n?").unwrap()
});
// Layer/pull progress lines
static PULL_LAYER_RE: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"(?m)^[a-f0-9]+: (Pull complete|Already exists|Pulling fs layer|Waiting|Downloading|Extracting)\n?").unwrap()
});
// "Pulling <service> ..."
static PULLING_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^Pulling [^\n]+\n?").unwrap());

pub fn compress_compose(subcmd: &str, raw: &str) -> String {
    let sub = subcmd.trim();
    if sub.starts_with("up") {
        return compress_up(raw);
    }
    if sub.starts_with("down") {
        return compress_down(raw);
    }
    if sub.starts_with("logs") {
        return compress_logs(raw);
    }
    if sub.starts_with("pull") {
        return compress_pull(raw);
    }
    if sub.starts_with("build") {
        // Delegate to docker build patterns
        let cleaned = compactor::normalise(raw);
        return compactor::collapse_blanks(&cleaned);
    }
    // ps, config, version — passthrough
    compactor::normalise(raw)
}

fn compress_up(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);

    // Count healthy/started containers
    let started: Vec<&str> = cleaned
        .lines()
        .filter(|l| {
            let t = l.trim();
            t.contains("Started") || t.contains("Healthy") || t.contains("Running")
        })
        .collect();
    let errors: Vec<&str> = cleaned
        .lines()
        .filter(|l| {
            let t = l.to_lowercase();
            t.contains("error") || t.contains("exit") || t.contains("unhealthy")
        })
        .collect();

    // Strip per-container Created/Started noise when no errors
    let s = CONTAINER_EVENT_RE.replace_all(&cleaned, "");

    // Keep: network creation, error lines, "healthy" checks, final status
    let mut kept: Vec<String> = Vec::new();
    for line in s.lines() {
        let t = line.trim();
        if t.is_empty() {
            continue;
        }
        let tl = t.to_lowercase();
        if tl.contains("network") && (tl.contains("created") || tl.contains("creating")) {
            kept.push(line.to_string());
            continue;
        }
        if tl.contains("error") || tl.contains("exit") || tl.contains("unhealthy") {
            kept.push(line.to_string());
            continue;
        }
        if tl.contains("healthy") {
            kept.push(line.to_string());
        }
    }

    if errors.is_empty() && !started.is_empty() {
        kept.push(format!("{} containers up", started.len()));
    }
    if kept.is_empty() {
        return compactor::collapse_blanks(&cleaned);
    }
    kept.join("\n")
}

fn compress_down(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    // Count stopped containers, collapse to summary
    let stopped = cleaned
        .lines()
        .filter(|l| l.trim().contains("Stopped") || l.trim().contains("Removed"))
        .count();

    let networks: Vec<&str> = cleaned
        .lines()
        .filter(|l| l.trim().contains("Network") && l.trim().contains("Removed"))
        .collect();

    let errors: Vec<&str> = cleaned
        .lines()
        .filter(|l| l.to_lowercase().contains("error"))
        .collect();

    let mut result = Vec::new();
    if stopped > 0 {
        result.push(format!("Stopped/removed {stopped} containers"));
    }
    result.extend(networks.iter().map(|l| l.to_string()));
    result.extend(errors.iter().map(|l| l.to_string()));
    if result.is_empty() {
        return compactor::collapse_blanks(&cleaned);
    }
    result.join("\n")
}

fn compress_logs(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    // Each line is typically "service  | log message" — keep as-is but truncate
    let lines: Vec<&str> = cleaned.lines().filter(|l| !l.trim().is_empty()).collect();
    const MAX_LINES: usize = 50;
    if lines.len() <= MAX_LINES {
        return lines.join("\n");
    }
    // Keep last MAX_LINES (most recent)
    let start = lines.len() - MAX_LINES;
    format!(
        "… [{} earlier lines omitted] …\n{}",
        start,
        lines[start..].join("\n")
    )
}

fn compress_pull(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let s = PULL_LAYER_RE.replace_all(&cleaned, "");
    let s = PULLING_RE.replace_all(&s, "");
    // Keep "Pulled" / "Digest:" / "Status:" lines per image
    let kept: Vec<&str> = s
        .lines()
        .filter(|l| {
            let t = l.trim();
            !t.is_empty()
                && (t.contains("Pulled")
                    || t.contains("Digest")
                    || t.contains("Status")
                    || t.contains("error"))
        })
        .collect();
    if kept.is_empty() {
        return compactor::collapse_blanks(&s);
    }
    kept.join("\n")
}

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

    #[test]
    fn up_strips_container_events_when_healthy() {
        let raw = " Container app-db-1  Created\n Container app-db-1  Started\n Container app-web-1  Created\n Container app-web-1  Started\n";
        let out = compress_compose("up", raw);
        assert!(!out.contains("Container app-db-1  Created"), "{out}");
    }

    #[test]
    fn up_keeps_error_lines() {
        let raw = " Container app-db-1  Started\nError: port is already allocated\n";
        let out = compress_compose("up", raw);
        assert!(out.contains("port is already allocated"), "{out}");
    }

    #[test]
    fn down_summarises_stopped_count() {
        let raw = " Container app-db-1  Stopped\n Container app-db-1  Removed\n Container app-web-1  Stopped\n Container app-web-1  Removed\n Network app_default  Removed\n";
        let out = compress_compose("down", raw);
        assert!(
            out.contains("Stopped") || out.contains("removed") || out.contains("2"),
            "{out}"
        );
    }

    #[test]
    fn logs_truncates_long_output() {
        let raw = (0..80)
            .map(|i| format!("web  | log line {i}\n"))
            .collect::<String>();
        let out = compress_compose("logs", &raw);
        assert!(out.contains("omitted") || out.contains(""), "{out}");
    }

    #[test]
    fn pull_strips_layer_progress() {
        let raw = "Pulling web ...\nabc123: Pulling fs layer\nabc123: Pull complete\ndef456: Already exists\nStatus: Downloaded newer image for nginx:latest\n";
        let out = compress_compose("pull", &raw);
        assert!(!out.contains("Pulling fs layer"), "{out}");
        assert!(!out.contains("Already exists"), "{out}");
        assert!(
            out.contains("Status") || out.contains("Downloaded"),
            "{out}"
        );
    }
}