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}