1pub mod policy;
37pub mod replay;
38pub mod summary;
39
40use std::path::Path;
41
42use serde::{Deserialize, Serialize};
43
44use crate::attention;
45use crate::conscience::{self, Challenge};
46use crate::outcome;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum Policy {
51 Fixed,
53 Cap(usize),
55 Attention,
57 Learned,
59}
60
61impl Policy {
62 #[must_use]
64 pub fn name(self) -> &'static str {
65 match self {
66 Policy::Fixed => "fixed",
67 Policy::Cap(_) => "cap",
68 Policy::Attention => "attention",
69 Policy::Learned => "learned",
70 }
71 }
72}
73
74#[must_use]
76pub fn policy() -> Policy {
77 precedence(std::env::var("BORG_SELECT").ok().as_deref(), conscience::ask_cap(), attention::on())
78}
79
80#[must_use]
88pub fn precedence(select: Option<&str>, cap: Option<usize>, attention: bool) -> Policy {
89 match (select, cap) {
90 (Some("fixed"), _) => Policy::Fixed,
91 (_, Some(n)) => Policy::Cap(n),
92 _ if attention => Policy::Attention,
93 _ => Policy::Learned,
94 }
95}
96
97#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
99#[serde(rename_all = "lowercase")]
100pub enum Route {
101 Ask,
103 Free,
105 Placebo,
107 #[default]
109 Not,
110}
111
112impl Route {
113 #[must_use]
114 pub fn name(self) -> &'static str {
115 match self {
116 Route::Ask => "ask",
117 Route::Free => "free",
118 Route::Placebo => "placebo",
119 Route::Not => "not",
120 }
121 }
122
123 #[must_use]
126 pub const fn arm(self) -> usize {
127 match self {
128 Route::Ask => 0,
129 Route::Free => 1,
130 Route::Placebo | Route::Not => 2,
131 }
132 }
133}
134
135pub const ARMS: usize = 3;
138
139#[derive(Debug, Clone, PartialEq)]
141pub struct Unit<'a> {
142 pub lesson: &'a str,
143 pub grade: u8,
145 pub rank: usize,
147}
148
149#[must_use]
151pub fn units<'a>(applied: &[(&'a Challenge, u8)]) -> Vec<Unit<'a>> {
152 applied.iter().enumerate().map(|(rank, (c, g))| Unit { lesson: c.id.as_str(), grade: *g, rank }).collect()
153}
154
155pub struct Ctx<'a> {
157 pub session: &'a str,
158 pub check: &'a str,
160 pub channel: &'static str,
162 pub lessons: &'a Path,
164 pub summary: &'a summary::Summary,
166 pub tokens: u32,
170 pub had: &'a [String],
178 pub alpha: f64,
183}
184
185#[must_use]
189pub fn shown_tokens(shown: &[&Challenge]) -> u32 {
190 let sum: usize = shown.iter().map(|c| crate::whole::cost(c)).sum();
191 sum.try_into().unwrap_or(u32::MAX)
192}
193
194#[must_use]
197pub fn nofree(applied: &[(&Challenge, u8)], had: &[String]) -> Vec<String> {
198 let mut out: Vec<String> = Vec::new();
199 for (c, _) in applied {
200 if had.contains(&c.id) && !out.contains(&c.id) {
201 out.push(c.id.clone());
202 }
203 }
204 out
205}
206
207#[must_use]
210pub fn logged_alpha(alpha: f64) -> Option<f64> {
211 (alpha != policy::ALPHA).then_some(alpha)
212}
213
214#[must_use]
217pub fn alpha_of(logged: Option<f64>) -> f64 {
218 logged.unwrap_or(policy::ALPHA)
219}
220
221#[derive(Debug, Clone, PartialEq, Default)]
223pub struct Log {
224 pub policy: &'static str,
226 pub source: &'static str,
233 pub xk: Option<u32>,
237 pub xs: Option<u32>,
238 pub alpha: Option<f64>,
242 pub fit: String,
244 pub gate: f64,
248 pub risk: f64,
251 pub attention: Option<String>,
254 pub psi: f64,
257}
258
259impl Log {
260 #[must_use]
263 pub fn line(&self, applied: usize, check: &str) -> String {
264 let fit = if self.fit.is_empty() { "-" } else { self.fit.as_str() };
265 format!("#borg-select policy={} source={} fit={fit} gate={:.4} applied={applied} @{check}", self.policy, self.source, self.gate)
266 }
267}
268
269#[derive(Debug, Clone)]
273pub struct Routed<'a> {
274 pub ask: Vec<&'a Challenge>,
275 pub free: Vec<&'a Challenge>,
276 pub placebo: Vec<&'a Challenge>,
277 pub p: Vec<f64>,
278 pub pf: Vec<f64>,
279 pub log: Log,
280}
281
282impl Routed<'_> {
283 #[must_use]
285 pub fn route(&self, id: &str) -> Route {
286 let has = |v: &[&Challenge]| v.iter().any(|c| c.id == id);
287 if has(&self.ask) {
288 Route::Ask
289 } else if has(&self.free) {
290 Route::Free
291 } else if has(&self.placebo) {
292 Route::Placebo
293 } else {
294 Route::Not
295 }
296 }
297}
298
299#[must_use]
310pub fn graded_under<'a>(policy: Policy, applied: &[(&'a Challenge, u8)], ctx: &Ctx) -> Routed<'a> {
311 let three = applied.iter().any(|(_, g)| *g == 3);
312 let fixed = |source: &'static str, placebos: bool, (ask, free): (Vec<&'a Challenge>, Vec<&'a Challenge>)| {
313 let (placebo, psi) = if placebos { draw_placebo(applied, &ask, &free, ctx) } else { (Vec::new(), 0.0) };
314 Routed {
315 p: attention::fixed_p(applied, &ask),
316 pf: attention::fixed_p(applied, &free),
317 ask,
318 free,
319 placebo,
320 log: Log { policy: policy.name(), source, fit: ctx.summary.fit.key.clone(), psi, ..Log::default() },
321 }
322 };
323 let mut r = match policy {
324 Policy::Attention if three => by_cut(applied, ctx),
325 Policy::Cap(n) => fixed("cap", false, conscience::graded_without(applied, Some(n), ctx.had)),
326 Policy::Learned => learned(applied, ctx),
327 Policy::Fixed => fixed("fixed", true, conscience::graded_without(applied, None, ctx.had)),
328 Policy::Attention => fixed("fixed", false, conscience::graded_without(applied, None, ctx.had)),
329 };
330 r.log.alpha = logged_alpha(ctx.alpha);
331 r
332}
333
334pub const PLACEBO_SLOTS: usize = 2;
337
338fn draw_placebo<'a>(applied: &[(&'a Challenge, u8)], ask: &[&Challenge], free: &[&Challenge], ctx: &Ctx) -> (Vec<&'a Challenge>, f64) {
345 let delivered = |id: &str| ask.iter().chain(free).any(|c| c.id == id);
346 let mut later: Vec<usize> = Vec::new();
347 for (j, (c, _)) in applied.iter().enumerate() {
348 if !delivered(&c.id) && !ctx.had.contains(&c.id) && !later.iter().any(|&i| applied[i].0.id == c.id) {
349 later.push(j);
350 }
351 }
352 let mut drawn = later.clone();
353 let mut rng = crate::memory::Rng::new(seed(ctx) ^ 0x706c_6163_6562_6f00);
355 for i in (1..drawn.len()).rev() {
356 drawn.swap(i, (rng.next_u64() % (i as u64 + 1)) as usize);
357 }
358 drawn.truncate(PLACEBO_SLOTS);
359 drawn.sort_unstable();
360 let psi = if later.is_empty() { 0.0 } else { outcome::round_p((PLACEBO_SLOTS as f64 / later.len() as f64).min(1.0)) };
361 (drawn.iter().map(|&j| applied[j].0).collect(), psi)
362}
363
364pub const REVERTED_SOURCE: &str = "reverted";
368
369fn learned<'a>(applied: &[(&'a Challenge, u8)], ctx: &Ctx) -> Routed<'a> {
375 let us = units(applied);
376 let s = ctx.summary;
377 let check = policy::Check { session: ctx.session, stop: ctx.channel == "stop", tokens: ctx.tokens };
378 let d = match s.monitor.reverted {
379 true => {
380 let base = policy::fixed_routes(&us, ctx.had);
381 let p = |r: Route| base.iter().map(|x| f64::from(u8::from(*x == r))).collect();
382 policy::Decision { p: p(Route::Ask), pf: p(Route::Free), routes: base, source: REVERTED_SOURCE, gate: 0.0, risk: 0.0, xk: None, xs: None }
383 }
384 false => policy::choose(&us, &check, &s.fit, &s.gate, ctx.alpha, seed(ctx), ctx.had),
385 };
386 let (ask, free) = ordered(applied, &d.routes);
387 let (placebo, psi) = draw_placebo(applied, &ask, &free, ctx);
388 Routed {
389 ask,
390 free,
391 placebo,
392 p: d.p,
393 pf: d.pf,
394 log: Log {
395 policy: Policy::Learned.name(),
396 source: d.source,
397 fit: s.fit.key.clone(),
398 gate: d.gate,
399 risk: d.risk,
400 xk: d.xk,
401 xs: d.xs,
402 alpha: None,
404 attention: None,
405 psi,
406 },
407 }
408}
409
410fn by_cut<'a>(applied: &[(&'a Challenge, u8)], ctx: &Ctx) -> Routed<'a> {
415 let outcomes = outcome::read(&outcome::path_beside(ctx.lessons));
416 let base = attention::cut(&attention::labels(&outcomes));
417 let (p, pf) = (attention::propensities(applied.len(), base.k), free_p(applied.len(), base.k));
418 let checks = outcomes.iter().filter(|o| o.kind == outcome::GRADED && !o.lessons.is_empty() && !o.failed).count();
421 let cut = match attention::explores(checks) {
422 true => attention::Cut { k: attention::RAIL, source: "explore", ..base },
423 false => base,
424 };
425 let (ask, free) = attention::split(applied, &cut);
426 let log = Log {
427 policy: Policy::Attention.name(),
428 source: cut.source,
429 fit: ctx.summary.fit.key.clone(),
430 attention: Some(attention::line(&cut, applied.len())),
431 ..Log::default()
432 };
433 Routed { ask, free, placebo: Vec::new(), p, pf, log }
434}
435
436fn free_p(n: usize, k: usize) -> Vec<f64> {
440 let q = 1.0 / attention::EXPLORE as f64;
441 let (cut, rail) = (k.min(attention::RAIL).min(n), attention::RAIL.min(n));
442 let within = |r: usize, from: usize| f64::from(u8::from(r >= from && r < from + conscience::CAP));
443 (0..n).map(|r| outcome::round_p((1.0 - q) * within(r, cut) + q * within(r, rail))).collect()
444}
445
446fn ordered<'a>(applied: &[(&'a Challenge, u8)], routes: &[Route]) -> (Vec<&'a Challenge>, Vec<&'a Challenge>) {
449 let mut order: Vec<usize> = (0..applied.len().min(routes.len())).collect();
450 order.sort_by_key(|&i| std::cmp::Reverse(applied[i].1));
451 let of = |r: Route| order.iter().filter(|&&i| routes[i] == r).map(|&i| applied[i].0).collect();
452 (of(Route::Ask), of(Route::Free))
453}
454
455#[must_use]
459pub fn seed(ctx: &Ctx) -> u64 {
460 let mut h = blake3::Hasher::new();
461 h.update(b"borg-select-seed-v1\0");
462 for part in [ctx.session, ctx.channel, ctx.check] {
463 h.update(part.as_bytes());
464 h.update(b"\0");
465 }
466 let bytes = h.finalize();
467 const WIDTH: usize = std::mem::size_of::<u64>();
469 let mut first = [0u8; WIDTH];
470 first.copy_from_slice(&bytes.as_bytes()[..WIDTH]);
471 u64::from_le_bytes(first)
472}
473
474#[must_use]
477pub fn pool_tokens(s: &summary::Summary, limit_ms: Option<u64>, session: &str) -> usize {
478 pool_tokens_under(policy(), s, limit_ms, session)
479}
480
481#[must_use]
487pub fn pool_tokens_under(policy: Policy, s: &summary::Summary, limit_ms: Option<u64>, session: &str) -> usize {
488 policy::pool_tokens(&s.fit, pool_active(policy, s), limit_ms, policy::session_seed(session))
489}
490
491fn pool_active(policy: Policy, s: &summary::Summary) -> bool {
497 policy == Policy::Learned && !s.monitor.reverted && (s.gate.allowance > 0.0 || s.fit.budget.is_some())
498}
499
500#[must_use]
504pub fn budget_of(lessons: &Path) -> Option<f64> {
505 budget_in(&crate::collective::load().ok().flatten()?, lessons)
506}
507
508fn budget_in(cfg: &crate::collective::Config, lessons: &Path) -> Option<f64> {
510 entry_in(cfg, lessons)?.budget()
511}
512
513#[must_use]
518pub fn risk_of(lessons: &Path) -> Option<f64> {
519 risk_in(&crate::collective::load().ok().flatten()?, lessons)
520}
521
522fn risk_in(cfg: &crate::collective::Config, lessons: &Path) -> Option<f64> {
524 entry_in(cfg, lessons)?.risk()
525}
526
527#[must_use]
531pub fn alpha(lessons: &Path) -> f64 {
532 risk_of(lessons).unwrap_or(policy::ALPHA)
533}
534
535fn entry_in<'a>(cfg: &'a crate::collective::Config, lessons: &Path) -> Option<&'a crate::collective::Entry> {
537 let store = lessons.parent()?;
538 let base = crate::collective::root_of(store);
539 cfg.collectives.iter().find(|c| crate::collective::store(&base, &c.name) == store)
540}
541
542pub const CHECK_HEX: usize = 12;
545
546#[must_use]
551pub fn check_id(session: &str, channel: &str, start_ms: i64, pid: u32) -> String {
552 let mut h = blake3::Hasher::new();
553 h.update(b"borg-check-v1\0");
554 h.update(format!("{session}\0{channel}\0{start_ms}\0{pid}").as_bytes());
555 h.finalize().to_hex()[..CHECK_HEX].to_string()
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561
562 fn lessons(n: usize) -> Vec<Challenge> {
563 (0..n).map(|i| Challenge { id: format!("L{i:02}"), ..Default::default() }).collect()
564 }
565
566 fn ids(v: &[&Challenge]) -> Vec<String> {
567 v.iter().map(|c| c.id.clone()).collect()
568 }
569
570 fn store(name: &str, outcomes: &[outcome::Outcome]) -> std::path::PathBuf {
572 let d = std::env::temp_dir().join(format!("borg-select-{name}-{}", std::process::id()));
573 let _ = std::fs::remove_dir_all(&d);
574 std::fs::create_dir_all(&d).unwrap();
575 let text: String = outcomes.iter().map(|o| format!("{}\n", serde_json::to_string(o).unwrap())).collect();
576 std::fs::write(d.join("outcomes.jsonl"), text).unwrap();
577 d.join("lessons.jsonl")
578 }
579
580 fn ctx<'a>(lessons: &'a Path, s: &'a summary::Summary) -> Ctx<'a> {
581 Ctx { session: "s", check: "0123456789ab", channel: "stop", lessons, summary: s, tokens: 0, had: &[], alpha: policy::ALPHA }
582 }
583
584 fn tables(n: usize) -> Vec<Vec<u8>> {
586 let mut out = vec![Vec::new()];
587 for len in 1..=n {
588 out.extend((0..3usize.pow(len as u32)).map(|mut x| {
589 (0..len)
590 .map(|_| {
591 let g = (x % 3) as u8 + 1;
592 x /= 3;
593 g
594 })
595 .collect()
596 }));
597 }
598 out
599 }
600
601 #[test]
602 fn the_switches_take_precedence_in_order() {
603 assert_eq!(precedence(None, None, false), Policy::Learned, "the default is the learned policy");
604 assert_eq!(precedence(Some("learned"), None, false), Policy::Learned, "naming it changes nothing");
605 assert_eq!(precedence(Some("fixed"), Some(2), true), Policy::Fixed, "fixed wins over both evaluation switches");
606 assert_eq!(precedence(Some("learned"), Some(2), true), Policy::Cap(2), "the cap wins over attention and learned");
607 assert_eq!(precedence(Some("learned"), None, true), Policy::Attention, "attention wins over learned");
608 assert_eq!(precedence(Some("nonsense"), None, false), Policy::Learned, "an unknown BORG_SELECT is the default, the learned policy");
609 assert_eq!(precedence(Some("Fixed"), None, false), Policy::Learned, "the value is exact");
610 assert_eq!(precedence(Some("fixed"), None, false), Policy::Fixed, "BORG_SELECT=fixed is the way back");
611 assert_eq!([Policy::Fixed, Policy::Cap(1), Policy::Attention, Policy::Learned].map(Policy::name), ["fixed", "cap", "attention", "learned"]);
612 }
613
614 fn candidates(applied: &[(&Challenge, u8)], r: &Routed, had: &[String]) -> Vec<String> {
617 let mut out: Vec<String> = Vec::new();
618 for (c, _) in applied {
619 if matches!(r.route(&c.id), Route::Not | Route::Placebo) && !had.contains(&c.id) && !out.contains(&c.id) {
620 out.push(c.id.clone());
621 }
622 }
623 out
624 }
625
626 #[test]
631 fn every_policy_without_a_cut_is_the_fixed_rule_on_every_grade_table() {
632 let ls = lessons(14);
633 let s = summary::Summary::default();
634 let none = std::path::PathBuf::from("/nonexistent/borg-select/lessons.jsonl");
635 let mut tables = tables(7);
636 tables.extend([vec![3; 12], [vec![1; 6], vec![3], vec![2; 7]].concat(), vec![2; 14], [vec![1; 11], vec![2; 3]].concat()]);
638 let some: Vec<String> = ["L00", "L03", "L11", "X"].map(String::from).to_vec();
641 for had in [&[][..], &some[..]] {
642 for grades in &tables {
643 let applied: Vec<(&Challenge, u8)> = ls.iter().zip(grades.iter().copied()).collect();
644 let at = Ctx { had, ..ctx(&none, &s) };
645 for (policy, cap) in [(Policy::Fixed, None), (Policy::Learned, None), (Policy::Cap(2), Some(2))] {
646 let r = graded_under(policy, &applied, &at);
647 let (ask, free) = conscience::graded_without(&applied, cap, had);
648 assert_eq!((ids(&r.ask), ids(&r.free)), (ids(&ask), ids(&free)), "{policy:?} {grades:?} {had:?}");
649 if had.is_empty() {
650 let (ask, free) = conscience::graded_capped(&applied, cap);
651 assert_eq!((ids(&r.ask), ids(&r.free)), (ids(&ask), ids(&free)), "no lesson had: the rule of every older record");
652 }
653 assert_eq!(r.p, attention::fixed_p(&applied, &ask), "{policy:?} {grades:?}");
654 assert_eq!(r.pf, attention::fixed_p(&applied, &free), "{policy:?} {grades:?}");
655 assert!(r.log.attention.is_none() && r.log.gate == 0.0);
656 assert_eq!(r.log.policy, policy.name());
657 let later = candidates(&applied, &r, had);
658 match policy {
659 Policy::Cap(_) => assert!(r.placebo.is_empty() && r.log.psi == 0.0),
660 _ => {
661 assert_eq!(r.placebo.len(), later.len().min(PLACEBO_SLOTS), "{policy:?} {grades:?}");
662 assert!(ids(&r.placebo).iter().all(|p| later.contains(p)), "{policy:?} {grades:?}");
663 let psi = if later.is_empty() { 0.0 } else { outcome::round_p((PLACEBO_SLOTS as f64 / later.len() as f64).min(1.0)) };
664 assert_eq!(r.log.psi, psi);
665 }
666 }
667 }
668 if !grades.contains(&3) {
669 let r = graded_under(Policy::Attention, &applied, &at);
670 let (ask, free) = conscience::graded_without(&applied, None, had);
671 assert_eq!((ids(&r.ask), ids(&r.free), r.log.source), (ids(&ask), ids(&free), "fixed"), "no 3: no cut");
672 assert!(r.placebo.is_empty() && r.log.psi == 0.0, "an evaluation switch draws no placebo");
673 }
674 }
675 }
676 }
677
678 #[test]
679 fn attention_with_a_three_is_the_cut_on_the_outcomes_beside_the_lessons() {
680 let ls = lessons(14);
682 let s = summary::Summary::default();
683 let grades: Vec<u8> = std::iter::once(3).chain(std::iter::repeat_n(1, 13)).collect();
684 let applied: Vec<(&Challenge, u8)> = ls.iter().zip(grades).collect();
685 let cold = store("attention-cold", &[]);
686 let r = graded_under(Policy::Attention, &applied, &ctx(&cold, &s));
687 assert_eq!((r.ask.len(), ids(&r.free)), (attention::PRIOR_K, ids(&applied[attention::PRIOR_K..].iter().map(|(c, _)| *c).collect::<Vec<_>>())));
688 assert_eq!(r.p, attention::propensities(14, attention::PRIOR_K));
689 assert_eq!(&r.pf[..attention::PRIOR_K], &[0.0; attention::PRIOR_K]);
690 assert_eq!(&r.pf[attention::PRIOR_K..], &[0.9; 3], "free unless the check explores, which asks them");
691 assert_eq!((r.log.policy, r.log.source), ("attention", "prior"));
692 assert_eq!(r.log.attention.as_deref(), Some("#borg-attention k=11 n=0 alpha=0.0010 source=prior applied=14"));
693 let graded = outcome::Outcome { kind: outcome::GRADED.into(), session: "x".into(), lessons: vec!["L0".into()], ..Default::default() };
695 let nine = store("attention-explore", &vec![graded.clone(); attention::EXPLORE - 1]);
696 let r = graded_under(Policy::Attention, &applied, &ctx(&nine, &s));
697 assert_eq!((r.ask.len(), r.free.len(), r.log.source), (14, 0, "explore"));
698 assert_eq!(r.p, attention::propensities(14, attention::PRIOR_K), "the propensities are the mixture's, not the override's");
699 let mut dirs = vec![cold, nine];
702 let (empty, failed) = (outcome::Outcome { lessons: Vec::new(), ..graded.clone() }, outcome::Outcome { failed: true, ..graded.clone() });
703 for (tag, ninth) in [("empty", empty), ("failed", failed)] {
704 let mut log = vec![graded.clone(); attention::EXPLORE - 2];
705 log.push(ninth);
706 let unmoved = store(&format!("attention-{tag}"), &log);
707 assert_eq!(graded_under(Policy::Attention, &applied, &ctx(&unmoved, &s)).log.source, "prior", "{tag}");
708 dirs.push(unmoved);
709 }
710 for d in dirs {
711 let _ = std::fs::remove_dir_all(d.parent().unwrap());
712 }
713 }
714
715 #[test]
716 fn free_propensities_mix_the_cut_and_the_explore_slice() {
717 let pf = free_p(35, 3);
719 assert_eq!(&pf[..3], &[0.0; 3]);
720 assert_eq!(&pf[3..13], &[0.9; 10]);
721 assert_eq!(&pf[13..20], &[0.0; 7]);
722 assert_eq!(&pf[20..30], &[0.1; 10]);
723 assert_eq!(&pf[30..], &[0.0; 5]);
724 assert_eq!(free_p(5, 11), [0.0; 5], "the cut and the rail ask all five");
725 let p = attention::propensities(35, 3);
727 assert!(p.iter().zip(&pf).all(|(a, f)| a + f <= 1.0 + 1e-9), "{p:?} {pf:?}");
728 }
729
730 #[test]
731 fn a_route_names_the_list_that_holds_the_lesson() {
732 let ls = lessons(3);
733 let applied: Vec<(&Challenge, u8)> = ls.iter().zip([3, 1, 2]).collect();
734 let none = std::path::PathBuf::from("/nonexistent/borg-select/lessons.jsonl");
735 let s = summary::Summary::default();
736 let mut r = graded_under(Policy::Fixed, &applied, &ctx(&none, &s));
737 assert_eq!(ids(&r.ask), ["L00", "L02", "L01"], "3s, then 2s, then 1s");
738 r.free = r.ask.split_off(2);
739 r.placebo = r.free.split_off(0);
740 assert_eq!(["L00", "L01", "L02", "L99"].map(|id| r.route(id)), [Route::Ask, Route::Placebo, Route::Ask, Route::Not]);
741 }
742
743 #[test]
744 fn the_select_line_carries_the_decision_and_its_check() {
745 let mut log = Log { policy: "learned", source: "fixed", ..Log::default() };
746 assert_eq!(log.line(4, "0123456789ab"), "#borg-select policy=learned source=fixed fit=- gate=0.0000 applied=4 @0123456789ab");
747 log.fit = "120-ab12".into();
748 log.gate = 0.25;
749 assert!(log.line(0, "ff").contains(" fit=120-ab12 gate=0.2500 applied=0 @ff"), "{}", log.line(0, "ff"));
750 assert_eq!(conscience::work_text(&format!("{}\nwork\n", log.line(1, "ab"))), "work\n");
752 }
753
754 fn deviating() -> summary::Summary {
756 let at = |name: &str| policy::NAMES.iter().position(|n| *n == name).unwrap();
757 let mut beta = vec![0.0; policy::P];
758 beta[at("intercept")] = -0.5;
759 beta[at("ask")] = -3.0;
760 beta[at("not")] = -1.0;
761 let mut chol = vec![0.0; policy::P * policy::P];
762 for k in 0..policy::P {
763 chol[k * policy::P + k] = 0.05;
764 }
765 let model = policy::Model { beta, chol, sd: vec![1.0; policy::P], spread: [0.1; 3], ..policy::Model::default() };
766 let rates = policy::Rates { free_stop: 1.0, free_live: 1.0, later: 0.0, wait: 0.0, ask_live: 1.0 };
767 let fit = policy::Fit { key: "9-abcdef01".into(), model: Some(model), rates, ..policy::Fit::default() };
768 let gate = summary::Gate { gain: vec![0.0; policy::DRAWS], allowance: 5.0, ..summary::Gate::default() };
769 summary::Summary { fit, gate, ..summary::Summary::default() }
770 }
771
772 #[test]
773 fn the_learned_policy_routes_by_its_fit_and_logs_it() {
774 let ls = lessons(4);
775 let applied: Vec<(&Challenge, u8)> = ls.iter().zip([3, 1, 2, 1]).collect();
776 let s = deviating();
777 let none = std::path::PathBuf::from("/nonexistent/borg-select/lessons.jsonl");
778 let live = Ctx { channel: "live", ..ctx(&none, &s) };
779 let r = graded_under(Policy::Learned, &applied, &live);
780 assert!(r.ask.is_empty(), "asking loses under this fit: {:?}", ids(&r.ask));
781 assert_eq!(ids(&r.free), ["L00", "L02", "L01", "L03"], "higher grade first, then the model's order");
782 assert_eq!((r.log.policy, r.log.source, r.log.fit.as_str()), ("learned", policy::EVIDENCE, "9-abcdef01"));
783 assert_eq!((r.log.xk, r.log.xs), (None, None), "on evidence the test does not run");
784 assert!(r.log.gate > 0.0);
785 assert!(r.pf.iter().all(|p| *p == 1.0) && r.p.iter().all(|p| *p == 0.0), "exact: {:?} {:?}", r.p, r.pf);
786 let r = graded_under(Policy::Learned, &applied, &ctx(&none, &s));
788 assert_eq!((ids(&r.ask), r.p[0]), (vec!["L00".to_string()], 1.0));
789 }
790
791 #[test]
796 fn a_live_check_under_the_test_asks_the_drawn_count_and_logs_it() {
797 let ls = lessons(4);
798 let applied: Vec<(&Challenge, u8)> = ls.iter().zip([2, 1, 2, 1]).collect();
799 let mut s = deviating();
800 if let Some(m) = s.fit.model.as_mut() {
801 m.beta = vec![0.0; policy::P];
802 m.beta[policy::NAMES.iter().position(|n| *n == "not").unwrap()] = -1.0;
803 }
804 let none = std::path::PathBuf::from("/nonexistent/borg-select/lessons.jsonl");
805 let order = ["L00", "L02", "L01", "L03"];
806 let mut seen = std::collections::BTreeSet::new();
807 for c in 0..40u32 {
808 let check = format!("{c:012x}");
809 let r = graded_under(Policy::Learned, &applied, &Ctx { channel: "live", check: &check, ..ctx(&none, &s) });
810 let (Some(k), Some(x)) = (r.log.xk, r.log.xs) else { panic!("check {c}: the test ran: {:?}", r.log) };
811 let x = x as usize;
812 assert_eq!(k, 4, "every lesson the fixed rule hands over is tested");
813 let named = |v: &[&str]| v.iter().map(|x| x.to_string()).collect::<Vec<_>>();
814 assert_eq!((ids(&r.ask), ids(&r.free)), (named(&order[..x]), named(&order[x..])), "check {c}");
815 assert_eq!(r.log.source, if x == 0 { policy::EXPERIMENT_CONTROL } else { policy::EXPERIMENT }, "check {c}");
816 assert_eq!(
817 (r.p.as_slice(), r.pf.as_slice()),
818 ([0.8, 0.4, 0.6, 0.2].as_slice(), [0.2, 0.6, 0.4, 0.8].as_slice()),
819 "each unit's share of the five counts"
820 );
821 seen.insert(x);
822 }
823 assert!(seen.len() > 2, "the count varies with the check: {seen:?}");
824 let r = graded_under(Policy::Learned, &applied, &ctx(&none, &s));
826 assert_eq!((r.log.source, r.log.xk, r.log.xs, r.ask.len()), (policy::EVIDENCE, None, None, 0));
827 }
828
829 #[test]
834 fn a_check_decides_and_logs_under_the_tolerance_in_its_ctx() {
835 let ls = lessons(4);
836 let applied: Vec<(&Challenge, u8)> = ls.iter().zip([2, 1, 2, 1]).collect();
837 let mut s = deviating();
838 if let Some(m) = s.fit.model.as_mut() {
839 m.beta = vec![0.0; policy::P];
840 m.beta[policy::NAMES.iter().position(|n| *n == "not").unwrap()] = -1.0;
841 }
842 let none = std::path::PathBuf::from("/nonexistent/borg-select/lessons.jsonl");
843 assert_eq!(alpha(&none), policy::ALPHA, "the config's α for this path");
844 let live = Ctx { channel: "live", ..ctx(&none, &s) };
845 let at = |alpha: f64| graded_under(Policy::Learned, &applied, &Ctx { alpha, ..live });
846 let default = at(policy::ALPHA);
847 assert_eq!((default.log.xk, default.log.alpha), (Some(4), None), "the test runs; the default is not logged");
848 let off = at(0.0);
849 assert_eq!((off.log.source, off.log.xk, off.log.xs, off.log.alpha), (policy::EVIDENCE, None, None, Some(0.0)), "{:?}", off.log);
850 assert_eq!((off.ask.len(), off.free.len()), (0, 4), "the fixed rule");
851 let wider = at(0.3);
852 assert_eq!((wider.log.xk, wider.log.alpha), (Some(4), Some(0.3)));
853 let fixed = graded_under(Policy::Fixed, &applied, &Ctx { alpha: 0.0, ..live });
854 assert_eq!(fixed.log.alpha, Some(0.0), "logged under every policy, for the cold start");
855 for a in [0.0, 0.3, policy::ALPHA] {
856 assert_eq!(alpha_of(logged_alpha(a)), a);
857 let replayed = at(alpha_of(at(a).log.alpha));
858 assert_eq!((replayed.log, replayed.p, replayed.pf), (at(a).log, at(a).p, at(a).pf), "α {a} replays as decided");
859 }
860 }
861
862 #[test]
864 fn a_reverted_store_keeps_the_fixed_rule() {
865 let ls = lessons(4);
866 let applied: Vec<(&Challenge, u8)> = ls.iter().zip([3, 1, 2, 1]).collect();
867 let mut s = deviating();
868 s.monitor.reverted = true;
869 let none = std::path::PathBuf::from("/nonexistent/borg-select/lessons.jsonl");
870 let r = graded_under(Policy::Learned, &applied, &Ctx { channel: "live", ..ctx(&none, &s) });
871 let (ask, free) = conscience::graded_capped(&applied, None);
872 assert_eq!((ids(&r.ask), ids(&r.free)), (ids(&ask), ids(&free)));
873 assert_eq!((r.log.source, r.log.gate), (REVERTED_SOURCE, 0.0));
874 assert_eq!(r.p, attention::fixed_p(&applied, &ask));
875 let had = ["L01".to_string()];
877 let r = graded_under(Policy::Learned, &applied, &Ctx { channel: "live", had: &had, ..ctx(&none, &s) });
878 assert_eq!(ids(&r.free), ids(&conscience::graded_without(&applied, None, &had).1));
879 assert_eq!((r.log.source, r.pf[1]), (REVERTED_SOURCE, 0.0));
880 }
881
882 #[test]
886 fn a_lesson_the_session_had_is_asked_but_never_handed_over_free() {
887 let ls = lessons(12);
888 let none = std::path::PathBuf::from("/nonexistent/borg-select/lessons.jsonl");
889 let s = summary::Summary::default();
890 let soft: Vec<(&Challenge, u8)> = ls.iter().zip(std::iter::once(2).chain(std::iter::repeat_n(1, 11))).collect();
892 let had = ["L00".to_string(), "X9".to_string()];
893 let free = |had: &[String]| ids(&conscience::graded_without(&soft, None, had).1);
894 assert_eq!(free(&[]), ids(&ls[..10].iter().collect::<Vec<_>>()));
895 assert_eq!(free(&had), ids(&ls[1..11].iter().collect::<Vec<_>>()), "a 2 the session had takes no slot; the next 1 does");
896 for policy in [Policy::Fixed, Policy::Learned] {
897 let r = graded_under(policy, &soft, &Ctx { had: &had, ..ctx(&none, &s) });
898 assert_eq!((ids(&r.free), r.pf[0]), (free(&had), 0.0), "{policy:?}");
899 assert!(!ids(&r.placebo).contains(&"L00".to_string()), "{policy:?}: a lesson the session had is no placebo");
900 }
901 let block: Vec<(&Challenge, u8)> = ls.iter().take(3).zip([3, 2, 1]).collect();
903 let ask = |had: &[String]| ids(&conscience::graded_without(&block, None, had).0);
904 assert_eq!(ask(&["L01".to_string()]), ask(&[]), "may still be asked");
905 let open = deviating();
908 let applied: Vec<(&Challenge, u8)> = ls.iter().take(4).zip([2, 1, 2, 1]).collect();
909 let live = ctx(&none, &open);
910 let all = graded_under(Policy::Learned, &applied, &live);
911 assert_eq!((all.log.source, all.route("L01")), (policy::EVIDENCE, Route::Free), "the scenario: without it, L01 goes free");
912 let had = ["L01".to_string()];
913 let r = graded_under(Policy::Learned, &applied, &Ctx { had: &had, ..live });
914 assert_eq!((r.route("L01"), r.pf[1]), (Route::Not, 0.0), "{:?}", r.pf);
915 assert_eq!(ids(&r.free), ["L00", "L02", "L03"]);
916 let twice: Vec<(&Challenge, u8)> = [&ls[3], &ls[1], &ls[3], &ls[2]].into_iter().map(|c| (c, 1)).collect();
918 assert_eq!(nofree(&twice, &["L02".to_string(), "L03".to_string(), "L99".to_string()]), ["L03", "L02"]);
919 assert!(nofree(&twice, &[]).is_empty());
920 }
921
922 #[test]
927 fn the_fixed_rule_draws_its_placebos_among_what_it_delivers_neither_way() {
928 let ls = lessons(14);
929 let none = std::path::PathBuf::from("/nonexistent/borg-select/lessons.jsonl");
930 let s = summary::Summary::default();
931 fn fixed<'a>(applied: &[(&'a Challenge, u8)], had: &[String], check: &str) -> Routed<'a> {
932 let (s, none) = (summary::Summary::default(), std::path::PathBuf::from("/nonexistent/borg-select/lessons.jsonl"));
933 graded_under(Policy::Fixed, applied, &Ctx { check, had, ..ctx(&none, &s) })
934 }
935 let block: Vec<(&Challenge, u8)> = ls.iter().take(12).zip(std::iter::once(3).chain(std::iter::repeat_n(1, 11))).collect();
937 let r = fixed(&block, &[], "0123456789ab");
938 assert_eq!((r.ask.len(), ids(&r.placebo), r.log.psi), (conscience::CAP, vec!["L10".to_string(), "L11".to_string()], 1.0));
939 let soft: Vec<(&Challenge, u8)> = ls.iter().map(|c| (c, 1)).collect();
941 let mut drawn = std::collections::BTreeMap::<String, u32>::new();
942 let checks = 200;
943 for c in 0..checks {
944 let check = format!("{c:012x}");
945 let r = fixed(&soft, &[], &check);
946 assert_eq!((r.free.len(), r.placebo.len(), r.log.psi), (conscience::CAP, PLACEBO_SLOTS, 0.5));
947 assert_eq!(ids(&r.placebo), ids(&fixed(&soft, &[], &check).placebo), "the check's seed decides");
948 assert!(r.placebo.windows(2).all(|w| w[0].id < w[1].id), "in the applies' order");
949 for p in &r.placebo {
950 *drawn.entry(p.id.clone()).or_default() += 1;
951 }
952 }
953 assert_eq!(drawn.keys().cloned().collect::<Vec<_>>(), ["L10", "L11", "L12", "L13"]);
954 assert!(drawn.values().all(|&n| (60..=140).contains(&n)), "each about half the checks: {drawn:?}");
955 let had = ["L12".to_string()];
957 for c in 0..20 {
958 let r = fixed(&soft, &had, &format!("{c:012x}"));
959 assert_eq!(r.log.psi, outcome::round_p(2.0 / 3.0));
960 assert!(!ids(&r.placebo).contains(&had[0]), "{:?}", ids(&r.placebo));
961 }
962 let named: Vec<(&Challenge, u8)> = ls.iter().take(10).chain([&ls[0], &ls[12], &ls[12]]).map(|c| (c, 1)).collect();
964 let r = fixed(&named, &[], "0123456789ab");
965 assert_eq!((ids(&r.placebo), r.log.psi), (vec!["L12".to_string()], 1.0));
966 for policy in [Policy::Cap(1), Policy::Attention] {
968 let r = graded_under(policy, &soft, &ctx(&none, &s));
969 assert!(r.placebo.is_empty() && r.log.psi == 0.0, "{policy:?}");
970 }
971 let cold = graded_under(Policy::Learned, &soft, &ctx(&none, &s));
972 assert_eq!((ids(&cold.placebo), cold.log.psi), (ids(&graded_under(Policy::Fixed, &soft, &ctx(&none, &s)).placebo), 0.5));
973 }
974
975 #[test]
979 fn the_pool_is_the_learned_policys_once_its_gate_opens_or_a_budget_binds() {
980 let pool = policy::PoolFit {
981 yields: vec![(50.0, 10.0); 8],
982 per_lesson: 300.0,
983 latency: (1000.0, 0.1, 500.0),
984 latency_n: policy::LATENCY_MIN as u32,
985 not: (-1.0, 0.1),
986 ..policy::PoolFit::default()
987 };
988 let base = crate::whole::TOKENS;
989 let limit = Some(60_000);
990 let unpriced = policy::Fit { pool: pool.clone(), ..policy::Fit::default() };
991 let grows = (0..20).map(|i| format!("s{i}")).find(|x| policy::pool_tokens(&unpriced, true, limit, policy::session_seed(x)) > base);
992 let session = grows.expect("the scenario: an active pool grows for some session");
993 let at = |policy: Policy, s: &summary::Summary| (pool_active(policy, s), pool_tokens_under(policy, s, limit, &session));
994 let mut s = summary::Summary { fit: unpriced, ..summary::Summary::default() };
995 assert_eq!(at(Policy::Learned, &s), (false, base), "gate closed, no budget");
996 s.gate.allowance = 1.0;
997 assert!(at(Policy::Learned, &s).0 && at(Policy::Learned, &s).1 > base, "gate open");
998 for policy in [Policy::Fixed, Policy::Cap(2), Policy::Attention] {
999 assert_eq!(at(policy, &s), (false, base), "{policy:?}");
1000 }
1001 s.monitor.reverted = true;
1002 assert_eq!(at(Policy::Learned, &s), (false, base), "reverted");
1003 let mut s = summary::Summary { fit: policy::Fit { pool, budget: Some(1.0), lambda: 1e-2, ..policy::Fit::default() }, ..summary::Summary::default() };
1005 let (active, tokens) = at(Policy::Learned, &s);
1006 assert!(active && tokens < base, "budget, gate closed: {tokens}");
1007 assert_eq!(at(Policy::Fixed, &s), (false, base), "the fixed rule keeps the baseline under a budget");
1008 s.monitor.reverted = true;
1009 assert_eq!(at(Policy::Learned, &s), (false, base), "reverted under a budget");
1010 }
1011
1012 #[test]
1019 fn the_gate_admits_at_the_pool_its_ledger_records() {
1020 let at = |name: &str| policy::NAMES.iter().position(|n| *n == name).unwrap();
1021 let mut beta = vec![0.0; policy::P];
1022 for (name, b) in [("intercept", -0.5), ("ask", -0.4), ("not", -0.3), ("ask_dilution", -0.1), ("free_dilution", -0.1), ("pool", -1.0)] {
1023 beta[at(name)] = b;
1024 }
1025 let mut chol = vec![0.0; policy::P * policy::P];
1028 for k in 0..policy::P {
1029 chol[k * policy::P + k] = if k == at("ask") {
1030 1.0
1031 } else if k == at("pool") {
1032 0.5
1033 } else {
1034 0.3
1035 };
1036 }
1037 let model = policy::Model { beta, chol, sd: vec![1.0; policy::P], spread: [0.3; 3], ..policy::Model::default() };
1038 let rates = policy::Rates { free_stop: 1.0, free_live: 1.0, later: 0.5, wait: 0.0, ask_live: 1.0 };
1039 let fit = policy::Fit { key: "9-pool0001".into(), model: Some(model), rates, ..policy::Fit::default() };
1040 let mut s = summary::Summary { fit, ..summary::Summary::default() };
1041 let tokens = u32::try_from(crate::whole::TOKENS / 4).unwrap();
1042 let (ls, none) = (lessons(10), std::path::PathBuf::from("/nonexistent/borg-select/lessons.jsonl"));
1043 let mut rng = crate::memory::Rng::new(77);
1044 let mut deviated = 0;
1045 for c in 0..400u64 {
1046 let (n, from) = (1 + (rng.next_u64() % 3) as usize, (rng.next_u64() % 10) as usize);
1047 let applied: Vec<(&Challenge, u8)> = (0..n).map(|i| (&ls[(from + i) % 10], 1 + (rng.next_u64() % 3) as u8)).collect();
1048 let (session, check, stop) = (format!("t{}", c / 10), format!("{c:012x}"), c % 5 == 0);
1049 let had: Vec<String> = if c % 2 == 0 { vec![ls[(rng.next_u64() % 10) as usize].id.clone()] } else { Vec::new() };
1051 let ctx = Ctx {
1052 session: &session,
1053 check: &check,
1054 channel: if stop { "stop" } else { "live" },
1055 lessons: &none,
1056 summary: &s,
1057 tokens,
1058 had: &had,
1059 alpha: policy::ALPHA,
1060 };
1061 let r = graded_under(Policy::Learned, &applied, &ctx);
1062 let us = units(&applied);
1063 let routes: Vec<Route> = applied.iter().map(|(l, _)| r.route(&l.id)).collect();
1064 assert!(applied.iter().zip(&r.pf).all(|((l, _), pf)| !had.contains(&l.id) || (r.route(&l.id) != Route::Free && *pf == 0.0)), "check {c}");
1065 let nofree = nofree(&applied, &had);
1066 deviated += usize::from(routes.iter().map(|x| if *x == Route::Placebo { Route::Not } else { *x }).ne(policy::fixed_routes(&us, &nofree)));
1067 let held = |g: &summary::Gate| policy::remaining(g) - g.allowance;
1068 let before = held(&s.gate);
1069 let ids = |v: &[&Challenge]| v.iter().map(|c| c.id.clone()).collect::<Vec<_>>();
1072 let o = crate::outcome::Outcome {
1073 kind: crate::outcome::GRADED.into(),
1074 session: session.clone(),
1075 check: check.clone(),
1076 channel: ctx.channel.into(),
1077 lessons: applied.iter().map(|(c, _)| c.id.clone()).collect(),
1078 grades: applied.iter().map(|(_, g)| *g).collect(),
1079 ranks: (0..n as u16).collect(),
1080 applied: n as u16,
1081 chosen: ids(&r.ask),
1082 free: ids(&r.free),
1083 placebo: ids(&r.placebo),
1084 p: r.p.clone(),
1085 pf: r.pf.clone(),
1086 psi: r.log.psi,
1087 nofree,
1088 policy: r.log.policy.into(),
1089 fit: r.log.fit.clone(),
1090 source: r.log.source.into(),
1091 tokens,
1092 ..Default::default()
1093 };
1094 s = summary::fold(&format!("{}\n", serde_json::to_string(&o).unwrap()), s);
1095 assert!(
1096 (r.log.risk - (before - held(&s.gate)).max(0.0)).abs() < 1e-9,
1097 "check {c}: risked {} but the ledger took {}",
1098 r.log.risk,
1099 before - held(&s.gate)
1100 );
1101 assert!(policy::remaining(&s.gate) >= -1e-9, "check {c}: the gate overspent, {}", policy::remaining(&s.gate));
1102 }
1103 assert!(deviated > 0, "the learned policy deviated, so the gate was put to the test");
1104 assert_eq!(policy::ledger(&s), s.gate, "the refit's ledger is the folds'");
1107 }
1108
1109 #[test]
1110 fn a_budget_is_read_for_the_collective_whose_store_it_is() {
1111 let base = Path::new("/home/x/.local/share/borg");
1112 let lessons = crate::collective::store(base, "team").join("lessons.jsonl");
1113 let cfg = |entry: &str| {
1114 serde_json::from_str::<crate::collective::Config>(&format!(r#"{{"collectives":[{{"name":"other","paths":[],"budget":5}},{entry}]}}"#)).unwrap()
1115 };
1116 assert_eq!(budget_in(&cfg(r#"{"name":"team","paths":[],"budget":400000}"#), &lessons), Some(400_000.0));
1117 assert_eq!(budget_in(&cfg(r#"{"name":"team","paths":[]}"#), &lessons), None, "off by default");
1118 assert_eq!(budget_in(&cfg(r#"{"name":"team","paths":[],"budget":0}"#), &lessons), None);
1119 assert_eq!(budget_in(&cfg(r#"{"name":"team","paths":[],"budget":"lots"}"#), &lessons), None);
1120 }
1121
1122 #[test]
1123 fn a_risk_is_read_for_the_collective_whose_store_it_is_and_only_as_a_share() {
1124 let base = Path::new("/home/x/.local/share/borg");
1125 let lessons = crate::collective::store(base, "team").join("lessons.jsonl");
1126 let cfg = |entry: &str| {
1127 serde_json::from_str::<crate::collective::Config>(&format!(r#"{{"collectives":[{{"name":"other","paths":[],"risk":0.5}},{entry}]}}"#)).unwrap()
1128 };
1129 assert_eq!(risk_in(&cfg(r#"{"name":"team","paths":[],"risk":0.2}"#), &lessons), Some(0.2));
1130 assert_eq!(risk_in(&cfg(r#"{"name":"team","paths":[],"risk":0}"#), &lessons), Some(0.0), "0 turns the test off");
1131 assert_eq!(risk_in(&cfg(r#"{"name":"team","paths":[],"risk":1}"#), &lessons), Some(1.0));
1132 assert_eq!(risk_in(&cfg(r#"{"name":"team","paths":[]}"#), &lessons), None, "unset: the policy's ALPHA");
1133 for bad in ["-0.1", "1.5", "\"lots\"", "null"] {
1134 assert_eq!(risk_in(&cfg(&format!(r#"{{"name":"team","paths":[],"risk":{bad}}}"#)), &lessons), None, "{bad}");
1135 }
1136 }
1137
1138 #[test]
1139 fn a_check_id_is_short_stable_hex_and_differs_per_check() {
1140 let id = check_id("s", "stop", 1_000, 42);
1141 assert_eq!(id.len(), CHECK_HEX);
1142 assert!(id.bytes().all(|b| b.is_ascii_hexdigit()), "{id}");
1143 assert_eq!(id, check_id("s", "stop", 1_000, 42), "stable");
1144 let others = [check_id("s", "live", 1_000, 42), check_id("s", "stop", 1_001, 42), check_id("s", "stop", 1_000, 43), check_id("t", "stop", 1_000, 42)];
1145 assert!(others.iter().all(|o| *o != id), "{others:?}");
1146 assert_ne!(check_id("ab", "c", 1, 1), check_id("a", "bc", 1, 1));
1148 }
1149
1150 #[test]
1151 fn a_seed_depends_on_session_channel_and_check() {
1152 let (s, none) = (summary::Summary::default(), std::path::PathBuf::new());
1153 let at = |session, channel, check| seed(&Ctx { session, check, channel, lessons: &none, summary: &s, tokens: 0, had: &[], alpha: policy::ALPHA });
1154 assert_eq!(at("s", "stop", "a1"), at("s", "stop", "a1"));
1155 assert_ne!(at("s", "stop", "a1"), at("s", "live", "a1"));
1156 assert_ne!(at("s", "stop", "a1"), at("s", "stop", "a2"));
1157 assert_ne!(at("s", "stop", "a1"), at("t", "stop", "a1"));
1158 }
1159}