Skip to main content

memra_sampling/
lib.rs

1//! Host-side sampler chain (BASE-2, MEMRA-BUILD-MAP §BASE-2). Ports llama.cpp CPU sampler
2//! semantics (llama-sampler.cpp): repetition/freq/presence penalties -> temperature -> top-k ->
3//! top-p -> min-p -> categorical draw. Greedy (temp<=0) = argmax, the bit-exact reference.
4//!
5//! Runs on the host over the full [n_vocab] f32 logit vector already brought back by the per-step
6//! D2H sync (decode.rs) — at B=2-4 this is single-µs, no GPU kernel needed (the GPU-fused sampler
7//! is a deferred PERF item, only needed once CUDA-graph removes the D2H barrier).
8
9use std::collections::HashMap;
10
11/// Sampler configuration. Defaults = greedy (temp 0). Order of application matches llama.cpp.
12#[derive(Clone, Debug)]
13pub struct SamplerConfig {
14    pub temperature: f32,      // <= 0.0 => greedy argmax (penalties/top-k/p ignored)
15    pub top_k: usize,          // 0 => disabled (keep all)
16    pub top_p: f32,            // 1.0 => disabled
17    pub min_p: f32,            // 0.0 => disabled
18    pub penalty_last_n: usize, // window of recent tokens for penalties (0 => disabled)
19    pub penalty_repeat: f32,   // 1.0 => disabled (llama default 1.0)
20    pub penalty_freq: f32,     // 0.0 => disabled
21    pub penalty_present: f32,  // 0.0 => disabled
22    pub seed: u64,
23}
24
25impl Default for SamplerConfig {
26    fn default() -> Self {
27        SamplerConfig {
28            temperature: 0.0,
29            top_k: 0,
30            top_p: 1.0,
31            min_p: 0.0,
32            penalty_last_n: 0,
33            penalty_repeat: 1.0,
34            penalty_freq: 0.0,
35            penalty_present: 0.0,
36            seed: 0,
37        }
38    }
39}
40
41/// SESSION-RESUME SAMPLER IDENTITY (lane/session-resume-sampler-predicate-20260820; receipts
42/// `research/spec-cache-20260818/SESSION-RESUME-PREDICATE.md`).
43///
44/// The canonical form of a request's sampler, for exactly one question: **may this request resume
45/// a parked whole session that some OTHER request's sampler shaped?** The spec pool's resume probe
46/// compared prompts and never samplers — that omission is how a filtered request inherited a draft
47/// graph captured unfiltered (`memra-engine` `SampledGraphKey`, lane/graph-s-key-exactness-
48/// 20260819). Keying the graph closed the exactness hole; it did not make cross-sampler resume
49/// SOUND, and the house posture in that situation is refuse-on-ambiguity with the refusal naming
50/// itself. This type is that predicate.
51///
52/// CANONICALIZATION, and why each rule is safe. Two encodings that name the same program must
53/// compare equal, or the predicate refuses resumes that cost nothing to allow:
54/// - `temperature <= 0.0` is GREEDY — `-1.0` and `0.0` are one program, so `temp_bits` is pinned
55///   to `0.0` in that regime and `greedy` carries the distinction. The greedy/sampled flip is
56///   itself a refusal: the two arms consume a parked `next_pred`/`pending_tok` differently and
57///   engage different captured draft graphs.
58/// - `top_k == 0`, `top_p >= 1.0`, `min_p <= 0.0` are each the OFF sentinel (matching
59///   `is_spec_sampling` and `SampledGraphKey::pure_temp`), canonicalized so `top_p 1.5` and
60///   `top_p 1.0` do not look like a change.
61/// - Penalties are OFF as a group iff `penalty_last_n == 0` or all three coefficients are neutral
62///   — the same `pen_on` predicate `spec.rs` computes. Off canonicalizes to the whole disabled
63///   tuple, so `penalty_last_n 64` with neutral coefficients equals penalties absent.
64///
65/// Float fields compare by BITS after canonicalization (no NaN/`-0.0` surprise), the same
66/// discipline `SampledGraphKey` uses.
67///
68/// `seed` IS carried and DELIBERATELY NOT COMPARED — see [`SamplerIdentity::mismatch`].
69#[derive(Clone, Copy, PartialEq, Eq, Debug)]
70pub struct SamplerIdentity {
71    greedy: bool,
72    temp_bits: u32,
73    /// Carried for the record and for callers that want to log it; NOT part of `mismatch`.
74    seed: u64,
75    top_k: usize,
76    top_p_bits: u32,
77    min_p_bits: u32,
78    penalty_last_n: usize,
79    penalty_repeat_bits: u32,
80    penalty_freq_bits: u32,
81    penalty_present_bits: u32,
82}
83
84impl SamplerIdentity {
85    /// Canonical identity of a sampler configuration.
86    pub fn of(cfg: &SamplerConfig) -> Self {
87        let greedy = cfg.temperature <= 0.0;
88        let pen_on = cfg.penalty_last_n > 0
89            && (cfg.penalty_repeat != 1.0 || cfg.penalty_freq != 0.0 || cfg.penalty_present != 0.0);
90        SamplerIdentity {
91            greedy,
92            temp_bits: if greedy { 0.0f32 } else { cfg.temperature }.to_bits(),
93            seed: cfg.seed,
94            top_k: cfg.top_k,
95            top_p_bits: if cfg.top_p >= 1.0 { 1.0f32 } else { cfg.top_p }.to_bits(),
96            min_p_bits: if cfg.min_p <= 0.0 { 0.0f32 } else { cfg.min_p }.to_bits(),
97            penalty_last_n: if pen_on { cfg.penalty_last_n } else { 0 },
98            penalty_repeat_bits: if pen_on { cfg.penalty_repeat } else { 1.0f32 }.to_bits(),
99            penalty_freq_bits: if pen_on { cfg.penalty_freq } else { 0.0f32 }.to_bits(),
100            penalty_present_bits: if pen_on { cfg.penalty_present } else { 0.0f32 }.to_bits(),
101        }
102    }
103
104    /// The seed this identity was built from (logging/receipts only — never compared).
105    pub fn seed(&self) -> u64 {
106        self.seed
107    }
108
109    /// The FIRST field on which `self` (an incoming request) differs from `parked` (the sampler
110    /// that shaped a parked session), as a stable name for the refusal line — `None` when the two
111    /// samplers are equivalent and the resume is legal. A refusal that does not say why is
112    /// indistinguishable from an unwired mechanism, so the name is the deliverable, not a nicety.
113    ///
114    /// Order is fixed and coarsest-first (`regime` before the field that only exists inside one
115    /// regime), so the reported name is the most informative one rather than an artifact of struct
116    /// layout.
117    ///
118    /// **`seed` IS NOT COMPARED, deliberately.** It is the one sampler field a resume may change,
119    /// for two reasons that are both mechanical:
120    /// - The only parked state that BAKES the seed is the sampled draft graph, and
121    ///   `SampledGraphKey` already carries `seed`: a seed change drops the parked graph and
122    ///   recaptures. (`memra-engine` `spec.rs`; pinned by `seed_alone_still_rekeys_the_draft_graph`
123    ///   in that crate's `sampled_graph_key` tests.)
124    /// - The session's persisted Philox counters (`SpecSession::sctr/uctr`) are counter-based:
125    ///   `philox(seed', ctr)` continued from another seed's counter position is an independent
126    ///   stream, not a repeated one. Reproducibility is already scoped per `(seed, session)`
127    ///   rather than per seed (`memra-server` `worker.rs`, the spec-burst sampling note), so a
128    ///   changed seed costs nothing that same-seed resume was not already costing.
129    ///
130    /// Comparing it would refuse essentially ALL sampled traffic: omitting `seed` on a serve
131    /// request draws fresh per-request entropy, so every turn of every seed-omitting conversation
132    /// would carry a "changed" seed. That is a cost with no soundness gain, which is exactly the
133    /// trade this predicate exists to make explicitly rather than by accident.
134    pub fn mismatch(&self, parked: &Self) -> Option<&'static str> {
135        if self.greedy != parked.greedy {
136            return Some("regime");
137        }
138        if self.temp_bits != parked.temp_bits {
139            return Some("temperature");
140        }
141        if self.top_k != parked.top_k {
142            return Some("top_k");
143        }
144        if self.top_p_bits != parked.top_p_bits {
145            return Some("top_p");
146        }
147        if self.min_p_bits != parked.min_p_bits {
148            return Some("min_p");
149        }
150        if self.penalty_last_n != parked.penalty_last_n {
151            return Some("penalty_last_n");
152        }
153        if self.penalty_repeat_bits != parked.penalty_repeat_bits {
154            return Some("penalty_repeat");
155        }
156        if self.penalty_freq_bits != parked.penalty_freq_bits {
157            return Some("penalty_freq");
158        }
159        if self.penalty_present_bits != parked.penalty_present_bits {
160            return Some("penalty_present");
161        }
162        None
163    }
164
165    /// THE PRE-LANE PREDICATE, RESTATED (teeth, not production). The spec pool-resume probe
166    /// applied no sampler test at all — it compared prompts and nothing else — so every sampler
167    /// pair was admitted. Restating it here keeps the refusal tests DECISIVE instead of
168    /// tautological: the same pair that `mismatch` names must be admitted by this, or the test is
169    /// asserting against a mechanism that never existed.
170    ///
171    /// It is also what `MEMRA_SPEC_RESUME_SAMPLER=0` selects at runtime (the rollback door and the
172    /// A/B arm the cost measurement needs), so this function is the single definition of "legacy"
173    /// for both the tests and the server.
174    pub fn legacy_admits(&self, _parked: &Self) -> bool {
175        true
176    }
177}
178
179#[derive(Clone, Copy)]
180enum NucleusOrdering {
181    Comparison,
182    PrefixRadix,
183}
184
185/// Stateful sampler: owns the RNG + the recent-token history (for penalties).
186pub struct Sampler {
187    cfg: SamplerConfig,
188    rng: SplitMix64,
189    history: Vec<u32>, // recently emitted tokens (for penalty window)
190    // Counts over exactly the active penalty window. Keeping this incrementally makes the host
191    // oracle cheaper too, and lets the serving path upload O(unique ids) sparse penalty state
192    // instead of either the full vocabulary or an O(history^2) device-side dedup walk.
193    penalty_counts: HashMap<u32, u32>,
194    nucleus_order: NucleusOrderScratch,
195    nucleus_ordering: NucleusOrdering,
196}
197
198impl Sampler {
199    pub fn new(cfg: SamplerConfig) -> Self {
200        Self::with_nucleus_ordering(cfg, NucleusOrdering::Comparison)
201    }
202
203    /// Ordering-only specialization for the qualified GLM plain TP2 serving route.
204    /// Broad synthetic rows regress when radix reaches a tie and repeats the sort;
205    /// other callers and sampling shapes therefore retain the comparison path.
206    /// This does not change any filter, probability arithmetic, or RNG draw.
207    pub fn for_glm5_plain_tp2(cfg: SamplerConfig) -> Self {
208        let neutral_penalties = cfg.penalty_last_n == 0
209            || (cfg.penalty_repeat == 1.0 && cfg.penalty_freq == 0.0 && cfg.penalty_present == 0.0);
210        let ordering = if cfg.temperature == 1.0
211            && cfg.top_p == 0.95
212            && cfg.top_k == 0
213            && cfg.min_p == 0.0
214            && neutral_penalties
215        {
216            NucleusOrdering::PrefixRadix
217        } else {
218            NucleusOrdering::Comparison
219        };
220        Self::with_nucleus_ordering(cfg, ordering)
221    }
222
223    fn with_nucleus_ordering(cfg: SamplerConfig, nucleus_ordering: NucleusOrdering) -> Self {
224        let rng = SplitMix64::new(cfg.seed);
225        Sampler {
226            cfg,
227            rng,
228            history: Vec::new(),
229            penalty_counts: HashMap::new(),
230            nucleus_order: NucleusOrderScratch::default(),
231            nucleus_ordering,
232        }
233    }
234
235    /// Diagnostic counts for the nucleus ordering mechanism, not sampling-policy state.
236    pub fn nucleus_sort_counts(&self) -> (u64, u64) {
237        (
238            self.nucleus_order.radix_calls,
239            self.nucleus_order.comparison_calls,
240        )
241    }
242
243    pub fn is_greedy(&self) -> bool {
244        self.cfg.temperature <= 0.0
245    }
246    /// Sampled spec in its FASTEST regime: pure temperature, no truncation filters, no
247    /// penalties. Filters and penalties are also distribution-exact under the rejection
248    /// verify (spec.rs applies both symmetrically to draft q and target p), so they remain
249    /// spec-ELIGIBLE — see `spec_eligible` in memra-server's worker, the authoritative
250    /// predicate. What they cost is the in-graph draft chain: the captured sampled draft
251    /// samples from the RAW softmax and can hold neither per-row filter stats nor a varying
252    /// penalty history, so `spec.rs` engages `graph_s` only in this pure-temp regime
253    /// (`pure_temp`) and otherwise falls back to the eager draft chain. This predicate names
254    /// that regime; it is NOT an eligibility test.
255    pub fn is_spec_sampling(&self) -> bool {
256        self.cfg.temperature > 0.0
257            && self.cfg.penalty_repeat == 1.0
258            && self.cfg.penalty_freq == 0.0
259            && self.cfg.penalty_present == 0.0
260            && self.cfg.top_k == 0
261            && self.cfg.top_p >= 1.0
262            && self.cfg.min_p <= 0.0
263    }
264    pub fn top_k(&self) -> usize {
265        self.cfg.top_k
266    }
267    pub fn penalty_last_n(&self) -> usize {
268        self.cfg.penalty_last_n
269    }
270    pub fn penalty_repeat(&self) -> f32 {
271        self.cfg.penalty_repeat
272    }
273    pub fn penalty_freq(&self) -> f32 {
274        self.cfg.penalty_freq
275    }
276    pub fn penalty_present(&self) -> f32 {
277        self.cfg.penalty_present
278    }
279    pub fn top_p(&self) -> f32 {
280        self.cfg.top_p
281    }
282    pub fn min_p(&self) -> f32 {
283        self.cfg.min_p
284    }
285    pub fn temperature(&self) -> f32 {
286        self.cfg.temperature
287    }
288    pub fn seed(&self) -> u64 {
289        self.cfg.seed
290    }
291    /// This sampler's canonical [`SamplerIdentity`] — the whole-session resume predicate's input.
292    pub fn identity(&self) -> SamplerIdentity {
293        SamplerIdentity::of(&self.cfg)
294    }
295
296    fn penalties_on(&self) -> bool {
297        self.cfg.penalty_last_n > 0
298            && (self.cfg.penalty_repeat != 1.0
299                || self.cfg.penalty_freq != 0.0
300                || self.cfg.penalty_present != 0.0)
301    }
302
303    /// Sparse `(token_id, count)` rows for the current penalty window. The order cannot affect
304    /// the arithmetic because every entry mutates one distinct logit; avoiding a per-token sort
305    /// is material on long agent histories.
306    pub fn penalty_counts(&self) -> Vec<(u32, u32)> {
307        debug_assert!(self.penalty_counts.values().all(|&count| count > 0));
308        self.penalty_counts
309            .iter()
310            .map(|(&id, &n)| (id, n))
311            .collect()
312    }
313
314    /// Record an emitted token so subsequent penalties see it.
315    pub fn accept(&mut self, token: u32) {
316        if self.penalties_on() {
317            let n = self.cfg.penalty_last_n;
318            if self.history.len() >= n {
319                let expired = self.history[self.history.len() - n];
320                let remove = {
321                    let count = self
322                        .penalty_counts
323                        .get_mut(&expired)
324                        .expect("active penalty window lost an accepted token");
325                    *count -= 1;
326                    *count == 0
327                };
328                if remove {
329                    self.penalty_counts.remove(&expired);
330                }
331            }
332            *self.penalty_counts.entry(token).or_insert(0) += 1;
333        }
334        self.history.push(token);
335    }
336
337    /// Sample the next token id from raw logits [n_vocab]. Does NOT mutate logits in place beyond
338    /// a local copy. Returns the chosen token id. (Caller should `accept()` it afterwards.)
339    pub fn sample(&mut self, logits: &[f32]) -> u32 {
340        // Greedy fast path: argmax over RAW logits (penalties don't change the argmax direction
341        // enough to matter for the reference path; llama greedy is also pre-penalty argmax only
342        // when no penalties set — but to stay correct under penalties we still apply them first).
343        if self.is_greedy()
344            && self.cfg.penalty_repeat == 1.0
345            && self.cfg.penalty_freq == 0.0
346            && self.cfg.penalty_present == 0.0
347        {
348            return argmax_u32(logits);
349        }
350
351        // Work on (id, logit) candidates.
352        let mut cand: Vec<(u32, f32)> = logits
353            .iter()
354            .enumerate()
355            .map(|(i, &l)| (i as u32, l))
356            .collect();
357
358        // 1. Penalties (operate on logits, over the last-n history window).
359        // `cand` is DENSE and INDEX-ALIGNED here by construction (built from
360        // `logits.iter().enumerate()` immediately above, nothing has filtered it yet), so the
361        // penalty pass indexes straight into it instead of hashing every candidate. See
362        // `apply_penalties_dense`.
363        self.apply_penalties_dense(&mut cand);
364
365        // Greedy-with-penalties: argmax after penalties, no sampling.
366        if self.is_greedy() {
367            let mut best = cand[0];
368            for &c in &cand[1..] {
369                if c.1 > best.1 {
370                    best = c;
371                }
372            }
373            return best.0;
374        }
375
376        // 2. Temperature scale.
377        if self.cfg.temperature > 0.0 && self.cfg.temperature != 1.0 {
378            let inv = 1.0 / self.cfg.temperature;
379            for c in cand.iter_mut() {
380                c.1 *= inv;
381            }
382        }
383
384        // 3. top-k: keep the k highest-logit candidates (partial sort by logit desc).
385        if self.cfg.top_k > 0 && self.cfg.top_k < cand.len() {
386            cand.sort_unstable_by(|a, b| b.1.total_cmp(&a.1));
387            cand.truncate(self.cfg.top_k);
388        }
389
390        // softmax over the surviving candidates (numerically stable).
391        softmax_inplace(&mut cand);
392
393        // 4. top-p (nucleus): smallest set whose cumulative prob >= top_p. Needs desc-by-prob order.
394        if self.cfg.top_p < 1.0 {
395            match self.nucleus_ordering {
396                NucleusOrdering::Comparison => self.nucleus_order.comparison(&mut cand),
397                NucleusOrdering::PrefixRadix => self.nucleus_order.sort(&mut cand, self.cfg.top_p),
398            }
399            let mut cum = 0.0f32;
400            let mut keep = 0usize;
401            for (i, c) in cand.iter().enumerate() {
402                cum += c.1;
403                keep = i + 1;
404                if cum >= self.cfg.top_p {
405                    break;
406                }
407            }
408            cand.truncate(keep.max(1));
409        }
410
411        // 5. min-p: keep candidates with prob >= min_p * max_prob.
412        if self.cfg.min_p > 0.0 {
413            let maxp = cand.iter().map(|c| c.1).fold(0.0f32, f32::max);
414            let thresh = self.cfg.min_p * maxp;
415            cand.retain(|c| c.1 >= thresh);
416            if cand.is_empty() {
417                return argmax_u32(logits);
418            } // safety
419        }
420
421        // renormalize the surviving probs and draw.
422        let sum: f32 = cand.iter().map(|c| c.1).sum();
423        let r = self.rng.next_f32() * sum;
424        let mut acc = 0.0f32;
425        for c in &cand {
426            acc += c.1;
427            if acc >= r {
428                return c.0;
429            }
430        }
431        cand.last().unwrap().0
432    }
433
434    /// llama.cpp penalty over a DENSE, INDEX-ALIGNED candidate slice: `cand[i].0 == i`.
435    ///
436    /// Same arithmetic as `apply_penalties_scan_reference`, on exactly the same elements, in the
437    /// same order — but O(distinct penalized tokens) instead of O(n_vocab) hash lookups.
438    ///
439    /// WHY (lane/glm5-host-audit, 2026-09-01). The scan form did one `HashMap<u32,u32>` SipHash
440    /// probe PER CANDIDATE, i.e. one per vocabulary entry: ~152k probes per token on the Qwen
441    /// class. `penalty_counts` holds at most `PEN_WINDOW_MAX` distinct ids and in practice the
442    /// number of distinct generated tokens so far, so the loop was inverted the expensive way
443    /// round. This matters in production, not in theory: `devsample_meta` refuses the device
444    /// sampler for any penalized config unless `MEMRA_SERVE_DEVPENALTY=1`, the whole fleet runs
445    /// it at 0, and a served model whose VENDOR-RECOMMENDED non-thinking arm carries
446    /// `presence_penalty` therefore lands every token of every request on this function.
447    ///
448    /// BIT-IDENTICAL BY CONSTRUCTION, and gated as such rather than asserted in prose: the set of
449    /// touched entries is identical (`penalty_counts` covers exactly the window, which the
450    /// debug_assert below re-checks), the per-entry arithmetic is copied unchanged, and each
451    /// entry is touched exactly once in both forms, so no float re-association is possible.
452    /// `apply_penalties_scan_reference` is kept as the ORACLE and
453    /// `dense_penalties_match_the_scan_reference_bitwise` compares them over randomized inputs.
454    ///
455    /// THE HOST ORACLE FOR DEVICE PENALTIES (lane/spec-exclusions-20260902): the memra-engine
456    /// `penalize_logits*` kernels (`cu/spec_sample.cu`, `keskar_penalize_rn`) are written to
457    /// produce these exact f32 bits for the same window, and the glm5 spec route's penalized
458    /// verify rows are gated against `penalized_logits` bit for bit
459    /// (`gpu_device_penalties_are_bit_identical_to_the_host_sampler`). Any change to the
460    /// arithmetic here is a change to the spec route's numerics too; keep the two in step.
461    fn apply_penalties_dense(&self, cand: &mut [(u32, f32)]) {
462        let n = self.cfg.penalty_last_n;
463        if n == 0 {
464            return;
465        }
466        if self.cfg.penalty_repeat == 1.0
467            && self.cfg.penalty_freq == 0.0
468            && self.cfg.penalty_present == 0.0
469        {
470            return;
471        }
472        let start = self.history.len().saturating_sub(n);
473        let window = &self.history[start..];
474        if window.is_empty() {
475            return;
476        }
477        debug_assert_eq!(
478            self.penalty_counts
479                .values()
480                .map(|&n| n as usize)
481                .sum::<usize>(),
482            window.len(),
483            "incremental penalty counts must cover the active history window"
484        );
485        for (&id, &cnt) in self.penalty_counts.iter() {
486            let Some(c) = cand.get_mut(id as usize) else {
487                // A count for an id outside the logits row: a vocab/count mismatch, which is a
488                // caller bug rather than something to silently skip. Loud in debug, inert in
489                // release (dropping the penalty is strictly safer than indexing out of bounds).
490                debug_assert!(
491                    false,
492                    "penalty count for id {id} is outside the {} candidate row",
493                    cand.len()
494                );
495                continue;
496            };
497            debug_assert_eq!(
498                c.0, id,
499                "apply_penalties_dense requires an index-aligned candidate slice"
500            );
501            // repeat: llama divides if logit>0 else multiplies (penalize toward 0)
502            if self.cfg.penalty_repeat != 1.0 {
503                if c.1 > 0.0 {
504                    c.1 /= self.cfg.penalty_repeat;
505                } else {
506                    c.1 *= self.cfg.penalty_repeat;
507                }
508            }
509            c.1 -= cnt as f32 * self.cfg.penalty_freq;
510            c.1 -= self.cfg.penalty_present; // presence: applied once if count>0
511        }
512    }
513
514    /// The penalized logits row this sampler would score the next token from, as bytes: a
515    /// copy of `logits` with the active penalty window applied through the ONE serving
516    /// penalty pass (`apply_penalties_dense`). Identity when penalties are off (window
517    /// empty or every coefficient neutral), exactly as `sample` sees it.
518    ///
519    /// EXISTS AS THE ORACLE SEAM for the device penalty kernels (lane/spec-exclusions-
520    /// 20260902): a spec route that penalizes verify rows on device claims "the same bits
521    /// the plain sampler produces", and that claim needs the plain sampler's bits to compare
522    /// against, not a re-derivation in a test. `sample` only ever returns the chosen id.
523    pub fn penalized_logits(&self, logits: &[f32]) -> Vec<f32> {
524        let mut cand: Vec<(u32, f32)> = logits
525            .iter()
526            .enumerate()
527            .map(|(i, &l)| (i as u32, l))
528            .collect();
529        self.apply_penalties_dense(&mut cand);
530        cand.into_iter().map(|(_, l)| l).collect()
531    }
532
533    /// llama.cpp penalty: for each token in the last-n history, repeat-divide/multiply its logit
534    /// and apply frequency*count + presence. (llama-sampler.cpp penalties.)
535    ///
536    /// THE ORACLE, not the serving path. `apply_penalties_dense` replaced it on the hot path
537    /// 2026-09-01; this form is retained verbatim so the replacement has something to be proven
538    /// bit-identical against, which is worth more than deleting it.
539    #[cfg(test)]
540    fn apply_penalties_scan_reference(&self, cand: &mut [(u32, f32)]) {
541        let n = self.cfg.penalty_last_n;
542        if n == 0 {
543            return;
544        }
545        if self.cfg.penalty_repeat == 1.0
546            && self.cfg.penalty_freq == 0.0
547            && self.cfg.penalty_present == 0.0
548        {
549            return;
550        }
551        let start = self.history.len().saturating_sub(n);
552        let window = &self.history[start..];
553        if window.is_empty() {
554            return;
555        }
556        debug_assert_eq!(
557            self.penalty_counts
558                .values()
559                .map(|&n| n as usize)
560                .sum::<usize>(),
561            window.len(),
562            "incremental penalty counts must cover the active history window"
563        );
564        for c in cand.iter_mut() {
565            if let Some(&cnt) = self.penalty_counts.get(&c.0) {
566                // repeat: llama divides if logit>0 else multiplies (penalize toward 0)
567                if self.cfg.penalty_repeat != 1.0 {
568                    if c.1 > 0.0 {
569                        c.1 /= self.cfg.penalty_repeat;
570                    } else {
571                        c.1 *= self.cfg.penalty_repeat;
572                    }
573                }
574                c.1 -= cnt as f32 * self.cfg.penalty_freq;
575                c.1 -= self.cfg.penalty_present; // presence: applied once if count>0
576            }
577        }
578    }
579}
580
581fn argmax_u32(logits: &[f32]) -> u32 {
582    let mut best = 0u32;
583    let mut bv = f32::NEG_INFINITY;
584    for (i, &v) in logits.iter().enumerate() {
585        if v > bv {
586            bv = v;
587            best = i as u32;
588        }
589    }
590    best
591}
592
593/// Ordering-only prefix radix path. Probability arithmetic and the downstream
594/// f32 cutoff/draw are unchanged. Resolve high-probability buckets first and stop
595/// at the exact nucleus, avoiding passes/copies over discarded vocabulary tails.
596/// The legacy unstable comparator still owns ties that affect retained IDs.
597#[derive(Default)]
598struct NucleusOrderScratch {
599    keys: Vec<u32>,
600    order: Vec<u32>,
601    scratch: Vec<u32>,
602    sorted: Vec<(u32, f32)>,
603    radix_calls: u64,
604    comparison_calls: u64,
605}
606
607enum NucleusVisit {
608    More,
609    Complete,
610    Tied,
611}
612
613fn descending_probability_key(value: f32) -> u32 {
614    let bits = value.to_bits();
615    let ascending = if bits & 0x8000_0000 == 0 {
616        bits ^ 0x8000_0000
617    } else {
618        !bits
619    };
620    // total_cmp distinguishes signed zeros, unlike DSV4's partial_cmp contract.
621    !ascending
622}
623
624impl NucleusOrderScratch {
625    fn comparison(&mut self, cand: &mut [(u32, f32)]) {
626        cand.sort_unstable_by(|a, b| b.1.total_cmp(&a.1));
627        self.comparison_calls += 1;
628    }
629
630    fn sort(&mut self, cand: &mut Vec<(u32, f32)>, top_p: f32) {
631        if cand.len() < 1024
632            || cand.len() > u32::MAX as usize
633            || cand.windows(2).all(|w| w[0].1.total_cmp(&w[1].1).is_ge())
634        {
635            self.comparison(cand);
636            return;
637        }
638        self.keys.resize(cand.len(), 0);
639        self.order.clear();
640        for (i, &(_, probability)) in cand.iter().enumerate() {
641            if !probability.is_finite() || probability < 0.0 {
642                self.comparison(cand);
643                return;
644            }
645            // Zero tails cannot contribute mass. If the positive prefix fails
646            // to reach the cutoff, fall back on the complete untouched input.
647            if probability != 0.0 {
648                self.keys[i] = descending_probability_key(probability);
649                self.order.push(i as u32);
650            }
651        }
652        self.scratch.resize(self.order.len(), 0);
653        self.sorted.clear();
654        let mut cumulative = 0.0f32;
655        let result = self.visit(cand, 0, self.order.len(), 24, top_p, &mut cumulative);
656        if !matches!(result, NucleusVisit::Complete) {
657            self.comparison(cand);
658            return;
659        }
660        // Only discard a tail after the exact original f32 cutoff is reached.
661        // The caller retains its original cutoff/min-p/renormalize/draw code.
662        cand.truncate(self.sorted.len());
663        cand.copy_from_slice(&self.sorted);
664        self.radix_calls += 1;
665    }
666
667    fn visit(
668        &mut self,
669        cand: &[(u32, f32)],
670        lo: usize,
671        hi: usize,
672        shift: i32,
673        top_p: f32,
674        cumulative: &mut f32,
675    ) -> NucleusVisit {
676        if shift < 0 && hi - lo > 1 {
677            return NucleusVisit::Tied;
678        }
679        if hi - lo <= 32 {
680            self.order[lo..hi]
681                .sort_unstable_by(|&a, &b| cand[b as usize].1.total_cmp(&cand[a as usize].1));
682            for i in lo..hi {
683                let candidate = cand[self.order[i] as usize];
684                // Same full key cannot straddle radix buckets. Look ahead in
685                // this leaf before retaining either member of an equal pair.
686                if i + 1 < hi
687                    && candidate
688                        .1
689                        .total_cmp(&cand[self.order[i + 1] as usize].1)
690                        .is_eq()
691                {
692                    return NucleusVisit::Tied;
693                }
694                self.sorted.push(candidate);
695                *cumulative += candidate.1;
696                if *cumulative >= top_p {
697                    return NucleusVisit::Complete;
698                }
699            }
700            return NucleusVisit::More;
701        }
702        let mut counts = [0usize; 256];
703        for &id in &self.order[lo..hi] {
704            counts[((self.keys[id as usize] >> shift) & 255) as usize] += 1;
705        }
706        let mut offsets = [0usize; 256];
707        let mut next = lo;
708        for (offset, &count) in offsets.iter_mut().zip(&counts) {
709            *offset = next;
710            next += count;
711        }
712        for &id in &self.order[lo..hi] {
713            let bucket = ((self.keys[id as usize] >> shift) & 255) as usize;
714            self.scratch[offsets[bucket]] = id;
715            offsets[bucket] += 1;
716        }
717        self.order[lo..hi].copy_from_slice(&self.scratch[lo..hi]);
718        let mut begin = lo;
719        for count in counts {
720            if count != 0 {
721                match self.visit(cand, begin, begin + count, shift - 8, top_p, cumulative) {
722                    NucleusVisit::More => {}
723                    result => return result,
724                }
725            }
726            begin += count;
727        }
728        NucleusVisit::More
729    }
730}
731
732/// Stable softmax over candidate logits, writing probs back into the logit slot.
733fn softmax_inplace(cand: &mut [(u32, f32)]) {
734    let maxl = cand.iter().map(|c| c.1).fold(f32::NEG_INFINITY, f32::max);
735    let mut sum = 0.0f32;
736    for c in cand.iter_mut() {
737        let e = (c.1 - maxl).exp();
738        c.1 = e;
739        sum += e;
740    }
741    let inv = if sum > 0.0 { 1.0 / sum } else { 0.0 };
742    for c in cand.iter_mut() {
743        c.1 *= inv;
744    }
745}
746
747/// SplitMix64 — deterministic seedable RNG (so a fixed seed reproduces the token stream for the
748/// validation gate). Not crypto; fine for sampling.
749struct SplitMix64 {
750    state: u64,
751}
752impl SplitMix64 {
753    fn new(seed: u64) -> Self {
754        SplitMix64 {
755            state: seed.wrapping_add(0x9E3779B97F4A7C15),
756        }
757    }
758    fn next_u64(&mut self) -> u64 {
759        self.state = self.state.wrapping_add(0x9E3779B97F4A7C15);
760        let mut z = self.state;
761        z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
762        z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
763        z ^ (z >> 31)
764    }
765    /// uniform f32 in [0,1).
766    fn next_f32(&mut self) -> f32 {
767        // top 24 bits -> [0,1)
768        ((self.next_u64() >> 40) as f32) / (1u32 << 24) as f32
769    }
770}
771
772#[cfg(test)]
773mod tests {
774    use super::*;
775
776    fn sorted_counts(s: &Sampler) -> Vec<(u32, u32)> {
777        let mut counts = s.penalty_counts();
778        counts.sort_unstable_by_key(|&(id, _)| id);
779        counts
780    }
781
782    /// THE GATE for the 2026-09-01 host-cost fix (lane/glm5-host-audit): the dense penalty pass
783    /// must reproduce the O(n_vocab) scan reference BIT FOR BIT, not merely closely.
784    ///
785    /// Compared as raw bit patterns (`to_bits`), because `==` on f32 would call two different
786    /// NaNs equal and would call `-0.0` and `0.0` equal — and `penalty_present` subtraction can
787    /// produce exactly `-0.0`. The inputs deliberately include negative logits (the repeat rule
788    /// branches on sign), repeated tokens (frequency count > 1), a token never generated (must be
789    /// untouched), and the three penalty coefficients both neutral and active.
790    #[test]
791    fn dense_penalties_match_the_scan_reference_bitwise() {
792        // A small deterministic LCG: this gate must not depend on a dev-dependency.
793        let mut state: u64 = 0x9E3779B97F4A7C15;
794        let mut next = || {
795            state = state
796                .wrapping_mul(6364136223846793005)
797                .wrapping_add(1442695040888963407);
798            ((state >> 33) as u32) as f32 / (u32::MAX >> 1) as f32 - 1.0
799        };
800
801        let vocab = 257usize; // prime-ish, and > any window used below
802        let mut cases = 0;
803        for &(repeat, freq, present) in &[
804            (1.0f32, 0.0f32, 0.0f32), // all neutral: both forms must no-op
805            (1.1, 0.0, 0.0),          // repeat only (exercises the sign branch)
806            (1.0, 0.5, 0.0),          // frequency only (count-scaled)
807            (1.0, 0.0, 1.5),          // presence only — the q38 non-thinking vendor arm
808            (1.1, 0.5, 1.5),          // all three together
809            (0.8, -0.25, -0.5),       // below/negative coefficients are legal API values
810        ] {
811            for &window in &[1usize, 3, 64, 8192] {
812                let cfg = SamplerConfig {
813                    temperature: 1.0,
814                    penalty_last_n: window,
815                    penalty_repeat: repeat,
816                    penalty_freq: freq,
817                    penalty_present: present,
818                    ..SamplerConfig::default()
819                };
820                let mut s = Sampler::new(cfg);
821                // Feed a history with repeats, and never feed id 0 or id 256 so the "untouched
822                // candidate" case is covered at both ends of the row.
823                for step in 0..40u32 {
824                    s.accept(1 + (step * 7) % 200);
825                    s.accept(1 + (step * 3) % 50); // guarantees counts > 1
826                }
827
828                let base: Vec<(u32, f32)> = (0..vocab).map(|i| (i as u32, next() * 8.0)).collect();
829                let mut dense = base.clone();
830                let mut reference = base.clone();
831                s.apply_penalties_dense(&mut dense);
832                s.apply_penalties_scan_reference(&mut reference);
833
834                assert_eq!(dense.len(), reference.len());
835                for (i, (d, r)) in dense.iter().zip(reference.iter()).enumerate() {
836                    assert_eq!(d.0, r.0, "candidate id moved at {i}");
837                    assert_eq!(
838                        d.1.to_bits(),
839                        r.1.to_bits(),
840                        "BIT DIVERGENCE at id {i} (repeat={repeat} freq={freq} \
841                         present={present} window={window}): dense {} vs reference {}",
842                        d.1,
843                        r.1
844                    );
845                }
846                // Ids never generated must be untouched — otherwise "identical" could be two
847                // equally-wrong passes.
848                assert_eq!(
849                    dense[0].1.to_bits(),
850                    base[0].1.to_bits(),
851                    "id 0 was penalized"
852                );
853                assert_eq!(
854                    dense[256].1.to_bits(),
855                    base[256].1.to_bits(),
856                    "id 256 was penalized"
857                );
858                cases += 1;
859            }
860        }
861        assert_eq!(cases, 24, "the coefficient x window matrix must all run");
862    }
863
864    /// The precondition the dense form trades correctness for speed on: the candidate row it is
865    /// handed is dense and index-aligned. Asserted on the REAL call path (`sample`), so a future
866    /// caller that filters before penalizing cannot quietly pass.
867    #[test]
868    fn the_sampled_path_penalizes_the_token_it_generated() {
869        let cfg = SamplerConfig {
870            temperature: 1.0,
871            penalty_last_n: 64,
872            penalty_present: 100.0, // large enough that the penalized id cannot win
873            seed: 7,
874            ..SamplerConfig::default()
875        };
876        let mut s = Sampler::new(cfg);
877        // Logits that make id 2 the runaway favourite, then penalize exactly id 2.
878        let logits = vec![0.0, 0.0, 20.0, 0.0];
879        s.accept(2);
880        for _ in 0..32 {
881            assert_ne!(
882                s.sample(&logits),
883                2,
884                "presence penalty on the generated id must move the draw off it, which only \
885                 happens if the dense pass indexed the right candidate"
886            );
887        }
888    }
889
890    #[test]
891    fn greedy_is_argmax() {
892        let mut s = Sampler::new(SamplerConfig::default()); // temp 0
893        let logits = vec![0.1, 5.0, 2.0, -1.0];
894        assert_eq!(s.sample(&logits), 1);
895    }
896
897    #[test]
898    fn temp_sampling_deterministic_with_seed() {
899        let cfg = SamplerConfig {
900            temperature: 1.0,
901            seed: 42,
902            ..Default::default()
903        };
904        let logits = vec![1.0, 2.0, 3.0, 0.5];
905        let a = Sampler::new(cfg.clone()).sample(&logits);
906        let b = Sampler::new(cfg).sample(&logits);
907        assert_eq!(a, b, "same seed must reproduce the draw");
908        assert!(a < 4);
909    }
910
911    #[test]
912    fn top_k_one_is_argmax() {
913        let cfg = SamplerConfig {
914            temperature: 1.0,
915            top_k: 1,
916            seed: 7,
917            ..Default::default()
918        };
919        let logits = vec![0.1, 5.0, 2.0, -1.0];
920        assert_eq!(
921            Sampler::new(cfg).sample(&logits),
922            1,
923            "top_k=1 collapses to argmax"
924        );
925    }
926
927    #[test]
928    fn min_p_keeps_only_high_prob() {
929        // logit 10 dominates; min_p 0.5 should drop the rest -> always pick id 2.
930        let cfg = SamplerConfig {
931            temperature: 1.0,
932            min_p: 0.5,
933            seed: 3,
934            ..Default::default()
935        };
936        let logits = vec![0.0, 0.0, 10.0, 0.0];
937        for _ in 0..16 {
938            assert_eq!(Sampler::new(cfg.clone()).sample(&logits), 2);
939        }
940    }
941
942    #[test]
943    fn repeat_penalty_suppresses_recent() {
944        // greedy + heavy repeat penalty: id 1 is argmax but recently emitted -> should drop it.
945        let cfg = SamplerConfig {
946            penalty_last_n: 8,
947            penalty_repeat: 100.0,
948            ..Default::default()
949        };
950        let mut s = Sampler::new(cfg);
951        s.accept(1); // 1 was just emitted
952        let logits = vec![4.0, 5.0, 4.5, 1.0]; // raw argmax = 1
953        let got = s.sample(&logits);
954        assert_ne!(
955            got, 1,
956            "recent token must be penalized out of greedy argmax"
957        );
958        assert_eq!(got, 2, "next-highest after penalizing 1");
959    }
960
961    #[test]
962    fn penalty_counts_follow_the_sliding_window() {
963        let cfg = SamplerConfig {
964            penalty_last_n: 4,
965            penalty_repeat: 1.1,
966            penalty_freq: 0.5,
967            penalty_present: 0.25,
968            ..Default::default()
969        };
970        let mut s = Sampler::new(cfg);
971        for tok in [7, 8, 7, 9] {
972            s.accept(tok);
973        }
974        assert_eq!(sorted_counts(&s), vec![(7, 2), (8, 1), (9, 1)]);
975
976        s.accept(8); // active window is now [8, 7, 9, 8]
977        assert_eq!(sorted_counts(&s), vec![(7, 1), (8, 2), (9, 1)]);
978        s.accept(10); // active window is now [7, 9, 8, 10]
979        assert_eq!(sorted_counts(&s), vec![(7, 1), (8, 1), (9, 1), (10, 1)]);
980        s.accept(11); // active window is now [9, 8, 10, 11]; final 7 expires
981        assert_eq!(sorted_counts(&s), vec![(8, 1), (9, 1), (10, 1), (11, 1)]);
982        assert!(!s.penalty_counts().iter().any(|&(id, n)| id == 7 || n == 0));
983    }
984
985    #[test]
986    fn full_context_and_disabled_penalty_counts_are_exact() {
987        let mut full = Sampler::new(SamplerConfig {
988            penalty_last_n: usize::MAX,
989            penalty_present: 1.5,
990            ..Default::default()
991        });
992        for tok in [3, 3, 4, 5, 3] {
993            full.accept(tok);
994        }
995        assert_eq!(sorted_counts(&full), vec![(3, 3), (4, 1), (5, 1)]);
996
997        let mut neutral = Sampler::new(SamplerConfig {
998            penalty_last_n: usize::MAX,
999            ..Default::default()
1000        });
1001        neutral.accept(3);
1002        assert!(neutral.penalty_counts().is_empty());
1003    }
1004}
1005
1006/// SESSION-RESUME SAMPLER PREDICATE teeth (lane/session-resume-sampler-predicate-20260820).
1007/// CPU-only, no GPU: the predicate is a pure function, so its whole contract is testable here and
1008/// a regression cannot hide behind "needs a card".
1009///
1010/// TEETH BOTH DIRECTIONS is the point. Every refusal test also asserts that `legacy_admits`
1011/// ADMITS the same pair — the pre-lane probe compared prompts and never samplers — so the test is
1012/// decisive (it fails on the old code) rather than tautological (passing because the pair was
1013/// never resumable for some other reason).
1014#[cfg(test)]
1015mod resume_sampler_predicate_tests {
1016    use super::*;
1017
1018    /// The vendor-default sampled shape the flip makes the majority of traffic.
1019    fn vendor() -> SamplerConfig {
1020        SamplerConfig {
1021            temperature: 0.7,
1022            top_k: 20,
1023            top_p: 0.95,
1024            seed: 20260820,
1025            ..Default::default()
1026        }
1027    }
1028
1029    /// Today's pure-temp shape — the one that parks a `graph_s`.
1030    fn pure_temp() -> SamplerConfig {
1031        SamplerConfig {
1032            temperature: 0.7,
1033            seed: 20260820,
1034            ..Default::default()
1035        }
1036    }
1037
1038    fn id(cfg: &SamplerConfig) -> SamplerIdentity {
1039        SamplerIdentity::of(cfg)
1040    }
1041
1042    // ---- direction 1: a SAME-sampler resume still resumes (no regression) ----
1043
1044    #[test]
1045    fn identical_sampler_resumes() {
1046        for cfg in [pure_temp(), vendor(), SamplerConfig::default()] {
1047            assert_eq!(
1048                id(&cfg).mismatch(&id(&cfg)),
1049                None,
1050                "a request must resume a session its own sampler shaped: {cfg:?}"
1051            );
1052        }
1053    }
1054
1055    #[test]
1056    fn disabled_sentinels_are_the_same_program() {
1057        // top_p >= 1.0, min_p <= 0.0, top_k == 0 all mean OFF; a client that spells OFF
1058        // differently on turn 2 must not lose its cache.
1059        let a = SamplerConfig {
1060            temperature: 0.7,
1061            top_p: 1.0,
1062            min_p: 0.0,
1063            ..Default::default()
1064        };
1065        let b = SamplerConfig {
1066            temperature: 0.7,
1067            top_p: 1.5,
1068            min_p: -1.0,
1069            ..Default::default()
1070        };
1071        assert_eq!(id(&a).mismatch(&id(&b)), None, "off spelled two ways");
1072    }
1073
1074    #[test]
1075    fn greedy_temperature_encodings_are_one_program() {
1076        let a = SamplerConfig {
1077            temperature: 0.0,
1078            ..Default::default()
1079        };
1080        let b = SamplerConfig {
1081            temperature: -1.0,
1082            ..Default::default()
1083        };
1084        assert_eq!(id(&a).mismatch(&id(&b)), None, "temp<=0 is one regime");
1085    }
1086
1087    #[test]
1088    fn neutral_penalty_coefficients_equal_penalties_absent() {
1089        // penalty_last_n set but every coefficient neutral == `pen_on == false` in spec.rs.
1090        let a = SamplerConfig {
1091            temperature: 0.7,
1092            penalty_last_n: 64,
1093            penalty_repeat: 1.0,
1094            penalty_freq: 0.0,
1095            penalty_present: 0.0,
1096            ..Default::default()
1097        };
1098        let b = SamplerConfig {
1099            temperature: 0.7,
1100            penalty_last_n: 0,
1101            ..Default::default()
1102        };
1103        assert_eq!(
1104            id(&a).mismatch(&id(&b)),
1105            None,
1106            "an inert penalty window is not a penalty change"
1107        );
1108    }
1109
1110    // ---- direction 2: a sampler-DIFFERING resume refuses, and names the field ----
1111
1112    #[test]
1113    fn the_reproduced_collision_pair_refuses_and_names_a_filter() {
1114        // The exact pair the predecessor reproduced on a live server: turn 1 pure-temp parks,
1115        // turn 2 adds top_p 0.95 / top_k 20 and resumes. Same seed, same temperature.
1116        let parked = id(&pure_temp());
1117        let incoming = id(&vendor());
1118        let field = incoming
1119            .mismatch(&parked)
1120            .expect("the reproduced collision pair must refuse");
1121        assert_eq!(field, "top_k", "coarsest-first order names top_k here");
1122        // DECISIVE: the pre-lane probe admitted exactly this pair.
1123        assert!(
1124            incoming.legacy_admits(&parked),
1125            "legacy must admit the collision pair, or this test proves nothing"
1126        );
1127    }
1128
1129    #[test]
1130    fn every_compared_field_refuses_on_its_own_and_names_itself() {
1131        let base = pure_temp();
1132        // A penalized base, so the three coefficients can each move ALONE: with penalties off on
1133        // the parked side, turning any of them on also moves `penalty_last_n`, and coarsest-first
1134        // order would (correctly) name the window instead of the coefficient.
1135        let pen_base = SamplerConfig {
1136            penalty_last_n: 64,
1137            penalty_repeat: 1.1,
1138            penalty_freq: 0.5,
1139            penalty_present: 0.5,
1140            ..base.clone()
1141        };
1142        // (field, parked, incoming) — exactly one canonical field differs in each row.
1143        let cases: [(&str, SamplerConfig, SamplerConfig); 9] = [
1144            (
1145                "regime",
1146                base.clone(),
1147                SamplerConfig {
1148                    temperature: 0.0,
1149                    ..base.clone()
1150                },
1151            ),
1152            (
1153                "temperature",
1154                base.clone(),
1155                SamplerConfig {
1156                    temperature: 0.8,
1157                    ..base.clone()
1158                },
1159            ),
1160            (
1161                "top_k",
1162                base.clone(),
1163                SamplerConfig {
1164                    top_k: 20,
1165                    ..base.clone()
1166                },
1167            ),
1168            (
1169                "top_p",
1170                base.clone(),
1171                SamplerConfig {
1172                    top_p: 0.95,
1173                    ..base.clone()
1174                },
1175            ),
1176            (
1177                "min_p",
1178                base.clone(),
1179                SamplerConfig {
1180                    min_p: 0.05,
1181                    ..base.clone()
1182                },
1183            ),
1184            (
1185                "penalty_last_n",
1186                pen_base.clone(),
1187                SamplerConfig {
1188                    penalty_last_n: 128,
1189                    ..pen_base.clone()
1190                },
1191            ),
1192            (
1193                "penalty_repeat",
1194                pen_base.clone(),
1195                SamplerConfig {
1196                    penalty_repeat: 1.2,
1197                    ..pen_base.clone()
1198                },
1199            ),
1200            (
1201                "penalty_freq",
1202                pen_base.clone(),
1203                SamplerConfig {
1204                    penalty_freq: 0.6,
1205                    ..pen_base.clone()
1206                },
1207            ),
1208            (
1209                "penalty_present",
1210                pen_base.clone(),
1211                SamplerConfig {
1212                    penalty_present: 0.6,
1213                    ..pen_base.clone()
1214                },
1215            ),
1216        ];
1217        for (expect, parked_cfg, cfg) in cases {
1218            let parked = id(&parked_cfg);
1219            let incoming = id(&cfg);
1220            assert_eq!(
1221                incoming.mismatch(&parked),
1222                Some(expect),
1223                "changing {expect} alone must refuse and name {expect} ({cfg:?})"
1224            );
1225            assert!(
1226                incoming.legacy_admits(&parked),
1227                "legacy must admit the {expect} change, or the refusal test is tautological"
1228            );
1229        }
1230        // Turning penalties ON from an unpenalized parked session is a `penalty_last_n` refusal —
1231        // the coarsest true statement about that pair, asserted so the order is pinned.
1232        assert_eq!(
1233            id(&pen_base).mismatch(&id(&base)),
1234            Some("penalty_last_n"),
1235            "penalties on vs off is named at the window, not at a coefficient"
1236        );
1237    }
1238
1239    #[test]
1240    fn greedy_to_sampled_and_back_both_refuse_as_regime() {
1241        let g = id(&SamplerConfig::default());
1242        let s = id(&pure_temp());
1243        assert_eq!(s.mismatch(&g), Some("regime"));
1244        assert_eq!(g.mismatch(&s), Some("regime"));
1245    }
1246
1247    // ---- the seed decision, pinned so it cannot change silently ----
1248
1249    #[test]
1250    fn seed_alone_does_not_refuse() {
1251        // DELIBERATE (see SamplerIdentity::mismatch): the draft graph is re-keyed on seed by
1252        // SampledGraphKey and the session's Philox counters are counter-based, so a changed seed
1253        // is sound — and comparing it would refuse every seed-omitting sampled conversation,
1254        // because an omitted seed draws fresh per-request entropy.
1255        let a = pure_temp();
1256        let b = SamplerConfig {
1257            seed: 999,
1258            ..a.clone()
1259        };
1260        assert_eq!(
1261            id(&a).mismatch(&id(&b)),
1262            None,
1263            "seed is carried but not compared"
1264        );
1265        assert_ne!(id(&a).seed(), id(&b).seed(), "the seed is still recorded");
1266    }
1267
1268    #[test]
1269    fn mismatch_is_symmetric_and_identity_is_an_equivalence() {
1270        let a = id(&pure_temp());
1271        let b = id(&vendor());
1272        assert_eq!(a.mismatch(&b).is_some(), b.mismatch(&a).is_some());
1273        assert_eq!(a.mismatch(&a), None);
1274        assert_eq!(b.mismatch(&b), None);
1275    }
1276}
1277
1278#[cfg(test)]
1279mod nucleus_order_tests;