1//! The part of the page that says where Whiskers uses Jev, with a diagram of a turn and real 2//! request bodies. Every fact on it is read from the code that does the using (`site::jev`, 3//! `whiskers_core::journey`, `whiskers_judge`), so editing the guard changes the page, and the 4//! tests below hold it to that. 5 6use jev_ui::components::{icons, window}; 7use jev_ui::flow::spine; 8use jev_ui::wire::{Bar, Panel, bars, exchange}; 9use crate::pic::{Row, facts}; 10use maud::{Markup, html}; 11use site::config::Pic; 12use site::jev::{EXAMPLE_AGE, Example, examples, rows}; 13use whiskers_core::journey::{JEV_USES, NOT_JEV, TURN}; 14use whiskers_core::{Direction, RefusalKind, Verdict}; 15use whiskers_judge::{MIN_SUITABLE, MODEL, Topic}; 16 17/// One icon for each thing Jev is not used for, in `NOT_JEV`'s order; the array's length is its 18/// own, so a new entry does not compile until it has one. 19const NOT_JEV_ICONS: [Pic; NOT_JEV.len()] = [Pic::Claude, Pic::Microphone, Pic::Picture, Pic::Magnifier, Pic::Clock, Pic::Book]; 20 21fn verdict_words(verdict: &Verdict) -> String { 22 match verdict { 23 Verdict::Allow => "Allowed.".to_owned(), 24 Verdict::Refuse { reason, kind: RefusalKind::NeedsAGrownUp } => format!("Refused ({reason}). She is sent to a grown-up and the parents' log shows it first."), 25 Verdict::Refuse { reason, kind: RefusalKind::OffLimits } => format!("Refused ({reason}). The fixed line is spoken instead."), 26 } 27} 28 29fn kind_words(kind: RefusalKind) -> &'static str { 30 match kind { 31 RefusalKind::NeedsAGrownUp => "sends her to a grown-up", 32 RefusalKind::OffLimits => "a fixed line steers elsewhere", 33 } 34} 35 36fn direction_words(direction: Direction) -> &'static str { 37 match direction { 38 Direction::FromChild => "what she said", 39 Direction::ToChild => "what the model wrote", 40 } 41} 42 43fn example(n: usize, e: &Example) -> Markup { 44 let head = format!("POST {}\nauthorization: Bearer [the service's key]\ncontent-type: application/json", jev_protocol::ENDPOINT); 45 html! { 46 div .case { 47 p { strong { (direction_words(e.direction)) ": " } "\u{201c}" (e.text) "\u{201d}" } 48 p .fine { (e.why) } 49 (exchange(&icons::TOOL, &format!("jev \u{b7} check {n} \u{b7} 2 questions"), "", n == 1, &[ 50 Panel { label: "request", head: Some(&head), body: &e.request }, 51 Panel { label: "response, written for this page", head: None, body: &e.response }, 52 ])) 53 p .verdict { (verdict_words(&e.verdict)) } 54 } 55 } 56} 57 58pub fn section() -> Markup { 59 let examples = examples(); 60 let diagram = rows(); 61 let summary = "A turn, step by step: where Jev is asked, and every way a turn can end in a fixed line instead of an answer."; 62 let bar_labels: Vec<String> = examples.iter().map(|e| format!("{} ({})", e.text, e.topic.label())).collect(); 63 let bar_rows: Vec<Bar<'_>> = examples 64 .iter() 65 .zip(&bar_labels) 66 .map(|(e, label)| Bar { label, p: e.suitable, won: matches!(e.verdict, Verdict::Allow) }) 67 .collect(); 68 html! { 69 div .stack { 70 section #jev aria-labelledby="jev-h" { 71 (window(Some(&icons::JEV), html! { h2 #jev-h { "Where Jev is used" } }, None, true, html! { 72 p { 73 "Jev is TypeSafe\u{2019}s System One model. It does not write: it answers typed questions with a probability. " 74 "In Whiskers it is the guard, and only the guard. These are all the places the code asks it something." 75 } 76 ol .uses { 77 @for u in JEV_USES { 78 li { 79 strong { (u.id) } 80 p { (u.asks) } 81 p .fine { "If Jev cannot answer: " (u.on_failure) "." } 82 } 83 } 84 } 85 p .fine { "A check that cannot be made is never an allow. A failed or throttled check is a refusal." } 86 })) 87 } 88 section aria-labelledby="turn-h" { 89 (window(Some(&icons::TOOL), html! { h2 #turn-h { "One turn, from voice to voice" } }, None, true, html! { 90 p { "Pink steps ask Jev. Each box on the right is a way the turn can end early, what the child hears instead, and what the parents\u{2019} log shows." } 91 (spine("One turn of Whiskers", summary, &diagram)) 92 p .fine { 93 "This is drawn from the list the core keeps of its own steps (" code { "whiskers-core/src/journey.rs" } "), and a test runs the real turn into every one of the " 94 (TURN.iter().map(|s| s.exits.len()).sum::<usize>()) " endings shown, so the drawing cannot show a step or an ending the code does not have." 95 } 96 })) 97 } 98 section aria-labelledby="check-h" { 99 (window(Some(&icons::JEV), html! { h2 #check-h { "One check: two questions, one decision" } }, None, true, html! { 100 p { 101 "Every message, in either direction, is put to Jev as two typed questions in a single request. A yes-or-no: is it entirely suitable for a child of her age? " 102 "And a choice: what is it mainly about? The first returns one probability, the second a probability for each of nine topics. The age is in the question; her name never is." 103 } 104 div .topics role="table" aria-label="The nine topics" { 105 @for (topic, label, what) in Topic::ALL { 106 div role="row" .topic { 107 span role="cell" .t { (label) } 108 span role="cell" { (what) } 109 span role="cell" .fine { 110 @if topic == Topic::Ordinary { "allowed, if sure enough" } 111 @else { (kind_words(topic.kind())) } 112 } 113 } 114 } 115 } 116 p { 117 "The decision is a small pure function, and nothing else decides: a message is allowed only if its topic is " strong { "ordinary" } 118 " and Jev is at least " strong { (MIN_SUITABLE) } " sure it is suitable (the dashed tick below). Anything else is refused. " 119 "What she says about being hurt or unsafe, or being asked to keep a secret from her parents, is refused in a way that sends her to a grown-up; " 120 "what the model writes that touches those topics is simply withheld." 121 } 122 (bars(&bar_rows, Some((MIN_SUITABLE, "allowed from here, if the topic is ordinary")))) 123 p .fine { "The three examples below, on that rule. The topic order is part of the question: Jev is known to favour earlier options, so it is not changed casually." } 124 })) 125 } 126 section aria-labelledby="ex-h" { 127 (window(Some(&icons::TOOL), html! { h2 #ex-h { "Three checks, with the real requests" } }, None, true, html! { 128 p { 129 "Sentences written for this page, asked for a " (EXAMPLE_AGE) "-year-old. Each request below is the exact body the guard builds, for model " code { (MODEL) } ", printed with whitespace added and nothing else. " 130 strong { "The responses are not recorded: " } 131 "this page never calls Jev, so the answers were written by hand in the shape Jev returns, with figures chosen for the example. They were checked by the same verifier the guard uses, which refuses a response that does not answer exactly the questions asked, and the verdict is the guard\u{2019}s own." 132 } 133 @for (i, e) in examples.iter().enumerate() { (example(i + 1, e)) } 134 })) 135 } 136 section aria-labelledby="not-h" { 137 (window(Some(&icons::JEV), html! { h2 #not-h { "Where Jev is not used" } }, None, true, html! { 138 (facts("Where Jev is not used", NOT_JEV.iter().zip(NOT_JEV_ICONS).map(|((what, why), icon)| Row { icon, title: html! { (what) }, text: html! { (why) } }).collect())) 139 p .fine { "Jev, and the service that holds its key, can be down or throttled. The tablet never holds the key. In that case no message is allowed through: she hears a fixed, kind line, and the parents\u{2019} log records the failure." } 140 })) 141 } 142 } 143 } 144} 145 146#[cfg(test)] 147mod tests { 148 use super::*; 149 use jev_ui::wire::printed_json; 150 151 fn page() -> String { 152 section().into_string() 153 } 154 155 fn unescape(text: &str) -> String { 156 text.replace("<", "<").replace(">", ">").replace(""", "\"").replace("'", "'").replace("&", "&") 157 } 158 159 #[test] 160 fn each_printed_request_and_response_is_the_body_the_code_holds() { 161 let html = page(); 162 for (i, e) in examples().iter().enumerate() { 163 let block = html.split("class=\"case\"").nth(i + 1).expect("a block per example"); 164 let block = block.split("class=\"case\"").next().unwrap(); 165 // The two JSON panels of this block, in order: request, then response. 166 let mut jsons = block.split("<pre class=\"json\">").skip(1).map(|part| part.split("</pre>").next().unwrap()); 167 let request = jsons.next().expect("a printed request"); 168 let response = jsons.next().expect("a printed response"); 169 let sent: serde_json::Value = serde_json::from_str(&e.request).unwrap(); 170 assert_eq!(printed_json(request).expect("the request prints as JSON"), sent); 171 // Nothing but whitespace was added: the keys are in the order the code sent them. 172 assert_eq!(serde_json::to_string(&printed_json(request).unwrap()).unwrap(), e.request); 173 assert_eq!(serde_json::to_string(&printed_json(response).unwrap()).unwrap(), e.response); 174 } 175 } 176 177 #[test] 178 fn the_page_lists_the_guards_topics_and_threshold_and_cannot_list_others() { 179 let html = unescape(&page()); 180 for (_, label, what) in Topic::ALL { 181 assert!(html.contains(label), "{label}"); 182 assert!(html.contains(what), "{label}'s description"); 183 } 184 assert_eq!(page().matches("class=\"topic\"").count(), Topic::ALL.len()); 185 assert!(html.contains(&format!("at least <strong>{MIN_SUITABLE}</strong>"))); 186 assert!(html.contains(&format!("left: {:.1}%", MIN_SUITABLE * 100.0)), "the tick is where the guard cuts"); 187 } 188 189 #[test] 190 fn the_diagram_and_the_list_of_uses_are_the_codes() { 191 let html = page(); 192 for step in TURN { 193 assert!(html.contains(&format!("data-id=\"{}\"", step.id)), "{}", step.id); 194 } 195 assert_eq!(html.matches("class=\"exit\"").count(), TURN.iter().map(|s| s.exits.len()).sum::<usize>()); 196 assert_eq!(html.matches("class=\"uses\"").count(), 1); 197 for u in JEV_USES { 198 assert!(unescape(&html).contains(u.asks), "{}", u.id); 199 } 200 for (what, _) in NOT_JEV { 201 assert!(html.contains(what), "{what}"); 202 } 203 } 204 205 #[test] 206 fn it_says_plainly_where_the_responses_came_from_and_what_a_failure_does() { 207 let html = unescape(&page()); 208 assert!(html.contains("The responses are not recorded")); 209 assert!(html.contains("never calls Jev")); 210 assert!(html.contains("A check that cannot be made is never an allow")); 211 assert!(html.contains("she hears a fixed, kind line")); 212 } 213 214 #[test] 215 fn the_example_sentences_hold_no_name_no_address_and_nothing_graphic() { 216 // The sentences and their captions only: the request also carries the guard's own topic 217 // descriptions, which name what it looks for. 218 for e in examples() { 219 let said = format!("{} {}", e.text, e.why).to_lowercase(); 220 for bad in ["http", "@", "street", "phone", "kill", "blood", "gun", "hurt", "die"] { 221 assert!(!said.contains(bad), "{bad}"); 222 } 223 } 224 } 225}