The judge: Jev's decisions about what a child may hear and say, and which memories fit what she said. Jev makes typed decisions over options Whiskers writes down; it never writes text, so this port returns decisions, not prose.

5use std::fmt;
7use whiskers_core::{Age, Direction, Verdict};
8
9use crate::error::Diagnostic;
10use crate::secrets::SecretName;

Why no decision came back. None of these is an Allow: a caller that cannot get a decision treats the message as refused.

14#[derive(Clone, Debug, PartialEq, Eq)]
15pub enum JudgeError {

Jev has no credentials. Nothing was asked.

17    NotConfigured(SecretName),

Jev was asked and did not answer in time (or the thing that asks it is gone).

19    NoAnswer,

Jev answered with a failure.

21    Failed(Diagnostic),

There is no judge worded for a child of this age.

23    NoJudgeFor(Age),

The shortlist cannot be ranked in one question (too many candidates, or none).

25    CannotRank { candidates: usize, why: Diagnostic },
26}
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 {}

Jev.

Contract, for every adapter:

  • Age, not name. The judge is given a child's age and the words; never a name.
  • Never an implied Allow. If no decision was made the answer is an Err, not Allow.
  • rerank returns one probability per candidate, in the candidates' order, or an Err. A shortlist of fewer than two cannot be ranked (Jev's choice needs two options to choose between): that is CannotRank, not a made-up answer, and a caller with one candidate has nothing to rank.
  • 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 {

Whether text, travelling direction, suits a child of age.

56    async fn check(&self, direction: Direction, age: Age, text: &str) -> Result<Verdict, JudgeError>;

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}