yarp-cli 0.3.0

Prune Pi command output and archive tool calls locally
Documentation
use super::classify::{body, contains_ascii_insensitive, trim_start};
use super::evidence::EvidenceClass;

#[must_use]
pub fn classify(line: &[u8]) -> EvidenceClass {
    let line = trim_start(body(line));
    if is_outcome(line) {
        EvidenceClass::Outcome
    } else if line.starts_with("ℹ".as_bytes()) || is_artifact(line) {
        EvidenceClass::Example
    } else if is_diagnostic(line) {
        EvidenceClass::Diagnostic
    } else if is_progress(line) || line.is_empty() {
        EvidenceClass::Noise
    } else if line.starts_with(b"> ")
        || line.starts_with(b"[build]")
        || line.starts_with(b"[package]")
    {
        EvidenceClass::Structure
    } else {
        EvidenceClass::Example
    }
}

fn is_outcome(line: &[u8]) -> bool {
    [
        &b"build successful"[..],
        &b"build failed"[..],
        &b"command failed with exit code"[..],
        &b" passed in "[..],
    ]
    .iter()
    .any(|needle| contains_ascii_insensitive(line, needle))
        || [
            &b"Finished "[..],
            &b"Test Files  "[..],
            &b"Tests  "[..],
            &b"Build complete"[..],
        ]
        .iter()
        .any(|prefix| starts_ascii_insensitive(line, prefix))
        || line.starts_with("✔ Build complete".as_bytes())
}

fn is_artifact(line: &[u8]) -> bool {
    line.windows("├─ ".len())
        .any(|window| window == "├─ ".as_bytes() || window == "└─ ".as_bytes())
}

fn is_diagnostic(line: &[u8]) -> bool {
    [
        &b"error:"[..],
        &b"error["[..],
        &b"warning:"[..],
        &b"warning["[..],
        &b"fatal:"[..],
        &b"panic:"[..],
        &b"exception:"[..],
        &b"traceback"[..],
        &b"failed "[..],
        &b"failure:"[..],
        &b"npm error"[..],
        &b"npm err!"[..],
    ]
    .iter()
    .any(|prefix| starts_ascii_insensitive(line, prefix))
        || line.starts_with("✖".as_bytes())
        || line.starts_with("×".as_bytes())
        || [
            &b"##[error]"[..],
            &b"##[warning]"[..],
            &b"[error]"[..],
            &b"[warning]"[..],
            &b": error "[..],
            &b": warning "[..],
            &b" - error ts"[..],
            &b" - warning ts"[..],
            &b" command failed with exit code "[..],
        ]
        .iter()
        .any(|needle| contains_ascii_insensitive(line, needle))
}

fn starts_ascii_insensitive(line: &[u8], prefix: &[u8]) -> bool {
    line.get(..prefix.len()).is_some_and(|candidate| {
        candidate
            .iter()
            .zip(prefix)
            .all(|(left, right)| left.eq_ignore_ascii_case(right))
    })
}

fn is_progress(line: &[u8]) -> bool {
    [
        &b"Compiling "[..],
        &b"Checking "[..],
        &b"Fresh "[..],
        &b"Downloaded "[..],
        &b"Downloading "[..],
        &b"Building "[..],
        &b"Generating "[..],
        &b"[ "[..],
        &b"[="[..],
    ]
    .iter()
    .any(|prefix| line.starts_with(prefix))
}

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

    #[test]
    fn artifact_names_do_not_become_build_diagnostics() {
        assert_eq!(
            classify("ℹ dist/errors-hash.js 1.25 kB | gzip: 0.52 kB\n".as_bytes()),
            EvidenceClass::Example
        );
        assert_eq!(
            classify("19:21:07   ├─ /graph/payment-failure/index.html (+3ms)\n".as_bytes()),
            EvidenceClass::Example
        );
        assert_eq!(
            classify(b"      \"code\": \"space.variable.ENRICH_MAX_ERROR_RATE\",\n"),
            EvidenceClass::Example
        );
        assert_eq!(
            classify(b"+        throw new Error(`compiled hook runtime is missing`);\n"),
            EvidenceClass::Example
        );
        assert_eq!(
            classify(b"src/main.ts:2:5 - error TS2322: incompatible type\n"),
            EvidenceClass::Diagnostic
        );
        assert_eq!(
            classify("\u{2009}ELIFECYCLE\u{2009} Command failed with exit code 1.\n".as_bytes()),
            EvidenceClass::Outcome
        );
        assert_eq!(classify(b"error: build failed\n"), EvidenceClass::Outcome);
    }
}