Bucketing & sticky assignment
Assignment must be deterministic (the same user always sees the same variant), stateless (no lookup table), and identical in every SDK. LLMJury freezes this as a contract:
bucket = MurmurHash3_x86_32("{salt}:{user_id}:{experiment_id}") % bucket_count
The bucket lands in integer cumulative allocation boundaries — the first slice whose boundary it
falls below wins. bucket_count is per-experiment config (default 1,000 = 0.1% granularity),
pinned for the life of an experiment version and delivered in the polled config — never
hardcoded in an SDK.
Cross-language identity
The Python, TypeScript, and Java SDKs and the backend all implement this hash bit-for-bit; a
shared 64-case fixture is asserted by every implementation in CI, so assign in any language
gives the same variant for the same inputs.
Sticky under change
Editing an experiment mints a new version but never reshuffles already-exposed users: a new
arm draws its traffic from unallocated or donor buckets via bucket reservation, not by remapping
existing boundaries. Changing bucket_count or the salt on a running experiment would re-bucket
everyone, so it's only allowed by creating a new experiment (audited, acknowledged).
Local compute also means no network on the hot path: assign reads the in-process config
cache, polled from GET /v1/config with ETag/304.