yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! The phase-1 toolchain **capability gate** (DESIGN §16.4, §16.6 W5 as amended).
//!
//! In phase 1 yog shells to the host's `bl`/`lernie`/`bz` binaries (lernie is not
//! lib-ready). Correctness argument (b) of §16.4 — that an agent driving yog's
//! nested world runs *yog's exact* balls/lernie/brazen — is only softly met while
//! the tools are host binaries, so this gate guards it. **Presence gating shipped
//! first and was proven blind** (a stale lernie predating `prime` passed, then
//! every Start died at `lernie prime` exit 2): a binary's *existence* says nothing
//! about its *verbs*. So the gate probes each tool by **capability** against the
//! normative driven-verb list (§16.6 W5) — one short `<tool> <verb> --help` per
//! verb (bz by `--version`), the list and the spawns living in [`probe`].
//!
//! A missing verb classifies [`ToolState::Mismatch`] **naming the verb and
//! carrying the remediation command** ([`remediation`], the §8.3
//! show-the-exact-command pattern); an unspawnable binary is [`ToolState::Missing`].
//! Only [`ToolState::Ok`] permits. The gate is consulted **inside the dispatch
//! layer** — [`ToolchainState::require`] refuses a mutating dispatch with the
//! verdict (`start::prepare`'s precondition and `actions::verbs`) — while the read
//! path is **never** gated: rendering ALWAYS continues.
//!
//! **Explicitly phase-1-scoped (§16.4):** phase 2's exact-pinned crates make the
//! version definitional and **DELETE this gate** — this module, its verb lists,
//! and the pane it feeds all die with phase 1.

use crate::cli_outbound::Binary;

mod probe;
pub(crate) use probe::classify;
pub use probe::{CliProbe, ToolProbe};

/// The phase-1 remediation for `bl` (§8.3): the exact reinstall command, carried
/// verbatim in every non-`Ok` verdict for the tool. Phase 2 dissolves the install
/// story entirely (§16.4), deleting this table.
const BL_FIX: &str = "reinstall balls to match yog's pinned verbs: `cargo install --locked balls`";
/// The phase-1 remediation for `lernie` (§8.3).
const LERNIE_FIX: &str =
    "reinstall lernie to match yog's pinned verbs: `cargo install --locked lernie`";
/// The phase-1 remediation for `bz` (§8.3).
const BZ_FIX: &str =
    "reinstall brazen to match yog's pinned probe: `cargo install --locked brazen`";

/// The phase-1 disclaimer, shown verbatim in the toolchain pane (§16.4): the gate
/// exists only because phase 1 shells to host tools; phase 2's exact-pinned crates
/// delete it.
pub(crate) const PHASE1_DISCLAIMER: &str = "Phase-1 gate: yog shells to host \
tools, probing each driven verb by capability. Phase 2's exact-pinned crates make \
the verbs definitional and delete this gate (DESIGN §16.4).";

/// The remediation command for `binary` (§8.3) — the static per-tool text every
/// non-`Ok` verdict carries so the surface shows the exact fix.
pub(crate) fn remediation(binary: Binary) -> &'static str {
    match binary {
        Binary::Bl => BL_FIX,
        Binary::Lernie => LERNIE_FIX,
        Binary::Bz => BZ_FIX,
    }
}

/// The display name of `binary` for verdicts and pane rows.
pub(crate) fn tool_name(binary: Binary) -> &'static str {
    match binary {
        Binary::Bl => "bl",
        Binary::Lernie => "lernie",
        Binary::Bz => "bz",
    }
}

/// One tool's capability verdict (§16.6 W5). `pub` so `tests/` asserts refusals
/// directly; a `Mismatch` **names the missing verb** and carries its remediation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ToolState {
    /// Every driven verb answered `--help`/`--version` with exit 0.
    Ok,
    /// A driven verb's probe exited non-zero — the tool lacks it (the gate
    /// *working*, §16.6 W5). Names the `verb` and the `remediation` command.
    Mismatch { verb: String, remediation: String },
    /// The binary could not be spawned at all — absent. Carries the same per-tool
    /// remediation (install it).
    Missing { remediation: String },
}

impl ToolState {
    /// Whether this state PERMITS the mutating verbs that need the tool (§16.4).
    /// Only `Ok` permits; a `Mismatch`/`Missing` REFUSES — never rendering.
    pub(crate) fn permits(&self) -> bool {
        matches!(self, ToolState::Ok)
    }

    /// The remediation command this verdict carries, if any (`None` for `Ok`) —
    /// the pane paints it beside the verdict (§8.3).
    pub(crate) fn remediation(&self) -> Option<&str> {
        match self {
            ToolState::Ok => None,
            ToolState::Mismatch { remediation, .. } | ToolState::Missing { remediation } => {
                Some(remediation)
            }
        }
    }

    /// A human-readable one-line verdict for the pane and the refusal message —
    /// the named verb (or absence) plus the remediation.
    pub(crate) fn describe(&self) -> String {
        match self {
            ToolState::Ok => "ok".to_owned(),
            ToolState::Mismatch { verb, remediation } => {
                format!("missing verb `{verb}` — {remediation}")
            }
            ToolState::Missing { remediation } => format!("not found — {remediation}"),
        }
    }
}

/// The classified state of every host tool (§16.6 W5): the read-only pane's source
/// and the mutating verbs' gate. Held by [`AppModel`](crate::AppModel), probed once
/// at startup (before first render) and on toolchain-pane refresh. `pub` so
/// `tests/` builds one (via [`probe`]) to drive `start::prepare`'s precondition.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolchainState {
    bl: ToolState,
    lernie: ToolState,
    bz: ToolState,
}

impl ToolchainState {
    fn state_of(&self, binary: Binary) -> &ToolState {
        match binary {
            Binary::Bl => &self.bl,
            Binary::Lernie => &self.lernie,
            Binary::Bz => &self.bz,
        }
    }

    /// Whether the mutating verbs needing `binary` are permitted (§16.4). `bz`
    /// gates no phase-1 mutating verb — no verb needs brazen — but it is probed
    /// and shown all the same (the tool with a `--version` capability).
    pub(crate) fn permits(&self, binary: Binary) -> bool {
        self.state_of(binary).permits()
    }

    /// The dispatch-layer gate (§16.4 W5): `Ok(())` permits the mutating verb that
    /// needs `binary`; a `Mismatch`/`Missing` rides back a typed [`Refusal`]
    /// naming the tool and its verdict, for the surface and the ops line. `start`'s
    /// precondition and `actions::verbs` both refuse through this — never only the
    /// shell's cosmetic greying (a gate only some verbs honor is not a gate).
    pub(crate) fn require(&self, binary: Binary) -> Result<(), Refusal> {
        let state = self.state_of(binary);
        if state.permits() {
            Ok(())
        } else {
            Err(Refusal {
                tool: tool_name(binary),
                state: state.clone(),
            })
        }
    }

    /// The three tools in pane order, each with its display name and state — the
    /// read-only pane's render source.
    pub(crate) fn rows(&self) -> [(&'static str, &ToolState); 3] {
        [("bl", &self.bl), ("lernie", &self.lernie), ("bz", &self.bz)]
    }
}

/// A dispatch-layer **refusal** (§16.4 W5): a mutating verb declined because the
/// tool it needs is not `Ok`. Carries the tool name and its verdict (the named
/// verb + remediation). `pub` — `tests/` asserts refusals; `start::prepare` folds
/// it into [`StartError`](crate::start::StartError), and the short verbs return it.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{tool}: {}", .state.describe())]
pub struct Refusal {
    pub tool: &'static str,
    pub state: ToolState,
}

/// Probe the whole toolchain through `runner` (§16.6 W5) — one capability
/// classification per tool against the normative driven-verb list. Pure over the
/// seam; [`AppModel::new`](crate::AppModel) calls it before first render, and
/// [`refresh_toolchain`](crate::AppModel::refresh_toolchain) on pane refresh.
pub fn probe(runner: &dyn ToolProbe) -> ToolchainState {
    ToolchainState {
        bl: classify(runner, Binary::Bl),
        lernie: classify(runner, Binary::Lernie),
        bz: classify(runner, Binary::Bz),
    }
}

#[cfg(test)]
mod tests;