Skip to main content

borg/
select.rs

1//! Which of a graded check's applies to ask, to hand over free, to judge blindly as a placebo, or to
2//! leave for later: the lesson selection policy. It replaces attention.rs, which stays one release as
3//! the `Attention` sub-policy.
4//!
5//! **Policies** (`policy`, the first that applies): `BORG_SELECT=fixed` the fixed rule;
6//! `BORG_STOP_ASK_CAP=N` the fixed rule asking only the first `N` 3s (evaluation only);
7//! `BORG_STOP_ATTENTION=on` attention.rs's learned cut (evaluation only); otherwise the learned policy
8//! (`Learned`, select/policy.rs), the default since its pre-registered checks passed under Rules v2
9//! (docs/evidence.md#lesson-selection: P4 had failed its first run and the rerun under the v1 design).
10//!
11//! **The fixed rule** (conscience.rs `graded_without`, no cap) asks every 3 and 2 and the 1s up to
12//! `conscience::CAP` when a check has a 3, else hands over the 2s and 1s free, up to `CAP`; a lesson the
13//! session already had delivered (`Ctx::had`) may still be asked but never takes a free slot, which its
14//! delivery would drop as given before. It also draws up to `PLACEBO_SLOTS` of the applies it delivers
15//! neither way for the placebo arm (`draw_placebo`): judged blindly where a free delivery would be, never
16//! shown, so what not delivering is worth gets labels at no cost to quality. Those are the labels the
17//! learned policy cannot get from the fixed rule's history, which asks and hands over by grade alone.
18//!
19//! **The learned policy** (select/policy.rs `choose`, policy v2) leaves the fixed rule only on evidence
20//! from a fitted value model, and on live checks (live mode, removed in 0.7.6: the test runs no more)
21//! ran a randomised test of how many of the fixed rule's delivered lessons to ask rather than hand
22//! over (nothing withheld), both under a
23//! conservative gate rationed by the collective's tolerance (`alpha`); cold start (no fit, or a gate
24//! that may risk nothing yet) is exactly the fixed rule, and so is a store the monitor reverted
25//! (select/summary.rs `REVERTED`). It draws its placebos
26//! among the applies it leaves for later, as the fixed rule does. The evaluation switches decide
27//! exactly as the hook did before this module (0.5.0's attention.rs `graded`) and draw no placebo.
28//!
29//! **What it reads.** Only `Ctx`: the summary the hook loaded once (select/summary.rs `load`, beside
30//! the lessons file), and for the `Attention` sub-policy the outcomes beside the lessons file
31//! (outcome.rs `path_beside`), never the routed collective's: an evaluation that points
32//! `BORG_CONSCIENCE` at its own store reads that store's outcomes, not the engineer's.
33//!
34//! Deterministic: a decision depends on the applies, the policy, the summary and the check id
35//! (`check_id`, `seed`), all logged.
36
37pub mod policy;
38pub mod replay;
39pub mod summary;
40
41use std::path::Path;
42
43use serde::{Deserialize, Serialize};
44
45use crate::attention;
46use crate::conscience::{self, Challenge};
47use crate::outcome;
48
49/// The policy in force for a graded check (module doc).
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum Policy {
52    /// `BORG_SELECT=fixed` whatever else is set: the fixed rule (module doc), the way back from the default.
53    Fixed,
54    /// `BORG_STOP_ASK_CAP=N`: a block asks only the first `N` 3s (conscience.rs `ask_cap`).
55    Cap(usize),
56    /// `BORG_STOP_ATTENTION=on`: attention.rs's conformal cut on the model's rank.
57    Attention,
58    /// The default, no switch set: the learned policy (select/policy.rs), since P0–P4 passed under Rules v2.
59    Learned,
60}
61
62impl Policy {
63    /// The name logged with each check (`Log::policy`, outcome.rs `policy`).
64    #[must_use]
65    pub fn name(self) -> &'static str {
66        match self {
67            Policy::Fixed => "fixed",
68            Policy::Cap(_) => "cap",
69            Policy::Attention => "attention",
70            Policy::Learned => "learned",
71        }
72    }
73}
74
75/// The policy this process runs under, from the environment (module doc).
76#[must_use]
77pub fn policy() -> Policy {
78    precedence(std::env::var("BORG_SELECT").ok().as_deref(), conscience::ask_cap(), attention::on())
79}
80
81/// `policy` over its three switches: `select` (`BORG_SELECT`), `cap` (`BORG_STOP_ASK_CAP` as
82/// conscience.rs `ask_cap` reads it) and `attention` (`BORG_STOP_ATTENTION=on`). `fixed` wins over
83/// both evaluation switches, the cap over attention, both over `learned`; any other `BORG_SELECT`,
84/// or none, is the default, the learned policy: its pre-registered checks passed under Rules v2
85/// (docs/evidence.md#lesson-selection, run v2 2026-10-29), and `BORG_SELECT=fixed` is the way back.
86/// Whatever evaluates the learned policy (select/replay.rs, the monitoring of what it would do) names
87/// `Policy::Learned` and never reads this.
88#[must_use]
89pub fn precedence(select: Option<&str>, cap: Option<usize>, attention: bool) -> Policy {
90    match (select, cap) {
91        (Some("fixed"), _) => Policy::Fixed,
92        (_, Some(n)) => Policy::Cap(n),
93        _ if attention => Policy::Attention,
94        _ => Policy::Learned,
95    }
96}
97
98/// What happens to one graded apply at its check.
99#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
100#[serde(rename_all = "lowercase")]
101pub enum Route {
102    /// Asked: in a block at the stop, as a question with the next prompt or tool call live.
103    Ask,
104    /// Handed over free, as context at the next prompt or touch (hook.rs `queued`).
105    Free,
106    /// Not delivered, but judged blindly where a free delivery would be: the judge's floor.
107    Placebo,
108    /// Not now: stays eligible for a later check.
109    #[default]
110    Not,
111}
112
113impl Route {
114    #[must_use]
115    pub fn name(self) -> &'static str {
116        match self {
117            Route::Ask => "ask",
118            Route::Free => "free",
119            Route::Placebo => "placebo",
120            Route::Not => "not",
121        }
122    }
123
124    /// The arm the route delivers by, an index below `ARMS`: ask 0, free 1, not now 2 (a placebo is
125    /// not delivered).
126    #[must_use]
127    pub const fn arm(self) -> usize {
128        match self {
129            Route::Ask => 0,
130            Route::Free => 1,
131            Route::Placebo | Route::Not => 2,
132        }
133    }
134}
135
136/// The arms a graded apply can be delivered by (`Route::arm`). Protocol: the length and order of
137/// every per-arm array (select/policy.rs `monitor`, select/replay.rs).
138pub const ARMS: usize = 3;
139
140/// One graded apply as the policy sees it, aligned with the check's applies.
141#[derive(Debug, Clone, PartialEq)]
142pub struct Unit<'a> {
143    pub lesson: &'a str,
144    /// 3 certain, 2 likely, 1 worth a thought.
145    pub grade: u8,
146    /// Its place in the model's applies, 0 first.
147    pub rank: usize,
148}
149
150/// The units of `applied`, in its order.
151#[must_use]
152pub fn units<'a>(applied: &[(&'a Challenge, u8)]) -> Vec<Unit<'a>> {
153    applied.iter().enumerate().map(|(rank, (c, g))| Unit { lesson: c.id.as_str(), grade: *g, rank }).collect()
154}
155
156/// The check a decision is made for.
157pub struct Ctx<'a> {
158    pub session: &'a str,
159    /// The check's id (`check_id`).
160    pub check: &'a str,
161    /// `stop` or `live`.
162    pub channel: &'static str,
163    /// The lessons file the conscience reads (hook.rs `lessons_path`): the store whose outcomes count.
164    pub lessons: &'a Path,
165    /// The selection summary loaded beside `lessons` (select/summary.rs `load`).
166    pub summary: &'a summary::Summary,
167    /// The check's pool tokens (`shown_tokens`, the graded record's `tokens`); 0 unknown, the
168    /// baseline's. The fit and the gate's ledger value the check at the logged tokens, so the learned
169    /// policy decides at them too: the gain the gate admits is then the gain its ledger records.
170    pub tokens: u32,
171    /// The lessons this session already had delivered (asked, delivered early or handed over free:
172    /// hook.rs `had_in` over the state read at the decision, `decide`) or has waiting in its queue to be
173    /// asked or handed over by another check (hook.rs `waiting_in`), in any order. Such a lesson may
174    /// still be asked, but never takes a free slot (the delivery would drop it as given before or queued
175    /// twice, conscience.rs `graded_without`), and is never a placebo (`draw_placebo`). Only those among the
176    /// applies count (`nofree`, what the graded record logs), so a replay passes the record's `nofree`;
177    /// empty for a record from before it, whose hook decided without.
178    pub had: &'a [String],
179    /// The collective's tolerance in force at the check (`alpha`: its `risk` in collectives.json, else
180    /// `policy::ALPHA`), read once by the hook and logged on the graded record (`Log::alpha`), so a
181    /// replay decides under the α the hook had (`alpha_of` the record's), not whatever the config says
182    /// for the replayed path now (a store copy maps to no collective).
183    pub alpha: f64,
184}
185
186/// The pool tokens of a check whose model call was shown `shown` (hook.rs `Call::order`): whole.rs
187/// `cost` summed over them, saturating. What the graded record logs (outcome.rs `tokens`) and the
188/// decision values (`Ctx::tokens`), from one definition so the two never part.
189#[must_use]
190pub fn shown_tokens(shown: &[&Challenge]) -> u32 {
191    let sum: usize = shown.iter().map(|c| crate::whole::cost(c)).sum();
192    sum.try_into().unwrap_or(u32::MAX)
193}
194
195/// The applies the session already had (`Ctx::had`), each once, in the applies' order: what the graded
196/// record logs as `nofree` (outcome.rs), all a replay needs of `had` to decide as the hook did.
197#[must_use]
198pub fn nofree(applied: &[(&Challenge, u8)], had: &[String]) -> Vec<String> {
199    let mut out: Vec<String> = Vec::new();
200    for (c, _) in applied {
201        if had.contains(&c.id) && !out.contains(&c.id) {
202            out.push(c.id.clone());
203        }
204    }
205    out
206}
207
208/// `Log::alpha` of a check decided under the tolerance `alpha`: None at `policy::ALPHA`. What the hook
209/// logs and `alpha_of` reads back, from one definition so the two never part.
210#[must_use]
211pub fn logged_alpha(alpha: f64) -> Option<f64> {
212    (alpha != policy::ALPHA).then_some(alpha)
213}
214
215/// The tolerance a graded record's check was decided under, from its logged `alpha` (`logged_alpha`):
216/// `policy::ALPHA` when none is logged, as on every record from before `risk`.
217#[must_use]
218pub fn alpha_of(logged: Option<f64>) -> f64 {
219    logged.unwrap_or(policy::ALPHA)
220}
221
222/// What a decision logs: the graded record's `policy` and `fit`, and the `#borg-select` line.
223#[derive(Debug, Clone, PartialEq, Default)]
224pub struct Log {
225    /// `Policy::name`.
226    pub policy: &'static str,
227    /// What set this check's routes: `fixed` (the fixed rule: under `Fixed`, `Attention` on a check
228    /// without a 3, and `Learned` at cold start), `cap`, attention's cut source (`prior`, `crc`,
229    /// `explore`; an `explore` check's outcomes label the cut, outcome.rs `explore`), or the learned
230    /// policy's (select/policy.rs `Decision::source`: `evidence`, `experiment`, `experiment_control`,
231    /// `gate` when the gate kept the fixed rule, `size` past `policy::SOLVE_MAX`, 0.6.0's `learned`;
232    /// `reverted` when the monitor reverted the store).
233    pub source: &'static str,
234    /// The randomised question-count test's k and s at this check (select/policy.rs `Decision::xk`,
235    /// `xs`), logged as the graded record's `xk` and `xs`; None outside the test and under every other
236    /// policy.
237    pub xk: Option<u32>,
238    pub xs: Option<u32>,
239    /// The tolerance the check was decided under (`Ctx::alpha`), under every policy so a cold start
240    /// replays the learned policy at the collective's α too; None at `policy::ALPHA`, so the record of a
241    /// collective without `risk` stays as before (`logged_alpha`, read back by `alpha_of`).
242    pub alpha: Option<f64>,
243    /// The fit the decision used (select/policy.rs `Fit::key`); empty for the prior.
244    pub fit: String,
245    /// The conservative gate's budget at this check: how much replayed quality (mistakes avoided) the
246    /// learned policy may still risk below the fixed rule. Not money: spend is only measured, unless an
247    /// engineer sets a budget for the collective. 0 at cold start and under every other policy.
248    pub gate: f64,
249    /// How much of `gate` this decision takes (select/policy.rs `Decision::risk`): 0 when it keeps the
250    /// fixed rule. Replayed, not logged: `borg debug select` P3 checks it never exceeds `gate`.
251    pub risk: f64,
252    /// `Attention` on a check with a 3: today's `#borg-attention` line (attention.rs `line`), which the
253    /// hook keeps writing while attention.rs is a sub-policy.
254    pub attention: Option<String>,
255    /// Each placebo candidate's chance of the draw (`draw_placebo`, outcome.rs `psi`); 0 without a candidate
256    /// and under the evaluation switches, which draw none.
257    pub psi: f64,
258}
259
260impl Log {
261    /// `#borg-select policy=<p> source=<s> fit=<key|-> gate=<B> applied=<m> @<check>`: one check's
262    /// decision, beside its `#borg-graded` line; `@<check>` ties it to the check's other lines.
263    #[must_use]
264    pub fn line(&self, applied: usize, check: &str) -> String {
265        let fit = if self.fit.is_empty() { "-" } else { self.fit.as_str() };
266        format!("#borg-select policy={} source={} fit={fit} gate={:.4} applied={applied} @{check}", self.policy, self.source, self.gate)
267    }
268}
269
270/// A graded check's decision: what to ask (in the order to ask it), to hand over free (in the order
271/// to deliver it), and to judge as placebo; per apply (aligned with the applies) its probability of
272/// being asked (`p`) and of being handed over free (`pf`) under the policy in force; and what to log.
273#[derive(Debug, Clone)]
274pub struct Routed<'a> {
275    pub ask: Vec<&'a Challenge>,
276    pub free: Vec<&'a Challenge>,
277    pub placebo: Vec<&'a Challenge>,
278    pub p: Vec<f64>,
279    pub pf: Vec<f64>,
280    pub log: Log,
281}
282
283impl Routed<'_> {
284    /// The route of lesson `id`: `Not` for a lesson the check neither asks, hands over nor judges.
285    #[must_use]
286    pub fn route(&self, id: &str) -> Route {
287        let has = |v: &[&Challenge]| v.iter().any(|c| c.id == id);
288        if has(&self.ask) {
289            Route::Ask
290        } else if has(&self.free) {
291            Route::Free
292        } else if has(&self.placebo) {
293            Route::Placebo
294        } else {
295            Route::Not
296        }
297    }
298}
299
300/// A graded check's decision under `policy` (the hook passes the policy in force, `policy`, through
301/// hook.rs `decide`): replaces 0.5.0's attention.rs `graded`. `Fixed` and `Cap` decide by the fixed
302/// rule (conscience.rs `graded_without` with `ctx.had`, `Cap` with its cap), whose propensities are
303/// its choice (attention.rs `fixed_p`); `Attention` on a check with a 3 by the learned cut on the
304/// outcomes beside `ctx.lessons`, with the explore mixture's propensities, and by the fixed rule
305/// otherwise; `Learned` on every check by select/policy.rs `choose` (the fixed rule at cold start or
306/// while the store is reverted), with its exact propensities (`learned`). `Fixed`
307/// and `Learned` draw placebos among the applies they deliver neither way (`draw_placebo`); the asked
308/// and free lists never depend on that draw, so the fixed rule's decision is the same with or without
309/// it (P0).
310#[must_use]
311pub fn graded_under<'a>(policy: Policy, applied: &[(&'a Challenge, u8)], ctx: &Ctx) -> Routed<'a> {
312    let three = applied.iter().any(|(_, g)| *g == 3);
313    let fixed = |source: &'static str, placebos: bool, (ask, free): (Vec<&'a Challenge>, Vec<&'a Challenge>)| {
314        let (placebo, psi) = if placebos { draw_placebo(applied, &ask, &free, ctx) } else { (Vec::new(), 0.0) };
315        Routed {
316            p: attention::fixed_p(applied, &ask),
317            pf: attention::fixed_p(applied, &free),
318            ask,
319            free,
320            placebo,
321            log: Log { policy: policy.name(), source, fit: ctx.summary.fit.key.clone(), psi, ..Log::default() },
322        }
323    };
324    let mut r = match policy {
325        Policy::Attention if three => by_cut(applied, ctx),
326        Policy::Cap(n) => fixed("cap", false, conscience::graded_without(applied, Some(n), ctx.had)),
327        Policy::Learned => learned(applied, ctx),
328        Policy::Fixed => fixed("fixed", true, conscience::graded_without(applied, None, ctx.had)),
329        Policy::Attention => fixed("fixed", false, conscience::graded_without(applied, None, ctx.had)),
330    };
331    r.log.alpha = logged_alpha(ctx.alpha);
332    r
333}
334
335/// Placebo slots per check: of the applies a check delivers neither way, at most this many are judged
336/// blindly (`Routed::placebo`). Guard: bounds the judge's extra work per check.
337pub const PLACEBO_SLOTS: usize = 2;
338
339/// The placebo draw of a check that asks `ask` and hands over `free` (`Fixed`, `Learned`): its
340/// candidates are the applies whose lesson it delivers neither way, in no copy, and the session never
341/// had (`Ctx::had`: a placebo measures not delivering, and the delivery drops a placebo of a lesson
342/// given before, hook.rs `queued`), each lesson once; up to `PLACEBO_SLOTS` of them drawn uniformly on
343/// the placebo's own stream of the check's seed, in the applies' order. With each candidate's chance
344/// of the draw, min(1, `PLACEBO_SLOTS` / their number), 0 without one.
345fn draw_placebo<'a>(applied: &[(&'a Challenge, u8)], ask: &[&Challenge], free: &[&Challenge], ctx: &Ctx) -> (Vec<&'a Challenge>, f64) {
346    let delivered = |id: &str| ask.iter().chain(free).any(|c| c.id == id);
347    let mut later: Vec<usize> = Vec::new();
348    for (j, (c, _)) in applied.iter().enumerate() {
349        if !delivered(&c.id) && !ctx.had.contains(&c.id) && !later.iter().any(|&i| applied[i].0.id == c.id) {
350            later.push(j);
351        }
352    }
353    let mut drawn = later.clone();
354    // The placebo's own stream of the check's seed (the tag spells "placebo").
355    let mut rng = crate::memory::Rng::new(seed(ctx) ^ 0x706c_6163_6562_6f00);
356    for i in (1..drawn.len()).rev() {
357        drawn.swap(i, (rng.next_u64() % (i as u64 + 1)) as usize);
358    }
359    drawn.truncate(PLACEBO_SLOTS);
360    drawn.sort_unstable();
361    let psi = if later.is_empty() { 0.0 } else { outcome::round_p((PLACEBO_SLOTS as f64 / later.len() as f64).min(1.0)) };
362    (drawn.iter().map(|&j| applied[j].0).collect(), psi)
363}
364
365/// `Log::source` of a learned check on a store the monitor reverted (select/summary.rs `REVERTED`).
366/// Protocol: logged as the graded record's `source`, which `borg debug select` P1 reads back as the
367/// revert the hook saw (select/replay.rs).
368pub const REVERTED_SOURCE: &str = "reverted";
369
370/// `Learned`: select/policy.rs `choose` on the summary's fit and gate at the check's pool tokens under
371/// the collective's tolerance in force (`Ctx::alpha`), never handing over free a lesson the session had (`Ctx::had`;
372/// the fixed rule while the store is reverted), then the placebo draw among the applies it delivers
373/// neither way (`draw_placebo`). The randomised question-count test's k and s go to the log (`Log::xk`,
374/// `xs`), which the graded record carries.
375fn learned<'a>(applied: &[(&'a Challenge, u8)], ctx: &Ctx) -> Routed<'a> {
376    let us = units(applied);
377    let s = ctx.summary;
378    let check = policy::Check { session: ctx.session, stop: ctx.channel == "stop", tokens: ctx.tokens };
379    let d = match s.monitor.reverted {
380        true => {
381            let base = policy::fixed_routes(&us, ctx.had);
382            let p = |r: Route| base.iter().map(|x| f64::from(u8::from(*x == r))).collect();
383            policy::Decision { p: p(Route::Ask), pf: p(Route::Free), routes: base, source: REVERTED_SOURCE, gate: 0.0, risk: 0.0, xk: None, xs: None }
384        }
385        false => policy::choose(&us, &check, &s.fit, &s.gate, ctx.alpha, seed(ctx), ctx.had),
386    };
387    let (ask, free) = ordered(applied, &d.routes);
388    let (placebo, psi) = draw_placebo(applied, &ask, &free, ctx);
389    Routed {
390        ask,
391        free,
392        placebo,
393        p: d.p,
394        pf: d.pf,
395        log: Log {
396            policy: Policy::Learned.name(),
397            source: d.source,
398            fit: s.fit.key.clone(),
399            gate: d.gate,
400            risk: d.risk,
401            xk: d.xk,
402            xs: d.xs,
403            // `graded_under` sets it, for every policy.
404            alpha: None,
405            attention: None,
406            psi,
407        },
408    }
409}
410
411/// `Attention` on a check with a 3: attention.rs's cut from the labels beside `ctx.lessons`, every
412/// `attention::EXPLORE`th graded check there exploring up to `attention::RAIL`. `p` and `pf` are the
413/// explore mixture over the cut before the explore override (attention.rs `propensities`, `free_p`).
414/// 0.5.0's sub-policy as it was: it knows nothing of `Ctx::had`.
415fn by_cut<'a>(applied: &[(&'a Challenge, u8)], ctx: &Ctx) -> Routed<'a> {
416    let outcomes = outcome::read(&outcome::path_beside(ctx.lessons));
417    let base = attention::cut(&attention::labels(&outcomes));
418    let (p, pf) = (attention::propensities(applied.len(), base.k), free_p(applied.len(), base.k));
419    // The cadence counts the checks it counted before 0.6.0 logged every model call: graded ones with
420    // applies whose call answered.
421    let checks = outcomes.iter().filter(|o| o.kind == outcome::GRADED && !o.lessons.is_empty() && !o.failed).count();
422    let cut = match attention::explores(checks) {
423        true => attention::Cut { k: attention::RAIL, source: "explore", ..base },
424        false => base,
425    };
426    let (ask, free) = attention::split(applied, &cut);
427    let log = Log {
428        policy: Policy::Attention.name(),
429        source: cut.source,
430        fit: ctx.summary.fit.key.clone(),
431        attention: Some(attention::line(&cut, applied.len())),
432        ..Log::default()
433    };
434    Routed { ask, free, placebo: Vec::new(), p, pf, log }
435}
436
437/// The learned cut's chance that rank r of `n` applies goes free under the base cut `k`: with
438/// q = 1/`attention::EXPLORE`, c = min(k, RAIL, n) and e = min(RAIL, n), it is (1 − q)·[c ≤ r < c +
439/// CAP] + q·[e ≤ r < e + CAP] (attention.rs `split` hands over at most `conscience::CAP`).
440fn free_p(n: usize, k: usize) -> Vec<f64> {
441    let q = 1.0 / attention::EXPLORE as f64;
442    let (cut, rail) = (k.min(attention::RAIL).min(n), attention::RAIL.min(n));
443    let within = |r: usize, from: usize| f64::from(u8::from(r >= from && r < from + conscience::CAP));
444    (0..n).map(|r| outcome::round_p((1.0 - q) * within(r, cut) + q * within(r, rail))).collect()
445}
446
447/// `routes` (aligned with `applied`) as the ask and free lists, each higher grade first, then in the
448/// model's order: the order the fixed rule asks and hands over in (conscience.rs `graded_capped`).
449fn ordered<'a>(applied: &[(&'a Challenge, u8)], routes: &[Route]) -> (Vec<&'a Challenge>, Vec<&'a Challenge>) {
450    let mut order: Vec<usize> = (0..applied.len().min(routes.len())).collect();
451    order.sort_by_key(|&i| std::cmp::Reverse(applied[i].1));
452    let of = |r: Route| order.iter().filter(|&&i| routes[i] == r).map(|&i| applied[i].0).collect();
453    (of(Route::Ask), of(Route::Free))
454}
455
456/// The seed of a check's draws: the first 8 bytes of blake3 over its session, channel and id, so a
457/// replay of the logged check draws the same, and a stop and a live check of one session never share
458/// draws.
459#[must_use]
460pub fn seed(ctx: &Ctx) -> u64 {
461    let mut h = blake3::Hasher::new();
462    h.update(b"borg-select-seed-v1\0");
463    for part in [ctx.session, ctx.channel, ctx.check] {
464        h.update(part.as_bytes());
465        h.update(b"\0");
466    }
467    let bytes = h.finalize();
468    // Derived: the bytes of the u64 seed.
469    const WIDTH: usize = std::mem::size_of::<u64>();
470    let mut first = [0u8; WIDTH];
471    first.copy_from_slice(&bytes.as_bytes()[..WIDTH]);
472    u64::from_le_bytes(first)
473}
474
475/// The pool budget in tokens for a check of `session` under the policy in force (`policy`,
476/// `pool_tokens_under`).
477#[must_use]
478pub fn pool_tokens(s: &summary::Summary, limit_ms: Option<u64>, session: &str) -> usize {
479    pool_tokens_under(policy(), s, limit_ms, session)
480}
481
482/// `pool_tokens` under `policy`, never the environment's: whole.rs `TOKENS` unless the pool is the
483/// learned policy's (`pool_active`); then the budget its fit chooses (select/policy.rs `pool_tokens`),
484/// never past the baseline without the harness's stop budget for the model call (`limit_ms`) and the
485/// latency evidence to fit in it. Fixed for a session between fits, so its prompt prefix stays stable.
486/// What a replay calls for the policy it replays.
487#[must_use]
488pub fn pool_tokens_under(policy: Policy, s: &summary::Summary, limit_ms: Option<u64>, session: &str) -> usize {
489    policy::pool_tokens(&s.fit, pool_active(policy, s), limit_ms, policy::session_seed(session))
490}
491
492/// Whether `policy` chooses the pool by its fit (`pool_tokens_under`): the learned policy on a store
493/// the monitor did not revert, once its gate may risk something or an engineer set a budget for the
494/// collective. A budget binds from the first fit that knows it, gate open or not: its price replays
495/// the pools as in force (select/policy.rs `price`), so a pool kept at the baseline while the gate is
496/// closed would spend past it. The fixed rule and a reverted store keep the baseline.
497fn pool_active(policy: Policy, s: &summary::Summary) -> bool {
498    policy == Policy::Learned && !s.monitor.reverted && (s.gate.allowance > 0.0 || s.fit.budget.is_some())
499}
500
501/// The engineer's budget for the collective whose store holds `lessons`: its `budget` in
502/// collectives.json (collective.rs `Entry::budget`: pool tokens per session, a positive number), None
503/// when unset (the default) or the config does not load.
504#[must_use]
505pub fn budget_of(lessons: &Path) -> Option<f64> {
506    budget_in(&crate::collective::load().ok().flatten()?, lessons)
507}
508
509/// `budget_of` in the config `cfg`.
510fn budget_in(cfg: &crate::collective::Config, lessons: &Path) -> Option<f64> {
511    entry_in(cfg, lessons)?.budget()
512}
513
514/// The engineer's tolerance for learning in the collective whose store holds `lessons`: its `risk` in
515/// collectives.json (collective.rs `Entry::risk`: the share of the fixed rule's value the randomised
516/// question-count test may risk, 0 to 1, 0 turning it off), None when unset (the default, under which
517/// the policy uses `policy::ALPHA`), not a number in range, or the config does not load.
518#[must_use]
519pub fn risk_of(lessons: &Path) -> Option<f64> {
520    risk_in(&crate::collective::load().ok().flatten()?, lessons)
521}
522
523/// `risk_of` in the config `cfg`.
524fn risk_in(cfg: &crate::collective::Config, lessons: &Path) -> Option<f64> {
525    entry_in(cfg, lessons)?.risk()
526}
527
528/// The learned policy's α for the store that holds `lessons`: the collective's `risk` (`risk_of`),
529/// `policy::ALPHA` unless it sets one. What the hook reads into `Ctx::alpha` at the check; a replay
530/// reads the record's instead (`alpha_of`).
531#[must_use]
532pub fn alpha(lessons: &Path) -> f64 {
533    risk_of(lessons).unwrap_or(policy::ALPHA)
534}
535
536/// The collective of `cfg` whose store holds `lessons`.
537fn entry_in<'a>(cfg: &'a crate::collective::Config, lessons: &Path) -> Option<&'a crate::collective::Entry> {
538    let store = lessons.parent()?;
539    let base = crate::collective::root_of(store);
540    cfg.collectives.iter().find(|c| crate::collective::store(&base, &c.name) == store)
541}
542
543/// Hex digits of a check id. Protocol: the length of the `@<check>` tags hook.rs already writes
544/// (`append_p_of`), 48 bits, so two checks of one store collide with negligible chance.
545pub const CHECK_HEX: usize = 12;
546
547/// A graded check's id: `CHECK_HEX` hex digits of blake3 over its session, channel (`stop`, `live`),
548/// start (epoch ms) and process id. Not a count of the session's checks: a stop and a live check of
549/// one session can run at once and would count the same lines. Computed once per check and logged,
550/// never recomputed.
551#[must_use]
552pub fn check_id(session: &str, channel: &str, start_ms: i64, pid: u32) -> String {
553    let mut h = blake3::Hasher::new();
554    h.update(b"borg-check-v1\0");
555    h.update(format!("{session}\0{channel}\0{start_ms}\0{pid}").as_bytes());
556    h.finalize().to_hex()[..CHECK_HEX].to_string()
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562
563    fn lessons(n: usize) -> Vec<Challenge> {
564        (0..n).map(|i| Challenge { id: format!("L{i:02}"), ..Default::default() }).collect()
565    }
566
567    fn ids(v: &[&Challenge]) -> Vec<String> {
568        v.iter().map(|c| c.id.clone()).collect()
569    }
570
571    /// A temp store: its lessons file (never written) and the outcomes beside it.
572    fn store(name: &str, outcomes: &[outcome::Outcome]) -> std::path::PathBuf {
573        let d = std::env::temp_dir().join(format!("borg-select-{name}-{}", std::process::id()));
574        let _ = std::fs::remove_dir_all(&d);
575        std::fs::create_dir_all(&d).unwrap();
576        let text: String = outcomes.iter().map(|o| format!("{}\n", serde_json::to_string(o).unwrap())).collect();
577        std::fs::write(d.join("outcomes.jsonl"), text).unwrap();
578        d.join("lessons.jsonl")
579    }
580
581    fn ctx<'a>(lessons: &'a Path, s: &'a summary::Summary) -> Ctx<'a> {
582        Ctx { session: "s", check: "0123456789ab", channel: "stop", lessons, summary: s, tokens: 0, had: &[], alpha: policy::ALPHA }
583    }
584
585    /// Every grade sequence of up to `n` applies over {1, 2, 3}.
586    fn tables(n: usize) -> Vec<Vec<u8>> {
587        let mut out = vec![Vec::new()];
588        for len in 1..=n {
589            out.extend((0..3usize.pow(len as u32)).map(|mut x| {
590                (0..len)
591                    .map(|_| {
592                        let g = (x % 3) as u8 + 1;
593                        x /= 3;
594                        g
595                    })
596                    .collect()
597            }));
598        }
599        out
600    }
601
602    #[test]
603    fn the_switches_take_precedence_in_order() {
604        assert_eq!(precedence(None, None, false), Policy::Learned, "the default is the learned policy");
605        assert_eq!(precedence(Some("learned"), None, false), Policy::Learned, "naming it changes nothing");
606        assert_eq!(precedence(Some("fixed"), Some(2), true), Policy::Fixed, "fixed wins over both evaluation switches");
607        assert_eq!(precedence(Some("learned"), Some(2), true), Policy::Cap(2), "the cap wins over attention and learned");
608        assert_eq!(precedence(Some("learned"), None, true), Policy::Attention, "attention wins over learned");
609        assert_eq!(precedence(Some("nonsense"), None, false), Policy::Learned, "an unknown BORG_SELECT is the default, the learned policy");
610        assert_eq!(precedence(Some("Fixed"), None, false), Policy::Learned, "the value is exact");
611        assert_eq!(precedence(Some("fixed"), None, false), Policy::Fixed, "BORG_SELECT=fixed is the way back");
612        assert_eq!([Policy::Fixed, Policy::Cap(1), Policy::Attention, Policy::Learned].map(Policy::name), ["fixed", "cap", "attention", "learned"]);
613    }
614
615    /// The placebo candidates of a decision: the applies whose lesson it delivers neither way, in no
616    /// copy, and the session never had, each once (`draw_placebo`).
617    fn candidates(applied: &[(&Challenge, u8)], r: &Routed, had: &[String]) -> Vec<String> {
618        let mut out: Vec<String> = Vec::new();
619        for (c, _) in applied {
620            if matches!(r.route(&c.id), Route::Not | Route::Placebo) && !had.contains(&c.id) && !out.contains(&c.id) {
621                out.push(c.id.clone());
622            }
623        }
624        out
625    }
626
627    /// Cold start is today's rule: every policy but attention with a 3 decides exactly as
628    /// conscience.rs `graded_without` with the session's `had` (`graded_capped` without one), in its
629    /// order, with its choice as the propensities; the fixed rule and the learned policy draw their
630    /// placebos among what they deliver neither way, the evaluation switches none.
631    #[test]
632    fn every_policy_without_a_cut_is_the_fixed_rule_on_every_grade_table() {
633        let ls = lessons(14);
634        let s = summary::Summary::default();
635        let none = std::path::PathBuf::from("/nonexistent/borg-select/lessons.jsonl");
636        let mut tables = tables(7);
637        // Past `conscience::CAP` asks and hand-overs.
638        tables.extend([vec![3; 12], [vec![1; 6], vec![3], vec![2; 7]].concat(), vec![2; 14], [vec![1; 11], vec![2; 3]].concat()]);
639        // No lesson had (a record from before `nofree`), and some: the first, one past the cap, one
640        // the model did not name.
641        let some: Vec<String> = ["L00", "L03", "L11", "X"].map(String::from).to_vec();
642        for had in [&[][..], &some[..]] {
643            for grades in &tables {
644                let applied: Vec<(&Challenge, u8)> = ls.iter().zip(grades.iter().copied()).collect();
645                let at = Ctx { had, ..ctx(&none, &s) };
646                for (policy, cap) in [(Policy::Fixed, None), (Policy::Learned, None), (Policy::Cap(2), Some(2))] {
647                    let r = graded_under(policy, &applied, &at);
648                    let (ask, free) = conscience::graded_without(&applied, cap, had);
649                    assert_eq!((ids(&r.ask), ids(&r.free)), (ids(&ask), ids(&free)), "{policy:?} {grades:?} {had:?}");
650                    if had.is_empty() {
651                        let (ask, free) = conscience::graded_capped(&applied, cap);
652                        assert_eq!((ids(&r.ask), ids(&r.free)), (ids(&ask), ids(&free)), "no lesson had: the rule of every older record");
653                    }
654                    assert_eq!(r.p, attention::fixed_p(&applied, &ask), "{policy:?} {grades:?}");
655                    assert_eq!(r.pf, attention::fixed_p(&applied, &free), "{policy:?} {grades:?}");
656                    assert!(r.log.attention.is_none() && r.log.gate == 0.0);
657                    assert_eq!(r.log.policy, policy.name());
658                    let later = candidates(&applied, &r, had);
659                    match policy {
660                        Policy::Cap(_) => assert!(r.placebo.is_empty() && r.log.psi == 0.0),
661                        _ => {
662                            assert_eq!(r.placebo.len(), later.len().min(PLACEBO_SLOTS), "{policy:?} {grades:?}");
663                            assert!(ids(&r.placebo).iter().all(|p| later.contains(p)), "{policy:?} {grades:?}");
664                            let psi = if later.is_empty() { 0.0 } else { outcome::round_p((PLACEBO_SLOTS as f64 / later.len() as f64).min(1.0)) };
665                            assert_eq!(r.log.psi, psi);
666                        }
667                    }
668                }
669                if !grades.contains(&3) {
670                    let r = graded_under(Policy::Attention, &applied, &at);
671                    let (ask, free) = conscience::graded_without(&applied, None, had);
672                    assert_eq!((ids(&r.ask), ids(&r.free), r.log.source), (ids(&ask), ids(&free), "fixed"), "no 3: no cut");
673                    assert!(r.placebo.is_empty() && r.log.psi == 0.0, "an evaluation switch draws no placebo");
674                }
675            }
676        }
677    }
678
679    #[test]
680    fn attention_with_a_three_is_the_cut_on_the_outcomes_beside_the_lessons() {
681        // No outcomes beside the lessons: the prior cut, 0 to 10, the rest handed over free.
682        let ls = lessons(14);
683        let s = summary::Summary::default();
684        let grades: Vec<u8> = std::iter::once(3).chain(std::iter::repeat_n(1, 13)).collect();
685        let applied: Vec<(&Challenge, u8)> = ls.iter().zip(grades).collect();
686        let cold = store("attention-cold", &[]);
687        let r = graded_under(Policy::Attention, &applied, &ctx(&cold, &s));
688        assert_eq!((r.ask.len(), ids(&r.free)), (attention::PRIOR_K, ids(&applied[attention::PRIOR_K..].iter().map(|(c, _)| *c).collect::<Vec<_>>())));
689        assert_eq!(r.p, attention::propensities(14, attention::PRIOR_K));
690        assert_eq!(&r.pf[..attention::PRIOR_K], &[0.0; attention::PRIOR_K]);
691        assert_eq!(&r.pf[attention::PRIOR_K..], &[0.9; 3], "free unless the check explores, which asks them");
692        assert_eq!((r.log.policy, r.log.source), ("attention", "prior"));
693        assert_eq!(r.log.attention.as_deref(), Some("#borg-attention k=11 n=0 alpha=0.0010 source=prior applied=14"));
694        // Nine graded checks with applies beside the lessons: this one, the tenth, explores up to the rail.
695        let graded = outcome::Outcome { kind: outcome::GRADED.into(), session: "x".into(), lessons: vec!["L0".into()], ..Default::default() };
696        let nine = store("attention-explore", &vec![graded.clone(); attention::EXPLORE - 1]);
697        let r = graded_under(Policy::Attention, &applied, &ctx(&nine, &s));
698        assert_eq!((r.ask.len(), r.free.len(), r.log.source), (14, 0, "explore"));
699        assert_eq!(r.p, attention::propensities(14, attention::PRIOR_K), "the propensities are the mixture's, not the override's");
700        // A check without applies, or whose call failed, was not counted before 0.6.0 logged one: as the
701        // ninth it does not make this check the tenth.
702        let mut dirs = vec![cold, nine];
703        let (empty, failed) = (outcome::Outcome { lessons: Vec::new(), ..graded.clone() }, outcome::Outcome { failed: true, ..graded.clone() });
704        for (tag, ninth) in [("empty", empty), ("failed", failed)] {
705            let mut log = vec![graded.clone(); attention::EXPLORE - 2];
706            log.push(ninth);
707            let unmoved = store(&format!("attention-{tag}"), &log);
708            assert_eq!(graded_under(Policy::Attention, &applied, &ctx(&unmoved, &s)).log.source, "prior", "{tag}");
709            dirs.push(unmoved);
710        }
711        for d in dirs {
712            let _ = std::fs::remove_dir_all(d.parent().unwrap());
713        }
714    }
715
716    #[test]
717    fn free_propensities_mix_the_cut_and_the_explore_slice() {
718        // 35 applies, cut 3: free 3..13 unless exploring (q = 0.1), then 20..30.
719        let pf = free_p(35, 3);
720        assert_eq!(&pf[..3], &[0.0; 3]);
721        assert_eq!(&pf[3..13], &[0.9; 10]);
722        assert_eq!(&pf[13..20], &[0.0; 7]);
723        assert_eq!(&pf[20..30], &[0.1; 10]);
724        assert_eq!(&pf[30..], &[0.0; 5]);
725        assert_eq!(free_p(5, 11), [0.0; 5], "the cut and the rail ask all five");
726        // Each rank's routes are disjoint: asked and free never add past 1.
727        let p = attention::propensities(35, 3);
728        assert!(p.iter().zip(&pf).all(|(a, f)| a + f <= 1.0 + 1e-9), "{p:?} {pf:?}");
729    }
730
731    #[test]
732    fn a_route_names_the_list_that_holds_the_lesson() {
733        let ls = lessons(3);
734        let applied: Vec<(&Challenge, u8)> = ls.iter().zip([3, 1, 2]).collect();
735        let none = std::path::PathBuf::from("/nonexistent/borg-select/lessons.jsonl");
736        let s = summary::Summary::default();
737        let mut r = graded_under(Policy::Fixed, &applied, &ctx(&none, &s));
738        assert_eq!(ids(&r.ask), ["L00", "L02", "L01"], "3s, then 2s, then 1s");
739        r.free = r.ask.split_off(2);
740        r.placebo = r.free.split_off(0);
741        assert_eq!(["L00", "L01", "L02", "L99"].map(|id| r.route(id)), [Route::Ask, Route::Placebo, Route::Ask, Route::Not]);
742    }
743
744    #[test]
745    fn the_select_line_carries_the_decision_and_its_check() {
746        let mut log = Log { policy: "learned", source: "fixed", ..Log::default() };
747        assert_eq!(log.line(4, "0123456789ab"), "#borg-select policy=learned source=fixed fit=- gate=0.0000 applied=4 @0123456789ab");
748        log.fit = "120-ab12".into();
749        log.gate = 0.25;
750        assert!(log.line(0, "ff").contains(" fit=120-ab12 gate=0.2500 applied=0 @ff"), "{}", log.line(0, "ff"));
751        // Filtered from what the pool and the model read, like every borg marker.
752        assert_eq!(conscience::work_text(&format!("{}\nwork\n", log.line(1, "ab"))), "work\n");
753    }
754
755    /// A fit sure that handing over beats asking, and a gate open wide: the learned policy deviates.
756    fn deviating() -> summary::Summary {
757        let at = |name: &str| policy::NAMES.iter().position(|n| *n == name).unwrap();
758        let mut beta = vec![0.0; policy::P];
759        beta[at("intercept")] = -0.5;
760        beta[at("ask")] = -3.0;
761        beta[at("not")] = -1.0;
762        let mut chol = vec![0.0; policy::P * policy::P];
763        for k in 0..policy::P {
764            chol[k * policy::P + k] = 0.05;
765        }
766        let model = policy::Model { beta, chol, sd: vec![1.0; policy::P], spread: [0.1; 3], ..policy::Model::default() };
767        let rates = policy::Rates { free_stop: 1.0, free_live: 1.0, later: 0.0, wait: 0.0, ask_live: 1.0 };
768        let fit = policy::Fit { key: "9-abcdef01".into(), model: Some(model), rates, ..policy::Fit::default() };
769        let gate = summary::Gate { gain: vec![0.0; policy::DRAWS], allowance: 5.0, ..summary::Gate::default() };
770        summary::Summary { fit, gate, ..summary::Summary::default() }
771    }
772
773    #[test]
774    fn the_learned_policy_routes_by_its_fit_and_logs_it() {
775        let ls = lessons(4);
776        let applied: Vec<(&Challenge, u8)> = ls.iter().zip([3, 1, 2, 1]).collect();
777        let s = deviating();
778        let none = std::path::PathBuf::from("/nonexistent/borg-select/lessons.jsonl");
779        let live = Ctx { channel: "live", ..ctx(&none, &s) };
780        let r = graded_under(Policy::Learned, &applied, &live);
781        assert!(r.ask.is_empty(), "asking loses under this fit: {:?}", ids(&r.ask));
782        assert_eq!(ids(&r.free), ["L00", "L02", "L01", "L03"], "higher grade first, then the model's order");
783        assert_eq!((r.log.policy, r.log.source, r.log.fit.as_str()), ("learned", policy::EVIDENCE, "9-abcdef01"));
784        assert_eq!((r.log.xk, r.log.xs), (None, None), "on evidence the test does not run");
785        assert!(r.log.gate > 0.0);
786        assert!(r.pf.iter().all(|p| *p == 1.0) && r.p.iter().all(|p| *p == 0.0), "exact: {:?} {:?}", r.p, r.pf);
787        // At the stop the 3 is asked whatever the fit says.
788        let r = graded_under(Policy::Learned, &applied, &ctx(&none, &s));
789        assert_eq!((ids(&r.ask), r.p[0]), (vec!["L00".to_string()], 1.0));
790    }
791
792    /// A live check under the randomised question-count test, through the hook's call: the drawn s of
793    /// the fixed rule's k delivered lessons asked (the first s in its order, 2s before 1s), the rest
794    /// handed over, k and s in the log for the graded record; s = 0, the fixed rule, is the control.
795    /// The fit knows asking and handing over to be worth the same, so no evidence moves it.
796    #[test]
797    fn a_live_check_under_the_test_asks_the_drawn_count_and_logs_it() {
798        let ls = lessons(4);
799        let applied: Vec<(&Challenge, u8)> = ls.iter().zip([2, 1, 2, 1]).collect();
800        let mut s = deviating();
801        if let Some(m) = s.fit.model.as_mut() {
802            m.beta = vec![0.0; policy::P];
803            m.beta[policy::NAMES.iter().position(|n| *n == "not").unwrap()] = -1.0;
804        }
805        let none = std::path::PathBuf::from("/nonexistent/borg-select/lessons.jsonl");
806        let order = ["L00", "L02", "L01", "L03"];
807        let mut seen = std::collections::BTreeSet::new();
808        for c in 0..40u32 {
809            let check = format!("{c:012x}");
810            let r = graded_under(Policy::Learned, &applied, &Ctx { channel: "live", check: &check, ..ctx(&none, &s) });
811            let (Some(k), Some(x)) = (r.log.xk, r.log.xs) else { panic!("check {c}: the test ran: {:?}", r.log) };
812            let x = x as usize;
813            assert_eq!(k, 4, "every lesson the fixed rule hands over is tested");
814            let named = |v: &[&str]| v.iter().map(|x| x.to_string()).collect::<Vec<_>>();
815            assert_eq!((ids(&r.ask), ids(&r.free)), (named(&order[..x]), named(&order[x..])), "check {c}");
816            assert_eq!(r.log.source, if x == 0 { policy::EXPERIMENT_CONTROL } else { policy::EXPERIMENT }, "check {c}");
817            assert_eq!(
818                (r.p.as_slice(), r.pf.as_slice()),
819                ([0.8, 0.4, 0.6, 0.2].as_slice(), [0.2, 0.6, 0.4, 0.8].as_slice()),
820                "each unit's share of the five counts"
821            );
822            seen.insert(x);
823        }
824        assert!(seen.len() > 2, "the count varies with the check: {seen:?}");
825        // At the stop the test never runs: the fixed rule, nothing logged.
826        let r = graded_under(Policy::Learned, &applied, &ctx(&none, &s));
827        assert_eq!((r.log.source, r.log.xk, r.log.xs, r.ask.len()), (policy::EVIDENCE, None, None, 0));
828    }
829
830    /// The tolerance is the check's (`Ctx::alpha`, which the hook reads and a replay takes from the
831    /// record), never the config's for the path decided on: here a path no collective maps, whose config
832    /// α is `policy::ALPHA`. At 0 the test is off and the live check keeps the fixed rule; the α is
833    /// logged under every policy unless it is the default, and reads back as it was decided.
834    #[test]
835    fn a_check_decides_and_logs_under_the_tolerance_in_its_ctx() {
836        let ls = lessons(4);
837        let applied: Vec<(&Challenge, u8)> = ls.iter().zip([2, 1, 2, 1]).collect();
838        let mut s = deviating();
839        if let Some(m) = s.fit.model.as_mut() {
840            m.beta = vec![0.0; policy::P];
841            m.beta[policy::NAMES.iter().position(|n| *n == "not").unwrap()] = -1.0;
842        }
843        let none = std::path::PathBuf::from("/nonexistent/borg-select/lessons.jsonl");
844        assert_eq!(alpha(&none), policy::ALPHA, "the config's α for this path");
845        let live = Ctx { channel: "live", ..ctx(&none, &s) };
846        let at = |alpha: f64| graded_under(Policy::Learned, &applied, &Ctx { alpha, ..live });
847        let default = at(policy::ALPHA);
848        assert_eq!((default.log.xk, default.log.alpha), (Some(4), None), "the test runs; the default is not logged");
849        let off = at(0.0);
850        assert_eq!((off.log.source, off.log.xk, off.log.xs, off.log.alpha), (policy::EVIDENCE, None, None, Some(0.0)), "{:?}", off.log);
851        assert_eq!((off.ask.len(), off.free.len()), (0, 4), "the fixed rule");
852        let wider = at(0.3);
853        assert_eq!((wider.log.xk, wider.log.alpha), (Some(4), Some(0.3)));
854        let fixed = graded_under(Policy::Fixed, &applied, &Ctx { alpha: 0.0, ..live });
855        assert_eq!(fixed.log.alpha, Some(0.0), "logged under every policy, for the cold start");
856        for a in [0.0, 0.3, policy::ALPHA] {
857            assert_eq!(alpha_of(logged_alpha(a)), a);
858            let replayed = at(alpha_of(at(a).log.alpha));
859            assert_eq!((replayed.log, replayed.p, replayed.pf), (at(a).log, at(a).p, at(a).pf), "α {a} replays as decided");
860        }
861    }
862
863    /// Guard: a store the monitor reverted keeps the fixed rule, whatever its fit and gate say.
864    #[test]
865    fn a_reverted_store_keeps_the_fixed_rule() {
866        let ls = lessons(4);
867        let applied: Vec<(&Challenge, u8)> = ls.iter().zip([3, 1, 2, 1]).collect();
868        let mut s = deviating();
869        s.monitor.reverted = true;
870        let none = std::path::PathBuf::from("/nonexistent/borg-select/lessons.jsonl");
871        let r = graded_under(Policy::Learned, &applied, &Ctx { channel: "live", ..ctx(&none, &s) });
872        let (ask, free) = conscience::graded_capped(&applied, None);
873        assert_eq!((ids(&r.ask), ids(&r.free)), (ids(&ask), ids(&free)));
874        assert_eq!((r.log.source, r.log.gate), (REVERTED_SOURCE, 0.0));
875        assert_eq!(r.p, attention::fixed_p(&applied, &ask));
876        // The fixed rule for a session that had a lesson: never handed over free again.
877        let had = ["L01".to_string()];
878        let r = graded_under(Policy::Learned, &applied, &Ctx { channel: "live", had: &had, ..ctx(&none, &s) });
879        assert_eq!(ids(&r.free), ids(&conscience::graded_without(&applied, None, &had).1));
880        assert_eq!((r.log.source, r.pf[1]), (REVERTED_SOURCE, 0.0));
881    }
882
883    /// A lesson the session already had may be asked again but never takes a free slot: the next 2 or
884    /// 1 takes it, under the fixed rule and the learned policy alike, its `pf` 0. What the record logs
885    /// (`nofree`) is all a replay needs of `had`.
886    #[test]
887    fn a_lesson_the_session_had_is_asked_but_never_handed_over_free() {
888        let ls = lessons(12);
889        let none = std::path::PathBuf::from("/nonexistent/borg-select/lessons.jsonl");
890        let s = summary::Summary::default();
891        // A 2 the session had and eleven 1s: without it the cap hands over L00..L09; with it L01..L10.
892        let soft: Vec<(&Challenge, u8)> = ls.iter().zip(std::iter::once(2).chain(std::iter::repeat_n(1, 11))).collect();
893        let had = ["L00".to_string(), "X9".to_string()];
894        let free = |had: &[String]| ids(&conscience::graded_without(&soft, None, had).1);
895        assert_eq!(free(&[]), ids(&ls[..10].iter().collect::<Vec<_>>()));
896        assert_eq!(free(&had), ids(&ls[1..11].iter().collect::<Vec<_>>()), "a 2 the session had takes no slot; the next 1 does");
897        for policy in [Policy::Fixed, Policy::Learned] {
898            let r = graded_under(policy, &soft, &Ctx { had: &had, ..ctx(&none, &s) });
899            assert_eq!((ids(&r.free), r.pf[0]), (free(&had), 0.0), "{policy:?}");
900            assert!(!ids(&r.placebo).contains(&"L00".to_string()), "{policy:?}: a lesson the session had is no placebo");
901        }
902        // With a 3 it is asked as before.
903        let block: Vec<(&Challenge, u8)> = ls.iter().take(3).zip([3, 2, 1]).collect();
904        let ask = |had: &[String]| ids(&conscience::graded_without(&block, None, had).0);
905        assert_eq!(ask(&["L01".to_string()]), ask(&[]), "may still be asked");
906        // The learned policy under a fit that hands everything over, at the stop (no test): not the one
907        // it had.
908        let open = deviating();
909        let applied: Vec<(&Challenge, u8)> = ls.iter().take(4).zip([2, 1, 2, 1]).collect();
910        let live = ctx(&none, &open);
911        let all = graded_under(Policy::Learned, &applied, &live);
912        assert_eq!((all.log.source, all.route("L01")), (policy::EVIDENCE, Route::Free), "the scenario: without it, L01 goes free");
913        let had = ["L01".to_string()];
914        let r = graded_under(Policy::Learned, &applied, &Ctx { had: &had, ..live });
915        assert_eq!((r.route("L01"), r.pf[1]), (Route::Not, 0.0), "{:?}", r.pf);
916        assert_eq!(ids(&r.free), ["L00", "L02", "L03"]);
917        // `nofree`: the applies among `had`, each once, in the applies' order.
918        let twice: Vec<(&Challenge, u8)> = [&ls[3], &ls[1], &ls[3], &ls[2]].into_iter().map(|c| (c, 1)).collect();
919        assert_eq!(nofree(&twice, &["L02".to_string(), "L03".to_string(), "L99".to_string()]), ["L03", "L02"]);
920        assert!(nofree(&twice, &[]).is_empty());
921    }
922
923    /// The fixed rule's placebo: up to `PLACEBO_SLOTS` of the applies it delivers neither way (past
924    /// `CAP`), drawn by the check's seed with ψ = min(1, slots / candidates), never a lesson the session
925    /// had or one another copy delivers; the evaluation switches draw none. What it asks and hands over
926    /// is the same with or without it.
927    #[test]
928    fn the_fixed_rule_draws_its_placebos_among_what_it_delivers_neither_way() {
929        let ls = lessons(14);
930        let none = std::path::PathBuf::from("/nonexistent/borg-select/lessons.jsonl");
931        let s = summary::Summary::default();
932        fn fixed<'a>(applied: &[(&'a Challenge, u8)], had: &[String], check: &str) -> Routed<'a> {
933            let (s, none) = (summary::Summary::default(), std::path::PathBuf::from("/nonexistent/borg-select/lessons.jsonl"));
934            graded_under(Policy::Fixed, applied, &Ctx { check, had, ..ctx(&none, &s) })
935        }
936        // A 3 and eleven 1s: ten asked, the two left both drawn.
937        let block: Vec<(&Challenge, u8)> = ls.iter().take(12).zip(std::iter::once(3).chain(std::iter::repeat_n(1, 11))).collect();
938        let r = fixed(&block, &[], "0123456789ab");
939        assert_eq!((r.ask.len(), ids(&r.placebo), r.log.psi), (conscience::CAP, vec!["L10".to_string(), "L11".to_string()], 1.0));
940        // Fourteen 1s: ten free, two of the four left, each with ψ = 0.5, uniformly over the checks.
941        let soft: Vec<(&Challenge, u8)> = ls.iter().map(|c| (c, 1)).collect();
942        let mut drawn = std::collections::BTreeMap::<String, u32>::new();
943        let checks = 200;
944        for c in 0..checks {
945            let check = format!("{c:012x}");
946            let r = fixed(&soft, &[], &check);
947            assert_eq!((r.free.len(), r.placebo.len(), r.log.psi), (conscience::CAP, PLACEBO_SLOTS, 0.5));
948            assert_eq!(ids(&r.placebo), ids(&fixed(&soft, &[], &check).placebo), "the check's seed decides");
949            assert!(r.placebo.windows(2).all(|w| w[0].id < w[1].id), "in the applies' order");
950            for p in &r.placebo {
951                *drawn.entry(p.id.clone()).or_default() += 1;
952            }
953        }
954        assert_eq!(drawn.keys().cloned().collect::<Vec<_>>(), ["L10", "L11", "L12", "L13"]);
955        assert!(drawn.values().all(|&n| (60..=140).contains(&n)), "each about half the checks: {drawn:?}");
956        // A lesson the session had is no candidate: three left, ψ = 2/3.
957        let had = ["L12".to_string()];
958        for c in 0..20 {
959            let r = fixed(&soft, &had, &format!("{c:012x}"));
960            assert_eq!(r.log.psi, outcome::round_p(2.0 / 3.0));
961            assert!(!ids(&r.placebo).contains(&had[0]), "{:?}", ids(&r.placebo));
962        }
963        // A lesson the check delivers in another copy, or names twice past the cap, is one candidate at most.
964        let named: Vec<(&Challenge, u8)> = ls.iter().take(10).chain([&ls[0], &ls[12], &ls[12]]).map(|c| (c, 1)).collect();
965        let r = fixed(&named, &[], "0123456789ab");
966        assert_eq!((ids(&r.placebo), r.log.psi), (vec!["L12".to_string()], 1.0));
967        // The evaluation switches draw none; the learned policy at cold start draws the same.
968        for policy in [Policy::Cap(1), Policy::Attention] {
969            let r = graded_under(policy, &soft, &ctx(&none, &s));
970            assert!(r.placebo.is_empty() && r.log.psi == 0.0, "{policy:?}");
971        }
972        let cold = graded_under(Policy::Learned, &soft, &ctx(&none, &s));
973        assert_eq!((ids(&cold.placebo), cold.log.psi), (ids(&graded_under(Policy::Fixed, &soft, &ctx(&none, &s)).placebo), 0.5));
974    }
975
976    /// Guard: the pool is the learned policy's only once its gate may risk something or an engineer set
977    /// a budget, never under the fixed rule or on a reverted store. The same fit grows the pool when
978    /// active, so the condition, not the fit, keeps the baseline.
979    #[test]
980    fn the_pool_is_the_learned_policys_once_its_gate_opens_or_a_budget_binds() {
981        let pool = policy::PoolFit {
982            yields: vec![(50.0, 10.0); 8],
983            per_lesson: 300.0,
984            latency: (1000.0, 0.1, 500.0),
985            latency_n: policy::LATENCY_MIN as u32,
986            latency_reach: 24_000,
987            not: (-1.0, 0.1),
988            ..policy::PoolFit::default()
989        };
990        let base = crate::whole::TOKENS;
991        let limit = Some(60_000);
992        let unpriced = policy::Fit { pool: pool.clone(), ..policy::Fit::default() };
993        let grows = (0..20).map(|i| format!("s{i}")).find(|x| policy::pool_tokens(&unpriced, true, limit, policy::session_seed(x)) > base);
994        let session = grows.expect("the scenario: an active pool grows for some session");
995        let at = |policy: Policy, s: &summary::Summary| (pool_active(policy, s), pool_tokens_under(policy, s, limit, &session));
996        let mut s = summary::Summary { fit: unpriced, ..summary::Summary::default() };
997        assert_eq!(at(Policy::Learned, &s), (false, base), "gate closed, no budget");
998        s.gate.allowance = 1.0;
999        assert!(at(Policy::Learned, &s).0 && at(Policy::Learned, &s).1 > base, "gate open");
1000        for policy in [Policy::Fixed, Policy::Cap(2), Policy::Attention] {
1001            assert_eq!(at(policy, &s), (false, base), "{policy:?}");
1002        }
1003        s.monitor.reverted = true;
1004        assert_eq!(at(Policy::Learned, &s), (false, base), "reverted");
1005        // A budget priced below the baseline binds with the gate closed.
1006        let mut s = summary::Summary { fit: policy::Fit { pool, budget: Some(1.0), lambda: 1e-2, ..policy::Fit::default() }, ..summary::Summary::default() };
1007        let (active, tokens) = at(Policy::Learned, &s);
1008        assert!(active && tokens < base, "budget, gate closed: {tokens}");
1009        assert_eq!(at(Policy::Fixed, &s), (false, base), "the fixed rule keeps the baseline under a budget");
1010        s.monitor.reverted = true;
1011        assert_eq!(at(Policy::Learned, &s), (false, base), "reverted under a budget");
1012    }
1013
1014    /// The gate's guarantee on the hook's path: a learned check decides at its pool tokens, for the
1015    /// lessons its session had, and the ledger (select/summary.rs fold, select/policy.rs `ledger`)
1016    /// re-values it at the same logged tokens against the same fixed rule (`ledger_add_without` with the
1017    /// record's `nofree`), so each check's risk is what its ledger entry takes and the budget never falls
1018    /// below 0. The fit has a pool slope and every pool is a quarter of the baseline's: deciding at the
1019    /// baseline's pool instead admits gains the ledger then does not record.
1020    #[test]
1021    fn the_gate_admits_at_the_pool_its_ledger_records() {
1022        let at = |name: &str| policy::NAMES.iter().position(|n| *n == name).unwrap();
1023        let mut beta = vec![0.0; policy::P];
1024        for (name, b) in [("intercept", -0.5), ("ask", -0.4), ("not", -0.3), ("ask_dilution", -0.1), ("free_dilution", -0.1), ("pool", -1.0)] {
1025            beta[at(name)] = b;
1026        }
1027        // Wide on asking, so the test's counts risk much and the evidence rule never deviates; the pool
1028        // slope drawn ≤ 0.
1029        let mut chol = vec![0.0; policy::P * policy::P];
1030        for k in 0..policy::P {
1031            chol[k * policy::P + k] = if k == at("ask") {
1032                1.0
1033            } else if k == at("pool") {
1034                0.5
1035            } else {
1036                0.3
1037            };
1038        }
1039        let model = policy::Model { beta, chol, sd: vec![1.0; policy::P], spread: [0.3; 3], ..policy::Model::default() };
1040        let rates = policy::Rates { free_stop: 1.0, free_live: 1.0, later: 0.5, wait: 0.0, ask_live: 1.0 };
1041        let fit = policy::Fit { key: "9-pool0001".into(), model: Some(model), rates, ..policy::Fit::default() };
1042        let mut s = summary::Summary { fit, ..summary::Summary::default() };
1043        let tokens = u32::try_from(crate::whole::TOKENS / 4).unwrap();
1044        let (ls, none) = (lessons(10), std::path::PathBuf::from("/nonexistent/borg-select/lessons.jsonl"));
1045        let mut rng = crate::memory::Rng::new(77);
1046        let mut deviated = 0;
1047        for c in 0..400u64 {
1048            let (n, from) = (1 + (rng.next_u64() % 3) as usize, (rng.next_u64() % 10) as usize);
1049            let applied: Vec<(&Challenge, u8)> = (0..n).map(|i| (&ls[(from + i) % 10], 1 + (rng.next_u64() % 3) as u8)).collect();
1050            let (session, check, stop) = (format!("t{}", c / 10), format!("{c:012x}"), c % 5 == 0);
1051            // Every other check's session already had one of the ten lessons.
1052            let had: Vec<String> = if c % 2 == 0 { vec![ls[(rng.next_u64() % 10) as usize].id.clone()] } else { Vec::new() };
1053            let ctx = Ctx {
1054                session: &session,
1055                check: &check,
1056                channel: if stop { "stop" } else { "live" },
1057                lessons: &none,
1058                summary: &s,
1059                tokens,
1060                had: &had,
1061                alpha: policy::ALPHA,
1062            };
1063            let r = graded_under(Policy::Learned, &applied, &ctx);
1064            let us = units(&applied);
1065            let routes: Vec<Route> = applied.iter().map(|(l, _)| r.route(&l.id)).collect();
1066            assert!(applied.iter().zip(&r.pf).all(|((l, _), pf)| !had.contains(&l.id) || (r.route(&l.id) != Route::Free && *pf == 0.0)), "check {c}");
1067            let nofree = nofree(&applied, &had);
1068            deviated += usize::from(routes.iter().map(|x| if *x == Route::Placebo { Route::Not } else { *x }).ne(policy::fixed_routes(&us, &nofree)));
1069            let held = |g: &summary::Gate| policy::remaining(g) - g.allowance;
1070            let before = held(&s.gate);
1071            // The record as the hook logs it (hook.rs `graded_outcome`), folded as every reader folds
1072            // it (select/summary.rs `fold`): the ledger entry is the fold's own.
1073            let ids = |v: &[&Challenge]| v.iter().map(|c| c.id.clone()).collect::<Vec<_>>();
1074            let o = crate::outcome::Outcome {
1075                kind: crate::outcome::GRADED.into(),
1076                session: session.clone(),
1077                check: check.clone(),
1078                channel: ctx.channel.into(),
1079                lessons: applied.iter().map(|(c, _)| c.id.clone()).collect(),
1080                grades: applied.iter().map(|(_, g)| *g).collect(),
1081                ranks: (0..n as u16).collect(),
1082                applied: n as u16,
1083                chosen: ids(&r.ask),
1084                free: ids(&r.free),
1085                placebo: ids(&r.placebo),
1086                p: r.p.clone(),
1087                pf: r.pf.clone(),
1088                psi: r.log.psi,
1089                nofree,
1090                policy: r.log.policy.into(),
1091                fit: r.log.fit.clone(),
1092                source: r.log.source.into(),
1093                tokens,
1094                ..Default::default()
1095            };
1096            s = summary::fold(&format!("{}\n", serde_json::to_string(&o).unwrap()), s);
1097            assert!(
1098                (r.log.risk - (before - held(&s.gate)).max(0.0)).abs() < 1e-9,
1099                "check {c}: risked {} but the ledger took {}",
1100                r.log.risk,
1101                before - held(&s.gate)
1102            );
1103            assert!(policy::remaining(&s.gate) >= -1e-9, "check {c}: the gate overspent, {}", policy::remaining(&s.gate));
1104        }
1105        assert!(deviated > 0, "the learned policy deviated, so the gate was put to the test");
1106        // A refit replays the ledger over the summary (select/policy.rs `ledger`) as the folds built it,
1107        // under each check's `nofree`.
1108        assert_eq!(policy::ledger(&s), s.gate, "the refit's ledger is the folds'");
1109    }
1110
1111    #[test]
1112    fn a_budget_is_read_for_the_collective_whose_store_it_is() {
1113        let base = Path::new("/home/x/.local/share/borg");
1114        let lessons = crate::collective::store(base, "team").join("lessons.jsonl");
1115        let cfg = |entry: &str| {
1116            serde_json::from_str::<crate::collective::Config>(&format!(r#"{{"collectives":[{{"name":"other","paths":[],"budget":5}},{entry}]}}"#)).unwrap()
1117        };
1118        assert_eq!(budget_in(&cfg(r#"{"name":"team","paths":[],"budget":400000}"#), &lessons), Some(400_000.0));
1119        assert_eq!(budget_in(&cfg(r#"{"name":"team","paths":[]}"#), &lessons), None, "off by default");
1120        assert_eq!(budget_in(&cfg(r#"{"name":"team","paths":[],"budget":0}"#), &lessons), None);
1121        assert_eq!(budget_in(&cfg(r#"{"name":"team","paths":[],"budget":"lots"}"#), &lessons), None);
1122    }
1123
1124    #[test]
1125    fn a_risk_is_read_for_the_collective_whose_store_it_is_and_only_as_a_share() {
1126        let base = Path::new("/home/x/.local/share/borg");
1127        let lessons = crate::collective::store(base, "team").join("lessons.jsonl");
1128        let cfg = |entry: &str| {
1129            serde_json::from_str::<crate::collective::Config>(&format!(r#"{{"collectives":[{{"name":"other","paths":[],"risk":0.5}},{entry}]}}"#)).unwrap()
1130        };
1131        assert_eq!(risk_in(&cfg(r#"{"name":"team","paths":[],"risk":0.2}"#), &lessons), Some(0.2));
1132        assert_eq!(risk_in(&cfg(r#"{"name":"team","paths":[],"risk":0}"#), &lessons), Some(0.0), "0 turns the test off");
1133        assert_eq!(risk_in(&cfg(r#"{"name":"team","paths":[],"risk":1}"#), &lessons), Some(1.0));
1134        assert_eq!(risk_in(&cfg(r#"{"name":"team","paths":[]}"#), &lessons), None, "unset: the policy's ALPHA");
1135        for bad in ["-0.1", "1.5", "\"lots\"", "null"] {
1136            assert_eq!(risk_in(&cfg(&format!(r#"{{"name":"team","paths":[],"risk":{bad}}}"#)), &lessons), None, "{bad}");
1137        }
1138    }
1139
1140    #[test]
1141    fn a_check_id_is_short_stable_hex_and_differs_per_check() {
1142        let id = check_id("s", "stop", 1_000, 42);
1143        assert_eq!(id.len(), CHECK_HEX);
1144        assert!(id.bytes().all(|b| b.is_ascii_hexdigit()), "{id}");
1145        assert_eq!(id, check_id("s", "stop", 1_000, 42), "stable");
1146        let others = [check_id("s", "live", 1_000, 42), check_id("s", "stop", 1_001, 42), check_id("s", "stop", 1_000, 43), check_id("t", "stop", 1_000, 42)];
1147        assert!(others.iter().all(|o| *o != id), "{others:?}");
1148        // The separators keep the parts apart: ("ab", "c") is not ("a", "bc").
1149        assert_ne!(check_id("ab", "c", 1, 1), check_id("a", "bc", 1, 1));
1150    }
1151
1152    #[test]
1153    fn a_seed_depends_on_session_channel_and_check() {
1154        let (s, none) = (summary::Summary::default(), std::path::PathBuf::new());
1155        let at = |session, channel, check| seed(&Ctx { session, check, channel, lessons: &none, summary: &s, tokens: 0, had: &[], alpha: policy::ALPHA });
1156        assert_eq!(at("s", "stop", "a1"), at("s", "stop", "a1"));
1157        assert_ne!(at("s", "stop", "a1"), at("s", "live", "a1"));
1158        assert_ne!(at("s", "stop", "a1"), at("s", "stop", "a2"));
1159        assert_ne!(at("s", "stop", "a1"), at("t", "stop", "a1"));
1160    }
1161}