jany 0.3.0

jev x any command: turn loosely ordered words into a command line, defined per command by a schema
//! The rules → (jev) → repair → assemble flow. Used by both main and `jany --test`.

use crate::assemble::{self, Assembled};
use crate::config::Config;
use crate::error::JanyError;
use crate::jev::{self, Answers, Oracle};
use crate::output::JevInfo;
use crate::questions;
use crate::repair;
use crate::rules;
use crate::schema::Schema;
use crate::token::Token;
use std::time::Instant;

pub struct Run {
    pub tokens: Vec<Token>,
    pub jev: Option<JevInfo>,
    pub out: Assembled,
}

/// With `oracle` = None, jev is never called (any unresolved word is Unresolved).
pub fn run(schema: &Schema, cfg: &Config, words: &[String], passthrough: &[String], oracle: Option<&dyn Oracle>, defaults_override: Option<&toml::Table>) -> Result<Run, JanyError> {
    let cmd_cfg = cfg.cmd.get(&schema.command.name).cloned().unwrap_or_default();
    let words = crate::config::expand_aliases(&cmd_cfg.aliases, words);
    let mut tokens = rules::classify(schema, &words);
    let mut info = None;
    let mut answers = Answers::new();

    if tokens.iter().any(|t| !t.resolved()) {
        let Some(oracle) = oracle else {
            let bad: Vec<&str> = tokens.iter().filter(|t| !t.resolved()).map(|t| t.text.as_str()).collect();
            return Err(JanyError::Unresolved(format!("{} (jev disabled)", bad.join(", "))));
        };
        let built = questions::build(schema, &tokens);
        let n = built.questions.len();
        let t0 = Instant::now();
        let res = oracle.decide(built.state, built.questions)?;
        info = Some(JevInfo { model: res.model.clone(), questions: n, ms: t0.elapsed().as_millis(), usage: res.usage.clone() });
        answers = res.answers;
        questions::apply(schema, &mut tokens, &answers);
    }
    repair::repair(schema, &mut tokens, &answers);

    let defaults = assemble::merge_defaults(&schema.defaults, defaults_override.unwrap_or(&cmd_cfg.defaults));
    let out = assemble::assemble(schema, &tokens, passthrough, &answers, &defaults)?;
    Ok(Run { tokens, jev: info, out })
}

pub fn oracle_from_config(cfg: &Config) -> Result<jev::client::OpenRouter, JanyError> {
    let api_key = std::env::var("OPENROUTER_API_KEY")
        .ok()
        .filter(|k| !k.is_empty())
        .or_else(|| cfg.jev.api_key.clone())
        .ok_or_else(|| JanyError::Jev("no API key. run `jany --setup` or set OPENROUTER_API_KEY (needed to interpret ambiguous words)".into()))?;
    Ok(jev::client::OpenRouter {
        api_key,
        model: cfg.jev.model.clone(),
        timeout: std::time::Duration::from_millis(cfg.jev.timeout_ms),
        max_retries: 3,
    })
}