jevhooks.git / web / src / rules.rs
rules.rsannotatedrules.rssource324 lines · 13.7 KB · raw

The rules that decide, drawn from the rules that decide.

Nothing about a rule is written here. The three networks are the plugin's own (jevhooks_rules, the crate the daemon runs), and rete-draw draws a network, so the page cannot show a rule the plugin does not have, or miss one it has. What is written here is a few worked cases for each: an answer Jev could give, read through the plugin's own thresholds, and run through the network. How a case ends is what the network said, never a word typed beside it.

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;

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}

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}

A network run to its end from known, each effect answered by perform: the rules that 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}

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}

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

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

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}

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}

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}

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}
263#[cfg(test)]
264mod tests {
265    use jevhooks_events::Verdict;
266
267    use super::*;

The drawings are of the plugin's own rules: every rule of every network is on the page by 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    }

The command cases are chosen to end every way a command can: allowed, held, asked about and 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    }

Measuring is a step the rules take: every command case fires the rule that measures first, 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    }

The turn-end and model cases end as their sentences say, and the two that are settled 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    }

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}