code wiki / _hdl_build / nx_cms_abtest.nx
nx_cms_abtest.nx source
↩ module page · 51 lines · 2694 B
1// nx_cms_abtest.nx -- CMS A/B TESTING + PERSONALIZATION (sovereign, deterministic, privacy-native).
2// The Nelio / Google-Optimize class, made Nishi-native: variant assignment is a DETERMINISTIC salted
3// hash computed ON-BOX -- the visitor is NEVER sent to a cloud experimentation service, and assignment
4// is REPRODUCIBLE (recompute which arm any visitor was in from (experiment, visitor) alone, storing no
5// PII = privacy-native + auditable experiments, the exceed angle over cloud A/B). Composes the canonical
6// nx_fnv (FNV-1a; cardinal law: never re-implement a hash inline). Supports even splits, weighted
7// rollouts (data-driven weights, rule 11), and a sticky/deterministic guarantee. license_tier: ORIGINAL
8import "nx_fnv.nx"
9import "nx_syscalls.nx"
10
11const NX_AB_RESOLUTION: i64 = 10000 // weight buckets / personalization resolution (basis points)
12
13func ab_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
14
15// deterministic non-negative hash of (experiment "|" visitor) via the canonical FNV-1a. Salting by
16// experiment makes assignments ACROSS experiments independent; the "|" separator stops exp/visitor
17// bytes from bleeding into each other. Sign-folded (mask high bit) so modulo is stable & overflow-free.
18func ab_hash(exp: *u8, visitor: *u8) -> i64 {
19 var h: i64 = fnv1a_init()
20 h = fnv1a_update(h, exp, ab_slen(exp))
21 h = fnv1a_update(h, "|" as *u8, 1)
22 h = fnv1a_update(h, visitor, ab_slen(visitor))
23 return h & 0x7fffffffffffffff
24}
25
26// even assignment to one of num_variants in [0, num_variants). Deterministic + sticky (same inputs ->
27// same arm forever, with no stored state). num_variants<=1 is a safe degenerate -> control arm 0.
28func ab_assign(exp: *u8, visitor: *u8, num_variants: i64) -> i64 {
29 if num_variants <= 1 { return 0 }
30 return ab_hash(exp, visitor) % num_variants
31}
32
33// position in [0, NX_AB_RESOLUTION) for weighted rollouts / personalization thresholds.
34func ab_position(exp: *u8, visitor: *u8) -> i64 {
35 return ab_hash(exp, visitor) % NX_AB_RESOLUTION
36}
37
38// weighted assignment: weights[i] in basis points summing to NX_AB_RESOLUTION (e.g. {9000,1000} =
39// a 90/10 rollout). Returns the variant whose cumulative band the visitor's position falls in.
40// Data-driven (rule 11): change the split by editing the weight table, never this code.
41func ab_assign_weighted(exp: *u8, visitor: *u8, weights: *i64, nweights: i64) -> i64 {
42 let pos: i64 = ab_position(exp, visitor)
43 var cum: i64 = 0
44 var i: i64 = 0
45 while i < nweights {
46 cum = cum + weights[i]
47 if pos < cum { return i }
48 i = i + 1
49 }
50 return nweights - 1 // remainder (rounding) -> last variant
51}