1//! The rules as a picture: the Rete network, left to right, with what is 2//! known about this query marked on it. Pure: a network and what is known 3//! in, SVG out. 4//! 5//! Columns: the facts, the tests on them (alpha nodes), the joins of tests 6//! (one column per depth), and the rules. A rule's first test is its own 7//! first join, so only joins of two or more tests are drawn as nodes. 8//! 9//! What the picture has to say, beyond the wiring: 10//! - which facts came in one request (the frames around the facts); 11//! - what Jev actually answered (the number on each fact); 12//! - what, of everything known, a rule that holds stands on (filled), and 13//! what was learned and not needed (outlined); 14//! - the order the rules decided in (the number on a rule); 15//! - that a rule's effect teaches a fact (the line from a rule back round 16//! to the fact, which is the loop the engine runs). 17 18use std::collections::BTreeSet; 19 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; 34/// Room round the network for the lines that run from a rule back to the 35/// fact it teaches: one lane per such fact. 36const GUTTER: i32 = 30; 37const LANE: i32 = 9; 38/// A frame's room above its first fact, for its caption, and round its sides. 39const CAPTION: i32 = 15; 40const PAD: i32 = 5; 41 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} 67 68/// 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} 74 75/// The first free row at or after `y` in a column, so two nodes that want 76/// 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} 83 84/// The facts that arrive together, and what brings them: every Jev fact in 85/// one request (some share a question), and each effect's fact by its own 86/// 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} 101 102/// The network with `known` marked on it. `fired` is the rules that decided 103/// a step, in order, as indices into the network's terminals. `shown` is 104/// what to print for a known fact: Jev's own number, where there is one. 105/// `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} 269 270/// 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} 276 277/// The rules on their own, as an SVG file: `/rules.svg`, for the READMEs to 278/// show the network the site really runs. Nothing is known, so every node 279/// waits. The page's diagram styles come with it, since an image is drawn 280/// 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} 299 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 } 323 324 /// 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 } 339 340 /// 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 } 352 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}