Skip to main content

borg/
top.rs

1//! `borg top [--dir DIR] [--once] [--json]`: the drones of each collective, one screen.
2//!
3//! Local drones come from presence records (presence.rs), remote ones from what the mesh daemon
4//! last heard from members (only for a collective that may sync). Drones are designated by start
5//! order across both, Borg style: "Two of Five". `--dir` shows only the collective DIR routes to;
6//! otherwise every collective on this machine that has drones. On a terminal it is a live screen until
7//! `q` (top_live.rs); piped, or with `--once`, it prints the text once; `--json` prints the same data
8//! for scripts.
9//!
10//! Messages (msg.rs): each local drone's unread count (what its next prompt or tool call shows), and
11//! the collective's last few messages, one line each. A remote drone's reads are claimed on its node.
12//! A sender is named by what its line claims, with the node the mesh checked it against (`sender`).
13//! The messages to the engineer the screen shows some of the text of are acknowledged as theirs when no
14//! session runs it, as `borg inbox` acknowledges what it lists from a terminal (`engineer_looks`,
15//! `acknowledge`): a drone's `borg top`, printed or live, acknowledges nothing; `--json` acknowledges
16//! nothing.
17//!
18//! Lesson selection (select.rs): one line per collective once its summary has folded a graded check,
19//! or while a policy other than the default (a switch) or the monitor's revert decides (`Selection`);
20//! `--json` carries it in full as `select`. `borg status` prints the same line, borg web the rest.
21//!
22//! borg itself (`version_line`): one line over the collectives when a newer release exists (update.rs)
23//! or a member's mesh protocol differs from this node's (protocol.rs), none otherwise. `borg status`
24//! and `borg doctor` say the rest; `--json` leaves it out.
25
26use 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    /// "Two of Five".
41    pub designation: String,
42    /// 1-based start order.
43    pub number: usize,
44    /// The member key of the node it runs on; empty for this machine.
45    pub node: String,
46    pub state: State,
47    /// Paused by a steering directive (local drones only).
48    pub paused: bool,
49    #[serde(flatten)]
50    pub p: Presence,
51}
52
53/// The messages side of a collective's screen.
54#[derive(Debug, Clone, Default, Serialize)]
55pub struct Inbox {
56    /// Per local drone's session, the messages not shown to it yet.
57    pub unread: BTreeMap<String, usize>,
58    /// The newest `RECENT` messages, newest first.
59    pub recent: Vec<Message>,
60    /// The messages sent in the last day.
61    pub day: usize,
62    /// This node as messages name it (msg.rs `node`).
63    #[serde(skip)]
64    pub here: String,
65    /// Per local drone's session that listens for wakes (listen.rs), whether its waiter runs.
66    pub listen: BTreeMap<String, crate::listen::Status>,
67}
68
69/// Display: how many messages the screen lists.
70pub 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
99/// The drones of the collective stored at `home` (pure over its inputs, for tests).
100pub 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    // A remote pid means nothing here: its state comes from its events and age only.
110    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
122/// The drones of the collective stored at `home` as of `now`; remote ones only if it may sync.
123pub 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
128/// The messages of the store at `home` for its `drones` as of `now` (one read of the log, one
129/// directory listing per local drone that has a message).
130pub 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
145/// Acknowledges the messages to the engineer among `shown`, the ones a screen of the store at `home`
146/// put in front of them, as the engineer of the repo at `cwd` (msg.rs `ack_engineer`), as `borg inbox`
147/// does for what it lists. `done` holds the ids an earlier frame of the same screen already passed on,
148/// so a live screen asks git for the engineer and reads the log's seen lines once per new message to
149/// an engineer, never on every frame; the ids are recorded whether the write worked or not (best
150/// effort, as the inbox's: unacknowledged, a message holds its sender's window until it expires).
151/// Only for a screen the engineer looks at (`engineer_looks`); `--json` is for scripts, not the
152/// engineer's eyes, and acknowledges nothing.
153pub 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
163/// Whether the engineer is the one looking at a screen of the collectives whose drones are `drones`:
164/// `caller` (msg.rs `caller`, the session whose shell runs this command) finds no session over any of
165/// them, as `borg inbox` acknowledges as the engineer only when it finds none. A harness runs a drone's
166/// commands piped, or on a pseudo-terminal where a plain `borg top` is the live screen (top_live.rs),
167/// and in the engineer's repo, so without this test a drone's `borg top` would acknowledge as the
168/// engineer what only its model saw: its senders' windows to the engineer opened,
169/// the messages read on every node. A shell naming sessions `caller` cannot tell apart is a session's
170/// too. Every collective's drones count, not only those of the one acknowledged in: opencode's shell
171/// names its session only through the drone of its pid, which may be another collective's.
172pub 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
176/// The directory whose git identity names the engineer looking at the screen: `dir` made absolute, else
177/// the current directory (as `borg inbox` takes it).
178pub(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
183/// Per local drone's session that listens for wakes, whether its waiter runs (one stat for any other).
184fn 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
192/// A drone's wake as the screen shows it: `on` (it listens, the next stop starts its waiter), `waiter`
193/// (its waiter runs), `-` (it is never woken, or this node cannot tell).
194pub fn wake_cell(inbox: &Inbox, d: &Drone) -> &'static str {
195    inbox.listen.get(&d.p.session).map_or("-", |s| s.name())
196}
197
198/// Per live local drone's session, how many of the already-read `all` messages were not shown to it
199/// (`here`: this node, msg.rs `node`).
200pub 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
208/// The drone of `drones` that is `session` on `node` (`here`: this node).
209fn 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
213/// Who sent `m`, as the screen says it: the drone its (node, `by`) pair is, else its label (msg.rs
214/// `sender_label`: the name it was sent under, else the start of `by`); the engineer by email. Every
215/// part but the node is the sender's claim, so a line from another node than `here` carries that node's
216/// short key (as msg.rs `line`).
217pub 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
229/// Whom `to` names, as the screen says it: `local` (this machine's sessions), `collective` (every
230/// member's), the engineer, the drone it is, else the start of the session id (msg.rs `short_session`).
231/// Another node's `local` (it never travels, msg.rs `stays_here`) is named with that node's short key
232/// (msg.rs `short_node`).
233pub 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
251/// One message as one screen line (without its age): `sender -> recipient: text` (msg.rs keeps the
252/// text on one line, its whitespace collapsed).
253pub 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
258/// A drone's unread count as the screen shows it: `-` where this node cannot tell (a remote or ended one).
259pub fn unread_cell(inbox: &Inbox, d: &Drone) -> String {
260    inbox.unread.get(&d.p.session).map_or_else(|| "-".into(), usize::to_string)
261}
262
263/// Which drone `target` names: "local", "collective" or "all", a designation ("Two", "Two of Five", "2") or a session
264/// id (or a unique prefix of at least 4 characters). `Ok(None)` is every drone. Live first, then local:
265/// an id or prefix that matches a running drone names it over an ended one (which never reads a message),
266/// and among those one on this machine over one on another node; a remote drone is named only when
267/// none here matches as well (or by its number, which names one drone of all).
268pub 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
292/// Display: the characters of a node key in a drone table's column (here, and top_live.rs's `NODE`
293/// column): enough to tell a collective's few members apart, short enough for the column. A message
294/// names its node longer (msg.rs `NODE_CHARS`), where the key is the one part the mesh checked.
295pub const COLUMN_NODE_CHARS: usize = 6;
296
297/// A node key as a drone table's column shows it: its first `COLUMN_NODE_CHARS`.
298pub fn column_node(node: &str) -> String {
299    node.chars().take(COLUMN_NODE_CHARS).collect()
300}
301
302/// Display: the characters of one message line on the printed screen (`render`): about one wide
303/// terminal line after the age column; `borg inbox --show` prints the whole text.
304const 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
310/// Whether a line of `total` columns, cut to `width` as `cut` cuts it (its first `width - 1` and `…`;
311/// top_live.rs cuts its lines the same), still shows any of its last `last` columns: all of them when
312/// the line fits, else those before the `…`. A message's line ends with its text, and one cut before
313/// any of it names only who wrote to whom.
314pub fn shows_last(total: usize, last: usize, width: usize) -> bool {
315    total <= width || total.saturating_sub(last) < width.saturating_sub(1)
316}
317
318/// Whether the printed screen's line for `m` (`render`: its `message_line` cut to `MESSAGE_CHARS`)
319/// shows some of its text, as a live screen's must (top_live.rs `frame`): only then did the engineer
320/// read it there, and only then does the printed screen acknowledge it (`screen_of`).
321fn 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
325/// One collective's screen.
326pub 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
377/// `to_json` with the messages side: each local drone's `unread`, and the collective's `messages`.
378pub 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// ---------------------------------------------------------------- lesson selection
395
396/// Lesson selection (select.rs) as the monitor shows it: one line per collective in `borg status` and
397/// `borg top`, in full on borg web's Conscience tab (`/api/select`). Every number is read from the
398/// store's selection summary (select/summary.rs `load`: its counts, its fit, its monitor) and the
399/// policy's own functions, never refitted here; only shares, the declaration's difference and the
400/// latency quantiles are taken of the summary's counts. A section the summary does not carry yet is
401/// empty, which every surface shows as not measured yet, never as 0. Spend is measured and shown, and
402/// priced only under a budget an engineer set for the collective (`budget`).
403#[derive(Debug, Clone, Default, PartialEq, Serialize)]
404pub struct Selection {
405    /// The policy a hook started from this environment runs under (select.rs `Policy::name`): `learned`
406    /// (the default), `fixed` (`BORG_SELECT=fixed`), `cap` or `attention` under an evaluation switch.
407    pub policy: &'static str,
408    /// The policy a hook runs under when no switch is set (select.rs `precedence` without one): the
409    /// fixed rule until the pre-registered checks pass (tasks/todo.md, amendment 2026-10-29).
410    pub default: &'static str,
411    /// What decides: the policy, or `reverted` (select.rs `REVERTED_SOURCE`) when the monitor saw harm
412    /// beyond the gate's share and put the fixed rule back (sticky).
413    pub source: &'static str,
414    /// The conservative gate's budget (select.rs `Log::gate`): the replayed quality (mistakes avoided)
415    /// the learned policy may still risk below the fixed rule, at the collective's risk share (`Test::risk`,
416    /// select/policy.rs `remaining_at`). Not money. 0 at cold start, where every decision is the fixed
417    /// rule's.
418    pub gate: f64,
419    /// The two parts the gate adds (select/policy.rs `gate_parts`), at the same risk share: the
420    /// allowance the fixed rule's value accrued, and the ledger's `DELTA` quantile (≤ 0 once the
421    /// deviations cost). A gate of 0 with a positive allowance is spent, not a cold start. Both 0
422    /// where `gate` is (another policy, a revert).
423    pub allowance: f64,
424    pub ledger: f64,
425    /// The fit decisions draw from (select/policy.rs `Fit::key`); empty: the prior.
426    pub fit: String,
427    /// The bytes of `outcomes.jsonl` the summary has folded.
428    pub at: u64,
429    /// The graded checks and the sessions folded.
430    pub checks: u64,
431    pub sessions: u64,
432    /// Per decision type the summary counts, the decisions that differed from the fixed rule's (`part`)
433    /// of those made (`whole`): `route`, the learned checks under the current fit whose routes did, on
434    /// evidence or by a played draw of the question-count test (select/summary.rs `Gate`; the test's
435    /// own draws, all-time, are in `test`). The pool's size and early delivery are not counted yet.
436    pub deviations: Vec<Count>,
437    /// Per reason a learned decision fell back to the fixed rule (select/summary.rs `FELL_BACK`, named as
438    /// the record's `reason`: `lock_busy`, `lock_failed`, `unrecorded`), the graded checks it did
439    /// (`part`) of those folded (`whole`, `checks`), every reason listed: a guard that binds often shows.
440    pub fallbacks: Vec<Count>,
441    /// What selection spent per cost source: its model calls, `stop` and `live` checks (select/summary.rs
442    /// `Spent`). Judging rides in them: a check's call also judges its session's pending questions and
443    /// placebos (predict.rs `judge_section`). `borg learn`'s own calls are not selection's; `borg stats`
444    /// counts them (stats.rs `MODEL_CALL`).
445    pub spend: Vec<Spend>,
446    /// The spend budget an engineer set for the collective and its price; None without one.
447    pub budget: Option<Budget>,
448    /// Effects with their intervals: grade × route, ask vs free, free vs placebo, dilution, the spread
449    /// between sessions and between lessons.
450    pub effects: Vec<Estimate>,
451    /// The forgetting half-life chosen, in session ticks; `Some(0)`: no forgetting.
452    pub half_life: Option<u32>,
453    /// Label health: the judged share per route (a verdict: changed, ignored or not arisen), the shares
454    /// of the judge's answers from `predict::PURE_UNCLEAR_FROM` on that were unclear (overall and per
455    /// route), not arisen and a remaining consequence, the placebo rate.
456    pub labels: Vec<Count>,
457    /// Label health: how the apply/skip declaration moves the judge (a random half is judged without it).
458    pub declaration: Option<Estimate>,
459    /// The delivery rates the fit learned (select/policy.rs `Rates`): `free_stop`, `free_live` and
460    /// `ask_live` weigh a route's value, `later` and `wait` are only shown. None before a fit.
461    pub delivery: Option<select::policy::Rates>,
462    /// The monitor's off-policy estimate and its health; None before it weighed a labelled apply.
463    pub evaluation: Option<Evaluation>,
464    /// What the checks measure for the learned policy at no cost to the fixed rule's quality; None
465    /// before a graded check is folded.
466    pub measurement: Option<Measurement>,
467    /// The pool's yield by fill position.
468    pub pool: Vec<Yield>,
469    /// Stop check latency per harness against its stop budget.
470    pub latency: Vec<Latency>,
471    /// The learned policy's randomised question-count test; None under another policy while the
472    /// summary counts none of its checks.
473    pub test: Option<Test>,
474}
475
476/// The randomised question-count test of the learned policy (select/policy.rs `choose`; docs/delivery.md
477/// "The question-count test"), all-time as the summary counts it (select/summary.rs `Experiment`), never
478/// recounted from the log. On a live check, of the k lessons below grade 3 the fixed rule delivers and
479/// that may go free, s drawn uniformly in 0..=k are asked and the rest handed over: nothing is withheld.
480#[derive(Debug, Clone, Default, PartialEq, Serialize)]
481pub struct Test {
482    /// The share of the fixed rule's value the gate may risk on it: the collective's `risk` in
483    /// collectives.json (select.rs `risk_of`), else select/policy.rs `ALPHA`. 0: the test is off.
484    pub risk: f64,
485    /// Whether the collective sets `risk` (false: the default `ALPHA`).
486    pub set: bool,
487    /// Checks the test drew a count for (played or refused by the gate), and of them the control arm's
488    /// (`experiment_control`: s is the fixed rule's own count, so it routed as the fixed rule).
489    pub ran: u64,
490    pub control: u64,
491    /// Of them, checks the gate refused it: the draw's risk did not fit what the gate may still risk, so
492    /// the fixed rule decided (source `gate`).
493    pub refused: u64,
494    /// The sessions with a check the test drew a count for.
495    pub sessions: u64,
496    /// Of `ran`, the drawn checks the summary still holds, which the readout's estimates read
497    /// (select/replay.rs `TestReadout::kept`): compaction drops the oldest checks, never the counts.
498    pub kept: u64,
499    /// Per k, how often each s was drawn (`draws[s]`, s from 0 to k) and the checks refused at that k.
500    pub by_k: Vec<Draws>,
501    /// The pre-registered readout (select/replay.rs `test_readout`): due after `TEST_CHECKS` checks drawn
502    /// in `TEST_MIN_SESSIONS` sessions with one (`due_min_sessions`), or `TEST_SESSIONS` sessions alone,
503    /// and only then estimated, each with its session-bootstrap
504    /// 95% interval: per number m ≥ 2 of the test's questions asked, the test's top lesson's change rate
505    /// against asked alone (`dilution`, unit `points changed`), and asking a grade 2 against handing it
506    /// over (`ask_free`, unit `points changed`). Empty and None until due, or with no drawn check.
507    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/// The test's checks at one k (`Test::by_k`): uniform draws give each s about `ran / (k + 1)`.
516#[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    /// The summary's counts (select/summary.rs `Experiment`) and its readout (select/replay.rs
525    /// `test_readout`), under the risk share `risk` the collective sets (None: `ALPHA`).
526    #[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        // An estimate as a share of 1 scaled to percent, named; none without a value.
533        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/// `part` of `whole` (a share), named.
575#[derive(Debug, Clone, Default, PartialEq, Serialize)]
576pub struct Count {
577    pub name: String,
578    pub part: u64,
579    pub whole: u64,
580}
581
582/// One cost source's spend over the sessions folded (`Selection::sessions`).
583#[derive(Debug, Clone, Default, PartialEq, Serialize)]
584pub struct Spend {
585    /// `stop` or `live`.
586    pub source: String,
587    pub calls: u64,
588    /// The extra rounds it cost the sessions (a stop block is one).
589    pub rounds: u64,
590    pub usd: f64,
591}
592
593/// An engineer's budget for the collective (`collectives.json` `budget`, select.rs `budget_of`): the
594/// pool tokens its checks may read per session on average, what they read per session, and the price
595/// per pool token the policy weighs a larger pool against (select/policy.rs `price`, λ).
596#[derive(Debug, Clone, Default, PartialEq, Serialize)]
597pub struct Budget {
598    pub tokens: f64,
599    pub spent: f64,
600    pub price: f64,
601}
602
603/// An effect and its interval, as the policy reports them.
604#[derive(Debug, Clone, Default, PartialEq, Serialize)]
605pub struct Estimate {
606    pub name: String,
607    /// `points` (percentage points of P(changed and avoids)), `coefficient` (a value-model coefficient,
608    /// on the logit scale), `logit` (a spread's standard deviation), or the question-count test's
609    /// readout's `percent changed` (the share of judged lessons that changed the work) and `points
610    /// changed` (a difference of such shares).
611    pub unit: String,
612    pub mean: f64,
613    pub lo: f64,
614    pub hi: f64,
615    /// The labelled units behind it.
616    pub n: u64,
617}
618
619/// The monitor's weighted estimate (select/policy.rs `Monitor`), the auto-revert's evidence, and how
620/// far it can be trusted.
621#[derive(Debug, Clone, Default, PartialEq, Serialize)]
622pub struct Evaluation {
623    /// The learned policy's sessions it read.
624    pub sessions: u64,
625    /// Per apply, (policy − fixed rule) + α × the fixed rule's value, and the confidence sequence's
626    /// upper bound on it (None until enough sessions): below 0 the store reverts to the fixed rule.
627    pub mean: f64,
628    pub ucb: Option<f64>,
629    /// The labelled units weighed, and their effective number ((Σw)² / Σw²).
630    pub units: u64,
631    pub ess: f64,
632    /// The largest weight against the mean weight.
633    pub max_weight: f64,
634    /// The units whose weight was truncated.
635    pub truncated: u64,
636}
637
638/// What the checks measure for the learned policy without costing the fixed rule any quality: under the
639/// fixed default the placebo collects the not-now labels the fixed rule's own history cannot give
640/// (tasks/todo.md, amendment 2026-10-29). Counted over the applies the summary keeps, as `labels`
641/// counts its placebo rows (select/summary.rs `Apply`, its route and label).
642#[derive(Debug, Clone, Default, PartialEq, Serialize)]
643pub struct Measurement {
644    /// Placebos drawn: applies no route delivered, judged blindly where a free delivery would be.
645    pub placebos: u64,
646    /// Of them, those the judge gave a verdict that says something (changed or ignored).
647    pub judged: u64,
648    /// The tail samples' lessons shown past the pool budget, and those that applied (whole.rs `tail`,
649    /// select/summary.rs `Pool::tail`): the yield past the budget, learned under every policy.
650    pub tail_shown: u64,
651    pub tail_applied: u64,
652}
653
654/// The pool's yield at fill positions `from..to` (0 first): of the lessons shown there, those applied,
655/// and the fitted chance (monotone over positions).
656#[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/// One harness's stop check latency against its stop budget, over the stops' model calls the summary
666/// keeps (select/summary.rs `Pool::stops`, the newest `LATENCY_KEEP`).
667#[derive(Debug, Clone, Default, PartialEq, Serialize)]
668pub struct Latency {
669    pub harness: String,
670    pub checks: u64,
671    /// The calls' median and 95th percentile (nearest rank, `QUANTILES`).
672    pub p50_ms: u64,
673    pub p95_ms: u64,
674    /// Its stop budget (harness `Caps::stop_budget`); None: it names none.
675    pub budget_ms: Option<u64>,
676    /// Calls that failed: they answered nothing (ran out of the time the budget left them, or the CLI
677    /// erred), so their checks asked nothing.
678    pub over: u64,
679}
680
681/// Display: the latency quantiles shown, the median and the 95th percentile (the tail a stop budget
682/// must hold, as the pool's latency model reads it: select/policy.rs `pool_feasible`).
683const QUANTILES: [f64; 2] = [0.50, 0.95];
684
685/// The nearest-rank `q` quantile of the ascending `sorted`; 0 for none.
686fn 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    /// What `summary` says, under `policy` and the risk share `risk` its collective sets (select.rs
693    /// `risk_of`; None: the default): copied from the summary, its fit and its monitor, never refitted.
694    /// What it does not hold yet stays empty: not measured.
695    #[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        // Every session folded, the compacted ones too, as the spend and pool tokens are.
704        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        // Folded with every graded check, as `checks` is: none before one is.
710        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        // A verdict (select/summary.rs `Label::verdict`): changed, ignored or not arisen.
723        let judged = |l: Label| l.verdict();
724        // Per route, its applies and those judged: the label health's rows, and the placebo's the
725        // measurement's. A free apply of a lesson its session had is dropped at delivery as given before,
726        // so no verdict can come of it: out of its row, as it is out of the summary's delivery tallies
727        // (select/summary.rs `Apply::had`). A question of one is delivered unless done by then and
728        // judged as any other, and a placebo is judged blind, never delivered: in, had or not.
729        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        // Every answer of the judge from `predict::PURE_UNCLEAR_FROM` on, the pending ones too, each
739        // apply's latest (`Apply::jv`): the pure unclear share is the 0.7.5 target (docs/evidence.md),
740        // overall and per route, not arisen and a remaining consequence apart from it. An older
741        // `asked_unclear` also held a remaining consequence: in none of them.
742        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        // The declaration's effect on the judge: the changed share of judged questions with it minus without.
764        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    /// Whether `borg top` shows the line: once the summary has folded a graded check, or while another
848    /// policy than the default (a switch) or the monitor's revert decides.
849    #[must_use]
850    pub fn shown(&self) -> bool {
851        self.checks > 0 || self.source != self.default
852    }
853}
854
855/// Method: the normal quantile of a 95% interval, for the estimates the monitoring shows.
856const Z95: f64 = 1.96;
857
858/// The value model's coefficients with their 95% intervals (unit `coefficient`, logit scale), from `n`
859/// labels, then the spread of the session, lesson and judge effects (unit `logit`, a standard
860/// deviation, shown as a point).
861fn 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    // The covariance is chol · cholᵀ: a coefficient's variance is the sum of squares of its row.
867    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/// The selection of the store whose lessons file is `lessons`, under `policy` (select.rs `policy`).
885#[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/// `s` as one line (`borg status`, `borg top`): what decides (the default named as such, a policy
891/// opted into by its switch), then the checks folded, how often the policy deviated from the fixed
892/// rule, the learned decisions the fixed rule took over per reason (only those that happened), the
893/// question-count test (`test_line`), what the checks measure for the learned policy (placebos), the
894/// monitor's reading (the
895/// auto-revert's evidence) with its weights' effective number and largest weight, the stops' latency
896/// per harness against its stop budget, and what selection spent per session; a budget and its price
897/// only where an engineer set one.
898#[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                // The ledger's quantile used the allowance up: a deviation passes only on its own
911                // positive evidence (select/policy.rs `choose`: risk ≤ 0).
912                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
972/// The question-count test as one part of the selection line: the checks it ran, its controls, those
973/// the gate refused, those the summary still holds once compaction dropped some (`kept`, what the
974/// readout reads), the risk share in force (`set` names the collective's own; 0 says it is off), and
975/// the readout: when it is due, and once due its ask-against-free estimate.
976fn 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/// `render` with the collective's selection line under it, when `borg top` shows one (`Selection::shown`).
994#[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
1003/// The line over the collectives when borg itself needs the engineer, none otherwise: a newer release
1004/// than this borg (`checked`, the last release check; ignored while `check` is off,
1005/// `BORG_UPDATE_CHECK=off`), then per store in `stores` (only one with `mesh/`) each member nothing
1006/// syncs with, and each member on an older protocol with what it misses (protocol.rs `NOTES`). A
1007/// member on this node's protocol, even on a newer borg, is no news here (`borg status` lists it).
1008pub 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
1030/// The collectives to show: the one `dir` routes to, or every one on this machine.
1031fn 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
1040/// Every collective to show with its drones, and the line on borg itself over them (`version_line`,
1041/// over every collective `dir` routes to, shown or not) (top_live.rs).
1042pub 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        // Every frame: the cache and its tail, never a rebuild of the whole log (select/summary.rs `peek`);
1065        // the checks' hooks keep the cache current.
1066        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
1074/// The printed screen, with the line on borg itself first (`version_line`; not in `--json`).
1075fn 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
1088/// The printed screen (or `--json`) of the collectives `routes`, `one` when `--dir` named the one to
1089/// show (shown even with nothing in it); when `caller` finds no session (`engineer_looks`), the text
1090/// screen acknowledges the messages to the engineer of the repo at `cwd` whose text it prints
1091/// (`prints_text`, `acknowledge`).
1092fn 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    // Every collective's drones, and what each store's text screen printed: whether the engineer is
1103    // the one looking, and so acknowledges it, is known once all are gathered.
1104    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        // An engineer's several emails show as their name (collective.rs "Engineers"), peers' too.
1110        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    // SAFETY: isatty only inspects the descriptor.
1156    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    /// A store's selection after 480 checks: the learned policy opted into, with gate budget, its route
1266    /// deviations, the placebos the checks drew, the monitor's reading and weights, spend from both
1267    /// check channels.
1268    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        // The fixed rule as the default names itself so and the opt-in; the learned policy opted into says so.
1304        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        // Where the learned policy is the default, the fixed rule is the switch.
1314        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    /// A gate of 0 is a cold start only without a fit or an allowance; a positive allowance the ledger's
1343    /// quantile used up is a spent gate, where a deviation needs positive evidence (a team store on
1344    /// 2026-10-31: allowance 0.076, ledger q05 −0.102, 74 evidence and test decisions that day).
1345    #[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        // A fit whose ledger accrued nothing yet: cold start.
1353        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        // No fit: whatever the ledger says, the prior decides as the fixed rule.
1359        s.fit.key.clear();
1360        assert!(line(&s).starts_with("learned, cold start: gate 0"), "{}", line(&s));
1361        // An allowance the ledger leaves room in: the gate as before.
1362        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        // Another policy shows neither part.
1366        let fixed = Selection::of(&s, Policy::Fixed, None);
1367        assert_eq!((fixed.allowance, fixed.ledger), (0.0, 0.0));
1368    }
1369
1370    /// `borg status`'s line, read from a store: one with nothing folded decides by the fixed rule under
1371    /// whichever policy; a switch names itself; no budget is mentioned.
1372    #[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    /// `borg top`'s screen keeps `render`'s text and adds the selection line under it only when shown.
1390    #[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    /// The fields borg web's Conscience tab reads (ui api.rs `parse_select`); a budget is null unless set.
1402    #[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    /// Label health's judged share of a route counts the applies a verdict can come of: a free apply of
1421    /// a lesson its session already had (an older fixed rule's, delivery dropped it as given before)
1422    /// is out of it, as it is out of the summary's delivery tallies; a question of a lesson handed over
1423    /// and not acted on is delivered and judged, so in.
1424    #[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            // B, handed over early and not acted on, is asked at c3: delivered, then judged.
1455            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    /// The verdict shares count the judge's answers from `predict::PURE_UNCLEAR_FROM` on, the pending ones
1470    /// too, overall and per route: an older `asked_unclear` (which also held a remaining consequence) is
1471    /// in none of them; an apply's latest answer is its own.
1472    #[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            // Before 0.7.5: an unclear free delivery, in no share.
1507            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    /// The placebos a fixed-rule store's checks drew, and those judged, as the summary folds them:
1537    /// a placebo judged ignored counts, one without a verdict is drawn only, a free lesson's verdict is
1538    /// not a placebo's; a tail sample's lessons shown and applied; a store with nothing folded has no
1539    /// measurement.
1540    #[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        // A pool the budget cut, its tail sample of two past it, T2 of them applied.
1566        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    /// The learned decisions the fixed rule took over, per reason, as the summary folds them from the
1594    /// checks' records: every reason a row of the graded checks folded, the line naming those that
1595    /// happened; nothing folded, no rows (not measured), and none happened, nothing on the line.
1596    #[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    /// The sections the summary holds are filled from it: per known harness its stops' latency against
1635    /// the harness's stop budget (failed calls counted, live and unknown harnesses' left out), the
1636    /// monitor's reading and weights, the fitted delivery rates; a summary without them leaves them
1637    /// empty.
1638    #[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        // Guard: a harness's newest `LATENCY_KEEP` stops are kept, the cache never grows past them.
1684        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    /// A learned live check of the question-count test, as hook.rs logs it: lessons A (grade 2) and B
1694    /// (grade 1), its `source`, k and s.
1695    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    /// The test's section is the summary's fold of the graded records (select/summary.rs `Experiment`),
1715    /// never a recount: drawn counts by k and s, played or refused, the controls, the gate's refusals
1716    /// by k (each also drawn), the sessions with a drawn count; an evidence check, a refused evidence
1717    /// deviation and another policy's record count nowhere. The risk share is the collective's when set, else `ALPHA`; the section shows
1718    /// under the learned policy, and under another only while the summary counts some of its checks.
1719    #[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        // Compaction dropped the oldest drawn check: the counts stay, the line says what the readout reads.
1749        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        // Off: said so while nothing ran.
1761        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        // Another policy: shown only while the summary counts the test's checks.
1767        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    /// The gate shown is the one in force: the ledger's allowance at the collective's risk share
1772    /// (select/policy.rs `remaining_at`, which `choose` decides by), the default `ALPHA`'s unless set,
1773    /// and 0 at `risk` 0 with nothing gained.
1774    #[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    /// Once due, the readout's estimates are the replay's (select/replay.rs `test_readout`) in percent:
1785    /// a grade 2 asked against handed over, which the line says, and no dilution at k = 1.
1786    #[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            // k = 1: A asked (s = 1) in even sessions, handed over (s = 0) in odd ones, each at 1/2.
1797            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            // Every question changed the work, every hand-over was ignored.
1821            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        // At k = 1 no second question can dilute the first: dilution starts at 2 asked against 1.
1833        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    /// The unread count is what the next prompt would show: a message shown once no longer counts,
1839    /// broadcasts only to drones running when sent, never its own; no log costs no read.
1840    #[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    /// The acknowledgements (`by`, ids) in the store's messages log.
1863    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    /// The calling session msg.rs `caller` tells from a shell whose session variables are `sessions` and
1874    /// harness pids `pids` (its own `pick`), whatever the shell running the tests sets.
1875    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    /// A store at `base`/`store` and a repo at `base`/`work` whose engineer is ann@x: one message to the
1880    /// engineer, one to a session. Returns the store, the repo and the message to the engineer.
1881    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    /// The printed screen acknowledges a message to the engineer it shows as the engineer of the repo
1897    /// it runs in, as `borg inbox` does from a terminal; a message to a session is never the engineer's
1898    /// to acknowledge, and `--json`, for scripts, acknowledges nothing.
1899    #[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    /// A drone's shell runs the screen piped and in the engineer's repo: it prints the message to the
1916    /// engineer and acknowledges nothing as them, as `borg inbox` would not. A session variable, two
1917    /// that no presence tells apart, or opencode's pid naming a drone of another collective on the
1918    /// screen: each is a session's; the same screen from a terminal acknowledges it.
1919    #[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    /// The printed screen acknowledges a message to the engineer only when its line, cut to
1948    /// `MESSAGE_CHARS`, still shows some of its text (`prints_text`), as the live screen does: a line
1949    /// whose sender and recipient fill it names only who wrote to whom, and its sender's window stays
1950    /// closed until the engineer reads the text elsewhere (`borg inbox`).
1951    #[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        // The engineer's address takes 60 columns, so a line's text starts after a sender's name + 66.
1956        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    /// Guard (Q1 pacing): a live screen acknowledges a message once per screen, then skips it without
1975    /// asking git or reading the log's seen lines again, whatever the log holds by then; a message
1976    /// that arrives later is acknowledged at the frame that first shows it.
1977    #[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        // The log loses the acknowledgement (as a bound or a TTL may cut it): the next frame passes
1994        // the message by, its id already done.
1995        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        // From another sender: the first's window holds its sender's next one until acknowledged.
2000        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    /// A line's `by` and `name` are its sender's claims: one from another node that names a local drone's
2008    /// session is not shown as that drone, and carries its node's key.
2009    #[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    /// The line on borg itself: a newer release (while the check is on), a member nothing syncs with and
2026    /// one on an older protocol with what it misses; nothing for a store without `mesh/` or members on
2027    /// this node's protocol.
2028    #[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(&current), 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    /// Local first: a prefix or id that matches a drone here and one of a member names the one here.
2074    #[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    /// An ended session here does not shadow a live one on a member's machine: it would never read the message.
2092    #[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}