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}