bctx-weave 0.1.4

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

// gh pr list / issue list — strip noisy metadata columns
static TABLE_SEPARATOR_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^[-─]+\n?").unwrap());
// gh run list verbose columns (e.g. timing, runner info) — remove runner OS tokens
static RUN_META_RE: Lazy<Regex> = Lazy::new(|| {
    Regex::new(
        r"\b(ubuntu-latest|ubuntu-22\.04|ubuntu-20\.04|macos-latest|macos-14|windows-latest)\b",
    )
    .unwrap()
});
// gh api JSON — large responses
static JSON_BLANK_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r#"(?m)^\s+"[a-z_]+": null,?\n?"#).unwrap());

// ── gh pr ─────────────────────────────────────────────────────────────────────

pub fn compress_pr_list(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let s = TABLE_SEPARATOR_RE.replace_all(&cleaned, "");
    // Keep: ID, title, state, author — strip URL/branch columns
    let kept: Vec<&str> = s
        .lines()
        .filter(|l| !l.trim().is_empty())
        .take(30)
        .collect();
    kept.join("\n")
}

pub fn compress_pr_view(raw: &str) -> String {
    // PR view — strip boilerplate, keep title/state/body/comments summary
    let cleaned = compactor::normalise(raw);
    let lines: Vec<&str> = cleaned.lines().collect();
    if lines.len() > 60 {
        let head = &lines[..20];
        let tail = &lines[lines.len() - 10..];
        let omitted = lines.len() - 30;
        return format!(
            "{}\n... [{} more lines omitted] ...\n{}",
            head.join("\n"),
            omitted,
            tail.join("\n")
        );
    }
    cleaned
}

pub fn compress_pr_checks(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    // Keep each check name + status; strip timing columns
    cleaned
        .lines()
        .filter(|l| !l.trim().is_empty())
        .collect::<Vec<_>>()
        .join("\n")
}

// ── gh issue ──────────────────────────────────────────────────────────────────

pub fn compress_issue_list(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let s = TABLE_SEPARATOR_RE.replace_all(&cleaned, "");
    let kept: Vec<&str> = s
        .lines()
        .filter(|l| !l.trim().is_empty())
        .take(30)
        .collect();
    kept.join("\n")
}

// ── gh run ────────────────────────────────────────────────────────────────────

pub fn compress_run_list(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    // Strip runner OS and trigger columns — keep: status, name, branch, duration
    let s = RUN_META_RE.replace_all(&cleaned, "");
    let kept: Vec<&str> = s
        .lines()
        .filter(|l| !l.trim().is_empty())
        .take(20)
        .collect();
    kept.join("\n")
}

pub fn compress_run_view(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    // Keep job names + statuses; strip verbose step logs
    let lines: Vec<&str> = cleaned.lines().collect();
    if lines.len() > 50 {
        let omitted = lines.len() - 50;
        return format!(
            "{}\n... [{} more log lines] ...",
            lines[..50].join("\n"),
            omitted
        );
    }
    cleaned
}

// ── gh api ────────────────────────────────────────────────────────────────────

pub fn compress_api(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    // Strip null fields, truncate large responses
    let s = JSON_BLANK_RE.replace_all(&cleaned, "");
    let lines: Vec<&str> = s.lines().collect();
    if lines.len() > 60 {
        return format!(
            "{}\n... [{} more lines — use --jq to filter] ...",
            lines[..60].join("\n"),
            lines.len() - 60
        );
    }
    s.into_owned()
}

// ── gh repo ───────────────────────────────────────────────────────────────────

pub fn compress_repo(raw: &str) -> String {
    // gh repo view / clone: keep essential info, strip boilerplate
    let cleaned = compactor::normalise(raw);
    let lines: Vec<&str> = cleaned.lines().collect();
    if lines.len() > 30 {
        return lines[..30].join("\n");
    }
    cleaned
}

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

pub fn compress_gh(subcmd: &str, raw: &str) -> String {
    let sub = subcmd.trim();
    if sub.starts_with("pr list") || sub == "pr" {
        return compress_pr_list(raw);
    }
    if sub.starts_with("pr view") {
        return compress_pr_view(raw);
    }
    if sub.starts_with("pr checks") {
        return compress_pr_checks(raw);
    }
    if sub.starts_with("issue list") || sub == "issue" {
        return compress_issue_list(raw);
    }
    if sub.starts_with("run list") {
        return compress_run_list(raw);
    }
    if sub.starts_with("run view") || sub.starts_with("run watch") {
        return compress_run_view(raw);
    }
    if sub.starts_with("api") {
        return compress_api(raw);
    }
    if sub.starts_with("repo") {
        return compress_repo(raw);
    }
    // Default: normalise + light truncation
    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 lines] ...",
            lines[..50].join("\n"),
            lines.len() - 50
        );
    }
    cleaned
}

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

    #[test]
    fn pr_list_keeps_entries() {
        let raw =
            "#42  Fix the bug            OPEN   user1\n#41  Update readme          MERGED user2\n";
        let out = compress_pr_list(raw);
        assert!(out.contains("#42"), "{out}");
        assert!(out.contains("#41"), "{out}");
    }

    #[test]
    fn pr_list_truncates_at_30() {
        let raw = (0..40)
            .map(|i| format!("#{i}  PR {i}  OPEN  user\n"))
            .collect::<String>();
        let out = compress_pr_list(raw.as_str());
        assert!(!out.contains("#39"), "{out}");
    }

    #[test]
    fn run_list_strips_runner_os() {
        let raw = "completed  success  CI  main  ubuntu-latest  2m\n";
        let out = compress_run_list(raw);
        assert!(!out.contains("ubuntu-latest"), "{out}");
    }

    #[test]
    fn api_truncates_large_response() {
        let raw = (0..80)
            .map(|i| format!("  \"field{i}\": \"value{i}\",\n"))
            .collect::<String>();
        let out = compress_api(&raw);
        assert!(out.contains("more lines"), "{out}");
    }
}