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::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.
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.
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.
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}