yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! Auth-shaped step-failure classification (DESIGN §8.3 detection, §15 M6 Z8): a
//! pure predicate over the already-derived step facts (§5.1 #10/#13 — framing
//! Failed + the response/error text) deciding whether a failed step *looks* like a
//! credential / authorization problem, so the Login affordance surfaces one click
//! away (beside the failed step and in the toolchain pane). Shell paints; this is
//! covered logic — the steps view-model reads [`is_auth_failure`] into its Login
//! flag.

use crate::git_tree::error_text;

/// Case-insensitive substrings that mark an **auth-shaped** failure — the
/// credential / auth / 401 / 403 / permission-denied class (§8.3). Matched against
/// the raw error event line ([`error_text`]), so both an HTTP status code and its
/// reason phrase catch. Deliberately narrow so a transport reset or a 500 never
/// fires this: bare `auth` is **excluded** (it hits `author`); the tokens are the
/// full `authenticat` / `authoriz` / `authoris` stems, and every other marker
/// names a genuinely auth-related concept.
const AUTH_MARKERS: &[&str] = &[
    "401",
    "403",
    "unauthorized", // the 401 reason phrase
    "unauthenticated",
    "forbidden", // the 403 reason phrase
    "permission denied",
    "permission-denied",
    "credential",  // credential / credentials
    "authenticat", // authentication / authenticate / not authenticated
    "authoriz",    // authorization / not authorized (US spelling)
    "authoris",    // authorisation (UK spelling)
    "api key",
    "api_key",
    "apikey",
];

/// Does `error_line` (a raw JSONL error event) look auth-shaped? Pure,
/// case-insensitive substring match against [`AUTH_MARKERS`].
pub fn looks_auth(error_line: &str) -> bool {
    let lower = error_line.to_ascii_lowercase();
    AUTH_MARKERS.iter().any(|marker| lower.contains(marker))
}

/// Whether a step's settled `response.json` is an **auth-shaped failure** (§8.3):
/// its framing is Failed (an error event settled the last segment) **and** that
/// error text matches [`looks_auth`]. [`error_text`] returning `Some` already
/// means framing Failed (they share the last-segment traversal), so one call
/// answers both facts — the single source the steps view-model reads for its
/// Login flag.
pub fn is_auth_failure(response_bytes: &[u8]) -> bool {
    error_text(response_bytes).is_some_and(|line| looks_auth(&line))
}

/// Whether an agent's **latest** step is an auth-shaped failure (§11 center):
/// the conversation-view detection that banners Login inline. Reads through the
/// steps view-model ([`crate::steps_view::build`] — the one owner of the
/// per-step Login flag), so the banner, the Steps tab, and the toolchain pane
/// all derive from the same classification. `state` is that build's §3.5
/// liveness argument, which this predicate itself does not consult: an
/// auth-shaped failure is settled error bytes, and settled bytes are the same
/// whoever holds the lock.
pub fn latest_step_auth_failed(
    workspace: &std::path::Path,
    agent_id: &str,
    state: crate::git_tree::AgentState,
) -> bool {
    crate::steps_view::build(workspace, agent_id, state)
        .steps
        .last()
        .is_some_and(|s| s.auth_failed)
}