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    /// Label health: the judge's effective length per model (judge_length.rs), the session-end judge's
475    /// prompt budget: the answered share per prompt-size step and what is explored. Read from the
476    /// store's judge cost lines by `selection`; empty from `Selection::of`.
477    pub judge: Vec<crate::judge_length::Length>,
478}
479
480/// The randomised question-count test of the learned policy (select/policy.rs `choose`; docs/delivery.md
481/// "The question-count test"), all-time as the summary counts it (select/summary.rs `Experiment`), never
482/// recounted from the log. On a live check, of the k lessons below grade 3 the fixed rule delivers and
483/// that may go free, s drawn uniformly in 0..=k are asked and the rest handed over: nothing is withheld.
484#[derive(Debug, Clone, Default, PartialEq, Serialize)]
485pub struct Test {
486    /// The share of the fixed rule's value the gate may risk on it: the collective's `risk` in
487    /// collectives.json (select.rs `risk_of`), else select/policy.rs `ALPHA`. 0: the test is off.
488    pub risk: f64,
489    /// Whether the collective sets `risk` (false: the default `ALPHA`).
490    pub set: bool,
491    /// Checks the test drew a count for (played or refused by the gate), and of them the control arm's
492    /// (`experiment_control`: s is the fixed rule's own count, so it routed as the fixed rule).
493    pub ran: u64,
494    pub control: u64,
495    /// Of them, checks the gate refused it: the draw's risk did not fit what the gate may still risk, so
496    /// the fixed rule decided (source `gate`).
497    pub refused: u64,
498    /// The sessions with a check the test drew a count for.
499    pub sessions: u64,
500    /// Of `ran`, the drawn checks the summary still holds, which the readout's estimates read
501    /// (select/replay.rs `TestReadout::kept`): compaction drops the oldest checks, never the counts.
502    pub kept: u64,
503    /// Per k, how often each s was drawn (`draws[s]`, s from 0 to k) and the checks refused at that k.
504    pub by_k: Vec<Draws>,
505    /// The pre-registered readout (select/replay.rs `test_readout`): due after `TEST_CHECKS` checks drawn
506    /// in `TEST_MIN_SESSIONS` sessions with one (`due_min_sessions`), or `TEST_SESSIONS` sessions alone,
507    /// and only then estimated, each with its session-bootstrap
508    /// 95% interval: per number m ≥ 2 of the test's questions asked, the test's top lesson's change rate
509    /// against asked alone (`dilution`, unit `points changed`), and asking a grade 2 against handing it
510    /// over (`ask_free`, unit `points changed`). Empty and None until due, or with no drawn check.
511    pub due: bool,
512    pub due_checks: u64,
513    pub due_sessions: u64,
514    pub due_min_sessions: u64,
515    pub dilution: Vec<Estimate>,
516    pub ask_free: Option<Estimate>,
517}
518
519/// The test's checks at one k (`Test::by_k`): uniform draws give each s about `ran / (k + 1)`.
520#[derive(Debug, Clone, Default, PartialEq, Serialize)]
521pub struct Draws {
522    pub k: u32,
523    pub draws: Vec<u64>,
524    pub refused: u64,
525}
526
527impl Test {
528    /// The summary's counts (select/summary.rs `Experiment`) and its readout (select/replay.rs
529    /// `test_readout`), under the risk share `risk` the collective sets (None: `ALPHA`).
530    #[must_use]
531    pub fn of(summary: &select::summary::Summary, risk: Option<f64>) -> Test {
532        use select::replay::{TEST_CHECKS, TEST_MIN_SESSIONS, TEST_SESSIONS, test_readout};
533        let e = &summary.experiment;
534        let (ran, refused) = e.totals();
535        let r = test_readout(summary);
536        // An estimate as a share of 1 scaled to percent, named; none without a value.
537        let estimate = |name: String, unit: &str, x: &crate::ope::Estimate| {
538            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 })
539        };
540        let mut by_k: Vec<Draws> = Vec::new();
541        let at = |by_k: &mut Vec<Draws>, k: u32| match by_k.iter().position(|d| d.k == k) {
542            Some(i) => i,
543            None => {
544                by_k.push(Draws { k, draws: vec![0; k as usize + 1], refused: 0 });
545                by_k.len() - 1
546            }
547        };
548        for &(k, s, n) in &e.ran {
549            let i = at(&mut by_k, k);
550            if let Some(c) = by_k[i].draws.get_mut(s as usize) {
551                *c += n;
552            }
553        }
554        for &(k, n) in &e.refused {
555            let i = at(&mut by_k, k);
556            by_k[i].refused += n;
557        }
558        by_k.sort_by_key(|d| d.k);
559        Test {
560            risk: risk.unwrap_or(select::policy::ALPHA),
561            set: risk.is_some(),
562            ran,
563            control: e.control,
564            refused,
565            sessions: u64::from(e.sessions),
566            kept: r.kept,
567            by_k,
568            due: r.due,
569            due_checks: TEST_CHECKS,
570            due_sessions: u64::from(TEST_SESSIONS),
571            due_min_sessions: u64::from(TEST_MIN_SESSIONS),
572            dilution: r.by_asked.iter().filter_map(|(m, x)| estimate(format!("{m} asked − 1 asked"), "points changed", x)).collect(),
573            ask_free: r.ask_free.as_ref().and_then(|x| estimate("grade 2 asked − handed over".into(), "points changed", x)),
574        }
575    }
576}
577
578/// `part` of `whole` (a share), named.
579#[derive(Debug, Clone, Default, PartialEq, Serialize)]
580pub struct Count {
581    pub name: String,
582    pub part: u64,
583    pub whole: u64,
584}
585
586/// One cost source's spend over the sessions folded (`Selection::sessions`).
587#[derive(Debug, Clone, Default, PartialEq, Serialize)]
588pub struct Spend {
589    /// `stop` or `live`.
590    pub source: String,
591    pub calls: u64,
592    /// The extra rounds it cost the sessions (a stop block is one).
593    pub rounds: u64,
594    pub usd: f64,
595}
596
597/// An engineer's budget for the collective (`collectives.json` `budget`, select.rs `budget_of`): the
598/// pool tokens its checks may read per session on average, what they read per session, and the price
599/// per pool token the policy weighs a larger pool against (select/policy.rs `price`, λ).
600#[derive(Debug, Clone, Default, PartialEq, Serialize)]
601pub struct Budget {
602    pub tokens: f64,
603    pub spent: f64,
604    pub price: f64,
605}
606
607/// An effect and its interval, as the policy reports them.
608#[derive(Debug, Clone, Default, PartialEq, Serialize)]
609pub struct Estimate {
610    pub name: String,
611    /// `points` (percentage points of P(changed and avoids)), `coefficient` (a value-model coefficient,
612    /// on the logit scale), `logit` (a spread's standard deviation), or the question-count test's
613    /// readout's `percent changed` (the share of judged lessons that changed the work) and `points
614    /// changed` (a difference of such shares).
615    pub unit: String,
616    pub mean: f64,
617    pub lo: f64,
618    pub hi: f64,
619    /// The labelled units behind it.
620    pub n: u64,
621}
622
623/// The monitor's weighted estimate (select/policy.rs `Monitor`), the auto-revert's evidence, and how
624/// far it can be trusted.
625#[derive(Debug, Clone, Default, PartialEq, Serialize)]
626pub struct Evaluation {
627    /// The learned policy's sessions it read.
628    pub sessions: u64,
629    /// Per apply, (policy − fixed rule) + α × the fixed rule's value, and the confidence sequence's
630    /// upper bound on it (None until enough sessions): below 0 the store reverts to the fixed rule.
631    pub mean: f64,
632    pub ucb: Option<f64>,
633    /// The labelled units weighed, and their effective number ((Σw)² / Σw²).
634    pub units: u64,
635    pub ess: f64,
636    /// The largest weight against the mean weight.
637    pub max_weight: f64,
638    /// The units whose weight was truncated.
639    pub truncated: u64,
640}
641
642/// What the checks measure for the learned policy without costing the fixed rule any quality: under the
643/// fixed default the placebo collects the not-now labels the fixed rule's own history cannot give
644/// (tasks/todo.md, amendment 2026-10-29). Counted over the applies the summary keeps, as `labels`
645/// counts its placebo rows (select/summary.rs `Apply`, its route and label).
646#[derive(Debug, Clone, Default, PartialEq, Serialize)]
647pub struct Measurement {
648    /// Placebos drawn: applies no route delivered, judged blindly where a free delivery would be.
649    pub placebos: u64,
650    /// Of them, those the judge gave a verdict that says something (changed or ignored).
651    pub judged: u64,
652    /// The tail samples' lessons shown past the pool budget, and those that applied (whole.rs `tail`,
653    /// select/summary.rs `Pool::tail`): the yield past the budget, learned under every policy.
654    pub tail_shown: u64,
655    pub tail_applied: u64,
656}
657
658/// The pool's yield at fill positions `from..to` (0 first): of the lessons shown there, those applied,
659/// and the fitted chance (monotone over positions).
660#[derive(Debug, Clone, Default, PartialEq, Serialize)]
661pub struct Yield {
662    pub from: u32,
663    pub to: u32,
664    pub shown: u64,
665    pub applied: u64,
666    pub rate: f64,
667}
668
669/// One harness's stop check latency against its stop budget, over the stops' model calls the summary
670/// keeps (select/summary.rs `Pool::stops`, the newest `LATENCY_KEEP`).
671#[derive(Debug, Clone, Default, PartialEq, Serialize)]
672pub struct Latency {
673    pub harness: String,
674    pub checks: u64,
675    /// The calls' median and 95th percentile (nearest rank, `QUANTILES`).
676    pub p50_ms: u64,
677    pub p95_ms: u64,
678    /// Its stop budget (harness `Caps::stop_budget`); None: it names none.
679    pub budget_ms: Option<u64>,
680    /// Calls that failed: they answered nothing (ran out of the time the budget left them, or the CLI
681    /// erred), so their checks asked nothing.
682    pub over: u64,
683}
684
685/// Display: the latency quantiles shown, the median and the 95th percentile (the tail a stop budget
686/// must hold, as the pool's latency model reads it: select/policy.rs `pool_feasible`).
687const QUANTILES: [f64; 2] = [0.50, 0.95];
688
689/// The nearest-rank `q` quantile of the ascending `sorted`; 0 for none.
690fn quantile(sorted: &[u64], q: f64) -> u64 {
691    let rank = ((q * sorted.len() as f64).ceil() as usize).clamp(1, sorted.len().max(1));
692    sorted.get(rank - 1).copied().unwrap_or(0)
693}
694
695impl Selection {
696    /// What `summary` says, under `policy` and the risk share `risk` its collective sets (select.rs
697    /// `risk_of`; None: the default): copied from the summary, its fit and its monitor, never refitted.
698    /// What it does not hold yet stays empty: not measured.
699    #[must_use]
700    pub fn of(summary: &select::summary::Summary, policy: select::Policy, risk: Option<f64>) -> Selection {
701        use select::summary::{LIVE, Label, STOP};
702        let s = summary;
703        let learned = policy == select::Policy::Learned;
704        let reverted = learned && s.monitor.reverted;
705        let default = select::precedence(None, None, false).name();
706        let count = |name: &str, part: usize, whole: usize| Count { name: name.into(), part: part as u64, whole: whole as u64 };
707        // Every session folded, the compacted ones too, as the spend and pool tokens are.
708        let (sessions, checks) = s.checked();
709        let deviations = match s.gate.checks {
710            0 => Vec::new(),
711            n => vec![count("route", s.gate.deviated as usize, n as usize)],
712        };
713        // Folded with every graded check, as `checks` is: none before one is.
714        let fallbacks = select::summary::FELL_BACK
715            .iter()
716            .zip(s.fell_back)
717            .filter(|_| checks > 0)
718            .map(|(reason, n)| Count { name: (*reason).into(), part: n, whole: checks })
719            .collect();
720        let spend = [("stop", STOP), ("live", LIVE)]
721            .into_iter()
722            .filter(|(_, i)| s.spend[*i].calls > 0)
723            .map(|(source, i)| Spend { source: source.into(), calls: s.spend[i].calls, rounds: s.spend[i].rounds, usd: s.spend[i].usd })
724            .collect();
725        let budget = s.fit.budget.map(|tokens| Budget { tokens, spent: s.pool.tokens as f64 / sessions.max(1) as f64, price: s.fit.lambda });
726        // A verdict (select/summary.rs `Label::verdict`): changed, ignored or not arisen.
727        let judged = |l: Label| l.verdict();
728        // Per route, its applies and those judged: the label health's rows, and the placebo's the
729        // measurement's. A free apply of a lesson its session had is dropped at delivery as given before,
730        // so no verdict can come of it: out of its row, as it is out of the summary's delivery tallies
731        // (select/summary.rs `Apply::had`). A question of one is delivered unless done by then and
732        // judged as any other, and a placebo is judged blind, never delivered: in, had or not.
733        let of = |route: select::Route| {
734            let of: Vec<_> = s.applies.iter().filter(|a| a.route == route && (route != select::Route::Free || !a.had)).collect();
735            (of.iter().filter(|a| judged(a.label)).count(), of.len())
736        };
737        let mut labels = Vec::new();
738        for (name, route) in [("judged, asked", select::Route::Ask), ("judged, free", select::Route::Free), ("judged, placebo", select::Route::Placebo)] {
739            let (part, whole) = of(route);
740            labels.push(count(name, part, whole));
741        }
742        // Every answer of the judge from `predict::PURE_UNCLEAR_FROM` on, the pending ones too, each
743        // apply's latest (`Apply::jv`): the pure unclear share is the 0.7.5 target (docs/evidence.md),
744        // overall and per route, not arisen and a remaining consequence apart from it. An older
745        // `asked_unclear` also held a remaining consequence: in none of them.
746        let answers: Vec<_> = s.applies.iter().filter(|a| a.jv >= crate::predict::PURE_UNCLEAR_FROM && (judged(a.label) || a.label.pending())).collect();
747        let share = |name: &str, label: Label, route: Option<select::Route>| {
748            let of: Vec<_> = answers.iter().filter(|a| route.is_none_or(|r| a.route == r)).collect();
749            count(name, of.iter().filter(|a| a.label == label).count(), of.len())
750        };
751        for (name, label) in [("unclear", Label::Unclear), ("not arisen", Label::NotArisen), ("consequence remains", Label::Remains)] {
752            labels.push(share(name, label, None));
753        }
754        for (name, route) in [("unclear, asked", select::Route::Ask), ("unclear, free", select::Route::Free), ("unclear, placebo", select::Route::Placebo)] {
755            labels.push(share(name, Label::Unclear, Some(route)));
756        }
757        let later = s.applies.iter().filter(|a| matches!(a.route, select::Route::Not | select::Route::Placebo)).count();
758        let (placebos_judged, placebos) = of(select::Route::Placebo);
759        labels.push(count("placebo", placebos, later));
760        labels.retain(|c| c.whole > 0);
761        let measurement = (checks > 0).then_some(Measurement {
762            placebos: placebos as u64,
763            judged: placebos_judged as u64,
764            tail_shown: s.pool.tail.0,
765            tail_applied: s.pool.tail.1,
766        });
767        // The declaration's effect on the judge: the changed share of judged questions with it minus without.
768        let share = |stripped: bool| {
769            let of: Vec<_> = s.applies.iter().filter(|a| a.route == select::Route::Ask && judged(a.label) && a.stripped == stripped).collect();
770            (of.iter().filter(|a| a.label == Label::Changed).count() as f64, of.len() as f64)
771        };
772        let ((c1, n1), (c0, n0)) = (share(false), share(true));
773        let declaration = (n1 > 0.0 && n0 > 0.0).then(|| {
774            let (r1, r0) = (c1 / n1, c0 / n0);
775            let se = (r1 * (1.0 - r1) / n1 + r0 * (1.0 - r0) / n0).sqrt();
776            let mean = 100.0 * (r1 - r0);
777            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 }
778        });
779        let pool = s
780            .pool
781            .bins
782            .iter()
783            .enumerate()
784            .filter(|(_, (shown, _))| *shown > 0)
785            .map(|(b, &(shown, applied))| Yield {
786                from: (1u32 << b) - 1,
787                to: (1u32 << (b + 1)) - 1,
788                shown,
789                applied,
790                rate: s.fit.pool.yields.get(b).map_or(applied as f64 / shown as f64, |(a, b)| a / (a + b)),
791            })
792            .collect();
793        let (allowance, ledger) = if learned && !reverted { select::policy::gate_parts(&s.gate, risk.unwrap_or(select::policy::ALPHA)) } else { (0.0, 0.0) };
794        let mon = &s.monitor;
795        let evaluation = (mon.sessions > 0).then(|| Evaluation {
796            sessions: u64::from(mon.sessions),
797            mean: mon.mean,
798            ucb: mon.ucb,
799            units: u64::from(mon.labelled),
800            ess: mon.ess,
801            max_weight: mon.max_weight,
802            truncated: u64::from(mon.truncated),
803        });
804        let latency = s
805            .pool
806            .stops
807            .iter()
808            .filter(|(_, calls)| !calls.is_empty())
809            .map(|(harness, calls)| {
810                let mut ms: Vec<u64> = calls.iter().map(|&(ms, _)| u64::from(ms)).collect();
811                ms.sort_unstable();
812                let budget = crate::harness::ALL.iter().find(|h| h.name() == harness).and_then(|h| h.caps().stop_budget);
813                Latency {
814                    harness: harness.clone(),
815                    checks: ms.len() as u64,
816                    p50_ms: quantile(&ms, QUANTILES[0]),
817                    p95_ms: quantile(&ms, QUANTILES[1]),
818                    budget_ms: budget.map(|b| u64::try_from(b.as_millis()).unwrap_or(u64::MAX)),
819                    over: calls.iter().filter(|(_, failed)| *failed).count() as u64,
820                }
821            })
822            .collect();
823        Selection {
824            policy: policy.name(),
825            default,
826            source: if reverted { select::REVERTED_SOURCE } else { policy.name() },
827            gate: if learned && !reverted { select::policy::remaining_at(&s.gate, risk.unwrap_or(select::policy::ALPHA)) } else { 0.0 },
828            allowance,
829            ledger,
830            fit: s.fit.key.clone(),
831            at: s.at,
832            checks,
833            sessions,
834            deviations,
835            fallbacks,
836            spend,
837            budget,
838            effects: s.fit.model.as_ref().map(|m| effects(m, s.fit.labels)).unwrap_or_default(),
839            half_life: s.fit.model.as_ref().map(|_| s.fit.half_life.unwrap_or(0)),
840            labels,
841            declaration,
842            delivery: (!s.fit.key.is_empty()).then(|| s.fit.rates.clone()),
843            evaluation,
844            measurement,
845            pool,
846            latency,
847            test: (learned || s.experiment.totals() != (0, 0)).then(|| Test::of(s, risk)),
848            judge: Vec::new(),
849        }
850    }
851
852    /// Whether `borg top` shows the line: once the summary has folded a graded check, or while another
853    /// policy than the default (a switch) or the monitor's revert decides.
854    #[must_use]
855    pub fn shown(&self) -> bool {
856        self.checks > 0 || self.source != self.default
857    }
858}
859
860/// Method: the normal quantile of a 95% interval, for the estimates the monitoring shows.
861const Z95: f64 = 1.96;
862
863/// The value model's coefficients with their 95% intervals (unit `coefficient`, logit scale), from `n`
864/// labels, then the spread of the session, lesson and judge effects (unit `logit`, a standard
865/// deviation, shown as a point).
866fn effects(m: &select::policy::Model, n: u32) -> Vec<Estimate> {
867    let p = select::policy::P;
868    if m.beta.len() != p || m.chol.len() != p * p {
869        return Vec::new();
870    }
871    // The covariance is chol · cholᵀ: a coefficient's variance is the sum of squares of its row.
872    let sd = |k: usize| m.chol[k * p..=k * p + k].iter().map(|x| x * x).sum::<f64>().sqrt();
873    let mut out: Vec<Estimate> = (0..p)
874        .map(|k| Estimate {
875            name: select::policy::NAMES[k].into(),
876            unit: "coefficient".into(),
877            mean: m.beta[k],
878            lo: m.beta[k] - Z95 * sd(k),
879            hi: m.beta[k] + Z95 * sd(k),
880            n: u64::from(n),
881        })
882        .collect();
883    for (name, v) in ["session spread", "lesson spread", "judge spread"].into_iter().zip(m.spread) {
884        out.push(Estimate { name: name.into(), unit: "logit".into(), mean: v, lo: v, hi: v, n: u64::from(n) });
885    }
886    out
887}
888
889/// The selection of the store whose lessons file is `lessons`, under `policy` (select.rs `policy`).
890#[must_use]
891pub fn selection(lessons: &Path, policy: select::Policy) -> Selection {
892    let mut s = Selection::of(&select::summary::load(lessons), policy, select::risk_of(lessons));
893    // The store's judge cost lines are in its `state/`, beside its lessons; the judge this environment runs.
894    let current = crate::hook::judge_model(&std::env::var("BORG_PREDICT_MODEL").unwrap_or_default());
895    s.judge = crate::judge_length::lengths(&lessons.with_file_name("state"), &current);
896    s
897}
898
899/// `n` with its thousands grouped (`72,000`).
900fn grouped(n: usize) -> String {
901    let digits = n.to_string();
902    let mut out = String::new();
903    for (i, c) in digits.chars().enumerate() {
904        if i > 0 && (digits.len() - i).is_multiple_of(3) {
905            out.push(',');
906        }
907        out.push(c);
908    }
909    out
910}
911
912/// One judge's effective length as one line (`borg status`): the budget and whether evidence moved it,
913/// the answered share per prompt-size step with what it decided, and the step explored with its chance.
914#[must_use]
915pub fn judge_length_line(l: &crate::judge_length::Length) -> String {
916    use crate::judge_length::Decision;
917    let start = crate::hook::END_START_BUDGET;
918    let how = if l.start {
919        "the start: no decided step moved it".to_string()
920    } else if l.chars > start {
921        format!("measured, up from the start {}", grouped(start))
922    } else {
923        format!("measured, down from the start {}", grouped(start))
924    };
925    let mut parts = vec![format!("{}: session-end budget {} chars ({how})", l.model, grouped(l.chars))];
926    let measured: Vec<String> = l
927        .steps
928        .iter()
929        .filter(|s| s.sent > 0)
930        .map(|s| {
931            let what = match s.decision {
932                Decision::Reference => format!("reference, line {:.0}%", 100.0 * l.line.unwrap_or(0.0)),
933                Decision::Below => "below the reference".into(),
934                Decision::Holds => "holds".into(),
935                Decision::Fails => "fails".into(),
936                Decision::Open => "open".into(),
937            };
938            format!("≤{} {:.0}% of {} [{:.0}–{:.0}%] {what}", grouped(s.upto), 100.0 * s.answered as f64 / s.sent as f64, s.sent, 100.0 * s.lo, 100.0 * s.hi)
939        })
940        .collect();
941    parts.push(if measured.is_empty() { "no judge call measured yet".into() } else { format!("answered by prompt size: {}", measured.join(", ")) });
942    if l.line.is_none() && !measured.is_empty() {
943        parts.push(format!(
944            "no reference yet (the smallest steps with {} lessons in {} calls)",
945            crate::judge_length::REFERENCE_LESSONS,
946            crate::judge_length::MIN_CALLS
947        ));
948    }
949    parts.push(match l.explore {
950        Some(up) if l.steps.iter().any(|s| s.upto == up && s.decision == Decision::Fails) => {
951            format!("exploring the failed ≤{} on {:.0}% of session-end passes", grouped(up), 100.0 * l.psi)
952        }
953        Some(up) => format!("exploring ≤{} on {:.0}% of session-end passes", grouped(up), 100.0 * l.psi),
954        None => "nothing explored".into(),
955    });
956    parts.join(" · ")
957}
958
959/// `s` as one line (`borg status`, `borg top`): what decides (the default named as such, a policy
960/// opted into by its switch), then the checks folded, how often the policy deviated from the fixed
961/// rule, the learned decisions the fixed rule took over per reason (only those that happened), the
962/// question-count test (`test_line`), what the checks measure for the learned policy (placebos), the
963/// monitor's reading (the
964/// auto-revert's evidence) with its weights' effective number and largest weight, the stops' latency
965/// per harness against its stop budget, and what selection spent per session; a budget and its price
966/// only where an engineer set one.
967#[must_use]
968pub fn selection_line(s: &Selection) -> String {
969    let default = s.source == s.default;
970    let head = match s.source {
971        select::REVERTED_SOURCE => "REVERTED to the fixed rule: the monitor saw harm beyond the gate's share (sticky)".to_string(),
972        "learned" => {
973            let opt = if default { "" } else { " (opt-in, BORG_SELECT=learned)" };
974            if s.gate > 0.0 {
975                format!("learned{opt}, gate {:.3} (quality it may still risk below the fixed rule)", s.gate)
976            } else if s.fit.is_empty() || s.allowance <= 0.0 {
977                format!("learned{opt}, cold start: gate 0, every decision the fixed rule's")
978            } else {
979                // The ledger's quantile used the allowance up: a deviation passes only on its own
980                // positive evidence (select/policy.rs `choose`: risk ≤ 0).
981                format!("learned{opt}, gate spent (ledger q05 {:+.3} against allowance {:.3}), deviations need positive evidence", s.ledger, s.allowance)
982            }
983        }
984        "fixed" if default => "the fixed rule (the default; BORG_SELECT=learned opts in to the learned policy)".into(),
985        "fixed" => "the fixed rule (BORG_SELECT=fixed)".into(),
986        "cap" => "the fixed rule with an ask cap (BORG_STOP_ASK_CAP, evaluation only)".into(),
987        "attention" => "attention's cut (BORG_STOP_ATTENTION=on, evaluation only)".into(),
988        other => other.to_string(),
989    };
990    if s.checks == 0 {
991        return format!("{head} · no graded check folded yet");
992    }
993    let mut parts = vec![head, format!("{} checks", s.checks)];
994    if !s.deviations.is_empty() {
995        parts.push(format!("deviated: {}", s.deviations.iter().map(|c| format!("{} {}/{}", c.name, c.part, c.whole)).collect::<Vec<_>>().join(", ")));
996    }
997    let fell: Vec<String> = s.fallbacks.iter().filter(|c| c.part > 0).map(|c| format!("{} {}", c.name, c.part)).collect();
998    if !fell.is_empty() {
999        parts.push(format!("learned decisions the fixed rule took over: {}", fell.join(", ")));
1000    }
1001    if let Some(t) = &s.test {
1002        parts.push(test_line(t));
1003    }
1004    if let Some(m) = &s.measurement {
1005        parts.push(format!("placebos {} drawn, {} judged", m.placebos, m.judged));
1006        if m.tail_shown > 0 {
1007            parts.push(format!("tail {} shown past the pool, {} applied", m.tail_shown, m.tail_applied));
1008        }
1009    }
1010    if let Some(e) = &s.evaluation {
1011        let reading = match e.ucb {
1012            Some(u) => format!("monitor {:+.3}, bound {u:+.3} over {} sessions (below 0 reverts)", e.mean, e.sessions),
1013            None => format!("monitor {:+.3} over {} sessions, no bound yet", e.mean, e.sessions),
1014        };
1015        parts.push(format!("{reading}, ESS {:.0} of {} weighed, largest weight {:.1}× the mean", e.ess, e.units, e.max_weight));
1016    }
1017    if !s.latency.is_empty() {
1018        let secs = |ms: u64| format!("{:.1} s", ms as f64 / 1000.0);
1019        let each: Vec<String> = s
1020            .latency
1021            .iter()
1022            .map(|l| {
1023                let budget = l.budget_ms.map_or_else(String::new, |b| format!(" of {}", secs(b)));
1024                let failed = if l.over > 0 { format!(", {} failed", l.over) } else { String::new() };
1025                format!("{} {}{budget}{failed}", l.harness, secs(l.p95_ms))
1026            })
1027            .collect();
1028        parts.push(format!("stop p95 {}", each.join("; ")));
1029    }
1030    if s.sessions > 0 && !s.spend.is_empty() {
1031        let n = s.sessions as f64;
1032        let (usd, rounds) = s.spend.iter().fold((0.0, 0), |(u, r), x| (u + x.usd, r + x.rounds));
1033        parts.push(format!("${:.2} and {:.1} extra rounds per session ({} sessions)", usd / n, rounds as f64 / n, s.sessions));
1034    }
1035    if let Some(b) = &s.budget {
1036        parts.push(format!("budget {:.0} pool tokens per session: {:.0} read, price {:.2e} per token", b.tokens, b.spent, b.price));
1037    }
1038    parts.join(" · ")
1039}
1040
1041/// The question-count test as one part of the selection line: the checks it ran, its controls, those
1042/// the gate refused, those the summary still holds once compaction dropped some (`kept`, what the
1043/// readout reads), the risk share in force (`set` names the collective's own; 0 says it is off), and
1044/// the readout: when it is due, and once due its ask-against-free estimate.
1045fn test_line(t: &Test) -> String {
1046    let risk = if t.set { format!("risk {:.2} (the collective's)", t.risk) } else { format!("risk {:.2}", t.risk) };
1047    match (t.ran, t.risk <= 0.0) {
1048        (0, true) => return "question-count test off (risk 0)".into(),
1049        (0, false) => return format!("question-count test: no check yet, {risk}"),
1050        _ => {}
1051    }
1052    let kept = if t.kept < t.ran { format!(", {} still in the summary", t.kept) } else { String::new() };
1053    let head =
1054        format!("question-count test {} drawn ({} control, {} refused by the gate{kept}) in {} sessions, {risk}", t.ran, t.control, t.refused, t.sessions);
1055    match (&t.ask_free, t.due) {
1056        (Some(e), true) => format!("{head}; readout: grade 2 asked − handed over {:+.1} [{:+.1}, {:+.1}] points changed", e.mean, e.lo, e.hi),
1057        (None, true) => format!("{head}; readout due, no judged grade 2 on both routes yet"),
1058        (_, false) => format!("{head}; readout after {} checks in {} sessions, or {} sessions", t.due_checks, t.due_min_sessions, t.due_sessions),
1059    }
1060}
1061
1062/// `render` with the collective's selection line under it, when `borg top` shows one (`Selection::shown`).
1063#[must_use]
1064pub fn render_with(name: &str, share: Share, drones: &[Drone], inbox: &Inbox, select: Option<&Selection>, now: i64) -> String {
1065    let mut s = render(name, share, drones, inbox, now);
1066    if let Some(sel) = select.filter(|s| s.shown()) {
1067        s.push_str(&format!("  selection: {}\n", selection_line(sel)));
1068    }
1069    s
1070}
1071
1072/// The line over the collectives when borg itself needs the engineer, none otherwise: a newer release
1073/// than this borg (`checked`, the last release check; ignored while `check` is off,
1074/// `BORG_UPDATE_CHECK=off`), then per store in `stores` (only one with `mesh/`) each member nothing
1075/// syncs with, and each member on an older protocol with what it misses (protocol.rs `NOTES`). A
1076/// member on this node's protocol, even on a newer borg, is no news here (`borg status` lists it).
1077pub fn version_line<'a>(stores: impl IntoIterator<Item = &'a Path>, checked: Option<&update::Checked>, check: bool) -> Option<String> {
1078    let mut parts: Vec<String> = Vec::new();
1079    if let Some(l) = checked.and_then(|c| c.latest.as_deref()).filter(|l| check && update::newer(l, update::VERSION)) {
1080        parts.push(format!("borg {l} is released, this is {}: borg update", update::VERSION));
1081    }
1082    let aliases = collective::aliases();
1083    let ours = protocol::current();
1084    for home in stores.into_iter().filter(|h| h.join("mesh").is_dir()) {
1085        let versions = protocol::versions(home);
1086        let mismatches = protocol::mismatches(home);
1087        let name = |key: &str| aliases.display(&protocol::member(key, versions.get(key)));
1088        for (key, m) in &mismatches {
1089            parts.push(format!("nothing syncs with {} ({}, this node {})", name(key), m.theirs, m.ours));
1090        }
1091        for (key, v) in versions.iter().filter(|(k, v)| v.protocol < ours && !mismatches.contains_key(*k)) {
1092            parts.push(format!("{} on /{} misses {}", name(key), v.protocol, protocol::misses(v.protocol, ours)));
1093        }
1094    }
1095    parts.dedup();
1096    (!parts.is_empty()).then(|| parts.join(" · "))
1097}
1098
1099/// The collectives to show: the one `dir` routes to, or every one on this machine.
1100fn routes(dir: Option<&str>) -> Result<Vec<Route>, String> {
1101    let cfg = collective::load()?;
1102    let base = learn::home();
1103    Ok(match dir {
1104        Some(d) => vec![collective::route(cfg.as_ref(), d, &base)].into_iter().filter(|r| *r != Route::Off).collect(),
1105        None => install::collectives(cfg.as_ref(), &base),
1106    })
1107}
1108
1109/// Every collective to show with its drones, and the line on borg itself over them (`version_line`,
1110/// over every collective `dir` routes to, shown or not) (top_live.rs).
1111pub fn gather(dir: Option<&str>, now: i64) -> Result<(Vec<crate::top_live::Coll>, Option<String>), String> {
1112    let mut out = Vec::new();
1113    let routes = routes(dir)?;
1114    let stores: Vec<&Path> = routes
1115        .iter()
1116        .filter_map(|r| match r {
1117            Route::Collective { home, .. } => Some(home.as_path()),
1118            Route::Off => None,
1119        })
1120        .collect();
1121    let version = version_line(stores, update::checked().as_ref(), update::on());
1122    for r in &routes {
1123        let Route::Collective { name, home, share } = r else { continue };
1124        presence::prune(home, now);
1125        let mut ds = drones(home, collective::may_sync(r), now);
1126        for d in &mut ds {
1127            d.p.engineer = collective::aliases().display(&d.p.engineer);
1128        }
1129        let inbox = inbox(home, &ds, now);
1130        if ds.is_empty() && inbox.recent.is_empty() && dir.is_none() {
1131            continue;
1132        }
1133        // Every frame: the cache and its tail, never a rebuild of the whole log (select/summary.rs `peek`);
1134        // the checks' hooks keep the cache current.
1135        let lessons = home.join("lessons.jsonl");
1136        let summary = select::summary::peek(&lessons);
1137        let select = Some(Selection::of(&summary, select::policy(), select::risk_of(&lessons))).filter(Selection::shown).map(|s| selection_line(&s));
1138        out.push(crate::top_live::Coll { name: name.clone(), home: home.clone(), share: *share, drones: ds, inbox, select });
1139    }
1140    Ok((out, version))
1141}
1142
1143/// The printed screen, with the line on borg itself first (`version_line`; not in `--json`).
1144fn screen(dir: Option<&str>, json_out: bool, now: i64) -> Result<String, String> {
1145    let routes = routes(dir)?;
1146    let text = screen_of(&routes, dir.is_some(), &engineer_dir(dir), msg::caller, json_out, now);
1147    let stores = routes.iter().filter_map(|r| match r {
1148        Route::Collective { home, .. } => Some(home.as_path()),
1149        Route::Off => None,
1150    });
1151    Ok(match version_line(stores, update::checked().as_ref(), update::on()).filter(|_| !json_out) {
1152        Some(v) => format!("{v}\n\n{text}"),
1153        None => text,
1154    })
1155}
1156
1157/// The printed screen (or `--json`) of the collectives `routes`, `one` when `--dir` named the one to
1158/// show (shown even with nothing in it); when `caller` finds no session (`engineer_looks`), the text
1159/// screen acknowledges the messages to the engineer of the repo at `cwd` whose text it prints
1160/// (`prints_text`, `acknowledge`).
1161fn screen_of(
1162    routes: &[Route],
1163    one: bool,
1164    cwd: &str,
1165    caller: impl Fn(&[Drone]) -> Result<Option<(String, &'static str)>, String>,
1166    json_out: bool,
1167    now: i64,
1168) -> String {
1169    let mut text = String::new();
1170    let mut all = Vec::new();
1171    // Every collective's drones, and what each store's text screen printed: whether the engineer is
1172    // the one looking, and so acknowledges it, is known once all are gathered.
1173    let (mut gathered, mut printed) = (Vec::new(), Vec::new());
1174    for r in routes {
1175        let Route::Collective { name, home, share } = r else { continue };
1176        presence::prune(home, now);
1177        let mut ds = drones(home, collective::may_sync(r), now);
1178        // An engineer's several emails show as their name (collective.rs "Engineers"), peers' too.
1179        for d in &mut ds {
1180            d.p.engineer = collective::aliases().display(&d.p.engineer);
1181        }
1182        let inbox = inbox(home, &ds, now);
1183        let sel = selection(&home.join("lessons.jsonl"), select::policy());
1184        if json_out {
1185            let mut v = to_json_with(name, *share, &ds, &inbox);
1186            v["select"] = json!(sel);
1187            all.push(v);
1188        } else if !ds.is_empty() || !inbox.recent.is_empty() || one {
1189            text.push_str(&render_with(name, *share, &ds, &inbox, Some(&sel), now));
1190            text.push('\n');
1191            printed.push((home, inbox.recent.into_iter().filter(|m| prints_text(&ds, &inbox.here, m)).collect::<Vec<_>>()));
1192        }
1193        gathered.push(ds);
1194    }
1195    if !printed.is_empty() && engineer_looks(caller, gathered.iter().map(Vec::as_slice)) {
1196        let mut done = HashSet::new();
1197        for (home, recent) in &printed {
1198            acknowledge(home, cwd, &recent.iter().collect::<Vec<_>>(), &mut done, now);
1199        }
1200    }
1201    if json_out {
1202        return serde_json::to_string_pretty(&all).unwrap_or_default() + "\n";
1203    }
1204    if text.is_empty() {
1205        text = "no drones (borg top lists sessions whose hooks ran in the last day)\n".into();
1206    }
1207    text
1208}
1209
1210pub fn main(args: &[&str]) -> i32 {
1211    let (mut dir, mut once, mut json_out) = (None, false, false);
1212    let mut it = args.iter();
1213    while let Some(a) = it.next() {
1214        match *a {
1215            "--dir" if dir.is_none() => match it.next() {
1216                Some(d) => dir = Some(*d),
1217                None => return usage("--dir needs a value"),
1218            },
1219            "--once" => once = true,
1220            "--json" => json_out = true,
1221            a => return usage(&format!("unknown flag {a}")),
1222        }
1223    }
1224    // SAFETY: isatty only inspects the descriptor.
1225    if !json_out && !once && unsafe { libc::isatty(1) } == 1 {
1226        return crate::top_live::run(dir);
1227    }
1228    match screen(dir, json_out, learn::now_ms()) {
1229        Ok(s) => {
1230            print!("{s}");
1231            0
1232        }
1233        Err(e) => {
1234            eprintln!("borg top: {e}");
1235            1
1236        }
1237    }
1238}
1239
1240fn usage(why: &str) -> i32 {
1241    eprintln!("borg top: {why}\nusage: borg top [--once] [--json] [--dir DIR]");
1242    2
1243}
1244
1245#[cfg(test)]
1246pub(crate) fn tests_fixture() -> Vec<Drone> {
1247    tests::fixture()
1248}
1249
1250#[cfg(test)]
1251pub(crate) fn tests_inbox() -> Inbox {
1252    tests::inbox_fixture()
1253}
1254
1255#[cfg(test)]
1256mod tests {
1257    use super::*;
1258
1259    fn p(session: &str, engineer: &str, started: i64, ms: i64, event: &str, prompt: &str, files: &[&str]) -> Presence {
1260        Presence {
1261            session: session.into(),
1262            harness: "claude".into(),
1263            engineer: engineer.into(),
1264            repo: "borg-ai".into(),
1265            collective: "corp".into(),
1266            prompt: prompt.into(),
1267            files: files.iter().map(|f| f.to_string()).collect(),
1268            event: event.into(),
1269            ms,
1270            started,
1271            pid: None,
1272        }
1273    }
1274
1275    const NOW: i64 = 10_000_000;
1276
1277    pub(crate) fn fixture() -> Vec<Drone> {
1278        let local = vec![
1279            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"]),
1280            p("cccc3333-x", "alice@x", 300, NOW - presence::IDLE_MS - 60_000, "stop", "write the release notes for the mesh daemon and more words", &[]),
1281        ];
1282        let remote = vec![presence::Remote {
1283            p: p("bbbb2222-y", "bob@y", 200, NOW - 120_000, "stop", "review PR 42", &["README.md"]),
1284            key: "beefcafe".repeat(8),
1285            sig: String::new(),
1286        }];
1287        assemble(local, remote, NOW, |_| true, |p| p.session.starts_with("aaaa"))
1288    }
1289
1290    fn message(by: &str, name: &str, to: &str, kind: msg::Kind, ms: i64, text: &str) -> Message {
1291        message_on("n1", by, name, to, kind, ms, text)
1292    }
1293
1294    fn message_on(node: &str, by: &str, name: &str, to: &str, kind: msg::Kind, ms: i64, text: &str) -> Message {
1295        let from = msg::Sender { by: by.into(), name: name.into(), harness: "claude".into(), node: node.into() };
1296        Message::new(kind, &from, to, None, ms, text)
1297    }
1298
1299    pub(crate) fn inbox_fixture() -> Inbox {
1300        Inbox {
1301            unread: [("aaaa1111-x".to_string(), 2), ("cccc3333-x".to_string(), 0)].into_iter().collect(),
1302            recent: vec![
1303                message("cccc3333-x", "Three of Three", "aaaa1111-x", msg::ASK, NOW - 60_000, "which   test\nflakes?"),
1304                message("engineer:alice@x", "alice@x", msg::ALL, msg::TELL, NOW - 120_000, "release at five"),
1305                message_on(&"feed".repeat(16), "gone-session", "Six of Six", "zzzz9999-q", msg::TELL, NOW - 180_000, "done"),
1306            ],
1307            day: 3,
1308            here: "n1".into(),
1309            listen: [("aaaa1111-x".to_string(), crate::listen::Status::Waits), ("cccc3333-x".to_string(), crate::listen::Status::Listens)]
1310                .into_iter()
1311                .collect(),
1312        }
1313    }
1314
1315    #[test]
1316    fn renders_one_screen_per_collective() {
1317        let out = render("corp", Share::Team, &fixture(), &inbox_fixture(), NOW);
1318        let expected = "\
1319corp (team): 3 drone(s), 2 active, 1 remote
1320  DRONE              SESSION  ENGINEER                 REPO           STATE    WAKE     IDLE UNREAD  DOING                                    FILES
1321  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
1322  Two of Three       bbbb222… bob@y @beefca            borg-ai        waiting  -          2m      -  review PR 42                             README.md
1323  Three of Three     cccc333… alice@x                  borg-ai        idle     on        31m      0  write the release notes for the mesh da…
1324  messages: 3 in the last day, 2 unread
1325      1m  Three of Three asks -> One of Three: which test flakes?
1326      2m  alice@x -> collective: release at five
1327      3m  Six of Six @feedfeedfeed -> zzzz9999: done
1328";
1329        assert_eq!(out, expected, "\n{out}");
1330        let none = Inbox::default();
1331        assert_eq!(render("private", Share::Local, &[], &none, NOW), "private (local, never leaves this machine): 0 drone(s), 0 active, 0 remote\n");
1332    }
1333
1334    /// A store's selection after 480 checks: the learned policy opted into, with gate budget, its route
1335    /// deviations, the placebos the checks drew, the monitor's reading and weights, spend from both
1336    /// check channels.
1337    fn learned() -> Selection {
1338        let spend = |source: &str, rounds, usd| Spend { source: source.into(), calls: 1, rounds, usd };
1339        let evaluation = Evaluation { sessions: 12, mean: 0.004, ucb: Some(0.031), units: 300, ess: 180.0, max_weight: 6.5, truncated: 3 };
1340        Selection {
1341            policy: "learned",
1342            default: "fixed",
1343            source: "learned",
1344            gate: 0.12,
1345            checks: 480,
1346            sessions: 12,
1347            deviations: vec![Count { name: "route".into(), part: 5, whole: 400 }],
1348            spend: vec![spend("stop", 4, 3.0), spend("live", 0, 2.04)],
1349            evaluation: Some(evaluation),
1350            measurement: Some(Measurement { placebos: 40, judged: 31, tail_shown: 12, tail_applied: 3 }),
1351            test: Some(Test {
1352                risk: 0.1,
1353                ran: 30,
1354                control: 9,
1355                refused: 4,
1356                sessions: 6,
1357                kept: 30,
1358                due_checks: 300,
1359                due_sessions: 30,
1360                due_min_sessions: 15,
1361                ..Test::default()
1362            }),
1363            ..Selection::default()
1364        }
1365    }
1366
1367    /// The judge's effective length in a store's selection (`borg status`, borg web, `borg top --json`):
1368    /// read from the store's judge cost lines per model, the current judge listed even without any; its
1369    /// line names the budget, each measured step's share with its decision, and what is explored.
1370    #[test]
1371    fn the_judges_effective_length_is_read_from_the_stores_cost_lines_and_shown() {
1372        let home = std::env::temp_dir().join(format!("borg-top-judge-length-{}", std::process::id()));
1373        let _ = std::fs::remove_dir_all(&home);
1374        std::fs::create_dir_all(home.join("state")).unwrap();
1375        let line = |chars: usize, n: u64, a: u64| {
1376            format!(
1377                "judge call=1 pending={n} resolved={a} chars={chars} returned= unmatched= verdicts=changed:{a},ignored:0,not_arisen:0,consequence_remains:0,unclear:{} reason_chars=1 model=x:big at=end\n",
1378                n - a
1379            )
1380            .repeat(10)
1381        };
1382        std::fs::write(home.join("state/s.cost"), line(30_000, 40, 20) + &line(70_000, 30, 15) + &line(100_000, 30, 15)).unwrap();
1383        let sel = selection(&home.join("lessons.jsonl"), select::policy());
1384        let big = sel.judge.iter().find(|l| l.model == "x:big").expect("the store's judge");
1385        assert_eq!((big.chars, big.start, big.explore), (108_000, false, Some(162_000)));
1386        assert_eq!(
1387            judge_length_line(big),
1388            "x:big: session-end budget 108,000 chars (measured, up from the start 72,000) · answered by prompt size: ≤32,000 50% of 400 [45–55%] reference, line 42%, ≤72,000 50% of 300 [44–56%] holds, ≤108,000 50% of 300 [44–56%] holds · exploring ≤162,000 on 20% of session-end passes"
1389        );
1390        let v = serde_json::to_value(&sel).unwrap();
1391        assert_eq!(v["judge"].as_array().map(|a| a.iter().any(|j| j["model"] == "x:big" && j["chars"] == 108_000)), Some(true), "{v}");
1392        let none = crate::judge_length::Length::of("y:new", &crate::judge_length::Steps::new());
1393        assert_eq!(
1394            judge_length_line(&none),
1395            "y:new: session-end budget 72,000 chars (the start: no decided step moved it) · no judge call measured yet · exploring ≤108,000 on 20% of session-end passes"
1396        );
1397        assert_eq!((grouped(0), grouped(999), grouped(1_000), grouped(1_234_567)), ("0".into(), "999".into(), "1,000".into(), "1,234,567".into()));
1398        let _ = std::fs::remove_dir_all(&home);
1399    }
1400
1401    #[test]
1402    fn the_selection_line_says_what_decides_and_prices_spend_only_under_a_budget() {
1403        use select::Policy;
1404        use select::summary::Summary;
1405        let fresh = |policy, default| Selection { default, ..Selection::of(&Summary::default(), policy, None) };
1406        // The fixed rule as the default names itself so and the opt-in; the learned policy opted into says so.
1407        let fixed = fresh(Policy::Fixed, "fixed");
1408        assert_eq!(selection_line(&fixed), "the fixed rule (the default; BORG_SELECT=learned opts in to the learned policy) · no graded check folded yet");
1409        assert!(!fixed.shown(), "the default with nothing folded: nothing to show on the drones screen");
1410        let opted = fresh(Policy::Learned, "fixed");
1411        assert_eq!(
1412            selection_line(&opted),
1413            "learned (opt-in, BORG_SELECT=learned), cold start: gate 0, every decision the fixed rule's · no graded check folded yet"
1414        );
1415        assert!(opted.shown(), "a switch decides: shown");
1416        // Where the learned policy is the default, the fixed rule is the switch.
1417        assert_eq!(selection_line(&fresh(Policy::Fixed, "learned")), "the fixed rule (BORG_SELECT=fixed) · no graded check folded yet");
1418        assert!(!fresh(Policy::Learned, "learned").shown() && fresh(Policy::Fixed, "learned").shown());
1419        let d = select::precedence(None, None, false);
1420        assert_eq!(Selection::of(&Summary::default(), Policy::Cap(2), None).default, d.name(), "the default is select.rs's, no switch set");
1421        assert!(!Selection::of(&Summary::default(), d, None).shown());
1422        let mut s = learned();
1423        let line = selection_line(&s);
1424        assert_eq!(
1425            line,
1426            "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)"
1427        );
1428        assert!(!line.contains("budget") && !line.contains("price"), "no budget set: spend measured, never priced: {line}");
1429        let mut early = learned();
1430        early.evaluation = Some(Evaluation { ucb: None, sessions: 3, units: 40, ess: 12.4, max_weight: 3.0, ..Evaluation::default() });
1431        let line = selection_line(&early);
1432        assert!(line.contains(" · monitor +0.000 over 3 sessions, no bound yet, ESS 12 of 40 weighed, largest weight 3.0× the mean · "), "{line}");
1433        early.latency = vec![
1434            Latency { harness: "codex".into(), checks: 20, p50_ms: 10_000, p95_ms: 19_000, budget_ms: Some(30_000), over: 1 },
1435            Latency { harness: "claude".into(), checks: 1, p50_ms: 4_000, p95_ms: 4_000, budget_ms: None, over: 0 },
1436        ];
1437        let line = selection_line(&early);
1438        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}");
1439        s.budget = Some(Budget { tokens: 30_000.0, spent: 24_000.0, price: 0.0002 });
1440        assert!(selection_line(&s).ends_with(" · budget 30000 pool tokens per session: 24000 read, price 2.00e-4 per token"), "{}", selection_line(&s));
1441        s.source = select::REVERTED_SOURCE;
1442        assert!(selection_line(&s).starts_with("REVERTED to the fixed rule"), "{}", selection_line(&s));
1443    }
1444
1445    /// A gate of 0 is a cold start only without a fit or an allowance; a positive allowance the ledger's
1446    /// quantile used up is a spent gate, where a deviation needs positive evidence (a team store on
1447    /// 2026-10-31: allowance 0.076, ledger q05 −0.102, 74 evidence and test decisions that day).
1448    #[test]
1449    fn a_spent_gate_is_not_a_cold_start() {
1450        use select::Policy;
1451        use select::summary::{Gate, Summary};
1452        let line = |s: &Summary| selection_line(&Selection { default: "learned", ..Selection::of(s, Policy::Learned, None) });
1453        let mut s = Summary::default();
1454        s.fit.key = "3556601-bc6d6191".into();
1455        // A fit whose ledger accrued nothing yet: cold start.
1456        assert!(line(&s).starts_with("learned, cold start: gate 0, every decision the fixed rule's"), "{}", line(&s));
1457        s.gate = Gate { gain: vec![-0.1018; select::policy::DRAWS], allowance: 0.0760, ..Gate::default() };
1458        let spent = Selection::of(&s, Policy::Learned, None);
1459        assert_eq!((spent.gate, spent.allowance, spent.ledger), (0.0, 0.0760, -0.1018));
1460        assert!(line(&s).starts_with("learned, gate spent (ledger q05 -0.102 against allowance 0.076), deviations need positive evidence · "), "{}", line(&s));
1461        // No fit: whatever the ledger says, the prior decides as the fixed rule.
1462        s.fit.key.clear();
1463        assert!(line(&s).starts_with("learned, cold start: gate 0"), "{}", line(&s));
1464        // An allowance the ledger leaves room in: the gate as before.
1465        s.fit.key = "3556601-bc6d6191".into();
1466        s.gate.gain = vec![-0.05; select::policy::DRAWS];
1467        assert!(line(&s).starts_with("learned, gate 0.026 (quality it may still risk below the fixed rule)"), "{}", line(&s));
1468        // Another policy shows neither part.
1469        let fixed = Selection::of(&s, Policy::Fixed, None);
1470        assert_eq!((fixed.allowance, fixed.ledger), (0.0, 0.0));
1471    }
1472
1473    /// `borg status`'s line, read from a store: one with nothing folded decides by the fixed rule under
1474    /// whichever policy; a switch names itself; no budget is mentioned.
1475    #[test]
1476    fn a_store_says_how_its_lessons_are_selected() {
1477        use select::Policy;
1478        let home = std::env::temp_dir().join(format!("borg-top-select-{}", std::process::id()));
1479        let _ = std::fs::remove_dir_all(&home);
1480        std::fs::create_dir_all(&home).unwrap();
1481        let lessons = home.join("lessons.jsonl");
1482        std::fs::write(&lessons, "").unwrap();
1483        let line = |lessons: &Path, policy| selection_line(&selection(lessons, policy));
1484        assert!(line(&lessons, Policy::Learned).ends_with("cold start: gate 0, every decision the fixed rule's · no graded check folded yet"));
1485        assert!(line(&lessons, Policy::Fixed).starts_with("the fixed rule ("), "{}", line(&lessons, Policy::Fixed));
1486        assert!(line(&lessons, Policy::Cap(2)).contains("(BORG_STOP_ASK_CAP, evaluation only)"));
1487        let absent = home.join("absent").join("lessons.jsonl");
1488        assert!(line(&absent, Policy::Attention).contains("(BORG_STOP_ATTENTION=on, evaluation only)"), "no store: the prior");
1489        let _ = std::fs::remove_dir_all(&home);
1490    }
1491
1492    /// `borg top`'s screen keeps `render`'s text and adds the selection line under it only when shown.
1493    #[test]
1494    fn the_selection_line_follows_the_screen_when_shown() {
1495        let (ds, inbox) = (fixture(), inbox_fixture());
1496        let plain = render("corp", Share::Team, &ds, &inbox, NOW);
1497        assert_eq!(render_with("corp", Share::Team, &ds, &inbox, None, NOW), plain);
1498        let cold = Selection::of(&select::summary::Summary::default(), select::precedence(None, None, false), None);
1499        assert_eq!(render_with("corp", Share::Team, &ds, &inbox, Some(&cold), NOW), plain, "the default at cold start: no line");
1500        let s = learned();
1501        assert_eq!(render_with("corp", Share::Team, &ds, &inbox, Some(&s), NOW), format!("{plain}  selection: {}\n", selection_line(&s)));
1502    }
1503
1504    /// The fields borg web's Conscience tab reads (ui api.rs `parse_select`); a budget is null unless set.
1505    #[test]
1506    fn the_selection_serialises_every_section_for_the_page() {
1507        let v = serde_json::to_value(learned()).unwrap();
1508        let mut keys: Vec<&str> = v.as_object().unwrap().keys().map(String::as_str).collect();
1509        keys.sort_unstable();
1510        let want = "allowance at budget checks declaration default delivery deviations effects evaluation fallbacks fit gate half_life judge labels latency ledger measurement policy pool sessions source spend test";
1511        assert_eq!(keys, want.split(' ').collect::<Vec<_>>());
1512        assert_eq!((&v["budget"], &v["spend"][0]["source"], &v["deviations"][0]["whole"]), (&Value::Null, &json!("stop"), &json!(400)));
1513        let mut keys: Vec<&str> = v["evaluation"].as_object().unwrap().keys().map(String::as_str).collect();
1514        keys.sort_unstable();
1515        assert_eq!(keys, ["ess", "max_weight", "mean", "sessions", "truncated", "ucb", "units"]);
1516        assert_eq!((&v["default"], &v["measurement"]), (&json!("fixed"), &json!({"placebos": 40, "judged": 31, "tail_shown": 12, "tail_applied": 3})));
1517        let mut keys: Vec<&str> = v["test"].as_object().unwrap().keys().map(String::as_str).collect();
1518        keys.sort_unstable();
1519        let want = "ask_free by_k control dilution due due_checks due_min_sessions due_sessions kept ran refused risk sessions set";
1520        assert_eq!(keys, want.split(' ').collect::<Vec<_>>());
1521    }
1522
1523    /// Label health's judged share of a route counts the applies a verdict can come of: a free apply of
1524    /// a lesson its session already had (an older fixed rule's, delivery dropped it as given before)
1525    /// is out of it, as it is out of the summary's delivery tallies; a question of a lesson handed over
1526    /// and not acted on is delivered and judged, so in.
1527    #[test]
1528    fn the_judged_share_leaves_out_what_its_session_already_had() {
1529        use crate::outcome::{self, Outcome};
1530        use select::summary::{Summary, fold};
1531        let rec = |kind: &str, check: &str, channel: &str, ms: i64| Outcome {
1532            kind: kind.into(),
1533            session: "s".into(),
1534            lessons: vec!["A".into()],
1535            check: check.into(),
1536            channel: channel.into(),
1537            ms,
1538            ..Default::default()
1539        };
1540        let graded = |check: &str, ms: i64| Outcome {
1541            grades: vec![2],
1542            ranks: vec![0],
1543            applied: 1,
1544            free: vec!["A".into()],
1545            pf: vec![1.0],
1546            policy: "fixed".into(),
1547            shown: 20,
1548            tokens: 6_000,
1549            ..rec(outcome::GRADED, check, "live", ms)
1550        };
1551        let log = [
1552            graded("c1", 10),
1553            Outcome { route: outcome::ROUTE_FREE.into(), ..rec(outcome::DELIVERY, "c1", "", 11) },
1554            Outcome { judge: 2, ..rec(outcome::CHANGED, "c1", "queued", 12) },
1555            graded("c2", 20),
1556            Outcome { route: outcome::ROUTE_DROPPED_DONE.into(), ..rec(outcome::DELIVERY, "c2", "", 21) },
1557            // B, handed over early and not acted on, is asked at c3: delivered, then judged.
1558            Outcome { lessons: vec!["B".into()], ..rec(outcome::ASKED, "", "early", 25) },
1559            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) },
1560            Outcome { lessons: vec!["B".into()], route: outcome::ROUTE_ASKED.into(), ..rec(outcome::DELIVERY, "c3", "", 31) },
1561            Outcome { lessons: vec!["B".into()], judge: 2, ..rec(outcome::CHANGED, "c3", "live", 32) },
1562        ];
1563        let text: String = log.iter().map(|o| format!("{}\n", serde_json::to_string(o).unwrap())).collect();
1564        let s = fold(&text, Summary::default());
1565        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");
1566        let sel = Selection::of(&s, select::Policy::Fixed, None);
1567        let row = |name: &str| sel.labels.iter().find(|c| c.name == name).map(|c| (c.part, c.whole));
1568        assert_eq!(row("judged, free"), Some((1, 1)), "c1's delivered and judged; c2's had, out: {:?}", sel.labels);
1569        assert_eq!(row("judged, asked"), Some((1, 1)), "c3's question of what B's session had, judged: {:?}", sel.labels);
1570    }
1571
1572    /// The verdict shares count the judge's answers from `predict::PURE_UNCLEAR_FROM` on, the pending ones
1573    /// too, overall and per route: an older `asked_unclear` (which also held a remaining consequence) is
1574    /// in none of them; an apply's latest answer is its own.
1575    #[test]
1576    fn the_verdict_shares_read_judge_three_answers_per_route() {
1577        use crate::outcome::{self, Outcome};
1578        use select::summary::{Summary, fold};
1579        let rec = |kind: &str, l: &str, check: &str, channel: &str, ms: i64, judge: u8| Outcome {
1580            kind: kind.into(),
1581            session: "s".into(),
1582            lessons: vec![l.into()],
1583            check: check.into(),
1584            channel: channel.into(),
1585            ms,
1586            judge,
1587            ..Default::default()
1588        };
1589        let graded = |check: &str, ms: i64, chosen: &[&str], free: &[&str], placebo: &[&str]| {
1590            let lessons: Vec<String> = chosen.iter().chain(free).chain(placebo).map(|l| l.to_string()).collect();
1591            let n = lessons.len();
1592            Outcome {
1593                grades: vec![2; n],
1594                ranks: (0..n as u16).collect(),
1595                applied: n as u16,
1596                chosen: chosen.iter().map(|l| l.to_string()).collect(),
1597                free: free.iter().map(|l| l.to_string()).collect(),
1598                placebo: placebo.iter().map(|l| l.to_string()).collect(),
1599                p: lessons.iter().map(|l| f64::from(u8::from(chosen.contains(&l.as_str())))).collect(),
1600                pf: lessons.iter().map(|l| f64::from(u8::from(free.contains(&l.as_str())))).collect(),
1601                psi: if placebo.is_empty() { 0.0 } else { 0.5 },
1602                policy: "fixed".into(),
1603                lessons,
1604                ..rec(outcome::GRADED, "", check, "stop", ms, 0)
1605            }
1606        };
1607        let deliver = |l: &str, check: &str, route: &str, ms: i64| Outcome { route: route.into(), ..rec(outcome::DELIVERY, l, check, "", ms, 0) };
1608        let log = [
1609            // Before 0.7.5: an unclear free delivery, in no share.
1610            graded("c0", 5, &[], &["O"], &[]),
1611            deliver("O", "c0", outcome::ROUTE_FREE, 6),
1612            rec(outcome::UNCLEAR, "O", "c0", "queued", 7, 2),
1613            graded("c1", 10, &["A", "B"], &["F", "G", "H"], &["P", "Q"]),
1614            deliver("A", "c1", outcome::ROUTE_ASKED, 11),
1615            deliver("B", "c1", outcome::ROUTE_ASKED, 11),
1616            deliver("F", "c1", outcome::ROUTE_FREE, 11),
1617            deliver("G", "c1", outcome::ROUTE_FREE, 11),
1618            deliver("H", "c1", outcome::ROUTE_FREE, 11),
1619            rec(outcome::CHANGED, "A", "c1", "stop", 20, 3),
1620            rec(outcome::UNCLEAR, "B", "c1", "stop", 20, 3),
1621            rec(outcome::UNCLEAR, "F", "c1", "queued", 20, 3),
1622            Outcome { at: outcome::AT_END.into(), ..rec(outcome::NOT_ARISEN, "F", "c1", "queued", 30, 3) },
1623            rec(outcome::REMAINS, "G", "c1", "queued", 20, 3),
1624            rec(outcome::UNCLEAR, "H", "c1", "queued", 20, 3),
1625            rec(outcome::UNCLEAR, "P", "c1", outcome::PLACEBO, 20, 3),
1626            rec(outcome::IGNORED, "Q", "c1", outcome::PLACEBO, 20, 3),
1627        ];
1628        let text: String = log.iter().map(|o| format!("{}\n", serde_json::to_string(o).unwrap())).collect();
1629        let sel = Selection::of(&fold(&text, Summary::default()), select::Policy::Fixed, None);
1630        let row = |name: &str| sel.labels.iter().find(|c| c.name == name).map(|c| (c.part, c.whole));
1631        assert_eq!(row("unclear"), Some((3, 7)), "B, H and P of seven judge-3 answers, O's older one out: {:?}", sel.labels);
1632        assert_eq!(row("not arisen"), Some((1, 7)));
1633        assert_eq!(row("consequence remains"), Some((1, 7)));
1634        assert_eq!(row("unclear, asked"), Some((1, 2)));
1635        assert_eq!(row("unclear, free"), Some((1, 3)), "F's end verdict replaced its unclear: {:?}", sel.labels);
1636        assert_eq!(row("unclear, placebo"), Some((1, 2)));
1637    }
1638
1639    /// The placebos a fixed-rule store's checks drew, and those judged, as the summary folds them:
1640    /// a placebo judged ignored counts, one without a verdict is drawn only, a free lesson's verdict is
1641    /// not a placebo's; a tail sample's lessons shown and applied; a store with nothing folded has no
1642    /// measurement.
1643    #[test]
1644    fn the_measurement_counts_the_placebos_the_fixed_rule_drew_and_judged() {
1645        use crate::outcome::{self, Outcome};
1646        use select::summary::{Summary, fold};
1647        let rec = |kind: &str, lessons: &[&str], check: &str, channel: &str, ms: i64| Outcome {
1648            kind: kind.into(),
1649            session: "s".into(),
1650            lessons: lessons.iter().map(|l| l.to_string()).collect(),
1651            check: check.into(),
1652            channel: channel.into(),
1653            ms,
1654            ..Default::default()
1655        };
1656        let graded = Outcome {
1657            grades: vec![1, 1, 2],
1658            ranks: vec![0, 1, 2],
1659            applied: 3,
1660            free: vec!["C".into()],
1661            placebo: vec!["A".into(), "B".into()],
1662            psi: 0.5,
1663            policy: "fixed".into(),
1664            shown: 20,
1665            tokens: 6_000,
1666            ..rec(outcome::GRADED, &["A", "B", "C"], "c1", "stop", 10)
1667        };
1668        // A pool the budget cut, its tail sample of two past it, T2 of them applied.
1669        let tailed = Outcome {
1670            grades: vec![1],
1671            ranks: vec![0],
1672            applied: 1,
1673            policy: "fixed".into(),
1674            shown: 20,
1675            tokens: 6_000,
1676            tail: vec!["T1".into(), "T2".into()],
1677            pt: 0.5,
1678            fill: vec![21, 25, 21],
1679            ..rec(outcome::GRADED, &["T2"], "c2", "stop", 14)
1680        };
1681        let log = [
1682            graded,
1683            tailed,
1684            Outcome { route: outcome::ROUTE_PLACEBO.into(), ..rec(outcome::DELIVERY, &["A"], "c1", "", 11) },
1685            Outcome { route: outcome::ROUTE_PLACEBO.into(), ..rec(outcome::DELIVERY, &["B"], "c1", "", 11) },
1686            Outcome { judge: 2, ..rec(outcome::IGNORED, &["A"], "c1", outcome::PLACEBO, 12) },
1687            Outcome { judge: 2, ..rec(outcome::CHANGED, &["C"], "c1", "queued", 13) },
1688        ];
1689        let text: String = log.iter().map(|o| format!("{}\n", serde_json::to_string(o).unwrap())).collect();
1690        let sel = Selection::of(&fold(&text, Summary::default()), select::Policy::Fixed, None);
1691        assert_eq!(sel.measurement, Some(Measurement { placebos: 2, judged: 1, tail_shown: 2, tail_applied: 1 }), "{sel:?}");
1692        assert!(selection_line(&sel).contains(" · 2 checks · placebos 2 drawn, 1 judged · tail 2 shown past the pool, 1 applied"), "{}", selection_line(&sel));
1693        assert_eq!(Selection::of(&Summary::default(), select::Policy::Fixed, None).measurement, None, "nothing folded: not measured");
1694    }
1695
1696    /// The learned decisions the fixed rule took over, per reason, as the summary folds them from the
1697    /// checks' records: every reason a row of the graded checks folded, the line naming those that
1698    /// happened; nothing folded, no rows (not measured), and none happened, nothing on the line.
1699    #[test]
1700    fn the_selection_counts_the_learned_decisions_the_fixed_rule_took_over_by_reason() {
1701        use crate::outcome::{self, Outcome};
1702        use select::summary::{Summary, fold};
1703        let graded = |check: &str, policy: &str, reason: &str, ms: i64| Outcome {
1704            kind: outcome::GRADED.into(),
1705            session: "s".into(),
1706            lessons: vec!["A".into()],
1707            grades: vec![1],
1708            ranks: vec![0],
1709            applied: 1,
1710            free: vec!["A".into()],
1711            pf: vec![1.0],
1712            channel: "live".into(),
1713            check: check.into(),
1714            policy: policy.into(),
1715            reason: reason.into(),
1716            ms,
1717            ..Default::default()
1718        };
1719        let log = [
1720            graded("c1", "learned", "", 1),
1721            graded("c2", "fixed", outcome::DECIDED_BUSY, 2),
1722            graded("c3", "fixed", outcome::DECIDED_NO_RECORD, 3),
1723            graded("c4", "fixed", outcome::DECIDED_BUSY, 4),
1724        ];
1725        let lines = |log: &[Outcome]| log.iter().map(|o| format!("{}\n", serde_json::to_string(o).unwrap())).collect::<String>();
1726        let sel = Selection::of(&fold(&lines(&log), Summary::default()), select::Policy::Learned, None);
1727        let rows: Vec<(&str, u64, u64)> = sel.fallbacks.iter().map(|c| (c.name.as_str(), c.part, c.whole)).collect();
1728        assert_eq!(rows, [("lock_busy", 2, 4), ("lock_failed", 0, 4), ("unrecorded", 1, 4)]);
1729        let line = selection_line(&sel);
1730        assert!(line.contains(" · 4 checks · learned decisions the fixed rule took over: lock_busy 2, unrecorded 1 · "), "{line}");
1731        let none = Selection::of(&fold(&lines(&log[..1]), Summary::default()), select::Policy::Learned, None);
1732        assert!(none.fallbacks.iter().all(|c| c.part == 0) && none.fallbacks.len() == 3, "{none:?}");
1733        assert!(!selection_line(&none).contains("took over"), "{}", selection_line(&none));
1734        assert!(Selection::of(&Summary::default(), select::Policy::Learned, None).fallbacks.is_empty(), "nothing folded: not measured");
1735    }
1736
1737    /// The sections the summary holds are filled from it: per known harness its stops' latency against
1738    /// the harness's stop budget (failed calls counted, live and unknown harnesses' left out), the
1739    /// monitor's reading and weights, the fitted delivery rates; a summary without them leaves them
1740    /// empty.
1741    #[test]
1742    fn the_selection_fills_latency_evaluation_and_delivery_from_the_summary() {
1743        use select::summary::{Summary, fold};
1744        let call = |harness: &str, channel: &str, dur_ms: u64, failed: bool| {
1745            let o = crate::outcome::Outcome {
1746                kind: "graded".into(),
1747                session: "s".into(),
1748                channel: channel.into(),
1749                harness: harness.into(),
1750                dur_ms,
1751                failed,
1752                tokens: 6_000,
1753                ..Default::default()
1754            };
1755            format!("{}\n", serde_json::to_string(&o).unwrap())
1756        };
1757        let mut log: String = (1..=20).map(|k| call("codex", "stop", k * 1_000, k == 20)).collect();
1758        log += &call("claude", "stop", 4_000, false);
1759        log += &call("codex", "live", 99_000, false);
1760        log += &call("nonesuch", "stop", 5_000, false);
1761        let mut s = fold(&log, Summary::default());
1762        s.monitor = select::policy::Monitor {
1763            sessions: 12,
1764            units: 400,
1765            mean: 0.004,
1766            ucb: Some(0.031),
1767            labelled: 300,
1768            ess: 180.0,
1769            max_weight: 6.5,
1770            truncated: 3,
1771            ..Default::default()
1772        };
1773        s.fit.key = "fit".into();
1774        s.fit.rates = select::policy::Rates { free_stop: 0.6, free_live: 0.8, later: 0.4, wait: 0.25, ask_live: 1.0 };
1775        let sel = Selection::of(&s, select::Policy::Learned, None);
1776        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);
1777        assert!(budget("codex").is_some() && budget("claude").is_none(), "codex names a stop budget, claude none");
1778        let want = vec![
1779            Latency { harness: "claude".into(), checks: 1, p50_ms: 4_000, p95_ms: 4_000, budget_ms: None, over: 0 },
1780            Latency { harness: "codex".into(), checks: 20, p50_ms: 10_000, p95_ms: 19_000, budget_ms: budget("codex"), over: 1 },
1781        ];
1782        assert_eq!(sel.latency, want);
1783        let e = sel.evaluation.as_ref().expect("the monitor weighed labelled applies");
1784        assert_eq!(*e, Evaluation { sessions: 12, mean: 0.004, ucb: Some(0.031), units: 300, ess: 180.0, max_weight: 6.5, truncated: 3 });
1785        assert_eq!(sel.delivery.as_ref().map(|r| r.wait), Some(0.25));
1786        // Guard: a harness's newest `LATENCY_KEEP` stops are kept, the cache never grows past them.
1787        let keep = select::summary::LATENCY_KEEP as u64;
1788        let many: String = (1..=keep + 1).map(|k| call("claude", "stop", k, false)).collect();
1789        let kept = Selection::of(&fold(&many, Summary::default()), select::Policy::Learned, None);
1790        assert_eq!((kept.latency[0].checks, kept.latency[0].p50_ms), (keep, 1 + keep / 2), "the oldest call dropped");
1791        let cold = Selection::of(&Summary::default(), select::Policy::Learned, None);
1792        assert!(cold.latency.is_empty() && cold.evaluation.is_none() && cold.delivery.is_none(), "{cold:?}");
1793        assert_eq!((quantile(&[], 0.95), quantile(&[7], 0.5)), (0, 7));
1794    }
1795
1796    /// A learned live check of the question-count test, as hook.rs logs it: lessons A (grade 2) and B
1797    /// (grade 1), its `source`, k and s.
1798    fn tested(check: &str, session: &str, ms: i64, source: &str, xk: Option<u32>, xs: Option<u32>) -> crate::outcome::Outcome {
1799        crate::outcome::Outcome {
1800            kind: crate::outcome::GRADED.into(),
1801            session: session.into(),
1802            lessons: vec!["A".into(), "B".into()],
1803            grades: vec![2, 1],
1804            ranks: vec![0, 1],
1805            applied: 2,
1806            check: check.into(),
1807            channel: "live".into(),
1808            policy: "learned".into(),
1809            source: source.into(),
1810            xk,
1811            xs,
1812            ms,
1813            ..Default::default()
1814        }
1815    }
1816
1817    /// The test's section is the summary's fold of the graded records (select/summary.rs `Experiment`),
1818    /// never a recount: drawn counts by k and s, played or refused, the controls, the gate's refusals
1819    /// by k (each also drawn), the sessions with a drawn count; an evidence check, a refused evidence
1820    /// deviation and another policy's record count nowhere. The risk share is the collective's when set, else `ALPHA`; the section shows
1821    /// under the learned policy, and under another only while the summary counts some of its checks.
1822    #[test]
1823    fn the_test_section_is_the_summarys_fold_of_its_checks() {
1824        use select::policy::{ALPHA, EVIDENCE, EXPERIMENT, EXPERIMENT_CONTROL, GATE};
1825        use select::summary::{Summary, fold};
1826        let log = [
1827            tested("c1", "s1", 1, EXPERIMENT, Some(2), Some(1)),
1828            tested("c2", "s1", 2, EXPERIMENT_CONTROL, Some(2), Some(0)),
1829            tested("c3", "s2", 3, EXPERIMENT, Some(2), Some(1)),
1830            tested("c4", "s2", 4, GATE, Some(2), Some(1)),
1831            tested("c5", "s2", 5, GATE, Some(1), Some(0)),
1832            tested("c6", "s3", 6, EVIDENCE, None, None),
1833            tested("c7", "s3", 7, GATE, None, None),
1834            crate::outcome::Outcome { policy: "fixed".into(), ..tested("c8", "s3", 8, EXPERIMENT, Some(2), Some(2)) },
1835        ];
1836        let text: String = log.iter().map(|o| format!("{}\n", serde_json::to_string(o).unwrap())).collect();
1837        let s = fold(&text, Summary::default());
1838        let t = Selection::of(&s, select::Policy::Learned, Some(0.2)).test.expect("the learned policy shows its test");
1839        let draws = |k: u32, draws: Vec<u64>, refused: u64| Draws { k, draws, refused };
1840        assert_eq!((t.ran, t.control, t.refused, t.sessions), (5, 1, 2, 2), "{t:?}");
1841        assert_eq!(t.by_k, vec![draws(1, vec![1, 0], 1), draws(2, vec![1, 3, 0], 1)]);
1842        assert_eq!((t.risk, t.set), (0.2, true), "the collective's risk");
1843        assert!(!t.due && t.dilution.is_empty() && t.ask_free.is_none(), "5 checks, 2 sessions: not due, nothing estimated");
1844        assert_eq!((t.due_checks, t.due_sessions), (select::replay::TEST_CHECKS, u64::from(select::replay::TEST_SESSIONS)));
1845        let line = selection_line(&Selection::of(&s, select::Policy::Learned, Some(0.2)));
1846        assert!(
1847            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 · "),
1848            "{line}"
1849        );
1850        assert_eq!(t.kept, t.ran, "every drawn check still held: {t:?}");
1851        // Compaction dropped the oldest drawn check: the counts stay, the line says what the readout reads.
1852        let mut compacted = s.clone();
1853        compacted.checks.remove(0);
1854        let c = Selection::of(&compacted, select::Policy::Learned, Some(0.2)).test.unwrap();
1855        assert_eq!((c.ran, c.kept), (5, 4), "{c:?}");
1856        assert!(
1857            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"),
1858            "{}",
1859            test_line(&c)
1860        );
1861        let default = Selection::of(&s, select::Policy::Learned, None).test.unwrap();
1862        assert_eq!((default.risk, default.set), (ALPHA, false), "unset: the policy's ALPHA");
1863        // Off: said so while nothing ran.
1864        let cold = Selection::of(&Summary::default(), select::Policy::Learned, Some(0.0));
1865        assert_eq!(cold.test.as_ref().map(|t| (t.ran, t.risk)), Some((0, 0.0)));
1866        assert_eq!(test_line(cold.test.as_ref().unwrap()), "question-count test off (risk 0)");
1867        let none = Selection::of(&Summary::default(), select::Policy::Learned, None).test.unwrap();
1868        assert_eq!(test_line(&none), "question-count test: no check yet, risk 0.10");
1869        // Another policy: shown only while the summary counts the test's checks.
1870        assert!(Selection::of(&Summary::default(), select::Policy::Fixed, None).test.is_none());
1871        assert_eq!(Selection::of(&s, select::Policy::Fixed, None).test.map(|t| t.ran), Some(5), "an earlier opt-in's checks");
1872    }
1873
1874    /// The gate shown is the one in force: the ledger's allowance at the collective's risk share
1875    /// (select/policy.rs `remaining_at`, which `choose` decides by), the default `ALPHA`'s unless set,
1876    /// and 0 at `risk` 0 with nothing gained.
1877    #[test]
1878    fn the_gate_shown_is_the_one_in_force_at_the_collectives_risk() {
1879        use select::summary::{Gate, Summary};
1880        let s = Summary { gate: Gate { gain: vec![0.0; select::policy::DRAWS], allowance: 0.3, ..Gate::default() }, ..Summary::default() };
1881        let gate = |risk: Option<f64>| Selection::of(&s, select::Policy::Learned, risk).gate;
1882        assert!((gate(None) - 0.3).abs() < 1e-12, "{}", gate(None));
1883        assert!((gate(Some(0.2)) - 0.6).abs() < 1e-12, "twice ALPHA: twice the allowance, {}", gate(Some(0.2)));
1884        assert_eq!(gate(Some(0.0)), 0.0, "risk 0");
1885    }
1886
1887    /// Once due, the readout's estimates are the replay's (select/replay.rs `test_readout`) in percent:
1888    /// a grade 2 asked against handed over, which the line says, and no dilution at k = 1.
1889    #[test]
1890    fn the_test_readout_shows_the_replays_estimates_once_due() {
1891        use crate::outcome::{self, Outcome};
1892        use select::policy::EXPERIMENT;
1893        use select::summary::{Summary, fold};
1894        let sessions = u64::from(select::replay::TEST_SESSIONS);
1895        let mut text = String::new();
1896        let mut line = |o: &Outcome| text.push_str(&format!("{}\n", serde_json::to_string(o).unwrap()));
1897        for i in 0..sessions {
1898            let (session, check, ms) = (format!("s{i}"), format!("{i:012x}"), 10 * i as i64);
1899            // k = 1: A asked (s = 1) in even sessions, handed over (s = 0) in odd ones, each at 1/2.
1900            let asked = i % 2 == 0;
1901            let g = Outcome {
1902                lessons: vec!["A".into()],
1903                grades: vec![2],
1904                ranks: vec![0],
1905                applied: 1,
1906                chosen: if asked { vec!["A".into()] } else { Vec::new() },
1907                free: if asked { Vec::new() } else { vec!["A".into()] },
1908                p: vec![0.5],
1909                pf: vec![0.5],
1910                ..tested(&check, &session, ms, EXPERIMENT, Some(1), Some(u32::from(asked)))
1911            };
1912            line(&g);
1913            let rec = |kind: &str, ms: i64| Outcome {
1914                kind: kind.into(),
1915                session: session.clone(),
1916                lessons: vec!["A".into()],
1917                check: check.clone(),
1918                ms,
1919                ..Default::default()
1920            };
1921            let route = if asked { outcome::ROUTE_ASKED } else { outcome::ROUTE_FREE };
1922            line(&Outcome { route: route.into(), ..rec(outcome::DELIVERY, ms + 1) });
1923            // Every question changed the work, every hand-over was ignored.
1924            let verdict = if asked { outcome::CHANGED } else { outcome::IGNORED };
1925            line(&Outcome { judge: 2, channel: if asked { "live" } else { "queued" }.into(), ..rec(verdict, ms + 2) });
1926        }
1927        let s = fold(&text, Summary::default());
1928        let t = Selection::of(&s, select::Policy::Learned, None).test.unwrap();
1929        assert!(t.due && t.sessions == sessions, "{t:?}");
1930        let r = select::replay::test_readout(&s);
1931        let want = r.ask_free.as_ref().and_then(|x| x.value).expect("judged 2s on both routes");
1932        let e = t.ask_free.as_ref().expect("estimated once due");
1933        assert_eq!((e.mean, e.unit.as_str()), (100.0 * want, "points changed"));
1934        assert!(e.mean > 0.0, "asking changed the work, handing over did not: {e:?}");
1935        // At k = 1 no second question can dilute the first: dilution starts at 2 asked against 1.
1936        assert!(t.dilution.is_empty() && r.by_asked.is_empty(), "{t:?}");
1937        let line = selection_line(&Selection::of(&s, select::Policy::Learned, None));
1938        assert!(line.contains(&format!("; readout: grade 2 asked − handed over {:+.1} [", e.mean)), "{line}");
1939    }
1940
1941    /// The unread count is what the next prompt would show: a message shown once no longer counts,
1942    /// broadcasts only to drones running when sent, never its own; no log costs no read.
1943    #[test]
1944    fn unread_counts_follow_the_shown_once_claims() {
1945        let home = std::env::temp_dir().join(format!("borg-top-inbox-{}", std::process::id()));
1946        let _ = std::fs::remove_dir_all(&home);
1947        std::fs::create_dir_all(&home).unwrap();
1948        let ds = fixture();
1949        assert!(inbox(&home, &ds, NOW).recent.is_empty());
1950        let here = msg::node(&home);
1951        msg::send(&home, message_on(&here, "cccc3333-x", "Three of Three", "aaaa1111-x", msg::TELL, NOW - 3_000, "look at recon"), NOW).unwrap();
1952        msg::send(&home, message_on(&here, "cccc3333-x", "Three of Three", msg::ALL, msg::TELL, NOW - 2_000, "lunch"), NOW).unwrap();
1953        let i = inbox(&home, &ds, NOW);
1954        assert_eq!(i.unread.get("aaaa1111-x"), Some(&2), "{i:?}");
1955        assert_eq!(i.unread.get("cccc3333-x"), Some(&0), "its own broadcast");
1956        assert!(!i.unread.contains_key(&ds[1].p.session), "remote: claimed on its node");
1957        assert_eq!((i.day, i.recent[0].text.as_str()), (2, "lunch"));
1958        let caps = crate::harness::by_name("claude").expect("a harness").caps();
1959        let ev = json!({"session_id": "aaaa1111-x"});
1960        assert!(msg::deliver(&home, caps, "prompt", &ev, Some(100), NOW, None).is_some());
1961        assert_eq!(inbox(&home, &ds, NOW).unread.get("aaaa1111-x"), Some(&0));
1962        let _ = std::fs::remove_dir_all(&home);
1963    }
1964
1965    /// The acknowledgements (`by`, ids) in the store's messages log.
1966    fn seen_lines(home: &Path) -> Vec<(String, Vec<String>)> {
1967        msg::lines(home)
1968            .into_iter()
1969            .filter_map(|l| match l {
1970                msg::Line::Seen(s) => Some((s.by, s.ids)),
1971                msg::Line::Message(_) => None,
1972            })
1973            .collect()
1974    }
1975
1976    /// The calling session msg.rs `caller` tells from a shell whose session variables are `sessions` and
1977    /// harness pids `pids` (its own `pick`), whatever the shell running the tests sets.
1978    fn shell(sessions: Vec<(String, &'static str)>, pids: Vec<(u32, &'static str)>) -> impl Fn(&[Drone]) -> Result<Option<(String, &'static str)>, String> {
1979        move |ds| msg::pick(sessions.clone(), &pids, ds)
1980    }
1981
1982    /// A store at `base`/`store` and a repo at `base`/`work` whose engineer is ann@x: one message to the
1983    /// engineer, one to a session. Returns the store, the repo and the message to the engineer.
1984    fn acked_fixture(base: &Path) -> (std::path::PathBuf, String, Message) {
1985        let _ = std::fs::remove_dir_all(base);
1986        let (home, work) = (base.join("store"), base.join("work"));
1987        std::fs::create_dir_all(&home).unwrap();
1988        std::fs::create_dir_all(&work).unwrap();
1989        let git = |a: &[&str]| std::process::Command::new("git").arg("-C").arg(&work).args(a).output().is_ok_and(|o| o.status.success());
1990        assert!(git(&["init", "-q"]) && git(&["config", "user.email", "ann@x"]));
1991        let here = msg::node(&home);
1992        let to_me = message_on(&here, "aaaa1111-x", "One of Three", msg::ENGINEER, msg::TELL, NOW - 3_000, "the build is green");
1993        let to_drone = message_on(&here, "cccc3333-x", "Three of Three", "aaaa1111-x", msg::TELL, NOW - 2_000, "look at recon");
1994        msg::send(&home, to_me.clone(), NOW).unwrap();
1995        msg::send(&home, to_drone, NOW).unwrap();
1996        (home, work.display().to_string(), to_me)
1997    }
1998
1999    /// The printed screen acknowledges a message to the engineer it shows as the engineer of the repo
2000    /// it runs in, as `borg inbox` does from a terminal; a message to a session is never the engineer's
2001    /// to acknowledge, and `--json`, for scripts, acknowledges nothing.
2002    #[test]
2003    fn the_screen_acknowledges_the_messages_to_the_engineer_it_prints() {
2004        let base = std::env::temp_dir().join(format!("borg-top-ack-{}", std::process::id()));
2005        let (home, cwd, to_me) = acked_fixture(&base);
2006        let routes = [Route::Collective { name: "corp".into(), home: home.clone(), share: Share::Local }];
2007        let terminal = || shell(Vec::new(), Vec::new());
2008        let json = screen_of(&routes, true, &cwd, terminal(), true, NOW);
2009        assert!(json.contains("the build is green") && seen_lines(&home).is_empty(), "--json acknowledges nothing: {json}");
2010        let text = screen_of(&routes, true, &cwd, terminal(), false, NOW);
2011        assert!(text.contains("the build is green") && text.contains("look at recon"), "{text}");
2012        assert_eq!(seen_lines(&home), [("engineer:ann@x".to_string(), vec![to_me.id.clone()])], "the engineer's message only, as theirs");
2013        screen_of(&routes, true, &cwd, terminal(), false, NOW + 1_000);
2014        assert_eq!(seen_lines(&home).len(), 1, "acknowledged once: a later look writes no second line");
2015        let _ = std::fs::remove_dir_all(&base);
2016    }
2017
2018    /// A drone's shell runs the screen piped and in the engineer's repo: it prints the message to the
2019    /// engineer and acknowledges nothing as them, as `borg inbox` would not. A session variable, two
2020    /// that no presence tells apart, or opencode's pid naming a drone of another collective on the
2021    /// screen: each is a session's; the same screen from a terminal acknowledges it.
2022    #[test]
2023    fn a_session_running_the_screen_acknowledges_nothing_as_the_engineer() {
2024        let base = std::env::temp_dir().join(format!("borg-top-ack-session-{}", std::process::id()));
2025        let (home, cwd, to_me) = acked_fixture(&base);
2026        let other = base.join("other");
2027        std::fs::create_dir_all(&other).unwrap();
2028        let pid = std::process::id();
2029        let oc = Presence { harness: "opencode".into(), pid: Some(pid), ..p("dddd4444-x", "ann@x", NOW - 9_000, NOW - 1_000, "prompt", "docs", &[]) };
2030        presence::write(&other, &oc).unwrap();
2031        let routes = [
2032            Route::Collective { name: "corp".into(), home: home.clone(), share: Share::Local },
2033            Route::Collective { name: "side".into(), home: other.clone(), share: Share::Local },
2034        ];
2035        let shells = [
2036            ("Claude Code", shell(vec![("eeee5555-x".into(), "claude")], Vec::new())),
2037            ("Codex in Claude Code, neither with presence", shell(vec![("eeee5555-x".into(), "claude"), ("ffff6666-x".into(), "codex")], Vec::new())),
2038            ("opencode, its drone in the other collective", shell(Vec::new(), vec![(pid, "opencode")])),
2039        ];
2040        for (what, caller) in shells {
2041            let text = screen_of(&routes, false, &cwd, caller, false, NOW);
2042            assert!(text.contains("the build is green"), "{what}: shown {text}");
2043            assert!(seen_lines(&home).is_empty() && seen_lines(&other).is_empty(), "{what}: acknowledged nothing as the engineer");
2044        }
2045        screen_of(&routes, false, &cwd, shell(Vec::new(), vec![(pid + 1, "opencode")]), false, NOW);
2046        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");
2047        let _ = std::fs::remove_dir_all(&base);
2048    }
2049
2050    /// The printed screen acknowledges a message to the engineer only when its line, cut to
2051    /// `MESSAGE_CHARS`, still shows some of its text (`prints_text`), as the live screen does: a line
2052    /// whose sender and recipient fill it names only who wrote to whom, and its sender's window stays
2053    /// closed until the engineer reads the text elsewhere (`borg inbox`).
2054    #[test]
2055    fn the_printed_screen_acknowledges_only_the_messages_whose_text_it_prints() {
2056        let base = std::env::temp_dir().join(format!("borg-top-ack-cut-{}", std::process::id()));
2057        let (home, cwd, to_me) = acked_fixture(&base);
2058        // The engineer's address takes 60 columns, so a line's text starts after a sender's name + 66.
2059        let email = format!("{}@x", "a".repeat(58));
2060        assert!(std::process::Command::new("git").args(["-C", &cwd, "config", "user.email", &email]).output().is_ok_and(|o| o.status.success()));
2061        let (here, to) = (msg::node(&home), format!("{}:{email}", msg::ENGINEER));
2062        let one = message_on(&here, "bbbb2222-x", &"n".repeat(52), &to, msg::TELL, NOW - 1_500, "one character shows");
2063        let none = message_on(&here, "dddd4444-x", &"n".repeat(53), &to, msg::TELL, NOW - 1_000, "none shows");
2064        msg::send(&home, one.clone(), NOW).unwrap();
2065        msg::send(&home, none.clone(), NOW).unwrap();
2066        let routes = [Route::Collective { name: "corp".into(), home: home.clone(), share: Share::Local }];
2067        let text = screen_of(&routes, true, &cwd, shell(Vec::new(), Vec::new()), false, NOW);
2068        assert!(text.contains(&format!("{} -> {email}: o…", one.name)) && text.contains(&format!("{} -> {email}: …", none.name)), "{text}");
2069        let mut acked: Vec<String> = seen_lines(&home).into_iter().flat_map(|(by, ids)| ids.into_iter().map(move |id| format!("{by} {id}"))).collect();
2070        acked.sort();
2071        let mut want = [format!("{to} {}", to_me.id), format!("{to} {}", one.id)];
2072        want.sort();
2073        assert_eq!(acked, want, "the line that shows one character of its text is read; the one cut before it is not");
2074        let _ = std::fs::remove_dir_all(&base);
2075    }
2076
2077    /// Guard (Q1 pacing): a live screen acknowledges a message once per screen, then skips it without
2078    /// asking git or reading the log's seen lines again, whatever the log holds by then; a message
2079    /// that arrives later is acknowledged at the frame that first shows it.
2080    #[test]
2081    fn a_live_screen_acknowledges_each_message_once() {
2082        let base = std::env::temp_dir().join(format!("borg-top-ack-once-{}", std::process::id()));
2083        let _ = std::fs::remove_dir_all(&base);
2084        std::fs::create_dir_all(&base).unwrap();
2085        let home = base.clone();
2086        let here = msg::node(&home);
2087        let to_me = |by: &str, ms: i64, text: &str| message_on(&here, by, "a drone", msg::ENGINEER, msg::TELL, ms, text);
2088        let first = to_me("aaaa1111-x", NOW - 3_000, "the build is green");
2089        msg::send(&home, first.clone(), NOW).unwrap();
2090        let cwd = base.display().to_string();
2091        let mut done = HashSet::new();
2092        let recent = |now: i64| inbox(&home, &[], now).recent;
2093        acknowledge(&home, &cwd, &recent(NOW).iter().collect::<Vec<_>>(), &mut done, NOW);
2094        assert_eq!(seen_lines(&home).len(), 1, "{:?}", seen_lines(&home));
2095        assert!(done.contains(&first.id));
2096        // The log loses the acknowledgement (as a bound or a TTL may cut it): the next frame passes
2097        // the message by, its id already done.
2098        let kept: String = std::fs::read_to_string(msg::path(&home)).unwrap().lines().filter(|l| !l.contains("\"seen\"")).map(|l| format!("{l}\n")).collect();
2099        std::fs::write(msg::path(&home), kept).unwrap();
2100        acknowledge(&home, &cwd, &recent(NOW + 1_000).iter().collect::<Vec<_>>(), &mut done, NOW + 1_000);
2101        assert!(seen_lines(&home).is_empty(), "skipped: no second ack of a done id");
2102        // From another sender: the first's window holds its sender's next one until acknowledged.
2103        let second = to_me("cccc3333-x", NOW + 2_000, "and the docs");
2104        msg::send(&home, second.clone(), NOW + 2_000).unwrap();
2105        acknowledge(&home, &cwd, &recent(NOW + 3_000).iter().collect::<Vec<_>>(), &mut done, NOW + 3_000);
2106        assert_eq!(seen_lines(&home).into_iter().map(|(_, ids)| ids).collect::<Vec<_>>(), [vec![second.id.clone()]], "only the new one");
2107        let _ = std::fs::remove_dir_all(&base);
2108    }
2109
2110    /// A line's `by` and `name` are its sender's claims: one from another node that names a local drone's
2111    /// session is not shown as that drone, and carries its node's key.
2112    #[test]
2113    fn a_sender_is_tied_to_the_node_its_line_names() {
2114        let ds = fixture();
2115        let own = message("aaaa1111-x", "anything", msg::ALL, msg::TELL, NOW, "hi");
2116        assert_eq!(sender(&ds, "n1", &own), "One of Three", "this node's drone");
2117        let forged = message_on("evil0123456789", "aaaa1111-x", "One of Three", msg::ALL, msg::TELL, NOW, "hi");
2118        assert_eq!(sender(&ds, "n1", &forged), "One of Three @evil01234567", "the claim, with the node it came from");
2119        let remote = message_on(&"beefcafe".repeat(8), &ds[1].p.session, "x", msg::ALL, msg::TELL, NOW, "hi");
2120        assert_eq!(sender(&ds, "n1", &remote), "Two of Three @beefcafebeef", "the remote drone on its own node");
2121        assert_eq!(recipient(&ds, "n1", "aaaa1111-x@n1"), "One of Three");
2122        assert_eq!(recipient(&ds, "n1", "aaaa1111-x@evil"), "aaaa1111", "not the local drone of that id");
2123        assert_eq!((recipient(&ds, "n1", "engineer"), recipient(&ds, "n1", msg::ALL)), ("engineer".into(), "collective".into()), "an `all` line");
2124        assert_eq!(recipient(&ds, "n1", "local@n1"), "local");
2125        assert_eq!(recipient(&ds, "n1", "local@beefcafebeefcafe00"), "local @beefcafebeef");
2126    }
2127
2128    /// The line on borg itself: a newer release (while the check is on), a member nothing syncs with and
2129    /// one on an older protocol with what it misses; nothing for a store without `mesh/` or members on
2130    /// this node's protocol.
2131    #[test]
2132    fn the_line_on_borg_itself_names_a_release_and_the_protocols_that_differ() {
2133        let home = std::env::temp_dir().join(format!("borg-top-version-{}", std::process::id()));
2134        let _ = std::fs::remove_dir_all(&home);
2135        std::fs::create_dir_all(&home).unwrap();
2136        let newer = update::Checked { checked_ms: 1, latest: Some("99.0.0".into()), error: None };
2137        let current = update::Checked { latest: Some(update::VERSION.into()), ..newer.clone() };
2138        assert_eq!(version_line([home.as_path()], None, true), None);
2139        assert_eq!(version_line([home.as_path()], Some(&current), true), None);
2140        let release = format!("borg 99.0.0 is released, this is {}: borg update", update::VERSION);
2141        assert_eq!(version_line([home.as_path()], Some(&newer), true), Some(release.clone()));
2142        assert_eq!(version_line([home.as_path()], Some(&newer), false), None, "BORG_UPDATE_CHECK=off");
2143
2144        let ours = protocol::current();
2145        let v = |protocol, engineer: Option<&str>| protocol::PeerVersion { borg: None, protocol, ms: 1, engineer: engineer.map(str::to_string) };
2146        protocol::note_version(&home, "k-same", &v(ours, Some("ann@x"))).unwrap();
2147        assert_eq!(version_line([home.as_path()], None, true), None, "a member on this node's protocol is no news");
2148        protocol::note_version(&home, "k-old", &v(ours - 1, Some("bob@x"))).unwrap();
2149        let mismatch = protocol::Mismatch { theirs: format!("borg/mesh/{}", ours + 1), ours: format!("borg/mesh/{ours}"), ms: 2 };
2150        protocol::note_mismatch(&home, "k-new", mismatch).unwrap();
2151        let line = version_line([home.as_path()], Some(&newer), true).unwrap();
2152        let old = format!("bob@x on /{} misses /{ours}: {}", ours - 1, protocol::note(ours).unwrap());
2153        let none = format!("nothing syncs with peer k-new (borg/mesh/{}, this node borg/mesh/{ours})", ours + 1);
2154        assert_eq!(line, format!("{release} · {none} · {old}"));
2155        let _ = std::fs::remove_dir_all(&home);
2156    }
2157
2158    #[test]
2159    fn json_for_scripts() {
2160        let v = to_json_with("corp", Share::Team, &fixture(), &inbox_fixture());
2161        assert_eq!((v["drones"][0]["unread"].as_u64(), v["drones"][1].get("unread")), (Some(2), None));
2162        assert_eq!((v["messages"]["day"].as_u64(), v["messages"]["recent"][0]["kind"].as_str()), (Some(3), Some("ask")));
2163        assert_eq!(v["collective"], "corp");
2164        let d = &v["drones"];
2165        assert_eq!(d.as_array().unwrap().len(), 3);
2166        assert_eq!(d[0]["designation"], "One of Three");
2167        assert_eq!(d[0]["session"], "aaaa1111-x");
2168        assert_eq!(d[0]["state"], "working");
2169        assert_eq!(d[0]["paused"], true);
2170        assert_eq!(d[1]["node"], "beefcafe".repeat(8));
2171        assert_eq!(d[1]["state"], "waiting");
2172        assert_eq!(d[2]["state"], "idle");
2173        assert_eq!(d[2]["files"], json!([]));
2174    }
2175
2176    /// Local first: a prefix or id that matches a drone here and one of a member names the one here.
2177    #[test]
2178    fn a_name_matching_here_and_on_a_member_names_the_drone_here() {
2179        let local = vec![p("aaaa1111-x", "alice@x", 100, NOW - 5_000, "post_tool", "a", &[])];
2180        let remote = ["aaaa1111-x", "aaaa2222-z"].map(|s| presence::Remote {
2181            p: p(s, "bob@y", 50, NOW - 5_000, "post_tool", "b", &[]),
2182            key: "beefcafe".repeat(8),
2183            sig: String::new(),
2184        });
2185        let ds = assemble(local, remote.into(), NOW, |_| true, |_| false);
2186        for t in ["aaaa", "aaaa1111", "aaaa1111-x"] {
2187            let d = resolve(&ds, t).unwrap().unwrap();
2188            assert!(d.node.is_empty() && d.p.session == "aaaa1111-x", "{t}");
2189        }
2190        assert_eq!(resolve(&ds, "aaaa2").unwrap().unwrap().node, "beefcafe".repeat(8), "only a member's matches");
2191        assert!(!resolve(&ds, "One").unwrap().unwrap().node.is_empty(), "a number names one drone of all, wherever it runs");
2192    }
2193
2194    /// An ended session here does not shadow a live one on a member's machine: it would never read the message.
2195    #[test]
2196    fn a_name_matching_an_ended_drone_here_and_a_live_member_names_the_live_one() {
2197        let local = vec![p("aaaa1111-x", "alice@x", 100, NOW - 5_000, "end", "a", &[])];
2198        let remote =
2199            vec![presence::Remote { p: p("aaaa2222-z", "bob@y", 50, NOW - 5_000, "post_tool", "b", &[]), key: "beefcafe".repeat(8), sig: String::new() }];
2200        let ds = assemble(local, remote, NOW, |_| true, |_| false);
2201        let d = resolve(&ds, "aaaa").unwrap().unwrap();
2202        assert_eq!((d.p.session.as_str(), d.node.as_str()), ("aaaa2222-z", "beefcafe".repeat(8).as_str()), "the live one, though remote");
2203        assert!(resolve(&ds, "aaaa1111").unwrap().unwrap().node.is_empty(), "an ended drone is still named by its own id");
2204    }
2205
2206    #[test]
2207    fn resolves_drones_by_designation_or_session() {
2208        let ds = fixture();
2209        assert_eq!(resolve(&ds, "Two").unwrap().unwrap().p.session, "bbbb2222-y");
2210        assert_eq!(resolve(&ds, "two of three").unwrap().unwrap().p.session, "bbbb2222-y");
2211        assert_eq!(resolve(&ds, "3").unwrap().unwrap().p.session, "cccc3333-x");
2212        assert_eq!(resolve(&ds, "aaaa").unwrap().unwrap().p.session, "aaaa1111-x");
2213        assert!(resolve(&ds, "all").unwrap().is_none());
2214        assert!(resolve(&ds, "Four").is_err());
2215        assert!(resolve(&ds, "zzzz").is_err());
2216        assert!(resolve(&ds, "aa").is_err(), "too short a prefix");
2217        assert!(["collective", "local"].iter().all(|w| resolve(&ds, w).unwrap().is_none()));
2218        assert_eq!(resolve(&ds, "bbbb").unwrap().unwrap().p.session, "bbbb2222-y", "nothing here matches: the member's");
2219        assert_eq!(word(21), "21");
2220    }
2221}