Skip to main content

jugar_probar/perf_gate/
protocol.rs

1//! PP-LLAMA-001 v3.0 §5.1 — protocol parameters, band configuration, and the
2//! conformance predicate that makes a shrunken run say so.
3//!
4//! # Where the numbers live (PP-33)
5//!
6//! Every protocol parameter is declared in `scripts/perf-matrix.yaml` under
7//! `protocol:` and read from there by [`ProtocolParams::from_matrix`]. The
8//! `pub const`s below are the **spec fallback**: they exist so this module's
9//! own tests have a value to compare against when the matrix has not yet been
10//! amended, and so [`ProtocolParams::from_matrix`] can be proven to *differ*
11//! from them (`conformance_violations_read_the_loaded_params_not_the_consts`).
12//! They are documented fallbacks, never a silent default: a matrix without a
13//! `protocol:` block makes [`ProtocolParams::from_matrix`] return `Err` naming
14//! the missing block.
15//!
16//! Nothing in this file is a threshold; they are all protocol parameters.
17
18use std::time::Duration;
19
20use serde::{Deserialize, Serialize};
21
22/// §4.4.1 — the client model. Closed-loop is the only model this module
23/// implements, and it is *recorded* rather than assumed so the choice is
24/// falsifiable from the receipt alone.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum ClientModel {
28    /// `c` workers; each issues a request, waits for completion, immediately
29    /// issues the next.
30    ClosedLoop,
31}
32
33/// §4.4.2 — warmup requests are `WARMUP_MULTIPLIER × c`, discarded.
34pub const WARMUP_MULTIPLIER: usize = 2;
35/// §4.4.2 — quiesce between warmup completion and the first sampled request.
36pub const QUIESCE: Duration = Duration::from_secs(5);
37/// §4.4.2 — minimum sampled requests is `max(MIN_SAMPLES_FLOOR, MIN_SAMPLES_PER_WORKER × c)`.
38pub const MIN_SAMPLES_FLOOR: usize = 30;
39/// §4.4.2 — the per-worker term of the minimum-sample rule.
40pub const MIN_SAMPLES_PER_WORKER: usize = 8;
41/// §4.4.2 — minimum wall-clock per band.
42pub const MIN_WALL_CLOCK: Duration = Duration::from_secs(60);
43/// §4.4.3 — hard per-request timeout. A request exceeding this increments `timeouts`.
44pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(120);
45/// §5.1 — cooldown between the two lanes of one interleaved replicate.
46pub const COOLDOWN: Duration = Duration::from_secs(10);
47/// §4.3 — full band replicates per cell. **Five**, not three: `n = 3` sizes an
48/// effect and bounds no variance, so no σ-dependent status may change below 5.
49pub const REPLICATES: usize = 5;
50/// §4.3 — replicates are interleaved A,B,A,B,…; a non-interleaved receipt is
51/// `NONCONFORMANT-VALID` (PP-9's key carries `interleaved: true`).
52pub const INTERLEAVED: bool = true;
53/// §5.1 W1 — generated tokens per request, on the wire as OpenAI `max_tokens`.
54pub const N_PREDICT: u32 = 128;
55/// §5.1 — the pinned sampler temperature for both lanes (PP-28).
56pub const SAMPLER_TEMPERATURE: f64 = 0.0;
57/// §5.1 — the pinned sampler seed for both lanes (PP-28).
58pub const SAMPLER_SEED: u64 = 0;
59/// §5.1 — `ignore_eos` on both lanes, so every retained sample runs to
60/// `n_predict` (PP-28).
61pub const SAMPLER_IGNORE_EOS: bool = true;
62/// PP-27 — a live stream has `median(ttft / e2e)` well below 1; a replayed one
63/// approaches 1 because the whole answer arrives at once. Matrix key
64/// `stream.live_ttft_over_e2e_max`.
65pub const STREAM_LIVE_TTFT_OVER_E2E_MAX: f64 = 0.95;
66/// PP-26 — tokens that must agree between `m=1` and the batched run. Matrix key
67/// `witness.min_agree_tokens`.
68pub const WITNESS_MIN_AGREE_TOKENS: u32 = 64;
69/// §4.4.4 — bootstrap resamples.
70pub const BOOTSTRAP_RESAMPLES: usize = 10_000;
71/// §4.4.4 — bootstrap seed. Goes in the receipt; the interval is reproducible
72/// from the retained samples with this value and no other.
73pub const BOOTSTRAP_SEED: u64 = 2026;
74
75/// §4.4.2 — `max(30, 8 × c)`.
76#[must_use]
77pub fn min_sampled_requests(concurrency: usize) -> usize {
78    MIN_SAMPLES_FLOOR.max(MIN_SAMPLES_PER_WORKER * concurrency)
79}
80
81/// §4.4.2 — `2 × c`.
82#[must_use]
83pub fn warmup_requests(concurrency: usize) -> usize {
84    WARMUP_MULTIPLIER * concurrency
85}
86
87/// `scripts/perf-matrix.yaml`, compiled in.
88///
89/// PP-33 puts every number the gate compares against in that file. Reading it
90/// at runtime would make the producer depend on a path that does not exist in a
91/// published crate; `include_str!` binds the exact bytes of the checkout the
92/// binary was built from, which is also what the receipt's `commit` claims.
93///
94/// PMAT-958: the bytes arrive through `build.rs`, which copies the workspace
95/// file into `OUT_DIR` and refuses to build if the vendored copy shipped in the
96/// crate (`perf-matrix.vendored.yaml`) differs from it; a published crate, which
97/// has no `scripts/`, embeds the vendored copy.
98pub const PERF_MATRIX_SOURCE: &str = include_str!(concat!(env!("OUT_DIR"), "/perf-matrix.yaml"));
99
100/// §5.1 / PP-28 — the sampler pinned on both lanes, on the wire in every receipt.
101#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
102#[serde(deny_unknown_fields)]
103pub struct Sampler {
104    /// Greedy decoding. `0.0` in every conformant run.
105    pub temperature: f64,
106    /// The seed both lanes were given.
107    pub seed: u64,
108    /// `true` in W1, so `completion_tokens == n_predict` on every sample.
109    pub ignore_eos: bool,
110}
111
112impl Sampler {
113    /// The §5.1 pin, as the spec fallback.
114    #[must_use]
115    pub const fn spec_fallback() -> Self {
116        Self {
117            temperature: SAMPLER_TEMPERATURE,
118            seed: SAMPLER_SEED,
119            ignore_eos: SAMPLER_IGNORE_EOS,
120        }
121    }
122}
123
124/// §5.1 — the protocol block, read from `perf-matrix.yaml` and emitted verbatim
125/// at the receipt's top level as `protocol`.
126///
127/// A reader that cannot see the window, the warmup, the cooldown, the sampler
128/// and the replicate count cannot tell a 60 s conformant band from a 5 s one,
129/// and two receipts written under different protocols are not comparable — which
130/// is why the whole block is also in the PP-22 join key.
131#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
132#[serde(deny_unknown_fields)]
133pub struct ProtocolParams {
134    /// `T` — the measurement window, in milliseconds.
135    pub window_ms: u64,
136    /// Warmup requests per worker, discarded (§4.4.2's `2 × c` as a per-worker
137    /// count, so the band-level figure is `warmup_requests_per_worker × c`).
138    pub warmup_requests_per_worker: u32,
139    /// Quiesce between warmup completion and the first sampled request.
140    pub quiesce_ms: u64,
141    /// §5.1 — cooldown between the two lanes of an interleaved replicate.
142    pub cooldown_ms: u64,
143    /// Generated tokens per request (`max_tokens` on the wire).
144    pub n_predict: u32,
145    /// Replicates per cell. The matrix declares the FLOOR (`replicates_min`);
146    /// a receipt records the number actually run and is `NONCONFORMANT-VALID`
147    /// below it.
148    pub replicates: u32,
149    /// Whether the replicates alternated A,B,A,B,….
150    pub interleaved: bool,
151    /// The pinned sampler (PP-28).
152    pub sampler: Sampler,
153}
154
155impl ProtocolParams {
156    /// The spec literals, as a documented fallback for tests and for callers
157    /// that must render a receipt before the matrix carries a `protocol:` block.
158    ///
159    /// Never reached silently by [`Self::from_matrix`], which returns `Err`.
160    #[must_use]
161    pub const fn spec_fallback() -> Self {
162        Self {
163            window_ms: MIN_WALL_CLOCK.as_millis() as u64,
164            warmup_requests_per_worker: WARMUP_MULTIPLIER as u32,
165            quiesce_ms: QUIESCE.as_millis() as u64,
166            cooldown_ms: COOLDOWN.as_millis() as u64,
167            n_predict: N_PREDICT,
168            replicates: REPLICATES as u32,
169            interleaved: INTERLEAVED,
170            sampler: Sampler::spec_fallback(),
171        }
172    }
173
174    /// Read the `protocol:` block out of the compiled-in `perf-matrix.yaml`.
175    ///
176    /// # Errors
177    /// When the file does not parse as YAML, or when it carries no `protocol:`
178    /// block, or when the block is missing a key. The error NAMES the missing
179    /// thing: a protocol silently defaulted to the Rust consts is exactly the
180    /// drift PP-33 exists to prevent.
181    pub fn from_matrix() -> Result<Self, String> {
182        Self::from_matrix_source(PERF_MATRIX_SOURCE)
183    }
184
185    /// [`Self::from_matrix`] against an explicit document, so a test can feed a
186    /// matrix that differs from the shipped one.
187    ///
188    /// # Errors
189    /// As [`Self::from_matrix`].
190    pub fn from_matrix_source(source: &str) -> Result<Self, String> {
191        let block = matrix_block(source, "protocol")?;
192        let raw: MatrixProtocolBlock = serde_yaml_ng::from_value(block)
193            .map_err(|e| format!("perf-matrix.yaml `protocol:` block: {e}"))?;
194        Ok(Self {
195            window_ms: raw.window_ms,
196            warmup_requests_per_worker: raw.warmup_requests_per_worker,
197            quiesce_ms: raw.quiesce_ms,
198            cooldown_ms: raw.cooldown_ms,
199            n_predict: raw.n_predict,
200            replicates: raw.replicates_min,
201            interleaved: raw.interleaved,
202            sampler: raw.sampler,
203        })
204    }
205
206    /// The parameters a receipt written on this checkout must use, **with the
207    /// provenance of where they came from**.
208    ///
209    /// [`Self::from_matrix`] is the honest reader and returns `Err`; this is the
210    /// one place the fallback is taken, and the returned [`ProtocolSource`] is
211    /// what the producer prints once and — on the fallback — names in
212    /// `unproduced_fields`.
213    ///
214    /// The silent version of this ([`Self::effective`], which discarded the
215    /// error) put the Rust consts on the wire under a `protocol:` block the
216    /// receipt then claimed came from the matrix. PP-33's whole point is that
217    /// every gated number lives in `perf-matrix.yaml`; a receipt that quietly
218    /// substituted a compiled-in copy is the drift it exists to prevent, and it
219    /// was invisible because nothing ever called `source()`.
220    #[must_use]
221    pub fn effective_with_source() -> (Self, ProtocolSource) {
222        match Self::from_matrix() {
223            Ok(params) => (params, ProtocolSource::Matrix),
224            Err(reason) => (Self::spec_fallback(), ProtocolSource::SpecFallback(reason)),
225        }
226    }
227
228    /// [`Self::effective_with_source`] without the provenance, for callers that
229    /// record it separately (the CLI producer) or do not write a receipt at all
230    /// (tests, `BandConfig`).
231    #[must_use]
232    pub fn effective() -> Self {
233        Self::effective_with_source().0
234    }
235
236    /// `"perf-matrix.yaml"` when the matrix declares a `protocol:` block, and
237    /// the reason the fallback was taken otherwise.
238    ///
239    /// # Errors
240    /// When the matrix has no `protocol:` block; the error is the provenance
241    /// note a caller puts in `unproduced_fields`.
242    pub fn source() -> Result<&'static str, String> {
243        Self::from_matrix().map(|_| "perf-matrix.yaml `protocol:`")
244    }
245}
246
247/// PP-33 — where a [`ProtocolParams`] came from.
248///
249/// Not a boolean: the fallback carries the reason it was taken, because "the
250/// matrix has no `protocol:` block" and "the matrix does not parse as YAML" ask
251/// for different fixes and a receipt that says only "fallback" tells the reader
252/// neither.
253#[derive(Debug, Clone, PartialEq, Eq)]
254pub enum ProtocolSource {
255    /// Read from the compiled-in `scripts/perf-matrix.yaml`.
256    Matrix,
257    /// The matrix could not supply them; the reason is carried verbatim.
258    SpecFallback(String),
259}
260
261impl ProtocolSource {
262    /// The single line the producer prints before the first request.
263    #[must_use]
264    pub fn announcement(&self) -> String {
265        match self {
266            Self::Matrix => "protocol: matrix (scripts/perf-matrix.yaml `protocol:`)".to_string(),
267            Self::SpecFallback(reason) => {
268                format!("protocol: spec fallback because {reason}")
269            }
270        }
271    }
272
273    /// The `unproduced_fields` entry, or `None` when the matrix supplied the
274    /// parameters and there is nothing unproduced.
275    #[must_use]
276    pub fn unproduced_note(&self) -> Option<String> {
277        match self {
278            Self::Matrix => None,
279            Self::SpecFallback(reason) => Some(format!(
280                "PP-33 protocol — the `protocol:` block on this receipt is the compiled-in Rust \
281                 spec fallback, NOT scripts/perf-matrix.yaml: {reason}. Every protocol parameter \
282                 a gate compares against must live in the matrix; these came from consts and are \
283                 unverifiable against it."
284            )),
285        }
286    }
287}
288
289/// PP-27 — `stream.live_ttft_over_e2e_max` from the matrix.
290///
291/// # Errors
292/// When the matrix has no `stream:` block or no `live_ttft_over_e2e_max` key.
293pub fn stream_live_ttft_over_e2e_max_from(source: &str) -> Result<f64, String> {
294    let block = matrix_block(source, "stream")?;
295    let raw: MatrixStreamBlock = serde_yaml_ng::from_value(block)
296        .map_err(|e| format!("perf-matrix.yaml `stream:` block: {e}"))?;
297    Ok(raw.live_ttft_over_e2e_max)
298}
299
300/// [`stream_live_ttft_over_e2e_max_from`] over the compiled-in matrix, falling
301/// back to [`STREAM_LIVE_TTFT_OVER_E2E_MAX`] when the block is absent.
302#[must_use]
303pub fn stream_live_ttft_over_e2e_max() -> f64 {
304    stream_live_ttft_over_e2e_max_from(PERF_MATRIX_SOURCE).unwrap_or(STREAM_LIVE_TTFT_OVER_E2E_MAX)
305}
306
307/// PP-26 — `witness.min_agree_tokens` from the matrix.
308///
309/// # Errors
310/// When the matrix has no `witness:` block or no `min_agree_tokens` key.
311pub fn witness_min_agree_tokens_from(source: &str) -> Result<u32, String> {
312    let block = matrix_block(source, "witness")?;
313    let raw: MatrixWitnessBlock = serde_yaml_ng::from_value(block)
314        .map_err(|e| format!("perf-matrix.yaml `witness:` block: {e}"))?;
315    Ok(raw.min_agree_tokens)
316}
317
318/// [`witness_min_agree_tokens_from`] over the compiled-in matrix, falling back
319/// to [`WITNESS_MIN_AGREE_TOKENS`] when the block is absent.
320#[must_use]
321pub fn witness_min_agree_tokens() -> u32 {
322    witness_min_agree_tokens_from(PERF_MATRIX_SOURCE).unwrap_or(WITNESS_MIN_AGREE_TOKENS)
323}
324
325/// Pull one top-level block out of the matrix, naming what is missing.
326fn matrix_block(source: &str, key: &str) -> Result<serde_yaml_ng::Value, String> {
327    let doc: serde_yaml_ng::Value = serde_yaml_ng::from_str(source)
328        .map_err(|e| format!("perf-matrix.yaml does not parse as YAML: {e}"))?;
329    doc.get(key).cloned().ok_or_else(|| {
330        format!(
331            "perf-matrix.yaml has no `{key}:` block — PP-33 requires every protocol parameter and \
332             threshold to live there; refusing to substitute the Rust spec fallback silently"
333        )
334    })
335}
336
337/// The matrix spelling of the protocol block. Governance keys
338/// (`threshold_class`, `author`, `prompt_tokens`) are ignored here rather than
339/// mirrored, so adding one does not have to touch this crate.
340///
341/// These three `Matrix*` readers are the deliberate exception to the
342/// `deny_unknown_fields` rule every other `Deserialize` type in `perf_gate/`
343/// carries: PP-33 requires the matrix to hold governance metadata beside each
344/// number, and refusing an unknown key here would make adding an `author:` to
345/// `perf-matrix.yaml` fail this crate's build. The receipt types are the ones
346/// that must refuse a key they do not understand — the receipt is the evidence,
347/// the matrix is the policy.
348#[derive(Debug, Deserialize)]
349struct MatrixProtocolBlock {
350    window_ms: u64,
351    warmup_requests_per_worker: u32,
352    quiesce_ms: u64,
353    cooldown_ms: u64,
354    n_predict: u32,
355    replicates_min: u32,
356    interleaved: bool,
357    sampler: Sampler,
358}
359
360#[derive(Debug, Deserialize)]
361struct MatrixStreamBlock {
362    live_ttft_over_e2e_max: f64,
363}
364
365#[derive(Debug, Deserialize)]
366struct MatrixWitnessBlock {
367    min_agree_tokens: u32,
368}
369
370/// One band's measurement parameters.
371///
372/// [`BandConfig::conformant`] is the only constructor that produces §4.4-legal
373/// values. [`BandConfig::relaxed`] exists so unit tests can exercise the driver
374/// in milliseconds instead of minutes — and every run carries
375/// [`BandConfig::conformance_violations`] into its receipt, so a relaxed run is
376/// self-identifying rather than indistinguishable from a real one. A knob that
377/// lets you shrink the window without saying so is how a gate stops being able
378/// to fail.
379#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
380#[serde(deny_unknown_fields)]
381pub struct BandConfig {
382    /// Fixed concurrency `c`.
383    pub concurrency: usize,
384    /// Warmup requests, discarded and never written to the receipt.
385    pub warmup_requests: usize,
386    /// Quiesce between warmup completion and the first sampled request.
387    pub quiesce: Duration,
388    /// Minimum sampled requests to issue before the window may close.
389    pub min_samples: usize,
390    /// Minimum wall-clock the window must stay open.
391    pub min_wall_clock: Duration,
392    /// Hard per-request timeout.
393    pub request_timeout: Duration,
394    /// §5.1 — the pause between the two lanes of one interleaved replicate.
395    /// Without it the second lane inherits the first lane's thermal and VRAM
396    /// state, which is the drift interleaving exists to cancel.
397    pub cooldown: Duration,
398    /// Recorded, not assumed.
399    pub client_model: ClientModel,
400}
401
402impl BandConfig {
403    /// The §4.4-conformant configuration for concurrency `c`.
404    ///
405    /// # Panics
406    /// Never; `concurrency` of 0 is clamped to 1 so the worker pool always has
407    /// a worker. A zero-worker band is unrepresentable rather than empty.
408    #[must_use]
409    pub fn conformant(concurrency: usize) -> Self {
410        let concurrency = concurrency.max(1);
411        Self {
412            concurrency,
413            warmup_requests: warmup_requests(concurrency),
414            quiesce: QUIESCE,
415            min_samples: min_sampled_requests(concurrency),
416            min_wall_clock: MIN_WALL_CLOCK,
417            request_timeout: REQUEST_TIMEOUT,
418            cooldown: COOLDOWN,
419            client_model: ClientModel::ClosedLoop,
420        }
421    }
422
423    /// A shrunken configuration for tests and smoke runs. **Never conformant**:
424    /// [`Self::conformance_violations`] is non-empty by construction unless the
425    /// caller happens to pass the spec values back in.
426    #[must_use]
427    pub fn relaxed(
428        concurrency: usize,
429        min_samples: usize,
430        min_wall_clock: Duration,
431        quiesce: Duration,
432    ) -> Self {
433        let concurrency = concurrency.max(1);
434        Self {
435            concurrency,
436            warmup_requests: warmup_requests(concurrency),
437            quiesce,
438            min_samples,
439            min_wall_clock,
440            request_timeout: REQUEST_TIMEOUT,
441            cooldown: COOLDOWN,
442            client_model: ClientModel::ClosedLoop,
443        }
444    }
445
446    /// [`Self::relaxed`] with the §5.1 cooldown shrunk too, for tests that must
447    /// exercise the cooldown departure itself.
448    #[must_use]
449    pub fn relaxed_with_cooldown(
450        concurrency: usize,
451        min_samples: usize,
452        min_wall_clock: Duration,
453        quiesce: Duration,
454        cooldown: Duration,
455    ) -> Self {
456        Self {
457            cooldown,
458            ..Self::relaxed(concurrency, min_samples, min_wall_clock, quiesce)
459        }
460    }
461
462    /// Every way this configuration departs from the protocol, in prose, for
463    /// the receipt.
464    ///
465    /// Compares against [`ProtocolParams::effective`] — the `protocol:` block of
466    /// `perf-matrix.yaml` when it exists (PP-33), the spec fallback otherwise —
467    /// so amending the matrix moves this predicate rather than a Rust literal.
468    #[must_use]
469    pub fn conformance_violations(&self) -> Vec<String> {
470        self.conformance_violations_against(&ProtocolParams::effective())
471    }
472
473    /// [`Self::conformance_violations`] against explicit parameters, so a test
474    /// can prove the predicate reads the loaded block and not a constant.
475    #[must_use]
476    pub fn conformance_violations_against(&self, params: &ProtocolParams) -> Vec<String> {
477        let mut out = Vec::new();
478        let want_warmup = params.warmup_requests_per_worker as usize * self.concurrency;
479        if self.warmup_requests < want_warmup {
480            out.push(format!(
481                "§4.4.2 warmup_requests={} < {}*c={want_warmup}",
482                self.warmup_requests, params.warmup_requests_per_worker
483            ));
484        }
485        let want_quiesce = Duration::from_millis(params.quiesce_ms);
486        if self.quiesce < want_quiesce {
487            out.push(format!(
488                "§4.4.2 quiesce={:?} < {want_quiesce:?}",
489                self.quiesce
490            ));
491        }
492        let want_samples = min_sampled_requests(self.concurrency);
493        if self.min_samples < want_samples {
494            out.push(format!(
495                "§4.4.2 min_samples={} < max(30, 8*c)={want_samples}",
496                self.min_samples
497            ));
498        }
499        let want_window = Duration::from_millis(params.window_ms);
500        if self.min_wall_clock < want_window {
501            out.push(format!(
502                "§5.1 min_wall_clock={:?} < window_ms={want_window:?}",
503                self.min_wall_clock
504            ));
505        }
506        let want_cooldown = Duration::from_millis(params.cooldown_ms);
507        if self.cooldown < want_cooldown {
508            out.push(format!(
509                "§5.1 cooldown={:?} < cooldown_ms={want_cooldown:?}",
510                self.cooldown
511            ));
512        }
513        if self.request_timeout != REQUEST_TIMEOUT {
514            out.push(format!(
515                "§4.4.3 request_timeout={:?} != 120s",
516                self.request_timeout
517            ));
518        }
519        out
520    }
521
522    /// True when [`Self::conformance_violations`] is empty.
523    #[must_use]
524    pub fn is_conformant(&self) -> bool {
525        self.conformance_violations().is_empty()
526    }
527}
528
529/// §4.4.3 / §4.4.7 — how one request ended, and the §4.4.7 `SUSPECT` fraction.
530///
531/// Both are defined once, in [`super::drain`], and re-exported here so
532/// §4.4.1-§4.4.5 code keeps its `protocol::` path. The four outcomes are the
533/// four counters the receipt must carry, and they are mutually exclusive, so
534/// `requested = completed + timeouts + truncated + errors` holds by
535/// construction rather than by convention.
536///
537/// **[`Outcome::AbandonedAtDrain`] is the §4.4.7 sense of `truncated`, NOT
538/// `finish_reason == "length"`.** Under W1 every request stops at
539/// `max_tokens = 128` with EOS ignored, so reading `truncated` as the
540/// finish-reason sense would exclude the entire workload from `agg_tok_s`'s
541/// numerator and report zero throughput for a healthy server. The variant is
542/// spelled `AbandonedAtDrain` rather than `Truncated` precisely so the two
543/// senses cannot be conflated; conflating them is how two conformant harnesses
544/// produce incomparable receipts.
545pub use super::drain::{Outcome, DRAIN_SUSPECT_FRACTION};
546
547/// §4.4.6 — the `tokenization` block lives in [`super::receipt`].
548///
549/// This module used to carry its own `Tokenization` struct with an
550/// `Option<String>` digest and a `validate()`.
551/// [`super::receipt::TokenizationBlock`] is the same §4.4.6 block as an enum,
552/// in which "`client_tokenizer` with no digest" is unrepresentable rather than
553/// merely rejected, and it is the one wired into the emitter
554/// `scripts/perf_gate.sh` reads. Keeping both would be two spellings of one
555/// schema, free to drift.
556pub use super::receipt::{TokenCountingMethod, TokenizationBlock};
557
558#[cfg(test)]
559mod tests {
560    use super::*;
561
562    /// A `perf-matrix.yaml` fragment carrying exactly the keys PP-33 puts
563    /// there. Used so the loader is exercised whether or not the shipped matrix
564    /// has been amended yet.
565    const FIXTURE_MATRIX: &str = "\
566schema_version: 2
567protocol:
568  window_ms: 60000
569  warmup_requests_per_worker: 2
570  quiesce_ms: 5000
571  cooldown_ms: 10000
572  n_predict: 128
573  prompt_tokens: 512
574  replicates_min: 5
575  interleaved: true
576  sampler: {temperature: 0.0, seed: 0, ignore_eos: true}
577  threshold_class: policy
578  author: spec-owner
579stream:
580  live_ttft_over_e2e_max: 0.95
581  threshold_class: policy
582  author: spec-owner
583witness:
584  min_agree_tokens: 64
585  threshold_class: policy
586  author: spec-owner
587";
588
589    #[test]
590    fn min_sampled_requests_is_max_30_or_8c() {
591        assert_eq!(min_sampled_requests(1), 30);
592        assert_eq!(min_sampled_requests(3), 30);
593        assert_eq!(min_sampled_requests(4), 32);
594        assert_eq!(min_sampled_requests(8), 64);
595        assert_eq!(min_sampled_requests(16), 128);
596    }
597
598    #[test]
599    fn warmup_is_two_per_worker() {
600        for c in [1_usize, 4, 8, 16] {
601            assert_eq!(warmup_requests(c), 2 * c);
602        }
603    }
604
605    /// The shipped defaults ARE the spec values. If someone edits a constant,
606    /// this is the test that reds.
607    #[test]
608    fn conformant_config_matches_the_spec_literals() {
609        for c in [1_usize, 4, 8, 16] {
610            let cfg = BandConfig::conformant(c);
611            assert_eq!(cfg.concurrency, c);
612            assert_eq!(cfg.warmup_requests, 2 * c);
613            assert_eq!(cfg.quiesce, Duration::from_secs(5));
614            assert_eq!(cfg.min_samples, 30.max(8 * c));
615            assert_eq!(cfg.min_wall_clock, Duration::from_secs(60));
616            assert_eq!(cfg.request_timeout, Duration::from_secs(120));
617            assert_eq!(cfg.cooldown, Duration::from_secs(10));
618            assert_eq!(cfg.client_model, ClientModel::ClosedLoop);
619            assert!(
620                cfg.is_conformant(),
621                "violations: {:?}",
622                cfg.conformance_violations()
623            );
624        }
625    }
626
627    /// A matrix source with no `protocol:` block does not quietly become the
628    /// Rust constants: the reader says which block is missing (PP-33).
629    #[test]
630    fn a_matrix_without_a_protocol_block_is_an_error_not_a_default() {
631        let err = ProtocolParams::from_matrix_source("schema_version: 2\nbands: [1, 4]\n")
632            .expect_err("no protocol block");
633        assert!(err.contains("`protocol:`"), "{err}");
634        assert!(err.contains("PP-33"), "{err}");
635    }
636
637    /// And a block that is present is read field by field.
638    #[test]
639    fn the_protocol_block_is_read_from_the_matrix() {
640        let p = ProtocolParams::from_matrix_source(FIXTURE_MATRIX).expect("block parses");
641        assert_eq!(p.window_ms, 60_000);
642        assert_eq!(p.warmup_requests_per_worker, 2);
643        assert_eq!(p.quiesce_ms, 5_000);
644        assert_eq!(p.cooldown_ms, 10_000);
645        assert_eq!(p.n_predict, 128);
646        assert_eq!(
647            p.replicates, 5,
648            "matrix `replicates_min` is the receipt's n floor"
649        );
650        assert!(p.interleaved);
651        assert_eq!(p.sampler.temperature, 0.0);
652        assert_eq!(p.sampler.seed, 0);
653        assert!(p.sampler.ignore_eos);
654    }
655
656    /// THE POINT: the conformance predicate reads the LOADED parameters. Feed a
657    /// matrix declaring a 120 s window and a 60 s band stops being conformant —
658    /// which a predicate hard-coded to `MIN_WALL_CLOCK` could not do.
659    #[test]
660    fn conformance_violations_read_the_loaded_params_not_the_consts() {
661        let cfg = BandConfig::conformant(4);
662        let spec = ProtocolParams::spec_fallback();
663        assert!(cfg.conformance_violations_against(&spec).is_empty());
664
665        let wider = ProtocolParams {
666            window_ms: 120_000,
667            ..spec
668        };
669        let v = cfg.conformance_violations_against(&wider);
670        assert_eq!(v.len(), 1, "{v:?}");
671        assert!(v[0].contains("min_wall_clock"), "{v:?}");
672    }
673
674    /// The cooldown is a departure like any other, and it is checked.
675    #[test]
676    fn a_missing_cooldown_is_a_conformance_violation() {
677        let cfg = BandConfig::relaxed_with_cooldown(
678            4,
679            32,
680            Duration::from_secs(60),
681            Duration::from_secs(5),
682            Duration::ZERO,
683        );
684        let v = cfg.conformance_violations();
685        assert_eq!(v.len(), 1, "{v:?}");
686        assert!(v[0].contains("cooldown"), "{v:?}");
687    }
688
689    /// `effective()` takes the matrix when it has the block and the documented
690    /// fallback otherwise, and `source()` says which — so a caller can name the
691    /// substitution in `unproduced_fields` rather than have it be silent.
692    #[test]
693    fn the_effective_params_say_where_they_came_from() {
694        let effective = ProtocolParams::effective();
695        match ProtocolParams::source() {
696            Ok(where_from) => {
697                assert_eq!(where_from, "perf-matrix.yaml `protocol:`");
698                assert_eq!(effective, ProtocolParams::from_matrix().expect("block"));
699            }
700            Err(reason) => {
701                assert!(reason.contains("`protocol:`"), "{reason}");
702                assert_eq!(effective, ProtocolParams::spec_fallback());
703            }
704        }
705    }
706
707    /// PP-27's and PP-26's numbers come out of the matrix too.
708    #[test]
709    fn the_stream_and_witness_thresholds_are_read_from_the_matrix() {
710        assert_eq!(
711            stream_live_ttft_over_e2e_max_from(FIXTURE_MATRIX).expect("stream block"),
712            0.95
713        );
714        assert_eq!(
715            witness_min_agree_tokens_from(FIXTURE_MATRIX).expect("witness block"),
716            64
717        );
718        assert!(stream_live_ttft_over_e2e_max_from("bands: [1]\n").is_err());
719        assert!(witness_min_agree_tokens_from("bands: [1]\n").is_err());
720    }
721
722    /// When `scripts/perf-matrix.yaml` DOES declare the block, it must agree
723    /// with the fallback the tests compare against — otherwise the two drift
724    /// and a receipt is written under one protocol and validated under another.
725    #[test]
726    fn the_shipped_matrix_block_when_present_agrees_with_the_spec_fallback() {
727        match ProtocolParams::from_matrix() {
728            Ok(loaded) => assert_eq!(
729                loaded,
730                ProtocolParams::spec_fallback(),
731                "scripts/perf-matrix.yaml `protocol:` disagrees with protocol.rs's fallback"
732            ),
733            Err(reason) => assert!(
734                reason.contains("`protocol:`"),
735                "the only acceptable absence is a named one: {reason}"
736            ),
737        }
738    }
739
740    /// PP-33 — `effective_with_source` says WHICH of the two sources supplied
741    /// the block, and the fallback carries the reason.
742    ///
743    /// `effective()` swallowed the error and `source()` was never called by
744    /// anything, so a run whose matrix did not parse put the compiled-in Rust
745    /// constants on the wire under a `protocol:` block the receipt then
746    /// presented as the matrix's. PP-33's whole point is that every number a
747    /// gate compares against lives in `perf-matrix.yaml`; a silent compiled-in
748    /// substitute is the drift it exists to prevent.
749    #[test]
750    fn the_protocol_source_is_reported_and_the_fallback_says_why() {
751        let (params, source) = ProtocolParams::effective_with_source();
752        match ProtocolParams::from_matrix() {
753            Ok(from_matrix) => {
754                assert_eq!(source, ProtocolSource::Matrix);
755                assert_eq!(params, from_matrix);
756                assert!(source.announcement().contains("matrix"), "{source:?}");
757                assert!(
758                    source.unproduced_note().is_none(),
759                    "the matrix supplied them; nothing is unproduced"
760                );
761            }
762            Err(reason) => {
763                assert_eq!(source, ProtocolSource::SpecFallback(reason.clone()));
764                assert_eq!(params, ProtocolParams::spec_fallback());
765            }
766        }
767
768        // The fallback's own two obligations, whatever the shipped matrix does:
769        // it names the reason on stdout, and it names itself in the receipt.
770        let fallback =
771            ProtocolSource::SpecFallback("perf-matrix.yaml has no `protocol:` block".to_string());
772        assert_eq!(
773            fallback.announcement(),
774            "protocol: spec fallback because perf-matrix.yaml has no `protocol:` block"
775        );
776        let note = fallback
777            .unproduced_note()
778            .expect("a fallback is an unproduced field");
779        assert!(note.contains("PP-33"), "{note}");
780        assert!(note.contains("no `protocol:` block"), "{note}");
781        assert!(
782            note.contains("NOT scripts/perf-matrix.yaml"),
783            "the note must say the block on the wire is not the matrix's: {note}"
784        );
785    }
786
787    /// The escape hatch must be visible from the receipt. A relaxed run that
788    /// reported itself conformant is exactly the fabricated-baseline class.
789    #[test]
790    fn relaxed_config_reports_every_departure() {
791        let cfg = BandConfig::relaxed(4, 8, Duration::from_millis(50), Duration::ZERO);
792        assert!(!cfg.is_conformant());
793        let v = cfg.conformance_violations();
794        assert_eq!(
795            v.len(),
796            3,
797            "expected quiesce+min_samples+min_wall, got {v:?}"
798        );
799        assert!(v.iter().any(|s| s.contains("quiesce")));
800        assert!(v.iter().any(|s| s.contains("min_samples")));
801        assert!(v.iter().any(|s| s.contains("min_wall_clock")));
802    }
803
804    #[test]
805    fn client_model_serializes_as_closed_loop() {
806        let j =
807            serde_json::to_string(&ClientModel::ClosedLoop).expect("ClientModel must serialize");
808        assert_eq!(j, "\"closed_loop\"");
809    }
810
811    /// `protocol::REQUEST_TIMEOUT` (a `Duration`) and `drain::REQUEST_TIMEOUT_MS`
812    /// (an `f64`) are the same §4.4.3 limit in two types the compiler cannot
813    /// unify. Editing one without the other is the drift this pins.
814    #[test]
815    fn the_two_request_timeout_spellings_agree() {
816        assert_eq!(
817            REQUEST_TIMEOUT.as_millis(),
818            u128::from(super::super::drain::REQUEST_TIMEOUT_MS as u64)
819        );
820    }
821
822    /// The §4.4.6 block is `receipt::TokenizationBlock`; the poka-yoke that used
823    /// to live on `protocol::Tokenization` moved with it and is still enforced.
824    #[test]
825    fn declared_method_and_available_counter_must_agree() {
826        let ct = TokenizationBlock::ClientTokenizer {
827            tokenizer_sha256: "c".repeat(64),
828            counts_special_tokens: true,
829            counts_prompt_echo: false,
830        };
831        assert!(ct.validate().is_ok());
832        assert!(ct.require_counter(false).is_err());
833        assert!(ct.require_counter(true).is_ok());
834
835        let su = TokenizationBlock::ServerUsage {
836            counts_special_tokens: true,
837            counts_prompt_echo: false,
838        };
839        assert!(su.require_counter(true).is_err());
840        assert!(su.require_counter(false).is_ok());
841    }
842}