1//! The rules that decide, drawn from the rules that decide. 2//! 3//! Nothing about a rule is written here. The three networks are the plugin's own 4//! (`jevhooks_rules`, the crate the daemon runs), and `rete-draw` draws a network, so the page 5//! cannot show a rule the plugin does not have, or miss one it has. What is written here is a few 6//! worked cases for each: an answer Jev could give, read through the plugin's own thresholds, and 7//! run through the network. How a case ends is what the network said, never a word typed beside it. 8 9use jevhooks_events::Tier; 10use jevhooks_rules::command::{self, Act, Command}; 11use jevhooks_rules::model::{self, Model}; 12use jevhooks_rules::stop::{self, Stop}; 13use maud::{Markup, html}; 14use rete::{Domain, Known, Network, Next, Then}; 15use rete_draw::Legend; 16 17/// What a rule does, as its label ends: the domain's own name for it. 18fn then<D: Domain>(then: Then<D>) -> String { 19 match then { 20 Then::Do(effect) => D::effect_name(effect), 21 Then::End(end) => D::end_name(end), 22 Then::Note(note) => D::note_name(note), 23 } 24} 25 26/// One frame, round the facts that are Jev's to give: they are asked for together. 27fn legend<D: Domain>() -> Legend<D> { 28 let asked: Vec<D::Fact> = D::facts().iter().copied().filter(|fact| D::asked_for(*fact)).collect(); 29 Legend { frames: vec![("Jev, in one request".to_owned(), asked)], then: then::<D> } 30} 31 32/// A network run to its end from `known`, each effect answered by `perform`: the rules that 33/// decided a step, in order, what is known at the end, and how it ended. 34pub fn run<D: Domain>(network: &Network<D>, mut known: Known<D>, perform: impl Fn(D::Effect) -> D::Value) -> (Vec<usize>, Known<D>, Next<D>) { 35 let mut fired = Vec::new(); 36 loop { 37 let (rule, next) = network.decide(&known); 38 fired.extend(rule); 39 match next { 40 Next::Do(effect) => known.learn(D::teaches(effect), perform(effect)), 41 end => return (fired, known, end), 42 } 43 } 44} 45 46/// How a run ended, in the words on a rule's label. 47fn ended<D: Domain>(next: &Next<D>) -> String { 48 match next { 49 Next::End(end) => D::end_name(*end), 50 Next::Ask(_) => "Jev is asked".to_owned(), 51 Next::Do(effect) => D::effect_name(*effect), 52 Next::Done => "nothing more to do".to_owned(), 53 } 54} 55 56/// One worked case: what was judged, how it ended, a sentence, and the network with it marked on. 57struct Case { 58 what: &'static str, 59 ended: String, 60 says: &'static str, 61 drawn: Markup, 62} 63 64fn case<D: Domain>( 65 network: &Network<D>, 66 what: &'static str, 67 says: &'static str, 68 known: Known<D>, 69 perform: impl Fn(D::Effect) -> D::Value, 70 shown: impl Fn(D::Fact) -> Option<String>, 71) -> Case { 72 let (fired, known, next) = run(network, known, perform); 73 Case { what, ended: ended(&next), says, drawn: rete_draw::rete(network, &known, &fired, &legend::<D>(), shown, |_| None) } 74} 75 76fn percent(probability: f64) -> String { 77 format!("{:.0}%", probability * 100.0) 78} 79 80/// A command, with `room_mb` megabytes to spare, that may or may not wait longer. 81fn command_case(network: &Network<Command>, what: &'static str, says: &'static str, asked: &command::Asked, room_mb: f64, may_wait: bool) -> Case { 82 let mut known = asked.known.clone(); 83 known.learn(command::Fact::Patience, command::patience(may_wait)); 84 let answers = &asked.answers; 85 case( 86 network, 87 what, 88 says, 89 known, 90 |command::Effect::Measure| command::room(answers.needs_mb(), Some(room_mb)), 91 |fact| { 92 Some(match fact { 93 command::Fact::Consequential => percent(answers.consequential()), 94 command::Fact::Ordinary => percent(answers.ordinary()), 95 command::Fact::HardToUndo | command::Fact::EasyToUndo => format!("{:.1}", answers.undo), 96 command::Fact::Load => format!("{:.1}", answers.load), 97 command::Fact::Room => format!("{:.1} GB", room_mb / 1024.0), 98 command::Fact::Patience => return None, 99 }) 100 }, 101 ) 102} 103 104fn command_cases() -> Vec<Case> { 105 let network = command::network(); 106 vec![ 107 command_case( 108 &network, 109 "cargo test", 110 "Jev is sure it builds or tests, it is undone by one command, and there is room for it. It runs, and you are not asked.", 111 &command::Asked::made_up(&[(Act::Build, 0.97), (Act::Edit, 0.02), (Act::Read, 0.01)], 0.9, 1.8), 112 8000.0, 113 true, 114 ), 115 command_case( 116 &network, 117 "cargo build --release, with 1 GB to spare", 118 "Nothing wrong with the command; the machine has no room for it yet. It waits, and is let through when a build ahead of it finishes.", 119 &command::Asked::made_up(&[(Act::Build, 0.98), (Act::Edit, 0.02)], 1.0, 2.6), 120 1000.0, 121 true, 122 ), 123 command_case( 124 &network, 125 "git push --force origin main", 126 "One consequential act, and Jev is sure of it. You are asked, with the reason. It would not have waited for memory either: the rules on what a command does come before the rules on room.", 127 &command::Asked::made_up(&[(Act::History, 0.93), (Act::Remote, 0.06), (Act::Edit, 0.01)], 2.4, 0.4), 128 8000.0, 129 true, 130 ), 131 command_case( 132 &network, 133 "a long command that reads some figures and calls another program", 134 "A real answer from the log: 56% only reads, 39% changes the machine. Not sure enough to wave through, not sure enough to stop. Jev has no opinion, and your own permission rules decide, as if the plugin were not there.", 135 &command::Asked::made_up(&[(Act::Read, 0.56), (Act::System, 0.39), (Act::Build, 0.05)], 0.4, 1.3), 136 8000.0, 137 true, 138 ), 139 ] 140} 141 142fn stop_case(network: &Network<Stop>, what: &'static str, says: &'static str, known: Known<Stop>, stopped_early: Option<f64>) -> Case { 143 case(network, what, says, known, |never| match never {}, move |fact| match fact { 144 stop::Fact::StoppedEarly => stopped_early.map(percent), 145 _ => None, 146 }) 147} 148 149fn stop_cases() -> Vec<Case> { 150 let network = stop::network(); 151 let done = stop::Asked::made_up(0.03); 152 let early = stop::Asked::made_up(0.93); 153 vec![ 154 stop_case( 155 &network, 156 "\u{201c}I added the flag. All 41 tests pass.\u{201d}", 157 "The work asked for was done. The turn ends.", 158 done.known.clone(), 159 Some(done.stopped_early), 160 ), 161 stop_case( 162 &network, 163 "\u{201c}Next I will add the flag and run the tests.\u{201d}", 164 "It says what it will do and has not done it, and names no obstacle. The turn is sent back to the assistant with the reason.", 165 early.known.clone(), 166 Some(early.stopped_early), 167 ), 168 stop_case( 169 &network, 170 "the end that follows a refusal", 171 "The turn was sent back once already. Whatever it says now, it ends: one refusal must never become a loop. Jev is not asked, so the fact that is Jev's is never learned.", 172 stop::before(true, false, true), 173 None, 174 ), 175 ] 176} 177 178fn model_case(network: &Network<Model>, what: &'static str, says: &'static str, known: Known<Model>) -> Case { 179 case(network, what, says, known, |never| match never {}, |_| None) 180} 181 182fn model_cases() -> Vec<Case> { 183 let network = model::network(); 184 vec![ 185 model_case( 186 &network, 187 "\u{201c}what branch am I on\u{201d}", 188 "A lookup, and Jev is sure. The turn goes to the smallest model.", 189 model::Asked::made_up([1.0, 0.0, 0.0, 0.0]).known, 190 ), 191 model_case( 192 &network, 193 "a prompt Jev puts at 60% a lookup, 30% routine, 10% judgment", 194 "The likeliest answer is a lookup, and the smallest model would be too small four times in ten. The cheapest model that is 75% likely to be enough is the next one up.", 195 model::Asked::made_up([0.6, 0.3, 0.1, 0.0]).known, 196 ), 197 model_case( 198 &network, 199 "any prompt, with model choice switched off", 200 "Which model you use is yours to hand over. Until you do, the session's own model stands and Jev is not asked.", 201 model::before(false), 202 ), 203 ] 204} 205 206/// The cases of one judgment, the first open. 207fn cases(cases: &[Case]) -> Markup { 208 html! { 209 div .cases { 210 @for (index, case) in cases.iter().enumerate() { 211 details .case open[index == 0] { 212 summary { span .what { (case.what) } " " span .chip.judged { (case.ended) } } 213 p { (case.says) } 214 (case.drawn) 215 } 216 } 217 } 218 } 219} 220 221/// How many rules, facts and questions a judgment has, counted from the network that runs. 222fn counts<D: Domain>(network: &Network<D>) -> (usize, usize, usize) { 223 let asked = D::facts().iter().filter(|fact| D::asked_for(**fact)).count(); 224 (network.terminals.len(), D::facts().len(), asked) 225} 226 227/// One judgment: its name, what it is, and its cases. 228pub struct Judgment { 229 pub title: &'static str, 230 pub says: Markup, 231 pub cases: Markup, 232} 233 234/// The three judgments, each with its network drawn for a few cases. 235pub fn judgments() -> [Judgment; 3] { 236 let (rules, facts, asked) = counts(&command::network()); 237 let command = Judgment { 238 title: "A command, before it runs", 239 says: html! { 240 (rules) " rules over " (facts) " facts. " (asked) " of the facts are read from Jev's three answers, each by a threshold; the other two are the machine's: whether there is room in memory, which is measured when the first rule says to, and whether the command may still wait for it." 241 }, 242 cases: cases(&command_cases()), 243 }; 244 let (rules, facts, asked) = counts(&stop::network()); 245 let stop = Judgment { 246 title: "A turn, before it ends", 247 says: html! { 248 (rules) " rules over " (facts) " facts, and only " (asked) " of them is Jev's. The other three are known before anything is asked, and when one of them settles it, Jev is not asked at all." 249 }, 250 cases: cases(&stop_cases()), 251 }; 252 let (rules, facts, asked) = counts(&model::network()); 253 let model = Judgment { 254 title: "A prompt, before its turn starts", 255 says: html! { 256 (rules) " rules over " (facts) " facts, " (asked) " of them Jev's: the cheapest of " (Tier::ALL.len()) " models that is 75% likely to be enough for the prompt. Off unless you switch it on." 257 }, 258 cases: cases(&model_cases()), 259 }; 260 [command, stop, model] 261} 262 263#[cfg(test)] 264mod tests { 265 use jevhooks_events::Verdict; 266 267 use super::*; 268 269 /// The drawings are of the plugin's own rules: every rule of every network is on the page by 270 /// its name, so a rule added to the plugin is on the page the next time it is built. 271 #[test] 272 fn every_rule_that_runs_is_drawn() { 273 let page: String = judgments().iter().map(|judgment| judgment.cases.clone().into_string()).collect(); 274 let names = command::RULES.iter().map(|rule| rule.name).chain(stop::RULES.iter().map(|rule| rule.name)).chain(model::RULES.iter().map(|rule| rule.name)); 275 for name in names { 276 assert!(page.contains(name), "{name}"); 277 } 278 } 279 280 /// The command cases are chosen to end every way a command can: allowed, held, asked about and 281 /// left alone. How each ends is what the network said for that answer. 282 #[test] 283 fn the_command_cases_end_every_way_a_command_can() { 284 let ends: Vec<String> = command_cases().into_iter().map(|case| case.ended).collect(); 285 assert_eq!(ends, [Verdict::Allow, Verdict::Hold, Verdict::Ask, Verdict::Pass].map(|verdict| command::verdict_name(verdict).to_owned())); 286 } 287 288 /// Measuring is a step the rules take: every command case fires the rule that measures first, 289 /// and then the one that ends it. 290 #[test] 291 fn a_command_is_measured_and_then_decided() { 292 let network = command::network(); 293 let asked = command::Asked::made_up(&[(Act::Build, 1.0)], 1.0, 2.0); 294 let mut known = asked.known.clone(); 295 known.learn(command::Fact::Patience, command::patience(true)); 296 let (fired, known, next) = run(&network, known, |command::Effect::Measure| command::room(asked.answers.needs_mb(), Some(500.0))); 297 assert_eq!(fired.len(), 2); 298 assert_eq!(network.terminals[fired[0]].name, "look for room"); 299 assert_eq!(known.get(command::Fact::Room), Some(command::Value::Short)); 300 assert_eq!(next, Next::End(Verdict::Hold)); 301 } 302 303 /// The turn-end and model cases end as their sentences say, and the two that are settled 304 /// before a question leave Jev's fact unknown. 305 #[test] 306 fn the_other_cases_end_as_they_say() { 307 let ends: Vec<String> = stop_cases().into_iter().map(|case| case.ended).collect(); 308 assert_eq!(ends, ["let it end", "send it back", "let it end"]); 309 let ends: Vec<String> = model_cases().into_iter().map(|case| case.ended).collect(); 310 assert_eq!(ends, ["haiku", "sonnet", "no change"]); 311 let (_, known, _) = run(&stop::network(), stop::before(true, false, true), |never| match never {}); 312 assert_eq!(known.get(stop::Fact::StoppedEarly), None); 313 let (_, known, _) = run(&model::network(), model::before(false), |never| match never {}); 314 assert_eq!(known.get(model::Fact::Enough), None); 315 } 316 317 /// The counts in the sentences are counted, so these are what the page says today. 318 #[test] 319 fn the_counts_are_the_networks_own() { 320 assert_eq!(counts(&command::network()), (8, 7, 5)); 321 assert_eq!(counts(&stop::network()), (5, 4, 1)); 322 assert_eq!(counts(&model::network()), (5, 2, 1)); 323 } 324}