sara-tasks 1.8.1

Sara — folder-aware task manager
pub struct AcceptanceGate {
    pub total: usize,
    pub ran: usize,
    pub passed: usize,
    pub cached: usize,
    pub failures: Vec<String>,
    pub missing_verify: Vec<String>,
    pub transcript: Vec<GateRun>,
}

impl AcceptanceGate {
    pub fn is_green(&self) -> bool {
        self.total > 0 && self.failures.is_empty() && self.missing_verify.is_empty()
    }

    pub fn reason(&self) -> String {
        if self.total == 0 {
            return "no acceptance criteria to prove — add one with `sara check <id> \"…\" --kind acceptance --verify \"<cmd>\"`".to_string();
        }
        let mut parts = Vec::new();
        if !self.missing_verify.is_empty() {
            parts.push(format!(
                "{} acceptance criterion/criteria have no verify command: {}",
                self.missing_verify.len(),
                self.missing_verify.join("; ")
            ));
        }
        if !self.failures.is_empty() {
            parts.push(format!(
                "{} acceptance verify command(s) failed: {}",
                self.failures.len(),
                self.failures.join("; ")
            ));
        }
        parts.join(" | ")
    }

    pub fn failure_detail(&self) -> String {
        let mut out = String::new();
        for r in self.transcript.iter().filter(|r| !r.passed) {
            if r.output.trim().is_empty() {
                continue;
            }
            let code = r
                .exit_code
                .map(|c| c.to_string())
                .unwrap_or_else(|| "n/a".into());
            out.push_str(&format!("\n$ {} (exit {})\n{}", r.cmd, code, r.output));
        }
        out
    }
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum GateOutput {
    Stream,
    Capture,
}

impl GateOutput {
    pub(super) fn say(self, line: impl std::fmt::Display) {
        if self == GateOutput::Stream {
            println!("{line}");
        }
    }
}

#[derive(Debug, Clone)]
pub struct GateRun {
    pub text: String,
    pub cmd: String,
    pub passed: bool,
    pub exit_code: Option<i32>,
    pub output: String,
}