Lucerna. Gates. Evaluate
(Lucerna v0.0.1-alpha.0)
The Gates evaluation engine — pure functions over a compiled runtime,
no I/O and no processes. Ported from @lucerna/gates-core
(sdks/core/src/evaluate.ts) onto the frozen bucketing contract in
Lucerna.Gates.Hashing; the golden vectors in
sdks/core/src/vectors.json pin parity.
The runtime document keeps its wire shape (string keys, camelCase);
decisions come out as atom-keyed maps with atom reasons matching the
wire codes (:flag_off, :rollout, :assigned, …).
Fail-safe rule: a gate carrying any rule element this engine version
doesn't recognize (unknown condition operator, unknown override kind)
is answered whole with :unsupported_rule — flag off, experiment
unassigned. An engine never half-evaluates a document it doesn't fully
understand.
Summary
Functions
One identity's decisions over the entire runtime.
Experiment assignment, sticky per (salt, user): eligibility gates first (running, environment, audience), then the holdout and traffic dice, then an allocation-weighted walk over the variants. The salt is per iteration, so restarting an iteration reshuffles every bucket by design.
Flag precedence, first hit wins
experiment/3 with the decision trace: {decision, trace_entries}.
flag/2 with the decision trace: {decision, trace_entries}.
Types
@type experiment_decision() :: %{ variant: variant() | nil, iteration: non_neg_integer(), reason: atom() }
Functions
One identity's decisions over the entire runtime.
@spec experiment(map(), map(), keyword()) :: experiment_decision()
Experiment assignment, sticky per (salt, user): eligibility gates first (running, environment, audience), then the holdout and traffic dice, then an allocation-weighted walk over the variants. The salt is per iteration, so restarting an iteration reshuffles every bucket by design.
@spec flag( map(), keyword() ) :: flag_decision()
Flag precedence, first hit wins:
- environment master switch off -> off
- overrides, in stored order (by user id, email domain, or trait)
- conditions — all must match (a flag's rule is a conjunction)
- percentage rollout, sticky by user id via the stored salt
A rollout between 0 and 100 with no user id serves off — there is nothing to be sticky by.
Options: :user_id, :traits (string-keyed, string-valued map),
:audiences — the runtime's audience table, resolving in_audience /
not_in_audience conditions; a referenced id that is absent evaluates
the condition to no-match, never to a wider serve.
@spec trace_experiment(map(), map(), keyword()) :: {experiment_decision(), [trace_entry()]}
experiment/3 with the decision trace: {decision, trace_entries}.
@spec trace_flag( map(), keyword() ) :: {flag_decision(), [trace_entry()]}
flag/2 with the decision trace: {decision, trace_entries}.