Rules vocabulary

Rules is the string-free authoring surface over the declarative Rule and Reduction data. Import it and reference fields by member expression, so a guard reads as one English-shaped line and still compiles to inspectable data the machine can export to its IR.

using static Trax.Effect.StateMachine.Rules;

A field is selected with Input(...) (the trigger input) or Field(...) (the snapshot context), then a terminal method produces the Rule or Reduction:

.When(Input((CoinInput i) => i.Coin).IsOneOf("quarter", "dollar"))
.Reduce(Set((UnlockedContext u) => u.PaidWith).FromInput((CoinInput i) => i.Coin))

The selector (i => i.Coin) is an expression tree, resolved to its JSON key (camelCase). There are no field-name strings, so a rename refactors the rule with the record.

Field selectors

MethodReturnsSelects
Input<TInput, TField>(Expression<Func<TInput, TField>> selector)FieldMatchera field on the trigger input
Field<TContext, TField>(Expression<Func<TContext, TField>> selector)FieldMatchera field on the snapshot context

Guard predicates

Terminal methods on FieldMatcher, each returning a Rule:

MethodHolds when
Present()the field exists and is not null
Absent()the field is missing or null
NonEmpty()a non-empty string or array
IsOneOf(params string[] values)a string equal to one of values
OfType(JsonFieldType type)the field is present and of that JSON type
GreaterThan(double value)a number greater than value
EqualTo(double value)a number equal to value
CountGreaterThan(int value)an array with more than value elements
CountAtLeast(int value)an array with at least value elements
LengthGreaterThan(int value)a string whose length is more than value
LengthAtLeast(int value)a string whose length is at least value
IsTrue() / IsFalse()a boolean field equal to true / false
ArrayOf(JsonFieldType type)an array whose every element is of type

Every predicate is total: a missing or wrong-typed field is false, never an exception. That is what lets guards run against an in-progress draft without throwing.

Combinators

MethodReturnsHolds when
All(params Rule[] rules)Ruleevery sub-rule holds (an empty set is vacuously true)
Any(params Rule[] rules)Ruleat least one sub-rule holds (an empty set is false)
.When(All(
    Field((CartContext c) => c.Items).CountAtLeast(1),
    Field((CartContext c) => c.Total).GreaterThan(0)))

Reductions

A reducer computes the destination context. Build one with these factories:

FactoryProduces
Keep()the current context, unchanged (the default when no reducer is declared)
Clear()an empty context
Reset()the machine's initial context
Set<TContext, TField>(Expression<Func<TContext, TField>> field)a SetBuilder; complete it below

Set(...) clones the context and assigns one field:

CompletionSets the field to
.FromInput<TInput, TField>(Expression<Func<TInput, TField>> selector)a field copied from the trigger input
.ToValue(JsonNode? value)a literal JSON value

Beyond the vocabulary

The vocabulary covers the common guards and reducers on purpose: a small, fixed set keeps the exported IR a declarative contract rather than a serialized-logic DSL. Logic that does not fit (a formula reducer, a multi-field invariant) has two homes. Rule.Custom(name) and Reduction.Custom(name) keep it in the IR by name, with a handler bound per runtime (CustomGuard / CustomReducer in C#). The delegate overloads, When(Func...) and Reduce(Func...), run the same in C# but are lost to the export: the edge is exported with no guard or reducer, as an unconditional edge that keeps the context, so a generated twin disagrees with the server on it. Use the delegates only on a machine with no twin. See Delegate vs declarative.