Skip to main content

Predicate Tools

3 min read

predict – "what would this rule do, and what facts must change?"

Run a FactPredicate against the current facts without firing it. Returns whether it would fire, the full clause-by-clause breakdown, and – if it wouldn't – a list of the smallest changes that would make it fire.


The Sherlock moment

LLM emits a rule. predict() says:

"This rule needs cartTotal ≥ 50 to fire; current value is 30. Set cartTotal to at least 50 to make it fire."

…and the LLM rewrites accordingly. Closes the iteration loop with one tree walk.

import { predict } from "@directive-run/core";

const predicate = {
  cartTotal: { $gte: 50 },
  region: { $in: ["US", "EU"] },
};

const facts = { cartTotal: 30, region: "ASIA" };

predict(predicate, facts);
// → {
//     wouldFire: false,
//     whenExplain: [
//       { path: "cartTotal", op: "$gte", expected: 50, actual: 30, pass: false },
//       { path: "region",    op: "$in",  expected: ["US","EU"], actual: "ASIA", pass: false },
//     ],
//     missingChanges: [
//       { path: "cartTotal", op: "$gte", expected: 50, actual: 30,
//         suggestion: "set cartTotal to at least 50 (currently 30)" },
//       { path: "region", op: "$in", expected: ["US","EU"], actual: "ASIA",
//         suggestion: "set region to one of ['US','EU'] (currently 'ASIA')" },
//     ],
//   }

Suggestions per operator

OperatorSuggestion shape
$eqset X to V (currently A)
$nechange X to anything other than V (currently A)
$gt / $gteset X above/to at least V (currently A)
$lt / $lteset X below/to at most V (currently A)
$in / $ninset X to one of [...] / something other than [...]
$exists: trueset X to a non-null value
$exists: falseunset X
$betweenset X between A and B (currently C)
$startsWith / $endsWith / $containsset X to start/end/contain V
$matchesset X to match the pattern /re/ (currently A)
$changedthe previous-vs-current change of X is required to differ

For combinators ($all, $any, $not), predict() recurses into children and surfaces failed leaves with the leaf-level suggestions.


Use cases

  • LLM iteration loop – emit → predict → reprompt. The model converges to a firing rule in 1-2 turns.
  • "Preview this rule" UI – show a user what their proposed rule would do against today's data, including the exact facts that would need to change.
  • Test-suite assertionexpect(predict(rule, facts).wouldFire).toBe(true).

What this does NOT do

  • Not a SAT solverpredict() reports failing leaves, not the minimal set of fact changes that satisfy the rule. For $any: [A, B] with both failed, it lists both – you pick which to change.
  • Doesn't mutate facts – pure read.

$changed and the prev snapshot (M10)

The $changed operator compares the current value against the previous one. Without a prev snapshot, the clause cannot be evaluated meaningfully.

predict() makes this loud: when the predicate contains a $changed clause AND you didn't pass prev, the result's missingChanges array includes a synthetic warning per $changed clause:

predict({ phase: { $changed: true } }, { phase: "green" });
// → {
//     wouldFire: false,
//     missingChanges: [
//       { path: "phase", op: "$changed", expected: true, actual: undefined,
//         suggestion: "$changed clause at \"phase\" cannot be evaluated without a `prev` snapshot – pass predict(predicate, facts, prev)." }
//     ],
//   }

Always pass prev (a snapshot of the prior facts) when your predicate uses $changed:

predict({ phase: { $changed: true } }, { phase: "green" }, { phase: "red" });
// → wouldFire: true

Result shape

PredictResult carries wouldFire, whenExplain, and missingChanges. It does not include the input predicate (the caller already has it – keep the reference at the call site if you need to display "what we predicted" alongside the result).

Pure read; does not mutate facts.


Reference

Previous
Predicate Codegen

Stay in the loop. Sign up for our newsletter.

We care about your data. We'll never share your email.

Powered by Directive. This signup uses a Directive module with facts, derivations, constraints, and resolvers – zero useState, zero useEffect. Read how it works

Directive - Constraint-Driven Runtime for TypeScript | AI Guardrails & State Management