whiskers.git / web / worker / src / explain.rs

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("&lt;", "<").replace("&gt;", ">").replace("&quot;", "\"").replace("&#39;", "'").replace("&amp;", "&")
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}