@lucerna-dev/gates-core
v0.0.1-alpha.1
Published
The Gates evaluation engine — pure functions over a compiled runtime document, no I/O, no platform APIs, no dependencies. It typechecks against a bare ES2022 lib, so it runs anywhere JavaScript does: Node, Bun, Deno, edge workers, browsers.
Downloads
72
Readme
@lucerna-dev/gates-core
The Gates evaluation engine — pure functions over a compiled runtime document, no I/O, no platform APIs, no dependencies. It typechecks against a bare ES2022 lib, so it runs anywhere JavaScript does: Node, Bun, Deno, edge workers, browsers.
Most apps never install this package directly. @lucerna-dev/gates-node downloads the runtime and evaluates with this engine in-process; @lucerna-dev/gates-browser receives decisions computed server-side by the same semantics. Reach for core when you are building your own provider (a custom transport, an offline harness, another platform SDK) and need the evaluation semantics without the plumbing.
Install
pnpm add @lucerna-dev/gates-coreQuickstart
import { evaluate, flagDecision, type GatesRuntime } from "@lucerna-dev/gates-core";
// A compiled runtime document. In production this comes from
// `GET /sdk/v1/gates/runtime` — @lucerna-dev/gates-node downloads and
// polls it for you.
const runtime: GatesRuntime = {
schemaVersion: 1,
environment: "production",
generatedAt: "2026-07-15T00:00:00.000Z",
kills: { payments: true },
flags: {
new_billing: {
enabled: true,
conditions: [],
overrides: [],
rollout: { percentage: 50, salt: "s_billing" },
},
},
experiments: {},
audiences: {},
};
const identity = { userId: "u_42", traits: { plan: "pro" } };
const decision = flagDecision(runtime, "new_billing", identity);
// { on: …, reason: "rollout" } — deterministic for this user and salt
const decisions = evaluate(runtime, identity);
// every kill, flag and experiment decision for one identityThis block runs verbatim in test/readme.test.ts — if it drifts from the package, the test suite fails.
API
| Export | What it does |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| evaluate(runtime, identity?) | Every decision for one identity — the SSR/bootstrap projection (GatesDecisions) |
| flagDecision(runtime, key, identity?) | One flag's FlagDecision; undefined when the key is not in the runtime |
| experimentAssignment(runtime, key, identity?) | One experiment's VariantAssignment; undefined for unknown keys |
| killState(runtime, key) | Kill switch state — false means the guarded path is killed; undefined for unknown keys |
| explainFlag(runtime, key, identity?) | The decision plus the rule-by-rule TraceStep trail that produced it |
| explainExperiment(runtime, key, identity?) | The assignment plus the gate-by-gate trail |
| evaluateFlag(flag, identity, trace?) | The flag evaluator itself, for callers that already hold a RuntimeFlag |
| evaluateExperiment(experiment, audiences, identity, trace?) | The experiment evaluator itself |
| bucket(salt, unitId) | The unit's bucket for a salt, in basis points [0, 10000) |
| murmur3(input, seed?) | MurmurHash3 x86 32-bit over the UTF-8 bytes, as uint32 |
| GATES_RUNTIME_SCHEMA_VERSION | The runtime schema this engine version understands (1) |
Runtime and decision types (GatesRuntime, GatesIdentity, FlagReason, AssignmentReason, …) are exported from src/runtime.ts and src/decisions.ts.
Guarantees & semantics
- Pure and deterministic. Same runtime + same identity → same decisions, on every platform, forever. No I/O, no clocks, no randomness.
- Bucketing is frozen: murmur3 (x86 32-bit) over
${salt}:${unitId}, basis points (hash % 10000), stored salts. Salts are authored server-side and carried in the runtime — renaming a key never reshuffles users. - Unknown rule elements fail safe. A gate carrying any rule this engine version doesn't recognize (condition operator, override kind) is answered whole with
unsupported_rule— flag off, experiment unassigned. An engine never half-evaluates a document it doesn't fully understand. Unknown fields are ignored; a changed meaning requires a newschemaVersion, additions do not. - Flag precedence, first hit wins: environment master switch → overrides in stored order (by user id or email domain) → conditions (a conjunction — all must match) → percentage rollout, sticky by user id via the stored salt.
- Experiment gates, in order: running → environment → audience → holdout → traffic → allocation-weighted variant walk. The salt is per iteration — restarting an iteration reshuffles every bucket by design.
- Partial allocation needs a
userId. A rollout strictly between 0 and 100 — or any experiment — with no user id answersmissing_user_id(off / unassigned): there is nothing to be sticky by. - The golden vectors are the compatibility contract.
src/vectors.jsonfreezes (runtime, identity) → decisions and is append-only; every Lucerna SDK in every language and the API's CI replay it bit-for-bit. A vector failure is a breaking change by definition.
Errors & failure modes
Nothing here throws in normal operation. Read helpers return undefined for keys not in the runtime — the platform SDK on top owns what to serve then (fail-open defaults live in the shells, not the engine). evaluate answers every key in the document; it never performs I/O and never rejects.
