bctx-weave 0.1.27

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

// "Dockerfile:10 DL3008 warning: Pin versions in apt get install."
static TEXT_FINDING_RE: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"^([^:]+:\d+) (DL\d+|SC\d+) (error|warning|info|style): (.+)$").unwrap()
});

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

    // JSON mode
    if cleaned.trim_start().starts_with('[') && cleaned.contains("\"code\"") {
        return compress_hadolint_json(&cleaned);
    }

    if cleaned.trim().is_empty() {
        return "hadolint: no issues".to_string();
    }

    let mut errors: Vec<String> = Vec::new();
    let mut warnings: Vec<String> = Vec::new();
    let mut info_count = 0usize;

    for line in cleaned.lines() {
        let t = line.trim();
        if t.is_empty() {
            continue;
        }
        if let Some(caps) = TEXT_FINDING_RE.captures(t) {
            let loc = &caps[1];
            let code = &caps[2];
            let level = &caps[3];
            let msg = &caps[4];
            // Strip redundant URL suffix in message
            let msg_clean = msg
                .split("https://")
                .next()
                .unwrap_or(msg)
                .trim_end_matches('.')
                .trim();
            let entry = format!("{loc} {code}: {msg_clean}");
            match level {
                "error" => errors.push(entry),
                "warning" => warnings.push(entry),
                _ => info_count += 1,
            }
        }
    }

    let total = errors.len() + warnings.len() + info_count;
    if total == 0 {
        return "hadolint: no issues".to_string();
    }

    let error_count = errors.len();
    let warning_count = warnings.len();
    let mut result: Vec<String> = errors;
    result.extend(warnings);
    if info_count > 0 {
        result.push(format!("({info_count} info/style issues suppressed)"));
    }
    result.push(format!(
        "hadolint: {warning_count} warnings, {error_count} errors",
    ));
    result.join("\n")
}

fn compress_hadolint_json(raw: &str) -> String {
    use once_cell::sync::Lazy;
    use regex::Regex;
    static CODE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#""code"\s*:\s*"([^"]+)""#).unwrap());
    static LEVEL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#""level"\s*:\s*"([^"]+)""#).unwrap());
    static MSG_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#""message"\s*:\s*"([^"]+)""#).unwrap());
    static LINE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#""line"\s*:\s*(\d+)"#).unwrap());

    let codes: Vec<&str> = CODE_RE
        .captures_iter(raw)
        .filter_map(|c| c.get(1).map(|m| m.as_str()))
        .collect();
    let levels: Vec<&str> = LEVEL_RE
        .captures_iter(raw)
        .filter_map(|c| c.get(1).map(|m| m.as_str()))
        .collect();
    let msgs: Vec<&str> = MSG_RE
        .captures_iter(raw)
        .filter_map(|c| c.get(1).map(|m| m.as_str()))
        .collect();
    let lines: Vec<&str> = LINE_RE
        .captures_iter(raw)
        .filter_map(|c| c.get(1).map(|m| m.as_str()))
        .collect();

    if codes.is_empty() {
        return "hadolint [json]: no issues".to_string();
    }

    let mut result: Vec<String> = codes
        .iter()
        .zip(levels.iter().chain(std::iter::repeat(&"warn")))
        .zip(lines.iter().chain(std::iter::repeat(&"?")))
        .zip(msgs.iter().chain(std::iter::repeat(&"")))
        .filter(|(((_, level), _), _)| **level != "info" && **level != "style")
        .map(|(((code, level), line), msg)| format!(":{line} {code} [{level}]: {msg}"))
        .collect();

    let info_count = codes
        .iter()
        .zip(levels.iter())
        .filter(|(_, l)| **l == "info" || **l == "style")
        .count();

    if info_count > 0 {
        result.push(format!("({info_count} info/style suppressed)"));
    }
    result.join("\n")
}

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

    #[test]
    fn text_mode_groups_by_severity() {
        let raw = "Dockerfile:3 DL3008 warning: Pin versions in apt get install.\nDockerfile:10 DL3009 warning: Delete the apt-get lists after installing.\nDockerfile:15 DL3007 error: Always tag the version.\n";
        let out = compress_hadolint(&raw);
        assert!(out.contains("DL3007") || out.contains("error"), "{out}");
        assert!(out.contains("DL3008") || out.contains("warning"), "{out}");
    }

    #[test]
    fn no_issues_clean() {
        let out = compress_hadolint("");
        assert!(out.contains("no issues"), "{out}");
    }

    #[test]
    fn json_mode_extracts_codes() {
        let raw = r#"[{"code":"DL3008","column":1,"file":"Dockerfile","level":"warning","line":3,"message":"Pin versions in apt get install."},{"code":"DL3007","column":1,"file":"Dockerfile","level":"error","line":15,"message":"Always tag the version."}]"#;
        let out = compress_hadolint(raw);
        assert!(out.contains("DL3008") || out.contains("warning"), "{out}");
    }
}