Skip to main content

jugar_probar/perf_gate/
receipt.rs

1//! PP-LLAMA-001 v3.0 Appendix B — the receipt emitter, and the typed reader
2//! that can refuse one.
3//!
4//! # The defect
5//!
6//! `scripts/perf_gate.sh` reads `drain_ms`, `tokenization.method`, `timeouts`
7//! and `requested`/`completed` off a receipt, and **nothing in the workspace
8//! wrote a receipt at all**. Measurement code can hold a perfect `drain_ms` in a
9//! struct field forever; the gate never sees a struct. The only artefact with
10//! `drain_ms` in it on `62d23d8d1` was the gate's own hand-typed selftest
11//! fixture (`"drain_ms":12`), so the gate was green on fiction and red on every
12//! measurement that could actually be taken.
13//!
14//! [`ReceiptInput::render`] closes that: it takes the **per-request terminal
15//! records** and derives the receipt from them. There is deliberately no way to
16//! hand it a `drain_ms`, a `timeouts` count, or a ratio — every number it emits
17//! is computed from samples that travel in the same receipt, which is the rule
18//! `scripts/lib/bench_receipt.py` already applies to ratios.
19//!
20//! # What v3 added, and why each one is a field rather than a convention
21//!
22//! - **`run_id`, `started_utc`, `clock_source` (PP-30).** Two lanes of one
23//!   invocation share a `run_id`, which is what makes PP-3's "the baseline is
24//!   from the same run" checkable rather than asserted. The id is derived —
25//!   `sha256(started_utc ‖ host ‖ client sha256 ‖ pid)[..32]` — so it is
26//!   reproducible from the receipt's own contents, which a UUID is not.
27//! - **A split `provenance` (PP-2, 18, 20, 25).** The v2.2 shape had ONE
28//!   `binary_sha256`, and the producer filled it from `std::env::current_exe()`
29//!   — the *client*, not the `apr serve` under test. PP-18 ("the subject was
30//!   built from an ancestor commit") and PP-25 ("one client binary drove both
31//!   lanes") are different claims about different binaries, and one field could
32//!   not carry both. [`Provenance::subject`] and [`Provenance::client`] are now
33//!   separate identities and the comparator has its own with a pin expiry.
34//! - **`protocol` (§5.1).** Window, warmup, quiesce, cooldown, `n_predict`,
35//!   replicate count, interleaving and the sampler pin. Two receipts written
36//!   under different protocols are not comparable, so all of it is also in the
37//!   PP-22 join key.
38//! - **`ladder` (PP-24).** Bands are derived from what both servers *admitted*,
39//!   not declared by the harness. A `c = 16` band against a subject that
40//!   admitted 11 slots measured a queue, not a server.
41//! - **A typed [`Receipt`] with `deny_unknown_fields`.** Until v3 the receipt
42//!   had a serialiser and no deserialiser, so every "strip field X" must-fire
43//!   was testable only in python inside `perf_gate.sh --selftest` — which does
44//!   not run in `workspace-test` at all (that image has no python3).
45//!
46//! # What this still refuses to emit
47//!
48//! - **§4.4.9's scheduler block.** `max_in_flight`, `admission_rejected`,
49//!   `preempted_recompute`, `preempted_swap`, `kv_blocks_*`, `gpu_layers_*`,
50//!   `backend_loaded[]`, `autofit_applied[]` are **server**-reported by
51//!   construction — PP-13 says `max_in_flight` "is reported by the **server**,
52//!   not inferred by the harness". A client-side estimate would be
53//!   indistinguishable from a real answer, which is worse than a missing field.
54//!   The block is omitted and named in `unproduced_fields`, with the reason.
55//! - **Arm D's `kv` block**, for the same reason, unless a caller supplies one
56//!   via [`KvBlock::from_server_report`].
57//! - **A ratio without a baseline.** [`super::drain::ComparatorStatus::Measured`]
58//!   is constructible only through `BandInput::join_status`, which refuses a
59//!   cross-run baseline, a join-key mismatch and a timed-out lane.
60//! - **A default `resolution`.** Every [`Provenance`] string is required and an
61//!   empty one is refused. A `--resolution` that defaults to `scripts/apr_bin.sh`
62//!   invents provenance, and invented provenance is indistinguishable from
63//!   measured provenance.
64
65use serde::{Deserialize, Serialize};
66use serde_json::{json, Map, Value};
67use sha2::{Digest, Sha256};
68use std::path::Path;
69use std::str::FromStr;
70
71use super::drain::{
72    AdmissionCap, BandContext, BandInput, BandStatus, ComparatorStatus, DerivedBand, SampleRow,
73    StreamMode, StreamWitness, SCHEMA_VERSION,
74};
75use super::join::{BandRatios, JoinKey};
76use super::protocol::ProtocolParams;
77use super::samples::SamplesFile;
78use super::witness::BatchInvarianceWitness;
79
80/// The spec string every v3 receipt carries.
81pub const SPEC_ID: &str = "PP-LLAMA-001 v3.0";
82
83/// PP-30 — the clock a plain `std` producer reads.
84pub const CLOCK_SOURCE_SYSTEM_REALTIME: &str = "std::time::SystemTime (CLOCK_REALTIME)";
85
86/// §4.4.9 fields a client cannot observe, and why. Emitted verbatim into the
87/// receipt's `unproduced_fields` rather than guessed at.
88pub const SERVER_ONLY_FIELDS: &str = "§4.4.9 scheduler block (max_in_flight, admission_rejected, \
89     preempted_recompute, preempted_swap, kv_blocks_total, kv_blocks_peak_used, \
90     kv_bytes_reserved, kv_bytes_used, gpu_layers_requested, gpu_layers_resolved, \
91     gpu_layers_total, backend_loaded[], autofit_applied[]) — every one is reported by the \
92     SERVER. PP-13: max_in_flight is reported by the server, not inferred by the harness; PP-2: \
93     gpu_layers_resolved is read from the loader and never inferred. A client-side estimate \
94     would read exactly like a measurement, so none is emitted.";
95
96/// PP-3 / PP-30 — the identifier both lanes of one harness invocation share.
97///
98/// 32 lowercase hex characters, **derived** rather than random:
99/// `sha256(started_utc ‖ host ‖ client_sha256 ‖ pid)[..32]`. §1(d) requires a
100/// receipt to be decidable from its own contents, and a UUID cannot be
101/// recomputed from the receipt it sits in — so a receipt could claim any
102/// `run_id` and nothing could check it. Every input here is already on the
103/// receipt.
104#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
105#[serde(try_from = "String", into = "String")]
106pub struct RunId(String);
107
108impl RunId {
109    /// Derive the id from the four facts that identify an invocation.
110    #[must_use]
111    pub fn derive(started_utc: &str, host: &str, client_sha256: &str, pid: u32) -> Self {
112        let mut hasher = Sha256::new();
113        hasher.update(started_utc.as_bytes());
114        hasher.update(host.as_bytes());
115        hasher.update(client_sha256.as_bytes());
116        hasher.update(pid.to_string().as_bytes());
117        let digest = format!("{:x}", hasher.finalize());
118        Self(digest[..32].to_string())
119    }
120
121    /// The 32 hex characters.
122    #[must_use]
123    pub fn as_str(&self) -> &str {
124        &self.0
125    }
126}
127
128impl TryFrom<String> for RunId {
129    type Error = String;
130
131    fn try_from(value: String) -> Result<Self, Self::Error> {
132        if value.len() == 32
133            && value
134                .bytes()
135                .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
136        {
137            Ok(Self(value))
138        } else {
139            Err(format!(
140                "run_id {value:?} is not 32 lowercase hex characters — PP-3 keys the baseline on \
141                 it, so a malformed one would make every ratio unjoinable"
142            ))
143        }
144    }
145}
146
147impl From<RunId> for String {
148    fn from(id: RunId) -> Self {
149        id.0
150    }
151}
152
153/// PP-30 — the current instant as RFC3339 UTC with milliseconds and a literal
154/// `Z`, the exact shape [`Provenance::validate`] accepts.
155///
156/// Not available on `wasm32`, where the crate has no clock dependency; a caller
157/// there supplies the timestamp it observed.
158#[cfg(not(target_arch = "wasm32"))]
159#[must_use]
160pub fn now_utc_millis() -> String {
161    chrono::Utc::now()
162        .format("%Y-%m-%dT%H:%M:%S%.3fZ")
163        .to_string()
164}
165
166/// The dispatch path a run actually took. Mirrors `bench_receipt.py`'s
167/// `COMPUTE_CLASSES`; PP-2 requires this be the path **taken**, read from the
168/// running process, never the hardware present.
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
170#[serde(rename_all = "snake_case")]
171pub enum ComputeClass {
172    /// SIMD on the host CPU.
173    Cpu,
174    /// NVIDIA CUDA.
175    Cuda,
176    /// Apple Metal.
177    Metal,
178    /// wgpu.
179    Wgpu,
180    /// The run could not determine its own path. Legal, and never a ratio.
181    Unknown,
182}
183
184impl ComputeClass {
185    /// The wire token, matching `bench_receipt.py`.
186    #[must_use]
187    pub fn wire_token(self) -> &'static str {
188        match self {
189            Self::Cpu => "cpu",
190            Self::Cuda => "cuda",
191            Self::Metal => "metal",
192            Self::Wgpu => "wgpu",
193            Self::Unknown => "unknown",
194        }
195    }
196}
197
198/// Parse the wire token back.
199///
200/// The reverse of [`ComputeClass::wire_token`] rather than a second table:
201/// `wire_token` is what `bench_receipt.py` matches against `COMPUTE_CLASSES`,
202/// so a parser with its own spelling would let a receipt be written with a
203/// class the validator then rejects — after the measurement had been taken.
204impl FromStr for ComputeClass {
205    type Err = String;
206
207    fn from_str(s: &str) -> Result<Self, Self::Err> {
208        [
209            Self::Cpu,
210            Self::Cuda,
211            Self::Metal,
212            Self::Wgpu,
213            Self::Unknown,
214        ]
215        .into_iter()
216        .find(|c| c.wire_token() == s)
217        .ok_or_else(|| {
218            format!(
219                "compute_class {s:?}: expected one of cpu, cuda, metal, wgpu, unknown (PP-2 \
220                 requires the path TAKEN, not the hardware present)"
221            )
222        })
223    }
224}
225
226/// PP-18 — the subject binary (`apr serve`) that served the band.
227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
228#[serde(deny_unknown_fields)]
229pub struct SubjectIdentity {
230    /// Absolute path to the binary that served.
231    pub path: String,
232    /// Its digest. 64 lowercase hex characters.
233    pub sha256: String,
234    /// The commit it was built from. PP-18 asserts this is an ancestor of the
235    /// commit under test.
236    pub commit: String,
237    /// Cargo features read **from the built binary**, never from `Cargo.toml`.
238    pub feature_set: Vec<String>,
239}
240
241/// PP-25 — the client binary that drove **both** lanes.
242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
243#[serde(deny_unknown_fields)]
244pub struct ClientIdentity {
245    /// Absolute path to the client.
246    pub path: String,
247    /// Its digest. 64 lowercase hex characters.
248    pub sha256: String,
249    /// The commit it was built from.
250    pub commit: String,
251    /// PP-3 / PP-30 — the client process id, the fourth input to
252    /// [`RunId::derive`].
253    ///
254    /// The id is documented as "reproducible from the receipt's own contents,
255    /// which a UUID is not" — and the pid was **not on the receipt**, so the
256    /// claim was false and no reader could check a `run_id` at all. Two
257    /// invocations that read the same millisecond on the same host with the
258    /// same client differ only here; without it they would share a `run_id` and
259    /// PP-3's same-run rule would join two runs.
260    pub pid: u32,
261}
262
263/// PP-20 — the pinned comparator build, and when the pin goes stale.
264#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
265#[serde(deny_unknown_fields)]
266pub struct ComparatorIdentity {
267    /// The upstream commit the comparator was built from.
268    pub commit: String,
269    /// The `cmake` line it was configured with.
270    pub cmake: String,
271    /// The built binary's digest. 64 lowercase hex characters.
272    pub sha256: String,
273    /// RFC3339 UTC instant after which every ratio against this pin is
274    /// `COMPARATOR_STALE`.
275    pub pin_expiry: String,
276    /// `GET /props` for this band, stored verbatim (§5.3).
277    pub props: Value,
278}
279
280/// PP-23's input — the model file the band served.
281#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
282#[serde(deny_unknown_fields)]
283pub struct ModelIdentity {
284    /// Absolute path to the weights.
285    pub path: String,
286    /// Its digest. 64 lowercase hex characters.
287    pub sha256: String,
288    /// `stat -c %s` of the file — the roofline's numerator input.
289    pub bytes: u64,
290}
291
292/// §4.2.2 identity plus the §4.2.3 join key. **No `Default` impl**: every field
293/// is a fact about a specific run, and a blank one that serialises is how a
294/// receipt acquires provenance it never had.
295///
296/// `Eq` is deliberately absent: `server_config` and `comparator.props` are
297/// verbatim JSON, which contains floats.
298#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
299#[serde(deny_unknown_fields)]
300pub struct Provenance {
301    /// The binary that ran, as an absolute path. Kept from v2.2 so today's
302    /// readers keep working; it is the **client** path, and
303    /// [`Self::client`] says so in a field that cannot be misread.
304    pub binary_path: String,
305    /// Host-local anti-substitution fingerprint of that same binary.
306    /// 64 lowercase hex characters.
307    pub binary_sha256: String,
308    /// How that path was resolved. **No default** — see the module docs.
309    pub resolution: String,
310    /// The dispatch path taken (PP-2).
311    pub compute_class: ComputeClass,
312    /// Join key: which host.
313    pub host: String,
314    /// Join key: which accelerator.
315    pub accelerator: String,
316    /// Join key: which model.
317    pub model: String,
318    /// Join key: which quantization.
319    pub quantization: String,
320    /// Cargo features read **from the built binary**, never from `Cargo.toml`.
321    pub feature_set: Vec<String>,
322    /// PP-30 — RFC3339 UTC with milliseconds and a literal `Z`.
323    pub started_utc: String,
324    /// PP-30 — which clock that instant came from.
325    pub clock_source: String,
326    /// PP-18 — the `apr serve` under test.
327    pub subject: SubjectIdentity,
328    /// PP-25 — the one client that drove both lanes.
329    pub client: ClientIdentity,
330    /// PP-20 — the comparator pin, when there was a comparator lane.
331    pub comparator: Option<ComparatorIdentity>,
332    /// PP-2 — `GET /v1/effective-config`, stored verbatim before the first
333    /// request. Verbatim because a harness that re-shapes the server's answer
334    /// is a harness that can lose the field the answer was needed for.
335    pub server_config: Option<Value>,
336    /// PP-23's input — the weights file.
337    pub model_file: Option<ModelIdentity>,
338}
339
340impl Provenance {
341    /// §4.2 checks that a receipt cannot be written without passing.
342    ///
343    /// # Errors
344    /// When any required field is empty, when any digest is not 64 lowercase
345    /// hex characters, when `started_utc` is not RFC3339 UTC (PP-30), or when
346    /// the declared `compute_class` is a path the build cannot reach (PP-2).
347    pub fn validate(&self) -> Result<(), String> {
348        for (name, value) in self.required_strings() {
349            if value.trim().is_empty() {
350                return Err(format!(
351                    "provenance.{name}: empty — this field has no default; a receipt that does \
352                     not say {name} is an anonymous number, not evidence"
353                ));
354            }
355        }
356        for (name, digest) in self.digests() {
357            if !is_sha256(digest) {
358                return Err(format!(
359                    "provenance.{name}: {digest:?} is not 64 lowercase hex characters"
360                ));
361            }
362        }
363        validate_rfc3339_utc_millis("provenance.started_utc", &self.started_utc)?;
364        if let Some(c) = &self.comparator {
365            validate_rfc3339_utc_millis("provenance.comparator.pin_expiry", &c.pin_expiry)?;
366        }
367        self.validate_feature_set()
368    }
369
370    /// PP-20 — did the comparator pin expire before this run started?
371    ///
372    /// Both instants are canonical RFC3339 UTC with the same field widths, so
373    /// lexicographic order **is** chronological order; [`Self::validate`]
374    /// refuses anything else, which is what makes the string comparison sound.
375    #[must_use]
376    pub fn comparator_is_stale(&self) -> bool {
377        self.comparator
378            .as_ref()
379            .is_some_and(|c| c.pin_expiry < self.started_utc)
380    }
381
382    fn required_strings(&self) -> Vec<(&'static str, &str)> {
383        let mut out = vec![
384            ("binary_path", self.binary_path.as_str()),
385            ("binary_sha256", self.binary_sha256.as_str()),
386            ("resolution", self.resolution.as_str()),
387            ("host", self.host.as_str()),
388            ("accelerator", self.accelerator.as_str()),
389            ("model", self.model.as_str()),
390            ("quantization", self.quantization.as_str()),
391            ("started_utc", self.started_utc.as_str()),
392            ("clock_source", self.clock_source.as_str()),
393            ("subject.path", self.subject.path.as_str()),
394            ("subject.commit", self.subject.commit.as_str()),
395            ("client.path", self.client.path.as_str()),
396            ("client.commit", self.client.commit.as_str()),
397        ];
398        if let Some(c) = &self.comparator {
399            out.push(("comparator.commit", c.commit.as_str()));
400            out.push(("comparator.cmake", c.cmake.as_str()));
401            out.push(("comparator.pin_expiry", c.pin_expiry.as_str()));
402        }
403        if let Some(m) = &self.model_file {
404            out.push(("model_file.path", m.path.as_str()));
405        }
406        out
407    }
408
409    fn digests(&self) -> Vec<(&'static str, &str)> {
410        let mut out = vec![
411            ("binary_sha256", self.binary_sha256.as_str()),
412            ("subject.sha256", self.subject.sha256.as_str()),
413            ("client.sha256", self.client.sha256.as_str()),
414        ];
415        if let Some(c) = &self.comparator {
416            out.push(("comparator.sha256", c.sha256.as_str()));
417        }
418        if let Some(m) = &self.model_file {
419            out.push(("model_file.sha256", m.sha256.as_str()));
420        }
421        out
422    }
423
424    /// PP-2's other half: a class the build cannot reach is a fabricated claim.
425    /// Checked against the **subject's** feature set, since the subject is the
426    /// process that took the path.
427    fn validate_feature_set(&self) -> Result<(), String> {
428        let needs_feature = matches!(self.compute_class, ComputeClass::Cuda | ComputeClass::Wgpu);
429        let token = self.compute_class.wire_token();
430        if needs_feature && !self.subject.feature_set.iter().any(|f| f == token) {
431            return Err(format!(
432                "provenance.compute_class={token} but subject.feature_set={:?} does not contain \
433                 it — a build without the feature cannot take that path (PP-2)",
434                self.subject.feature_set
435            ));
436        }
437        Ok(())
438    }
439}
440
441/// PP-30 — RFC3339 UTC with exactly three fractional digits and a literal `Z`.
442///
443/// Hand-checked rather than parsed with a calendar library: the receipt needs a
444/// *canonical* spelling, not merely a parseable instant, because PP-20 compares
445/// `pin_expiry` with `started_utc` as strings. `2026-09-02T10:11:12+00:00` is a
446/// valid RFC3339 timestamp and would sort wrongly against this shape, so it is
447/// refused rather than normalised.
448fn validate_rfc3339_utc_millis(field: &str, value: &str) -> Result<(), String> {
449    const SHAPE: &str = "YYYY-MM-DDTHH:MM:SS.mmmZ";
450    let bytes = value.as_bytes();
451    let ok = bytes.len() == 24
452        && bytes.iter().enumerate().all(|(i, b)| match i {
453            4 | 7 => *b == b'-',
454            10 => *b == b'T',
455            13 | 16 => *b == b':',
456            19 => *b == b'.',
457            23 => *b == b'Z',
458            _ => b.is_ascii_digit(),
459        });
460    if !ok {
461        return Err(format!(
462            "{field}: {value:?} is not {SHAPE} — PP-30 needs a canonical UTC instant, because \
463             PP-20 compares a pin expiry against it as a string and any other spelling sorts \
464             wrongly"
465        ));
466    }
467    Ok(())
468}
469
470/// §4.4.6 — how tokens were counted.
471#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
472#[serde(rename_all = "snake_case")]
473pub enum TokenCountingMethod {
474    /// Counts taken from the server's own `usage` fields — two servers'
475    /// `usage` fields are two implementations' opinions.
476    ServerUsage,
477    /// Counts computed client-side with the model's own tokenizer. Canonical.
478    ClientTokenizer,
479}
480
481impl TokenCountingMethod {
482    /// The wire token `perf_gate.sh` reads from `tokenization.method`.
483    #[must_use]
484    pub fn wire_token(self) -> &'static str {
485        match self {
486            Self::ServerUsage => "server_usage",
487            Self::ClientTokenizer => "client_tokenizer",
488        }
489    }
490}
491
492/// §4.4.6 — the `tokenization` block, required in every receipt.
493///
494/// `method` has **no default** (PP-11). The variants below make
495/// "`client_tokenizer` with no digest" unrepresentable rather than merely
496/// rejected, and `counts_special_tokens` / `counts_prompt_echo` are plain
497/// non-optional booleans the caller must state.
498///
499/// > `counts_*` under `server_usage` is an operator **declaration** about the
500/// > server's counting semantics, not a client measurement — §4.4.6 is titled
501/// > "Token counting must be **declared**". It is required rather than
502/// > defaulted precisely so it cannot be silently wrong.
503#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
504#[serde(tag = "method", rename_all = "snake_case", deny_unknown_fields)]
505pub enum TokenizationBlock {
506    /// Server-reported counts.
507    ServerUsage {
508        /// Whether the count includes special tokens.
509        counts_special_tokens: bool,
510        /// Whether the count includes the echoed prompt.
511        counts_prompt_echo: bool,
512    },
513    /// Client-side counts with the model's own tokenizer.
514    ClientTokenizer {
515        /// Digest of the tokenizer actually used. 64 lowercase hex characters.
516        tokenizer_sha256: String,
517        /// Whether the count includes special tokens.
518        counts_special_tokens: bool,
519        /// Whether the count includes the echoed prompt.
520        counts_prompt_echo: bool,
521    },
522}
523
524impl TokenizationBlock {
525    /// The declared counting method.
526    #[must_use]
527    pub fn method(&self) -> TokenCountingMethod {
528        match self {
529            Self::ServerUsage { .. } => TokenCountingMethod::ServerUsage,
530            Self::ClientTokenizer { .. } => TokenCountingMethod::ClientTokenizer,
531        }
532    }
533
534    /// # Errors
535    /// When a `client_tokenizer` digest is not 64 lowercase hex characters.
536    pub fn validate(&self) -> Result<(), String> {
537        match self {
538            Self::ServerUsage { .. } => Ok(()),
539            Self::ClientTokenizer {
540                tokenizer_sha256, ..
541            } if is_sha256(tokenizer_sha256) => Ok(()),
542            Self::ClientTokenizer {
543                tokenizer_sha256, ..
544            } => Err(format!(
545                "tokenization.tokenizer_sha256: {tokenizer_sha256:?} is not 64 lowercase hex \
546                 characters — §4.4.6 requires it when method = client_tokenizer"
547            )),
548        }
549    }
550
551    /// Poka-yoke for transports: a declared method the transport cannot honour
552    /// is refused at construction, not silently downgraded at measure time.
553    ///
554    /// # Errors
555    /// When the declared method and the available counting machinery disagree.
556    pub fn require_counter(&self, has_client_counter: bool) -> Result<(), String> {
557        match (self.method(), has_client_counter) {
558            (TokenCountingMethod::ClientTokenizer, false) => Err(
559                "tokenization.method = client_tokenizer but no client TokenCounter was supplied"
560                    .to_string(),
561            ),
562            (TokenCountingMethod::ServerUsage, true) => Err(
563                "tokenization.method = server_usage but a client TokenCounter was supplied; \
564                 declare client_tokenizer or drop the counter"
565                    .to_string(),
566            ),
567            _ => Ok(()),
568        }
569    }
570}
571
572/// Arm D's memory block. **Server-reported**: the only constructor says so in
573/// its name, so a client-side guess has nowhere to enter.
574///
575/// The two byte figures are required. The two counters are `Option`, because
576/// `apr serve` reports `admission_rejected` and `preempted_swap` as `null` —
577/// it has no KV-admission refusal path and no swap path, so there is no
578/// quantity for either to denote. Three of the four numbers were therefore
579/// being thrown away with the block: `kv` could never be produced at all.
580///
581/// `null` is the honest spelling and **not** `0`: "this server does not count
582/// them" and "this server counted none" are different facts, and Arm D reads
583/// `admission_rejected > 0` as evidence. `perf_gate.sh:752` already names a
584/// null counter in its `missing` list, so a partial block is reported as
585/// partial rather than read as a zeroed one.
586#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
587#[serde(deny_unknown_fields)]
588pub struct KvBlock {
589    bytes_used: u64,
590    bytes_reserved: u64,
591    admission_rejected: Option<u64>,
592    preempted_swap: Option<u64>,
593}
594
595impl KvBlock {
596    /// Build the block from figures the **server** reported. A counter the
597    /// server did not report is `None`, never `0`.
598    #[must_use]
599    pub fn from_server_report(
600        bytes_used: u64,
601        bytes_reserved: u64,
602        admission_rejected: Option<u64>,
603        preempted_swap: Option<u64>,
604    ) -> Self {
605        Self {
606            bytes_used,
607            bytes_reserved,
608            admission_rejected,
609            preempted_swap,
610        }
611    }
612
613    /// The counter names this server did not report, for `unproduced_fields`.
614    ///
615    /// Empty when the block is complete — which is the must-not-fire side: a
616    /// complete block must name nothing.
617    #[must_use]
618    pub fn uncounted_fields(&self) -> Vec<&'static str> {
619        let mut out = Vec::new();
620        if self.admission_rejected.is_none() {
621            out.push("kv.admission_rejected");
622        }
623        if self.preempted_swap.is_none() {
624            out.push("kv.preempted_swap");
625        }
626        out
627    }
628}
629
630/// PP-24 — what each lane's server said it would admit.
631#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
632#[serde(deny_unknown_fields)]
633pub struct SlotsAdmitted {
634    /// The subject's reported slot count. `None` when it reported none.
635    pub apr: Option<u32>,
636    /// The comparator's reported slot count. `None` when there was no lane.
637    pub llama: Option<u32>,
638}
639
640/// PP-24 — the band ladder: declared, and what both servers actually admitted.
641///
642/// A `c = 16` band against a subject that admitted 11 slots measured a queue,
643/// not a server. So the bands that may carry numbers are **derived** from the
644/// admissions rather than declared by the harness.
645#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
646#[serde(deny_unknown_fields)]
647pub struct Ladder {
648    /// The bands the matrix declares.
649    pub declared: Vec<u32>,
650    /// `{c ∈ declared : c ≤ min(slots_admitted)}`.
651    pub derived: Vec<u32>,
652    /// What each lane reported.
653    pub slots_admitted: SlotsAdmitted,
654}
655
656impl Ladder {
657    /// Derive the ladder from the declared bands and the two servers' reports.
658    ///
659    /// When neither lane reported a slot count the derived ladder is the
660    /// declared one — and the caller names the absence in `unproduced_fields`,
661    /// which [`ReceiptInput::render`] does. Silently narrowing the ladder on no
662    /// evidence would drop bands that ran perfectly well.
663    #[must_use]
664    pub fn derive(declared: &[u32], slots_admitted: SlotsAdmitted) -> Self {
665        let cap = match (slots_admitted.apr, slots_admitted.llama) {
666            (Some(a), Some(l)) => Some(a.min(l)),
667            (Some(a), None) => Some(a),
668            (None, Some(l)) => Some(l),
669            (None, None) => None,
670        };
671        let derived = declared
672            .iter()
673            .copied()
674            .filter(|c| cap.admits(*c))
675            .collect();
676        Self {
677            declared: declared.to_vec(),
678            derived,
679            slots_admitted,
680        }
681    }
682
683    /// True when neither lane reported a slot count, so the ladder is the
684    /// declared one on no evidence.
685    #[must_use]
686    pub fn is_underived(&self) -> bool {
687        self.slots_admitted.apr.is_none() && self.slots_admitted.llama.is_none()
688    }
689}
690
691/// `cap.is_none() || cap >= c`, as a named predicate so the ladder rule reads
692/// the way PP-24 states it.
693trait CapExt {
694    fn admits(self, c: u32) -> bool;
695}
696
697impl CapExt for Option<u32> {
698    fn admits(self, c: u32) -> bool {
699        match self {
700            None => true,
701            Some(cap) => cap >= c,
702        }
703    }
704}
705
706/// PP-23 — the memory-bandwidth ceiling on **per-sequence** decode.
707///
708/// `bandwidth_bytes_per_sec / model_bytes` tokens per second: decoding one
709/// token reads the whole model once. Compared to `dec(1)` and to nothing else —
710/// an aggregate over `c` sequences amortises the read across them and is
711/// legitimately above the single-sequence ceiling (gx10's c=8 aggregate did exactly
712/// that and was correct; the figures live in
713/// evidence/perf-gate-001-w1-gx10/receipt.r1.json and are not restated here, PP-12).
714#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
715#[serde(deny_unknown_fields)]
716pub struct Roofline {
717    /// Measured `[V]` memory bandwidth, bytes per second.
718    pub bandwidth_bytes_per_sec: f64,
719    /// `stat -c %s` of the weights.
720    pub model_bytes: u64,
721}
722
723impl Roofline {
724    /// The ceiling, in tokens per second. `None` when the model has no size.
725    #[must_use]
726    pub fn tok_per_sec(self) -> Option<f64> {
727        if self.model_bytes == 0 || self.bandwidth_bytes_per_sec <= 0.0 {
728            return None;
729        }
730        Some(self.bandwidth_bytes_per_sec / self.model_bytes as f64)
731    }
732}
733
734/// §5.1 — which workload the band set ran.
735#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
736pub enum Workload {
737    /// Homogeneous, `prompt_tokens = 512 ± 8`, `n_predict = 128`, ignore-EOS.
738    W1,
739    /// Ragged prompt and generation mixture, with an injector at `window/2`.
740    W2,
741}
742
743impl Workload {
744    /// The wire token.
745    #[must_use]
746    pub fn wire_token(self) -> &'static str {
747        match self {
748            Self::W1 => "W1",
749            Self::W2 => "W2",
750        }
751    }
752}
753
754/// Parse the wire token back. As [`ComputeClass`]'s, derived from
755/// `wire_token` so the two cannot drift.
756impl FromStr for Workload {
757    type Err = String;
758
759    fn from_str(s: &str) -> Result<Self, Self::Err> {
760        [Self::W1, Self::W2]
761            .into_iter()
762            .find(|w| w.wire_token() == s)
763            .ok_or_else(|| format!("workload {s:?}: expected W1 or W2 (§5.1)"))
764    }
765}
766
767/// SHA-256 of a file, as the 64 lowercase hex characters
768/// [`Provenance::validate`] and `bench_receipt.py` both demand.
769///
770/// # Errors
771/// When `path` cannot be opened or read.
772pub fn sha256_file(path: &Path) -> std::io::Result<String> {
773    let mut file = std::fs::File::open(path)?;
774    let mut hasher = Sha256::new();
775    std::io::copy(&mut file, &mut hasher)?;
776    Ok(format!("{:x}", hasher.finalize()))
777}
778
779/// Everything needed to render one host × workload receipt.
780#[derive(Debug, Clone, PartialEq)]
781pub struct ReceiptInput {
782    /// PP-4 — the wire schema version. `3` for PP-LLAMA-001 v3.0.
783    pub schema_version: u32,
784    /// PP-3 — the id both lanes of this invocation share.
785    pub run_id: RunId,
786    /// §4.2.2 identity and §4.2.3 join key.
787    pub provenance: Provenance,
788    /// §4.4.6 counting declaration.
789    pub tokenization: TokenizationBlock,
790    /// §5.1 workload.
791    pub workload: Workload,
792    /// §5.1 protocol parameters, from `perf-matrix.yaml`.
793    pub protocol: ProtocolParams,
794    /// The commit under test (PP-21).
795    pub commit: String,
796    /// PP-24 — the declared and derived band ladder.
797    pub ladder: Ladder,
798    /// One entry per band, each carrying its own per-request records.
799    pub bands: Vec<BandInput>,
800    /// Arm D's server-reported memory block, when the server reported one.
801    pub kv: Option<KvBlock>,
802    /// PP-23 — the memory-bandwidth ceiling, when a `[V]` bandwidth exists.
803    pub roofline: Option<Roofline>,
804}
805
806impl ReceiptInput {
807    /// A receipt at the current schema version with no `kv` block and no
808    /// roofline, which is the shape a first conformant run has.
809    #[must_use]
810    pub fn new(
811        run_id: RunId,
812        provenance: Provenance,
813        tokenization: TokenizationBlock,
814        workload: Workload,
815        protocol: ProtocolParams,
816        commit: impl Into<String>,
817        ladder: Ladder,
818        bands: Vec<BandInput>,
819    ) -> Self {
820        Self {
821            schema_version: SCHEMA_VERSION,
822            run_id,
823            provenance,
824            tokenization,
825            workload,
826            protocol,
827            commit: commit.into(),
828            ladder,
829            bands,
830            kv: None,
831            roofline: None,
832        }
833    }
834
835    /// The band-derivation context this receipt implies.
836    #[must_use]
837    pub fn band_context(&self) -> BandContext {
838        BandContext {
839            schema_version: self.schema_version,
840            replicates: self.protocol.replicates,
841            interleaved: self.protocol.interleaved,
842            comparator_stale: self.provenance.comparator_is_stale(),
843            ..BandContext::default()
844        }
845    }
846
847    /// PP-22 — the join key for one of this receipt's bands.
848    #[must_use]
849    pub fn join_key(&self, band: &BandInput) -> JoinKey {
850        JoinKey::of(self, band)
851    }
852
853    /// Derive and render the receipt.
854    ///
855    /// # Errors
856    /// When provenance or the tokenization block is invalid, when there are no
857    /// bands, when any band contradicts its own clock (see
858    /// [`BandInput::derive`]), when a band sits above the derived ladder
859    /// without an admission cap or a decision (PP-24), when `dec(1)` exceeds the
860    /// roofline (PP-23), or when the retained samples are a constant —
861    /// `bench_receipt.py` calls that the fabricated-measurement shape (F12).
862    pub fn render(&self) -> Result<Value, String> {
863        self.provenance.validate()?;
864        self.tokenization.validate()?;
865        self.check_ladder_is_derived()?;
866        if self.bands.is_empty() {
867            return Err(
868                "receipt has no bands — a measurement over zero bands is a vacuous pass"
869                    .to_string(),
870            );
871        }
872        let ctx = self.band_context();
873        let stale = ctx.comparator_stale;
874        let mut bands = Vec::with_capacity(self.bands.len());
875        for input in &self.bands {
876            self.check_ladder(input)?;
877            let mut derived = input.derive_in(&ctx)?.with_join_key(self.join_key(input));
878            if stale {
879                let expiry = self
880                    .provenance
881                    .comparator
882                    .as_ref()
883                    .map_or("", |c| c.pin_expiry.as_str());
884                derived = derived.marked_comparator_stale(expiry, &self.provenance.started_utc);
885            }
886            bands.push(derived);
887        }
888        self.check_roofline(&bands)?;
889        let samples = samples_ms(&bands);
890        validate_samples(&samples)?;
891        Ok(self.assemble(&bands, samples))
892    }
893
894    /// [`Self::render`], as pretty-printed JSON.
895    ///
896    /// # Errors
897    /// As [`Self::render`]; serialisation itself cannot fail for this shape.
898    pub fn render_string(&self) -> Result<String, String> {
899        let value = self.render()?;
900        serde_json::to_string_pretty(&value).map_err(|e| format!("serialising receipt: {e}"))
901    }
902
903    /// PP-24 — `ladder.derived` must be the one `declared` and `slots_admitted`
904    /// produce.
905    ///
906    /// [`Ladder`] has public fields and travels through the producer as data,
907    /// so `derived` was whatever the caller put there. Nothing recomputed it —
908    /// which means a hand-written `derived: [1, 4, 8, 16]` beside
909    /// `slots_admitted: {apr: 4}` excused every band from [`Self::check_ladder`]
910    /// and put four bands on the wire that measured a queue. The ladder is a
911    /// FUNCTION of two recorded inputs, and here it is applied.
912    fn check_ladder_is_derived(&self) -> Result<(), String> {
913        let recomputed = Ladder::derive(&self.ladder.declared, self.ladder.slots_admitted);
914        if recomputed.derived == self.ladder.derived {
915            return Ok(());
916        }
917        Err(format!(
918            "PP-24: ladder.derived is {:?} but declared {:?} with slots_admitted apr={:?} \
919             llama={:?} derives {:?} — `derived` is `{{c ∈ declared : c ≤ min(slots_admitted)}}`, \
920             not a field a producer may state. A supplied ladder that disagrees with its own \
921             inputs excuses exactly the bands PP-24 exists to exclude.",
922            self.ladder.derived,
923            self.ladder.declared,
924            self.ladder.slots_admitted.apr,
925            self.ladder.slots_admitted.llama,
926            recomputed.derived
927        ))
928    }
929
930    /// PP-24 — a band above the derived ladder must say who decided it could
931    /// run, or which lane capped it.
932    fn check_ladder(&self, band: &BandInput) -> Result<(), String> {
933        if self.ladder.derived.contains(&band.concurrency) {
934            return Ok(());
935        }
936        match &band.comparator {
937            ComparatorStatus::NotApplicable { .. } => Ok(()),
938            ComparatorStatus::Unmeasured {
939                admission_capped: Some(_),
940                ..
941            } => Ok(()),
942            _ => Err(format!(
943                "PP-24: band c={} is not in the derived ladder {:?} (slots_admitted apr={:?} \
944                 llama={:?}) and carries neither an admission cap nor a decision — a band above \
945                 what the servers admitted measured a queue, not a server",
946                band.concurrency,
947                self.ladder.derived,
948                self.ladder.slots_admitted.apr,
949                self.ladder.slots_admitted.llama
950            )),
951        }
952    }
953
954    /// PP-23 — `dec(1)` above the ceiling is schema-fatal. The **aggregate** is
955    /// never compared: over `c` sequences one weight read serves `c` tokens, so
956    /// an aggregate above the single-sequence ceiling is expected, not a defect.
957    fn check_roofline(&self, bands: &[DerivedBand]) -> Result<(), String> {
958        let Some(ceiling) = self.roofline.and_then(Roofline::tok_per_sec) else {
959            return Ok(());
960        };
961        for b in bands.iter().filter(|b| b.concurrency == 1) {
962            if let Some(dec) = b.decode_tok_per_sec {
963                if dec > ceiling {
964                    return Err(format!(
965                        "PP-23: decode_tok_per_sec={dec:.1} at c=1 exceeds the memory-bandwidth \
966                         ceiling {ceiling:.1} tok/s — decoding a token reads the whole model \
967                         once, so this is not a fast run, it is a wrong measurement"
968                    ));
969                }
970            }
971        }
972        Ok(())
973    }
974
975    fn assemble(&self, bands: &[DerivedBand], samples: Vec<f64>) -> Value {
976        let mut map = Map::new();
977        map.insert("spec".into(), json!(SPEC_ID));
978        map.insert("schema_version".into(), json!(self.schema_version));
979        map.insert("run_id".into(), json!(self.run_id.as_str()));
980        map.insert("commit".into(), json!(self.commit));
981        map.insert("workload".into(), json!(self.workload.wire_token()));
982        map.insert("protocol".into(), to_value(&self.protocol));
983        map.insert("client_model".into(), json!("closed_loop"));
984        map.insert("provenance".into(), to_value(&self.provenance));
985        map.insert("tokenization".into(), to_value(&self.tokenization));
986        insert_counts(&mut map, bands);
987        map.insert(
988            "short_of_n_predict".into(),
989            json!(sum(bands, |b| b.short_of_n_predict)),
990        );
991        map.insert("drain_ms".into(), json!(receipt_drain_ms(bands)));
992        map.insert("n".into(), json!(samples.len()));
993        map.insert("samples_ms".into(), json!(samples));
994        map.insert("ladder".into(), to_value(&self.ladder));
995        let roofline = self.roofline.and_then(Roofline::tok_per_sec);
996        let render_ctx = RenderContexts {
997            subject: RenderContext {
998                agg1: band_metric(bands, 1, |b| b.aggregate_tok_per_sec),
999                dec1: band_metric(bands, 1, |b| b.decode_tok_per_sec),
1000                roofline,
1001            },
1002            // PP-3: the comparator lane's own c=1 band is the baseline of THIS
1003            // receipt's c=1 band. Its `agg(1)`/`dec(1)` are the only correct
1004            // denominators for the baselines' scaling_efficiency and
1005            // overhead_share.
1006            comparator: RenderContext {
1007                agg1: baseline_metric(bands, 1, |b| b.aggregate_tok_per_sec),
1008                dec1: baseline_metric(bands, 1, |b| b.decode_tok_per_sec),
1009                roofline,
1010            },
1011        };
1012        map.insert(
1013            "bands".into(),
1014            Value::Array(
1015                bands
1016                    .iter()
1017                    .map(|b| band_json(b, &render_ctx.subject, Some(&render_ctx)))
1018                    .collect(),
1019            ),
1020        );
1021        if let Some(kv) = self.kv {
1022            map.insert("kv".into(), to_value(&kv));
1023        }
1024        map.insert("unproduced_fields".into(), json!(self.unproduced(bands)));
1025        Value::Object(map)
1026    }
1027
1028    fn unproduced(&self, bands: &[DerivedBand]) -> Vec<String> {
1029        let mut out = vec![SERVER_ONLY_FIELDS.to_string()];
1030        match &self.kv {
1031            None => out.push(
1032                "Arm D `kv` block (bytes_used, bytes_reserved, admission_rejected, \
1033                 preempted_swap) — server-reported. Absent here, so this receipt is legal at \
1034                 merge phase and correctly FAILS at release phase rather than carrying invented \
1035                 memory figures."
1036                    .to_string(),
1037            ),
1038            Some(kv) => {
1039                let uncounted = kv.uncounted_fields();
1040                if !uncounted.is_empty() {
1041                    out.push(format!(
1042                        "Arm D {uncounted:?} — the server reported the KV byte figures but not \
1043                         these counters: the mechanism they would count does not exist on this \
1044                         build. They are null rather than 0, because \"not counted\" and \
1045                         \"counted none\" are different facts and Arm D reads one of them as \
1046                         evidence."
1047                    ));
1048                }
1049            }
1050        }
1051        if self.roofline.is_none() {
1052            out.push(
1053                "PP-23 roofline_tok_per_sec — no `[V]` memory bandwidth is committed for this \
1054                 host, so the ceiling is null on every band. A vendor GB/s figure is not a \
1055                 measurement (PP-12)."
1056                    .to_string(),
1057            );
1058        }
1059        if self.ladder.is_underived() {
1060            out.push(format!(
1061                "PP-24 ladder.slots_admitted — neither lane reported a slot count, so \
1062                 ladder.derived is the declared set {:?} on no evidence. The band ceiling is \
1063                 server-reported (PP-13) and this run did not observe one.",
1064                self.ladder.declared
1065            ));
1066        }
1067        if self.provenance.server_config.is_none() {
1068            out.push(
1069                "PP-2 provenance.server_config — `GET /v1/effective-config` was not stored, so \
1070                 resolved max_batch, GpuProfile, scheduler identity and the memory fields are \
1071                 absent. Every one of them is server-reported; none is inferred here."
1072                    .to_string(),
1073            );
1074        }
1075        out.extend(bands.iter().flat_map(|b| b.unproduced.clone()));
1076        out
1077    }
1078}
1079
1080/// Receipt-level figures the per-band renderer needs, **for one lane**.
1081///
1082/// `scaling_efficiency` is `agg(c) / (c · agg(1))` and `overhead_share` is
1083/// `agg(1) / dec(1)`: both divide a band by its OWN lane's `c = 1` figures.
1084/// Rendering the baseline with the subject's context computed
1085/// `llama_agg(c) / (c · apr_agg(1))` — a number that is not a scaling
1086/// efficiency of anything, and that moves when the subject gets faster. So the
1087/// comparator lane gets its own context, built from the baselines.
1088struct RenderContext {
1089    agg1: Option<f64>,
1090    dec1: Option<f64>,
1091    roofline: Option<f64>,
1092}
1093
1094/// Both lanes' contexts, so `band_json` renders each band against its own.
1095struct RenderContexts {
1096    subject: RenderContext,
1097    comparator: RenderContext,
1098}
1099
1100fn band_metric(
1101    bands: &[DerivedBand],
1102    concurrency: u32,
1103    f: impl Fn(&DerivedBand) -> Option<f64>,
1104) -> Option<f64> {
1105    bands
1106        .iter()
1107        .find(|b| b.concurrency == concurrency)
1108        .and_then(f)
1109}
1110
1111/// The same figure, taken from the COMPARATOR lane: the baseline attached to
1112/// the band at `concurrency`. `None` when that band did not join.
1113fn baseline_metric(
1114    bands: &[DerivedBand],
1115    concurrency: u32,
1116    f: impl Fn(&DerivedBand) -> Option<f64>,
1117) -> Option<f64> {
1118    match &bands
1119        .iter()
1120        .find(|b| b.concurrency == concurrency)?
1121        .comparator
1122    {
1123        ComparatorStatus::Measured(join) => f(join.baseline()),
1124        ComparatorStatus::NotApplicable { .. } | ComparatorStatus::Unmeasured { .. } => None,
1125    }
1126}
1127
1128/// PP-10 at receipt level: the **maximum** band drain, not a mean or a sum.
1129///
1130/// The `SUSPECT` rule is per-band and asks whether *one request dominated a
1131/// window*. Averaging four bands hides the one that did; summing invents a
1132/// drain phase no band ran. The worst band is the one a reader must see.
1133fn receipt_drain_ms(bands: &[DerivedBand]) -> f64 {
1134    bands.iter().map(|b| b.drain_ms).fold(0.0_f64, f64::max)
1135}
1136
1137fn insert_counts(map: &mut Map<String, Value>, bands: &[DerivedBand]) {
1138    map.insert("requested".into(), json!(sum(bands, |b| b.requested)));
1139    map.insert("completed".into(), json!(sum(bands, |b| b.completed)));
1140    map.insert("timeouts".into(), json!(sum(bands, |b| b.timeouts)));
1141    map.insert("truncated".into(), json!(sum(bands, |b| b.truncated)));
1142    map.insert("errors".into(), json!(sum(bands, |b| b.errors)));
1143}
1144
1145fn sum(bands: &[DerivedBand], f: impl Fn(&DerivedBand) -> usize) -> usize {
1146    bands.iter().map(f).sum()
1147}
1148
1149fn samples_ms(bands: &[DerivedBand]) -> Vec<f64> {
1150    bands.iter().flat_map(|b| b.latencies_ms.clone()).collect()
1151}
1152
1153/// PP-7 and F12, applied by the producer rather than discovered by the validator.
1154fn validate_samples(samples: &[f64]) -> Result<(), String> {
1155    if samples.is_empty() {
1156        return Err(
1157            "samples_ms would be empty — no band completed a single request, and a \
1158                    receipt with no retained samples permanently forecloses the bootstrap (PP-7)"
1159                .to_string(),
1160        );
1161    }
1162    let first = samples[0];
1163    if samples.len() > 1 && samples.iter().all(|s| (s - first).abs() < f64::EPSILON) {
1164        return Err(format!(
1165            "samples_ms: all {} samples identical ({first}) — a real timing distribution is not \
1166             constant; this is the fabricated-measurement shape (F12)",
1167            samples.len()
1168        ));
1169    }
1170    Ok(())
1171}
1172
1173/// Serialise a value that cannot fail to serialise (no maps with non-string
1174/// keys, no non-finite floats reachable from a validated receipt).
1175fn to_value<T: Serialize>(value: &T) -> Value {
1176    serde_json::to_value(value).unwrap_or(Value::Null)
1177}
1178
1179/// One band as JSON, against `ctx` — **its own lane's** receipt-level figures.
1180///
1181/// `comparator` is `Some` for a subject band (and carries both lanes' contexts,
1182/// so the baseline can be rendered against the comparator's) and `None` for a
1183/// baseline: a baseline that carried a baseline of its own would be a chain,
1184/// which `ReceiptBand::validate` refuses.
1185fn band_json(b: &DerivedBand, ctx: &RenderContext, comparator: Option<&RenderContexts>) -> Value {
1186    let mut map = Map::new();
1187    map.insert("concurrency".into(), json!(b.concurrency));
1188    map.insert("replicate".into(), json!(b.replicate));
1189    map.insert("status".into(), json!(b.status.wire_token()));
1190    if let Some(agg) = b.aggregate_tok_per_sec {
1191        map.insert("aggregate_tok_per_sec".into(), json!(agg));
1192    }
1193    map.insert("tokens_total".into(), json!(b.tokens_total));
1194    map.insert("span_ms".into(), json!(b.span_ms));
1195    map.insert("window_ms".into(), json!(b.window_ms));
1196    map.insert("drain_ms".into(), json!(b.drain_ms));
1197    map.insert("requested".into(), json!(b.requested));
1198    map.insert("completed".into(), json!(b.completed));
1199    map.insert("timeouts".into(), json!(b.timeouts));
1200    map.insert("truncated".into(), json!(b.truncated));
1201    map.insert("errors".into(), json!(b.errors));
1202    map.insert("short_of_n_predict".into(), json!(b.short_of_n_predict));
1203    map.insert("suspect".into(), json!(b.suspect));
1204    map.insert(
1205        "stream_mode".into(),
1206        b.stream_mode.map_or(Value::Null, |m| to_value(&m)),
1207    );
1208    map.insert(
1209        "stream_witness".into(),
1210        b.stream_witness.map_or(Value::Null, |w| to_value(&w)),
1211    );
1212    map.insert(
1213        "witness".into(),
1214        b.witness.as_ref().map_or(Value::Null, to_value),
1215    );
1216    map.insert("scaling_efficiency".into(), scaling_efficiency(b, ctx));
1217    map.insert("overhead_share".into(), overhead_share(b, ctx));
1218    map.insert(
1219        "roofline_tok_per_sec".into(),
1220        ctx.roofline.map_or(Value::Null, |r| json!(r)),
1221    );
1222    map.insert(
1223        "samples_file".into(),
1224        b.samples_file.as_ref().map_or(Value::Null, to_value),
1225    );
1226    map.insert("samples".into(), to_value(&b.samples));
1227    map.insert(
1228        "join_key".into(),
1229        b.join_key.as_ref().map_or(Value::Null, to_value),
1230    );
1231    if let Some(run_id) = &b.run_id {
1232        map.insert("run_id".into(), json!(run_id.as_str()));
1233    }
1234    insert_optional_latency(&mut map, b);
1235    if let Some(contexts) = comparator {
1236        insert_comparator(&mut map, &b.comparator, contexts);
1237    }
1238    Value::Object(map)
1239}
1240
1241/// Streaming-only metrics. Absent means absent: no zero stands in for one.
1242fn insert_optional_latency(map: &mut Map<String, Value>, b: &DerivedBand) {
1243    for (key, value) in [
1244        ("decode_tok_per_sec", b.decode_tok_per_sec),
1245        ("ttft_p50_ms", b.ttft_p50_ms),
1246        ("ttft_p95_ms", b.ttft_p95_ms),
1247        ("itl_p50_ms", b.itl_p50_ms),
1248        ("itl_p95_ms", b.itl_p95_ms),
1249    ] {
1250        if let Some(v) = value {
1251            map.insert(key.into(), json!(v));
1252        }
1253    }
1254    if let Some(prefill) = b.prefill_tok_per_sec {
1255        map.insert("prefill_tok_per_sec".into(), json!(prefill));
1256        // PP-13: the number is the SERVER's, and the receipt says so beside it.
1257        map.insert("prefill_source".into(), json!("server"));
1258    }
1259}
1260
1261/// PP-31 — `agg(c) / (c · agg(1))`. Null at `c = 1` (where it is 1 by
1262/// construction) and null without an `agg(1)`. REPORTED, never ratcheted: an
1263/// improvement to `agg(1)` lowers it, and failing a build for getting faster is
1264/// the defect PP-31 exists to remove.
1265fn scaling_efficiency(b: &DerivedBand, ctx: &RenderContext) -> Value {
1266    if b.concurrency <= 1 {
1267        return Value::Null;
1268    }
1269    match (b.aggregate_tok_per_sec, ctx.agg1) {
1270        (Some(agg), Some(agg1)) if agg1 > 0.0 => {
1271            json!(agg / (f64::from(b.concurrency) * agg1))
1272        }
1273        _ => Value::Null,
1274    }
1275}
1276
1277/// §3 `overhead_share` — `agg(1) / dec(1)`, per lane. Only meaningful at
1278/// `c = 1`, where the two are measurements of the same single stream: the gap
1279/// between them is everything that is not decode.
1280fn overhead_share(b: &DerivedBand, ctx: &RenderContext) -> Value {
1281    if b.concurrency != 1 {
1282        return Value::Null;
1283    }
1284    match (ctx.agg1, ctx.dec1) {
1285        (Some(agg1), Some(dec1)) if dec1 > 0.0 => json!(agg1 / dec1),
1286        _ => Value::Null,
1287    }
1288}
1289
1290fn insert_comparator(
1291    map: &mut Map<String, Value>,
1292    status: &ComparatorStatus,
1293    ctx: &RenderContexts,
1294) {
1295    map.insert("comparator_status".into(), json!(status.wire_token()));
1296    match status {
1297        ComparatorStatus::NotApplicable {
1298            decided_by,
1299            reason,
1300            budget,
1301        } => {
1302            map.insert("comparator_decided_by".into(), json!(decided_by));
1303            map.insert("comparator_reason".into(), json!(reason));
1304            if let Some(b) = budget {
1305                map.insert("comparator_budget".into(), json!(b));
1306            }
1307            map.insert("baseline".into(), Value::Null);
1308            map.insert("ratios".into(), Value::Null);
1309        }
1310        ComparatorStatus::Unmeasured {
1311            owner,
1312            reason,
1313            admission_capped,
1314        } => {
1315            map.insert("comparator_owner".into(), json!(owner));
1316            map.insert("comparator_reason".into(), json!(reason));
1317            if let Some(cap) = admission_capped {
1318                map.insert("comparator_admission_capped".into(), to_value(cap));
1319            }
1320            map.insert("baseline".into(), Value::Null);
1321            map.insert("ratios".into(), Value::Null);
1322        }
1323        ComparatorStatus::Measured(join) => {
1324            // The baseline is rendered by the SAME function, minus its own
1325            // baseline/ratios: a comparator lane that did not have to satisfy
1326            // every receipt rule is not a baseline (PP-3). Its per-lane figures
1327            // come from `ctx.comparator`, never from the subject's.
1328            map.insert(
1329                "baseline".into(),
1330                band_json(join.baseline(), &ctx.comparator, None),
1331            );
1332            map.insert("ratios".into(), to_value(join.ratios()));
1333        }
1334    }
1335}
1336
1337fn is_sha256(value: &str) -> bool {
1338    value.len() == 64
1339        && value
1340            .bytes()
1341            .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
1342}
1343
1344// ---------------------------------------------------------------------------
1345// The typed reader
1346// ---------------------------------------------------------------------------
1347
1348/// A receipt, read back. `deny_unknown_fields` at every level.
1349///
1350/// # Why the producer has a reader at all
1351///
1352/// Until v3 the receipt had a serialiser and no deserialiser. Every must-fire
1353/// of the form "strip field X and prove the gate reds" was therefore testable
1354/// only in python inside `perf_gate.sh --selftest` — and that runs only in the
1355/// `ci` job, never in `workspace-test`, whose image has no python3. So the Rust
1356/// half of those rules could not be turned red at all.
1357///
1358/// `deny_unknown_fields` is the other half: a receipt carrying a key this type
1359/// does not know is refused rather than silently ignored, which is what stops a
1360/// producer from inventing `agg_ratio` beside a band and a reader from quietly
1361/// dropping it.
1362#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1363#[serde(deny_unknown_fields)]
1364pub struct Receipt {
1365    /// `"PP-LLAMA-001 v3.0"`.
1366    pub spec: String,
1367    /// PP-4 — `3` for v3.
1368    pub schema_version: u32,
1369    /// PP-3 — shared by both lanes of one invocation.
1370    pub run_id: RunId,
1371    /// The commit under test.
1372    pub commit: String,
1373    /// §5.1 workload.
1374    pub workload: Workload,
1375    /// §5.1 protocol parameters.
1376    pub protocol: ProtocolParams,
1377    /// §4.4.1 — always `closed_loop` in this producer.
1378    pub client_model: String,
1379    /// §4.2.2 identity.
1380    pub provenance: Provenance,
1381    /// §4.4.6 counting declaration.
1382    pub tokenization: TokenizationBlock,
1383    /// Requests issued across every band.
1384    pub requested: usize,
1385    /// Requests that completed.
1386    pub completed: usize,
1387    /// Requests that hit the hard timeout.
1388    pub timeouts: usize,
1389    /// Requests abandoned at a drain deadline.
1390    pub truncated: usize,
1391    /// Requests that failed otherwise.
1392    pub errors: usize,
1393    /// PP-28 — completed requests short of `n_predict`, over every band.
1394    pub short_of_n_predict: usize,
1395    /// PP-10 — the worst band's drain phase.
1396    pub drain_ms: f64,
1397    /// Retained sample count.
1398    pub n: usize,
1399    /// Retained per-request end-to-end latencies.
1400    pub samples_ms: Vec<f64>,
1401    /// PP-24 — the ladder.
1402    pub ladder: Ladder,
1403    /// The bands.
1404    pub bands: Vec<ReceiptBand>,
1405    /// Arm D's server-reported memory block.
1406    #[serde(default, skip_serializing_if = "Option::is_none")]
1407    pub kv: Option<KvBlock>,
1408    /// Fields this producer could not produce, each with its reason.
1409    pub unproduced_fields: Vec<String>,
1410    /// PP-21 — the detached HMAC block `scripts/perf_receipt_sign.sh` appends on
1411    /// the measuring host. The renderer never writes it (it must not be able to
1412    /// sign its own output), so the reader carries it opaquely: a signed receipt
1413    /// must parse, and `scripts/lib/receipt_sig.py --verify` is the verifier.
1414    #[serde(default, skip_serializing_if = "Option::is_none")]
1415    pub signature: Option<serde_json::Value>,
1416}
1417
1418/// One band, read back.
1419#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1420#[serde(deny_unknown_fields)]
1421#[allow(clippy::struct_excessive_bools)]
1422pub struct ReceiptBand {
1423    /// Fixed concurrency `c`.
1424    pub concurrency: u32,
1425    /// Which replicate, 1-based.
1426    pub replicate: u32,
1427    /// §7.4 status token.
1428    pub status: String,
1429    /// §3 `agg`. Absent on an `INVALID-CORRECTNESS` band.
1430    #[serde(default, skip_serializing_if = "Option::is_none")]
1431    pub aggregate_tok_per_sec: Option<f64>,
1432    /// `agg`'s numerator.
1433    pub tokens_total: u64,
1434    /// `agg`'s denominator, in milliseconds.
1435    pub span_ms: f64,
1436    /// `T`.
1437    pub window_ms: f64,
1438    /// PP-10 drain phase.
1439    pub drain_ms: f64,
1440    /// Requests issued.
1441    pub requested: usize,
1442    /// Requests completed.
1443    pub completed: usize,
1444    /// Requests that timed out.
1445    pub timeouts: usize,
1446    /// Requests abandoned at the drain deadline.
1447    pub truncated: usize,
1448    /// Requests that failed otherwise.
1449    pub errors: usize,
1450    /// PP-28 count for this band.
1451    pub short_of_n_predict: usize,
1452    /// PP-10 `SUSPECT` annotations.
1453    pub suspect: Vec<String>,
1454    /// PP-27 — what the server declared.
1455    pub stream_mode: Option<StreamMode>,
1456    /// PP-27 — what the client observed.
1457    pub stream_witness: Option<StreamWitness>,
1458    /// PP-26 — the correctness witness.
1459    pub witness: Option<BatchInvarianceWitness>,
1460    /// PP-31 — reported, never ratcheted.
1461    pub scaling_efficiency: Option<f64>,
1462    /// §3 `overhead_share`, at c=1 only.
1463    pub overhead_share: Option<f64>,
1464    /// PP-23 ceiling.
1465    pub roofline_tok_per_sec: Option<f64>,
1466    /// PP-7 — the retained gz side file.
1467    pub samples_file: Option<SamplesFile>,
1468    /// PP-7 — the per-request rows.
1469    pub samples: Vec<SampleRow>,
1470    /// PP-22 — the key this band joins on.
1471    pub join_key: Option<JoinKey>,
1472    /// PP-3 — the run this band belongs to. Present on a baseline.
1473    #[serde(default, skip_serializing_if = "Option::is_none")]
1474    pub run_id: Option<RunId>,
1475    /// §3 `dec`.
1476    #[serde(default, skip_serializing_if = "Option::is_none")]
1477    pub decode_tok_per_sec: Option<f64>,
1478    /// p50 TTFT.
1479    #[serde(default, skip_serializing_if = "Option::is_none")]
1480    pub ttft_p50_ms: Option<f64>,
1481    /// p95 TTFT.
1482    #[serde(default, skip_serializing_if = "Option::is_none")]
1483    pub ttft_p95_ms: Option<f64>,
1484    /// p50 pooled ITL.
1485    #[serde(default, skip_serializing_if = "Option::is_none")]
1486    pub itl_p50_ms: Option<f64>,
1487    /// p95 pooled ITL.
1488    #[serde(default, skip_serializing_if = "Option::is_none")]
1489    pub itl_p95_ms: Option<f64>,
1490    /// §3 `prefill`, server-reported.
1491    #[serde(default, skip_serializing_if = "Option::is_none")]
1492    pub prefill_tok_per_sec: Option<f64>,
1493    /// `"server"` whenever `prefill_tok_per_sec` is present (PP-13).
1494    #[serde(default, skip_serializing_if = "Option::is_none")]
1495    pub prefill_source: Option<String>,
1496    /// Legacy comparator posture token.
1497    #[serde(default, skip_serializing_if = "Option::is_none")]
1498    pub comparator_status: Option<String>,
1499    /// Who owes an `UNMEASURED` comparator.
1500    #[serde(default, skip_serializing_if = "Option::is_none")]
1501    pub comparator_owner: Option<String>,
1502    /// Who decided a `NOT_APPLICABLE` comparator.
1503    #[serde(default, skip_serializing_if = "Option::is_none")]
1504    pub comparator_decided_by: Option<String>,
1505    /// Why.
1506    #[serde(default, skip_serializing_if = "Option::is_none")]
1507    pub comparator_reason: Option<String>,
1508    /// PP-24 — the server-reported ceiling behind an `NA`.
1509    #[serde(default, skip_serializing_if = "Option::is_none")]
1510    pub comparator_budget: Option<String>,
1511    /// PP-24 — which lane capped admission.
1512    #[serde(default, skip_serializing_if = "Option::is_none")]
1513    pub comparator_admission_capped: Option<AdmissionCap>,
1514    /// PP-3 — the comparator lane's band. Absent on a baseline itself.
1515    #[serde(default, skip_serializing_if = "Option::is_none")]
1516    pub baseline: Option<Box<ReceiptBand>>,
1517    /// P-5 — the ratios. Absent on a baseline itself.
1518    #[serde(default, skip_serializing_if = "Option::is_none")]
1519    pub ratios: Option<BandRatios>,
1520}
1521
1522impl Receipt {
1523    /// Parse a receipt, refusing any key this type does not know.
1524    ///
1525    /// # Errors
1526    /// On malformed JSON, a missing required field, or an unknown one.
1527    pub fn parse(text: &str) -> Result<Self, String> {
1528        serde_json::from_str(text).map_err(|e| format!("parsing receipt: {e}"))
1529    }
1530
1531    /// The L1 rules a reader can apply to a receipt it did not produce.
1532    ///
1533    /// # Errors
1534    /// When the spec string or schema version is wrong, when provenance fails
1535    /// its own checks (PP-2, 18, 20, 25, 30), when a band's `status` is outside
1536    /// the §7.4 vocabulary, or when a band carries `ratios` without a
1537    /// `baseline` (PP-3, PP-17).
1538    pub fn validate(&self) -> Result<(), String> {
1539        if self.spec != SPEC_ID {
1540            return Err(format!(
1541                "receipt.spec is {:?}, expected {SPEC_ID:?}",
1542                self.spec
1543            ));
1544        }
1545        if self.schema_version != SCHEMA_VERSION {
1546            return Err(format!(
1547                "receipt.schema_version is {}, expected {SCHEMA_VERSION} — a receipt at another \
1548                 version is historical and is never a baseline (PP-4)",
1549                self.schema_version
1550            ));
1551        }
1552        self.provenance.validate()?;
1553        self.tokenization.validate()?;
1554        if self.bands.is_empty() {
1555            return Err(
1556                "receipt has no bands — a measurement over zero bands is a vacuous \
1557                        pass"
1558                    .to_string(),
1559            );
1560        }
1561        self.check_run_id()?;
1562        for band in &self.bands {
1563            band.validate()?;
1564        }
1565        Ok(())
1566    }
1567
1568    /// PP-3 / §1(d) — the `run_id` a reader recomputes from this receipt's own
1569    /// four inputs must be the one written on it.
1570    ///
1571    /// Every input is now on the wire: `provenance.started_utc`,
1572    /// `provenance.host`, `provenance.client.sha256`, `provenance.client.pid`.
1573    /// Before `pid` was carried, "derived rather than random, so it is
1574    /// reproducible from the receipt" was a comment and not a checkable claim:
1575    /// a receipt could state any 32 hex characters and nothing could disagree.
1576    fn check_run_id(&self) -> Result<(), String> {
1577        let recomputed = RunId::derive(
1578            &self.provenance.started_utc,
1579            &self.provenance.host,
1580            &self.provenance.client.sha256,
1581            self.provenance.client.pid,
1582        );
1583        if recomputed == self.run_id {
1584            return Ok(());
1585        }
1586        Err(format!(
1587            "PP-3: run_id is {} but sha256(started_utc ‖ host ‖ client.sha256 ‖ client.pid)[..32]              over this receipt's own provenance is {} — the id is DERIVED, and one that its own              contents do not reproduce identifies nothing",
1588            self.run_id.as_str(),
1589            recomputed.as_str()
1590        ))
1591    }
1592}
1593
1594impl ReceiptBand {
1595    /// The band-level half of [`Receipt::validate`].
1596    ///
1597    /// # Errors
1598    /// When `status` is outside the §7.4 vocabulary, when `ratios` appear
1599    /// without a `baseline` (PP-3), or when a `baseline` carries its own
1600    /// baseline (a baseline is one lane, not a chain).
1601    pub fn validate(&self) -> Result<(), String> {
1602        let known = BandStatus::vocabulary()
1603            .iter()
1604            .any(|s| s.wire_token() == self.status);
1605        if !known {
1606            return Err(format!(
1607                "band c={}: status {:?} is outside the §7.4 vocabulary {:?}",
1608                self.concurrency,
1609                self.status,
1610                BandStatus::vocabulary()
1611                    .iter()
1612                    .map(|s| s.wire_token())
1613                    .collect::<Vec<_>>()
1614            ));
1615        }
1616        if self.ratios.is_some() && self.baseline.is_none() {
1617            return Err(format!(
1618                "PP-3 band c={}: `ratios` without a `baseline` — a ratio is representable only \
1619                 against a baseline object that itself passes every receipt rule and shares the \
1620                 run_id",
1621                self.concurrency
1622            ));
1623        }
1624        if let Some(baseline) = &self.baseline {
1625            if baseline.baseline.is_some() || baseline.ratios.is_some() {
1626                return Err(format!(
1627                    "PP-3 band c={}: the baseline carries its own baseline/ratios — a baseline is \
1628                     one comparator lane, not a chain of them",
1629                    self.concurrency
1630                ));
1631            }
1632            baseline.validate()?;
1633        }
1634        Ok(())
1635    }
1636}
1637
1638#[cfg(test)]
1639mod producer_tests {
1640    //! The conversions the CLI needs, the digest it cannot type, and the
1641    //! validators that stop a receipt from being written at all.
1642
1643    use super::*;
1644    use std::io::Write;
1645
1646    /// A parser with its own spelling would let a receipt be written with a
1647    /// class `bench_receipt.py` then rejects — after the band had already run.
1648    #[test]
1649    fn compute_class_roundtrip_is_the_only_spelling() {
1650        for c in [
1651            ComputeClass::Cpu,
1652            ComputeClass::Cuda,
1653            ComputeClass::Metal,
1654            ComputeClass::Wgpu,
1655            ComputeClass::Unknown,
1656        ] {
1657            assert_eq!(
1658                ComputeClass::from_str(c.wire_token()).expect("wire token must parse"),
1659                c
1660            );
1661        }
1662    }
1663
1664    /// `bench_receipt.py`'s `COMPUTE_CLASSES` tuple, spelled out here so that
1665    /// adding a variant without teaching the validator goes red.
1666    #[test]
1667    fn the_wire_tokens_are_bench_receipt_pys_compute_classes() {
1668        let tokens: Vec<&str> = ["cpu", "cuda", "metal", "wgpu", "unknown"].into();
1669        for t in &tokens {
1670            assert!(ComputeClass::from_str(t).is_ok(), "{t} must parse");
1671        }
1672        assert!(ComputeClass::from_str("tpu").is_err());
1673        assert!(ComputeClass::from_str("gpu").is_err());
1674        assert!(
1675            ComputeClass::from_str("CUDA").is_err(),
1676            "case is load-bearing"
1677        );
1678    }
1679
1680    #[test]
1681    fn workload_roundtrips_and_refuses_anything_else() {
1682        for w in [Workload::W1, Workload::W2] {
1683            assert_eq!(Workload::from_str(w.wire_token()).expect("parses"), w);
1684        }
1685        assert!(Workload::from_str("W3").is_err());
1686        assert!(Workload::from_str("w1").is_err());
1687    }
1688
1689    /// The digest must be the shape `Provenance::validate` accepts, or the
1690    /// producer writes a receipt its own validator rejects.
1691    #[test]
1692    fn sha256_file_produces_a_digest_provenance_accepts() {
1693        let dir = tempfile::tempdir().expect("tempdir");
1694        let path = dir.path().join("payload.bin");
1695        let mut f = std::fs::File::create(&path).expect("create");
1696        f.write_all(b"abc").expect("write");
1697        drop(f);
1698
1699        let digest = sha256_file(&path).expect("hashes");
1700        // The published SHA-256 of "abc".
1701        assert_eq!(
1702            digest,
1703            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
1704        );
1705        assert_eq!(digest.len(), 64);
1706        assert!(is_sha256(&digest), "must satisfy the receipt's own check");
1707    }
1708
1709    #[test]
1710    fn sha256_file_reports_a_missing_file_rather_than_a_digest() {
1711        assert!(sha256_file(Path::new("/nonexistent/perf-025")).is_err());
1712    }
1713
1714    /// PP-3 / PP-30 — the id is DERIVED, so a reader holding the receipt can
1715    /// recompute it. A random id could be claimed and never checked.
1716    #[test]
1717    fn the_run_id_is_derived_from_the_receipts_own_contents() {
1718        let a = RunId::derive("2026-09-02T10:11:12.345Z", "lambda", &"c".repeat(64), 4242);
1719        let b = RunId::derive("2026-09-02T10:11:12.345Z", "lambda", &"c".repeat(64), 4242);
1720        assert_eq!(a, b, "the same four facts give the same id");
1721        assert_eq!(a.as_str().len(), 32);
1722        assert!(a
1723            .as_str()
1724            .bytes()
1725            .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
1726
1727        for changed in [
1728            RunId::derive("2026-09-02T10:11:12.346Z", "lambda", &"c".repeat(64), 4242),
1729            RunId::derive("2026-09-02T10:11:12.345Z", "gx10", &"c".repeat(64), 4242),
1730            RunId::derive("2026-09-02T10:11:12.345Z", "lambda", &"d".repeat(64), 4242),
1731            RunId::derive("2026-09-02T10:11:12.345Z", "lambda", &"c".repeat(64), 4243),
1732        ] {
1733            assert_ne!(a, changed, "every input must move the id");
1734        }
1735    }
1736
1737    /// And a malformed one does not parse, so it cannot reach the join.
1738    #[test]
1739    fn a_malformed_run_id_is_refused() {
1740        assert!(RunId::try_from("abc".to_string()).is_err());
1741        assert!(RunId::try_from("A".repeat(32)).is_err(), "case matters");
1742        assert!(RunId::try_from("z".repeat(32)).is_err(), "hex only");
1743        assert!(RunId::try_from("a".repeat(33)).is_err());
1744        assert!(RunId::try_from("a".repeat(32)).is_ok());
1745    }
1746
1747    /// PP-30 — the canonical spelling is the one PP-20 can compare as a string.
1748    #[test]
1749    fn started_utc_must_be_rfc3339_utc() {
1750        assert!(validate_rfc3339_utc_millis("t", "2026-09-02T10:11:12.345Z").is_ok());
1751        for bad in [
1752            "",
1753            "2026-09-02",
1754            "2026-09-02T10:11:12Z",
1755            "2026-09-02T10:11:12.345+00:00",
1756            "2026-09-02t10:11:12.345Z",
1757            "2026-09-02T10:11:12.3456Z",
1758            "not-a-time-at-all-....Z",
1759        ] {
1760            assert!(
1761                validate_rfc3339_utc_millis("t", bad).is_err(),
1762                "{bad:?} must be refused"
1763            );
1764        }
1765    }
1766
1767    /// And the canonical spelling really does sort chronologically, which is
1768    /// what PP-20's string comparison rests on.
1769    #[test]
1770    fn canonical_timestamps_sort_chronologically() {
1771        let mut times = vec![
1772            "2026-12-01T00:00:00.000Z".to_string(),
1773            "2026-09-02T10:11:12.345Z".to_string(),
1774            "2026-09-02T10:11:12.344Z".to_string(),
1775            "2025-01-01T00:00:00.000Z".to_string(),
1776        ];
1777        times.sort();
1778        assert_eq!(
1779            times,
1780            vec![
1781                "2025-01-01T00:00:00.000Z",
1782                "2026-09-02T10:11:12.344Z",
1783                "2026-09-02T10:11:12.345Z",
1784                "2026-12-01T00:00:00.000Z",
1785            ]
1786        );
1787    }
1788
1789    /// PP-24 — the ladder is the declared set capped by the SMALLER admission.
1790    #[test]
1791    fn ladder_derives_from_the_minimum_admission() {
1792        let declared = [1_u32, 4, 8, 16];
1793        let l = Ladder::derive(
1794            &declared,
1795            SlotsAdmitted {
1796                apr: Some(11),
1797                llama: Some(16),
1798            },
1799        );
1800        assert_eq!(l.derived, vec![1, 4, 8], "c=16 exceeds the subject's 11");
1801        assert!(!l.is_underived());
1802
1803        let other_way = Ladder::derive(
1804            &declared,
1805            SlotsAdmitted {
1806                apr: Some(16),
1807                llama: Some(4),
1808            },
1809        );
1810        assert_eq!(other_way.derived, vec![1, 4], "the comparator caps too");
1811
1812        let one_lane = Ladder::derive(
1813            &declared,
1814            SlotsAdmitted {
1815                apr: Some(8),
1816                llama: None,
1817            },
1818        );
1819        assert_eq!(one_lane.derived, vec![1, 4, 8]);
1820
1821        let blind = Ladder::derive(
1822            &declared,
1823            SlotsAdmitted {
1824                apr: None,
1825                llama: None,
1826            },
1827        );
1828        assert_eq!(
1829            blind.derived,
1830            vec![1, 4, 8, 16],
1831            "no evidence does not narrow the ladder"
1832        );
1833        assert!(blind.is_underived(), "…but it is named as unevidenced");
1834    }
1835
1836    /// PP-30 — the helper produces exactly the canonical shape the validator
1837    /// accepts. A producer whose own timestamp its validator rejects would fail
1838    /// only after the measurement had been paid for.
1839    #[cfg(not(target_arch = "wasm32"))]
1840    #[test]
1841    fn now_utc_millis_is_the_shape_the_validator_accepts() {
1842        let now = now_utc_millis();
1843        validate_rfc3339_utc_millis("now", &now)
1844            .unwrap_or_else(|e| panic!("{now:?} must satisfy the receipt's own check: {e}"));
1845        assert!(now.ends_with('Z'));
1846        assert_eq!(now.len(), 24);
1847    }
1848
1849    /// PP-23 — the ceiling is bandwidth over model bytes, and a model with no
1850    /// size has no ceiling rather than an infinite one.
1851    #[test]
1852    fn the_roofline_is_bandwidth_over_model_bytes() {
1853        let r = Roofline {
1854            bandwidth_bytes_per_sec: 1_008_000_000_000.0,
1855            model_bytes: 4_683_073_440,
1856        };
1857        let ceiling = r.tok_per_sec().expect("a sized model has a ceiling");
1858        assert!((ceiling - 215.2).abs() < 0.1, "{ceiling}");
1859        assert!(Roofline {
1860            bandwidth_bytes_per_sec: 1.0,
1861            model_bytes: 0
1862        }
1863        .tok_per_sec()
1864        .is_none());
1865        assert!(Roofline {
1866            bandwidth_bytes_per_sec: 0.0,
1867            model_bytes: 10
1868        }
1869        .tok_per_sec()
1870        .is_none());
1871    }
1872}