lmjtfy.git / packages / rules / src / lib.rs
lib.rsannotatedlib.rssource542 lines · 17.9 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 input is a whole question, not one that stops part way, as it does while it is still being typed.

74    Whole,

The LLM wrote at least one question Jev can take.

76    Drafted,

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

78    Judged,
79}

Where a fact comes from.

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

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

86    Jev,

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

88    Llm,

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

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

Everything the fact can turn out to be.

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

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

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

What a fact turned out to be.

149#[derive(Clone, Copy, Debug, PartialEq, Eq)]
150pub enum Value {
151    Bool(bool),

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

153    Given,
154}
156impl Value {
157    pub fn label(self) -> &'static str {
158        match self {
159            Value::Bool(true) => "yes",
160            Value::Bool(false) => "no",
161            Value::Given => "known",
162        }
163    }
164}

One test on one fact: an alpha node.

167#[derive(Clone, Copy, Debug, PartialEq, Eq)]
168pub enum Test {
169    Is(Fact, Value),

The fact is known, whatever it is.

171    Known(Fact),
172}
174impl Test {
175    pub fn fact(self) -> Fact {
176        match self {
177            Test::Is(fact, _) | Test::Known(fact) => fact,
178        }
179    }

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

182    pub fn label(self) -> String {
183        match self {
184            Test::Is(_, value) => format!("= {}", value.label()),
185            Test::Known(_) => "is known".to_owned(),
186        }
187    }
188}

What the LLM is asked to write.

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

The options of a pick.

194    Options,

The levels of a how-much.

196    Scale,

Each of several questions, as whatever it is.

198    Split,
199}

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

202#[derive(Clone, Copy, Debug, PartialEq, Eq)]
203pub 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]).

207    Draft(Want),

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

209    Judge,
210}
212impl Effect {
213    pub fn teaches(self) -> Fact {
214        match self {
215            Effect::Draft(_) => Fact::Drafted,
216            Effect::Judge => Fact::Judged,
217        }
218    }
219}

How a rule ends a query outright.

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

Jev cannot judge the input.

225    NotAQuestion,
226}

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

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

The question may go on the public feed.

233    List,

The input stops part way: the visitor is likely still typing it.

235    Unfinished,

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

238    Show(Fact),
239}
241#[derive(Clone, Copy, Debug, PartialEq, Eq)]
242pub enum Then {
243    Do(Effect),
244    End(End),
245    Note(Note),
246}
247
248pub struct Rule {
249    pub name: &'static str,
250    pub when: &'static [Test],
251    pub then: Then,
252}
253
254const YES: Value = Value::Bool(true);
255const NO: Value = Value::Bool(false);
256const 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.

261pub const RULES: &[Rule] = &[
262    Rule { name: "refuse", when: &[Test::Is(Fact::Answerable, NO)], then: Then::End(End::NotAQuestion) },
263    Rule {
264        name: "answer yes or no",
265        when: &[ONE[0], ONE[1], Test::Is(Fact::Reads(Kind::Noul), YES), Test::Known(Fact::Yes)],
266        then: Then::Note(Note::Show(Fact::Yes)),
267    },
268    Rule {
269        name: "grade it",
270        when: &[
271            ONE[0],
272            ONE[1],
273            Test::Is(Fact::Reads(Kind::Score), YES),
274            Test::Is(Fact::Scale, NO),
275            Test::Known(Fact::Degree),
276        ],
277        then: Then::Note(Note::Show(Fact::Degree)),
278    },
279    Rule {
280        name: "give the odds",
281        when: &[ONE[0], ONE[1], Test::Is(Fact::Reads(Kind::Chance), YES), Test::Known(Fact::Chance)],
282        then: Then::Note(Note::Show(Fact::Chance)),
283    },
284    Rule {
285        name: "draft a scale",
286        when: &[ONE[0], ONE[1], Test::Is(Fact::Reads(Kind::Score), YES), Test::Is(Fact::Scale, YES)],
287        then: Then::Do(Effect::Draft(Want::Scale)),
288    },
289    Rule {
290        name: "draft options",
291        when: &[ONE[0], ONE[1], Test::Is(Fact::Reads(Kind::Choice), YES)],
292        then: Then::Do(Effect::Draft(Want::Options)),
293    },
294    Rule {
295        name: "split it up",
296        when: &[Test::Is(Fact::Answerable, YES), Test::Is(Fact::Several, YES)],
297        then: Then::Do(Effect::Draft(Want::Split)),
298    },
299    Rule { name: "judge the drafts", when: &[Test::Is(Fact::Drafted, YES)], then: Then::Do(Effect::Judge) },
300    Rule {
301        name: "show the answers",
302        when: &[Test::Is(Fact::Judged, YES)],
303        then: Then::Note(Note::Show(Fact::Judged)),
304    },
305    Rule {
306        name: "list it",
307        when: &[Test::Is(Fact::Answerable, YES), Test::Is(Fact::Fit, YES)],
308        then: Then::Note(Note::List),
309    },
310    // It decides no step: a question sent as it stands is answered as it
311    // stands. The line under the box reads it while the visitor types.
312    Rule { name: "wait for the rest", when: &[Test::Is(Fact::Whole, NO)], then: Then::Note(Note::Unfinished) },
313];

What is known about a query so far.

316#[derive(Clone, Debug, Default, PartialEq)]
317pub struct Known(BTreeMap<Fact, Value>);
319impl Known {
320    pub fn learn(&mut self, fact: Fact, value: Value) {
321        self.0.insert(fact, value);
322    }
323
324    pub fn get(&self, fact: Fact) -> Option<Value> {
325        self.0.get(&fact).copied()
326    }
327
328    pub fn forget(&mut self, fact: Fact) {
329        self.0.remove(&fact);
330    }
331}

Where a node stands, given what is known.

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

Every test up to here holds.

337    Holds,

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

339    Fails,

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

341    Waits,
342}
344impl State {
345    fn and(self, other: State) -> State {
346        match (self, other) {
347            (State::Fails, _) | (_, State::Fails) => State::Fails,
348            (State::Holds, State::Holds) => State::Holds,
349            _ => State::Waits,
350        }
351    }
352}

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

356#[derive(Clone, Copy, Debug, PartialEq, Eq)]
357pub struct Join {
358    pub left: Option<usize>,

Index into [Network::alphas].

360    pub alpha: usize,

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

362    pub depth: usize,
363}

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

366#[derive(Clone, Copy, Debug)]
367pub struct Terminal {
368    pub name: &'static str,
369    pub join: usize,
370    pub then: Then,
371}

The rules as a Rete network.

374#[derive(Clone, Debug)]
375pub struct Network {

One per distinct test, however many rules use it.

377    pub alphas: Vec<Test>,

Shared by every rule that begins with the same tests.

379    pub joins: Vec<Join>,
380    pub terminals: Vec<Terminal>,
381}

What to do next about a query.

384#[derive(Clone, Debug, PartialEq, Eq)]
385pub enum Next {

Ask Jev for all of these, in one request.

387    Ask(Vec<Fact>),
388    Do(Effect),

A rule ends the query outright.

390    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.

393    Done,
394}
396impl Network {
397    pub fn compile(rules: &[Rule]) -> Self {
398        let mut network = Network { alphas: Vec::new(), joins: Vec::new(), terminals: Vec::new() };
399        for rule in rules {
400            let mut left = None;
401            for (depth, test) in rule.when.iter().enumerate() {
402                let alpha = network.alphas.iter().position(|known| known == test).unwrap_or_else(|| {
403                    network.alphas.push(*test);
404                    network.alphas.len() - 1
405                });
406                let join = Join { left, alpha, depth: depth + 1 };
407                left = Some(network.joins.iter().position(|known| *known == join).unwrap_or_else(|| {
408                    network.joins.push(join);
409                    network.joins.len() - 1
410                }));
411            }
412            let join = left.expect("a rule has at least one test");
413            network.terminals.push(Terminal { name: rule.name, join, then: rule.then });
414        }
415        network
416    }

lmjtfy's own network.

419    pub fn lmjtfy() -> Self {
420        Network::compile(RULES)
421    }
423    pub fn alpha(&self, alpha: usize, known: &Known) -> State {
424        let test = self.alphas[alpha];
425        match (test, known.get(test.fact())) {
426            (_, None) => State::Waits,
427            (Test::Known(_), Some(_)) => State::Holds,
428            (Test::Is(_, wanted), Some(value)) if wanted == value => State::Holds,
429            (Test::Is(..), Some(_)) => State::Fails,
430        }
431    }
432
433    pub fn join(&self, join: usize, known: &Known) -> State {
434        let Join { left, alpha, .. } = self.joins[join];
435        let here = self.alpha(alpha, known);
436        left.map_or(here, |left| self.join(left, known).and(here))
437    }

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

440    fn facts(&self, join: usize) -> Vec<Fact> {
441        let Join { left, alpha, .. } = self.joins[join];
442        let mut facts = left.map_or_else(Vec::new, |left| self.facts(left));
443        facts.push(self.alphas[alpha].fact());
444        facts
445    }

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.

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

What to do, given what is known.

478    pub fn next(&self, known: &Known) -> Next {
479        self.decide(known).1
480    }
482    fn held<'a>(&'a self, known: &'a Known) -> impl Iterator<Item = &'a Terminal> {
483        self.terminals.iter().filter(move |terminal| self.join(terminal.join, known) == State::Holds)
484    }

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

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

Whether a rule that holds says note.

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

The answers to show, in rule order.

497    pub fn shows(&self, known: &Known) -> Vec<Fact> {
498        self.held(known)
499            .filter_map(|terminal| match terminal.then {
500                Then::Note(Note::Show(fact)) => Some(fact),
501                _ => None,
502            })
503            .collect()
504    }

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

508    pub fn wants(&self, known: &Known) -> Vec<Want> {
509        let mut wants: Vec<Want> = self
510            .held(known)
511            .filter_map(|terminal| match terminal.then {
512                Then::Do(Effect::Draft(want)) => Some(want),
513                _ => None,
514            })
515            .collect();
516        wants.sort();
517        wants.dedup();
518        wants
519    }

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

523    pub fn used(&self, known: &Known) -> (Vec<bool>, Vec<bool>) {
524        let mut joins = vec![false; self.joins.len()];
525        let mut alphas = vec![false; self.alphas.len()];
526        for terminal in &self.terminals {
527            if self.join(terminal.join, known) != State::Holds {
528                continue;
529            }
530            let mut at = Some(terminal.join);
531            while let Some(join) = at {
532                joins[join] = true;
533                alphas[self.joins[join].alpha] = true;
534                at = self.joins[join].left;
535            }
536        }
537        (joins, alphas)
538    }
539}
541#[cfg(test)]
542mod tests;