bctx-weave 0.1.9

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

// Jujutsu change IDs are 12-char hex — shorten to 8
static JJ_CHANGE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([0-9a-f]{12,40})\b").unwrap());
// "Working copy changes:" header noise
static JJ_COPY_HDR_RE: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"(?m)^(Working copy |Parent commit|Working copy now at)[^\n]*\n?").unwrap()
});
// jj log header decoration lines: "◆ ○ @" etc. are meaningful, keep them.
// Strip empty separator lines that jj emits between commits.
static JJ_EMPTY_SEP_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^[│ ]*\n").unwrap());

// ── jj log ────────────────────────────────────────────────────────────────────

pub fn compress_jj_log(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let s = JJ_EMPTY_SEP_RE.replace_all(&cleaned, "");
    let lines: Vec<&str> = s.lines().filter(|l| !l.trim().is_empty()).collect();
    if lines.len() <= 30 {
        return lines.join("\n");
    }
    let head = 15;
    let tail = 10;
    let skipped = lines.len() - head - tail;
    format!(
        "{}\n... [{skipped} commits omitted] ...\n{}",
        lines[..head].join("\n"),
        lines[lines.len() - tail..].join("\n"),
    )
}

// ── jj diff ───────────────────────────────────────────────────────────────────

pub fn compress_jj_diff(raw: &str) -> String {
    // jj diff format is similar to git diff — reuse the same heuristics
    let cleaned = compactor::normalise(raw);
    // Count diff sections
    let file_headers: Vec<&str> = cleaned
        .lines()
        .filter(|l| {
            l.starts_with("diff ")
                || l.starts_with("Modified")
                || l.starts_with("Added")
                || l.starts_with("Removed")
        })
        .collect();
    if file_headers.len() <= 8 {
        return compactor::collapse_blanks(&cleaned);
    }
    // Too many files: summarise
    let summary: Vec<&str> = cleaned
        .lines()
        .filter(|l| {
            l.starts_with("diff ")
                || l.starts_with("Modified")
                || l.starts_with("Added")
                || l.starts_with("Removed")
                || l.starts_with("@@")
        })
        .take(20)
        .collect();
    format!(
        "{}\n[{} more files not shown]",
        summary.join("\n"),
        file_headers.len() - 8
    )
}

// ── jj status ─────────────────────────────────────────────────────────────────

pub fn compress_jj_status(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let s = JJ_COPY_HDR_RE.replace_all(&cleaned, "");
    compactor::collapse_blanks(&s)
}

// ── jj describe / commit ──────────────────────────────────────────────────────

pub fn compress_jj_describe(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    // "Working copy now at: xxxxxxxx description" → keep
    // Strip the full 40-char change ID, shorten to 8
    JJ_CHANGE_RE
        .replace_all(&cleaned, |caps: &regex::Captures| {
            caps[1][..8.min(caps[1].len())].to_string()
        })
        .to_string()
}

// ── top-level dispatcher ──────────────────────────────────────────────────────

pub fn compress_jj(subcmd: &str, raw: &str) -> String {
    let sub = subcmd.trim();
    if sub.starts_with("log") {
        return compress_jj_log(raw);
    }
    if sub.starts_with("diff") {
        return compress_jj_diff(raw);
    }
    if sub.starts_with("status") || sub.starts_with("st") {
        return compress_jj_status(raw);
    }
    if sub.starts_with("describe") || sub.starts_with("commit") {
        return compress_jj_describe(raw);
    }
    compactor::normalise(raw)
}

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

    #[test]
    fn jj_log_truncates_long_history() {
        let raw = (0..40)
            .map(|i| format!("○ abc{i:04}ef ({i}d ago) user@host - feat: thing {i}"))
            .collect::<Vec<_>>()
            .join("\n");
        let out = compress_jj_log(&raw);
        assert!(out.contains("omitted"), "{out}");
    }

    #[test]
    fn jj_log_passthrough_short_history() {
        let raw = "@ abc12345 (now) user@host - WIP\n○ def56789 (1h ago) user@host - add thing\n";
        let out = compress_jj_log(raw);
        assert!(out.contains("WIP"), "{out}");
        assert!(!out.contains("omitted"), "{out}");
    }

    #[test]
    fn jj_status_strips_working_copy_header() {
        let raw = "Working copy changes:\nM src/main.rs\nA src/lib.rs\nWorking copy now at: abc12345 WIP\n";
        let out = compress_jj_status(raw);
        assert!(!out.contains("Working copy changes:"), "{out}");
        assert!(out.contains("src/main.rs"), "{out}");
    }

    #[test]
    fn jj_describe_shortens_change_id() {
        let raw = "Working copy now at: abc123456789def0 feat: add new thing\n";
        let out = compress_jj_describe(raw);
        assert!(!out.contains("abc123456789def0"), "{out}");
        assert!(out.contains("abc12345"), "{out}");
    }
}