1//! The judge: Jev's decisions about what a child may hear and say, and which memories fit what she 2//! said. Jev makes typed decisions over options Whiskers writes down; it never writes text, so this 3//! port returns decisions, not prose. 4 5use std::fmt; 6 7use whiskers_core::{Age, Direction, Verdict}; 8 9use crate::error::Diagnostic; 10use crate::secrets::SecretName; 11 12/// Why no decision came back. None of these is an `Allow`: a caller that cannot get a decision treats 13/// the message as refused. 14#[derive(Clone, Debug, PartialEq, Eq)] 15pub enum JudgeError { 16 /// Jev has no credentials. Nothing was asked. 17 NotConfigured(SecretName), 18 /// Jev was asked and did not answer in time (or the thing that asks it is gone). 19 NoAnswer, 20 /// Jev answered with a failure. 21 Failed(Diagnostic), 22 /// There is no judge worded for a child of this age. 23 NoJudgeFor(Age), 24 /// The shortlist cannot be ranked in one question (too many candidates, or none). 25 CannotRank { candidates: usize, why: Diagnostic }, 26} 27 28impl fmt::Display for JudgeError { 29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 30 match self { 31 // The tablet and its logs have always read this phrase for "Jev is not set up here". 32 JudgeError::NotConfigured(_) => write!(f, "no Jev client"), 33 JudgeError::NoAnswer => write!(f, "no answer from Jev"), 34 JudgeError::Failed(d) => d.fmt(f), 35 JudgeError::NoJudgeFor(_) => write!(f, "no judge for that age"), 36 JudgeError::CannotRank { candidates, why } => write!(f, "cannot rank {candidates} candidates: {why}"), 37 } 38 } 39} 40 41impl std::error::Error for JudgeError {} 42 43/// Jev. 44/// 45/// Contract, for every adapter: 46/// 47/// - **Age, not name.** The judge is given a child's age and the words; never a name. 48/// - **Never an implied `Allow`.** If no decision was made the answer is an `Err`, not `Allow`. 49/// - **`rerank` returns one probability per candidate, in the candidates' order,** or an `Err`. A 50/// shortlist of fewer than two cannot be ranked (Jev's choice needs two options to choose between): 51/// that is `CannotRank`, not a made-up answer, and a caller with one candidate has nothing to rank. 52/// - **Never logs the words.** 53#[expect(async_fn_in_trait, reason = "a Worker's futures hold JavaScript values and cannot be Send, so no Send bound may be required here")] 54pub trait Judge { 55 /// Whether `text`, travelling `direction`, suits a child of `age`. 56 async fn check(&self, direction: Direction, age: Age, text: &str) -> Result<Verdict, JudgeError>; 57 58 /// How well each of `candidates` (memories) fits `query` (what she just said). 59 async fn rerank(&self, query: &str, candidates: &[String]) -> Result<Vec<f32>, JudgeError>; 60}