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::Note(Note::Unfinished) => "say it reads as unfinished",
61        Then::Do(Effect::Draft(Want::Options)) => "ask the LLM for options",
62        Then::Do(Effect::Draft(Want::Scale)) => "ask the LLM for a scale",
63        Then::Do(Effect::Draft(Want::Split)) => "ask the LLM to split it",
64        Then::Do(Effect::Judge) => "ask Jev",
65    }
66}

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

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

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.

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

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.

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

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.

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

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

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

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.

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

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

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

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

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