fix(workforce): a crew should not read as an alphabetical run

Seeding the name pick with the role index (0..n) started every crew at the top
of the pool and took the next free names, so the first mission after the switch
to per-mission crews hired Aarav, Abebe, Adaora, Adrian, Agnieszka. Unique and
correct, and transparently generated.

Seed from the claw's own uuid instead. UUIDv7 puts its random bytes LAST — the
leading bytes are a timestamp, which would cluster the same way — so the tail
is what spreads five picks across the whole pool.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-10 15:57:54 -07:00
co-authored by Claude Opus 5
parent 98037f9b3e
commit d3a398716b
2 changed files with 56 additions and 2 deletions
+37
View File
@@ -111,6 +111,43 @@ mod tests {
assert!(NAMES.len() >= 150, "pool too small for one crew per mission");
}
/// A crew should not read as an alphabetical run.
///
/// With the role index as the seed, every crew started at the top of the
/// pool and took the next free names — the first real mission hired Aarav,
/// Abebe, Adaora, Adrian, Agnieszka. Unique and correct, and obviously
/// generated. Callers now seed from the claw's uuid tail, so this checks
/// that well-spread seeds actually land in different regions of the pool
/// rather than clustering at one end.
#[test]
fn spread_seeds_do_not_produce_an_alphabetical_run() {
let index_of = |n: &str| NAMES.iter().position(|c| *c == n).expect("name in pool");
let seeds = [
0x9e37_79b9_7f4a_7c15u64,
0x1234_5678_9abc_def0,
0xfeed_face_dead_beef,
0x0f0f_0f0f_f0f0_f0f0,
0xa5a5_5a5a_c3c3_3c3c,
];
let mut taken: Vec<String> = Vec::new();
let mut positions = Vec::new();
for s in seeds {
let n = pick(&taken, s);
positions.push(index_of(&n) as i64);
taken.push(n);
}
// Adjacent picks landing within a couple of slots of each other is the
// clustering signature; require the crew to span a real distance.
let (min, max) = (
*positions.iter().min().unwrap(),
*positions.iter().max().unwrap(),
);
assert!(
max - min > (NAMES.len() as i64) / 3,
"crew clustered in one region of the pool: {positions:?}"
);
}
/// The scenario the operator actually asked for: consecutive missions must
/// not hand back the same names. Reuse is off, so mission two staffs from
/// what mission one left.