The option that means "none of these pictures fits". Never an icon's name (those are lowercase words).
5pub const NONE_FITS: &str = "NONE";
Jev must put at least this much on one picture for it to be used. A first guess (2026-10-05), to be tuned from what the choices look like on real facts: with twenty options a calibrated top choice is seldom above 0.9, and a wrong picture on a card costs more than a generic star does.
10pub const MIN_ICON_CONFIDENCE: f64 = 0.3;
Which picture shows a memory best, as one Jev Choice over a shortlist, with a way out when none does.
18impl IconChoice {
Needs at least one name; the question always adds [NONE_FITS]. options are (name, what it shows),
best candidate first.
21 pub fn new(options: &[(String, String)]) -> Result<Self, ProtocolError> { 22 debug!("building an icon question over {} pictures", options.len()); 23 let mut questions = Questions::new(); 24 let key = questions.choice( 25 "picture", 26 Choice::new( 27 Json::text( 28 "The state holds one thing a toy cat remembers about a young child. Which picture shows what it is \ 29 about best? If none of the pictures is about it, pick NONE.", 30 ), 31 options 32 .iter() 33 .map(|(name, what)| (name.clone(), Some(Json::text(what)))) 34 .chain(std::iter::once((NONE_FITS.to_owned(), Some(Json::text("none of the pictures is about it"))))), 35 )?, 36 )?; 37 Ok(Self { questions, key }) 38 }
40 pub fn state(memory: &str) -> Json { 41 let json = serde_json::json!({ "memory": memory }); 42 trace!("icon state: memory of {} chars", memory.len()); 43 Json::canonical(&json.to_string()).unwrap_or_else(|e| { 44 warn!("icon state is not canonical JSON ({e:?}); sending it as plain text"); 45 Json::text(memory) 46 }) 47 }
The chosen picture's name, or None when Jev chose NONE or was not sure enough of any.
The decision, apart from asking: the top option, unless it is NONE or too unsure.
57pub fn decide<'a>(choice: &str, probabilities: impl Iterator<Item = (&'a str, f64)>) -> Option<String> { 58 if choice == NONE_FITS { 59 return None; 60 } 61 let p = probabilities.filter(|(l, _)| *l == choice).map(|(_, p)| p).next().unwrap_or(0.0); 62 (p >= MIN_ICON_CONFIDENCE).then(|| choice.to_owned()) 63}
65#[cfg(test)] 66mod tests { 67 use super::*; 68 69 fn options(n: usize) -> Vec<(String, String)> { 70 (0..n).map(|i| (format!("icon-{i}"), format!("a picture of icon {i}"))).collect() 71 } 72 73 #[test] 74 fn the_question_builds_for_one_to_many_pictures() { 75 assert!(IconChoice::new(&options(1)).is_ok()); 76 assert!(IconChoice::new(&options(20)).is_ok()); 77 assert!(IconChoice::new(&options(0)).is_err(), "NONE alone is not a choice"); 78 } 79 80 #[test] 81 fn none_and_an_unsure_top_choice_are_no_picture() { 82 let p = |v: &[(&'static str, f64)]| v.to_vec().into_iter(); 83 assert_eq!(decide("cat", p(&[("cat", 0.8), ("dog", 0.1)])), Some("cat".into())); 84 assert_eq!(decide(NONE_FITS, p(&[(NONE_FITS, 0.9), ("cat", 0.05)])), None); 85 assert_eq!(decide("cat", p(&[("cat", 0.2), ("dog", 0.19)])), None, "too unsure"); 86 assert_eq!(decide("cat", p(&[])), None, "no probability, no picture"); 87 } 88}