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.