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.
A frame's room above its first fact, for its caption, and round its sides.
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.
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.
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.
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}