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::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} 66 67/// 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} 73 74/// The first free row at or after `y` in a column, so two nodes that want 75/// 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} 82 83/// The facts that arrive together, and what brings them: every Jev fact in 84/// one request (some share a question), and each effect's fact by its own 85/// 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} 100 101/// The network with `known` marked on it. `fired` is the rules that decided 102/// a step, in order, as indices into the network's terminals. `shown` is 103/// what to print for a known fact: Jev's own number, where there is one. 104/// `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} 268 269/// 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} 275 276/// The rules on their own, as an SVG file: `/rules.svg`, for the READMEs to 277/// show the network the site really runs. Nothing is known, so every node 278/// waits. The page's diagram styles come with it, since an image is drawn 279/// 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} 298 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 } 322 323 /// 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 } 338 339 /// 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 } 351 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}