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