use std::collections::HashSet;
use crate::conscience::{self, Challenge, Names};
pub const TOKENS: usize = 12_000;
pub const QUESTION_CHARS: usize = 240;
pub const CONSEQUENCE_CHARS: usize = 160;
pub use borg_trace::CHARS_PER_TOKEN;
pub fn on() -> bool {
!std::env::var("BORG_STOP_POOL").is_ok_and(|v| v.trim().eq_ignore_ascii_case("shortlist"))
}
pub fn classic() -> bool {
std::env::var("BORG_STOP_FORMAT").is_ok_and(|v| v.trim().eq_ignore_ascii_case("classic"))
}
pub fn graded() -> bool {
!std::env::var("BORG_STOP_GRADED").is_ok_and(|v| v.trim().eq_ignore_ascii_case("off"))
}
pub fn tokens() -> usize {
std::env::var("BORG_STOP_POOL_TOKENS").ok().and_then(|v| v.trim().parse().ok()).unwrap_or(TOKENS)
}
pub fn line(c: &Challenge) -> String {
let flat = |s: &str, n: usize| crate::predict::clip(&s.split_whitespace().collect::<Vec<_>>().join(" "), n);
let q = flat(&c.question, QUESTION_CHARS);
if c.consequence.trim().is_empty() { format!("[{}] {q}", c.id) } else { format!("[{}] {q} -> {}", c.id, flat(&c.consequence, CONSEQUENCE_CHARS)) }
}
pub fn cost(c: &Challenge) -> usize {
(line(c).chars().count() + 1).div_ceil(CHARS_PER_TOKEN)
}
pub fn eligible<'a>(pool: &'a [Challenge], ranked: &[(f64, &'a Challenge)], scope_text: &str) -> Vec<&'a Challenge> {
let have = Names::of(scope_text);
let successors = crate::trust::successors(pool);
let recalled: HashSet<&str> = ranked.iter().map(|(_, c)| c.id.as_str()).collect();
pool.iter()
.filter(|c| !c.off_system && conscience::in_scope(c, &have) && !successors.contains_key(&c.id))
.filter(|c| !c.dormant || recalled.contains(c.id.as_str()))
.collect()
}
pub fn select<'a>(
eligible: Vec<&'a Challenge>,
shortlist: impl FnOnce() -> Vec<&'a Challenge>,
ranked: &[(f64, &'a Challenge)],
tokens: usize,
) -> (Vec<&'a Challenge>, bool) {
let f = select_filled(eligible, shortlist, ranked, tokens);
(f.shown, f.fell)
}
#[derive(Debug, Clone)]
pub struct Filled<'a> {
pub shown: Vec<&'a Challenge>,
pub fell: bool,
pub order: Vec<&'a Challenge>,
pub dropped: Vec<&'a Challenge>,
}
pub fn select_filled<'a>(
eligible: Vec<&'a Challenge>,
shortlist: impl FnOnce() -> Vec<&'a Challenge>,
ranked: &[(f64, &'a Challenge)],
tokens: usize,
) -> Filled<'a> {
let allowed: HashSet<&str> = eligible.iter().map(|c| c.id.as_str()).collect();
let mut rest: Vec<&Challenge> = eligible.clone();
rest.sort_by_key(|l| std::cmp::Reverse(l.origin_ms));
let fits = eligible.iter().map(|c| cost(c)).sum::<usize>() <= tokens;
let first = if fits { Vec::new() } else { shortlist() };
let order = first.into_iter().chain(ranked.iter().map(|(_, c)| *c)).chain(rest);
let (mut picked, mut dropped): (Vec<&Challenge>, Vec<&Challenge>) = (Vec::new(), Vec::new());
let mut met: HashSet<&str> = HashSet::new();
let mut used = 0;
for c in order {
if !allowed.contains(c.id.as_str()) || !met.insert(c.id.as_str()) {
continue;
}
let k = cost(c);
if fits || used + k <= tokens {
used += k;
picked.push(c);
} else {
dropped.push(c);
}
}
if fits {
return Filled { shown: eligible, fell: false, order: picked, dropped };
}
let shown: HashSet<&str> = picked.iter().map(|c| c.id.as_str()).collect();
Filled { shown: eligible.into_iter().filter(|c| shown.contains(c.id.as_str())).collect(), fell: true, order: picked, dropped }
}
pub const TAIL_SAMPLE: usize = 2;
#[must_use]
pub fn tail<'a>(dropped: &[&'a Challenge], seed: u64) -> (Vec<(usize, &'a Challenge)>, f64) {
let (n, k) = (dropped.len(), TAIL_SAMPLE.min(dropped.len()));
if k == 0 {
return (Vec::new(), 0.0);
}
let mut at: Vec<usize> = (0..n).collect();
let mut rng = crate::memory::Rng::new(seed);
for i in 0..k {
let j = i + (rng.next_u64() % (n - i) as u64) as usize;
at.swap(i, j);
}
let mut drawn = at[..k].to_vec();
drawn.sort_unstable();
(drawn.into_iter().map(|i| (i, dropped[i])).collect(), k as f64 / n as f64)
}
#[cfg(test)]
mod tests {
use super::*;
fn lesson(id: &str, q: &str, ms: i64) -> Challenge {
Challenge { id: id.into(), question: q.into(), consequence: "bad".into(), origin_ms: ms, ..Default::default() }
}
fn ids(v: &[&Challenge]) -> Vec<String> {
v.iter().map(|c| c.id.clone()).collect()
}
#[test]
fn a_line_is_trimmed_and_flat() {
let c = Challenge { id: "L1".into(), question: format!("Did I\n check {}?", "x".repeat(500)), consequence: "c\nd".into(), ..Default::default() };
let l = line(&c);
assert!(l.starts_with("[L1] Did I check x") && l.ends_with("… -> c d") && !l.contains('\n'), "{l}");
assert!(l.chars().count() < QUESTION_CHARS + 20, "{}", l.len());
assert_eq!(line(&Challenge { id: "q".into(), question: "Q?".into(), ..Default::default() }), "[q] Q?");
}
#[test]
fn the_pool_honours_scope_system_supersession_and_dormancy() {
let mut pool = vec![
lesson("free", "generic question about tests", 1),
Challenge { scope: vec!["Orion".into()], ..lesson("orion", "orion drain rule", 2) },
Challenge { scope: vec!["Beacon".into()], ..lesson("beacon", "beacon relaxed baseline", 3) },
Challenge { off_system: true, ..lesson("other", "another system", 4) },
lesson("old", "the old rule about locks", 5),
Challenge { supersedes: vec!["old".into()], source: "queen".into(), ..lesson("new", "the new rule about locks", 6) },
Challenge { dormant: true, ..lesson("sleepy", "dormant lesson about caches", 7) },
Challenge { dormant: true, ..lesson("woken", "dormant lesson about websocket deadlocks", 8) },
];
pool[5].trust = 0.9;
let scope = "repo orion-text\nwork on the websocket deadlocks";
let ranked = conscience::rank_scoped(&pool, "websocket deadlocks", scope);
assert_eq!(ids(&eligible(&pool, &ranked, scope)), ["free", "orion", "new", "woken"]);
}
#[test]
fn everything_fits_in_store_order_else_the_shortlist_order_fills_the_budget() {
let pool: Vec<Challenge> = (0..10).map(|i| lesson(&format!("L{i}"), &format!("question number {i} about topic{i}"), i)).collect();
let all: Vec<&Challenge> = pool.iter().collect();
let each = cost(&pool[0]);
let mut called = false;
let ranked = vec![(3.0, &pool[7]), (2.0, &pool[5]), (1.0, &pool[2])];
let f = select_filled(
all.clone(),
|| {
called = true;
vec![]
},
&ranked,
10 * each,
);
assert_eq!((f.shown.len(), f.fell, called, f.dropped.len()), (10, false, false, 0), "fits: no shortlist needed");
assert_eq!(ids(&f.order), ["L7", "L5", "L2", "L9", "L8", "L6", "L4", "L3", "L1", "L0"]);
let f = select_filled(all, || vec![&pool[7], &pool[2]], &ranked, 4 * each);
assert!(f.fell);
assert_eq!(ids(&f.shown), ["L2", "L5", "L7", "L9"]);
assert_eq!(ids(&f.order), ["L7", "L2", "L5", "L9"], "the order they took the budget in");
assert!(f.shown.iter().map(|c| cost(c)).sum::<usize>() <= 4 * each);
assert_eq!(ids(&f.dropped), ["L8", "L6", "L4", "L3", "L1", "L0"]);
}
#[test]
fn the_budget_never_shows_a_lesson_outside_the_eligible_set() {
let pool: Vec<Challenge> = (0..4).map(|i| lesson(&format!("L{i}"), "q", i)).collect();
let elig = vec![&pool[0], &pool[1]];
let f = select_filled(elig, || vec![&pool[3], &pool[2], &pool[1]], &[(1.0, &pool[3])], 1);
assert!(f.shown.is_empty() && f.order.is_empty(), "budget too small for any line");
assert_eq!(ids(&f.dropped), ["L1", "L0"], "dropped, but never one outside the eligible set");
let f = select_filled(vec![&pool[0], &pool[1]], || vec![&pool[3], &pool[1]], &[], cost(&pool[1]));
assert_eq!((ids(&f.shown), f.fell, ids(&f.order), ids(&f.dropped)), (vec!["L1".to_string()], true, vec!["L1".to_string()], vec!["L0".to_string()]));
}
#[test]
fn the_tail_is_a_small_uniform_seeded_sample_of_what_the_budget_dropped() {
let pool: Vec<Challenge> = (0..10).map(|i| lesson(&format!("D{i}"), "q", i)).collect();
let dropped: Vec<&Challenge> = pool.iter().collect();
let (t, pt) = tail(&dropped, 7);
assert_eq!((t.len(), pt), (TAIL_SAMPLE, TAIL_SAMPLE as f64 / 10.0));
assert!(t.windows(2).all(|w| w[0].0 < w[1].0), "in the order the fill met them: {t:?}");
assert!(t.iter().all(|(i, c)| dropped[*i].id == c.id));
let at = |t: &[(usize, &Challenge)]| t.iter().map(|(i, _)| *i).collect::<Vec<_>>();
assert_eq!(at(&tail(&dropped, 7).0), at(&t), "seeded");
let mut hits = [0usize; 10];
for seed in 0..10_000 {
for (i, _) in tail(&dropped, seed).0 {
hits[i] += 1;
}
}
assert!(hits.iter().all(|h| (1_700..2_300).contains(h)), "{hits:?}");
let (all, pt) = tail(&dropped[..1], 3);
assert_eq!((all.len(), pt), (1, 1.0), "fewer than the sample: all of them, surely");
let (none, pt) = tail(&[], 3);
assert!(none.is_empty() && pt == 0.0);
}
}