use std::collections::{HashMap, HashSet};
use crate::conscience::Challenge;
use crate::outcome::{self, Outcome};
pub const D: f64 = 0.5;
pub const REGRET: f64 = 1.0;
pub const TESTED_W: f64 = 0.5;
pub const VETOED_W: u32 = 3;
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)]
pub struct Weights {
pub vetoed: f64,
pub regret: f64,
pub tested: f64,
}
pub const WEIGHTS: Weights = Weights { vetoed: VETOED_W as f64, regret: REGRET, tested: TESTED_W };
pub const D_CONSOLIDATED: f64 = 0.3;
pub const CONSOLIDATE_SESSIONS: usize = 3;
pub const DORMANT_BELOW: f64 = -2.0;
#[derive(Debug, Default)]
pub struct Clock {
first: Vec<(i64, String)>,
at: HashMap<String, u64>,
repos: Vec<HashSet<String>>,
}
impl Clock {
pub fn of(outcomes: &[Outcome]) -> Self {
let mut first: HashMap<&str, i64> = HashMap::new();
let mut repos: HashMap<&str, HashSet<String>> = HashMap::new();
for o in outcomes.iter().filter(|o| ticks(&o.kind) && outcome::is_result(o)) {
let m = first.entry(o.session.as_str()).or_insert(o.ms);
*m = (*m).min(o.ms);
if !o.repo.is_empty() {
repos.entry(o.session.as_str()).or_default().insert(o.repo.clone());
}
}
Self::from_sessions(first.into_iter().map(|(s, ms)| (s.to_string(), ms, repos.remove(s).unwrap_or_default())))
}
pub fn from_sessions(sessions: impl IntoIterator<Item = (String, i64, HashSet<String>)>) -> Self {
let mut rows: Vec<((i64, String), HashSet<String>)> = sessions.into_iter().map(|(s, ms, r)| ((ms, s), r)).collect();
rows.sort_by(|a, b| a.0.cmp(&b.0));
let at = rows.iter().enumerate().map(|(i, ((_, s), _))| (s.clone(), i as u64)).collect();
let (first, repos) = rows.into_iter().unzip();
Clock { first, at, repos }
}
pub fn eligible(&self, c: &Challenge) -> Option<Vec<u64>> {
if c.reach != crate::reach::REPO || c.repo.is_empty() {
return None;
}
let tagged = outcome::tag(&c.repo);
Some((0..self.repos.len() as u64).filter(|t| self.repos[*t as usize].iter().any(|r| r.eq_ignore_ascii_case(&c.repo) || *r == tagged)).collect())
}
pub fn now(&self, session: Option<&str>) -> u64 {
session.and_then(|s| self.at.get(s).copied()).unwrap_or(self.first.len() as u64)
}
pub fn of_session(&self, session: &str) -> u64 {
self.at.get(session).copied().unwrap_or(self.first.len() as u64)
}
pub fn at_ms(&self, ms: i64) -> u64 {
self.first.partition_point(|(m, _)| *m < ms) as u64
}
}
pub fn activation(ages: impl IntoIterator<Item = u64>, d: f64) -> f64 {
ages.into_iter().map(|a| (a.max(1) as f64).powf(-d)).sum::<f64>().ln()
}
pub fn squash(a: f64) -> f64 {
2.0 / (1.0 + (-a / 2.0).exp())
}
#[derive(Debug, Clone, PartialEq)]
pub struct Memory {
pub activation: f64,
pub changed: u32,
pub ignored: u32,
pub corrected: u32,
pub vetoed: u32,
pub regretted: f64,
pub tested: u32,
pub consolidated: bool,
pub dormant: bool,
}
impl Memory {
pub fn alpha_with(&self, w: &Weights) -> f64 {
1.0 + self.changed as f64 + w.tested * self.tested as f64
}
pub fn beta_with(&self, w: &Weights) -> f64 {
(1 + self.ignored + outcome::CORRECTED_W * self.corrected) as f64 + w.vetoed * self.vetoed as f64 + w.regret * self.regretted
}
pub fn alpha(&self) -> f64 {
self.alpha_with(&WEIGHTS)
}
pub fn beta(&self) -> f64 {
self.beta_with(&WEIGHTS)
}
pub fn mean(&self) -> f64 {
self.alpha() / (self.alpha() + self.beta())
}
pub fn weight(&self, id: &str, session: Option<&str>) -> f64 {
let x = match session {
Some(s) => draw(self.alpha(), self.beta(), seed(s, id)),
None => self.mean(),
};
squash(self.activation) * (0.5 + x)
}
}
pub fn ticks(kind: &str) -> bool {
outcome::SHARED_KINDS.contains(&kind) && kind != outcome::TESTED
}
pub fn result_of(kind: &str) -> Option<u8> {
match kind {
outcome::VETOED => Some(3),
outcome::CORRECTED => Some(2),
outcome::CHANGED => Some(1),
outcome::IGNORED => Some(0),
_ => None,
}
}
#[derive(Debug, Default)]
pub struct Folded<'a> {
pub clock: Clock,
pub result: HashMap<&'a str, HashMap<&'a str, u8>>,
pub uses: HashMap<&'a str, HashSet<&'a str>>,
pub repos: HashMap<&'a str, HashSet<&'a str>>,
pub regret: Vec<(&'a str, &'a str, f64)>,
pub tested: HashMap<&'a str, HashSet<&'a str>>,
pub corrections: HashMap<&'a str, HashSet<i64>>,
}
impl<'a> Folded<'a> {
pub fn of(outcomes: &'a [Outcome]) -> Self {
let mut f = Folded { clock: Clock::of(outcomes), ..Folded::default() };
let mut regret: HashMap<(&str, &str), (f64, &str)> = HashMap::new();
for o in outcomes.iter().filter(|o| outcome::is_result(o)) {
if is_correction(o) {
f.corrections.entry(o.session.as_str()).or_default().insert(o.ms);
}
if o.kind == outcome::TESTED {
for l in &o.lessons {
f.tested.entry(l.as_str()).or_default().insert(o.session.as_str());
}
continue;
}
if o.kind == outcome::REGRETTED {
for l in &o.lessons {
let e = regret.entry((l.as_str(), o.cause.as_str())).or_insert((0.0, o.session.as_str()));
e.0 = e.0.max(o.credit.clamp(0.0, 1.0));
}
continue;
}
let Some(r) = result_of(&o.kind) else { continue };
for l in &o.lessons {
let e = f.result.entry(l.as_str()).or_default().entry(o.session.as_str()).or_insert(r);
*e = (*e).max(r);
if r == 1 {
f.uses.entry(l.as_str()).or_default().insert(o.session.as_str());
f.repos.entry(l.as_str()).or_default().insert(o.repo.as_str());
}
}
}
f.regret = blame(regret.into_iter().map(|((l, _), (credit, s))| (l, s, credit)));
f
}
}
pub fn blame<L: Copy + Ord, S: Copy + Ord>(regrets: impl IntoIterator<Item = (L, S, f64)>) -> Vec<(L, S, f64)> {
let mut v: Vec<(L, S, f64)> = regrets.into_iter().collect();
v.sort_unstable_by(|a, b| (a.0, a.1).cmp(&(b.0, b.1)).then(a.2.total_cmp(&b.2)));
let mut out: Vec<(L, S, f64)> = Vec::new();
for (l, s, credit) in v {
match out.last_mut() {
Some(last) if (last.0, last.1) == (l, s) => last.2 += credit,
_ => out.push((l, s, credit)),
}
}
out
}
pub fn derive(outcomes: &[Outcome], lessons: &[Challenge], session: Option<&str>) -> HashMap<String, Memory> {
derive_folded(Folded::of(outcomes), lessons, session)
}
pub fn derive_folded(f: Folded<'_>, lessons: &[Challenge], session: Option<&str>) -> HashMap<String, Memory> {
let Folded { clock, result, uses, repos, regret, tested, corrections } = f;
let now = clock.now(session);
let mut capped: Vec<(&str, f64)> = regret
.into_iter()
.filter(|(l, s, _)| result.get(l).and_then(|m| m.get(s)).is_none_or(|r| *r < 2))
.map(|(l, s, credit)| (l, credit.min(regret_cap(corrections.get(s).map_or(0, HashSet::len)))))
.collect();
capped.sort_unstable_by(|a, b| a.0.cmp(b.0).then(a.1.total_cmp(&b.1)));
let mut regretted: HashMap<&str, f64> = HashMap::new();
for (l, credit) in capped {
*regretted.entry(l).or_default() += credit;
}
let mut eligible: HashMap<(&str, &str), Option<Vec<u64>>> = HashMap::new();
lessons
.iter()
.map(|c| {
let per = result.get(c.id.as_str());
let count = |k: u8| per.map_or(0, |m| m.values().filter(|r| **r == k).count() as u32);
let (changed, ignored, corrected, vetoed) = (count(1), count(0), count(2), count(3));
let used: Vec<u64> = uses.get(c.id.as_str()).map(|u| u.iter().map(|s| clock.of_session(s)).collect()).unwrap_or_default();
let changed_sessions = used.len();
let consolidated = changed_sessions >= CONSOLIDATE_SESSIONS && (repos.get(c.id.as_str()).is_some_and(|r| r.len() >= 2) || c.engineers.len() >= 2);
let d = if consolidated { D_CONSOLIDATED } else { D };
let born = clock.at_ms(c.origin_ms);
let eligible = eligible.entry((c.reach.as_str(), c.repo.as_str())).or_insert_with(|| clock.eligible(c));
let rank = |t: u64| eligible.as_ref().map_or(t, |e| e.partition_point(|x| *x < t) as u64);
let now = rank(now);
let ages = used.into_iter().chain([born]).map(|t| now.saturating_sub(rank(t)) + 1);
let a = activation(ages, d);
let regretted = regretted.get(c.id.as_str()).copied().unwrap_or(0.0);
let tested = tested.get(c.id.as_str()).map_or(0, |t| t.iter().filter(|s| per.and_then(|m| m.get(**s)).is_none_or(|r| *r < 2)).count() as u32);
let m = Memory { activation: a, changed, ignored, corrected, vetoed, regretted, tested, consolidated, dormant: !consolidated && a < DORMANT_BELOW };
(c.id.clone(), m)
})
.collect()
}
pub fn regret_cap(corrections: usize) -> f64 {
corrections.max(1) as f64
}
fn is_correction(o: &Outcome) -> bool {
is_correction_of(&o.kind, &o.cause)
}
pub fn is_correction_of(kind: &str, cause: &str) -> bool {
match kind {
outcome::CORRECTED | outcome::VETOED => true,
outcome::MISSED => cause.is_empty(),
_ => false,
}
}
pub fn seed(session: &str, lesson: &str) -> u64 {
let mut h: u64 = 0xcbf29ce484222325;
for b in session.bytes().chain([0]).chain(lesson.bytes()) {
h ^= b as u64;
h = h.wrapping_mul(0x100000001b3);
}
h
}
pub struct Rng(u64);
impl Rng {
pub fn new(seed: u64) -> Self {
Rng(seed)
}
pub fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9e3779b97f4a7c15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
z ^ (z >> 31)
}
pub fn unit(&mut self) -> f64 {
((self.next_u64() >> 11) as f64 + 0.5) / (1u64 << 53) as f64
}
}
pub fn round_seeded(x: f64, seed: u64) -> u32 {
let (whole, frac) = (x.floor(), x - x.floor());
match frac == 0.0 || Rng::new(seed).unit() >= frac {
true => whole as u32,
false => whole as u32 + 1,
}
}
pub fn draw(alpha: f64, beta: f64, seed: u64) -> f64 {
sample_beta(round_seeded(alpha, seed ^ 0x416c_7068), round_seeded(beta, seed ^ 0x5265_6772), seed)
}
pub fn sample_beta(a: u32, b: u32, seed: u64) -> f64 {
let scale = (a.max(b) as f64 / 1000.0).max(1.0);
let (a, b) = (((a as f64 / scale).round() as u32).max(1), ((b as f64 / scale).round() as u32).max(1));
let mut rng = Rng::new(seed);
let mut gamma = |k: u32| (0..k).map(|_| -rng.unit().ln()).sum::<f64>();
let x = gamma(a);
let y = gamma(b);
x / (x + y)
}
pub fn shuffle<T>(items: &mut [T], session: &str) {
let mut rng = Rng::new(seed(session, "#order"));
for i in (1..items.len()).rev() {
let j = (rng.next_u64() % (i as u64 + 1)) as usize;
items.swap(i, j);
}
}
pub struct Distinctive {
df: HashMap<String, usize>,
n: usize,
}
impl Distinctive {
pub fn new(all: &[Challenge]) -> Self {
let mut df: HashMap<String, usize> = HashMap::new();
for c in all {
for t in crate::conscience::tokens(&c.question).into_iter().collect::<HashSet<_>>() {
*df.entry(t).or_default() += 1;
}
}
Distinctive { df, n: all.len().max(1) }
}
pub fn is(&self, t: &str) -> bool {
self.df.get(t).is_some_and(|d| 3 * d <= self.n)
}
pub fn words(&self, text: &str) -> HashSet<String> {
crate::conscience::tokens(text).into_iter().filter(|t| self.is(t)).collect()
}
}
pub const NOVEL_WORDS: usize = 3;
pub fn resolve(asked: &[&Challenge], all: &[Challenge], before: &str, after: &str, final_msg: &str, min_new: usize) -> (Vec<String>, Vec<String>) {
let words = Distinctive::new(all);
let had: HashSet<String> = crate::conscience::tokens(before).into_iter().collect();
let new: HashSet<String> = words.words(&format!("{after}\n{final_msg}")).into_iter().filter(|w| !had.contains(w)).collect();
let lower = final_msg.to_lowercase();
let (mut changed, mut ignored) = (Vec::new(), Vec::new());
for c in asked {
let verdict = declared(&lower, &c.id.to_lowercase());
let applies = match verdict {
Some(v) => v,
None => words.words(&c.question).intersection(&new).count() >= min_new,
};
match applies {
true => changed.push(c.id.clone()),
false => ignored.push(c.id.clone()),
}
}
(changed, ignored)
}
pub fn declared(lower: &str, id: &str) -> Option<bool> {
let mut out = None;
let mut from = 0;
while let Some(i) = lower[from..].find(id) {
let at = from + i + id.len();
let rest: String = lower[at..].chars().take(16).collect();
let rest = rest.trim_start_matches(|c: char| c == ']' || c == '`' || c == '*' || c == ':' || c == '-' || c == '–' || c.is_whitespace());
if rest.starts_with("applies") || rest.starts_with("apply") {
out = Some(true);
} else if rest.starts_with("skip") {
out = Some(false);
}
from = at;
}
out
}
pub fn strip_declaration(text: &str, id: &str) -> String {
let (lower, id) = (text.to_ascii_lowercase(), id.to_ascii_lowercase());
if id.is_empty() {
return text.to_string();
}
let mut out = String::with_capacity(text.len());
let (mut from, mut kept) = (0, 0);
while let Some(i) = lower[from..].find(&id) {
let start = from + i;
let at = start + id.len();
let gap = lower[at..].len()
- lower[at..].trim_start_matches(|c: char| c == ']' || c == '`' || c == '*' || c == ':' || c == '-' || c == '–' || c.is_whitespace()).len();
let rest = &lower[at + gap..];
let word = ["applies", "apply", "skip"].iter().find(|w| rest.starts_with(**w)).map_or(0, |w| w.len());
let opens = start == 0 || !lower[..start].ends_with(|c: char| c.is_ascii_alphanumeric());
if word > 0 && opens && gap > 0 {
out.push_str(&text[kept..start]);
kept = at + gap + word;
}
from = at;
}
out.push_str(&text[kept..]);
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::outcome::{CHANGED, CORRECTED, IGNORED, NOT_CORRECTED};
#[test]
fn a_stripped_declaration_is_gone_for_the_judge_and_the_rest_stays() {
for said in ["Mdrain: applies. Replaced the reset arc.", "- [Mdrain] - skip", "`Mdrain`: APPLY", "ok mdrain:skip then"] {
let s = strip_declaration(said, "Mdrain");
assert_eq!(declared(&s.to_lowercase(), "mdrain"), None, "{said} -> {s}");
assert!(declared(&said.to_lowercase(), "mdrain").is_some(), "{said}");
}
let both = "Mdrain: applies. New: skip. Mdrain is the budget lesson.";
let s = strip_declaration(both, "Mdrain");
assert_eq!(declared(&s.to_lowercase(), "new"), Some(false), "{s}");
assert!(s.contains("Replaced") || s.contains("Mdrain is the budget lesson"), "{s}");
assert_eq!(strip_declaration("no declaration here", "Mdrain"), "no declaration here");
assert_eq!(strip_declaration("XMdrain: applies", "Mdrain"), "XMdrain: applies", "another id ending in it");
}
fn o(kind: &str, session: &str, lesson: &str, ms: i64, repo: &str) -> Outcome {
Outcome { kind: kind.into(), session: session.into(), lessons: vec![lesson.into()], ms, repo: repo.into(), ..Default::default() }
}
fn lesson(id: &str, ms: i64) -> Challenge {
Challenge { id: id.into(), origin_ms: ms, question: format!("question of {id}"), ..Default::default() }
}
fn sessions(n: usize) -> Vec<Outcome> {
(0..n).map(|i| o(outcome::ASKED, &format!("s{i:03}"), "other", 100 * (i as i64 + 1), "r")).collect()
}
#[test]
fn a_new_lesson_is_neutral_and_activation_decays_with_the_session_clock_not_wall_time() {
let l = lesson("L", 0);
let m = &derive(&[], std::slice::from_ref(&l), None)["L"];
assert_eq!((m.activation, m.mean(), m.weight("L", None)), (0.0, 0.5, 1.0));
let m = &derive(&sessions(10), std::slice::from_ref(&l), None)["L"];
assert!((m.activation - (11f64).powf(-0.5).ln()).abs() < 1e-12);
assert!(m.weight("L", None) < 1.0 && m.weight("L", None) > 0.5);
let spread: Vec<Outcome> = sessions(10)
.into_iter()
.enumerate()
.map(|(i, mut x)| {
x.ms = if i < 5 { x.ms } else { x.ms + 365 * 86_400_000 };
x
})
.collect();
assert_eq!(derive(&spread, &[l], None)["L"].activation, m.activation);
}
#[test]
fn a_repo_convention_ages_only_with_the_sessions_of_its_repo_and_a_team_lesson_with_all() {
let conv = Challenge { reach: crate::reach::REPO.into(), repo: "a".into(), ..lesson("C", 0) };
let both = [lesson("T", 0), conv];
let in_repo = |log: Vec<Outcome>, repo: &dyn Fn(usize) -> String| -> Vec<Outcome> {
log.into_iter()
.enumerate()
.map(|(i, mut x)| {
x.repo = repo(i);
x
})
.collect()
};
let m = derive(&in_repo(sessions(60), &|_| "b".into()), &both, None);
assert!(m["T"].dormant);
assert_eq!((m["C"].activation, m["C"].dormant), (0.0, false));
for repo in ["a".to_string(), "A".to_string(), outcome::tag("a")] {
let m = derive(&in_repo(sessions(60), &|_| repo.clone()), &both, None);
assert_eq!(m["C"].activation, m["T"].activation, "{repo}");
assert!(m["C"].dormant, "{repo}");
}
let mut log = in_repo(sessions(40), &|i| if i % 4 == 0 { "a".into() } else { "b".into() });
log.push(o(CHANGED, "s004", "C", 501, "a"));
let m = derive(&log, &both, None);
let want = activation([11, 10], D);
assert!((m["C"].activation - want).abs() < 1e-12, "{} vs {want}", m["C"].activation);
}
#[test]
fn activation_is_monotone_in_uses_and_spaced_uses_outlast_massed_ones() {
let l = lesson("L", 0);
let mut log = sessions(30);
let a0 = derive(&log, std::slice::from_ref(&l), None)["L"].activation;
log.push(o(CHANGED, "s010", "L", 1101, "r"));
let a1 = derive(&log, std::slice::from_ref(&l), None)["L"].activation;
log.push(o(CHANGED, "s020", "L", 2101, "r"));
let a2 = derive(&log, std::slice::from_ref(&l), None)["L"].activation;
assert!(a0 < a1 && a1 < a2, "{a0} {a1} {a2}");
let at = |uses: &[usize]| {
let mut log = sessions(60);
log.extend(uses.iter().map(|i| o(CHANGED, &format!("s{i:03}"), "L", 100 * (*i as i64 + 1) + 1, "r")));
derive(&log, std::slice::from_ref(&l), None)["L"].activation
};
assert!(at(&[1, 20]) > at(&[1, 2]));
}
#[test]
fn belief_counts_each_session_once_corrected_over_changed_over_ignored() {
let l = lesson("L", 0);
let log = vec![
o(CHANGED, "a", "L", 1, "r"),
o(NOT_CORRECTED, "a", "L", 2, "r"), o(IGNORED, "b", "L", 3, "r"),
o(CHANGED, "c", "L", 4, "r"),
o(CORRECTED, "c", "L", 5, "r"), o(NOT_CORRECTED, "d", "L", 6, "r"), ];
let m = &derive(&log, &[l], None)["L"];
assert_eq!((m.changed, m.ignored, m.corrected), (1, 1, 1));
assert_eq!((m.alpha(), m.beta()), (2.0, 4.0));
}
#[test]
fn an_unjudged_settle_is_censored_and_a_later_verdict_counts_once() {
let l = lesson("L", 0);
let mem = |log: &[Outcome]| derive(log, std::slice::from_ref(&l), None)["L"].clone();
let asked = Outcome { check: "c1".into(), ..o(outcome::ASKED, "a", "L", 1, "r") };
let censored = Outcome { check: "c1".into(), reason: outcome::UNJUDGED_NO_CALL.into(), ..o(NOT_CORRECTED, "a", "L", 5, "r") };
let alone = mem(std::slice::from_ref(&asked));
assert_eq!(mem(&[asked.clone(), censored.clone()]), alone, "no belief effect: not an ignore");
for (kind, want) in [(CHANGED, (1, 0)), (IGNORED, (0, 1))] {
let later = Outcome { check: "c1".into(), ..o(kind, "a", "L", 9, "r") };
let m = mem(&[asked.clone(), censored.clone(), later]);
assert_eq!((m.changed, m.ignored), want, "{kind}");
}
}
#[test]
fn a_placebo_or_unclear_result_moves_no_belief() {
let l = lesson("L", 0);
let mem = |log: &[Outcome]| derive(log, std::slice::from_ref(&l), None)["L"].clone();
let asked = o(outcome::ASKED, "a", "L", 1, "r");
let alone = mem(std::slice::from_ref(&asked));
for kind in [CHANGED, IGNORED, CORRECTED, outcome::UNCLEAR] {
let placebo = Outcome { channel: outcome::PLACEBO.into(), ..o(kind, "b", "L", 5, "r") };
assert_eq!(mem(&[asked.clone(), placebo]), alone, "placebo {kind}");
}
assert_eq!(mem(&[asked.clone(), Outcome { channel: "stop".into(), ..o(outcome::UNCLEAR, "a", "L", 5, "r") }]), alone);
}
#[test]
fn a_not_arisen_or_remaining_verdict_moves_no_belief() {
let l = lesson("L", 0);
let mem = |log: &[Outcome]| derive(log, std::slice::from_ref(&l), None)["L"].clone();
let asked = Outcome { channel: "stop".into(), ..o(outcome::ASKED, "a", "L", 1, "r") };
let alone = mem(std::slice::from_ref(&asked));
for kind in [outcome::NOT_ARISEN, outcome::REMAINS] {
for channel in ["stop", "live", "queued", outcome::PLACEBO] {
let v = Outcome { channel: channel.into(), reasons: vec!["only docs changed".into()], ..o(kind, "a", "L", 5, "r") };
assert!(!outcome::is_result(&v), "{kind} {channel}");
assert_eq!(mem(&[asked.clone(), v]), alone, "{kind} {channel}");
}
}
let tests = "fail 2\npass 3\n";
let na = Outcome { channel: "stop".into(), ..o(outcome::NOT_ARISEN, "a", "L", 5, "r") };
assert_eq!(outcome::tested(std::slice::from_ref(&asked), "a", tests).len(), 1, "red then green: credited");
assert!(outcome::tested(&[asked.clone(), na.clone()], "a", tests).is_empty());
let changed = Outcome { channel: "stop".into(), ..o(CHANGED, "a", "L", 6, "r") };
assert_eq!(outcome::tested(&[asked.clone(), na, changed], "a", tests).len(), 1);
let late = Outcome { channel: "stop".into(), ..o(outcome::ASKED, "a", "L", 10, "r") };
let tests = "fail 11\npass 12\n";
for (channel, ms) in [(outcome::PLACEBO, 5), ("queued", 5), ("early", 5), ("stop", 5)] {
let before = Outcome { channel: channel.into(), ..o(outcome::NOT_ARISEN, "a", "L", ms, "r") };
assert_eq!(outcome::tested(&[before, late.clone()], "a", tests).len(), 1, "{channel} not arisen before the ask");
}
let after = Outcome { channel: outcome::PLACEBO.into(), ..o(outcome::NOT_ARISEN, "a", "L", 11, "r") };
assert_eq!(outcome::tested(&[late.clone(), after], "a", tests).len(), 1, "a placebo's verdict after the ask");
let on_it = Outcome { channel: "stop".into(), ..o(outcome::NOT_ARISEN, "a", "L", 11, "r") };
assert!(outcome::tested(&[asked, late, on_it], "a", tests).is_empty(), "the question's own verdict");
}
#[test]
fn one_sessions_regrets_blame_a_lesson_at_most_its_corrections() {
let l = lesson("L", 0);
let mem = |log: &[Outcome]| derive(log, std::slice::from_ref(&l), None)["L"].clone();
let asked = o(outcome::ASKED, "a", "L", 1, "r");
let mut log = vec![asked.clone()];
log.extend((0..5).map(|i| regret("a", "L", 10 + i, &format!("R{i}"), 0.5)));
assert_eq!(mem(&log).regretted, regret_cap(0));
assert_eq!(regret_cap(0), 1.0);
let mut twice = log.clone();
twice.extend([o(CORRECTED, "a", "X", 3, "r"), o(outcome::VETOED, "a", "Y", 4, "r"), o(CORRECTED, "a", "Z", 4, "r")]);
assert_eq!(mem(&twice).regretted, 2.0);
let mut other = log.clone();
other.extend([o(outcome::ASKED, "b", "L", 2, "r"), regret("b", "L", 20, "Rb", 0.5)]);
assert_eq!(mem(&other).regretted, 1.5);
}
#[test]
fn the_misses_a_regret_writes_leave_its_sessions_cap_unchanged() {
let l = lesson("L", 0);
let mem = |log: &[Outcome]| derive(log, std::slice::from_ref(&l), None)["L"].clone();
let mut log = vec![o(outcome::ASKED, "a", "L", 1, "r")];
for i in 0..5 {
log.push(regret("a", "L", 10 + i, &format!("R{i}"), 0.5));
log.push(Outcome { cause: format!("R{i}"), ..o(outcome::MISSED, "a", &format!("M{i}"), 10 + i, "r") });
}
assert_eq!(mem(&log).regretted, regret_cap(0), "the regrets' own misses are no corrections");
let tagged: Vec<Outcome> =
log.iter().map(|x| Outcome { cause: if x.cause.is_empty() { String::new() } else { outcome::tag(&x.cause) }, ..x.clone() }).collect();
assert_eq!(mem(&tagged).regretted, regret_cap(0));
let mut corrected = log.clone();
corrected.extend([o(outcome::MISSED, "a", "X", 3, "r"), o(outcome::MISSED, "a", "Y", 4, "r")]);
assert_eq!(mem(&corrected).regretted, 2.0);
}
#[test]
fn a_veto_outweighs_a_correction_and_a_change_in_its_session() {
let l = lesson("L", 0);
let log = vec![o(CHANGED, "a", "L", 1, "r"), o(CORRECTED, "a", "L", 2, "r"), o(outcome::VETOED, "a", "L", 3, "r"), o(CORRECTED, "b", "L", 4, "r")];
let m = &derive(&log, std::slice::from_ref(&l), None)["L"];
assert_eq!((m.changed, m.ignored, m.corrected, m.vetoed), (0, 0, 1, 1));
assert_eq!((m.alpha(), m.beta()), (1.0, (1 + 2 + 3) as f64));
let without = derive(&log[1..], std::slice::from_ref(&l), None)["L"].activation;
assert!(m.activation > without, "{} {without}", m.activation);
let only = |kind: &str| derive(&[o(kind, "a", "L", 1, "r")], std::slice::from_ref(&l), None)["L"].mean();
assert!(only(outcome::VETOED) < only(CORRECTED), "{} {}", only(outcome::VETOED), only(CORRECTED));
assert_eq!(only(outcome::VETOED), 1.0 / 5.0);
}
fn regret(session: &str, lesson: &str, ms: i64, cause: &str, credit: f64) -> Outcome {
Outcome { cause: cause.into(), credit, ..o(outcome::REGRETTED, session, lesson, ms, "r") }
}
#[test]
fn a_regret_lowers_belief_less_than_a_correction() {
let l = lesson("L", 0);
let mem = |log: &[Outcome]| derive(log, std::slice::from_ref(&l), None)["L"].clone();
let asked = o(outcome::ASKED, "a", "L", 1, "r");
let full = mem(&[asked.clone(), regret("a", "L", 9, "R1", 1.0)]);
assert_eq!((full.regretted, full.beta(), full.mean()), (1.0, 2.0, 1.0 / 3.0));
assert_eq!(mem(&[asked.clone(), o(CORRECTED, "a", "L", 9, "r")]).mean(), 1.0 / 4.0);
let log = vec![asked.clone(), regret("a", "L", 9, "R1", 0.25), regret("a", "L", 9, "R1", 0.25), regret("a", "L", 12, "R2", 0.5)];
assert_eq!(mem(&log).regretted, 0.75);
let mut reversed = log.clone();
reversed.reverse();
assert_eq!(mem(&reversed), mem(&log));
let changed = mem(&[asked.clone(), o(CHANGED, "a", "L", 5, "r"), regret("a", "L", 9, "R1", 0.5)]);
assert_eq!((changed.changed, changed.regretted), (1, 0.5));
assert_eq!(mem(&[asked.clone(), regret("a", "L", 9, "R1", 1.0)]).activation, mem(std::slice::from_ref(&asked)).activation);
for kind in [CORRECTED, outcome::VETOED] {
let late = mem(&[asked.clone(), regret("a", "L", 9, "R1", 0.5), o(kind, "a", "L", 12, "r")]);
assert_eq!(late.regretted, 0.0, "{kind}");
assert_eq!(late.beta(), mem(&[asked.clone(), o(kind, "a", "L", 12, "r")]).beta(), "{kind}");
}
let other = mem(&[asked.clone(), o(CORRECTED, "b", "L", 3, "r"), regret("a", "L", 9, "R1", 0.5)]);
assert_eq!(other.regretted, 0.5, "another session's correction leaves the regret");
}
#[test]
fn an_older_stores_hive_verdicts_change_no_belief_and_no_tick() {
let l = lesson("L", 0);
let mem = |log: &[Outcome]| derive(log, std::slice::from_ref(&l), None)["L"].clone();
let asked = o(outcome::ASKED, "a", "L", 1, "r");
for kind in ["verified", "rejected", "hacked"] {
let old = Outcome { cause: "g/t1.v1".into(), ..o(kind, "a", "L", 9, "r") };
assert_eq!(mem(&[asked.clone(), old.clone()]), mem(std::slice::from_ref(&asked)), "{kind}");
assert_eq!(Clock::of(&[old]).now(None), 0, "{kind}");
}
}
#[test]
fn a_green_test_run_weighs_half_a_change_and_never_ticks() {
let l = lesson("L", 0);
let mem = |log: &[Outcome]| derive(log, std::slice::from_ref(&l), None)["L"].clone();
let asked = o(outcome::ASKED, "a", "L", 1, "r");
let t = mem(&[asked.clone(), o(outcome::TESTED, "a", "L", 9, "r"), o(outcome::TESTED, "a", "L", 10, "r")]);
assert_eq!((t.tested, t.alpha(), t.changed), (1, 1.0 + TESTED_W, 0), "once per session, never a use");
assert_eq!(t.activation, mem(std::slice::from_ref(&asked)).activation);
assert_eq!(Clock::of(&[o(outcome::TESTED, "z", "L", 9, "r")]).now(None), 0, "no tick");
for k in [outcome::CORRECTED, outcome::VETOED] {
let log = [o(outcome::TESTED, "a", "L", 9, "r"), asked.clone(), o(k, "a", "L", 5, "r")];
assert_eq!(mem(&log).tested, 0, "{k}");
}
assert_eq!(Weights { tested: 2.0, ..WEIGHTS }.tested, 2.0);
assert_eq!(t.alpha_with(&Weights { tested: 2.0, ..WEIGHTS }), 3.0);
}
#[test]
fn fractional_beta_sampling_is_unbiased_and_integer_beta_unchanged() {
let n = 20_000;
let mean = (0..n).map(|i| round_seeded(2.5, seed(&format!("s{i}"), "L")) as f64).sum::<f64>() / n as f64;
assert!((mean - 2.5).abs() < 0.02, "{mean}");
assert!((0..100).all(|i| round_seeded(3.0, i) == 3));
let m = Memory { activation: 0.0, changed: 2, ignored: 1, corrected: 1, vetoed: 0, regretted: 0.0, tested: 0, consolidated: false, dormant: false };
assert_eq!(m.weight("L", Some("s")), squash(0.0) * (0.5 + sample_beta(3, 4, seed("s", "L"))));
assert_eq!(m.weight("L", Some("s")), 1.0618369485259265);
}
#[test]
fn fractional_alpha_draws_unbiased_and_integer_unchanged() {
assert!((0..200).all(|i| draw(3.0, 4.0, i) == sample_beta(3, 4, i)));
assert!((0..200).all(|i| draw(2.0, 2.5, i) == sample_beta(2, round_seeded(2.5, i ^ 0x5265_6772), i)));
let n = 20_000;
let mean = |a: f64| (0..n).map(|i| draw(a, 1.0, seed(&format!("s{i}"), "L"))).sum::<f64>() / n as f64;
let (lo, mid, hi) = (mean(1.0), mean(1.5), mean(2.0));
assert!((mid - (lo + hi) / 2.0).abs() < 0.01, "{lo} {mid} {hi}");
let m = Memory { tested: 2, ..derive(&[], &[lesson("L", 0)], None)["L"].clone() };
let quarter = Weights { tested: 0.25, ..WEIGHTS };
assert_eq!((m.alpha_with(&quarter), m.alpha(), m.beta_with(&quarter)), (1.5, 2.0, 1.0));
}
#[test]
fn derived_memory_is_independent_of_log_order_so_merging_logs_merges_memory() {
let ls = [lesson("L", 150), lesson("M", 0), Challenge { engineers: vec!["a@x".into(), "b@x".into()], ..lesson("N", 0) }];
let mut log = sessions(12);
for (k, s) in ["s002", "s005", "s009"].iter().enumerate() {
log.push(o(CHANGED, s, "N", 300 * k as i64, "r"));
log.push(o(IGNORED, s, "M", 300 * k as i64, "r"));
}
log.push(o(CORRECTED, "s007", "L", 800, "r"));
let base = derive(&log, &ls, Some("s011"));
let mut rng = Rng::new(7);
for _ in 0..20 {
let mut shuffled = log.clone();
for i in (1..shuffled.len()).rev() {
shuffled.swap(i, (rng.next_u64() % (i as u64 + 1)) as usize);
}
assert_eq!(derive(&shuffled, &ls, Some("s011")), base);
}
let (a, b) = log.split_at(log.len() / 2);
assert_eq!(derive(&[b, a].concat(), &ls, Some("s011")), base);
assert!(base["N"].consolidated && !base["M"].consolidated);
}
#[test]
fn consolidation_needs_spread_uses_and_dormancy_waits_for_a_long_silence() {
let l = lesson("L", 0);
let mut log = sessions(80);
for i in [1, 2, 3] {
log.push(o(CHANGED, &format!("s{i:03}"), "L", 100 * (i + 1) + 1, "one-repo"));
}
let m = &derive(&log, std::slice::from_ref(&l), None)["L"];
assert!(!m.consolidated, "one repo, one engineer");
log.push(o(CHANGED, "s004", "L", 501, "other-repo"));
let m = &derive(&log, std::slice::from_ref(&l), None)["L"];
assert!(m.consolidated && !m.dormant);
let fresh = lesson("F", 0);
assert!(!derive(&sessions(40), std::slice::from_ref(&fresh), None)["F"].dormant);
let mut quiet = sessions(80);
assert!(derive(&quiet, std::slice::from_ref(&fresh), None)["F"].dormant);
quiet.push(o(CHANGED, "s079", "F", 8001, "r"));
assert!(!derive(&quiet, &[fresh], None)["F"].dormant, "one use reactivates");
}
#[test]
fn thompson_samples_are_stable_per_session_spread_when_uncertain_and_tight_when_proven() {
assert_eq!(sample_beta(3, 4, seed("s", "L")), sample_beta(3, 4, seed("s", "L")));
assert_ne!(sample_beta(3, 4, seed("s", "L")), sample_beta(3, 4, seed("t", "L")));
let draws = |a, b| (0..400).map(|i| sample_beta(a, b, seed(&format!("s{i}"), "L"))).collect::<Vec<f64>>();
let mean = |v: &[f64]| v.iter().sum::<f64>() / v.len() as f64;
let sd = |v: &[f64]| (v.iter().map(|x| (x - mean(v)).powi(2)).sum::<f64>() / v.len() as f64).sqrt();
let (new, proven, harmful) = (draws(1, 1), draws(21, 3), draws(1, 21));
assert!((mean(&new) - 0.5).abs() < 0.05 && sd(&new) > 0.25, "uniform: {} {}", mean(&new), sd(&new));
assert!(mean(&proven) > 0.8 && sd(&proven) < 0.1);
assert!(mean(&harmful) < 0.1);
assert!(new.iter().chain(&proven).all(|x| *x > 0.0 && *x < 1.0));
let m = Memory { activation: 0.0, changed: 20, ignored: 2, corrected: 0, vetoed: 0, regretted: 0.0, tested: 0, consolidated: false, dormant: false };
assert!(m.weight("L", Some("s")) > 1.2 && m.weight("L", Some("s")) < 1.5);
}
#[test]
fn resolve_reads_declarations_first_then_edits_on_the_lessons_point() {
let mk = |id: &str, q: &str| Challenge { id: id.into(), question: q.into(), ..Default::default() };
let all = vec![
mk("Idrain", "Is the per-turn hint place drained by an explicit consumer?"),
mk("Ijudge", "Is the replay judge exactly the named Vertex model?"),
mk("Ihost", "Does the replay run on live hosts?"),
mk("x1", "Is the plan complete?"),
mk("x2", "Is the plan tested?"),
mk("x3", "Is the plan small?"),
];
let asked: Vec<&Challenge> = all[..3].iter().collect();
let fin = "Revised.\n`Ijudge`: applies — switched to Vertex.\n- Ihost: skip (offline task)\n";
let before = "Add a per-turn hint budget to TextNet; plan: reset arc on the starting transition";
let edits = "src/TextNet.java drain the hint place with an explicit consumer transition";
let (changed, ignored) = resolve(&asked, &all, before, edits, fin, NOVEL_WORDS);
assert_eq!((changed, ignored), (vec!["Idrain".to_string(), "Ijudge".to_string()], vec!["Ihost".to_string()]));
assert_eq!(resolve(&asked, &all, before, "", "Done.", NOVEL_WORDS).0, Vec::<String>::new());
assert!(resolve(&asked[..1], &all, before, "renamed the hint label, drained", "", NOVEL_WORDS).0.is_empty());
assert!(resolve(&asked[..1], &all, "the hint place is drained by an explicit consumer", edits, "", NOVEL_WORDS).0.is_empty());
}
#[test]
fn shuffle_is_a_stable_permutation_per_session() {
let mut a: Vec<u32> = (0..8).collect();
let mut b = a.clone();
shuffle(&mut a, "s1");
shuffle(&mut b, "s1");
assert_eq!(a, b);
let mut sorted = a.clone();
sorted.sort();
assert_eq!(sorted, (0..8).collect::<Vec<_>>());
let firsts: HashSet<u32> = (0..40)
.map(|i| {
let mut v: Vec<u32> = (0..8).collect();
shuffle(&mut v, &format!("s{i}"));
v[0]
})
.collect();
assert!(firsts.len() >= 5, "{firsts:?}");
}
}