yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! The per-verb **capability probe** (DESIGN §16.6 W5 as amended): the normative
//! driven-verb list, each verb probed by one short `<tool> <verb> --help` spawn
//! (bz by `--version`), classified against the pre-split [`super`] verdict types.
//!
//! Pre-split out of [`super`] for the verb-probe growth (§16.6 W5): the list and
//! the spawns live here, the verdict types and the gate there. The read-only
//! premise is **per tool**: `lernie` is clap, where `--help` is contractual;
//! `bl`'s convention (`<verb> --help` ⇒ exit 0, unknown verb ⇒ exit 2) is pinned
//! empirically on the tested tuple — a future `bl` whose probe mutates or exits
//! non-zero classifies [`Mismatch`](super::ToolState::Mismatch), which is the gate
//! *working*, not a false negative.
//!
//! The spawn is an injected seam ([`ToolProbe`]): [`classify`] is pure over it and
//! table-tested without a live host; [`CliProbe`] is the production wiring over
//! [`Cli`], constructed at the shell boundary per tool.

use super::{ToolState, remediation};
use crate::cli_outbound::{Binary, Chunk, Cli, ExitInfo, Stream};
use std::io;

/// `lernie`'s driven verbs (§16.6 W5 normative list): each probed `lernie <verb>
/// --help` ⇒ exit 0 (clap-contractual). A missing verb classifies `Mismatch`.
const LERNIE_VERBS: [&str; 7] = [
    "prime", "new", "prompt", "message", "stop", "scan", "config",
];
/// `bl`'s driven verbs (§16.6 W5 normative list): each probed `bl <verb> --help`
/// ⇒ 0, unknown ⇒ 2 (pinned empirically on the tested tuple).
const BL_VERBS: [&str; 8] = [
    "create", "claim", "unclaim", "close", "update", "list", "show", "conf",
];
const HELP: &str = "--help";
/// `bz`'s one capability (§16.6 W5): `bz --version` ⇒ exit 0.
const VERSION: &str = "--version";

/// The per-verb capability probe seam (§16.6 W5): run one short spawn — `<binary>
/// <args…>` — and return its exit code; `Err` ⇒ the binary could not be spawned
/// (absent). Injected so [`classify`] is table-tested without a live host;
/// [`CliProbe`] is the production wiring.
pub trait ToolProbe {
    /// Run `<binary> <args…>` to completion and return its exit code (shell
    /// convention). `Err` only on a **spawn** failure (the binary is absent).
    fn status(&self, binary: Binary, args: &[&str]) -> io::Result<i32>;
}

/// The `(verb, argv)` capability probes for `binary` (§16.6 W5): `lernie`/`bl` →
/// each driven verb with `--help`; `bz` → its single `--version`.
fn probes(binary: Binary) -> Vec<(&'static str, Vec<&'static str>)> {
    match binary {
        Binary::Lernie => LERNIE_VERBS.iter().map(|v| (*v, vec![*v, HELP])).collect(),
        Binary::Bl => BL_VERBS.iter().map(|v| (*v, vec![*v, HELP])).collect(),
        Binary::Bz => vec![(VERSION, vec![VERSION])],
    }
}

/// Classify one tool by capability (§16.6 W5): probe each driven verb through the
/// seam; the first non-zero exit is a [`Mismatch`](ToolState::Mismatch) naming
/// that verb, a spawn failure is [`Missing`](ToolState::Missing), all-zero is
/// [`Ok`](ToolState::Ok). Pure over the seam and table-tested.
pub(crate) fn classify(runner: &dyn ToolProbe, binary: Binary) -> ToolState {
    for (verb, args) in probes(binary) {
        match runner.status(binary, &args) {
            Ok(0) => {}
            Ok(_) => {
                return ToolState::Mismatch {
                    verb: verb.to_owned(),
                    remediation: remediation(binary).to_owned(),
                };
            }
            Err(_) => {
                return ToolState::Missing {
                    remediation: remediation(binary).to_owned(),
                };
            }
        }
    }
    ToolState::Ok
}

/// The production [`ToolProbe`] over [`Cli`]. Construct at the shell boundary with
/// `Cli::resolve_in_world(..)` per tool (§16.4 phase-1: the standing world env
/// resolves the nested paths; this only checks the verbs are the tested ones).
pub struct CliProbe {
    bl: Cli,
    lernie: Cli,
    bz: Cli,
}

impl CliProbe {
    /// Wire the three resolved tool CLIs (bl, lernie, bz).
    pub fn new(bl: Cli, lernie: Cli, bz: Cli) -> Self {
        Self { bl, lernie, bz }
    }

    fn cli(&self, binary: Binary) -> &Cli {
        match binary {
            Binary::Bl => &self.bl,
            Binary::Lernie => &self.lernie,
            Binary::Bz => &self.bz,
        }
    }
}

impl ToolProbe for CliProbe {
    fn status(&self, binary: Binary, args: &[&str]) -> io::Result<i32> {
        let stream = self.cli(binary).run(args).map_err(io::Error::other)?;
        Ok(drain_exit(stream))
    }
}

/// Drain a probe stream to end-of-output and collapse its terminal status to the
/// shell-convention exit integer (§8). Output is discarded — only the exit gates.
fn drain_exit(stream: Stream) -> i32 {
    let mut exit = ExitInfo::Unknown;
    for chunk in stream {
        if let Chunk::Exited(e) = chunk {
            exit = e;
        }
    }
    exit.shell_code()
}