yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! Goal composition + the pre-mint identity preview (DESIGN §3.3, §3.4).
//!
//! Everything here is pure. The **payload prefill** ([`prefill`]) is the editable
//! text the operator sees: empty (bare), a target preamble naming the directory
//! verbatim (path), or the ball's title/body/worktree preamble (ball). The
//! **identity** is *not* prefill — [`identity_preview`] renders the greyed
//! `You are <name>.` prediction the composer shows before submit, and
//! [`identity_preamble`] is the phase-1 interim stamp
//! (`… Stamp every bl verb you run with --as <name>.`) the executor prepends at
//! fire (§3.3, load-bearing until W9). [`preview`] resolves the target name from
//! a pure read (names-root readdir + already-fetched claimants) and pairs it with
//! the prefill; the mint is re-derived and stamped at fire — the preview is a
//! prediction, the stamp is the truth.
//!
//! The ball header composed here ([`ball_preamble`]) is also the single source
//! the derived conversation↔ball join reads back: [`parse_ball_stamp`] is its
//! inverse, one module owning both compose and parse (§3.3, PRINCIPLES "single
//! source of truth" — change the `Ball <id>:` format here and the derivation
//! follows).

use super::{BallSpec, Payload, PlanInputs, StartInputs, Target};
use crate::binding::work_worktree_path;
use crate::names::Rng;
use std::path::{Path, PathBuf};

/// The greyed identity **preview** (§3.3): the single line the composer shows
/// above the box before submit. A prediction — re-derived and stamped at fire.
pub fn identity_preview(name: &str) -> String {
    format!("You are {name}.")
}

/// The harness-stamped identity **preamble** prepended at fire (§3.3, phase-1
/// interim, load-bearing until W9): who the agent is plus the stamp instruction,
/// so a host `bl` verb the agent runs claims under the workspace name.
pub fn identity_preamble(name: &str) -> String {
    format!("You are {name}. Stamp every bl verb you run with --as {name}.")
}

/// The composer view-model (§3.3): the greyed identity `preview` line and the
/// editable payload `prefill`. Both are pure reads — nothing spawns (I7).
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Composer {
    pub preview: String,
    pub prefill: String,
}

/// The pre-submit composer view-model (§3.3): resolve the target name from the
/// pure read and pair its `You are <name>.` preview with the payload prefill.
/// The mint draws from `rng`; an exhausted pool yields an empty preview (the
/// prediction simply has no name — fire logs the abort, §8.1). Nothing spawns.
pub fn preview(inputs: &StartInputs, rng: &mut dyn Rng) -> Composer {
    // An exhausted pool has no name to predict → an empty preview (fire logs the
    // abort, §8.1); `unwrap_or_default` folds that case with no separate branch.
    let preview = mint_name_of(
        &inputs.target,
        &inputs.yog_data_root,
        &inputs.claimants,
        rng,
    )
    .map(|name| identity_preview(&name))
    .unwrap_or_default();
    let worktree = canonical_worktree(&inputs.payload, &inputs.balls_state_root);
    Composer {
        preview,
        prefill: prefill(&inputs.payload, worktree.as_deref()),
    }
}

/// The editable payload prefill (§3.3), per rung. The identity line is **not**
/// here — it is stamped at fire. Bare is empty (the operator types); path and
/// ball carry their target preambles verbatim. `worktree` is the resolved ball
/// worktree the composer names (§3.3, threaded from the claim cross-check): the
/// canonical `<id>` leaf or the `<id>-<claimant>` variant bl actually minted, so
/// the preamble never names a nonexistent path. `None` for bare/path/new rungs.
pub(super) fn prefill(payload: &Payload, worktree: Option<&Path>) -> String {
    match payload {
        Payload::Bare => String::new(),
        Payload::Path { dir } => path_preamble(dir),
        Payload::Ball {
            project,
            ball: BallSpec::Existing {
                id, title, body, ..
            },
        } => ball_preamble(id, title, body, worktree.unwrap_or(project), project),
        Payload::Ball {
            ball: BallSpec::New { title, body },
            ..
        } => format!("Ball (new): {title}\n\n{body}"),
    }
}

/// The path rung's target preamble (§3.3): the working directory named verbatim.
fn path_preamble(dir: &Path) -> String {
    format!(
        "The working directory for this conversation is:\n{}\nDo all work there, by absolute path. Do not rely on the current directory.",
        dir.display(),
    )
}

/// The ball worktree the composer/preamble names for an **existing** ball (§3.3,
/// §3.5): the canonical `work_worktree_path` `<id>` leaf — the pure formula the
/// planner previews and the resume path falls back to. `None` for bare/path/new
/// rungs. The executor overrides this with the claim's cross-checked worktree
/// (the `<id>-<claimant>` variant when bl minted it — addendum: never a guess).
pub(super) fn canonical_worktree(payload: &Payload, balls_state_root: &Path) -> Option<PathBuf> {
    match payload {
        Payload::Ball {
            project,
            ball: BallSpec::Existing { id, .. },
        } => Some(work_worktree_path(balls_state_root, project, id, None)),
        _ => None,
    }
}

/// A workspace's name — its path leaf (§3.1): the `--as`/`YOG_NAME`/identity stamp
/// for an [`Target::Existing`](super::Target) focus (named or foreign). Empty for
/// a rootless path (never a real workspace).
pub(super) fn leaf_name(workspace: &Path) -> String {
    workspace
        .file_name()
        .map(|s| s.to_string_lossy().into_owned())
        .unwrap_or_default()
}

/// The §3.3 ball worktree preamble verbatim: the ball header, the body, and the
/// durable target-repo binding (the absolute work-worktree path rides in the
/// goal *content* because lernie has no target-repo concept, §3.3).
fn ball_preamble(id: &str, title: &str, body: &str, worktree: &Path, project: &Path) -> String {
    format!(
        "Ball {id}: {title}\n\n{body}\n\nThe project repository checkout for this work is the git worktree at:\n{worktree}   (branch work/{id} of {project})\nDo all repository work there, by absolute path. Do not rely on the current directory.",
        worktree = worktree.display(),
        project = project.display(),
    )
}

/// The ball id a conversation root's `goal.md` carries (§3.3): the inverse of
/// [`ball_preamble`]'s `Ball {id}: {title}` header. The harness stamps the
/// identity preamble *above* the header (§3.3), so the scan is line-wise — the
/// first line shaped `Ball <id>: <rest>` yields `<id>`. `None` for a bare/path
/// conversation (no header) or any goal without one. The one parse paired with
/// the one compose above: a start-flow ball is the only conversation-level
/// attribution that exists (§3.2), so a single id — never a set — is derivable.
pub fn parse_ball_stamp(goal: &str) -> Option<String> {
    goal.lines().find_map(stamp_id)
}

/// The ball id in one `Ball <id>: <title>` line, else `None`. A well-formed id
/// carries no whitespace (the compose emits a single token); that guard rejects
/// a prose line merely opening with the word `Ball` and an empty id.
fn stamp_id(line: &str) -> Option<String> {
    let (id, _title) = line.strip_prefix("Ball ")?.split_once(": ")?;
    (!id.is_empty() && !id.contains(char::is_whitespace)).then(|| id.to_owned())
}

/// The per-rung driver cwd (§3.4): `~` (bare / a not-yet-created ball), the given
/// directory (path), or the resolved work worktree (an existing ball). `worktree`
/// is the claim's cross-checked path (canonical or `<id>-<claimant>`); the
/// existing-ball arm prefers it, falling back to `~` only defensively (the
/// executor always resolves one). Belt-and-suspenders beside the goal-content
/// binding (§3.3).
pub(super) fn driver_cwd(payload: &Payload, home: &Path, worktree: Option<&Path>) -> PathBuf {
    match payload {
        Payload::Path { dir } => dir.clone(),
        Payload::Ball {
            ball: BallSpec::Existing { .. },
            ..
        } => worktree.unwrap_or(home).to_path_buf(),
        Payload::Bare
        | Payload::Ball {
            ball: BallSpec::New { .. },
            ..
        } => home.to_path_buf(),
    }
}

/// The composer's fire-time parameters as a [`Prepared`](super::Prepared): the
/// resolved name, its workspace path, the per-rung driver cwd, and the editable
/// goal prefill (identity stamped later). `worktree` is the resolved ball worktree
/// (§3.3, addendum): the planner passes the canonical formula, the executor the
/// claim's cross-checked path. The single source both [`super::plan`]'s `Prompt`
/// step and [`super::prepare`]'s return derive from.
pub(super) fn compose_prepared(inputs: &PlanInputs, worktree: Option<&Path>) -> super::Prepared {
    super::Prepared {
        name: inputs.name.clone(),
        workspace: inputs.workspace.clone(),
        cwd: driver_cwd(&inputs.payload, &inputs.home, worktree),
        goal: prefill(&inputs.payload, worktree),
    }
}

/// Resolve a [`Target`] to a name (§3.4): the existing name verbatim, or a fresh
/// mint over the injected RNG and the occupied set (names-root readdir +
/// `claimants`, §3.1). Pure — the caller ([`preview`] / [`resolve_name`]) decides
/// whether an exhausted pool is a silent no-preview or a logged abort.
pub(super) fn mint_name_of(
    target: &Target,
    yog_data_root: &Path,
    claimants: &[String],
    rng: &mut dyn Rng,
) -> Result<String, crate::names::MintError> {
    match target {
        Target::Existing { workspace } => Ok(leaf_name(workspace)),
        Target::Mint => {
            let root = crate::binding::names_root(yog_data_root);
            let occupied = crate::names::occupied(&root, claimants);
            crate::names::mint(rng, &occupied)
        }
    }
}