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