lmjtfy.git / apps / lmjtfy / src / diagram.rs

The rules as a picture: the Rete network, left to right, with what is known about this query marked on it. Pure: a network and what is known in, SVG out.

Columns: the facts, the tests on them (alpha nodes), the joins of tests (one column per depth), and the rules. A rule's first test is its own first join, so only joins of two or more tests are drawn as nodes.

What the picture has to say, beyond the wiring:

  • which facts came in one request (the frames around the facts);
  • what Jev actually answered (the number on each fact);
  • what, of everything known, a rule that holds stands on (filled), and what was learned and not needed (outlined);
  • the order the rules decided in (the number on a rule);
  • that a rule's effect teaches a fact (the line from a rule back round to the fact, which is the loop the engine runs).
18use std::collections::BTreeSet;
20use maud::{Markup, html};
21use rules::{Effect, End, Fact, Known, Network, Note, Source, State, Test, Then, Want};
22
23const ROW: i32 = 28;
24const TOP: i32 = 32;
25const BOX_H: i32 = 18;
26const FACT_X: i32 = 0;
27const FACT_W: i32 = 150;
28const ALPHA_X: i32 = 184;
29const ALPHA_W: i32 = 74;
30const JOIN_X: i32 = 294;
31const JOIN_STEP: i32 = 40;
32const JOIN_R: i32 = 8;
33const RULE_W: i32 = 262;

Room round the network for the lines that run from a rule back to the fact it teaches: one lane per such fact.

36const GUTTER: i32 = 30;
37const LANE: i32 = 9;

A frame's room above its first fact, for its caption, and round its sides.

39const CAPTION: i32 = 15;
40const PAD: i32 = 5;
42fn class(state: State, used: bool) -> &'static str {
43    match (state, used) {
44        (State::Holds, true) => "holds",
45        // Known to hold, and no rule that holds needed it.
46        (State::Holds, false) => "spare",
47        (State::Fails, _) => "fails",
48        (State::Waits, _) => "waits",
49    }
50}
51
52fn then(then: Then) -> &'static str {
53    match then {
54        Then::End(End::NotAQuestion) => "say no",
55        Then::Note(Note::Show(Fact::Yes)) => "show Jev's yes or no",
56        Then::Note(Note::Show(Fact::Degree)) => "show Jev's degree",
57        Then::Note(Note::Show(Fact::Chance)) => "show Jev's probability",
58        Then::Note(Note::Show(_)) => "show them",
59        Then::Note(Note::List) => "put it on the feed",
60        Then::Do(Effect::Draft(Want::Options)) => "ask the LLM for options",
61        Then::Do(Effect::Draft(Want::Scale)) => "ask the LLM for a scale",
62        Then::Do(Effect::Draft(Want::Split)) => "ask the LLM to split it",
63        Then::Do(Effect::Judge) => "ask Jev",
64    }
65}

Something on the diagram that can be clicked, in the playground.

68#[derive(Clone, Copy)]
69pub enum Clicked {
70    Fact(Fact),
71    Test(Test),
72}

The first free row at or after y in a column, so two nodes that want the same place do not sit on each other.

76fn place(taken: &mut BTreeSet<i32>, mut y: i32) -> i32 {
77    while !taken.insert(y) {
78        y += ROW / 2;
79    }
80    y
81}

The facts that arrive together, and what brings them: every Jev fact in one request (some share a question), and each effect's fact by its own call.

86fn frames() -> Vec<(String, Vec<Fact>)> {
87    let jev: Vec<Fact> = Fact::ALL.into_iter().filter(|fact| fact.source() == Source::Jev).collect();
88    let mut questions: Vec<&str> = jev.iter().map(|fact| ask::question_id(*fact)).collect();
89    questions.dedup();
90    let mut frames = vec![(format!("jev · {} questions, one request", questions.len()), jev)];
91    for fact in Fact::ALL {
92        match fact.source() {
93            Source::Jev => {}
94            Source::Llm => frames.push(("llm · 1 call".to_owned(), vec![fact])),
95            Source::JevAgain => frames.push(("jev · 1 request".to_owned(), vec![fact])),
96        }
97    }
98    frames
99}

The network with known marked on it. fired is the rules that decided a step, in order, as indices into the network's terminals. shown is what to print for a known fact: Jev's own number, where there is one. link is where a test leads when it is clicked, for the playground.

105pub fn rete(
106    network: &Network,
107    known: &Known,
108    fired: &[usize],
109    shown: impl Fn(Fact) -> Option<String>,
110    link: impl Fn(Clicked) -> Option<String>,
111) -> Markup {
112    let linked = |clicked: Clicked, node: Markup| match link(clicked) {
113        Some(href) => html! { a href=(href) { (node) } },
114        None => node,
115    };
116    // Alphas in fact order, so a fact's tests sit together beside it.
117    let mut order: Vec<usize> = (0..network.alphas.len()).collect();
118    order.sort_by_key(|&alpha| network.alphas[alpha].fact());
119    let mut alpha_y = vec![0; network.alphas.len()];
120    for (row, &alpha) in order.iter().enumerate() {
121        alpha_y[alpha] = TOP + row as i32 * ROW;
122    }
123    let fact_y = |fact: Fact| {
124        let ys: Vec<i32> =
125            (0..network.alphas.len()).filter(|&a| network.alphas[a].fact() == fact).map(|a| alpha_y[a]).collect();
126        (!ys.is_empty()).then(|| ys.iter().sum::<i32>() / ys.len() as i32)
127    };
128
129    let depth = network.joins.iter().map(|join| join.depth).max().unwrap_or(1);
130    let join_x = |d: usize| JOIN_X + (d as i32 - 2) * JOIN_STEP;
131    let rule_x = join_x(depth) + JOIN_STEP;
132    let width = rule_x + RULE_W;
133    let height = TOP + network.alphas.len() as i32 * ROW - ROW / 2;
134
135    // Where each join's output leaves from: an alpha's right edge for a
136    // rule's first test, the join's own node otherwise.
137    let mut columns: Vec<BTreeSet<i32>> = vec![BTreeSet::new(); depth + 1];
138    let mut out: Vec<(i32, i32)> = Vec::with_capacity(network.joins.len());
139    for join in &network.joins {
140        out.push(if join.depth == 1 {
141            (ALPHA_X + ALPHA_W, alpha_y[join.alpha])
142        } else {
143            (join_x(join.depth), place(&mut columns[join.depth], alpha_y[join.alpha]))
144        });
145    }
146    let mut rule_rows = BTreeSet::new();
147    let rule_y: Vec<i32> = network.terminals.iter().map(|t| place(&mut rule_rows, out[t.join].1)).collect();
148
149    let (used_joins, used_alphas) = network.used(known);
150    let alpha_class = |alpha: usize| class(network.alpha(alpha, known), used_alphas[alpha]);
151    let join_class = |join: usize| class(network.join(join, known), used_joins[join]);
152    let fact_class = |fact: Fact| match known.get(fact) {
153        None => "waits",
154        Some(_) if (0..network.alphas.len()).any(|a| network.alphas[a].fact() == fact && used_alphas[a]) => "holds",
155        Some(_) => "spare",
156    };
157
158    // Each effect's fact has a lane: out to the right of its rules, under
159    // the network, and up the left to the fact.
160    let taught: Vec<Fact> = Fact::ALL.into_iter().filter(|fact| fact.source() != Source::Jev).collect();
161    let lane = |fact: Fact| taught.iter().position(|taught| *taught == fact).map(|lane| 10 + lane as i32 * LANE);
162
163    html! {
164        div .rete {
165            svg viewBox={ (-GUTTER) " 0 " (width + 2 * GUTTER) " " (height + GUTTER) } role="img"
166                aria-label="The rules as a Rete network" {
167                // Frames and lines first, so the nodes are drawn over them.
168                @for (caption, facts) in frames() {
169                    @let ys: Vec<i32> = facts.iter().filter_map(|fact| fact_y(*fact)).collect();
170                    @if let (Some(top), Some(bottom)) = (ys.iter().min(), ys.iter().max()) {
171                        @let top = top - BOX_H / 2 - CAPTION;
172                        g .frame {
173                            rect x=(FACT_X - PAD) y=(top) width=(FACT_W + 2 * PAD)
174                                height=(bottom + BOX_H / 2 + PAD - top) {}
175                            text x=(FACT_X) y=(top + 11) { (caption) }
176                        }
177                    }
178                }
179                @for (alpha, test) in network.alphas.iter().enumerate() {
180                    @if let Some(y) = fact_y(test.fact()) {
181                        line class=(alpha_class(alpha)) x1=(FACT_X + FACT_W + PAD) y1=(y) x2=(ALPHA_X) y2=(alpha_y[alpha]) {}
182                    }
183                }
184                // The lines into a join that a fired rule stands on are drawn
185                // last, over the ones beside them: several joins share the
186                // line that brings what came before.
187                @for last in [false, true] {
188                    @for (index, join) in network.joins.iter().enumerate() {
189                        @if let (Some(left), true) = (join.left, used_joins[index] == last) {
190                            @let (x, y) = out[index];
191                            @let (from_x, from_y) = out[left];
192                            @let from_x = if network.joins[left].depth == 1 { from_x } else { from_x + JOIN_R };
193                            @let edge = if from_y < y { y - JOIN_R } else { y + JOIN_R };
194                            // What came before arrives from above or below;
195                            // the test joined here arrives from the left.
196                            path class=(class(network.join(left, known), last))
197                                d={ "M" (from_x) " " (from_y) " H" (x) " V" (edge) } {}
198                            line class=(class(network.alpha(join.alpha, known), last))
199                                x1=(ALPHA_X + ALPHA_W) y1=(alpha_y[join.alpha]) x2=(x - JOIN_R) y2=(y) {}
200                        }
201                    }
202                }
203                @for (terminal, y) in network.terminals.iter().zip(&rule_y) {
204                    @let (x, from_y) = out[terminal.join];
205                    @let x = if network.joins[terminal.join].depth == 1 { x } else { x + JOIN_R };
206                    line class=(join_class(terminal.join)) x1=(x) y1=(from_y) x2=(rule_x) y2=(*y) {}
207                    @if let Then::Do(effect) = terminal.then {
208                        @let fact = effect.teaches();
209                        @if let (Some(to), Some(lane)) = (fact_y(fact), lane(fact)) {
210                            @let tip = FACT_X - PAD;
211                            g class={ "back " (join_class(terminal.join)) } {
212                                path d={
213                                    "M" (width) " " (*y) " H" (width + lane) " V" (height + lane)
214                                    " H" (-lane - PAD) " V" (to) " H" (tip - 5)
215                                } {}
216                                polygon points={ (tip) "," (to) " " (tip - 6) "," (to - 3) " " (tip - 6) "," (to + 3) } {}
217                            }
218                        }
219                    }
220                }
221
222                @for fact in Fact::ALL {
223                    @if let Some(y) = fact_y(fact) {
224                        (linked(Clicked::Fact(fact), html! {
225                            g class={ "fact " (fact_class(fact)) } {
226                                rect x=(FACT_X) y=(y - BOX_H / 2) width=(FACT_W) height=(BOX_H) {}
227                                text x=(FACT_X + 6) y=(y + 4) { (fact.name()) }
228                                @if let Some(value) = known.get(fact) {
229                                    text x=(FACT_X + FACT_W - 6) y=(y + 4) text-anchor="end" {
230                                        (shown(fact).unwrap_or_else(|| value.label().to_owned()))
231                                    }
232                                }
233                            }
234                        }))
235                    }
236                }
237                @for (alpha, test) in network.alphas.iter().enumerate() {
238                    (linked(Clicked::Test(*test), html! {
239                        g class={ "alpha " (alpha_class(alpha)) } {
240                            rect x=(ALPHA_X) y=(alpha_y[alpha] - BOX_H / 2) width=(ALPHA_W) height=(BOX_H) {}
241                            text x=(ALPHA_X + 6) y=(alpha_y[alpha] + 4) { (test.label()) }
242                        }
243                    }))
244                }
245                @for (index, join) in network.joins.iter().enumerate() {
246                    @if join.depth > 1 {
247                        g class={ "join " (join_class(index)) } {
248                            circle cx=(out[index].0) cy=(out[index].1) r=(JOIN_R) {}
249                            text x=(out[index].0) y=(out[index].1 + 4) text-anchor="middle" { "&" }
250                        }
251                    }
252                }
253                @for (index, (terminal, y)) in network.terminals.iter().zip(&rule_y).enumerate() {
254                    @let nth = fired.iter().position(|fired| *fired == index);
255                    g class={ "rule " (join_class(terminal.join)) @if nth.is_some() { " fired" } } {
256                        rect x=(rule_x) y=(y - BOX_H / 2) width=(RULE_W) height=(BOX_H) {}
257                        text x=(rule_x + 6) y=(y + 4) { (terminal.name) " → " (then(terminal.then)) }
258                        @if let Some(nth) = nth {
259                            rect .nth x=(width - BOX_H) y=(y - BOX_H / 2) width=(BOX_H) height=(BOX_H) {}
260                            text .nth x=(width - BOX_H / 2) y=(y + 4) text-anchor="middle" { (nth + 1) }
261                        }
262                    }
263                }
264            }
265        }
266    }
267}

The rules that decided a step, in order, numbered, for the title bar.

270pub fn sequence(network: &Network, fired: &[usize]) -> String {
271    let steps: Vec<String> =
272        fired.iter().enumerate().map(|(nth, rule)| format!("{} {}", nth + 1, network.terminals[*rule].name)).collect();
273    steps.join(" · ")
274}

The rules on their own, as an SVG file: /rules.svg, for the READMEs to show the network the site really runs. Nothing is known, so every node waits. The page's diagram styles come with it, since an image is drawn without the page's stylesheet.

280pub fn standalone(network: &Network) -> String {
281    let drawn = rete(network, &Known::default(), &[], |_| None, |_| None).into_string();
282    let svg = drawn.trim_start_matches(r#"<div class="rete">"#).trim_end_matches("</div>");
283    let mut style = String::from(
284        "svg { --ink: #1e1e1e; --paper: #fefefe; --pink: #f386a1; --mono: \"JetBrains Mono\", ui-monospace, monospace; \
285         background: #fefefe; font: 11px var(--mono); }\n",
286    );
287    // The page's `.rete` rules, aimed at this file's own elements.
288    for rule in include_str!("page.css").lines().filter(|line| line.starts_with(".rete ") && !line.starts_with(".rete svg")) {
289        style.push_str(&rule.replace(".rete ", ""));
290        style.push('\n');
291    }
292    svg.replacen("<svg ", &format!(r#"<svg xmlns="http://www.w3.org/2000/svg" "#), 1).replacen(
293        r#"aria-label="The rules as a Rete network">"#,
294        &format!(r#"aria-label="The rules as a Rete network"><style>{style}</style>"#),
295        1,
296    )
297}
299#[cfg(test)]
300mod tests {
301    #[test]
302    fn the_rules_stand_alone_as_an_svg_file() {
303        let svg = super::standalone(&rules::Network::lmjtfy());
304        assert!(svg.starts_with(r#"<svg xmlns="http://www.w3.org/2000/svg" "#), "{}", &svg[..80]);
305        assert!(svg.ends_with("</svg>"));
306        assert!(svg.contains("<style>") && svg.contains("g.holds rect"), "the page's styles come with it");
307        assert!(!svg.contains(".rete"), "its selectors are its own");
308        assert!(!svg.contains("<a "), "nothing in it links");
309    }
310
311    use rules::{Kind, Next, Value};
312
313    use super::*;
314
315    fn none(_: Fact) -> Option<String> {
316        None
317    }
318
319    fn unlinked(_: Clicked) -> Option<String> {
320        None
321    }

What Jev's one request teaches, with reads the kinds that hold.

324    fn facts(reads: &[Kind], scale: bool) -> Known {
325        let mut known = Known::default();
326        known.learn(Fact::Answerable, Value::Bool(true));
327        known.learn(Fact::Several, Value::Bool(false));
328        known.learn(Fact::Scale, Value::Bool(scale));
329        known.learn(Fact::Yes, Value::Given);
330        known.learn(Fact::Degree, Value::Given);
331        known.learn(Fact::Chance, Value::Given);
332        known.learn(Fact::Fit, Value::Bool(true));
333        for kind in Kind::ALL {
334            known.learn(Fact::Reads(kind), Value::Bool(reads.contains(&kind)));
335        }
336        known
337    }

Does what the engine says until the query ends, as the Worker does.

340    fn run(network: &Network, known: &mut Known) -> Vec<usize> {
341        let mut fired = Vec::new();
342        loop {
343            let (by, next) = network.decide(known);
344            fired.extend(by);
345            match next {
346                Next::Do(effect) => known.learn(effect.teaches(), Value::Bool(true)),
347                _ => return fired,
348            }
349        }
350    }
352    #[test]
353    fn nothing_known_draws_every_node_waiting() {
354        let network = Network::lmjtfy();
355        let svg = rete(&network, &Known::default(), &[], none, unlinked).into_string();
356        assert_eq!(svg.matches(r#"<g class="alpha waits">"#).count(), network.alphas.len());
357        assert_eq!(svg.matches(r#"<g class="rule waits">"#).count(), network.terminals.len());
358        assert_eq!(svg.matches(r#"<g class="fact waits">"#).count(), Fact::ALL.len());
359        assert!(!svg.contains("holds") && !svg.contains("spare") && !svg.contains("fired"));
360    }
361
362    #[test]
363    fn the_jev_facts_are_framed_as_one_request() {
364        let svg = rete(&Network::lmjtfy(), &Known::default(), &[], none, unlinked).into_string();
365        // Eleven facts from eight questions: the four readings are one.
366        assert!(svg.contains("jev · 8 questions, one request"), "{svg}");
367        assert!(svg.contains("llm · 1 call"));
368        assert_eq!(svg.matches(r#"<g class="frame">"#).count(), 3);
369    }
370
371    #[test]
372    fn a_yes_or_no_question_lights_its_rule_and_fails_the_drafting_ones() {
373        let network = Network::lmjtfy();
374        let mut all = facts(&[Kind::Noul], false);
375        let fired = run(&network, &mut all);
376        let svg = rete(&network, &all, &fired, none, unlinked).into_string();
377        // Nothing decided a step: the answer came with the facts.
378        assert!(fired.is_empty());
379        // "answer yes or no" and "list it" hold.
380        assert_eq!(svg.matches(r#"<g class="rule holds">"#).count(), 2);
381        assert!(svg.contains("answer yes or no → show Jev"));
382        // several and scale were learned, and no rule that holds needed... several is needed (= no).
383        assert!(svg.contains(r#"<g class="fact spare">"#));
384    }
385
386    #[test]
387    fn a_pick_runs_three_steps_and_they_are_numbered() {
388        let network = Network::lmjtfy();
389        let mut all = facts(&[Kind::Choice], false);
390        let fired = run(&network, &mut all);
391        let svg = rete(&network, &all, &fired, none, unlinked).into_string();
392        assert_eq!(sequence(&network, &fired), "1 draft options · 2 judge the drafts");
393        assert_eq!(svg.matches(r#" fired">"#).count(), 2);
394        assert!(svg.contains(r#"<text class="nth""#));
395        // yes and degree were asked for, and no rule that holds used them.
396        assert!(svg.matches(r#"<g class="fact spare">"#).count() >= 2);
397    }
398
399    #[test]
400    fn facts_and_tests_are_links_when_they_are_given_one() {
401        let network = Network::lmjtfy();
402        let link = |clicked: Clicked| {
403            Some(match clicked {
404                Clicked::Fact(fact) => format!("/fact?{}", fact.name()),
405                Clicked::Test(test) => format!("/test?{}", test.fact().name()),
406            })
407        };
408        let svg = rete(&network, &Known::default(), &[], none, link).into_string();
409        assert_eq!(svg.matches(r#"<a href="/test?"#).count(), network.alphas.len());
410        assert_eq!(svg.matches(r#"<a href="/fact?"#).count(), Fact::ALL.len());
411    }
412
413    #[test]
414    fn a_fact_shows_jevs_number_when_there_is_one() {
415        let mut all = Known::default();
416        all.learn(Fact::Answerable, Value::Bool(true));
417        let shown = |fact: Fact| (fact == Fact::Answerable).then(|| "0.97".to_owned());
418        let svg = rete(&Network::lmjtfy(), &all, &[], shown, unlinked).into_string();
419        assert!(svg.contains(">0.97</text>"));
420        assert!(svg.contains(">answerable</text>"));
421    }
422
423    #[test]
424    fn every_rule_with_an_effect_has_a_line_back_to_the_fact_it_teaches() {
425        let network = Network::lmjtfy();
426        let svg = rete(&network, &Known::default(), &[], none, unlinked).into_string();
427        let effects = network.terminals.iter().filter(|t| matches!(t.then, Then::Do(_))).count();
428        assert_eq!(effects, 4);
429        assert_eq!(svg.matches(r#"<g class="back "#).count(), effects);
430    }
431
432    #[test]
433    fn no_two_nodes_in_a_column_share_a_place() {
434        let mut taken = BTreeSet::new();
435        assert_eq!(place(&mut taken, 20), 20);
436        assert_eq!(place(&mut taken, 20), 34);
437        assert_eq!(place(&mut taken, 20), 48);
438    }
439}