///|
pub fn stable_hash(text : String) -> Int {
let mut h = 216613
for ch in text {
let code = ch.to_int()
h = (h * 167 + code + 97) % 2147483
}
if h < 0 {
-h
} else {
h
}
}
///|
pub fn rollout_bucket(user_id : String, salt : String) -> Int {
stable_hash(salt + ":" + user_id) % 10000
}
///|
pub fn in_percent_bucket(
user_id : String,
salt : String,
percent_bp : Int,
) -> Bool {
let safe_percent = clamp(percent_bp, 0, 10000)
rollout_bucket(user_id, salt) < safe_percent
}
///|
pub fn cohort_label(
user_id : String,
salt : String,
ring_count : Int,
) -> String {
let safe_count = if ring_count <= 0 { 1 } else { ring_count }
let index = stable_hash("ring:" + salt + ":" + user_id) % safe_count
"ring-" + index.to_string()
}
///|
pub fn split_bucket(bucket_bp : Int) -> String {
if bucket_bp < 1000 {
"canary-seed"
} else if bucket_bp < 3000 {
"early-adopter"
} else if bucket_bp < 7000 {
"mainline"
} else if bucket_bp < 9000 {
"late-majority"
} else {
"safety-tail"
}
}
///|
pub fn stable_sample_key(user : UserProfile, salt : String) -> String {
user.id +
":" +
region_text(user.region) +
":" +
tier_text(user.tier) +
":" +
salt
}
///|
pub fn traffic_weighted_bucket(user : UserProfile, salt : String) -> Int {
let base = rollout_bucket(user.id, salt)
let weight = clamp(user.traffic_units, 1, 100)
(base + weight * 37) % 10000
}