Context
Several features need the same thing: a test over facts about a customer, a cart, a record or an order history, which yields a yes or a no and a reason for the answer. Segments, discounts and promotions, pricing rules, related-product rules, automations and visibility rules all need one. Without a shared engine, each consumer would write its own evaluator, with its own operators and its own reading of a missing value, and one rule could give different answers in two places. A rule that a tenant writes is data that references registered code, which 0227 allows, and it never carries logic of its own (0154). The owner decided the shape in the final structure review of 2026-10-24.Decision
-
One engine.
@stackra/rulesis a free framework package in theapplicationcategory (0127). It evaluates conditions for every rule consumer. A consumer never builds an evaluator of its own. -
Condition tree. A condition is a tree of
all,anyandnotnodes. Its leaves test one field with one operator against a value. - Typed operators. Each field type has an operator set that the engine fixes: string, number, money (0220), date, enum, list and geo. A consumer chooses fields, and it does not choose operators.
- Fact sources. Each consumer registers a fact source, which is a typed description of the fields its rules may test. Examples are customer attributes, cart totals, order history aggregates and record fields. The engine knows no commerce. A field that no fact source registers cannot be tested.
-
Versioned rule sets. A rule set is a model that carries revisions (
HasRevisions, 0321). An evaluation runs against one stated revision, and its explanation names that revision. - Deterministic evaluation. The same facts and the same revision give the same result. The current time is a fact that the caller supplies, and the engine does not read a clock.
- Explanation. Every result says which condition matched or failed, by its path in the tree.
-
Rule builder. The package ships a rule-builder panel component in its
reactentry. The panel offers only the registered fields and the operators of their types. - Consumers own the outcome. The engine returns a result and nothing more. What a match does belongs to the consumer:
- Tenant data, never code. A rule is tenant data that references only registered fields and operators. That is the data-that-references-registered-code case of 0227. It is not an expression language. Arbitrary logic runs only in the sandboxed code step of 0154.
- IAM conditions. IAM conditions (0208) may reuse the operator set. The IAM policy decision point keeps its own evaluation path, for performance and security.
-
Entries. The package has the
contracts,core,nodeandreactentries. It addsconfigandtestingwhere it needs them, as 0294 decision 15 allows.
Consequences
- A consumer gets evaluation, explanation and versioning without writing them, and two consumers that test the same field get the same answer.
- A tenant cannot test a field that no fact source registers. Adding a field to a rule is a change that the owner of the consumer makes in code.
- Adding an operator is a change to the engine, and no consumer changes with it.
- A rule that a tenant writes is versioned, explained and unable to run code.
Enforcement
- planned — gen check: a rule set that names an unregistered field, or an operator outside the operator set of its field type, fails.
- planned — tests in
@stackra/rules: determinism for a fixed revision and fixed facts, and an explanation that names the matched or failed path. - planned — an oxlint boundary rule: no evaluator of condition trees outside
@stackra/rules, and no dynamic code evaluation in the package.