lmjtfy.git / packages / rules / src / lib.rs
lib.rsannotatedlib.rssource532 lines · 17.4 KB · raw

How lmjtfy decides what to do with a query: rules over facts, compiled into a Rete network that the page draws.

A fact is something known about the query: whether Jev can answer it, which kinds of question it reads as, whether it is several questions. A rule is a list of tests on facts and what to do when they all hold. The network is those rules with their shared tests merged: one alpha node per distinct test, and a chain of join nodes per rule, shared where rules begin alike.

Most facts are Jev's to give: each is one typed question about the input. A rules engine would ask for them one at a time, as each rule came to need one. This one does not. [Network::next] collects every Jev fact that any rule still alive is waiting on, and asks for them together, so they go out as one request with N questions (the user, 2026-10-02: "roll up multiple rules that all use jev and parallelize them by firing a single request with N questions to backfill"). Asking Jev more in one request is what keeps the LLM, which costs far more, for the queries that need it.

Nothing here does I/O. The Worker asks [Network::next] what to do, does it, records what it learned in [Known], and asks again.

22#![forbid(unsafe_code)]
24use std::collections::BTreeMap;
25
26use serde::{Deserialize, Serialize};

A way to read an input as a question, which is also the Jev type that answers it.

30#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
31pub enum Kind {

Yes or no, or how likely.

33    Noul,

The best among several possibilities.

35    Choice,

A degree, an amount, a rating.

37    Score,

The chance of something: how likely, what are the odds. Jev answers it as a yes-or-no about the thing itself, and the probability of yes is the answer.

41    Chance,
42}
44impl Kind {
45    pub const ALL: [Kind; 4] = [Kind::Noul, Kind::Choice, Kind::Score, Kind::Chance];
46}

Something known about a query.

49#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
50pub enum Fact {

Jev can judge the input at all.

52    Answerable,

It asks more than one separate question.

54    Several,

The input is to be read as this kind of question. Jev gives a probability for each kind; the likeliest is read, and so is the runner-up when Jev is split between them. So one, or two, hold.

58    Reads(Kind),

A how-much question that needs a scale written for it: a general scale would answer it badly.

61    Scale,

The answer to the input, read as a yes-or-no question.

63    Yes,

The answer to the input, read as a how-much question, on a fixed scale that needs nobody to write it.

66    Degree,

Whether the thing the input asks the chances of is so. The answer to a how-likely question is this fact's probability.

69    Chance,

The input is fit to show to strangers on a public page.

71    Fit,

The LLM wrote at least one question Jev can take.

73    Drafted,

Jev answered at least one of the LLM's questions.

75    Judged,
76}

Where a fact comes from.

79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub enum Source {

A Jev question about the input. Every such fact that is wanted is asked for in the same request.

83    Jev,

The LLM, when a rule's effect asks it.

85    Llm,

Jev, when a rule's effect asks it what the LLM wrote.

87    JevAgain,
88}
90impl Fact {
91    pub const ALL: [Fact; 13] = [
92        Fact::Answerable,
93        Fact::Several,
94        Fact::Reads(Kind::Noul),
95        Fact::Reads(Kind::Choice),
96        Fact::Reads(Kind::Score),
97        Fact::Reads(Kind::Chance),
98        Fact::Scale,
99        Fact::Yes,
100        Fact::Degree,
101        Fact::Chance,
102        Fact::Fit,
103        Fact::Drafted,
104        Fact::Judged,
105    ];
106
107    pub fn source(self) -> Source {
108        match self {
109            Fact::Drafted => Source::Llm,
110            Fact::Judged => Source::JevAgain,
111            _ => Source::Jev,
112        }
113    }

Everything the fact can turn out to be.

116    pub fn values(self) -> &'static [Value] {
117        match self {
118            Fact::Yes | Fact::Degree | Fact::Chance => &[Value::Given],
119            _ => &[Value::Bool(true), Value::Bool(false)],
120        }
121    }

The fact's name on the diagram and in a playground link.

124    pub fn name(self) -> &'static str {
125        match self {
126            Fact::Answerable => "answerable",
127            Fact::Several => "several",
128            Fact::Reads(Kind::Noul) => "yes-or-no",
129            Fact::Reads(Kind::Choice) => "pick",
130            Fact::Reads(Kind::Score) => "how-much",
131            Fact::Reads(Kind::Chance) => "how-likely",
132            Fact::Chance => "chance",
133            Fact::Scale => "scale",
134            Fact::Yes => "yes",
135            Fact::Degree => "degree",
136            Fact::Fit => "fit",
137            Fact::Drafted => "drafted",
138            Fact::Judged => "judged",
139        }
140    }
141}

What a fact turned out to be.

144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145pub enum Value {
146    Bool(bool),

Known, with a value no rule tests: the answer itself.

148    Given,
149}
151impl Value {
152    pub fn label(self) -> &'static str {
153        match self {
154            Value::Bool(true) => "yes",
155            Value::Bool(false) => "no",
156            Value::Given => "known",
157        }
158    }
159}

One test on one fact: an alpha node.

162#[derive(Clone, Copy, Debug, PartialEq, Eq)]
163pub enum Test {
164    Is(Fact, Value),

The fact is known, whatever it is.

166    Known(Fact),
167}
169impl Test {
170    pub fn fact(self) -> Fact {
171        match self {
172            Test::Is(fact, _) | Test::Known(fact) => fact,
173        }
174    }

The test without its fact's name, as the diagram labels the node.

177    pub fn label(self) -> String {
178        match self {
179            Test::Is(_, value) => format!("= {}", value.label()),
180            Test::Known(_) => "is known".to_owned(),
181        }
182    }
183}

What the LLM is asked to write.

186#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
187pub enum Want {

The options of a pick.

189    Options,

The levels of a how-much.

191    Scale,

Each of several questions, as whatever it is.

193    Split,
194}

Something a rule does that costs a call and teaches a fact.

197#[derive(Clone, Copy, Debug, PartialEq, Eq)]
198pub enum Effect {

Ask the LLM to write Jev's questions. Teaches [Fact::Drafted]. Every drafting rule that holds is served by the one call ([Network::wants]).

202    Draft(Want),

Ask Jev the questions the LLM wrote. Teaches [Fact::Judged].

204    Judge,
205}
207impl Effect {
208    pub fn teaches(self) -> Fact {
209        match self {
210            Effect::Draft(_) => Fact::Drafted,
211            Effect::Judge => Fact::Judged,
212        }
213    }
214}

How a rule ends a query outright.

217#[derive(Clone, Copy, Debug, PartialEq, Eq)]
218pub enum End {

Jev cannot judge the input.

220    NotAQuestion,
221}

Something that is so when a rule holds, and that decides no step: the Worker reads it once the query has ended.

225#[derive(Clone, Copy, Debug, PartialEq, Eq)]
226pub enum Note {

The question may go on the public feed.

228    List,

This fact is an answer to show: Jev's own (Yes, Degree, Chance), or its answers to what the LLM wrote (Judged).

231    Show(Fact),
232}
234#[derive(Clone, Copy, Debug, PartialEq, Eq)]
235pub enum Then {
236    Do(Effect),
237    End(End),
238    Note(Note),
239}
240
241pub struct Rule {
242    pub name: &'static str,
243    pub when: &'static [Test],
244    pub then: Then,
245}
246
247const YES: Value = Value::Bool(true);
248const NO: Value = Value::Bool(false);
249const ONE: [Test; 2] = [Test::Is(Fact::Answerable, YES), Test::Is(Fact::Several, NO)];

lmjtfy's rules. Order is priority: of the rules that hold, the first with something left to do decides. When Jev is split between two readings, the rules of both hold, and both are answered.

254pub const RULES: &[Rule] = &[
255    Rule { name: "refuse", when: &[Test::Is(Fact::Answerable, NO)], then: Then::End(End::NotAQuestion) },
256    Rule {
257        name: "answer yes or no",
258        when: &[ONE[0], ONE[1], Test::Is(Fact::Reads(Kind::Noul), YES), Test::Known(Fact::Yes)],
259        then: Then::Note(Note::Show(Fact::Yes)),
260    },
261    Rule {
262        name: "grade it",
263        when: &[
264            ONE[0],
265            ONE[1],
266            Test::Is(Fact::Reads(Kind::Score), YES),
267            Test::Is(Fact::Scale, NO),
268            Test::Known(Fact::Degree),
269        ],
270        then: Then::Note(Note::Show(Fact::Degree)),
271    },
272    Rule {
273        name: "give the odds",
274        when: &[ONE[0], ONE[1], Test::Is(Fact::Reads(Kind::Chance), YES), Test::Known(Fact::Chance)],
275        then: Then::Note(Note::Show(Fact::Chance)),
276    },
277    Rule {
278        name: "draft a scale",
279        when: &[ONE[0], ONE[1], Test::Is(Fact::Reads(Kind::Score), YES), Test::Is(Fact::Scale, YES)],
280        then: Then::Do(Effect::Draft(Want::Scale)),
281    },
282    Rule {
283        name: "draft options",
284        when: &[ONE[0], ONE[1], Test::Is(Fact::Reads(Kind::Choice), YES)],
285        then: Then::Do(Effect::Draft(Want::Options)),
286    },
287    Rule {
288        name: "split it up",
289        when: &[Test::Is(Fact::Answerable, YES), Test::Is(Fact::Several, YES)],
290        then: Then::Do(Effect::Draft(Want::Split)),
291    },
292    Rule { name: "judge the drafts", when: &[Test::Is(Fact::Drafted, YES)], then: Then::Do(Effect::Judge) },
293    Rule {
294        name: "show the answers",
295        when: &[Test::Is(Fact::Judged, YES)],
296        then: Then::Note(Note::Show(Fact::Judged)),
297    },
298    Rule {
299        name: "list it",
300        when: &[Test::Is(Fact::Answerable, YES), Test::Is(Fact::Fit, YES)],
301        then: Then::Note(Note::List),
302    },
303];

What is known about a query so far.

306#[derive(Clone, Debug, Default, PartialEq)]
307pub struct Known(BTreeMap<Fact, Value>);
309impl Known {
310    pub fn learn(&mut self, fact: Fact, value: Value) {
311        self.0.insert(fact, value);
312    }
313
314    pub fn get(&self, fact: Fact) -> Option<Value> {
315        self.0.get(&fact).copied()
316    }
317
318    pub fn forget(&mut self, fact: Fact) {
319        self.0.remove(&fact);
320    }
321}

Where a node stands, given what is known.

324#[derive(Clone, Copy, Debug, PartialEq, Eq)]
325pub enum State {

Every test up to here holds.

327    Holds,

A test up to here is known to fail. Nothing downstream can fire.

329    Fails,

No test has failed, and at least one is on a fact not yet known.

331    Waits,
332}
334impl State {
335    fn and(self, other: State) -> State {
336        match (self, other) {
337            (State::Fails, _) | (_, State::Fails) => State::Fails,
338            (State::Holds, State::Holds) => State::Holds,
339            _ => State::Waits,
340        }
341    }
342}

A join node: everything left requires, and one more test. left is another join, or None when this is a rule's first test.

346#[derive(Clone, Copy, Debug, PartialEq, Eq)]
347pub struct Join {
348    pub left: Option<usize>,

Index into [Network::alphas].

350    pub alpha: usize,

How many tests are joined here: 1 for a rule's first.

352    pub depth: usize,
353}

A rule's place in the network: the join that holds when the rule does.

356#[derive(Clone, Copy, Debug)]
357pub struct Terminal {
358    pub name: &'static str,
359    pub join: usize,
360    pub then: Then,
361}

The rules as a Rete network.

364#[derive(Clone, Debug)]
365pub struct Network {

One per distinct test, however many rules use it.

367    pub alphas: Vec<Test>,

Shared by every rule that begins with the same tests.

369    pub joins: Vec<Join>,
370    pub terminals: Vec<Terminal>,
371}

What to do next about a query.

374#[derive(Clone, Debug, PartialEq, Eq)]
375pub enum Next {

Ask Jev for all of these, in one request.

377    Ask(Vec<Fact>),
378    Do(Effect),

A rule ends the query outright.

380    End(End),

No rule has anything left to do and nothing more can be learned: the query is over, and what to show is in the notes.

383    Done,
384}
386impl Network {
387    pub fn compile(rules: &[Rule]) -> Self {
388        let mut network = Network { alphas: Vec::new(), joins: Vec::new(), terminals: Vec::new() };
389        for rule in rules {
390            let mut left = None;
391            for (depth, test) in rule.when.iter().enumerate() {
392                let alpha = network.alphas.iter().position(|known| known == test).unwrap_or_else(|| {
393                    network.alphas.push(*test);
394                    network.alphas.len() - 1
395                });
396                let join = Join { left, alpha, depth: depth + 1 };
397                left = Some(network.joins.iter().position(|known| *known == join).unwrap_or_else(|| {
398                    network.joins.push(join);
399                    network.joins.len() - 1
400                }));
401            }
402            let join = left.expect("a rule has at least one test");
403            network.terminals.push(Terminal { name: rule.name, join, then: rule.then });
404        }
405        network
406    }

lmjtfy's own network.

409    pub fn lmjtfy() -> Self {
410        Network::compile(RULES)
411    }
413    pub fn alpha(&self, alpha: usize, known: &Known) -> State {
414        let test = self.alphas[alpha];
415        match (test, known.get(test.fact())) {
416            (_, None) => State::Waits,
417            (Test::Known(_), Some(_)) => State::Holds,
418            (Test::Is(_, wanted), Some(value)) if wanted == value => State::Holds,
419            (Test::Is(..), Some(_)) => State::Fails,
420        }
421    }
422
423    pub fn join(&self, join: usize, known: &Known) -> State {
424        let Join { left, alpha, .. } = self.joins[join];
425        let here = self.alpha(alpha, known);
426        left.map_or(here, |left| self.join(left, known).and(here))
427    }

The facts a join's tests are on, first test first.

430    fn facts(&self, join: usize) -> Vec<Fact> {
431        let Join { left, alpha, .. } = self.joins[join];
432        let mut facts = left.map_or_else(Vec::new, |left| self.facts(left));
433        facts.push(self.alphas[alpha].fact());
434        facts
435    }

What to do, given what is known, and the rule that says so (its index in [Network::terminals]), when one does.

A rule that holds decides, in rule order; an effect whose fact is already known has been done and is passed over. If no rule decides, every Jev fact that a rule not yet failed is waiting on is wanted, and they are asked for together.

444    pub fn decide(&self, known: &Known) -> (Option<usize>, Next) {
445        for (index, terminal) in self.terminals.iter().enumerate() {
446            if self.join(terminal.join, known) != State::Holds {
447                continue;
448            }
449            match terminal.then {
450                Then::End(end) => return (Some(index), Next::End(end)),
451                Then::Do(effect) if known.get(effect.teaches()).is_none() => return (Some(index), Next::Do(effect)),
452                Then::Do(_) | Then::Note(_) => {}
453            }
454        }
455        let mut wanted: Vec<Fact> = self
456            .terminals
457            .iter()
458            .filter(|terminal| self.join(terminal.join, known) == State::Waits)
459            .flat_map(|terminal| self.facts(terminal.join))
460            .filter(|fact| fact.source() == Source::Jev && known.get(*fact).is_none())
461            .collect();
462        wanted.sort();
463        wanted.dedup();
464        (None, if wanted.is_empty() { Next::Done } else { Next::Ask(wanted) })
465    }

What to do, given what is known.

468    pub fn next(&self, known: &Known) -> Next {
469        self.decide(known).1
470    }
472    fn held<'a>(&'a self, known: &'a Known) -> impl Iterator<Item = &'a Terminal> {
473        self.terminals.iter().filter(move |terminal| self.join(terminal.join, known) == State::Holds)
474    }

The names of the rules that hold, in rule order.

477    pub fn holding(&self, known: &Known) -> Vec<&'static str> {
478        self.held(known).map(|terminal| terminal.name).collect()
479    }

Whether a rule that holds says note.

482    pub fn notes(&self, known: &Known, note: Note) -> bool {
483        self.held(known).any(|terminal| terminal.then == Then::Note(note))
484    }

The answers to show, in rule order.

487    pub fn shows(&self, known: &Known) -> Vec<Fact> {
488        self.held(known)
489            .filter_map(|terminal| match terminal.then {
490                Then::Note(Note::Show(fact)) => Some(fact),
491                _ => None,
492            })
493            .collect()
494    }

Everything the LLM is to write, for every drafting rule that holds: one call serves them all.

498    pub fn wants(&self, known: &Known) -> Vec<Want> {
499        let mut wants: Vec<Want> = self
500            .held(known)
501            .filter_map(|terminal| match terminal.then {
502                Then::Do(Effect::Draft(want)) => Some(want),
503                _ => None,
504            })
505            .collect();
506        wants.sort();
507        wants.dedup();
508        wants
509    }

The joins and alphas that a rule that holds stands on: what, of everything known, turned out to matter.

513    pub fn used(&self, known: &Known) -> (Vec<bool>, Vec<bool>) {
514        let mut joins = vec![false; self.joins.len()];
515        let mut alphas = vec![false; self.alphas.len()];
516        for terminal in &self.terminals {
517            if self.join(terminal.join, known) != State::Holds {
518                continue;
519            }
520            let mut at = Some(terminal.join);
521            while let Some(join) = at {
522                joins[join] = true;
523                alphas[self.joins[join].alpha] = true;
524                at = self.joins[join].left;
525            }
526        }
527        (joins, alphas)
528    }
529}
531#[cfg(test)]
532mod tests;