Skip to main content

cortiq_engine/
router.rs

1//! Recon-argmin skill routing (spec §9, P1 signal-consistency): the
2//! container's selection descriptors define per-skill affine subspaces
3//! over φ(x); the winner is the skill that reconstructs φ best. No
4//! trained gate — routing is a property of the skills themselves.
5//!
6//! The decision layer is the debugged cortiq-router recipe (the task-routing
7//! service, `cortiq-bot/cortiq-router/src/router.rs`): raw squared
8//! reconstruction error per skill, a temperature-calibrated softmax over
9//! −error for the confidence, and a NOVELTY ENSEMBLE of three independent
10//! OOD signals — the winner's error as a z-score against its own training
11//! shell, the leader margin, and the calibrated confidence — thresholded by
12//! θ that was set to the (1−fpr) quantile of in-scope held-out scores.
13//! Files without the calibration fall back to the normalized error E with a
14//! fixed threshold (the pre-calibration behaviour, unchanged).
15//!
16//! Router policy v2 (`header.router`, bit ROUTER_V2, spec §9.4) sits on top
17//! of the same recipe: the BACKBONE is a class with its own descriptor, the
18//! calibration is fitted over skills + backbone, and a skill runs only when
19//! [`decide_backbone_gated`] says so — everything else, including a stale
20//! calibration, runs the backbone. v2 skill records are never routed by
21//! the legacy argmin ([`error_rows`] skips them).
22
23use crate::pipeline::Pipeline;
24use base64::Engine as _;
25use cortiq_core::format::{CmfHeader, RoutingCalibration, SelectionDescriptor, SkillRecord};
26use cortiq_core::knowledge::{BACKBONE_CLASS_ID, METRIC_MSE_UNIT, parse_hex64};
27use cortiq_core::quant::{f16_to_f32, f32_to_f16};
28use cortiq_core::{CmfModel, PhiSpec, RouterPolicy, hash64};
29
30/// Ensemble weights and margin sharpness (cortiq-router constants).
31pub const NOVELTY_W_ENERGY: f32 = 0.5;
32pub const NOVELTY_W_MARGIN: f32 = 0.25;
33pub const NOVELTY_W_CONF: f32 = 0.25;
34pub const NOVELTY_MARGIN_K: f32 = 8.0;
35
36#[derive(Debug, Clone)]
37pub struct SkillRoute {
38    pub id: String,
39    /// Normalized reconstruction error E = ‖r − BBᵀr‖²/‖φ‖² ∈ [0, 1]; lower = closer.
40    pub error: f32,
41    /// Raw squared reconstruction error (the calibrated recipe's quantity).
42    pub raw_error: f32,
43    /// Calibrated probability (temperature softmax over −raw_error) — 0 when
44    /// the file carries no calibration.
45    pub probability: f32,
46}
47
48/// The full routing decision for one prompt.
49#[derive(Debug, Clone)]
50pub struct Routing {
51    /// best-first
52    pub scores: Vec<SkillRoute>,
53    /// winner's calibrated confidence (0 without calibration)
54    pub confidence: f32,
55    /// leader margin in `1/(1+err)` units
56    pub margin: f32,
57    /// novelty ensemble score ∈ [0,1] (NaN without calibration)
58    pub novelty: f32,
59    /// OOD verdict: calibrated θ when present, else E_min > `fallback_tau`
60    pub is_novel: bool,
61    pub calibrated: bool,
62}
63
64impl Routing {
65    pub fn winner(&self) -> Option<&SkillRoute> {
66        self.scores.first()
67    }
68}
69
70pub fn decode_f16(b64: &str) -> Option<Vec<f32>> {
71    let bytes = base64::engine::general_purpose::STANDARD.decode(b64).ok()?;
72    Some(
73        bytes
74            .chunks_exact(2)
75            .map(|c| f16_to_f32(u16::from_le_bytes([c[0], c[1]])))
76            .collect(),
77    )
78}
79
80fn sigmoid(x: f32) -> f32 {
81    1.0 / (1.0 + (-x).exp())
82}
83
84fn softmax(v: &mut [f32]) {
85    if v.is_empty() {
86        return;
87    }
88    let mx = v.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
89    let mut s = 0.0f32;
90    for x in v.iter_mut() {
91        *x = (*x - mx).exp();
92        s += *x;
93    }
94    for x in v.iter_mut() {
95        *x /= s.max(1e-30);
96    }
97}
98
99/// Raw squared reconstruction error of φ against a (mean, basis rows) subspace.
100pub fn recon_error(phi: &[f32], mean: &[f32], basis: &[f32], rank: usize) -> f32 {
101    let hidden = phi.len();
102    let r: Vec<f32> = phi.iter().zip(mean).map(|(p, m)| p - m).collect();
103    let rr: f32 = r.iter().map(|v| v * v).sum();
104    let mut proj = 0f32;
105    for k in 0..rank {
106        let row = &basis[k * hidden..(k + 1) * hidden];
107        let c: f32 = row.iter().zip(&r).map(|(b, v)| b * v).sum();
108        proj += c * c;
109    }
110    (rr - proj).max(0.0)
111}
112
113/// Decision from per-skill (id, raw_error, err_mean, err_std, ‖φ‖²) rows —
114/// the pure recipe, shared by `route_full` and the file-level calibration.
115pub fn decide(
116    rows: &[ErrorRow],
117    calib: Option<&cortiq_core::format::RoutingCalibration>,
118    fallback_tau: f32,
119) -> Routing {
120    let mut idx: Vec<usize> = (0..rows.len()).collect();
121    idx.sort_by(|&a, &b| rows[a].1.total_cmp(&rows[b].1));
122    let mut scores: Vec<SkillRoute> = idx
123        .iter()
124        .map(|&i| SkillRoute {
125            id: rows[i].0.clone(),
126            error: rows[i].1 / rows[i].4.max(1e-12),
127            raw_error: rows[i].1,
128            probability: 0.0,
129        })
130        .collect();
131    if scores.is_empty() {
132        return Routing {
133            scores,
134            confidence: 0.0,
135            margin: 0.0,
136            novelty: 1.0,
137            is_novel: true,
138            calibrated: calib.is_some(),
139        };
140    }
141    let (Some(c), Some(&top)) = (calib, idx.first()) else {
142        let e_min = scores[0].error;
143        return Routing {
144            scores,
145            confidence: 0.0,
146            margin: 0.0,
147            novelty: f32::NAN,
148            is_novel: e_min > fallback_tau,
149            calibrated: false,
150        };
151    };
152    // confidence: temperature softmax over −raw_error
153    let mut logits: Vec<f32> = idx
154        .iter()
155        .map(|&i| -rows[i].1 / c.temperature.max(1e-3))
156        .collect();
157    softmax(&mut logits);
158    for (s, p) in scores.iter_mut().zip(&logits) {
159        s.probability = *p;
160    }
161    let confidence = logits[0];
162    // margin in 1/(1+err) units
163    let inv = |e: f32| 1.0 / (1.0 + e);
164    let margin = if idx.len() > 1 {
165        inv(rows[idx[0]].1) - inv(rows[idx[1]].1)
166    } else {
167        inv(rows[idx[0]].1)
168    };
169    // energy: winner z-score against its training shell
170    let (em, es) = (
171        rows[top].2.unwrap_or(0.0),
172        rows[top].3.unwrap_or(1.0).max(1e-4),
173    );
174    let z = (rows[top].1 - em) / es;
175    let novelty = NOVELTY_W_ENERGY * sigmoid(z)
176        + NOVELTY_W_MARGIN / (1.0 + margin * NOVELTY_MARGIN_K)
177        + NOVELTY_W_CONF * (1.0 - confidence);
178    Routing {
179        scores,
180        confidence,
181        margin,
182        novelty,
183        is_novel: novelty > c.novelty_theta,
184        calibrated: true,
185    }
186}
187
188/// Per-skill error rows for a φ (skills with malformed descriptors skipped).
189pub fn error_rows(
190    model: &CmfModel,
191    phi_of_layer: &mut dyn FnMut(usize) -> Vec<f32>,
192) -> Vec<ErrorRow> {
193    let hidden = model.arch().hidden_size;
194    let mut rows = Vec::new();
195    for skill in &model.header.skills {
196        // v2 records route only through the backbone-gated policy — never
197        // through argmin-over-skills, where a single skill wins every
198        // prompt.
199        if skill.is_v2() {
200            continue;
201        }
202        let Some(sel) = &skill.selection else {
203            continue;
204        };
205        let unit = match sel.metric.as_str() {
206            "mse" => false,
207            "mse_unit" => true,
208            m => {
209                tracing::warn!("skill '{}': unknown metric '{}'", skill.id, m);
210                continue;
211            }
212        };
213        let mut phi = phi_of_layer(sel.phi_layer);
214        if unit {
215            let n = phi.iter().map(|x| x * x).sum::<f32>().sqrt().max(1e-12);
216            for x in phi.iter_mut() {
217                *x /= n;
218            }
219        }
220        let (Some(mean), Some(basis)) = (decode_f16(&sel.mean), decode_f16(&sel.basis)) else {
221            tracing::error!("skill '{}': malformed selection payload", skill.id);
222            continue;
223        };
224        if mean.len() != hidden || basis.len() != sel.rank * hidden || phi.len() != hidden {
225            tracing::error!("skill '{}': selection dims mismatch", skill.id);
226            continue;
227        }
228        let e = recon_error(&phi, &mean, &basis, sel.rank);
229        let pp: f32 = phi.iter().map(|v| v * v).sum();
230        rows.push((skill.id.clone(), e, sel.err_mean, sel.err_std, pp));
231    }
232    rows
233}
234
235/// Full decision for a prompt.
236pub fn route_full(
237    model: &CmfModel,
238    pipeline: &mut Pipeline,
239    ids: &[u32],
240    fallback_tau: f32,
241) -> Routing {
242    let mut phi_cache: Vec<(usize, Vec<f32>)> = Vec::new();
243    let mut phi_of = |layer: usize| -> Vec<f32> {
244        if let Some((_, p)) = phi_cache.iter().find(|(l, _)| *l == layer) {
245            return p.clone();
246        }
247        let p = pipeline.probe_phi(ids, layer);
248        phi_cache.push((layer, p.clone()));
249        p
250    };
251    let rows = error_rows(model, &mut phi_of);
252    decide(&rows, model.header.routing.as_ref(), fallback_tau)
253}
254
255/// Score every routable skill; sorted best-first (compatibility API).
256pub fn route(model: &CmfModel, pipeline: &mut Pipeline, ids: &[u32]) -> Vec<SkillRoute> {
257    route_full(model, pipeline, ids, 0.30).scores
258}
259
260/// Held-out in-scope φ samples of every skill (from the descriptors), as
261/// (skill index, φ). Empty when no skill carries them.
262pub fn holdout_phis(model: &CmfModel) -> Vec<(usize, Vec<f32>)> {
263    let hidden = model.arch().hidden_size;
264    let mut out = Vec::new();
265    for (si, skill) in model.header.skills.iter().enumerate() {
266        let Some(sel) = &skill.selection else {
267            continue;
268        };
269        let (Some(h), Some(n)) = (sel.holdout.as_ref(), sel.holdout_n) else {
270            continue;
271        };
272        let Some(v) = decode_f16(h) else { continue };
273        if v.len() != n * hidden {
274            continue;
275        }
276        for i in 0..n {
277            out.push((si, v[i * hidden..(i + 1) * hidden].to_vec()));
278        }
279    }
280    out
281}
282
283/// Fit the file-level calibration from the skills' held-out φ samples: the
284/// temperature by NLL of the true skill under softmax(−err/T) over a
285/// geometric grid, then θ as the (1−fpr) quantile of in-scope novelty
286/// scores. Every skill's φ must be at the SAME phi_layer (mixed layers are
287/// scored per skill; the samples of a skill are compared against every
288/// descriptor's own layer only when equal — otherwise skipped).
289pub fn calibrate(
290    model: &CmfModel,
291    target_fpr: f32,
292) -> Option<cortiq_core::format::RoutingCalibration> {
293    let samples = holdout_phis(model);
294    if samples.is_empty() {
295        return None;
296    }
297    // per sample: rows over all skills — φ is a per-layer quantity, so only
298    // skills sharing the sample's phi_layer are comparable
299    let skills = &model.header.skills;
300    let mut per_sample: Vec<(Vec<ErrorRow>, usize)> = Vec::new();
301    for (si, phi) in &samples {
302        let layer = skills[*si]
303            .selection
304            .as_ref()
305            .map(|s| s.phi_layer)
306            .unwrap_or(0);
307        let mut phi_of =
308            |l: usize| -> Vec<f32> { if l == layer { phi.clone() } else { Vec::new() } };
309        let rows = error_rows(model, &mut phi_of);
310        let Some(pos) = rows.iter().position(|r| r.0 == skills[*si].id) else {
311            continue;
312        };
313        per_sample.push((rows, pos));
314    }
315    if per_sample.is_empty() {
316        return None;
317    }
318    // temperature: geometric grid, minimize NLL of the true skill
319    let mut best_t = 1.0f32;
320    let mut best_nll = f32::INFINITY;
321    let mut t = 1e-3f32;
322    // errors are raw squared residuals of hidden states — the scale spans
323    // orders of magnitude across models, hence the wide grid
324    while t <= 1e6 {
325        let mut nll = 0.0f32;
326        for (rows, pos) in &per_sample {
327            let mut logits: Vec<f32> = rows.iter().map(|r| -r.1 / t).collect();
328            softmax(&mut logits);
329            nll -= logits[*pos].max(1e-9).ln();
330        }
331        if nll < best_nll {
332            best_nll = nll;
333            best_t = t;
334        }
335        t *= 1.15;
336    }
337    let mut cal = cortiq_core::format::RoutingCalibration {
338        temperature: best_t,
339        novelty_theta: 0.5,
340        samples: per_sample.len(),
341        target_fpr,
342    };
343    // θ: (1−fpr) quantile of the in-scope novelty scores
344    let mut nov: Vec<f32> = per_sample
345        .iter()
346        .map(|(rows, _)| decide(rows, Some(&cal), 1.0).novelty)
347        .filter(|v| v.is_finite())
348        .collect();
349    nov.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
350    if !nov.is_empty() {
351        let q = (1.0 - target_fpr).clamp(0.0, 1.0);
352        let idx = (((nov.len() - 1) as f32) * q).round() as usize;
353        cal.novelty_theta = (nov[idx.min(nov.len() - 1)] + 1e-4).min(0.999);
354    }
355    Some(cal)
356}
357
358// ───────────────────────── router policy v2 ─────────────────────────
359
360/// One φ-error row: (class id, raw squared error, err_mean, err_std, ‖φ‖²)
361/// — the tuple [`decide`] takes.
362pub type ErrorRow = (String, f32, Option<f32>, Option<f32>, f32);
363
364/// What a request runs on.
365#[derive(Debug, Clone, PartialEq, Eq)]
366pub enum RouteTarget {
367    Backbone,
368    Skill(String),
369}
370
371/// A router-v2 decision: the target, the full scored routing (backbone row
372/// included, id [`BACKBONE_CLASS_ID`]) and a human-readable reason.
373#[derive(Debug, Clone)]
374pub struct RouteDecision {
375    pub target: RouteTarget,
376    pub routing: Routing,
377    pub reason: String,
378}
379
380impl RouteDecision {
381    /// The chosen skill id, `None` for the backbone.
382    pub fn skill(&self) -> Option<&str> {
383        match &self.target {
384            RouteTarget::Skill(s) => Some(s),
385            RouteTarget::Backbone => None,
386        }
387    }
388
389    /// A decision nobody computed: the caller pinned the target
390    /// (`--skill <id>`, `--skill none`, `--route backbone`, or a file
391    /// without routing). No scores, novelty NaN.
392    pub fn forced(target: RouteTarget, reason: impl Into<String>) -> Self {
393        Self {
394            target,
395            routing: Routing {
396                scores: Vec::new(),
397                confidence: 0.0,
398                margin: 0.0,
399                novelty: f32::NAN,
400                is_novel: false,
401                calibrated: false,
402            },
403            reason: reason.into(),
404        }
405    }
406
407    /// `"backbone"` or the skill id.
408    pub fn target_label(&self) -> &str {
409        self.skill().unwrap_or("backbone")
410    }
411
412    /// Unit error E of the backbone class (router-v2 decisions only).
413    pub fn e_base(&self) -> Option<f32> {
414        self.routing
415            .scores
416            .iter()
417            .find(|s| s.id == BACKBONE_CLASS_ID)
418            .map(|s| s.error)
419    }
420
421    /// Unit error E of the nearest SKILL class (the best non-backbone
422    /// row; for a legacy file the argmin winner).
423    pub fn e_skill(&self) -> Option<f32> {
424        self.routing
425            .scores
426            .iter()
427            .find(|s| s.id != BACKBONE_CLASS_ID)
428            .map(|s| s.error)
429    }
430
431    /// The nearest skill's id (whether or not it won).
432    pub fn nearest_skill(&self) -> Option<&str> {
433        self.routing
434            .scores
435            .iter()
436            .find(|s| s.id != BACKBONE_CLASS_ID)
437            .map(|s| s.id.as_str())
438    }
439
440    /// One human line: `route: <target> | novelty … | E_base … | E_skill …
441    /// | <reason>` (the stderr / server-log form).
442    pub fn describe(&self) -> String {
443        let f = |v: Option<f32>| match v {
444            Some(x) if x.is_finite() => format!("{x:.4}"),
445            _ => "—".to_string(),
446        };
447        let nov = if self.routing.novelty.is_finite() {
448            format!("{:.3}", self.routing.novelty)
449        } else {
450            "—".to_string()
451        };
452        format!(
453            "route: {} | novelty {nov} | E_base {} | E_skill {} | {}",
454            self.target_label(),
455            f(self.e_base()),
456            f(self.e_skill()),
457            self.reason
458        )
459    }
460
461    /// The decision as the tools and `serve` report it:
462    /// `{target, novelty, e_base, e_skill, reason}` (non-finite → null).
463    pub fn summary_json(&self) -> serde_json::Value {
464        let num = |v: Option<f32>| -> serde_json::Value {
465            match v {
466                Some(x) if x.is_finite() => serde_json::json!(x),
467                _ => serde_json::Value::Null,
468            }
469        };
470        serde_json::json!({
471            "target": self.target_label(),
472            "novelty": num(Some(self.routing.novelty)),
473            "e_base": num(self.e_base()),
474            "e_skill": num(self.e_skill()),
475            "reason": self.reason,
476        })
477    }
478}
479
480/// Knobs of [`route_request_with`].
481#[derive(Debug, Clone, Copy, Default)]
482pub struct RouteOptions {
483    /// Debug / gate measurement: every non-retired calibration class may
484    /// win — `quarantine` (fresh bake), `stale_regate` (re-gate after a
485    /// requant) and an `active` record whose gate is not measured yet —
486    /// not only the auto-routable ones. Never on in production: such a
487    /// skill is not allowed to auto-route.
488    pub include_quarantine: bool,
489}
490
491/// May the decision PICK this skill class under `opts`? Auto-routable
492/// (v2, `active`, gate `measured`), or — gate measurement only
493/// ([`RouteOptions::include_quarantine`]) — any other non-retired v2
494/// class. Every calibration class is SCORED either way (see
495/// [`policy_rows`]); this only decides whether its win runs the skill.
496pub fn is_routable(s: &SkillRecord, opts: RouteOptions) -> bool {
497    s.is_auto_routable()
498        || (opts.include_quarantine
499            && s.is_v2()
500            && s.selection.is_some()
501            && s.status.as_deref() != Some("retired"))
502}
503
504/// The skills [`route_policy_with`] can pick under `opts` (calibration
505/// classes that are [`is_routable`]), in id order.
506pub fn routable_skills<'a>(header: &'a CmfHeader, opts: RouteOptions) -> Vec<&'a SkillRecord> {
507    calibration_classes(header)
508        .into_iter()
509        .filter(|s| is_routable(s, opts))
510        .collect()
511}
512
513/// Why no skill can win a router-v2 decision on this file under `opts`
514/// (`None` = at least one can): no policy, no calibration, a stale
515/// `skills_hash`, or no routable class. A gate measured while this is
516/// `Some` is vacuous — every prompt runs the backbone by construction.
517pub fn no_candidate_reason(header: &CmfHeader, opts: RouteOptions) -> Option<String> {
518    let Some(policy) = &header.router else {
519        return Some("the file declares no router policy (ROUTER_V2)".into());
520    };
521    if header.routing.is_none() {
522        return Some("the router is not calibrated (header.routing absent)".into());
523    }
524    let h = skills_hash(header);
525    if parse_hex64(&policy.skills_hash) != Some(h) {
526        return Some(format!(
527            "skills_hash mismatch (router {}, file {h:016x}): the calibration is stale",
528            policy.skills_hash
529        ));
530    }
531    if routable_skills(header, opts).is_empty() {
532        let classes: Vec<String> = calibration_classes(header)
533            .iter()
534            .map(|s| format!("{}={}", s.id, s.status.as_deref().unwrap_or("?")))
535            .collect();
536        return Some(format!(
537            "no routable skill class (classes {classes:?}; include_quarantine {})",
538            opts.include_quarantine
539        ));
540    }
541    None
542}
543
544// ───────────────────────── prompt contract ─────────────────────────
545
546/// The ChatML frame Embryo skills are trained under: `<|im_start|>role\n
547/// text<|im_end|>\n` per turn, the answer after `<|im_start|>assistant\n`.
548pub const PROMPT_CONTRACT_CMF_IM_V1: &str = "cmf-im-v1";
549const IM_USER: &str = "<|im_start|>user\n";
550const IM_ASSISTANT: &str = "<|im_end|>\n<|im_start|>assistant\n";
551
552/// One user turn rendered under cmf-im-v1 — the frame `dump-logits`,
553/// `probe-utility` and the φ calibration use.
554pub fn render_cmf_im_v1(user_text: &str) -> String {
555    format!("{IM_USER}{user_text}{IM_ASSISTANT}")
556}
557
558/// The user text of a prompt that is EXACTLY one cmf-im-v1 user turn
559/// plus the generation prompt (no system turn, no history, no template
560/// markers inside the text); `None` otherwise.
561pub fn cmf_im_v1_single_user_turn(prompt: &str) -> Option<&str> {
562    let q = prompt.strip_prefix(IM_USER)?.strip_suffix(IM_ASSISTANT)?;
563    (!q.contains("<|im_start|>") && !q.contains("<|im_end|>")).then_some(q)
564}
565
566/// How the prompt a lane will generate from is framed.
567#[derive(Debug, Clone, Copy, PartialEq, Eq)]
568pub enum PromptFrame {
569    /// Raw completion text (no chat template).
570    Raw,
571    /// The cmf-im-v1 ChatML frame (a template or the ChatML fallback
572    /// that renders `<|im_start|>role\n…<|im_end|>`).
573    CmfImV1,
574    /// Some other chat template.
575    Other,
576}
577
578impl PromptFrame {
579    fn label(self) -> &'static str {
580        match self {
581            Self::Raw => "a raw completion prompt",
582            Self::CmfImV1 => "cmf-im-v1",
583            Self::Other => "another chat template",
584        }
585    }
586}
587
588/// The frame a tokenizer's chat rendering produces: its template when it
589/// carries one (cmf-im-v1 iff it spells `<|im_start|>`), else the ChatML
590/// fallback — cmf-im-v1 only when the vocabulary has both markers.
591pub fn chat_frame(tok: &crate::tokenizer::Tokenizer) -> PromptFrame {
592    match &tok.chat_template {
593        Some(t) if t.contains("<|im_start|>") && t.contains("<|im_end|>") => PromptFrame::CmfImV1,
594        Some(_) => PromptFrame::Other,
595        None if tok.im_start_id.is_some() && tok.im_end_id.is_some() => PromptFrame::CmfImV1,
596        None => PromptFrame::Other,
597    }
598}
599
600/// The chosen skill's `prompt_contract` against the frame the caller
601/// generates with — the one check `run`, `explain` and `serve` share.
602/// Returns the final decision and whether the caller must RENDER the
603/// user text under cmf-im-v1 ([`render_cmf_im_v1`]) for the skill lane.
604///
605/// * backbone, a skill without a contract, or a frame that satisfies it:
606///   unchanged, no rendering;
607/// * a cmf-im-v1 skill on a raw prompt with `can_render`: the skill runs
608///   on the rendered turn (the frame it was trained and gated under);
609/// * anything else: the BACKBONE runs, reason "prompt contract … not
610///   satisfied" — a skill never continues a context it was not trained on.
611pub fn enforce_prompt_contract(
612    header: &CmfHeader,
613    decision: RouteDecision,
614    frame: PromptFrame,
615    can_render: bool,
616) -> (RouteDecision, bool) {
617    let Some(id) = decision.skill() else {
618        return (decision, false);
619    };
620    let Some(contract) = header
621        .skills
622        .iter()
623        .find(|s| s.id == id)
624        .and_then(|s| s.prompt_contract.clone())
625    else {
626        return (decision, false);
627    };
628    let known = contract == PROMPT_CONTRACT_CMF_IM_V1;
629    if known && frame == PromptFrame::CmfImV1 {
630        return (decision, false);
631    }
632    if known && frame == PromptFrame::Raw && can_render {
633        let mut d = decision;
634        d.reason = format!(
635            "{}; prompt rendered as {PROMPT_CONTRACT_CMF_IM_V1} (the skill's contract)",
636            d.reason
637        );
638        return (d, true);
639    }
640    let reason = format!(
641        "prompt contract '{contract}' of skill '{id}' not satisfied ({}): the backbone runs \
642         [decision was: {}]",
643        frame.label(),
644        decision.reason
645    );
646    (
647        RouteDecision {
648            target: RouteTarget::Backbone,
649            routing: decision.routing,
650            reason,
651        },
652        false,
653    )
654}
655
656/// Does the file declare router policy v2 (`header.router`, bit
657/// ROUTER_V2)? Then only [`decide_backbone_gated`] may pick a skill.
658pub fn is_router_v2(model: &CmfModel) -> bool {
659    model.header.router.is_some()
660}
661
662/// The per-request decision `run`, `explain`, `serve` and the probes
663/// share (spec §9.4). `backbone` must be a pipeline WITHOUT a skill
664/// overlay; its sequence state is reset (φ is a fresh prefill).
665///
666/// * ROUTER_V2 file: `ids = phi.prefix_ids ++ encode_plain(user_text) ++
667///   phi.suffix_ids` (no added-token matching inside the user text), φ =
668///   [`Pipeline::probe_phi_span`] over the user-text span at `phi.layer`,
669///   rows = backbone + EVERY calibration class (the set the calibration
670///   was fitted on); a skill runs only when the winner is routable
671///   ([`route_policy_with`]).
672/// * legacy file: recon-argmin over the v1 skills on the encoded prompt
673///   (unchanged), except that a NOVEL input runs the backbone — the
674///   argmin winner used to run whatever the novelty verdict said.
675pub fn route_request(model: &CmfModel, backbone: &mut Pipeline, user_text: &str) -> RouteDecision {
676    route_request_with(model, backbone, user_text, RouteOptions::default())
677}
678
679/// [`route_request`] with [`RouteOptions`].
680pub fn route_request_with(
681    model: &CmfModel,
682    backbone: &mut Pipeline,
683    user_text: &str,
684    opts: RouteOptions,
685) -> RouteDecision {
686    // Router v2: q is tokenized as plain text — a literal `<|im_end|>` in
687    // the user's message stays bytes, as in the trainer's `Bpe::encode`
688    // the descriptors and the calibration were built with (R7). Legacy
689    // files keep the historical `encode` (their descriptors were fitted
690    // on it).
691    let q_ids = if model.header.router.is_some() {
692        backbone.tokenizer.encode_plain(user_text)
693    } else {
694        backbone.tokenizer.encode(user_text)
695    };
696    route_request_ids(model, backbone, &q_ids, opts)
697}
698
699/// [`route_request_with`] for every router policy: under `key_span` the φ
700/// is pooled over the table key the message names, and a message without
701/// a key runs the backbone with no forward at all.
702pub fn route_request_keyed(
703    model: &std::sync::Arc<CmfModel>,
704    backbone: &mut Pipeline,
705    user_text: &str,
706    opts: RouteOptions,
707) -> RouteDecision {
708    match &model.header.router {
709        Some(p) if p.phi.pool == cortiq_core::knowledge::phi_pool::KEY_SPAN => {
710            let holders = crate::lookup::key_holders(model, user_text);
711            let no_key = RouteDecision::forced(RouteTarget::Backbone, "no table key in the message");
712            if holders.is_empty() {
713                return route_keyless(model, backbone, p, user_text, no_key);
714            }
715            let d = match crate::lookup::key_span_phi(model, backbone, &p.phi, user_text) {
716                Some(phi) => route_policy_candidates(&model.header, &phi, opts, &holders),
717                None => no_key,
718            };
719            // A word of a table in a question no record answers: the same decision
720            if d.target == RouteTarget::Backbone {
721                return route_keyless(model, backbone, p, user_text, d);
722            }
723            d
724        }
725        _ => route_request_with(model, backbone, user_text, opts),
726    }
727}
728
729/// [`route_request_keyed`] with conversation memory: `turns` = the user
730/// messages, the last one first. A last message without a table key, after a
731/// turn that named one, is decided on the follow-up φ at its own margin.
732pub fn route_turns_keyed(
733    model: &std::sync::Arc<CmfModel>,
734    backbone: &mut Pipeline,
735    turns: &[&str],
736    opts: RouteOptions,
737) -> RouteDecision {
738    let last = turns.first().copied().unwrap_or("");
739    let Some(p) = model.header.router.as_ref().filter(|p| {
740        p.phi.pool == cortiq_core::knowledge::phi_pool::KEY_SPAN && p.phi.followup_alpha.is_some()
741    }) else {
742        return route_request_keyed(model, backbone, last, opts);
743    };
744    if turns.len() < 2 || crate::lookup::message_names_key(model, last) {
745        return route_request_keyed(model, backbone, last, opts);
746    }
747    let d = match crate::lookup::followup_phi(model, backbone, &p.phi, last, &turns[1..]) {
748        Some((phi, holders)) => {
749            if let (Some(f), Some(m)) = (p.followup.as_ref(), p.phi.followup_margin) {
750                route_followup(&model.header, f, m, &phi, &holders, opts)
751            } else {
752                let mut header = model.header.clone();
753                if let (Some(r), Some(m)) = (header.router.as_mut(), p.phi.followup_margin) {
754                    r.margin = m;
755                }
756                route_policy_candidates(&header, &phi, opts, &holders)
757            }
758        }
759        None => RouteDecision::forced(RouteTarget::Backbone, "no table key in the conversation"),
760    };
761    if d.target == RouteTarget::Backbone {
762        return route_keyless(model, backbone, p, last, d);
763    }
764    d
765}
766
767/// Longest message the keyless decision looks at, in tokens: a question for
768/// a fact is short, and the probe runs before the answer.
769const KEYLESS_MAX_TOKENS: usize = 160;
770
771/// A message no record took and no follow-up: the whole-message φ against
772/// the two topologies of [`cortiq_core::knowledge::KeylessPolicy`]. `otherwise`
773/// is the decision when it does not ask for a fact.
774fn route_keyless(
775    model: &std::sync::Arc<CmfModel>,
776    backbone: &mut Pipeline,
777    p: &RouterPolicy,
778    user_text: &str,
779    otherwise: RouteDecision,
780) -> RouteDecision {
781    let Some(k) = p.keyless.as_ref() else {
782        return otherwise;
783    };
784    let q = backbone.tokenizer.encode_plain(user_text);
785    if q.is_empty() || q.len() > KEYLESS_MAX_TOKENS {
786        return otherwise;
787    }
788    let (ids, span) = phi_span_ids(&p.phi, &q);
789    let hidden = backbone.hidden_size;
790    let rows = backbone.probe_hidden_rows(&ids, p.phi.layer);
791    let phi = pool_key_span(&rows, hidden, span, (0, 0), 1.0);
792    let (Some(fact), Some(chat)) = (
793        descriptor_row(&k.record, &k.fact, &phi, hidden),
794        descriptor_row(BACKBONE_CLASS_ID, &k.chat, &phi, hidden),
795    ) else {
796        return otherwise;
797    };
798    let (e_fact, e_chat) = (fact.1 / fact.4.max(1e-12), chat.1 / chat.4.max(1e-12));
799    if !(e_fact + k.margin < e_chat) {
800        return otherwise;
801    }
802    RouteDecision {
803        target: RouteTarget::Skill(k.record.clone()),
804        routing: decide(&[chat, fact], None, 1.0),
805        reason: format!("asks for a fact outside the records: E {e_fact:.4} + {:.4} < E_chat {e_chat:.4}", k.margin),
806    }
807}
808
809/// The follow-up decision on its own topologies: the nearest of the records
810/// that hold the earlier key, against unrelated messages in the same form.
811fn route_followup(
812    header: &CmfHeader,
813    f: &cortiq_core::knowledge::FollowupTopologies,
814    margin: f32,
815    phi: &[f32],
816    holders: &[String],
817    opts: RouteOptions,
818) -> RouteDecision {
819    let hidden = header.arch.hidden_size;
820    let phi = unit_copy(phi);
821    let base = descriptor_row(BACKBONE_CLASS_ID, &f.base, &phi, hidden);
822    let skills: Vec<ErrorRow> = holders
823        .iter()
824        .filter(|id| header.skills.iter().any(|s| s.id == **id && is_routable(s, opts)))
825        .filter_map(|id| descriptor_row(id, f.skills.get(id)?, &phi, hidden))
826        .collect();
827    let mut rows: Vec<ErrorRow> = base.iter().cloned().collect();
828    rows.extend(skills.iter().cloned());
829    let routing = decide(&rows, None, 1.0);
830    let backbone = |routing: Routing, reason: String| RouteDecision { target: RouteTarget::Backbone, routing, reason };
831    let (Some(base), Some(best)) = (base, skills.iter().min_by(|a, b| a.1.total_cmp(&b.1))) else {
832        return backbone(routing, "no follow-up topology for the records holding the key".into());
833    };
834    let (e_base, e_skill) = (base.1 / base.4.max(1e-12), best.1 / best.4.max(1e-12));
835    if !(e_skill + margin < e_base) {
836        let reason = format!("follow-up margin not beaten: E_skill {e_skill:.4} + {margin:.4} ≥ E_base {e_base:.4}");
837        return backbone(routing, reason);
838    }
839    let reason = format!("follow-up on '{}': E {e_skill:.4} + {margin:.4} < E_base {e_base:.4}", best.0);
840    RouteDecision { target: RouteTarget::Skill(best.0.clone()), routing, reason }
841}
842
843/// [`route_request_with`] on an already encoded user text.
844pub fn route_request_ids(
845    model: &CmfModel,
846    backbone: &mut Pipeline,
847    q_ids: &[u32],
848    opts: RouteOptions,
849) -> RouteDecision {
850    match &model.header.router {
851        Some(policy) if policy.phi.pool == cortiq_core::knowledge::phi_pool::KEY_SPAN => {
852            RouteDecision::forced(
853                RouteTarget::Backbone,
854                "router.phi.pool key_span needs the message text (route_request_keyed)",
855            )
856        }
857        Some(policy) => {
858            let (ids, span) = phi_span_ids(&policy.phi, q_ids);
859            let phi = backbone.probe_phi_span(&ids, policy.phi.layer, span);
860            route_policy_with(&model.header, &phi, opts)
861        }
862        None => route_legacy(model, backbone, q_ids),
863    }
864}
865
866/// Legacy files (no ROUTER_V2): the recon-argmin winner, unless the input
867/// is novel (calibrated novelty > θ, or E_min > `CMF_OOD_TAU`, default
868/// 0.30, without a calibration) — then the backbone.
869fn route_legacy(model: &CmfModel, backbone: &mut Pipeline, ids: &[u32]) -> RouteDecision {
870    let tau = std::env::var("CMF_OOD_TAU")
871        .ok()
872        .and_then(|v| v.parse().ok())
873        .unwrap_or(0.30);
874    let routing = route_full(model, backbone, ids, tau);
875    let Some(w) = routing.winner().cloned() else {
876        return RouteDecision {
877            target: RouteTarget::Backbone,
878            routing,
879            reason: "no routable skill in this container".into(),
880        };
881    };
882    if routing.is_novel {
883        let reason = if routing.calibrated {
884            format!(
885                "novel input (novelty {:.3} over θ): nearest '{}' not taken",
886                routing.novelty, w.id
887            )
888        } else {
889            format!(
890                "novel input (E_min {:.4} > τ {tau}, uncalibrated file): nearest '{}' not taken",
891                w.error, w.id
892            )
893        };
894        return RouteDecision {
895            target: RouteTarget::Backbone,
896            routing,
897            reason,
898        };
899    }
900    let reason = format!(
901        "legacy recon-argmin: '{}' (E {:.4}), in scope",
902        w.id, w.error
903    );
904    RouteDecision {
905        target: RouteTarget::Skill(w.id),
906        routing,
907        reason,
908    }
909}
910
911/// The skill classes a router-v2 calibration covers: v2 records with a
912/// selection descriptor, not retired, sorted by id. Status changes other
913/// than retirement (quarantine → active, stale_regate) do not change the
914/// class set, so they do not invalidate the calibration.
915pub fn calibration_classes(header: &CmfHeader) -> Vec<&SkillRecord> {
916    let mut v: Vec<&SkillRecord> = header
917        .skills
918        .iter()
919        .filter(|s| s.is_v2() && s.selection.is_some() && s.status.as_deref() != Some("retired"))
920        .collect();
921    v.sort_by(|a, b| a.id.as_bytes().cmp(b.id.as_bytes()));
922    v
923}
924
925/// hash64 binding a calibration to the descriptors it was fitted on:
926/// `b"cmf-skills-v2\0"`, then the backbone descriptor (`router.base`, a
927/// single 0 byte when there is no router), then every
928/// [`calibration_classes`] descriptor in id order. Per descriptor:
929/// `id 0 metric 0 phi_layer(u64 LE) rank(u64 LE) mean 0 basis 0` +
930/// err_mean and err_std as (tag u8, f32 bits LE) + `0x1e`. The holdout
931/// samples are not part of it (they feed the fit, not the decision).
932/// `router.skills_hash` must equal this or the backbone runs.
933pub fn skills_hash(header: &CmfHeader) -> u64 {
934    fn push(buf: &mut Vec<u8>, id: &str, d: &SelectionDescriptor) {
935        buf.extend_from_slice(id.as_bytes());
936        buf.push(0);
937        buf.extend_from_slice(d.metric.as_bytes());
938        buf.push(0);
939        buf.extend_from_slice(&(d.phi_layer as u64).to_le_bytes());
940        buf.extend_from_slice(&(d.rank as u64).to_le_bytes());
941        buf.extend_from_slice(d.mean.as_bytes());
942        buf.push(0);
943        buf.extend_from_slice(d.basis.as_bytes());
944        buf.push(0);
945        for x in [d.err_mean, d.err_std] {
946            match x {
947                Some(v) => {
948                    buf.push(1);
949                    buf.extend_from_slice(&v.to_bits().to_le_bytes());
950                }
951                None => buf.push(0),
952            }
953        }
954        buf.push(0x1e);
955    }
956    let mut buf = b"cmf-skills-v2\0".to_vec();
957    match &header.router {
958        Some(r) => push(&mut buf, BACKBONE_CLASS_ID, &r.base),
959        None => buf.push(0),
960    }
961    for s in calibration_classes(header) {
962        push(&mut buf, &s.id, s.selection.as_ref().expect("filtered"));
963    }
964    hash64(&buf)
965}
966
967/// Canonical φ input for a user message (spec §9.4): `prefix_ids ++ q_ids
968/// ++ suffix_ids`, and the positions of `q_ids` — the span φ is pooled
969/// over. `q_ids` is `Tokenizer::encode_plain(q)` exactly (no BOS, no
970/// added-token matching — the trainer's `Bpe::encode`).
971pub fn phi_span_ids(policy: &PhiSpec, q_ids: &[u32]) -> (Vec<u32>, std::ops::Range<usize>) {
972    let mut ids =
973        Vec::with_capacity(policy.prefix_ids.len() + q_ids.len() + policy.suffix_ids.len());
974    ids.extend_from_slice(&policy.prefix_ids);
975    let start = ids.len();
976    ids.extend_from_slice(q_ids);
977    let end = ids.len();
978    ids.extend_from_slice(&policy.suffix_ids);
979    (ids, start..end)
980}
981
982/// `span_mean` + `unit`: mean of the per-position hidden rows
983/// (`hiddens` = n × `hidden`, the residual stream AFTER `phi.layer`) over
984/// `span`, unit-normalized. An empty span gives the zero vector, which the
985/// decision treats as degenerate (backbone).
986pub fn pool_span_unit(hiddens: &[f32], hidden: usize, span: std::ops::Range<usize>) -> Vec<f32> {
987    let rows = hiddens.len() / hidden.max(1);
988    let (a, b) = (span.start.min(rows), span.end.min(rows));
989    let mut phi = vec![0.0f32; hidden];
990    if b <= a {
991        return phi;
992    }
993    for r in a..b {
994        for (p, x) in phi.iter_mut().zip(&hiddens[r * hidden..(r + 1) * hidden]) {
995            *p += x;
996        }
997    }
998    let n = phi.iter().map(|x| x * x).sum::<f32>().sqrt();
999    if n > 0.0 {
1000        phi.iter_mut().for_each(|x| *x /= n);
1001    }
1002    phi
1003}
1004
1005/// `key_span` pool: `hiddens` = n × `hidden`, `span` = the user text in the
1006/// canonical input, `window` = the key tokens inside the user text.
1007pub fn pool_key_span(
1008    hiddens: &[f32],
1009    hidden: usize,
1010    span: std::ops::Range<usize>,
1011    window: (usize, usize),
1012    alpha: f32,
1013) -> Vec<f32> {
1014    let unit_mean = |rows: &mut dyn Iterator<Item = usize>| -> Vec<f32> {
1015        let mut p = vec![0f32; hidden];
1016        for r in rows {
1017            for (a, x) in p.iter_mut().zip(&hiddens[r * hidden..(r + 1) * hidden]) {
1018                *a += x;
1019            }
1020        }
1021        unit_copy(&p)
1022    };
1023    let (ka, kb) = (span.start + window.0, span.start + window.1);
1024    let key = unit_mean(&mut (ka..kb));
1025    let rest = unit_mean(&mut (span.start..ka).chain(kb..span.end));
1026    let mix: Vec<f32> = key.iter().zip(&rest).map(|(k, r)| k + alpha * r).collect();
1027    unit_copy(&mix)
1028}
1029
1030// ───────────────────────── descriptor fitting ─────────────────────────
1031
1032/// Size of the held-out tail [`fit_descriptor`] keeps out of the fit (the
1033/// LAST `holdout_count(n)` samples, in input order): 20 %, at least 1,
1034/// and never so many that fewer than 2 train samples remain once n ≥ 3
1035/// (the trainer's `holdout_count`).
1036pub fn holdout_count(n: usize) -> usize {
1037    (n / 5).clamp(1, n.saturating_sub(2).max(1))
1038}
1039
1040/// f16 LE base64 of `v` — the on-disk form of the descriptor vectors
1041/// (`SelectionDescriptor.mean` / `basis` / `holdout`); [`decode_f16`] is
1042/// its inverse.
1043pub fn encode_f16(v: &[f32]) -> String {
1044    let bytes: Vec<u8> = v
1045        .iter()
1046        .flat_map(|x| f32_to_f16(*x).to_le_bytes())
1047        .collect();
1048    base64::engine::general_purpose::STANDARD.encode(bytes)
1049}
1050
1051fn f16_round(v: &[f32]) -> Vec<f32> {
1052    v.iter().map(|x| f16_to_f32(f32_to_f16(*x))).collect()
1053}
1054
1055/// Deterministic standard-normal values (LCG + Box–Muller): the start of
1056/// the subspace iteration and the replacement of a collapsed direction.
1057fn gauss_vec(seed: u64, n: usize) -> Vec<f32> {
1058    let mut s = seed
1059        .wrapping_mul(6_364_136_223_846_793_005)
1060        .wrapping_add(1_442_695_040_888_963_407);
1061    let mut next = || {
1062        s = s
1063            .wrapping_mul(6_364_136_223_846_793_005)
1064            .wrapping_add(1_442_695_040_888_963_407);
1065        (s >> 11) as f64 / (1u64 << 53) as f64
1066    };
1067    let mut out = Vec::with_capacity(n + 1);
1068    while out.len() < n {
1069        let (u1, u2) = (next().max(1e-12), next());
1070        let r = (-2.0 * u1.ln()).sqrt();
1071        let t = 2.0 * std::f64::consts::PI * u2;
1072        out.push((r * t.cos()) as f32);
1073        out.push((r * t.sin()) as f32);
1074    }
1075    out.truncate(n);
1076    out
1077}
1078
1079/// Top-`k` principal directions (orthonormal rows, `k × h`) of the
1080/// CENTERED samples by subspace (block power) iteration on the implicit
1081/// covariance Σ ccᵀ — no `h × h` matrix, `O(iters · n · k · h)`. The
1082/// iterate is re-orthonormalized (Gram–Schmidt) every step; a direction
1083/// that collapses to zero (a rank-deficient cloud) is replaced by a fresh
1084/// random one orthogonal to the rest, so the rows stay orthonormal.
1085/// Deterministic for a given `seed`.
1086pub fn top_principal_rows(
1087    centered: &[Vec<f32>],
1088    h: usize,
1089    k: usize,
1090    iters: usize,
1091    seed: u64,
1092) -> Vec<f32> {
1093    let mut q = gauss_vec(seed.wrapping_add(777), k * h);
1094    let mut spare = seed.wrapping_add(1);
1095    let mut orth = |q: &mut [f32]| {
1096        for i in 0..k {
1097            loop {
1098                for j in 0..i {
1099                    let dot: f32 = (0..h).map(|t| q[i * h + t] * q[j * h + t]).sum();
1100                    for t in 0..h {
1101                        q[i * h + t] -= dot * q[j * h + t];
1102                    }
1103                }
1104                let nrm: f32 = (0..h)
1105                    .map(|t| q[i * h + t] * q[i * h + t])
1106                    .sum::<f32>()
1107                    .sqrt();
1108                if nrm > 1e-12 {
1109                    for t in 0..h {
1110                        q[i * h + t] /= nrm;
1111                    }
1112                    break;
1113                }
1114                let fresh = gauss_vec(spare, h);
1115                spare = spare.wrapping_add(1);
1116                q[i * h..(i + 1) * h].copy_from_slice(&fresh);
1117            }
1118        }
1119    };
1120    orth(&mut q);
1121    let mut tmp = vec![0f32; k * h];
1122    let mut coef = vec![0f32; k];
1123    for _ in 0..iters {
1124        tmp.fill(0.0);
1125        for c in centered {
1126            for (i, ci) in coef.iter_mut().enumerate() {
1127                *ci = q[i * h..(i + 1) * h]
1128                    .iter()
1129                    .zip(c)
1130                    .map(|(a, b)| a * b)
1131                    .sum();
1132            }
1133            for (i, &ci) in coef.iter().enumerate() {
1134                if ci != 0.0 {
1135                    for (t, x) in tmp[i * h..(i + 1) * h].iter_mut().zip(c) {
1136                        *t += ci * x;
1137                    }
1138                }
1139            }
1140        }
1141        q.copy_from_slice(&tmp);
1142        orth(&mut q);
1143    }
1144    q
1145}
1146
1147/// Fit a router-v2 descriptor (`mse_unit`) from RAW φ samples — the
1148/// trainer's recipe (`cortiq-embryo` `skill::fit_selection`) for the
1149/// runtime tools (`cortiq route-fit`): every φ is unit-normalized; the
1150/// LAST [`holdout_count`] samples are held out and never enter the fit;
1151/// mean + top-`rank` PCA rows ([`top_principal_rows`], `rank` clamped to
1152/// `train − 1` and ≥ 1) of the train part; `err_mean` / `err_std` of the
1153/// train reconstruction errors against the descriptor AS STORED (f16
1154/// rounded — the numbers the decision sees); the held-out unit φ carried
1155/// in `holdout` for [`calibrate_v2`]. `None` for fewer than 2 samples or
1156/// empty / ragged vectors.
1157pub fn fit_descriptor(
1158    phis_raw: &[Vec<f32>],
1159    phi_layer: usize,
1160    rank: usize,
1161) -> Option<SelectionDescriptor> {
1162    let n = phis_raw.len();
1163    if n < 2 {
1164        return None;
1165    }
1166    let h = phis_raw[0].len();
1167    if h == 0 || phis_raw.iter().any(|p| p.len() != h) {
1168        return None;
1169    }
1170    let phis: Vec<Vec<f32>> = phis_raw.iter().map(|p| unit_copy(p)).collect();
1171    let n_hold = holdout_count(n);
1172    let (train, hold) = phis.split_at(n - n_hold);
1173    let nt = train.len();
1174    let mut mean = vec![0f32; h];
1175    for p in train {
1176        for (m, v) in mean.iter_mut().zip(p) {
1177            *m += v / nt as f32;
1178        }
1179    }
1180    let centered: Vec<Vec<f32>> = train
1181        .iter()
1182        .map(|p| p.iter().zip(&mean).map(|(v, m)| v - m).collect())
1183        .collect();
1184    let rank = rank.min(nt.saturating_sub(1)).max(1);
1185    let basis = top_principal_rows(&centered, h, rank, 120, 99);
1186    let (mq, bq) = (f16_round(&mean), f16_round(&basis));
1187    let errs: Vec<f32> = train.iter().map(|p| recon_error(p, &mq, &bq, rank)).collect();
1188    let em = errs.iter().sum::<f32>() / nt as f32;
1189    let es = (errs.iter().map(|e| (e - em).powi(2)).sum::<f32>() / nt as f32)
1190        .sqrt()
1191        .max(1e-4);
1192    let hold_flat: Vec<f32> = hold.concat();
1193    Some(SelectionDescriptor {
1194        metric: METRIC_MSE_UNIT.into(),
1195        phi_layer,
1196        mean: encode_f16(&mean),
1197        basis: encode_f16(&basis),
1198        rank,
1199        err_mean: Some(em),
1200        err_std: Some(es),
1201        holdout: Some(encode_f16(&hold_flat)),
1202        holdout_n: Some(hold.len()),
1203    })
1204}
1205
1206/// The cmf-im-v1 φ frame (spec §9.4) of a tokenizer: `prefix_ids =
1207/// encode("<|im_start|>user\n")`, `suffix_ids =
1208/// encode("<|im_end|>\n<|im_start|>assistant\n")` with the added tokens —
1209/// what the trainer's `phi_spec` renders (`Bpe::encode_with_specials`)
1210/// and checks the runtime against. Refused when the tokenizer has no
1211/// `<|im_start|>` / `<|im_end|>` added token (the frame would tokenize as
1212/// text and never match a chat prompt).
1213pub fn cmf_im_v1_phi_spec(tok: &crate::tokenizer::Tokenizer, layer: usize) -> Result<PhiSpec, String> {
1214    for s in ["<|im_start|>", "<|im_end|>"] {
1215        if tok.encode(s).len() != 1 {
1216            return Err(format!(
1217                "the tokenizer has no added token {s}: the cmf-im-v1 frame cannot be rendered"
1218            ));
1219        }
1220    }
1221    Ok(PhiSpec {
1222        layer,
1223        pool: "span_mean".into(),
1224        norm: "unit".into(),
1225        key_alpha: None,
1226        followup_alpha: None,
1227        followup_margin: None,
1228        prefix_ids: tok.encode(IM_USER),
1229        suffix_ids: tok.encode(IM_ASSISTANT),
1230    })
1231}
1232
1233fn unit_copy(phi: &[f32]) -> Vec<f32> {
1234    let n = phi.iter().map(|x| x * x).sum::<f32>().sqrt();
1235    if n > 0.0 {
1236        phi.iter().map(|x| x / n).collect()
1237    } else {
1238        phi.to_vec()
1239    }
1240}
1241
1242/// Row of a unit φ against one `mse_unit` descriptor (None if malformed).
1243pub fn descriptor_row(
1244    id: &str,
1245    sel: &SelectionDescriptor,
1246    phi_unit: &[f32],
1247    hidden: usize,
1248) -> Option<ErrorRow> {
1249    if sel.metric != METRIC_MSE_UNIT {
1250        return None;
1251    }
1252    let (mean, basis) = (decode_f16(&sel.mean)?, decode_f16(&sel.basis)?);
1253    if mean.len() != hidden || basis.len() != sel.rank * hidden || phi_unit.len() != hidden {
1254        return None;
1255    }
1256    let e = recon_error(phi_unit, &mean, &basis, sel.rank);
1257    let pp: f32 = phi_unit.iter().map(|v| v * v).sum();
1258    Some((id.to_string(), e, sel.err_mean, sel.err_std, pp))
1259}
1260
1261/// The backbone row and the rows of EVERY calibration class
1262/// ([`calibration_classes`]: v2, with a descriptor, not retired — the set
1263/// [`calibrate_v2`] fits T and θ over and [`skills_hash`] binds) for φ at
1264/// `router.phi.layer` (normalized here). Whether a class may WIN is the
1265/// decision's business ([`is_routable`]): dropping a quarantined class
1266/// from the rows would move the argmin — its in-scope prompt would go to
1267/// the nearest ACTIVE skill — and sharpen the softmax over fewer classes
1268/// (lower novelty than calibrated). Without a router: (None, []).
1269pub fn policy_rows(header: &CmfHeader, phi: &[f32]) -> (Option<ErrorRow>, Vec<ErrorRow>) {
1270    let Some(r) = &header.router else {
1271        return (None, Vec::new());
1272    };
1273    let hidden = header.arch.hidden_size;
1274    let phi = unit_copy(phi);
1275    let base = descriptor_row(BACKBONE_CLASS_ID, &r.base, &phi, hidden);
1276    let skills = calibration_classes(header)
1277        .into_iter()
1278        .filter_map(|s| {
1279            let sel = s.selection.as_ref()?;
1280            if sel.phi_layer != r.phi.layer {
1281                return None;
1282            }
1283            descriptor_row(&s.id, sel, &phi, hidden)
1284        })
1285        .collect();
1286    (base, skills)
1287}
1288
1289/// Policy `backbone_gated` (spec §9.4) with every skill row a candidate
1290/// (the calibration's own view: [`calibrate_v2`] measures what the file
1291/// would do were every class active). See [`decide_backbone_gated_with`].
1292pub fn decide_backbone_gated(
1293    base: Option<&ErrorRow>,
1294    skills: &[ErrorRow],
1295    calibration: Option<&RoutingCalibration>,
1296    policy: Option<&RouterPolicy>,
1297    file_skills_hash: u64,
1298) -> RouteDecision {
1299    decide_backbone_gated_with(
1300        base,
1301        skills,
1302        &|_| true,
1303        calibration,
1304        policy,
1305        file_skills_hash,
1306    )
1307}
1308
1309/// Policy `backbone_gated` (spec §9.4) — the one decision `run`, `explain`,
1310/// `serve` and the probes share. `skills` are the rows of EVERY
1311/// calibration class (the argmin and the softmax run over the same class
1312/// set the calibration was fitted on); `routable(id)` says which of them
1313/// may run. A skill wins iff: a policy is declared, its `skills_hash`
1314/// equals `file_skills_hash` (the calibration is not stale), a calibration
1315/// and a backbone row exist, some class is routable, the calibrated winner
1316/// is a skill AND routable, the input is not novel, and `E_skill + margin
1317/// < E_base` in unit error. Every other case runs the BACKBONE
1318/// (fail-closed) — a prompt nearest to a quarantined class runs the
1319/// backbone, never the next-nearest active skill. The negated comparisons
1320/// are deliberate: a NaN error or norm fails closed.
1321#[allow(clippy::neg_cmp_op_on_partial_ord)]
1322pub fn decide_backbone_gated_with(
1323    base: Option<&ErrorRow>,
1324    skills: &[ErrorRow],
1325    routable: &dyn Fn(&str) -> bool,
1326    calibration: Option<&RoutingCalibration>,
1327    policy: Option<&RouterPolicy>,
1328    file_skills_hash: u64,
1329) -> RouteDecision {
1330    let mut rows: Vec<ErrorRow> = Vec::with_capacity(skills.len() + 1);
1331    rows.extend(base.cloned());
1332    rows.extend(skills.iter().cloned());
1333    let backbone = |routing: Routing, reason: String| RouteDecision {
1334        target: RouteTarget::Backbone,
1335        routing,
1336        reason,
1337    };
1338    let Some(policy) = policy else {
1339        return backbone(
1340            decide(&rows, None, 1.0),
1341            "no router policy (ROUTER_V2 absent): the backbone runs".into(),
1342        );
1343    };
1344    if parse_hex64(&policy.skills_hash) != Some(file_skills_hash) {
1345        return backbone(
1346            decide(&rows, None, 1.0),
1347            format!(
1348                "skills_hash mismatch (router {}, file {file_skills_hash:016x}): the calibration \
1349                 is stale — recalibrate",
1350                policy.skills_hash
1351            ),
1352        );
1353    }
1354    let Some(cal) = calibration else {
1355        return backbone(decide(&rows, None, 1.0), "router not calibrated".into());
1356    };
1357    let Some(base) = base else {
1358        return backbone(
1359            decide(&rows, Some(cal), 1.0),
1360            "no backbone descriptor row".into(),
1361        );
1362    };
1363    if !(base.4 > 1e-12) {
1364        return backbone(
1365            decide(&rows, Some(cal), 1.0),
1366            "degenerate φ (empty span)".into(),
1367        );
1368    }
1369    let routing = decide(&rows, Some(cal), 1.0);
1370    if !skills.iter().any(|r| routable(&r.0)) {
1371        return backbone(
1372            routing,
1373            "no routable skill (v2, active, measured gate)".into(),
1374        );
1375    }
1376    let e_base = base.1 / base.4.max(1e-12);
1377    let Some(winner) = routing.winner().cloned() else {
1378        return backbone(routing, "no scored class".into());
1379    };
1380    if winner.id == BACKBONE_CLASS_ID {
1381        return backbone(
1382            routing,
1383            format!("the backbone is the nearest class (E_base {e_base:.4})"),
1384        );
1385    }
1386    if !routable(&winner.id) {
1387        return backbone(
1388            routing,
1389            format!(
1390                "nearest class '{}' is not routable (not active with a measured gate): the \
1391                 backbone runs",
1392                winner.id
1393            ),
1394        );
1395    }
1396    if routing.is_novel {
1397        let reason = format!(
1398            "novel input (novelty {:.3} > θ {:.3})",
1399            routing.novelty, cal.novelty_theta
1400        );
1401        return backbone(routing, reason);
1402    }
1403    let e_skill = winner.error;
1404    if !(e_skill + policy.margin < e_base) {
1405        return backbone(
1406            routing,
1407            format!(
1408                "margin not beaten: E_skill {e_skill:.4} + margin {:.4} ≥ E_base {e_base:.4}",
1409                policy.margin
1410            ),
1411        );
1412    }
1413    let reason = format!(
1414        "skill '{}': E {e_skill:.4} + margin {:.4} < E_base {e_base:.4}, confidence {:.3}, \
1415         novelty {:.3} ≤ θ {:.3}",
1416        winner.id, policy.margin, routing.confidence, routing.novelty, cal.novelty_theta
1417    );
1418    RouteDecision {
1419        target: RouteTarget::Skill(winner.id),
1420        routing,
1421        reason,
1422    }
1423}
1424
1425/// [`policy_rows`] + [`decide_backbone_gated`] with the file's own
1426/// calibration and [`skills_hash`]: the decision for a canonical φ.
1427pub fn route_policy(header: &CmfHeader, phi: &[f32]) -> RouteDecision {
1428    route_policy_with(header, phi, RouteOptions::default())
1429}
1430
1431/// [`route_policy`] with [`RouteOptions`]: rows over every calibration
1432/// class, a win runs the skill only when it [`is_routable`] under `opts`.
1433pub fn route_policy_with(header: &CmfHeader, phi: &[f32], opts: RouteOptions) -> RouteDecision {
1434    let (base, skills) = policy_rows(header, phi);
1435    let routable = |id: &str| {
1436        header
1437            .skills
1438            .iter()
1439            .find(|s| s.id == id)
1440            .is_some_and(|s| is_routable(s, opts))
1441    };
1442    decide_backbone_gated_with(
1443        base.as_ref(),
1444        &skills,
1445        &routable,
1446        header.routing.as_ref(),
1447        header.router.as_ref(),
1448        skills_hash(header),
1449    )
1450}
1451
1452/// [`route_policy_with`] with only `candidates` among the skill rows: a
1453/// table that does not hold the key the message names cannot answer it.
1454pub fn route_policy_candidates(
1455    header: &CmfHeader,
1456    phi: &[f32],
1457    opts: RouteOptions,
1458    candidates: &[String],
1459) -> RouteDecision {
1460    let (base, mut skills) = policy_rows(header, phi);
1461    skills.retain(|r| candidates.iter().any(|c| *c == r.0));
1462    let routable = |id: &str| {
1463        header
1464            .skills
1465            .iter()
1466            .find(|s| s.id == id)
1467            .is_some_and(|s| is_routable(s, opts))
1468    };
1469    let d = decide_backbone_gated_with(
1470        base.as_ref(),
1471        &skills,
1472        &routable,
1473        header.routing.as_ref(),
1474        header.router.as_ref(),
1475        skills_hash(header),
1476    );
1477    // A record's own stricter margin
1478    let margin = d
1479        .skill()
1480        .and_then(|id| header.skills.iter().find(|s| s.id == id))
1481        .and_then(|s| s.lookup.as_ref())
1482        .and_then(|l| l.route_margin);
1483    if let (Some(m), Some(eb), Some(es)) = (margin, d.e_base(), d.e_skill()) {
1484        if !(es + m < eb) {
1485            let reason = format!("record margin not beaten: E_skill {es:.4} + {m:.4} ≥ E_base {eb:.4}");
1486            return RouteDecision { target: RouteTarget::Backbone, routing: d.routing, reason };
1487        }
1488    }
1489    d
1490}
1491
1492/// Upper end of the two-sided Clopper–Pearson (exact binomial) interval
1493/// at `confidence` (0.95 → the 97.5 % Beta quantile) for `k` events in `n`
1494/// trials. `n == 0` → 1.
1495pub fn clopper_pearson_upper(k: usize, n: usize, confidence: f64) -> f64 {
1496    if n == 0 || k >= n {
1497        return 1.0;
1498    }
1499    let q = 1.0 - (1.0 - confidence) / 2.0;
1500    let (a, b) = ((k + 1) as f64, (n - k) as f64);
1501    if k == 0 {
1502        // closed form: I_x(1, n) = 1 − (1−x)^n
1503        return 1.0 - (1.0 - q).powf(1.0 / n as f64);
1504    }
1505    // bisection on the regularized incomplete beta
1506    let (mut lo, mut hi) = (0.0f64, 1.0f64);
1507    for _ in 0..200 {
1508        let mid = 0.5 * (lo + hi);
1509        if reg_inc_beta(a, b, mid) < q {
1510            lo = mid;
1511        } else {
1512            hi = mid;
1513        }
1514    }
1515    0.5 * (lo + hi)
1516}
1517
1518fn ln_gamma(x: f64) -> f64 {
1519    // Lanczos (g = 7, n = 9)
1520    const C: [f64; 9] = [
1521        0.999_999_999_999_809_9,
1522        676.520_368_121_885_1,
1523        -1_259.139_216_722_402_8,
1524        771.323_428_777_653_1,
1525        -176.615_029_162_140_6,
1526        12.507_343_278_686_905,
1527        -0.138_571_095_265_720_12,
1528        9.984_369_578_019_572e-6,
1529        1.505_632_735_149_311_6e-7,
1530    ];
1531    if x < 0.5 {
1532        return (std::f64::consts::PI / (std::f64::consts::PI * x).sin()).ln() - ln_gamma(1.0 - x);
1533    }
1534    let x = x - 1.0;
1535    let mut acc = C[0];
1536    for (i, c) in C.iter().enumerate().skip(1) {
1537        acc += c / (x + i as f64);
1538    }
1539    let t = x + 7.5;
1540    0.5 * (2.0 * std::f64::consts::PI).ln() + (x + 0.5) * t.ln() - t + acc.ln()
1541}
1542
1543/// Regularized incomplete beta I_x(a, b) (continued fraction, Lentz).
1544fn reg_inc_beta(a: f64, b: f64, x: f64) -> f64 {
1545    if x <= 0.0 {
1546        return 0.0;
1547    }
1548    if x >= 1.0 {
1549        return 1.0;
1550    }
1551    let ln_front = ln_gamma(a + b) - ln_gamma(a) - ln_gamma(b) + a * x.ln() + b * (1.0 - x).ln();
1552    let cf = |a: f64, b: f64, x: f64| -> f64 {
1553        let tiny = 1e-300;
1554        let (qab, qap, qam) = (a + b, a + 1.0, a - 1.0);
1555        let mut c = 1.0;
1556        let mut d = 1.0 - qab * x / qap;
1557        if d.abs() < tiny {
1558            d = tiny;
1559        }
1560        d = 1.0 / d;
1561        let mut h = d;
1562        for m in 1..400 {
1563            let m = m as f64;
1564            let m2 = 2.0 * m;
1565            let aa = m * (b - m) * x / ((qam + m2) * (a + m2));
1566            d = 1.0 + aa * d;
1567            if d.abs() < tiny {
1568                d = tiny;
1569            }
1570            c = 1.0 + aa / c;
1571            if c.abs() < tiny {
1572                c = tiny;
1573            }
1574            d = 1.0 / d;
1575            h *= d * c;
1576            let aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2));
1577            d = 1.0 + aa * d;
1578            if d.abs() < tiny {
1579                d = tiny;
1580            }
1581            c = 1.0 + aa / c;
1582            if c.abs() < tiny {
1583                c = tiny;
1584            }
1585            d = 1.0 / d;
1586            let del = d * c;
1587            h *= del;
1588            if (del - 1.0).abs() < 1e-15 {
1589                break;
1590            }
1591        }
1592        h
1593    };
1594    if x < (a + 1.0) / (a + b + 2.0) {
1595        ln_front.exp() * cf(a, b, x) / a
1596    } else {
1597        1.0 - ln_front.exp() * cf(b, a, 1.0 - x) / b
1598    }
1599}
1600
1601/// Calibration v2 (spec §9.4), pure over the header: classes = the
1602/// backbone (`router.base`, its holdout = general prompts) + every
1603/// [`calibration_classes`] skill (its holdout = in-scope prompts). The
1604/// temperature minimizes the NLL of the true class over ALL classes
1605/// including the backbone; θ is the (1−`target_fpr`) quantile of novelty
1606/// on the in-scope skill holdouts. `measured` reports, on these same
1607/// holdouts and under the full [`decide_backbone_gated`] decision with every
1608/// calibrated skill as a candidate, the in-scope recall and the backbone
1609/// false-accept rate with its Clopper–Pearson 95 % upper bound. These are
1610/// calibration-set numbers; the gate measures a DISJOINT general-eval set.
1611/// Write the result with `header.routing = cal` and `router.skills_hash =
1612/// hex(skills_hash(header))`.
1613pub fn calibrate_v2(
1614    header: &CmfHeader,
1615    target_fpr: f32,
1616) -> Result<(RoutingCalibration, serde_json::Value), String> {
1617    let policy = header
1618        .router
1619        .as_ref()
1620        .ok_or("calibrate_v2: the file declares no router policy (header.router)")?;
1621    let hidden = header.arch.hidden_size;
1622    let classes = calibration_classes(header);
1623    if classes.is_empty() {
1624        return Err("calibrate_v2: no v2 skill with a selection descriptor".into());
1625    }
1626    // (id, descriptor) — index 0 is the backbone.
1627    let mut descs: Vec<(&str, &SelectionDescriptor)> = vec![(BACKBONE_CLASS_ID, &policy.base)];
1628    for s in &classes {
1629        let sel = s.selection.as_ref().expect("filtered");
1630        if sel.metric != METRIC_MSE_UNIT || sel.phi_layer != policy.phi.layer {
1631            return Err(format!(
1632                "calibrate_v2: skill '{}' descriptor is {}@{}, the policy needs {METRIC_MSE_UNIT}@{}",
1633                s.id, sel.metric, sel.phi_layer, policy.phi.layer
1634            ));
1635        }
1636        descs.push((&s.id, sel));
1637    }
1638    let holdout = |d: &SelectionDescriptor| -> Vec<Vec<f32>> {
1639        let (Some(h), Some(n)) = (d.holdout.as_ref(), d.holdout_n) else {
1640            return Vec::new();
1641        };
1642        match decode_f16(h) {
1643            Some(v) if v.len() == n * hidden => (0..n)
1644                .map(|i| unit_copy(&v[i * hidden..(i + 1) * hidden]))
1645                .collect(),
1646            _ => Vec::new(),
1647        }
1648    };
1649    // samples: (true class, rows over every class)
1650    let mut samples: Vec<(usize, Vec<ErrorRow>)> = Vec::new();
1651    for (ci, (_, d)) in descs.iter().enumerate() {
1652        for phi in holdout(d) {
1653            let rows: Option<Vec<ErrorRow>> = descs
1654                .iter()
1655                .map(|(id, dd)| descriptor_row(id, dd, &phi, hidden))
1656                .collect();
1657            let rows = rows.ok_or("calibrate_v2: a descriptor is malformed (dims / base64)")?;
1658            samples.push((ci, rows));
1659        }
1660    }
1661    let n_general = samples.iter().filter(|(c, _)| *c == 0).count();
1662    let n_in = samples.len() - n_general;
1663    if n_general == 0 {
1664        return Err("calibrate_v2: router.base carries no holdout (general prompts)".into());
1665    }
1666    if n_in == 0 {
1667        return Err("calibrate_v2: no skill carries an in-scope holdout".into());
1668    }
1669
1670    // Temperature: NLL of the true class over all classes (geometric grid).
1671    let mut best_t = 1.0f32;
1672    let mut best_nll = f32::INFINITY;
1673    let mut t = 1e-3f32;
1674    while t <= 1e6 {
1675        let mut nll = 0.0f32;
1676        for (ci, rows) in &samples {
1677            let mut logits: Vec<f32> = rows.iter().map(|r| -r.1 / t).collect();
1678            softmax(&mut logits);
1679            nll -= logits[*ci].max(1e-9).ln();
1680        }
1681        if nll < best_nll {
1682            best_nll = nll;
1683            best_t = t;
1684        }
1685        t *= 1.15;
1686    }
1687    let mut cal = RoutingCalibration {
1688        temperature: best_t,
1689        novelty_theta: 0.5,
1690        samples: samples.len(),
1691        target_fpr,
1692    };
1693    // θ: (1−fpr) quantile of in-scope novelty.
1694    let mut nov: Vec<f32> = samples
1695        .iter()
1696        .filter(|(c, _)| *c > 0)
1697        .map(|(_, rows)| decide(rows, Some(&cal), 1.0).novelty)
1698        .filter(|v| v.is_finite())
1699        .collect();
1700    nov.sort_by(|a, b| a.total_cmp(b));
1701    if !nov.is_empty() {
1702        let q = (1.0 - target_fpr).clamp(0.0, 1.0);
1703        let idx = (((nov.len() - 1) as f32) * q).round() as usize;
1704        cal.novelty_theta = (nov[idx.min(nov.len() - 1)] + 1e-4).min(0.999);
1705    }
1706
1707    // Measured under the full decision, every calibrated skill a candidate.
1708    let hash = skills_hash(header);
1709    let mut pol = policy.clone();
1710    pol.skills_hash = format!("{hash:016x}");
1711    let (mut fa, mut hit) = (0usize, 0usize);
1712    let mut per: Vec<(usize, usize)> = vec![(0, 0); descs.len()];
1713    for (ci, rows) in &samples {
1714        let d = decide_backbone_gated(Some(&rows[0]), &rows[1..], Some(&cal), Some(&pol), hash);
1715        if *ci == 0 {
1716            fa += usize::from(d.skill().is_some());
1717        } else {
1718            per[*ci].0 += 1;
1719            if d.skill() == Some(descs[*ci].0) {
1720                hit += 1;
1721                per[*ci].1 += 1;
1722            }
1723        }
1724    }
1725    let per_skill: serde_json::Map<String, serde_json::Value> = descs
1726        .iter()
1727        .enumerate()
1728        .skip(1)
1729        .map(|(i, (id, _))| {
1730            let (n, h) = per[i];
1731            (
1732                id.to_string(),
1733                serde_json::json!({"n": n, "recall": if n > 0 { h as f64 / n as f64 } else { f64::NAN }}),
1734            )
1735        })
1736        .collect();
1737    let measured = serde_json::json!({
1738        "set": "calibration",
1739        "n_in": n_in,
1740        "n_general": n_general,
1741        "in_scope_recall": hit as f64 / n_in as f64,
1742        "false_accept": fa as f64 / n_general as f64,
1743        "false_accept_upper95": clopper_pearson_upper(fa, n_general, 0.95),
1744        "temperature": cal.temperature,
1745        "novelty_theta": cal.novelty_theta,
1746        "target_fpr": target_fpr,
1747        "margin": policy.margin,
1748        "skills_hash": format!("{hash:016x}"),
1749        "per_skill": per_skill,
1750    });
1751    Ok((cal, measured))
1752}
1753
1754#[cfg(test)]
1755mod tests {
1756    use super::*;
1757    use cortiq_core::knowledge::skill_kind;
1758
1759    const HID: usize = 16;
1760
1761    #[test]
1762    fn pool_key_span_mixes_the_key_and_the_rest_as_unit_vectors() {
1763        // hidden 2, 4 positions, user text = positions 1..4, key = its first token
1764        let rows = [9.0, 9.0, 3.0, 0.0, 0.0, 5.0, 0.0, 5.0];
1765        let phi = pool_key_span(&rows, 2, 1..4, (0, 1), 0.5);
1766        let n = (1.0f32 + 0.25).sqrt();
1767        assert!((phi[0] - 1.0 / n).abs() < 1e-6 && (phi[1] - 0.5 / n).abs() < 1e-6, "{phi:?}");
1768        // a message that is only the key: the key alone
1769        let phi = pool_key_span(&rows[..4], 2, 1..2, (0, 1), 0.5);
1770        assert!((phi[0] - 1.0).abs() < 1e-6 && phi[1].abs() < 1e-6, "{phi:?}");
1771    }
1772
1773    #[test]
1774    fn holdout_count_is_a_fifth_at_least_one_and_leaves_train_samples() {
1775        for (n, want) in [(1, 1), (2, 1), (3, 1), (5, 1), (6, 1), (10, 2), (50, 10), (4000, 800)] {
1776            assert_eq!(holdout_count(n), want, "n = {n}");
1777        }
1778    }
1779
1780    #[test]
1781    fn encode_f16_round_trips_through_decode() {
1782        let v: Vec<f32> = (0..HID).map(|i| (i as f32 - 7.5) * 0.125).collect();
1783        assert_eq!(decode_f16(&encode_f16(&v)).unwrap(), v);
1784    }
1785
1786    /// A cloud spread along one direction (plus a little noise): the
1787    /// rank-1 basis is that direction, the rows are orthonormal at rank
1788    /// 2, the held-out tail is 20 % and never enters the mean, the error
1789    /// statistics are finite, and too few samples give None.
1790    #[test]
1791    fn fit_descriptor_recovers_the_dominant_direction() {
1792        let h = 8;
1793        let base: Vec<f32> = (0..h).map(|i| 1.0 + 0.05 * i as f32).collect();
1794        let dir: Vec<f32> = (0..h).map(|i| if i % 2 == 0 { 1.0 } else { -1.0 }).collect();
1795        let noise = gauss_vec(5, 40 * h);
1796        let samples: Vec<Vec<f32>> = (0..40)
1797            .map(|s| {
1798                let a = (s as f32 / 39.0 - 0.5) * 2.0;
1799                (0..h)
1800                    .map(|i| base[i] + a * dir[i] + 0.02 * noise[s * h + i])
1801                    .collect()
1802            })
1803            .collect();
1804        let d = fit_descriptor(&samples, 3, 1).expect("fit");
1805        assert_eq!(d.metric, METRIC_MSE_UNIT);
1806        assert_eq!(d.phi_layer, 3);
1807        assert_eq!(d.rank, 1);
1808        assert_eq!(d.holdout_n, Some(8));
1809        let basis = decode_f16(&d.basis).unwrap();
1810        assert_eq!(basis.len(), h);
1811        // The centered unit cloud varies along the unit-normalized dir
1812        // component orthogonal to the mean: the found row is aligned with
1813        // the unit-projected `dir` (sign free).
1814        let mean = decode_f16(&d.mean).unwrap();
1815        let unit = |v: &[f32]| unit_copy(v);
1816        let mut proj = vec![0f32; h];
1817        let (hi, lo) = (unit(&samples[39]), unit(&samples[0]));
1818        for i in 0..h {
1819            proj[i] = hi[i] - lo[i];
1820        }
1821        let proj = unit(&proj);
1822        let cos: f32 = basis.iter().zip(&proj).map(|(a, b)| a * b).sum();
1823        assert!(cos.abs() > 0.95, "cos {cos}");
1824        assert!(d.err_mean.unwrap().is_finite() && d.err_std.unwrap() >= 1e-4);
1825        assert_eq!(mean.len(), h);
1826        let hold = decode_f16(d.holdout.as_ref().unwrap()).unwrap();
1827        assert_eq!(hold.len(), 8 * h);
1828        // The held-out rows are the last 8 samples, unit-normalized.
1829        let last = unit(&samples[39]);
1830        for (a, b) in hold[7 * h..].iter().zip(&last) {
1831            assert!((a - b).abs() < 2e-3, "{a} vs {b}");
1832        }
1833        // rank 2: orthonormal rows.
1834        let d2 = fit_descriptor(&samples, 3, 2).unwrap();
1835        let b2 = decode_f16(&d2.basis).unwrap();
1836        let dot: f32 = b2[..h].iter().zip(&b2[h..]).map(|(a, b)| a * b).sum();
1837        let n1: f32 = b2[..h].iter().map(|a| a * a).sum::<f32>().sqrt();
1838        assert!(dot.abs() < 1e-2 && (n1 - 1.0).abs() < 1e-2, "dot {dot} n1 {n1}");
1839        // rank is clamped to train − 1 (3 samples → train 2 → rank 1).
1840        assert_eq!(fit_descriptor(&samples[..3], 0, 16).unwrap().rank, 1);
1841        assert!(fit_descriptor(&samples[..1], 0, 1).is_none());
1842        assert!(fit_descriptor(&[vec![1.0], vec![1.0, 2.0]], 0, 1).is_none());
1843        // Deterministic.
1844        assert_eq!(fit_descriptor(&samples, 3, 2).unwrap().basis, d2.basis);
1845    }
1846
1847    fn f16_b64(v: &[f32]) -> String {
1848        let bytes: Vec<u8> = v
1849            .iter()
1850            .flat_map(|x| cortiq_core::quant::f32_to_f16(*x).to_le_bytes())
1851            .collect();
1852        base64::engine::general_purpose::STANDARD.encode(bytes)
1853    }
1854
1855    fn header() -> CmfHeader {
1856        serde_json::from_value(serde_json::json!({
1857            "version": 2,
1858            "quant_type": "F32",
1859            "arch": {
1860                "arch_name": "synthetic", "hidden_size": HID, "intermediate_size": 32,
1861                "num_layers": 8, "num_attention_heads": 2, "num_kv_heads": 1, "head_dim": 8,
1862                "vocab_size": 16, "layer_types": vec!["FullAttention"; 8],
1863                "rms_norm_eps": 1e-6, "max_position_embeddings": 64
1864            }
1865        }))
1866        .unwrap()
1867    }
1868
1869    /// Deterministic xorshift noise in [−1, 1).
1870    struct Rng(u64);
1871    impl Rng {
1872        fn next(&mut self) -> f32 {
1873            self.0 ^= self.0 << 13;
1874            self.0 ^= self.0 >> 7;
1875            self.0 ^= self.0 << 17;
1876            ((self.0 >> 40) as f32 / (1u64 << 24) as f32) * 2.0 - 1.0
1877        }
1878    }
1879
1880    /// A unit sample of class `axis`: e_axis + σ·noise.
1881    fn sample(rng: &mut Rng, axis: usize, sigma: f32) -> Vec<f32> {
1882        let mut v: Vec<f32> = (0..HID).map(|_| sigma * rng.next()).collect();
1883        v[axis] += 1.0;
1884        unit_copy(&v)
1885    }
1886
1887    /// Descriptor fitted on `train` samples of class `axis`: mean = unit
1888    /// centroid, rank-1 basis on a noise axis, error stats of the train set.
1889    fn descriptor(
1890        train: &[Vec<f32>],
1891        holdout: &[Vec<f32>],
1892        noise_axis: usize,
1893    ) -> SelectionDescriptor {
1894        let mut mean = vec![0.0f32; HID];
1895        for s in train {
1896            for (m, x) in mean.iter_mut().zip(s) {
1897                *m += x / train.len() as f32;
1898            }
1899        }
1900        let mut basis = vec![0.0f32; HID];
1901        basis[noise_axis] = 1.0;
1902        let q = |v: &[f32]| -> Vec<f32> {
1903            v.iter()
1904                .map(|x| f16_to_f32(cortiq_core::quant::f32_to_f16(*x)))
1905                .collect()
1906        };
1907        let (mq, bq) = (q(&mean), q(&basis));
1908        let errs: Vec<f32> = train.iter().map(|s| recon_error(s, &mq, &bq, 1)).collect();
1909        let em = errs.iter().sum::<f32>() / errs.len() as f32;
1910        let es = (errs.iter().map(|e| (e - em).powi(2)).sum::<f32>() / errs.len() as f32).sqrt();
1911        SelectionDescriptor {
1912            metric: "mse_unit".into(),
1913            phi_layer: 2,
1914            mean: f16_b64(&mean),
1915            basis: f16_b64(&basis),
1916            rank: 1,
1917            err_mean: Some(em),
1918            err_std: Some(es),
1919            holdout: Some(f16_b64(&holdout.concat())),
1920            holdout_n: Some(holdout.len()),
1921        }
1922    }
1923
1924    fn policy(base: SelectionDescriptor) -> RouterPolicy {
1925        RouterPolicy {
1926            version: 2,
1927            policy: "backbone_gated".into(),
1928            granularity: "request".into(),
1929            phi: PhiSpec {
1930                layer: 2,
1931                pool: "span_mean".into(),
1932                norm: "unit".into(),
1933                key_alpha: None,
1934                followup_alpha: None,
1935                followup_margin: None,
1936                prefix_ids: vec![1, 7, 8],
1937                suffix_ids: vec![2, 1, 9],
1938            },
1939            base,
1940            margin: 0.05,
1941            skills_hash: "0".into(),
1942            measured: None,
1943            followup: None,
1944            keyless: None,
1945        }
1946    }
1947
1948    fn skill(id: &str, sel: SelectionDescriptor, status: &str) -> SkillRecord {
1949        SkillRecord {
1950            id: id.into(),
1951            layers: vec![5],
1952            selection: Some(sel),
1953            kind: Some(skill_kind::FFN_REPLACE.into()),
1954            status: Some(status.into()),
1955            gate: Some(serde_json::json!({"status": "measured"})),
1956            ..Default::default()
1957        }
1958    }
1959
1960    fn row(id: &str, e: f32) -> ErrorRow {
1961        (id.into(), e, Some(0.05), Some(0.05), 1.0)
1962    }
1963
1964    fn truth_policy() -> RouterPolicy {
1965        let mut p = policy(SelectionDescriptor {
1966            metric: "mse_unit".into(),
1967            phi_layer: 2,
1968            mean: String::new(),
1969            basis: String::new(),
1970            rank: 0,
1971            err_mean: None,
1972            err_std: None,
1973            holdout: None,
1974            holdout_n: None,
1975        });
1976        p.skills_hash = "00000000000000ab".into();
1977        p
1978    }
1979
1980    #[test]
1981    fn backbone_gated_truth_table() {
1982        let pol = truth_policy();
1983        let cal = RoutingCalibration {
1984            temperature: 0.1,
1985            novelty_theta: 0.99,
1986            samples: 10,
1987            target_fpr: 0.05,
1988        };
1989        let (base, near) = (row(BACKBONE_CLASS_ID, 0.9), row("herbs", 0.05));
1990        let go = |b: Option<&ErrorRow>,
1991                  s: &[ErrorRow],
1992                  c: Option<&RoutingCalibration>,
1993                  p: Option<&RouterPolicy>,
1994                  h: u64| { decide_backbone_gated(b, s, c, p, h) };
1995        let std_skills = std::slice::from_ref(&near);
1996
1997        // The skill wins only when everything holds.
1998        let d = go(Some(&base), std_skills, Some(&cal), Some(&pol), 0xab);
1999        assert_eq!(d.target, RouteTarget::Skill("herbs".into()), "{}", d.reason);
2000
2001        let cases: Vec<(&str, RouteDecision, &str)> = vec![
2002            (
2003                "no policy",
2004                go(Some(&base), std_skills, Some(&cal), None, 0xab),
2005                "no router policy",
2006            ),
2007            (
2008                "hash mismatch",
2009                go(Some(&base), std_skills, Some(&cal), Some(&pol), 0xac),
2010                "skills_hash mismatch",
2011            ),
2012            (
2013                "no calibration",
2014                go(Some(&base), std_skills, None, Some(&pol), 0xab),
2015                "not calibrated",
2016            ),
2017            (
2018                "no base row",
2019                go(None, std_skills, Some(&cal), Some(&pol), 0xab),
2020                "no backbone descriptor",
2021            ),
2022            (
2023                "no skills",
2024                go(Some(&base), &[], Some(&cal), Some(&pol), 0xab),
2025                "no routable skill",
2026            ),
2027            (
2028                "winner is the backbone",
2029                go(
2030                    Some(&row(BACKBONE_CLASS_ID, 0.01)),
2031                    std_skills,
2032                    Some(&cal),
2033                    Some(&pol),
2034                    0xab,
2035                ),
2036                "backbone is the nearest",
2037            ),
2038            (
2039                "novel",
2040                go(
2041                    Some(&base),
2042                    std_skills,
2043                    Some(&RoutingCalibration {
2044                        novelty_theta: 0.0,
2045                        ..cal.clone()
2046                    }),
2047                    Some(&pol),
2048                    0xab,
2049                ),
2050                "novel input",
2051            ),
2052            (
2053                "margin not beaten",
2054                go(
2055                    Some(&row(BACKBONE_CLASS_ID, 0.32)),
2056                    &[row("herbs", 0.30)],
2057                    Some(&cal),
2058                    Some(&pol),
2059                    0xab,
2060                ),
2061                "margin not beaten",
2062            ),
2063            (
2064                "degenerate φ",
2065                go(
2066                    Some(&(BACKBONE_CLASS_ID.into(), 0.9, None, None, 0.0)),
2067                    std_skills,
2068                    Some(&cal),
2069                    Some(&pol),
2070                    0xab,
2071                ),
2072                "degenerate",
2073            ),
2074        ];
2075        for (name, d, needle) in cases {
2076            assert_eq!(
2077                d.target,
2078                RouteTarget::Backbone,
2079                "case '{name}' routed to a skill"
2080            );
2081            assert!(
2082                d.reason.contains(needle),
2083                "case '{name}': reason '{}'",
2084                d.reason
2085            );
2086        }
2087    }
2088
2089    /// A unit sample around Σ w·e_axis + σ·noise.
2090    fn sample_mix(rng: &mut Rng, axes: &[(usize, f32)], sigma: f32) -> Vec<f32> {
2091        let mut v: Vec<f32> = (0..HID).map(|_| sigma * rng.next()).collect();
2092        for &(a, w) in axes {
2093            v[a] += w;
2094        }
2095        unit_copy(&v)
2096    }
2097
2098    /// R1: the runtime decision scores EVERY calibration class, the set
2099    /// `calibrate_v2` fitted on. `mushrooms` (quarantine) lies inside the
2100    /// affine subspace of `herbs` (active): its in-scope prompt is nearest
2101    /// to `mushrooms` → the BACKBONE runs. With the quarantined class
2102    /// dropped from the rows (the pre-fix `policy_rows`), the same prompt
2103    /// went to `herbs`.
2104    #[test]
2105    fn a_prompt_nearest_to_a_quarantined_class_runs_the_backbone() {
2106        let mut rng = Rng(0x5eed_1234_abcd_0001);
2107        let draw = |rng: &mut Rng, axes: &[(usize, f32)], n: usize| -> Vec<Vec<f32>> {
2108            (0..n).map(|_| sample_mix(rng, axes, 0.02)).collect()
2109        };
2110        let (gen_ax, herb_ax, mush_ax) = (vec![(0, 1.0)], vec![(1, 1.0)], vec![(1, 1.0), (4, 0.8)]);
2111        let (gt, gh) = (draw(&mut rng, &gen_ax, 200), draw(&mut rng, &gen_ax, 100));
2112        let (ht, hh) = (draw(&mut rng, &herb_ax, 200), draw(&mut rng, &herb_ax, 100));
2113        let (mt, mh) = (draw(&mut rng, &mush_ax, 200), draw(&mut rng, &mush_ax, 100));
2114        let mut h = header();
2115        h.router = Some(policy(descriptor(&gt, &gh, 3)));
2116        // herbs: mean e1, basis e4 — mushrooms (e1 + 0.8·e4) lie close to
2117        // that line, nearer to their own class.
2118        h.skills.push(skill("herbs", descriptor(&ht, &hh, 4), "active"));
2119        h.skills
2120            .push(skill("mushrooms", descriptor(&mt, &mh, 5), "quarantine"));
2121        let (mut cal, _) = calibrate_v2(&h, 0.05).unwrap();
2122        // Isolate the argmin: novelty never vetoes in this test.
2123        cal.novelty_theta = 0.999;
2124        h.routing = Some(cal.clone());
2125        let hash = skills_hash(&h);
2126        h.router.as_mut().unwrap().skills_hash = format!("{hash:016x}");
2127
2128        let (mut to_backbone, mut old_to_herbs) = (0usize, 0usize);
2129        let n = 100;
2130        for _ in 0..n {
2131            let q = sample_mix(&mut rng, &mush_ax, 0.02);
2132            let d = route_policy(&h, &q);
2133            assert_eq!(
2134                d.nearest_skill(),
2135                Some("mushrooms"),
2136                "the quarantined class is scored: {}",
2137                d.reason
2138            );
2139            if d.target == RouteTarget::Backbone {
2140                to_backbone += 1;
2141                assert!(d.reason.contains("not routable"), "{}", d.reason);
2142            }
2143            // Pre-fix rows: backbone + auto-routable skills only.
2144            let (base, rows) = policy_rows(&h, &q);
2145            let herbs_only: Vec<ErrorRow> = rows.into_iter().filter(|r| r.0 == "herbs").collect();
2146            let old = decide_backbone_gated(base.as_ref(), &herbs_only, Some(&cal), h.router.as_ref(), hash);
2147            old_to_herbs += usize::from(old.skill() == Some("herbs"));
2148            // The gate-measurement flag lets the quarantined class win.
2149            let dq = route_policy_with(&h, &q, RouteOptions { include_quarantine: true });
2150            assert_eq!(dq.skill(), Some("mushrooms"), "{}", dq.reason);
2151        }
2152        assert_eq!(to_backbone, n, "a mushroom prompt ran a skill");
2153        assert!(
2154            old_to_herbs * 10 >= n * 9,
2155            "the pre-fix decision did not misroute ({old_to_herbs}/{n}) — the regression is untested"
2156        );
2157        // herbs keeps its own prompts.
2158        for _ in 0..50 {
2159            let d = route_policy(&h, &sample_mix(&mut rng, &herb_ax, 0.02));
2160            assert_eq!(d.skill(), Some("herbs"), "{}", d.reason);
2161        }
2162        // stale_regate is scored too, and runnable only under the flag.
2163        h.skills[1].status = Some("stale_regate".into());
2164        let q = sample_mix(&mut rng, &mush_ax, 0.02);
2165        assert_eq!(route_policy(&h, &q).target, RouteTarget::Backbone);
2166        assert_eq!(
2167            route_policy_with(&h, &q, RouteOptions { include_quarantine: true }).skill(),
2168            Some("mushrooms")
2169        );
2170        assert!(no_candidate_reason(&h, RouteOptions::default()).is_none());
2171        h.skills[0].status = Some("quarantine".into());
2172        assert!(
2173            no_candidate_reason(&h, RouteOptions::default())
2174                .is_some_and(|r| r.contains("no routable skill class"))
2175        );
2176        assert!(no_candidate_reason(&h, RouteOptions { include_quarantine: true }).is_none());
2177    }
2178
2179    #[test]
2180    fn prompt_contract_is_enforced_in_one_place() {
2181        let mut h = header();
2182        h.skills.push(SkillRecord {
2183            prompt_contract: Some(PROMPT_CONTRACT_CMF_IM_V1.into()),
2184            ..skill("herbs", truth_policy().base, "active")
2185        });
2186        h.skills.push(SkillRecord {
2187            prompt_contract: Some("other-v9".into()),
2188            ..skill("odd", truth_policy().base, "active")
2189        });
2190        h.skills.push(skill("free", truth_policy().base, "active"));
2191        let pick = |id: &str| RouteDecision::forced(RouteTarget::Skill(id.into()), "t");
2192        // cmf-im-v1 frame satisfies it; a raw prompt is rendered when allowed.
2193        let (d, r) = enforce_prompt_contract(&h, pick("herbs"), PromptFrame::CmfImV1, false);
2194        assert_eq!((d.skill(), r), (Some("herbs"), false));
2195        let (d, r) = enforce_prompt_contract(&h, pick("herbs"), PromptFrame::Raw, true);
2196        assert_eq!((d.skill(), r), (Some("herbs"), true));
2197        // Not renderable here, another template, an unknown contract → backbone.
2198        for (id, frame, can) in [
2199            ("herbs", PromptFrame::Raw, false),
2200            ("herbs", PromptFrame::Other, true),
2201            ("odd", PromptFrame::CmfImV1, true),
2202        ] {
2203            let (d, r) = enforce_prompt_contract(&h, pick(id), frame, can);
2204            assert_eq!(d.target, RouteTarget::Backbone, "{id} {frame:?}");
2205            assert!(!r && d.reason.contains("not satisfied"), "{}", d.reason);
2206        }
2207        // No contract, or the backbone: untouched.
2208        let (d, r) = enforce_prompt_contract(&h, pick("free"), PromptFrame::Raw, false);
2209        assert_eq!((d.skill(), r), (Some("free"), false));
2210        let bb = RouteDecision::forced(RouteTarget::Backbone, "t");
2211        assert_eq!(enforce_prompt_contract(&h, bb, PromptFrame::Other, false).0.skill(), None);
2212        // The single-turn form and its inverse.
2213        let p = render_cmf_im_v1("Что лечит зверобой?");
2214        assert_eq!(p, "<|im_start|>user\nЧто лечит зверобой?<|im_end|>\n<|im_start|>assistant\n");
2215        assert_eq!(cmf_im_v1_single_user_turn(&p), Some("Что лечит зверобой?"));
2216        assert_eq!(cmf_im_v1_single_user_turn("Что лечит зверобой?"), None);
2217        let two = format!("{}{}", render_cmf_im_v1("a"), "b<|im_end|>\n<|im_start|>user\nc<|im_end|>\n<|im_start|>assistant\n");
2218        assert_eq!(cmf_im_v1_single_user_turn(&two), None, "history is not one turn");
2219        assert_eq!(
2220            cmf_im_v1_single_user_turn(
2221                "<|im_start|>system\ns<|im_end|>\n<|im_start|>user\nq<|im_end|>\n<|im_start|>assistant\n"
2222            ),
2223            None
2224        );
2225    }
2226
2227    #[test]
2228    fn skills_hash_tracks_descriptors_not_status() {
2229        let mut rng = Rng(7);
2230        let tr: Vec<Vec<f32>> = (0..20).map(|_| sample(&mut rng, 1, 0.1)).collect();
2231        let mut h = header();
2232        h.router = Some(policy(descriptor(&tr, &[], 3)));
2233        h.skills
2234            .push(skill("herbs", descriptor(&tr, &[], 4), "quarantine"));
2235        let h0 = skills_hash(&h);
2236        h.skills[0].status = Some("active".into());
2237        h.skills[0].gate = Some(serde_json::json!({"status": "measured", "x": 1}));
2238        h.skills[0].selection.as_mut().unwrap().holdout = Some("AAAA".into());
2239        assert_eq!(
2240            skills_hash(&h),
2241            h0,
2242            "status/gate/holdout do not invalidate the calibration"
2243        );
2244        // A v1 record is not a class.
2245        h.skills.push(SkillRecord {
2246            id: "legacy".into(),
2247            selection: Some(descriptor(&tr, &[], 5)),
2248            ..Default::default()
2249        });
2250        assert_eq!(skills_hash(&h), h0);
2251        // A changed descriptor, a new class, a retirement, a new base: stale.
2252        let mut h2 = h.clone();
2253        h2.skills[0].selection.as_mut().unwrap().err_mean = Some(0.123);
2254        assert_ne!(skills_hash(&h2), h0);
2255        let mut h3 = h.clone();
2256        h3.skills
2257            .push(skill("more", descriptor(&tr, &[], 6), "quarantine"));
2258        assert_ne!(skills_hash(&h3), h0);
2259        let mut h4 = h.clone();
2260        h4.skills[0].status = Some("retired".into());
2261        assert_ne!(skills_hash(&h4), h0);
2262        let mut h5 = h.clone();
2263        h5.router.as_mut().unwrap().base.rank = 0;
2264        assert_ne!(skills_hash(&h5), h0);
2265    }
2266
2267    #[test]
2268    fn phi_span_is_the_user_text_only() {
2269        let p = policy(descriptor(&[vec![1.0; HID]], &[], 0)).phi;
2270        let (ids, span) = phi_span_ids(&p, &[40, 41, 42, 43]);
2271        assert_eq!(ids, vec![1, 7, 8, 40, 41, 42, 43, 2, 1, 9]);
2272        assert_eq!(span, 3..7);
2273        assert_eq!(&ids[span.clone()], &[40, 41, 42, 43]);
2274        // span_mean + unit over exactly those rows
2275        let hiddens: Vec<f32> = (0..ids.len())
2276            .flat_map(|r| {
2277                (0..HID).map(move |c| {
2278                    if (3..7).contains(&r) {
2279                        (c == 0) as u8 as f32
2280                    } else {
2281                        100.0
2282                    }
2283                })
2284            })
2285            .collect();
2286        let phi = pool_span_unit(&hiddens, HID, span);
2287        assert!((phi[0] - 1.0).abs() < 1e-6 && phi[1..].iter().all(|x| *x == 0.0));
2288        assert!(
2289            pool_span_unit(&hiddens, HID, 5..5)
2290                .iter()
2291                .all(|x| *x == 0.0)
2292        );
2293    }
2294
2295    #[test]
2296    fn clopper_pearson_matches_reference_values() {
2297        // exact two-sided 95 % intervals (standard tables)
2298        assert!((clopper_pearson_upper(0, 10, 0.95) - 0.308_5).abs() < 1e-3);
2299        assert!((clopper_pearson_upper(5, 10, 0.95) - 0.812_9).abs() < 1e-3);
2300        assert!((clopper_pearson_upper(1, 100, 0.95) - 0.054_5).abs() < 1e-3);
2301        let u = clopper_pearson_upper(0, 500, 0.95);
2302        assert!((u - 0.007_351).abs() < 1e-5, "{u}");
2303        assert_eq!(clopper_pearson_upper(3, 3, 0.95), 1.0);
2304    }
2305
2306    /// Two separable classes (backbone around e0, herbs around e1): the
2307    /// fitted policy accepts no general sample and recalls the in-scope
2308    /// ones, and the file-level decision agrees on fresh samples.
2309    #[test]
2310    fn calibrate_v2_separates_backbone_and_skill() {
2311        let mut rng = Rng(0x9e37_79b9_7f4a_7c15);
2312        let draw = |rng: &mut Rng, axis: usize, n: usize| -> Vec<Vec<f32>> {
2313            (0..n).map(|_| sample(rng, axis, 0.1)).collect()
2314        };
2315        let (gt, gh) = (draw(&mut rng, 0, 200), draw(&mut rng, 0, 120));
2316        let (st, sh) = (draw(&mut rng, 1, 200), draw(&mut rng, 1, 120));
2317        let mut h = header();
2318        h.router = Some(policy(descriptor(&gt, &gh, 3)));
2319        h.skills
2320            .push(skill("herbs", descriptor(&st, &sh, 4), "active"));
2321
2322        let (cal, measured) = calibrate_v2(&h, 0.01).unwrap();
2323        assert_eq!(measured["false_accept"], 0.0, "{measured}");
2324        assert!(
2325            measured["in_scope_recall"].as_f64().unwrap() >= 0.98,
2326            "{measured}"
2327        );
2328        assert!(
2329            measured["false_accept_upper95"].as_f64().unwrap() < 0.031,
2330            "{measured}"
2331        );
2332        assert_eq!(measured["n_general"], 120);
2333        assert_eq!(measured["n_in"], 120);
2334
2335        // Publish the calibration the way a tool would, then route fresh φ.
2336        h.routing = Some(cal);
2337        let hash = skills_hash(&h);
2338        h.router.as_mut().unwrap().skills_hash = format!("{hash:016x}");
2339        let (mut fa, mut hit) = (0, 0);
2340        for _ in 0..200 {
2341            fa += usize::from(
2342                route_policy(&h, &sample(&mut rng, 0, 0.1))
2343                    .skill()
2344                    .is_some(),
2345            );
2346            hit +=
2347                usize::from(route_policy(&h, &sample(&mut rng, 1, 0.1)).skill() == Some("herbs"));
2348        }
2349        assert_eq!(fa, 0);
2350        assert!(hit >= 190, "recall {hit}/200");
2351        // An off-manifold input is not captured by the skill.
2352        let d = route_policy(&h, &sample(&mut rng, 9, 0.1));
2353        assert_eq!(d.target, RouteTarget::Backbone, "{}", d.reason);
2354
2355        // A quarantined skill is never auto-routed; a stale hash routes nothing.
2356        let mut q = h.clone();
2357        q.skills[0].status = Some("quarantine".into());
2358        assert_eq!(
2359            route_policy(&q, &sample(&mut rng, 1, 0.1)).target,
2360            RouteTarget::Backbone
2361        );
2362        let mut stale = h.clone();
2363        stale
2364            .skills
2365            .push(skill("new", descriptor(&st, &[], 5), "active"));
2366        let d = route_policy(&stale, &sample(&mut rng, 1, 0.1));
2367        assert_eq!(d.target, RouteTarget::Backbone);
2368        assert!(d.reason.contains("stale"), "{}", d.reason);
2369    }
2370}