1use std::collections::{BTreeMap, HashSet};
27use std::path::Path;
28
29use serde::Serialize;
30use serde_json::{Value, json};
31
32use crate::clock::{DAY_MS, ago};
33use crate::collective::{self, Route, Share};
34use crate::msg::{self, Address, Message};
35use crate::presence::{self, Presence, State};
36use crate::{install, learn, protocol, select, steer, update};
37
38#[derive(Debug, Clone, Serialize)]
39pub struct Drone {
40 pub designation: String,
42 pub number: usize,
44 pub node: String,
46 pub state: State,
47 pub paused: bool,
49 #[serde(flatten)]
50 pub p: Presence,
51}
52
53#[derive(Debug, Clone, Default, Serialize)]
55pub struct Inbox {
56 pub unread: BTreeMap<String, usize>,
58 pub recent: Vec<Message>,
60 pub day: usize,
62 #[serde(skip)]
64 pub here: String,
65 pub listen: BTreeMap<String, crate::listen::Status>,
67}
68
69pub const RECENT: usize = 5;
71
72const NUMBERS: [&str; 20] = [
73 "One",
74 "Two",
75 "Three",
76 "Four",
77 "Five",
78 "Six",
79 "Seven",
80 "Eight",
81 "Nine",
82 "Ten",
83 "Eleven",
84 "Twelve",
85 "Thirteen",
86 "Fourteen",
87 "Fifteen",
88 "Sixteen",
89 "Seventeen",
90 "Eighteen",
91 "Nineteen",
92 "Twenty",
93];
94
95pub fn word(n: usize) -> String {
96 NUMBERS.get(n.wrapping_sub(1)).map(|s| s.to_string()).unwrap_or_else(|| n.to_string())
97}
98
99pub fn assemble(local: Vec<Presence>, remote: Vec<presence::Remote>, now: i64, alive: impl Fn(u32) -> bool, paused: impl Fn(&Presence) -> bool) -> Vec<Drone> {
101 let mut all: Vec<(String, Presence, State, bool)> = local
102 .into_iter()
103 .map(|p| {
104 let s = presence::state(&p, now, &alive);
105 let z = s != State::Ended && paused(&p);
106 (String::new(), p, s, z)
107 })
108 .collect();
109 all.extend(remote.into_iter().map(|r| {
111 let s = presence::state(&r.p, now, |_| true);
112 (r.key, r.p, s, false)
113 }));
114 all.sort_by(|a, b| (a.1.started, &a.0, &a.1.session).cmp(&(b.1.started, &b.0, &b.1.session)));
115 let n = all.len();
116 all.into_iter()
117 .enumerate()
118 .map(|(i, (node, p, state, paused))| Drone { designation: format!("{} of {}", word(i + 1), word(n)), number: i + 1, node, state, paused, p })
119 .collect()
120}
121
122pub fn drones(home: &Path, may_sync: bool, now: i64) -> Vec<Drone> {
124 let remote = if may_sync { presence::remote(home, now) } else { Vec::new() };
125 assemble(presence::local(home, now), remote, now, presence::alive, |p| steer::paused(home, &p.session, Some(p.started), now))
126}
127
128pub fn inbox(home: &Path, drones: &[Drone], now: i64) -> Inbox {
131 let listen = listen_of(home, drones);
132 if std::fs::metadata(msg::path(home)).is_err() {
133 return Inbox { listen, ..Inbox::default() };
134 }
135 let all: Vec<Message> = msg::read(home).into_iter().filter(|m| now - m.ms <= msg::TTL_MS).collect();
136 let here = msg::node(home);
137 let unread = unread_of(home, drones, &here, &all, now);
138 let day = all.iter().filter(|m| now - m.ms <= DAY_MS).count();
139 let mut recent = all;
140 recent.sort_by(|a, b| (b.ms, &b.id).cmp(&(a.ms, &a.id)));
141 recent.truncate(RECENT);
142 Inbox { unread, recent, day, here, listen }
143}
144
145pub fn acknowledge(home: &Path, cwd: &str, shown: &[&Message], done: &mut HashSet<String>, now: i64) {
154 let to_engineer: Vec<&Message> =
155 shown.iter().copied().filter(|m| matches!(Address::parse(&m.to), Some(Address::Engineer { .. })) && !done.contains(&m.id)).collect();
156 if to_engineer.is_empty() {
157 return;
158 }
159 let _ = msg::ack_engineer(home, cwd, &to_engineer, now);
160 done.extend(to_engineer.iter().map(|m| m.id.clone()));
161}
162
163pub fn engineer_looks<'a>(caller: impl Fn(&[Drone]) -> Result<Option<(String, &'static str)>, String>, mut drones: impl Iterator<Item = &'a [Drone]>) -> bool {
173 drones.all(|ds| matches!(caller(ds), Ok(None)))
174}
175
176pub(crate) fn engineer_dir(dir: Option<&str>) -> String {
179 let d = dir.map_or_else(|| std::env::current_dir().unwrap_or_default(), std::path::PathBuf::from);
180 std::path::absolute(&d).unwrap_or(d).display().to_string()
181}
182
183fn listen_of(home: &Path, drones: &[Drone]) -> BTreeMap<String, crate::listen::Status> {
185 drones
186 .iter()
187 .filter(|d| d.node.is_empty() && d.state != State::Ended)
188 .filter_map(|d| crate::listen::status(home, &d.p.session).map(|s| (d.p.session.clone(), s)))
189 .collect()
190}
191
192pub fn wake_cell(inbox: &Inbox, d: &Drone) -> &'static str {
195 inbox.listen.get(&d.p.session).map_or("-", |s| s.name())
196}
197
198pub fn unread_of(home: &Path, drones: &[Drone], here: &str, all: &[Message], now: i64) -> BTreeMap<String, usize> {
201 drones
202 .iter()
203 .filter(|d| d.node.is_empty() && d.state != State::Ended)
204 .map(|d| (d.p.session.clone(), msg::unread(home, all, here, &d.p.session, d.p.started, now)))
205 .collect()
206}
207
208fn drone_at<'a>(drones: &'a [Drone], here: &str, node: &str, session: &str) -> Option<&'a Drone> {
210 drones.iter().find(|d| d.p.session == session && if d.node.is_empty() { node == here } else { d.node == node })
211}
212
213pub fn sender(drones: &[Drone], here: &str, m: &Message) -> String {
218 let name = match Address::parse(&m.by) {
219 Some(Address::Engineer { email }) if email.is_empty() => msg::ENGINEER.into(),
220 Some(Address::Engineer { email }) => collective::aliases().display(&email),
221 _ => match drone_at(drones, here, &m.node, &m.by) {
222 Some(d) => d.designation.clone(),
223 None => msg::sender_label(m),
224 },
225 };
226 if m.node == here { name } else { format!("{name} @{}", msg::short_node(&m.node)) }
227}
228
229pub fn recipient(drones: &[Drone], here: &str, to: &str) -> String {
234 match Address::parse(to) {
235 Some(Address::Engineer { email }) if !email.is_empty() => collective::aliases().display(&email),
236 Some(Address::Local { node }) if node == here => msg::LOCAL.into(),
237 Some(Address::Local { node }) => format!("{} @{}", msg::LOCAL, msg::short_node(&node)),
238 Some(Address::All) => msg::COLLECTIVE.into(),
239 Some(Address::Session { id, node }) => {
240 let d = match node {
241 Some(n) => drone_at(drones, here, &n, &id),
242 None => drones.iter().find(|d| d.p.session == id),
243 };
244 d.map_or_else(|| msg::short_session(&id), |d| d.designation.clone())
245 }
246 Some(a) => a.to_string(),
247 None => msg::short_session(to),
248 }
249}
250
251pub fn message_line(drones: &[Drone], here: &str, m: &Message) -> String {
254 let ask = if m.asks() { " asks" } else { "" };
255 format!("{}{ask} -> {}: {}", sender(drones, here, m), recipient(drones, here, &m.to), m.text)
256}
257
258pub fn unread_cell(inbox: &Inbox, d: &Drone) -> String {
260 inbox.unread.get(&d.p.session).map_or_else(|| "-".into(), usize::to_string)
261}
262
263pub fn resolve<'a>(drones: &'a [Drone], target: &str) -> Result<Option<&'a Drone>, String> {
269 let t = target.trim();
270 if [msg::ALL, msg::COLLECTIVE, msg::LOCAL].iter().any(|w| t.eq_ignore_ascii_case(w)) {
271 return Ok(None);
272 }
273 let first = t.split_whitespace().next().unwrap_or_default();
274 let number = first.parse::<usize>().ok().or_else(|| NUMBERS.iter().position(|w| w.eq_ignore_ascii_case(first)).map(|i| i + 1));
275 if let Some(n) = number {
276 return drones.iter().find(|d| d.number == n).map(Some).ok_or_else(|| format!("no drone {target}: there are {} (`borg top`)", drones.len()));
277 }
278 let mut hits: Vec<&Drone> = drones.iter().filter(|d| d.p.session == t || (t.len() >= 4 && d.p.session.starts_with(t))).collect();
279 if hits.iter().any(|d| d.state != State::Ended) {
280 hits.retain(|d| d.state != State::Ended);
281 }
282 if hits.iter().any(|d| d.node.is_empty()) {
283 hits.retain(|d| d.node.is_empty());
284 }
285 match hits.as_slice() {
286 [d] => Ok(Some(d)),
287 [] => Err(format!("no drone or session {target:?} (`borg top`)")),
288 _ => Err(format!("{target:?} names {} sessions: give more of the id", hits.len())),
289 }
290}
291
292pub const COLUMN_NODE_CHARS: usize = 6;
296
297pub fn column_node(node: &str) -> String {
299 node.chars().take(COLUMN_NODE_CHARS).collect()
300}
301
302const MESSAGE_CHARS: usize = 120;
305
306fn cut(s: &str, n: usize) -> String {
307 if s.chars().count() <= n { s.to_string() } else { format!("{}…", s.chars().take(n - 1).collect::<String>()) }
308}
309
310pub fn shows_last(total: usize, last: usize, width: usize) -> bool {
315 total <= width || total.saturating_sub(last) < width.saturating_sub(1)
316}
317
318fn prints_text(drones: &[Drone], here: &str, m: &Message) -> bool {
322 shows_last(message_line(drones, here, m).chars().count(), m.text.chars().count(), MESSAGE_CHARS)
323}
324
325pub fn render(name: &str, share: Share, drones: &[Drone], inbox: &Inbox, now: i64) -> String {
327 let working = drones.iter().filter(|d| matches!(d.state, State::Working | State::Waiting)).count();
328 let remote = drones.iter().filter(|d| !d.node.is_empty()).count();
329 let share = match share {
330 Share::Team => "team",
331 Share::Local => "local, never leaves this machine",
332 };
333 let mut s = format!("{name} ({share}): {} drone(s), {working} active, {remote} remote\n", drones.len());
334 if !drones.is_empty() {
335 s.push_str(&drone_table(drones, inbox, now));
336 }
337 if !inbox.recent.is_empty() {
338 s.push_str(&format!(" messages: {} in the last day, {} unread\n", inbox.day, inbox.unread.values().sum::<usize>()));
339 for m in &inbox.recent {
340 s.push_str(&format!(" {:>6} {}\n", ago(now - m.ms), cut(&message_line(drones, &inbox.here, m), MESSAGE_CHARS)));
341 }
342 }
343 s
344}
345
346fn drone_table(drones: &[Drone], inbox: &Inbox, now: i64) -> String {
347 let mut s = format!(
348 " {:<18} {:<8} {:<24} {:<14} {:<8} {:<6} {:>6} {:>6} {:<40} {}\n",
349 "DRONE", "SESSION", "ENGINEER", "REPO", "STATE", "WAKE", "IDLE", "UNREAD", "DOING", "FILES"
350 );
351 for d in drones {
352 let state = if d.paused { "paused" } else { d.state.name() };
353 let engineer = if d.node.is_empty() { d.p.engineer.clone() } else { format!("{} @{}", d.p.engineer, column_node(&d.node)) };
354 let line = format!(
355 " {:<18} {:<8} {:<24} {:<14} {:<8} {:<6} {:>6} {:>6} {:<40} {}",
356 d.designation,
357 cut(&d.p.session, 8),
358 cut(if engineer.is_empty() { "?" } else { &engineer }, 24),
359 cut(&d.p.repo, 14),
360 state,
361 wake_cell(inbox, d),
362 ago(now - d.p.ms),
363 unread_cell(inbox, d),
364 cut(&d.p.prompt, 40),
365 d.p.files.join(", ")
366 );
367 s.push_str(line.trim_end());
368 s.push('\n');
369 }
370 s
371}
372
373pub fn to_json(name: &str, share: Share, drones: &[Drone]) -> Value {
374 json!({"collective": name, "share": if share == Share::Team { "team" } else { "local" }, "drones": drones})
375}
376
377pub fn to_json_with(name: &str, share: Share, drones: &[Drone], inbox: &Inbox) -> Value {
379 let mut v = to_json(name, share, drones);
380 if let Some(ds) = v["drones"].as_array_mut() {
381 for (o, d) in ds.iter_mut().zip(drones) {
382 if let Some(n) = inbox.unread.get(&d.p.session) {
383 o["unread"] = json!(n);
384 }
385 if let Some(l) = inbox.listen.get(&d.p.session) {
386 o["listen"] = json!(l);
387 }
388 }
389 }
390 v["messages"] = json!({"day": inbox.day, "recent": inbox.recent});
391 v
392}
393
394#[derive(Debug, Clone, Default, PartialEq, Serialize)]
404pub struct Selection {
405 pub policy: &'static str,
408 pub default: &'static str,
411 pub source: &'static str,
414 pub gate: f64,
419 pub allowance: f64,
424 pub ledger: f64,
425 pub fit: String,
427 pub at: u64,
429 pub checks: u64,
431 pub sessions: u64,
432 pub deviations: Vec<Count>,
437 pub fallbacks: Vec<Count>,
441 pub spend: Vec<Spend>,
446 pub budget: Option<Budget>,
448 pub effects: Vec<Estimate>,
451 pub half_life: Option<u32>,
453 pub labels: Vec<Count>,
457 pub declaration: Option<Estimate>,
459 pub delivery: Option<select::policy::Rates>,
462 pub evaluation: Option<Evaluation>,
464 pub measurement: Option<Measurement>,
467 pub pool: Vec<Yield>,
469 pub latency: Vec<Latency>,
471 pub test: Option<Test>,
474}
475
476#[derive(Debug, Clone, Default, PartialEq, Serialize)]
481pub struct Test {
482 pub risk: f64,
485 pub set: bool,
487 pub ran: u64,
490 pub control: u64,
491 pub refused: u64,
494 pub sessions: u64,
496 pub kept: u64,
499 pub by_k: Vec<Draws>,
501 pub due: bool,
508 pub due_checks: u64,
509 pub due_sessions: u64,
510 pub due_min_sessions: u64,
511 pub dilution: Vec<Estimate>,
512 pub ask_free: Option<Estimate>,
513}
514
515#[derive(Debug, Clone, Default, PartialEq, Serialize)]
517pub struct Draws {
518 pub k: u32,
519 pub draws: Vec<u64>,
520 pub refused: u64,
521}
522
523impl Test {
524 #[must_use]
527 pub fn of(summary: &select::summary::Summary, risk: Option<f64>) -> Test {
528 use select::replay::{TEST_CHECKS, TEST_MIN_SESSIONS, TEST_SESSIONS, test_readout};
529 let e = &summary.experiment;
530 let (ran, refused) = e.totals();
531 let r = test_readout(summary);
532 let estimate = |name: String, unit: &str, x: &crate::ope::Estimate| {
534 x.value.map(|v| Estimate { name, unit: unit.into(), mean: 100.0 * v, lo: 100.0 * x.lo, hi: 100.0 * x.hi, n: x.n as u64 })
535 };
536 let mut by_k: Vec<Draws> = Vec::new();
537 let at = |by_k: &mut Vec<Draws>, k: u32| match by_k.iter().position(|d| d.k == k) {
538 Some(i) => i,
539 None => {
540 by_k.push(Draws { k, draws: vec![0; k as usize + 1], refused: 0 });
541 by_k.len() - 1
542 }
543 };
544 for &(k, s, n) in &e.ran {
545 let i = at(&mut by_k, k);
546 if let Some(c) = by_k[i].draws.get_mut(s as usize) {
547 *c += n;
548 }
549 }
550 for &(k, n) in &e.refused {
551 let i = at(&mut by_k, k);
552 by_k[i].refused += n;
553 }
554 by_k.sort_by_key(|d| d.k);
555 Test {
556 risk: risk.unwrap_or(select::policy::ALPHA),
557 set: risk.is_some(),
558 ran,
559 control: e.control,
560 refused,
561 sessions: u64::from(e.sessions),
562 kept: r.kept,
563 by_k,
564 due: r.due,
565 due_checks: TEST_CHECKS,
566 due_sessions: u64::from(TEST_SESSIONS),
567 due_min_sessions: u64::from(TEST_MIN_SESSIONS),
568 dilution: r.by_asked.iter().filter_map(|(m, x)| estimate(format!("{m} asked − 1 asked"), "points changed", x)).collect(),
569 ask_free: r.ask_free.as_ref().and_then(|x| estimate("grade 2 asked − handed over".into(), "points changed", x)),
570 }
571 }
572}
573
574#[derive(Debug, Clone, Default, PartialEq, Serialize)]
576pub struct Count {
577 pub name: String,
578 pub part: u64,
579 pub whole: u64,
580}
581
582#[derive(Debug, Clone, Default, PartialEq, Serialize)]
584pub struct Spend {
585 pub source: String,
587 pub calls: u64,
588 pub rounds: u64,
590 pub usd: f64,
591}
592
593#[derive(Debug, Clone, Default, PartialEq, Serialize)]
597pub struct Budget {
598 pub tokens: f64,
599 pub spent: f64,
600 pub price: f64,
601}
602
603#[derive(Debug, Clone, Default, PartialEq, Serialize)]
605pub struct Estimate {
606 pub name: String,
607 pub unit: String,
612 pub mean: f64,
613 pub lo: f64,
614 pub hi: f64,
615 pub n: u64,
617}
618
619#[derive(Debug, Clone, Default, PartialEq, Serialize)]
622pub struct Evaluation {
623 pub sessions: u64,
625 pub mean: f64,
628 pub ucb: Option<f64>,
629 pub units: u64,
631 pub ess: f64,
632 pub max_weight: f64,
634 pub truncated: u64,
636}
637
638#[derive(Debug, Clone, Default, PartialEq, Serialize)]
643pub struct Measurement {
644 pub placebos: u64,
646 pub judged: u64,
648 pub tail_shown: u64,
651 pub tail_applied: u64,
652}
653
654#[derive(Debug, Clone, Default, PartialEq, Serialize)]
657pub struct Yield {
658 pub from: u32,
659 pub to: u32,
660 pub shown: u64,
661 pub applied: u64,
662 pub rate: f64,
663}
664
665#[derive(Debug, Clone, Default, PartialEq, Serialize)]
668pub struct Latency {
669 pub harness: String,
670 pub checks: u64,
671 pub p50_ms: u64,
673 pub p95_ms: u64,
674 pub budget_ms: Option<u64>,
676 pub over: u64,
679}
680
681const QUANTILES: [f64; 2] = [0.50, 0.95];
684
685fn quantile(sorted: &[u64], q: f64) -> u64 {
687 let rank = ((q * sorted.len() as f64).ceil() as usize).clamp(1, sorted.len().max(1));
688 sorted.get(rank - 1).copied().unwrap_or(0)
689}
690
691impl Selection {
692 #[must_use]
696 pub fn of(summary: &select::summary::Summary, policy: select::Policy, risk: Option<f64>) -> Selection {
697 use select::summary::{LIVE, Label, STOP};
698 let s = summary;
699 let learned = policy == select::Policy::Learned;
700 let reverted = learned && s.monitor.reverted;
701 let default = select::precedence(None, None, false).name();
702 let count = |name: &str, part: usize, whole: usize| Count { name: name.into(), part: part as u64, whole: whole as u64 };
703 let (sessions, checks) = s.checked();
705 let deviations = match s.gate.checks {
706 0 => Vec::new(),
707 n => vec![count("route", s.gate.deviated as usize, n as usize)],
708 };
709 let fallbacks = select::summary::FELL_BACK
711 .iter()
712 .zip(s.fell_back)
713 .filter(|_| checks > 0)
714 .map(|(reason, n)| Count { name: (*reason).into(), part: n, whole: checks })
715 .collect();
716 let spend = [("stop", STOP), ("live", LIVE)]
717 .into_iter()
718 .filter(|(_, i)| s.spend[*i].calls > 0)
719 .map(|(source, i)| Spend { source: source.into(), calls: s.spend[i].calls, rounds: s.spend[i].rounds, usd: s.spend[i].usd })
720 .collect();
721 let budget = s.fit.budget.map(|tokens| Budget { tokens, spent: s.pool.tokens as f64 / sessions.max(1) as f64, price: s.fit.lambda });
722 let judged = |l: Label| l.verdict();
724 let of = |route: select::Route| {
730 let of: Vec<_> = s.applies.iter().filter(|a| a.route == route && (route != select::Route::Free || !a.had)).collect();
731 (of.iter().filter(|a| judged(a.label)).count(), of.len())
732 };
733 let mut labels = Vec::new();
734 for (name, route) in [("judged, asked", select::Route::Ask), ("judged, free", select::Route::Free), ("judged, placebo", select::Route::Placebo)] {
735 let (part, whole) = of(route);
736 labels.push(count(name, part, whole));
737 }
738 let answers: Vec<_> = s.applies.iter().filter(|a| a.jv >= crate::predict::PURE_UNCLEAR_FROM && (judged(a.label) || a.label.pending())).collect();
743 let share = |name: &str, label: Label, route: Option<select::Route>| {
744 let of: Vec<_> = answers.iter().filter(|a| route.is_none_or(|r| a.route == r)).collect();
745 count(name, of.iter().filter(|a| a.label == label).count(), of.len())
746 };
747 for (name, label) in [("unclear", Label::Unclear), ("not arisen", Label::NotArisen), ("consequence remains", Label::Remains)] {
748 labels.push(share(name, label, None));
749 }
750 for (name, route) in [("unclear, asked", select::Route::Ask), ("unclear, free", select::Route::Free), ("unclear, placebo", select::Route::Placebo)] {
751 labels.push(share(name, Label::Unclear, Some(route)));
752 }
753 let later = s.applies.iter().filter(|a| matches!(a.route, select::Route::Not | select::Route::Placebo)).count();
754 let (placebos_judged, placebos) = of(select::Route::Placebo);
755 labels.push(count("placebo", placebos, later));
756 labels.retain(|c| c.whole > 0);
757 let measurement = (checks > 0).then_some(Measurement {
758 placebos: placebos as u64,
759 judged: placebos_judged as u64,
760 tail_shown: s.pool.tail.0,
761 tail_applied: s.pool.tail.1,
762 });
763 let share = |stripped: bool| {
765 let of: Vec<_> = s.applies.iter().filter(|a| a.route == select::Route::Ask && judged(a.label) && a.stripped == stripped).collect();
766 (of.iter().filter(|a| a.label == Label::Changed).count() as f64, of.len() as f64)
767 };
768 let ((c1, n1), (c0, n0)) = (share(false), share(true));
769 let declaration = (n1 > 0.0 && n0 > 0.0).then(|| {
770 let (r1, r0) = (c1 / n1, c0 / n0);
771 let se = (r1 * (1.0 - r1) / n1 + r0 * (1.0 - r0) / n0).sqrt();
772 let mean = 100.0 * (r1 - r0);
773 Estimate { name: "declaration".into(), unit: "points".into(), mean, lo: mean - 100.0 * Z95 * se, hi: mean + 100.0 * Z95 * se, n: (n1 + n0) as u64 }
774 });
775 let pool = s
776 .pool
777 .bins
778 .iter()
779 .enumerate()
780 .filter(|(_, (shown, _))| *shown > 0)
781 .map(|(b, &(shown, applied))| Yield {
782 from: (1u32 << b) - 1,
783 to: (1u32 << (b + 1)) - 1,
784 shown,
785 applied,
786 rate: s.fit.pool.yields.get(b).map_or(applied as f64 / shown as f64, |(a, b)| a / (a + b)),
787 })
788 .collect();
789 let (allowance, ledger) = if learned && !reverted { select::policy::gate_parts(&s.gate, risk.unwrap_or(select::policy::ALPHA)) } else { (0.0, 0.0) };
790 let mon = &s.monitor;
791 let evaluation = (mon.sessions > 0).then(|| Evaluation {
792 sessions: u64::from(mon.sessions),
793 mean: mon.mean,
794 ucb: mon.ucb,
795 units: u64::from(mon.labelled),
796 ess: mon.ess,
797 max_weight: mon.max_weight,
798 truncated: u64::from(mon.truncated),
799 });
800 let latency = s
801 .pool
802 .stops
803 .iter()
804 .filter(|(_, calls)| !calls.is_empty())
805 .map(|(harness, calls)| {
806 let mut ms: Vec<u64> = calls.iter().map(|&(ms, _)| u64::from(ms)).collect();
807 ms.sort_unstable();
808 let budget = crate::harness::ALL.iter().find(|h| h.name() == harness).and_then(|h| h.caps().stop_budget);
809 Latency {
810 harness: harness.clone(),
811 checks: ms.len() as u64,
812 p50_ms: quantile(&ms, QUANTILES[0]),
813 p95_ms: quantile(&ms, QUANTILES[1]),
814 budget_ms: budget.map(|b| u64::try_from(b.as_millis()).unwrap_or(u64::MAX)),
815 over: calls.iter().filter(|(_, failed)| *failed).count() as u64,
816 }
817 })
818 .collect();
819 Selection {
820 policy: policy.name(),
821 default,
822 source: if reverted { select::REVERTED_SOURCE } else { policy.name() },
823 gate: if learned && !reverted { select::policy::remaining_at(&s.gate, risk.unwrap_or(select::policy::ALPHA)) } else { 0.0 },
824 allowance,
825 ledger,
826 fit: s.fit.key.clone(),
827 at: s.at,
828 checks,
829 sessions,
830 deviations,
831 fallbacks,
832 spend,
833 budget,
834 effects: s.fit.model.as_ref().map(|m| effects(m, s.fit.labels)).unwrap_or_default(),
835 half_life: s.fit.model.as_ref().map(|_| s.fit.half_life.unwrap_or(0)),
836 labels,
837 declaration,
838 delivery: (!s.fit.key.is_empty()).then(|| s.fit.rates.clone()),
839 evaluation,
840 measurement,
841 pool,
842 latency,
843 test: (learned || s.experiment.totals() != (0, 0)).then(|| Test::of(s, risk)),
844 }
845 }
846
847 #[must_use]
850 pub fn shown(&self) -> bool {
851 self.checks > 0 || self.source != self.default
852 }
853}
854
855const Z95: f64 = 1.96;
857
858fn effects(m: &select::policy::Model, n: u32) -> Vec<Estimate> {
862 let p = select::policy::P;
863 if m.beta.len() != p || m.chol.len() != p * p {
864 return Vec::new();
865 }
866 let sd = |k: usize| m.chol[k * p..=k * p + k].iter().map(|x| x * x).sum::<f64>().sqrt();
868 let mut out: Vec<Estimate> = (0..p)
869 .map(|k| Estimate {
870 name: select::policy::NAMES[k].into(),
871 unit: "coefficient".into(),
872 mean: m.beta[k],
873 lo: m.beta[k] - Z95 * sd(k),
874 hi: m.beta[k] + Z95 * sd(k),
875 n: u64::from(n),
876 })
877 .collect();
878 for (name, v) in ["session spread", "lesson spread", "judge spread"].into_iter().zip(m.spread) {
879 out.push(Estimate { name: name.into(), unit: "logit".into(), mean: v, lo: v, hi: v, n: u64::from(n) });
880 }
881 out
882}
883
884#[must_use]
886pub fn selection(lessons: &Path, policy: select::Policy) -> Selection {
887 Selection::of(&select::summary::load(lessons), policy, select::risk_of(lessons))
888}
889
890#[must_use]
899pub fn selection_line(s: &Selection) -> String {
900 let default = s.source == s.default;
901 let head = match s.source {
902 select::REVERTED_SOURCE => "REVERTED to the fixed rule: the monitor saw harm beyond the gate's share (sticky)".to_string(),
903 "learned" => {
904 let opt = if default { "" } else { " (opt-in, BORG_SELECT=learned)" };
905 if s.gate > 0.0 {
906 format!("learned{opt}, gate {:.3} (quality it may still risk below the fixed rule)", s.gate)
907 } else if s.fit.is_empty() || s.allowance <= 0.0 {
908 format!("learned{opt}, cold start: gate 0, every decision the fixed rule's")
909 } else {
910 format!("learned{opt}, gate spent (ledger q05 {:+.3} against allowance {:.3}), deviations need positive evidence", s.ledger, s.allowance)
913 }
914 }
915 "fixed" if default => "the fixed rule (the default; BORG_SELECT=learned opts in to the learned policy)".into(),
916 "fixed" => "the fixed rule (BORG_SELECT=fixed)".into(),
917 "cap" => "the fixed rule with an ask cap (BORG_STOP_ASK_CAP, evaluation only)".into(),
918 "attention" => "attention's cut (BORG_STOP_ATTENTION=on, evaluation only)".into(),
919 other => other.to_string(),
920 };
921 if s.checks == 0 {
922 return format!("{head} · no graded check folded yet");
923 }
924 let mut parts = vec![head, format!("{} checks", s.checks)];
925 if !s.deviations.is_empty() {
926 parts.push(format!("deviated: {}", s.deviations.iter().map(|c| format!("{} {}/{}", c.name, c.part, c.whole)).collect::<Vec<_>>().join(", ")));
927 }
928 let fell: Vec<String> = s.fallbacks.iter().filter(|c| c.part > 0).map(|c| format!("{} {}", c.name, c.part)).collect();
929 if !fell.is_empty() {
930 parts.push(format!("learned decisions the fixed rule took over: {}", fell.join(", ")));
931 }
932 if let Some(t) = &s.test {
933 parts.push(test_line(t));
934 }
935 if let Some(m) = &s.measurement {
936 parts.push(format!("placebos {} drawn, {} judged", m.placebos, m.judged));
937 if m.tail_shown > 0 {
938 parts.push(format!("tail {} shown past the pool, {} applied", m.tail_shown, m.tail_applied));
939 }
940 }
941 if let Some(e) = &s.evaluation {
942 let reading = match e.ucb {
943 Some(u) => format!("monitor {:+.3}, bound {u:+.3} over {} sessions (below 0 reverts)", e.mean, e.sessions),
944 None => format!("monitor {:+.3} over {} sessions, no bound yet", e.mean, e.sessions),
945 };
946 parts.push(format!("{reading}, ESS {:.0} of {} weighed, largest weight {:.1}× the mean", e.ess, e.units, e.max_weight));
947 }
948 if !s.latency.is_empty() {
949 let secs = |ms: u64| format!("{:.1} s", ms as f64 / 1000.0);
950 let each: Vec<String> = s
951 .latency
952 .iter()
953 .map(|l| {
954 let budget = l.budget_ms.map_or_else(String::new, |b| format!(" of {}", secs(b)));
955 let failed = if l.over > 0 { format!(", {} failed", l.over) } else { String::new() };
956 format!("{} {}{budget}{failed}", l.harness, secs(l.p95_ms))
957 })
958 .collect();
959 parts.push(format!("stop p95 {}", each.join("; ")));
960 }
961 if s.sessions > 0 && !s.spend.is_empty() {
962 let n = s.sessions as f64;
963 let (usd, rounds) = s.spend.iter().fold((0.0, 0), |(u, r), x| (u + x.usd, r + x.rounds));
964 parts.push(format!("${:.2} and {:.1} extra rounds per session ({} sessions)", usd / n, rounds as f64 / n, s.sessions));
965 }
966 if let Some(b) = &s.budget {
967 parts.push(format!("budget {:.0} pool tokens per session: {:.0} read, price {:.2e} per token", b.tokens, b.spent, b.price));
968 }
969 parts.join(" · ")
970}
971
972fn test_line(t: &Test) -> String {
977 let risk = if t.set { format!("risk {:.2} (the collective's)", t.risk) } else { format!("risk {:.2}", t.risk) };
978 match (t.ran, t.risk <= 0.0) {
979 (0, true) => return "question-count test off (risk 0)".into(),
980 (0, false) => return format!("question-count test: no check yet, {risk}"),
981 _ => {}
982 }
983 let kept = if t.kept < t.ran { format!(", {} still in the summary", t.kept) } else { String::new() };
984 let head =
985 format!("question-count test {} drawn ({} control, {} refused by the gate{kept}) in {} sessions, {risk}", t.ran, t.control, t.refused, t.sessions);
986 match (&t.ask_free, t.due) {
987 (Some(e), true) => format!("{head}; readout: grade 2 asked − handed over {:+.1} [{:+.1}, {:+.1}] points changed", e.mean, e.lo, e.hi),
988 (None, true) => format!("{head}; readout due, no judged grade 2 on both routes yet"),
989 (_, false) => format!("{head}; readout after {} checks in {} sessions, or {} sessions", t.due_checks, t.due_min_sessions, t.due_sessions),
990 }
991}
992
993#[must_use]
995pub fn render_with(name: &str, share: Share, drones: &[Drone], inbox: &Inbox, select: Option<&Selection>, now: i64) -> String {
996 let mut s = render(name, share, drones, inbox, now);
997 if let Some(sel) = select.filter(|s| s.shown()) {
998 s.push_str(&format!(" selection: {}\n", selection_line(sel)));
999 }
1000 s
1001}
1002
1003pub fn version_line<'a>(stores: impl IntoIterator<Item = &'a Path>, checked: Option<&update::Checked>, check: bool) -> Option<String> {
1009 let mut parts: Vec<String> = Vec::new();
1010 if let Some(l) = checked.and_then(|c| c.latest.as_deref()).filter(|l| check && update::newer(l, update::VERSION)) {
1011 parts.push(format!("borg {l} is released, this is {}: borg update", update::VERSION));
1012 }
1013 let aliases = collective::aliases();
1014 let ours = protocol::current();
1015 for home in stores.into_iter().filter(|h| h.join("mesh").is_dir()) {
1016 let versions = protocol::versions(home);
1017 let mismatches = protocol::mismatches(home);
1018 let name = |key: &str| aliases.display(&protocol::member(key, versions.get(key)));
1019 for (key, m) in &mismatches {
1020 parts.push(format!("nothing syncs with {} ({}, this node {})", name(key), m.theirs, m.ours));
1021 }
1022 for (key, v) in versions.iter().filter(|(k, v)| v.protocol < ours && !mismatches.contains_key(*k)) {
1023 parts.push(format!("{} on /{} misses {}", name(key), v.protocol, protocol::misses(v.protocol, ours)));
1024 }
1025 }
1026 parts.dedup();
1027 (!parts.is_empty()).then(|| parts.join(" · "))
1028}
1029
1030fn routes(dir: Option<&str>) -> Result<Vec<Route>, String> {
1032 let cfg = collective::load()?;
1033 let base = learn::home();
1034 Ok(match dir {
1035 Some(d) => vec![collective::route(cfg.as_ref(), d, &base)].into_iter().filter(|r| *r != Route::Off).collect(),
1036 None => install::collectives(cfg.as_ref(), &base),
1037 })
1038}
1039
1040pub fn gather(dir: Option<&str>, now: i64) -> Result<(Vec<crate::top_live::Coll>, Option<String>), String> {
1043 let mut out = Vec::new();
1044 let routes = routes(dir)?;
1045 let stores: Vec<&Path> = routes
1046 .iter()
1047 .filter_map(|r| match r {
1048 Route::Collective { home, .. } => Some(home.as_path()),
1049 Route::Off => None,
1050 })
1051 .collect();
1052 let version = version_line(stores, update::checked().as_ref(), update::on());
1053 for r in &routes {
1054 let Route::Collective { name, home, share } = r else { continue };
1055 presence::prune(home, now);
1056 let mut ds = drones(home, collective::may_sync(r), now);
1057 for d in &mut ds {
1058 d.p.engineer = collective::aliases().display(&d.p.engineer);
1059 }
1060 let inbox = inbox(home, &ds, now);
1061 if ds.is_empty() && inbox.recent.is_empty() && dir.is_none() {
1062 continue;
1063 }
1064 let lessons = home.join("lessons.jsonl");
1067 let summary = select::summary::peek(&lessons);
1068 let select = Some(Selection::of(&summary, select::policy(), select::risk_of(&lessons))).filter(Selection::shown).map(|s| selection_line(&s));
1069 out.push(crate::top_live::Coll { name: name.clone(), home: home.clone(), share: *share, drones: ds, inbox, select });
1070 }
1071 Ok((out, version))
1072}
1073
1074fn screen(dir: Option<&str>, json_out: bool, now: i64) -> Result<String, String> {
1076 let routes = routes(dir)?;
1077 let text = screen_of(&routes, dir.is_some(), &engineer_dir(dir), msg::caller, json_out, now);
1078 let stores = routes.iter().filter_map(|r| match r {
1079 Route::Collective { home, .. } => Some(home.as_path()),
1080 Route::Off => None,
1081 });
1082 Ok(match version_line(stores, update::checked().as_ref(), update::on()).filter(|_| !json_out) {
1083 Some(v) => format!("{v}\n\n{text}"),
1084 None => text,
1085 })
1086}
1087
1088fn screen_of(
1093 routes: &[Route],
1094 one: bool,
1095 cwd: &str,
1096 caller: impl Fn(&[Drone]) -> Result<Option<(String, &'static str)>, String>,
1097 json_out: bool,
1098 now: i64,
1099) -> String {
1100 let mut text = String::new();
1101 let mut all = Vec::new();
1102 let (mut gathered, mut printed) = (Vec::new(), Vec::new());
1105 for r in routes {
1106 let Route::Collective { name, home, share } = r else { continue };
1107 presence::prune(home, now);
1108 let mut ds = drones(home, collective::may_sync(r), now);
1109 for d in &mut ds {
1111 d.p.engineer = collective::aliases().display(&d.p.engineer);
1112 }
1113 let inbox = inbox(home, &ds, now);
1114 let sel = selection(&home.join("lessons.jsonl"), select::policy());
1115 if json_out {
1116 let mut v = to_json_with(name, *share, &ds, &inbox);
1117 v["select"] = json!(sel);
1118 all.push(v);
1119 } else if !ds.is_empty() || !inbox.recent.is_empty() || one {
1120 text.push_str(&render_with(name, *share, &ds, &inbox, Some(&sel), now));
1121 text.push('\n');
1122 printed.push((home, inbox.recent.into_iter().filter(|m| prints_text(&ds, &inbox.here, m)).collect::<Vec<_>>()));
1123 }
1124 gathered.push(ds);
1125 }
1126 if !printed.is_empty() && engineer_looks(caller, gathered.iter().map(Vec::as_slice)) {
1127 let mut done = HashSet::new();
1128 for (home, recent) in &printed {
1129 acknowledge(home, cwd, &recent.iter().collect::<Vec<_>>(), &mut done, now);
1130 }
1131 }
1132 if json_out {
1133 return serde_json::to_string_pretty(&all).unwrap_or_default() + "\n";
1134 }
1135 if text.is_empty() {
1136 text = "no drones (borg top lists sessions whose hooks ran in the last day)\n".into();
1137 }
1138 text
1139}
1140
1141pub fn main(args: &[&str]) -> i32 {
1142 let (mut dir, mut once, mut json_out) = (None, false, false);
1143 let mut it = args.iter();
1144 while let Some(a) = it.next() {
1145 match *a {
1146 "--dir" if dir.is_none() => match it.next() {
1147 Some(d) => dir = Some(*d),
1148 None => return usage("--dir needs a value"),
1149 },
1150 "--once" => once = true,
1151 "--json" => json_out = true,
1152 a => return usage(&format!("unknown flag {a}")),
1153 }
1154 }
1155 if !json_out && !once && unsafe { libc::isatty(1) } == 1 {
1157 return crate::top_live::run(dir);
1158 }
1159 match screen(dir, json_out, learn::now_ms()) {
1160 Ok(s) => {
1161 print!("{s}");
1162 0
1163 }
1164 Err(e) => {
1165 eprintln!("borg top: {e}");
1166 1
1167 }
1168 }
1169}
1170
1171fn usage(why: &str) -> i32 {
1172 eprintln!("borg top: {why}\nusage: borg top [--once] [--json] [--dir DIR]");
1173 2
1174}
1175
1176#[cfg(test)]
1177pub(crate) fn tests_fixture() -> Vec<Drone> {
1178 tests::fixture()
1179}
1180
1181#[cfg(test)]
1182pub(crate) fn tests_inbox() -> Inbox {
1183 tests::inbox_fixture()
1184}
1185
1186#[cfg(test)]
1187mod tests {
1188 use super::*;
1189
1190 fn p(session: &str, engineer: &str, started: i64, ms: i64, event: &str, prompt: &str, files: &[&str]) -> Presence {
1191 Presence {
1192 session: session.into(),
1193 harness: "claude".into(),
1194 engineer: engineer.into(),
1195 repo: "borg-ai".into(),
1196 collective: "corp".into(),
1197 prompt: prompt.into(),
1198 files: files.iter().map(|f| f.to_string()).collect(),
1199 event: event.into(),
1200 ms,
1201 started,
1202 pid: None,
1203 }
1204 }
1205
1206 const NOW: i64 = 10_000_000;
1207
1208 pub(crate) fn fixture() -> Vec<Drone> {
1209 let local = vec![
1210 p("aaaa1111-x", "alice@x", 100, NOW - 5_000, "post_tool", "fix the flaky sync test", &["crates/borg-mesh/src/sync.rs", "src/recon.rs"]),
1211 p("cccc3333-x", "alice@x", 300, NOW - presence::IDLE_MS - 60_000, "stop", "write the release notes for the mesh daemon and more words", &[]),
1212 ];
1213 let remote = vec![presence::Remote {
1214 p: p("bbbb2222-y", "bob@y", 200, NOW - 120_000, "stop", "review PR 42", &["README.md"]),
1215 key: "beefcafe".repeat(8),
1216 sig: String::new(),
1217 }];
1218 assemble(local, remote, NOW, |_| true, |p| p.session.starts_with("aaaa"))
1219 }
1220
1221 fn message(by: &str, name: &str, to: &str, kind: msg::Kind, ms: i64, text: &str) -> Message {
1222 message_on("n1", by, name, to, kind, ms, text)
1223 }
1224
1225 fn message_on(node: &str, by: &str, name: &str, to: &str, kind: msg::Kind, ms: i64, text: &str) -> Message {
1226 let from = msg::Sender { by: by.into(), name: name.into(), harness: "claude".into(), node: node.into() };
1227 Message::new(kind, &from, to, None, ms, text)
1228 }
1229
1230 pub(crate) fn inbox_fixture() -> Inbox {
1231 Inbox {
1232 unread: [("aaaa1111-x".to_string(), 2), ("cccc3333-x".to_string(), 0)].into_iter().collect(),
1233 recent: vec![
1234 message("cccc3333-x", "Three of Three", "aaaa1111-x", msg::ASK, NOW - 60_000, "which test\nflakes?"),
1235 message("engineer:alice@x", "alice@x", msg::ALL, msg::TELL, NOW - 120_000, "release at five"),
1236 message_on(&"feed".repeat(16), "gone-session", "Six of Six", "zzzz9999-q", msg::TELL, NOW - 180_000, "done"),
1237 ],
1238 day: 3,
1239 here: "n1".into(),
1240 listen: [("aaaa1111-x".to_string(), crate::listen::Status::Waits), ("cccc3333-x".to_string(), crate::listen::Status::Listens)]
1241 .into_iter()
1242 .collect(),
1243 }
1244 }
1245
1246 #[test]
1247 fn renders_one_screen_per_collective() {
1248 let out = render("corp", Share::Team, &fixture(), &inbox_fixture(), NOW);
1249 let expected = "\
1250corp (team): 3 drone(s), 2 active, 1 remote
1251 DRONE SESSION ENGINEER REPO STATE WAKE IDLE UNREAD DOING FILES
1252 One of Three aaaa111… alice@x borg-ai paused waiter 5s 2 fix the flaky sync test crates/borg-mesh/src/sync.rs, src/recon.rs
1253 Two of Three bbbb222… bob@y @beefca borg-ai waiting - 2m - review PR 42 README.md
1254 Three of Three cccc333… alice@x borg-ai idle on 31m 0 write the release notes for the mesh da…
1255 messages: 3 in the last day, 2 unread
1256 1m Three of Three asks -> One of Three: which test flakes?
1257 2m alice@x -> collective: release at five
1258 3m Six of Six @feedfeedfeed -> zzzz9999: done
1259";
1260 assert_eq!(out, expected, "\n{out}");
1261 let none = Inbox::default();
1262 assert_eq!(render("private", Share::Local, &[], &none, NOW), "private (local, never leaves this machine): 0 drone(s), 0 active, 0 remote\n");
1263 }
1264
1265 fn learned() -> Selection {
1269 let spend = |source: &str, rounds, usd| Spend { source: source.into(), calls: 1, rounds, usd };
1270 let evaluation = Evaluation { sessions: 12, mean: 0.004, ucb: Some(0.031), units: 300, ess: 180.0, max_weight: 6.5, truncated: 3 };
1271 Selection {
1272 policy: "learned",
1273 default: "fixed",
1274 source: "learned",
1275 gate: 0.12,
1276 checks: 480,
1277 sessions: 12,
1278 deviations: vec![Count { name: "route".into(), part: 5, whole: 400 }],
1279 spend: vec![spend("stop", 4, 3.0), spend("live", 0, 2.04)],
1280 evaluation: Some(evaluation),
1281 measurement: Some(Measurement { placebos: 40, judged: 31, tail_shown: 12, tail_applied: 3 }),
1282 test: Some(Test {
1283 risk: 0.1,
1284 ran: 30,
1285 control: 9,
1286 refused: 4,
1287 sessions: 6,
1288 kept: 30,
1289 due_checks: 300,
1290 due_sessions: 30,
1291 due_min_sessions: 15,
1292 ..Test::default()
1293 }),
1294 ..Selection::default()
1295 }
1296 }
1297
1298 #[test]
1299 fn the_selection_line_says_what_decides_and_prices_spend_only_under_a_budget() {
1300 use select::Policy;
1301 use select::summary::Summary;
1302 let fresh = |policy, default| Selection { default, ..Selection::of(&Summary::default(), policy, None) };
1303 let fixed = fresh(Policy::Fixed, "fixed");
1305 assert_eq!(selection_line(&fixed), "the fixed rule (the default; BORG_SELECT=learned opts in to the learned policy) · no graded check folded yet");
1306 assert!(!fixed.shown(), "the default with nothing folded: nothing to show on the drones screen");
1307 let opted = fresh(Policy::Learned, "fixed");
1308 assert_eq!(
1309 selection_line(&opted),
1310 "learned (opt-in, BORG_SELECT=learned), cold start: gate 0, every decision the fixed rule's · no graded check folded yet"
1311 );
1312 assert!(opted.shown(), "a switch decides: shown");
1313 assert_eq!(selection_line(&fresh(Policy::Fixed, "learned")), "the fixed rule (BORG_SELECT=fixed) · no graded check folded yet");
1315 assert!(!fresh(Policy::Learned, "learned").shown() && fresh(Policy::Fixed, "learned").shown());
1316 let d = select::precedence(None, None, false);
1317 assert_eq!(Selection::of(&Summary::default(), Policy::Cap(2), None).default, d.name(), "the default is select.rs's, no switch set");
1318 assert!(!Selection::of(&Summary::default(), d, None).shown());
1319 let mut s = learned();
1320 let line = selection_line(&s);
1321 assert_eq!(
1322 line,
1323 "learned (opt-in, BORG_SELECT=learned), gate 0.120 (quality it may still risk below the fixed rule) · 480 checks · deviated: route 5/400 · question-count test 30 drawn (9 control, 4 refused by the gate) in 6 sessions, risk 0.10; readout after 300 checks in 15 sessions, or 30 sessions · placebos 40 drawn, 31 judged · tail 12 shown past the pool, 3 applied · monitor +0.004, bound +0.031 over 12 sessions (below 0 reverts), ESS 180 of 300 weighed, largest weight 6.5× the mean · $0.42 and 0.3 extra rounds per session (12 sessions)"
1324 );
1325 assert!(!line.contains("budget") && !line.contains("price"), "no budget set: spend measured, never priced: {line}");
1326 let mut early = learned();
1327 early.evaluation = Some(Evaluation { ucb: None, sessions: 3, units: 40, ess: 12.4, max_weight: 3.0, ..Evaluation::default() });
1328 let line = selection_line(&early);
1329 assert!(line.contains(" · monitor +0.000 over 3 sessions, no bound yet, ESS 12 of 40 weighed, largest weight 3.0× the mean · "), "{line}");
1330 early.latency = vec![
1331 Latency { harness: "codex".into(), checks: 20, p50_ms: 10_000, p95_ms: 19_000, budget_ms: Some(30_000), over: 1 },
1332 Latency { harness: "claude".into(), checks: 1, p50_ms: 4_000, p95_ms: 4_000, budget_ms: None, over: 0 },
1333 ];
1334 let line = selection_line(&early);
1335 assert!(line.contains(" · stop p95 codex 19.0 s of 30.0 s, 1 failed; claude 4.0 s · $"), "per harness, against its stop budget: {line}");
1336 s.budget = Some(Budget { tokens: 30_000.0, spent: 24_000.0, price: 0.0002 });
1337 assert!(selection_line(&s).ends_with(" · budget 30000 pool tokens per session: 24000 read, price 2.00e-4 per token"), "{}", selection_line(&s));
1338 s.source = select::REVERTED_SOURCE;
1339 assert!(selection_line(&s).starts_with("REVERTED to the fixed rule"), "{}", selection_line(&s));
1340 }
1341
1342 #[test]
1346 fn a_spent_gate_is_not_a_cold_start() {
1347 use select::Policy;
1348 use select::summary::{Gate, Summary};
1349 let line = |s: &Summary| selection_line(&Selection { default: "learned", ..Selection::of(s, Policy::Learned, None) });
1350 let mut s = Summary::default();
1351 s.fit.key = "3556601-bc6d6191".into();
1352 assert!(line(&s).starts_with("learned, cold start: gate 0, every decision the fixed rule's"), "{}", line(&s));
1354 s.gate = Gate { gain: vec![-0.1018; select::policy::DRAWS], allowance: 0.0760, ..Gate::default() };
1355 let spent = Selection::of(&s, Policy::Learned, None);
1356 assert_eq!((spent.gate, spent.allowance, spent.ledger), (0.0, 0.0760, -0.1018));
1357 assert!(line(&s).starts_with("learned, gate spent (ledger q05 -0.102 against allowance 0.076), deviations need positive evidence · "), "{}", line(&s));
1358 s.fit.key.clear();
1360 assert!(line(&s).starts_with("learned, cold start: gate 0"), "{}", line(&s));
1361 s.fit.key = "3556601-bc6d6191".into();
1363 s.gate.gain = vec![-0.05; select::policy::DRAWS];
1364 assert!(line(&s).starts_with("learned, gate 0.026 (quality it may still risk below the fixed rule)"), "{}", line(&s));
1365 let fixed = Selection::of(&s, Policy::Fixed, None);
1367 assert_eq!((fixed.allowance, fixed.ledger), (0.0, 0.0));
1368 }
1369
1370 #[test]
1373 fn a_store_says_how_its_lessons_are_selected() {
1374 use select::Policy;
1375 let home = std::env::temp_dir().join(format!("borg-top-select-{}", std::process::id()));
1376 let _ = std::fs::remove_dir_all(&home);
1377 std::fs::create_dir_all(&home).unwrap();
1378 let lessons = home.join("lessons.jsonl");
1379 std::fs::write(&lessons, "").unwrap();
1380 let line = |lessons: &Path, policy| selection_line(&selection(lessons, policy));
1381 assert!(line(&lessons, Policy::Learned).ends_with("cold start: gate 0, every decision the fixed rule's · no graded check folded yet"));
1382 assert!(line(&lessons, Policy::Fixed).starts_with("the fixed rule ("), "{}", line(&lessons, Policy::Fixed));
1383 assert!(line(&lessons, Policy::Cap(2)).contains("(BORG_STOP_ASK_CAP, evaluation only)"));
1384 let absent = home.join("absent").join("lessons.jsonl");
1385 assert!(line(&absent, Policy::Attention).contains("(BORG_STOP_ATTENTION=on, evaluation only)"), "no store: the prior");
1386 let _ = std::fs::remove_dir_all(&home);
1387 }
1388
1389 #[test]
1391 fn the_selection_line_follows_the_screen_when_shown() {
1392 let (ds, inbox) = (fixture(), inbox_fixture());
1393 let plain = render("corp", Share::Team, &ds, &inbox, NOW);
1394 assert_eq!(render_with("corp", Share::Team, &ds, &inbox, None, NOW), plain);
1395 let cold = Selection::of(&select::summary::Summary::default(), select::precedence(None, None, false), None);
1396 assert_eq!(render_with("corp", Share::Team, &ds, &inbox, Some(&cold), NOW), plain, "the default at cold start: no line");
1397 let s = learned();
1398 assert_eq!(render_with("corp", Share::Team, &ds, &inbox, Some(&s), NOW), format!("{plain} selection: {}\n", selection_line(&s)));
1399 }
1400
1401 #[test]
1403 fn the_selection_serialises_every_section_for_the_page() {
1404 let v = serde_json::to_value(learned()).unwrap();
1405 let mut keys: Vec<&str> = v.as_object().unwrap().keys().map(String::as_str).collect();
1406 keys.sort_unstable();
1407 let want = "allowance at budget checks declaration default delivery deviations effects evaluation fallbacks fit gate half_life labels latency ledger measurement policy pool sessions source spend test";
1408 assert_eq!(keys, want.split(' ').collect::<Vec<_>>());
1409 assert_eq!((&v["budget"], &v["spend"][0]["source"], &v["deviations"][0]["whole"]), (&Value::Null, &json!("stop"), &json!(400)));
1410 let mut keys: Vec<&str> = v["evaluation"].as_object().unwrap().keys().map(String::as_str).collect();
1411 keys.sort_unstable();
1412 assert_eq!(keys, ["ess", "max_weight", "mean", "sessions", "truncated", "ucb", "units"]);
1413 assert_eq!((&v["default"], &v["measurement"]), (&json!("fixed"), &json!({"placebos": 40, "judged": 31, "tail_shown": 12, "tail_applied": 3})));
1414 let mut keys: Vec<&str> = v["test"].as_object().unwrap().keys().map(String::as_str).collect();
1415 keys.sort_unstable();
1416 let want = "ask_free by_k control dilution due due_checks due_min_sessions due_sessions kept ran refused risk sessions set";
1417 assert_eq!(keys, want.split(' ').collect::<Vec<_>>());
1418 }
1419
1420 #[test]
1425 fn the_judged_share_leaves_out_what_its_session_already_had() {
1426 use crate::outcome::{self, Outcome};
1427 use select::summary::{Summary, fold};
1428 let rec = |kind: &str, check: &str, channel: &str, ms: i64| Outcome {
1429 kind: kind.into(),
1430 session: "s".into(),
1431 lessons: vec!["A".into()],
1432 check: check.into(),
1433 channel: channel.into(),
1434 ms,
1435 ..Default::default()
1436 };
1437 let graded = |check: &str, ms: i64| Outcome {
1438 grades: vec![2],
1439 ranks: vec![0],
1440 applied: 1,
1441 free: vec!["A".into()],
1442 pf: vec![1.0],
1443 policy: "fixed".into(),
1444 shown: 20,
1445 tokens: 6_000,
1446 ..rec(outcome::GRADED, check, "live", ms)
1447 };
1448 let log = [
1449 graded("c1", 10),
1450 Outcome { route: outcome::ROUTE_FREE.into(), ..rec(outcome::DELIVERY, "c1", "", 11) },
1451 Outcome { judge: 2, ..rec(outcome::CHANGED, "c1", "queued", 12) },
1452 graded("c2", 20),
1453 Outcome { route: outcome::ROUTE_DROPPED_DONE.into(), ..rec(outcome::DELIVERY, "c2", "", 21) },
1454 Outcome { lessons: vec!["B".into()], ..rec(outcome::ASKED, "", "early", 25) },
1456 Outcome { lessons: vec!["B".into()], grades: vec![3], chosen: vec!["B".into()], free: Vec::new(), p: vec![1.0], pf: vec![0.0], ..graded("c3", 30) },
1457 Outcome { lessons: vec!["B".into()], route: outcome::ROUTE_ASKED.into(), ..rec(outcome::DELIVERY, "c3", "", 31) },
1458 Outcome { lessons: vec!["B".into()], judge: 2, ..rec(outcome::CHANGED, "c3", "live", 32) },
1459 ];
1460 let text: String = log.iter().map(|o| format!("{}\n", serde_json::to_string(o).unwrap())).collect();
1461 let s = fold(&text, Summary::default());
1462 assert_eq!(s.applies.iter().map(|a| a.had).collect::<Vec<_>>(), [false, true, true], "c2's A was handed over at c1, c3's B early");
1463 let sel = Selection::of(&s, select::Policy::Fixed, None);
1464 let row = |name: &str| sel.labels.iter().find(|c| c.name == name).map(|c| (c.part, c.whole));
1465 assert_eq!(row("judged, free"), Some((1, 1)), "c1's delivered and judged; c2's had, out: {:?}", sel.labels);
1466 assert_eq!(row("judged, asked"), Some((1, 1)), "c3's question of what B's session had, judged: {:?}", sel.labels);
1467 }
1468
1469 #[test]
1473 fn the_verdict_shares_read_judge_three_answers_per_route() {
1474 use crate::outcome::{self, Outcome};
1475 use select::summary::{Summary, fold};
1476 let rec = |kind: &str, l: &str, check: &str, channel: &str, ms: i64, judge: u8| Outcome {
1477 kind: kind.into(),
1478 session: "s".into(),
1479 lessons: vec![l.into()],
1480 check: check.into(),
1481 channel: channel.into(),
1482 ms,
1483 judge,
1484 ..Default::default()
1485 };
1486 let graded = |check: &str, ms: i64, chosen: &[&str], free: &[&str], placebo: &[&str]| {
1487 let lessons: Vec<String> = chosen.iter().chain(free).chain(placebo).map(|l| l.to_string()).collect();
1488 let n = lessons.len();
1489 Outcome {
1490 grades: vec![2; n],
1491 ranks: (0..n as u16).collect(),
1492 applied: n as u16,
1493 chosen: chosen.iter().map(|l| l.to_string()).collect(),
1494 free: free.iter().map(|l| l.to_string()).collect(),
1495 placebo: placebo.iter().map(|l| l.to_string()).collect(),
1496 p: lessons.iter().map(|l| f64::from(u8::from(chosen.contains(&l.as_str())))).collect(),
1497 pf: lessons.iter().map(|l| f64::from(u8::from(free.contains(&l.as_str())))).collect(),
1498 psi: if placebo.is_empty() { 0.0 } else { 0.5 },
1499 policy: "fixed".into(),
1500 lessons,
1501 ..rec(outcome::GRADED, "", check, "stop", ms, 0)
1502 }
1503 };
1504 let deliver = |l: &str, check: &str, route: &str, ms: i64| Outcome { route: route.into(), ..rec(outcome::DELIVERY, l, check, "", ms, 0) };
1505 let log = [
1506 graded("c0", 5, &[], &["O"], &[]),
1508 deliver("O", "c0", outcome::ROUTE_FREE, 6),
1509 rec(outcome::UNCLEAR, "O", "c0", "queued", 7, 2),
1510 graded("c1", 10, &["A", "B"], &["F", "G", "H"], &["P", "Q"]),
1511 deliver("A", "c1", outcome::ROUTE_ASKED, 11),
1512 deliver("B", "c1", outcome::ROUTE_ASKED, 11),
1513 deliver("F", "c1", outcome::ROUTE_FREE, 11),
1514 deliver("G", "c1", outcome::ROUTE_FREE, 11),
1515 deliver("H", "c1", outcome::ROUTE_FREE, 11),
1516 rec(outcome::CHANGED, "A", "c1", "stop", 20, 3),
1517 rec(outcome::UNCLEAR, "B", "c1", "stop", 20, 3),
1518 rec(outcome::UNCLEAR, "F", "c1", "queued", 20, 3),
1519 Outcome { at: outcome::AT_END.into(), ..rec(outcome::NOT_ARISEN, "F", "c1", "queued", 30, 3) },
1520 rec(outcome::REMAINS, "G", "c1", "queued", 20, 3),
1521 rec(outcome::UNCLEAR, "H", "c1", "queued", 20, 3),
1522 rec(outcome::UNCLEAR, "P", "c1", outcome::PLACEBO, 20, 3),
1523 rec(outcome::IGNORED, "Q", "c1", outcome::PLACEBO, 20, 3),
1524 ];
1525 let text: String = log.iter().map(|o| format!("{}\n", serde_json::to_string(o).unwrap())).collect();
1526 let sel = Selection::of(&fold(&text, Summary::default()), select::Policy::Fixed, None);
1527 let row = |name: &str| sel.labels.iter().find(|c| c.name == name).map(|c| (c.part, c.whole));
1528 assert_eq!(row("unclear"), Some((3, 7)), "B, H and P of seven judge-3 answers, O's older one out: {:?}", sel.labels);
1529 assert_eq!(row("not arisen"), Some((1, 7)));
1530 assert_eq!(row("consequence remains"), Some((1, 7)));
1531 assert_eq!(row("unclear, asked"), Some((1, 2)));
1532 assert_eq!(row("unclear, free"), Some((1, 3)), "F's end verdict replaced its unclear: {:?}", sel.labels);
1533 assert_eq!(row("unclear, placebo"), Some((1, 2)));
1534 }
1535
1536 #[test]
1541 fn the_measurement_counts_the_placebos_the_fixed_rule_drew_and_judged() {
1542 use crate::outcome::{self, Outcome};
1543 use select::summary::{Summary, fold};
1544 let rec = |kind: &str, lessons: &[&str], check: &str, channel: &str, ms: i64| Outcome {
1545 kind: kind.into(),
1546 session: "s".into(),
1547 lessons: lessons.iter().map(|l| l.to_string()).collect(),
1548 check: check.into(),
1549 channel: channel.into(),
1550 ms,
1551 ..Default::default()
1552 };
1553 let graded = Outcome {
1554 grades: vec![1, 1, 2],
1555 ranks: vec![0, 1, 2],
1556 applied: 3,
1557 free: vec!["C".into()],
1558 placebo: vec!["A".into(), "B".into()],
1559 psi: 0.5,
1560 policy: "fixed".into(),
1561 shown: 20,
1562 tokens: 6_000,
1563 ..rec(outcome::GRADED, &["A", "B", "C"], "c1", "stop", 10)
1564 };
1565 let tailed = Outcome {
1567 grades: vec![1],
1568 ranks: vec![0],
1569 applied: 1,
1570 policy: "fixed".into(),
1571 shown: 20,
1572 tokens: 6_000,
1573 tail: vec!["T1".into(), "T2".into()],
1574 pt: 0.5,
1575 fill: vec![21, 25, 21],
1576 ..rec(outcome::GRADED, &["T2"], "c2", "stop", 14)
1577 };
1578 let log = [
1579 graded,
1580 tailed,
1581 Outcome { route: outcome::ROUTE_PLACEBO.into(), ..rec(outcome::DELIVERY, &["A"], "c1", "", 11) },
1582 Outcome { route: outcome::ROUTE_PLACEBO.into(), ..rec(outcome::DELIVERY, &["B"], "c1", "", 11) },
1583 Outcome { judge: 2, ..rec(outcome::IGNORED, &["A"], "c1", outcome::PLACEBO, 12) },
1584 Outcome { judge: 2, ..rec(outcome::CHANGED, &["C"], "c1", "queued", 13) },
1585 ];
1586 let text: String = log.iter().map(|o| format!("{}\n", serde_json::to_string(o).unwrap())).collect();
1587 let sel = Selection::of(&fold(&text, Summary::default()), select::Policy::Fixed, None);
1588 assert_eq!(sel.measurement, Some(Measurement { placebos: 2, judged: 1, tail_shown: 2, tail_applied: 1 }), "{sel:?}");
1589 assert!(selection_line(&sel).contains(" · 2 checks · placebos 2 drawn, 1 judged · tail 2 shown past the pool, 1 applied"), "{}", selection_line(&sel));
1590 assert_eq!(Selection::of(&Summary::default(), select::Policy::Fixed, None).measurement, None, "nothing folded: not measured");
1591 }
1592
1593 #[test]
1597 fn the_selection_counts_the_learned_decisions_the_fixed_rule_took_over_by_reason() {
1598 use crate::outcome::{self, Outcome};
1599 use select::summary::{Summary, fold};
1600 let graded = |check: &str, policy: &str, reason: &str, ms: i64| Outcome {
1601 kind: outcome::GRADED.into(),
1602 session: "s".into(),
1603 lessons: vec!["A".into()],
1604 grades: vec![1],
1605 ranks: vec![0],
1606 applied: 1,
1607 free: vec!["A".into()],
1608 pf: vec![1.0],
1609 channel: "live".into(),
1610 check: check.into(),
1611 policy: policy.into(),
1612 reason: reason.into(),
1613 ms,
1614 ..Default::default()
1615 };
1616 let log = [
1617 graded("c1", "learned", "", 1),
1618 graded("c2", "fixed", outcome::DECIDED_BUSY, 2),
1619 graded("c3", "fixed", outcome::DECIDED_NO_RECORD, 3),
1620 graded("c4", "fixed", outcome::DECIDED_BUSY, 4),
1621 ];
1622 let lines = |log: &[Outcome]| log.iter().map(|o| format!("{}\n", serde_json::to_string(o).unwrap())).collect::<String>();
1623 let sel = Selection::of(&fold(&lines(&log), Summary::default()), select::Policy::Learned, None);
1624 let rows: Vec<(&str, u64, u64)> = sel.fallbacks.iter().map(|c| (c.name.as_str(), c.part, c.whole)).collect();
1625 assert_eq!(rows, [("lock_busy", 2, 4), ("lock_failed", 0, 4), ("unrecorded", 1, 4)]);
1626 let line = selection_line(&sel);
1627 assert!(line.contains(" · 4 checks · learned decisions the fixed rule took over: lock_busy 2, unrecorded 1 · "), "{line}");
1628 let none = Selection::of(&fold(&lines(&log[..1]), Summary::default()), select::Policy::Learned, None);
1629 assert!(none.fallbacks.iter().all(|c| c.part == 0) && none.fallbacks.len() == 3, "{none:?}");
1630 assert!(!selection_line(&none).contains("took over"), "{}", selection_line(&none));
1631 assert!(Selection::of(&Summary::default(), select::Policy::Learned, None).fallbacks.is_empty(), "nothing folded: not measured");
1632 }
1633
1634 #[test]
1639 fn the_selection_fills_latency_evaluation_and_delivery_from_the_summary() {
1640 use select::summary::{Summary, fold};
1641 let call = |harness: &str, channel: &str, dur_ms: u64, failed: bool| {
1642 let o = crate::outcome::Outcome {
1643 kind: "graded".into(),
1644 session: "s".into(),
1645 channel: channel.into(),
1646 harness: harness.into(),
1647 dur_ms,
1648 failed,
1649 tokens: 6_000,
1650 ..Default::default()
1651 };
1652 format!("{}\n", serde_json::to_string(&o).unwrap())
1653 };
1654 let mut log: String = (1..=20).map(|k| call("codex", "stop", k * 1_000, k == 20)).collect();
1655 log += &call("claude", "stop", 4_000, false);
1656 log += &call("codex", "live", 99_000, false);
1657 log += &call("nonesuch", "stop", 5_000, false);
1658 let mut s = fold(&log, Summary::default());
1659 s.monitor = select::policy::Monitor {
1660 sessions: 12,
1661 units: 400,
1662 mean: 0.004,
1663 ucb: Some(0.031),
1664 labelled: 300,
1665 ess: 180.0,
1666 max_weight: 6.5,
1667 truncated: 3,
1668 ..Default::default()
1669 };
1670 s.fit.key = "fit".into();
1671 s.fit.rates = select::policy::Rates { free_stop: 0.6, free_live: 0.8, later: 0.4, wait: 0.25, ask_live: 1.0 };
1672 let sel = Selection::of(&s, select::Policy::Learned, None);
1673 let budget = |h: &str| crate::harness::ALL.iter().find(|x| x.name() == h).and_then(|x| x.caps().stop_budget).map(|b| b.as_millis() as u64);
1674 assert!(budget("codex").is_some() && budget("claude").is_none(), "codex names a stop budget, claude none");
1675 let want = vec![
1676 Latency { harness: "claude".into(), checks: 1, p50_ms: 4_000, p95_ms: 4_000, budget_ms: None, over: 0 },
1677 Latency { harness: "codex".into(), checks: 20, p50_ms: 10_000, p95_ms: 19_000, budget_ms: budget("codex"), over: 1 },
1678 ];
1679 assert_eq!(sel.latency, want);
1680 let e = sel.evaluation.as_ref().expect("the monitor weighed labelled applies");
1681 assert_eq!(*e, Evaluation { sessions: 12, mean: 0.004, ucb: Some(0.031), units: 300, ess: 180.0, max_weight: 6.5, truncated: 3 });
1682 assert_eq!(sel.delivery.as_ref().map(|r| r.wait), Some(0.25));
1683 let keep = select::summary::LATENCY_KEEP as u64;
1685 let many: String = (1..=keep + 1).map(|k| call("claude", "stop", k, false)).collect();
1686 let kept = Selection::of(&fold(&many, Summary::default()), select::Policy::Learned, None);
1687 assert_eq!((kept.latency[0].checks, kept.latency[0].p50_ms), (keep, 1 + keep / 2), "the oldest call dropped");
1688 let cold = Selection::of(&Summary::default(), select::Policy::Learned, None);
1689 assert!(cold.latency.is_empty() && cold.evaluation.is_none() && cold.delivery.is_none(), "{cold:?}");
1690 assert_eq!((quantile(&[], 0.95), quantile(&[7], 0.5)), (0, 7));
1691 }
1692
1693 fn tested(check: &str, session: &str, ms: i64, source: &str, xk: Option<u32>, xs: Option<u32>) -> crate::outcome::Outcome {
1696 crate::outcome::Outcome {
1697 kind: crate::outcome::GRADED.into(),
1698 session: session.into(),
1699 lessons: vec!["A".into(), "B".into()],
1700 grades: vec![2, 1],
1701 ranks: vec![0, 1],
1702 applied: 2,
1703 check: check.into(),
1704 channel: "live".into(),
1705 policy: "learned".into(),
1706 source: source.into(),
1707 xk,
1708 xs,
1709 ms,
1710 ..Default::default()
1711 }
1712 }
1713
1714 #[test]
1720 fn the_test_section_is_the_summarys_fold_of_its_checks() {
1721 use select::policy::{ALPHA, EVIDENCE, EXPERIMENT, EXPERIMENT_CONTROL, GATE};
1722 use select::summary::{Summary, fold};
1723 let log = [
1724 tested("c1", "s1", 1, EXPERIMENT, Some(2), Some(1)),
1725 tested("c2", "s1", 2, EXPERIMENT_CONTROL, Some(2), Some(0)),
1726 tested("c3", "s2", 3, EXPERIMENT, Some(2), Some(1)),
1727 tested("c4", "s2", 4, GATE, Some(2), Some(1)),
1728 tested("c5", "s2", 5, GATE, Some(1), Some(0)),
1729 tested("c6", "s3", 6, EVIDENCE, None, None),
1730 tested("c7", "s3", 7, GATE, None, None),
1731 crate::outcome::Outcome { policy: "fixed".into(), ..tested("c8", "s3", 8, EXPERIMENT, Some(2), Some(2)) },
1732 ];
1733 let text: String = log.iter().map(|o| format!("{}\n", serde_json::to_string(o).unwrap())).collect();
1734 let s = fold(&text, Summary::default());
1735 let t = Selection::of(&s, select::Policy::Learned, Some(0.2)).test.expect("the learned policy shows its test");
1736 let draws = |k: u32, draws: Vec<u64>, refused: u64| Draws { k, draws, refused };
1737 assert_eq!((t.ran, t.control, t.refused, t.sessions), (5, 1, 2, 2), "{t:?}");
1738 assert_eq!(t.by_k, vec![draws(1, vec![1, 0], 1), draws(2, vec![1, 3, 0], 1)]);
1739 assert_eq!((t.risk, t.set), (0.2, true), "the collective's risk");
1740 assert!(!t.due && t.dilution.is_empty() && t.ask_free.is_none(), "5 checks, 2 sessions: not due, nothing estimated");
1741 assert_eq!((t.due_checks, t.due_sessions), (select::replay::TEST_CHECKS, u64::from(select::replay::TEST_SESSIONS)));
1742 let line = selection_line(&Selection::of(&s, select::Policy::Learned, Some(0.2)));
1743 assert!(
1744 line.contains(" · question-count test 5 drawn (1 control, 2 refused by the gate) in 2 sessions, risk 0.20 (the collective's); readout after 300 checks in 15 sessions, or 30 sessions · "),
1745 "{line}"
1746 );
1747 assert_eq!(t.kept, t.ran, "every drawn check still held: {t:?}");
1748 let mut compacted = s.clone();
1750 compacted.checks.remove(0);
1751 let c = Selection::of(&compacted, select::Policy::Learned, Some(0.2)).test.unwrap();
1752 assert_eq!((c.ran, c.kept), (5, 4), "{c:?}");
1753 assert!(
1754 test_line(&c).starts_with("question-count test 5 drawn (1 control, 2 refused by the gate, 4 still in the summary) in 2 sessions"),
1755 "{}",
1756 test_line(&c)
1757 );
1758 let default = Selection::of(&s, select::Policy::Learned, None).test.unwrap();
1759 assert_eq!((default.risk, default.set), (ALPHA, false), "unset: the policy's ALPHA");
1760 let cold = Selection::of(&Summary::default(), select::Policy::Learned, Some(0.0));
1762 assert_eq!(cold.test.as_ref().map(|t| (t.ran, t.risk)), Some((0, 0.0)));
1763 assert_eq!(test_line(cold.test.as_ref().unwrap()), "question-count test off (risk 0)");
1764 let none = Selection::of(&Summary::default(), select::Policy::Learned, None).test.unwrap();
1765 assert_eq!(test_line(&none), "question-count test: no check yet, risk 0.10");
1766 assert!(Selection::of(&Summary::default(), select::Policy::Fixed, None).test.is_none());
1768 assert_eq!(Selection::of(&s, select::Policy::Fixed, None).test.map(|t| t.ran), Some(5), "an earlier opt-in's checks");
1769 }
1770
1771 #[test]
1775 fn the_gate_shown_is_the_one_in_force_at_the_collectives_risk() {
1776 use select::summary::{Gate, Summary};
1777 let s = Summary { gate: Gate { gain: vec![0.0; select::policy::DRAWS], allowance: 0.3, ..Gate::default() }, ..Summary::default() };
1778 let gate = |risk: Option<f64>| Selection::of(&s, select::Policy::Learned, risk).gate;
1779 assert!((gate(None) - 0.3).abs() < 1e-12, "{}", gate(None));
1780 assert!((gate(Some(0.2)) - 0.6).abs() < 1e-12, "twice ALPHA: twice the allowance, {}", gate(Some(0.2)));
1781 assert_eq!(gate(Some(0.0)), 0.0, "risk 0");
1782 }
1783
1784 #[test]
1787 fn the_test_readout_shows_the_replays_estimates_once_due() {
1788 use crate::outcome::{self, Outcome};
1789 use select::policy::EXPERIMENT;
1790 use select::summary::{Summary, fold};
1791 let sessions = u64::from(select::replay::TEST_SESSIONS);
1792 let mut text = String::new();
1793 let mut line = |o: &Outcome| text.push_str(&format!("{}\n", serde_json::to_string(o).unwrap()));
1794 for i in 0..sessions {
1795 let (session, check, ms) = (format!("s{i}"), format!("{i:012x}"), 10 * i as i64);
1796 let asked = i % 2 == 0;
1798 let g = Outcome {
1799 lessons: vec!["A".into()],
1800 grades: vec![2],
1801 ranks: vec![0],
1802 applied: 1,
1803 chosen: if asked { vec!["A".into()] } else { Vec::new() },
1804 free: if asked { Vec::new() } else { vec!["A".into()] },
1805 p: vec![0.5],
1806 pf: vec![0.5],
1807 ..tested(&check, &session, ms, EXPERIMENT, Some(1), Some(u32::from(asked)))
1808 };
1809 line(&g);
1810 let rec = |kind: &str, ms: i64| Outcome {
1811 kind: kind.into(),
1812 session: session.clone(),
1813 lessons: vec!["A".into()],
1814 check: check.clone(),
1815 ms,
1816 ..Default::default()
1817 };
1818 let route = if asked { outcome::ROUTE_ASKED } else { outcome::ROUTE_FREE };
1819 line(&Outcome { route: route.into(), ..rec(outcome::DELIVERY, ms + 1) });
1820 let verdict = if asked { outcome::CHANGED } else { outcome::IGNORED };
1822 line(&Outcome { judge: 2, channel: if asked { "live" } else { "queued" }.into(), ..rec(verdict, ms + 2) });
1823 }
1824 let s = fold(&text, Summary::default());
1825 let t = Selection::of(&s, select::Policy::Learned, None).test.unwrap();
1826 assert!(t.due && t.sessions == sessions, "{t:?}");
1827 let r = select::replay::test_readout(&s);
1828 let want = r.ask_free.as_ref().and_then(|x| x.value).expect("judged 2s on both routes");
1829 let e = t.ask_free.as_ref().expect("estimated once due");
1830 assert_eq!((e.mean, e.unit.as_str()), (100.0 * want, "points changed"));
1831 assert!(e.mean > 0.0, "asking changed the work, handing over did not: {e:?}");
1832 assert!(t.dilution.is_empty() && r.by_asked.is_empty(), "{t:?}");
1834 let line = selection_line(&Selection::of(&s, select::Policy::Learned, None));
1835 assert!(line.contains(&format!("; readout: grade 2 asked − handed over {:+.1} [", e.mean)), "{line}");
1836 }
1837
1838 #[test]
1841 fn unread_counts_follow_the_shown_once_claims() {
1842 let home = std::env::temp_dir().join(format!("borg-top-inbox-{}", std::process::id()));
1843 let _ = std::fs::remove_dir_all(&home);
1844 std::fs::create_dir_all(&home).unwrap();
1845 let ds = fixture();
1846 assert!(inbox(&home, &ds, NOW).recent.is_empty());
1847 let here = msg::node(&home);
1848 msg::send(&home, message_on(&here, "cccc3333-x", "Three of Three", "aaaa1111-x", msg::TELL, NOW - 3_000, "look at recon"), NOW).unwrap();
1849 msg::send(&home, message_on(&here, "cccc3333-x", "Three of Three", msg::ALL, msg::TELL, NOW - 2_000, "lunch"), NOW).unwrap();
1850 let i = inbox(&home, &ds, NOW);
1851 assert_eq!(i.unread.get("aaaa1111-x"), Some(&2), "{i:?}");
1852 assert_eq!(i.unread.get("cccc3333-x"), Some(&0), "its own broadcast");
1853 assert!(!i.unread.contains_key(&ds[1].p.session), "remote: claimed on its node");
1854 assert_eq!((i.day, i.recent[0].text.as_str()), (2, "lunch"));
1855 let caps = crate::harness::by_name("claude").expect("a harness").caps();
1856 let ev = json!({"session_id": "aaaa1111-x"});
1857 assert!(msg::deliver(&home, caps, "prompt", &ev, Some(100), NOW, None).is_some());
1858 assert_eq!(inbox(&home, &ds, NOW).unread.get("aaaa1111-x"), Some(&0));
1859 let _ = std::fs::remove_dir_all(&home);
1860 }
1861
1862 fn seen_lines(home: &Path) -> Vec<(String, Vec<String>)> {
1864 msg::lines(home)
1865 .into_iter()
1866 .filter_map(|l| match l {
1867 msg::Line::Seen(s) => Some((s.by, s.ids)),
1868 msg::Line::Message(_) => None,
1869 })
1870 .collect()
1871 }
1872
1873 fn shell(sessions: Vec<(String, &'static str)>, pids: Vec<(u32, &'static str)>) -> impl Fn(&[Drone]) -> Result<Option<(String, &'static str)>, String> {
1876 move |ds| msg::pick(sessions.clone(), &pids, ds)
1877 }
1878
1879 fn acked_fixture(base: &Path) -> (std::path::PathBuf, String, Message) {
1882 let _ = std::fs::remove_dir_all(base);
1883 let (home, work) = (base.join("store"), base.join("work"));
1884 std::fs::create_dir_all(&home).unwrap();
1885 std::fs::create_dir_all(&work).unwrap();
1886 let git = |a: &[&str]| std::process::Command::new("git").arg("-C").arg(&work).args(a).output().is_ok_and(|o| o.status.success());
1887 assert!(git(&["init", "-q"]) && git(&["config", "user.email", "ann@x"]));
1888 let here = msg::node(&home);
1889 let to_me = message_on(&here, "aaaa1111-x", "One of Three", msg::ENGINEER, msg::TELL, NOW - 3_000, "the build is green");
1890 let to_drone = message_on(&here, "cccc3333-x", "Three of Three", "aaaa1111-x", msg::TELL, NOW - 2_000, "look at recon");
1891 msg::send(&home, to_me.clone(), NOW).unwrap();
1892 msg::send(&home, to_drone, NOW).unwrap();
1893 (home, work.display().to_string(), to_me)
1894 }
1895
1896 #[test]
1900 fn the_screen_acknowledges_the_messages_to_the_engineer_it_prints() {
1901 let base = std::env::temp_dir().join(format!("borg-top-ack-{}", std::process::id()));
1902 let (home, cwd, to_me) = acked_fixture(&base);
1903 let routes = [Route::Collective { name: "corp".into(), home: home.clone(), share: Share::Local }];
1904 let terminal = || shell(Vec::new(), Vec::new());
1905 let json = screen_of(&routes, true, &cwd, terminal(), true, NOW);
1906 assert!(json.contains("the build is green") && seen_lines(&home).is_empty(), "--json acknowledges nothing: {json}");
1907 let text = screen_of(&routes, true, &cwd, terminal(), false, NOW);
1908 assert!(text.contains("the build is green") && text.contains("look at recon"), "{text}");
1909 assert_eq!(seen_lines(&home), [("engineer:ann@x".to_string(), vec![to_me.id.clone()])], "the engineer's message only, as theirs");
1910 screen_of(&routes, true, &cwd, terminal(), false, NOW + 1_000);
1911 assert_eq!(seen_lines(&home).len(), 1, "acknowledged once: a later look writes no second line");
1912 let _ = std::fs::remove_dir_all(&base);
1913 }
1914
1915 #[test]
1920 fn a_session_running_the_screen_acknowledges_nothing_as_the_engineer() {
1921 let base = std::env::temp_dir().join(format!("borg-top-ack-session-{}", std::process::id()));
1922 let (home, cwd, to_me) = acked_fixture(&base);
1923 let other = base.join("other");
1924 std::fs::create_dir_all(&other).unwrap();
1925 let pid = std::process::id();
1926 let oc = Presence { harness: "opencode".into(), pid: Some(pid), ..p("dddd4444-x", "ann@x", NOW - 9_000, NOW - 1_000, "prompt", "docs", &[]) };
1927 presence::write(&other, &oc).unwrap();
1928 let routes = [
1929 Route::Collective { name: "corp".into(), home: home.clone(), share: Share::Local },
1930 Route::Collective { name: "side".into(), home: other.clone(), share: Share::Local },
1931 ];
1932 let shells = [
1933 ("Claude Code", shell(vec![("eeee5555-x".into(), "claude")], Vec::new())),
1934 ("Codex in Claude Code, neither with presence", shell(vec![("eeee5555-x".into(), "claude"), ("ffff6666-x".into(), "codex")], Vec::new())),
1935 ("opencode, its drone in the other collective", shell(Vec::new(), vec![(pid, "opencode")])),
1936 ];
1937 for (what, caller) in shells {
1938 let text = screen_of(&routes, false, &cwd, caller, false, NOW);
1939 assert!(text.contains("the build is green"), "{what}: shown {text}");
1940 assert!(seen_lines(&home).is_empty() && seen_lines(&other).is_empty(), "{what}: acknowledged nothing as the engineer");
1941 }
1942 screen_of(&routes, false, &cwd, shell(Vec::new(), vec![(pid + 1, "opencode")]), false, NOW);
1943 assert_eq!(seen_lines(&home), [("engineer:ann@x".to_string(), vec![to_me.id.clone()])], "a terminal (no drone of its pid): the engineer's");
1944 let _ = std::fs::remove_dir_all(&base);
1945 }
1946
1947 #[test]
1952 fn the_printed_screen_acknowledges_only_the_messages_whose_text_it_prints() {
1953 let base = std::env::temp_dir().join(format!("borg-top-ack-cut-{}", std::process::id()));
1954 let (home, cwd, to_me) = acked_fixture(&base);
1955 let email = format!("{}@x", "a".repeat(58));
1957 assert!(std::process::Command::new("git").args(["-C", &cwd, "config", "user.email", &email]).output().is_ok_and(|o| o.status.success()));
1958 let (here, to) = (msg::node(&home), format!("{}:{email}", msg::ENGINEER));
1959 let one = message_on(&here, "bbbb2222-x", &"n".repeat(52), &to, msg::TELL, NOW - 1_500, "one character shows");
1960 let none = message_on(&here, "dddd4444-x", &"n".repeat(53), &to, msg::TELL, NOW - 1_000, "none shows");
1961 msg::send(&home, one.clone(), NOW).unwrap();
1962 msg::send(&home, none.clone(), NOW).unwrap();
1963 let routes = [Route::Collective { name: "corp".into(), home: home.clone(), share: Share::Local }];
1964 let text = screen_of(&routes, true, &cwd, shell(Vec::new(), Vec::new()), false, NOW);
1965 assert!(text.contains(&format!("{} -> {email}: o…", one.name)) && text.contains(&format!("{} -> {email}: …", none.name)), "{text}");
1966 let mut acked: Vec<String> = seen_lines(&home).into_iter().flat_map(|(by, ids)| ids.into_iter().map(move |id| format!("{by} {id}"))).collect();
1967 acked.sort();
1968 let mut want = [format!("{to} {}", to_me.id), format!("{to} {}", one.id)];
1969 want.sort();
1970 assert_eq!(acked, want, "the line that shows one character of its text is read; the one cut before it is not");
1971 let _ = std::fs::remove_dir_all(&base);
1972 }
1973
1974 #[test]
1978 fn a_live_screen_acknowledges_each_message_once() {
1979 let base = std::env::temp_dir().join(format!("borg-top-ack-once-{}", std::process::id()));
1980 let _ = std::fs::remove_dir_all(&base);
1981 std::fs::create_dir_all(&base).unwrap();
1982 let home = base.clone();
1983 let here = msg::node(&home);
1984 let to_me = |by: &str, ms: i64, text: &str| message_on(&here, by, "a drone", msg::ENGINEER, msg::TELL, ms, text);
1985 let first = to_me("aaaa1111-x", NOW - 3_000, "the build is green");
1986 msg::send(&home, first.clone(), NOW).unwrap();
1987 let cwd = base.display().to_string();
1988 let mut done = HashSet::new();
1989 let recent = |now: i64| inbox(&home, &[], now).recent;
1990 acknowledge(&home, &cwd, &recent(NOW).iter().collect::<Vec<_>>(), &mut done, NOW);
1991 assert_eq!(seen_lines(&home).len(), 1, "{:?}", seen_lines(&home));
1992 assert!(done.contains(&first.id));
1993 let kept: String = std::fs::read_to_string(msg::path(&home)).unwrap().lines().filter(|l| !l.contains("\"seen\"")).map(|l| format!("{l}\n")).collect();
1996 std::fs::write(msg::path(&home), kept).unwrap();
1997 acknowledge(&home, &cwd, &recent(NOW + 1_000).iter().collect::<Vec<_>>(), &mut done, NOW + 1_000);
1998 assert!(seen_lines(&home).is_empty(), "skipped: no second ack of a done id");
1999 let second = to_me("cccc3333-x", NOW + 2_000, "and the docs");
2001 msg::send(&home, second.clone(), NOW + 2_000).unwrap();
2002 acknowledge(&home, &cwd, &recent(NOW + 3_000).iter().collect::<Vec<_>>(), &mut done, NOW + 3_000);
2003 assert_eq!(seen_lines(&home).into_iter().map(|(_, ids)| ids).collect::<Vec<_>>(), [vec![second.id.clone()]], "only the new one");
2004 let _ = std::fs::remove_dir_all(&base);
2005 }
2006
2007 #[test]
2010 fn a_sender_is_tied_to_the_node_its_line_names() {
2011 let ds = fixture();
2012 let own = message("aaaa1111-x", "anything", msg::ALL, msg::TELL, NOW, "hi");
2013 assert_eq!(sender(&ds, "n1", &own), "One of Three", "this node's drone");
2014 let forged = message_on("evil0123456789", "aaaa1111-x", "One of Three", msg::ALL, msg::TELL, NOW, "hi");
2015 assert_eq!(sender(&ds, "n1", &forged), "One of Three @evil01234567", "the claim, with the node it came from");
2016 let remote = message_on(&"beefcafe".repeat(8), &ds[1].p.session, "x", msg::ALL, msg::TELL, NOW, "hi");
2017 assert_eq!(sender(&ds, "n1", &remote), "Two of Three @beefcafebeef", "the remote drone on its own node");
2018 assert_eq!(recipient(&ds, "n1", "aaaa1111-x@n1"), "One of Three");
2019 assert_eq!(recipient(&ds, "n1", "aaaa1111-x@evil"), "aaaa1111", "not the local drone of that id");
2020 assert_eq!((recipient(&ds, "n1", "engineer"), recipient(&ds, "n1", msg::ALL)), ("engineer".into(), "collective".into()), "an `all` line");
2021 assert_eq!(recipient(&ds, "n1", "local@n1"), "local");
2022 assert_eq!(recipient(&ds, "n1", "local@beefcafebeefcafe00"), "local @beefcafebeef");
2023 }
2024
2025 #[test]
2029 fn the_line_on_borg_itself_names_a_release_and_the_protocols_that_differ() {
2030 let home = std::env::temp_dir().join(format!("borg-top-version-{}", std::process::id()));
2031 let _ = std::fs::remove_dir_all(&home);
2032 std::fs::create_dir_all(&home).unwrap();
2033 let newer = update::Checked { checked_ms: 1, latest: Some("99.0.0".into()), error: None };
2034 let current = update::Checked { latest: Some(update::VERSION.into()), ..newer.clone() };
2035 assert_eq!(version_line([home.as_path()], None, true), None);
2036 assert_eq!(version_line([home.as_path()], Some(¤t), true), None);
2037 let release = format!("borg 99.0.0 is released, this is {}: borg update", update::VERSION);
2038 assert_eq!(version_line([home.as_path()], Some(&newer), true), Some(release.clone()));
2039 assert_eq!(version_line([home.as_path()], Some(&newer), false), None, "BORG_UPDATE_CHECK=off");
2040
2041 let ours = protocol::current();
2042 let v = |protocol, engineer: Option<&str>| protocol::PeerVersion { borg: None, protocol, ms: 1, engineer: engineer.map(str::to_string) };
2043 protocol::note_version(&home, "k-same", &v(ours, Some("ann@x"))).unwrap();
2044 assert_eq!(version_line([home.as_path()], None, true), None, "a member on this node's protocol is no news");
2045 protocol::note_version(&home, "k-old", &v(ours - 1, Some("bob@x"))).unwrap();
2046 let mismatch = protocol::Mismatch { theirs: format!("borg/mesh/{}", ours + 1), ours: format!("borg/mesh/{ours}"), ms: 2 };
2047 protocol::note_mismatch(&home, "k-new", mismatch).unwrap();
2048 let line = version_line([home.as_path()], Some(&newer), true).unwrap();
2049 let old = format!("bob@x on /{} misses /{ours}: {}", ours - 1, protocol::note(ours).unwrap());
2050 let none = format!("nothing syncs with peer k-new (borg/mesh/{}, this node borg/mesh/{ours})", ours + 1);
2051 assert_eq!(line, format!("{release} · {none} · {old}"));
2052 let _ = std::fs::remove_dir_all(&home);
2053 }
2054
2055 #[test]
2056 fn json_for_scripts() {
2057 let v = to_json_with("corp", Share::Team, &fixture(), &inbox_fixture());
2058 assert_eq!((v["drones"][0]["unread"].as_u64(), v["drones"][1].get("unread")), (Some(2), None));
2059 assert_eq!((v["messages"]["day"].as_u64(), v["messages"]["recent"][0]["kind"].as_str()), (Some(3), Some("ask")));
2060 assert_eq!(v["collective"], "corp");
2061 let d = &v["drones"];
2062 assert_eq!(d.as_array().unwrap().len(), 3);
2063 assert_eq!(d[0]["designation"], "One of Three");
2064 assert_eq!(d[0]["session"], "aaaa1111-x");
2065 assert_eq!(d[0]["state"], "working");
2066 assert_eq!(d[0]["paused"], true);
2067 assert_eq!(d[1]["node"], "beefcafe".repeat(8));
2068 assert_eq!(d[1]["state"], "waiting");
2069 assert_eq!(d[2]["state"], "idle");
2070 assert_eq!(d[2]["files"], json!([]));
2071 }
2072
2073 #[test]
2075 fn a_name_matching_here_and_on_a_member_names_the_drone_here() {
2076 let local = vec![p("aaaa1111-x", "alice@x", 100, NOW - 5_000, "post_tool", "a", &[])];
2077 let remote = ["aaaa1111-x", "aaaa2222-z"].map(|s| presence::Remote {
2078 p: p(s, "bob@y", 50, NOW - 5_000, "post_tool", "b", &[]),
2079 key: "beefcafe".repeat(8),
2080 sig: String::new(),
2081 });
2082 let ds = assemble(local, remote.into(), NOW, |_| true, |_| false);
2083 for t in ["aaaa", "aaaa1111", "aaaa1111-x"] {
2084 let d = resolve(&ds, t).unwrap().unwrap();
2085 assert!(d.node.is_empty() && d.p.session == "aaaa1111-x", "{t}");
2086 }
2087 assert_eq!(resolve(&ds, "aaaa2").unwrap().unwrap().node, "beefcafe".repeat(8), "only a member's matches");
2088 assert!(!resolve(&ds, "One").unwrap().unwrap().node.is_empty(), "a number names one drone of all, wherever it runs");
2089 }
2090
2091 #[test]
2093 fn a_name_matching_an_ended_drone_here_and_a_live_member_names_the_live_one() {
2094 let local = vec![p("aaaa1111-x", "alice@x", 100, NOW - 5_000, "end", "a", &[])];
2095 let remote =
2096 vec![presence::Remote { p: p("aaaa2222-z", "bob@y", 50, NOW - 5_000, "post_tool", "b", &[]), key: "beefcafe".repeat(8), sig: String::new() }];
2097 let ds = assemble(local, remote, NOW, |_| true, |_| false);
2098 let d = resolve(&ds, "aaaa").unwrap().unwrap();
2099 assert_eq!((d.p.session.as_str(), d.node.as_str()), ("aaaa2222-z", "beefcafe".repeat(8).as_str()), "the live one, though remote");
2100 assert!(resolve(&ds, "aaaa1111").unwrap().unwrap().node.is_empty(), "an ended drone is still named by its own id");
2101 }
2102
2103 #[test]
2104 fn resolves_drones_by_designation_or_session() {
2105 let ds = fixture();
2106 assert_eq!(resolve(&ds, "Two").unwrap().unwrap().p.session, "bbbb2222-y");
2107 assert_eq!(resolve(&ds, "two of three").unwrap().unwrap().p.session, "bbbb2222-y");
2108 assert_eq!(resolve(&ds, "3").unwrap().unwrap().p.session, "cccc3333-x");
2109 assert_eq!(resolve(&ds, "aaaa").unwrap().unwrap().p.session, "aaaa1111-x");
2110 assert!(resolve(&ds, "all").unwrap().is_none());
2111 assert!(resolve(&ds, "Four").is_err());
2112 assert!(resolve(&ds, "zzzz").is_err());
2113 assert!(resolve(&ds, "aa").is_err(), "too short a prefix");
2114 assert!(["collective", "local"].iter().all(|w| resolve(&ds, w).unwrap().is_none()));
2115 assert_eq!(resolve(&ds, "bbbb").unwrap().unwrap().p.session, "bbbb2222-y", "nothing here matches: the member's");
2116 assert_eq!(word(21), "21");
2117 }
2118}