use std::collections::{BTreeMap, HashMap};
use super::records::Stratum;
#[derive(Debug, Clone)]
pub struct Candidate {
pub sha: String,
pub repo: String,
pub author: String,
pub stratum: Stratum,
}
#[derive(Debug, Clone, Copy)]
pub struct DrawParams {
pub size: usize,
pub cap: usize,
pub seed: u64,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct Draw {
pub selected: Vec<usize>,
pub population: BTreeMap<Stratum, usize>,
pub sampled: BTreeMap<Stratum, usize>,
}
struct SplitMix64(u64);
impl SplitMix64 {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn below(&mut self, bound: u64) -> u64 {
let zone = u64::MAX - (u64::MAX % bound);
loop {
let v = self.next();
if v < zone {
return v % bound;
}
}
}
}
fn stratum_seed(seed: u64, stratum: Stratum) -> u64 {
let mut h = blake3::Hasher::new();
h.update(&seed.to_le_bytes());
h.update(stratum.as_str().as_bytes());
let bytes = h.finalize();
let mut first = [0u8; 8];
first.copy_from_slice(&bytes.as_bytes()[..8]);
u64::from_le_bytes(first)
}
pub(crate) fn seeded_shuffle<T>(items: &mut [T], seed: u64) {
let mut rng = SplitMix64(seed);
for i in (1..items.len()).rev() {
let j = rng.below(i as u64 + 1) as usize;
items.swap(i, j);
}
}
pub fn allocate(size: usize, capacity: &[usize]) -> Vec<usize> {
let mut alloc = vec![0usize; capacity.len()];
let mut active: Vec<usize> = (0..capacity.len()).filter(|&i| capacity[i] > 0).collect();
let mut remaining = size;
while remaining > 0 && !active.is_empty() {
let share = remaining / active.len();
let saturated: Vec<usize> = active
.iter()
.copied()
.filter(|&i| capacity[i] <= share)
.collect();
if saturated.is_empty() {
let extra = remaining % active.len();
for (pos, &i) in active.iter().enumerate() {
alloc[i] = share + usize::from(pos < extra);
}
break;
}
for &i in &saturated {
alloc[i] = capacity[i];
remaining -= capacity[i];
}
active.retain(|i| !saturated.contains(i));
}
debug_assert!(alloc.iter().zip(capacity).all(|(a, c)| a <= c));
alloc
}
pub fn draw(candidates: &[Candidate], params: &DrawParams) -> Draw {
let mut by_stratum: BTreeMap<Stratum, Vec<usize>> = BTreeMap::new();
for (i, c) in candidates.iter().enumerate() {
by_stratum.entry(c.stratum).or_default().push(i);
}
let mut eligible: Vec<(Stratum, Vec<usize>)> = Vec::new();
for stratum in Stratum::ALL {
let Some(members) = by_stratum.get_mut(&stratum) else {
continue;
};
members.sort_by(|a, b| candidates[*a].sha.cmp(&candidates[*b].sha));
seeded_shuffle(members, stratum_seed(params.seed, stratum));
let mut per_repo: HashMap<&str, usize> = HashMap::new();
let mut per_author: HashMap<&str, usize> = HashMap::new();
let mut walk = Vec::new();
for &i in members.iter() {
let c = &candidates[i];
let r = per_repo.entry(c.repo.as_str()).or_default();
let a = per_author.entry(c.author.as_str()).or_default();
if *r < params.cap && *a < params.cap {
*r += 1;
*a += 1;
walk.push(i);
}
}
eligible.push((stratum, walk));
}
let capacity: Vec<usize> = eligible.iter().map(|(_, w)| w.len()).collect();
let alloc = allocate(params.size, &capacity);
let mut out = Draw::default();
for ((stratum, walk), k) in eligible.into_iter().zip(alloc) {
out.population
.insert(stratum, by_stratum.get(&stratum).map_or(0, Vec::len));
out.sampled.insert(stratum, k);
out.selected.extend(walk.into_iter().take(k));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::classify::TraceTier;
fn population() -> Vec<Candidate> {
let strata = [Stratum::Exact, Stratum::RegexHigh, Stratum::CatchAll];
(0..600)
.map(|i| Candidate {
sha: format!("{:040x}", i * 7919),
repo: format!("repo{}", i % 13),
author: format!("dev{}", i % 17),
stratum: strata[i % 3],
})
.collect()
}
#[test]
fn allocate_redistributes_small_strata() {
assert_eq!(allocate(10, &[100, 100, 100]), vec![4, 3, 3]);
assert_eq!(allocate(12, &[2, 100, 100]), vec![2, 5, 5]);
assert_eq!(allocate(12, &[0, 100, 1, 3]), vec![0, 8, 1, 3]);
assert_eq!(allocate(50, &[3, 4]), vec![3, 4]);
}
#[test]
fn same_seed_same_sample() {
let pop = population();
let p = DrawParams {
size: 60,
cap: 5,
seed: 42,
};
let shas = |pop: &[Candidate], d: &Draw| -> Vec<String> {
d.selected.iter().map(|&i| pop[i].sha.clone()).collect()
};
let a = shas(&pop, &draw(&pop, &p));
let mut reversed = pop.clone();
reversed.reverse();
let b = shas(&reversed, &draw(&reversed, &p));
assert_eq!(a, b);
let c = shas(&pop, &draw(&pop, &DrawParams { seed: 43, ..p }));
assert_eq!(c.len(), a.len());
assert_ne!(a, c);
}
#[test]
fn caps_hold_per_stratum() {
let pop = population();
let d = draw(
&pop,
&DrawParams {
size: 300,
cap: 2,
seed: 7,
},
);
let mut repo: HashMap<(Stratum, &str), usize> = HashMap::new();
let mut author: HashMap<(Stratum, &str), usize> = HashMap::new();
for &i in &d.selected {
let c = &pop[i];
*repo.entry((c.stratum, c.repo.as_str())).or_default() += 1;
*author.entry((c.stratum, c.author.as_str())).or_default() += 1;
}
assert!(repo.values().all(|&n| n <= 2));
assert!(author.values().all(|&n| n <= 2));
assert!(d.sampled.values().all(|&n| n <= 26));
assert_eq!(d.population.values().sum::<usize>(), 600);
}
#[test]
fn strata_partition_tiers() {
use Stratum as S;
use TraceTier as T;
assert_eq!(S::classify(T::Exact, "feature", 0.95), S::Exact);
assert_eq!(S::classify(T::Regex, "bugfix", 0.9), S::RegexHigh);
assert_eq!(S::classify(T::Regex, "bugfix", 0.55), S::RegexMid);
assert_eq!(S::classify(T::Regex, "bugfix", 0.8), S::RegexOther);
assert_eq!(S::classify(T::CatchAll, "maintenance", 0.3), S::CatchAll);
assert_eq!(
S::classify(T::Unclassified, "uncategorized", 0.0),
S::Unknown
);
assert_eq!(S::classify(T::Fuzzy, "merge", 0.6), S::Fuzzy);
assert_eq!(S::classify(T::WeightedSum, "test", 0.5), S::WeightedSum);
assert_eq!(S::classify(T::Llm, "feature", 0.8), S::Other);
}
}