Skip to main content

jugar_probar/perf_gate/
ab.rs

1//! PP-LLAMA-001 v3.0 §10 / PP-32 — the engine track's `AbRecord`.
2//!
3//! # What this is for, and what it must be unable to say
4//!
5//! Engine work — a kernel change, a flag, a scheduler fix — proceeds from today
6//! and needs no comparator, no matrix run and no §12 row. What it must never do
7//! is announce a parity ratio (PP-12): "the fused kernel is N times faster" is a
8//! claim about two builds of `apr`, and it acquires a comparator only by
9//! sleight of hand.
10//!
11//! So PP-32 is a *shape* rule: this record **has no field able to hold a
12//! comparator, a second runtime name, or a parity verdict**, and
13//! `deny_unknown_fields` means one cannot be added by a producer either. A JSON
14//! document carrying `comparator`, `runtime`, `llama_agg` or `parity` does not
15//! parse. That is the whole guard — there is nothing to remember to check.
16//!
17//! # What it must say
18//!
19//! - `delta_kind`: `config` (one binary, one flag) or `code` (two binaries, two
20//!   commits). Both arms' effective configs are diffed and **any difference
21//!   outside the declared delta is a hard error** — a `code` arm pair that also
22//!   moved a flag measured two changes and attributed them to one.
23//! - Two arms, each with its own `commit` and `sha256`. For a `config` delta
24//!   they are the same build twice, and the record still says so explicitly.
25//! - `interleaved: true` and a strictly alternating `order`. §4.3's reasoning
26//!   is identical here: thermal and warm-cache state drift across a sweep.
27//! - `prediction`, written **before** the run. A prediction recorded afterwards
28//!   is a description.
29//! - `interval`, the §4.3 replicate estimator over the per-replicate `agg`. It
30//!   is stored on the wire *and* re-derived by [`AbRecord::validate`], so a
31//!   stated interval its own replicates do not produce is refused — the same
32//!   rule `bench_receipt.py` applies to ratios.
33
34use serde::{Deserialize, Serialize};
35use serde_json::Value;
36
37use super::join::Ratio;
38use super::receipt::RunId;
39use super::replicate::{log_ratio_bound_or_point, ArmOrder, ReplicatePair};
40
41/// Which of the two arms. There are exactly two, and neither is "the
42/// comparator": both are `apr`.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(rename_all = "lowercase")]
45pub enum ArmId {
46    /// The control: the tree as it stands.
47    A,
48    /// The change under test.
49    B,
50}
51
52/// §10 — what differs between the arms.
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "snake_case", deny_unknown_fields)]
55pub enum DeltaKind {
56    /// One binary, one flag. The flag name is the entire declared delta.
57    Config {
58        /// The single effective-config key the arms are allowed to differ on.
59        flag: String,
60    },
61    /// Two binaries, two commits. The declared delta is the code; the arms'
62    /// effective configs must be **identical**.
63    Code,
64}
65
66impl DeltaKind {
67    /// The effective-config keys the arms are permitted to differ on.
68    #[must_use]
69    pub fn declared_keys(&self) -> Vec<&str> {
70        match self {
71            Self::Config { flag } => vec![flag.as_str()],
72            Self::Code => Vec::new(),
73        }
74    }
75}
76
77/// One arm's identity and the configuration it actually resolved to.
78#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
79#[serde(deny_unknown_fields)]
80pub struct Arm {
81    /// Which arm.
82    pub id: ArmId,
83    /// The commit the binary was built from.
84    pub commit: String,
85    /// The binary's digest. 64 lowercase hex characters.
86    pub sha256: String,
87    /// `GET /v1/effective-config`, verbatim.
88    pub effective_config: Value,
89}
90
91/// One key on which the two arms' effective configs differ.
92#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
93#[serde(deny_unknown_fields)]
94pub struct ConfigDiff {
95    /// The effective-config key.
96    pub key: String,
97    /// Arm A's value.
98    pub a: Value,
99    /// Arm B's value.
100    pub b: Value,
101}
102
103/// One replicate of one arm.
104#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
105#[serde(deny_unknown_fields)]
106pub struct AbReplicate {
107    /// Which arm ran.
108    pub arm: ArmId,
109    /// Aggregate throughput for this replicate.
110    pub agg: f64,
111    /// Median per-request decode, when the run streamed.
112    pub dec: Option<f64>,
113    /// Server-reported prefill, when the server timed it.
114    pub prefill: Option<f64>,
115}
116
117/// PP-32 — the engine track's record. Two arms of `apr`, interleaved, with a
118/// prediction written before the run.
119///
120/// There is deliberately no `comparator`, no `runtime`, no `baseline` and no
121/// `verdict` field, and `deny_unknown_fields` stops one being smuggled in.
122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
123#[serde(deny_unknown_fields)]
124pub struct AbRecord {
125    /// The harness invocation both arms ran inside.
126    pub run_id: RunId,
127    /// RFC3339 UTC start instant.
128    pub started_utc: String,
129    /// Which host.
130    pub host: String,
131    /// What differs between the arms.
132    pub delta_kind: DeltaKind,
133    /// What the change was predicted to do, written before the run.
134    pub prediction: String,
135    /// Exactly two arms.
136    pub arms: [Arm; 2],
137    /// Must be `true`; §4.3's reasoning applies unchanged.
138    pub interleaved: bool,
139    /// The execution sequence, strictly alternating.
140    pub order: Vec<ArmId>,
141    /// The keys the two effective configs actually differ on.
142    pub effective_config_diff: Vec<ConfigDiff>,
143    /// One entry per element of `order`, in the same sequence.
144    pub replicates: Vec<AbReplicate>,
145    /// §4.3 — the exponentiated one-sided bound on `ln(B/A)` over the paired
146    /// replicates. `None` when the design cannot support one.
147    pub interval: Option<Ratio>,
148}
149
150impl AbRecord {
151    /// Parse a record, refusing any key this type does not know — including,
152    /// by construction, `comparator`.
153    ///
154    /// # Errors
155    /// On malformed JSON, a missing required field, or an unknown one.
156    pub fn parse(text: &str) -> Result<Self, String> {
157        serde_json::from_str(text).map_err(|e| format!("parsing AbRecord: {e}"))
158    }
159
160    /// §10 — every rule the record must satisfy.
161    ///
162    /// # Errors
163    /// When the arms are not `A` and `B`, when `interleaved` is false, when
164    /// `order` does not strictly alternate, when the replicates do not match
165    /// the order, when an effective-config difference lies outside the declared
166    /// delta, when a `code` delta has identical shas on both arms, when the
167    /// prediction is empty, or when the stored `interval` is not the one the
168    /// replicates produce.
169    pub fn validate(&self) -> Result<(), String> {
170        self.validate_arms()?;
171        if !self.interleaved {
172            return Err(
173                "PP-32: interleaved=false — the two arms must alternate within one harness \
174                 invocation. Thermal state, warm caches and free VRAM drift across a sweep, and \
175                 a block of A followed by a block of B measures the drift as well as the change"
176                    .to_string(),
177            );
178        }
179        self.validate_order()?;
180        self.validate_config_diff()?;
181        if self.prediction.trim().is_empty() {
182            return Err(
183                "PP-32: prediction is empty — §10 is 'predict, then verify', and a prediction \
184                 recorded after the run is a description"
185                    .to_string(),
186            );
187        }
188        self.validate_interval()
189    }
190
191    /// §4.3 — the interval the replicates themselves produce.
192    ///
193    /// `subject` is arm B (the change) and `comparator` is arm A (the control),
194    /// so a ratio above 1 means the change was faster.
195    ///
196    /// # The order is READ, not assumed
197    ///
198    /// A replicate is a **pair of adjacent runs** in this record's own `order`,
199    /// and its [`ArmOrder`] is which of the two came first. The previous
200    /// spelling paired the k-th `A` with the k-th `B` — however far apart they
201    /// ran — and computed the order from `k % 2` and the first entry of
202    /// `order`, which alternates by construction whatever `order` says.
203    /// `log_ratio_lcb`'s counterbalancing refusal therefore could not fire from
204    /// here at all: a blocked `A,A,A,B,B,B` sweep, the exact design §4.3's
205    /// interleaving exists to reject, produced a **bound** as though it had
206    /// alternated. Now a chunk that is not one of each arm yields `None`, and
207    /// the pair order is the one the record recorded.
208    ///
209    /// Note the two disciplines this exposes, which are not the same rule:
210    /// [`Self::validate_order`] requires the RUN sequence to alternate
211    /// (`A,B,A,B,…`), while `log_ratio_lcb` requires the PAIR order to
212    /// counterbalance (`AB, BA, AB, …`). An `A,B,A,B,…` record runs A first
213    /// every time, so a first-run order effect is confounded with the arm and
214    /// §4.3 gives it a point estimate and no bound. That is the honest reading
215    /// of the design it recorded; it is not something this function can fix.
216    #[must_use]
217    pub fn derived_interval(&self) -> Option<Ratio> {
218        log_ratio_bound_or_point(&self.replicate_pairs()?)
219    }
220
221    /// The interleaved pairs this record's `order` actually describes, or
222    /// `None` when it describes none.
223    fn replicate_pairs(&self) -> Option<Vec<ReplicatePair>> {
224        if self.order.len() < 2 || self.replicates.len() != self.order.len() {
225            return None;
226        }
227        let mut pairs = Vec::with_capacity(self.order.len() / 2);
228        for (k, chunk) in self.order.as_chunks::<2>().0.iter().enumerate() {
229            let (first, second) = (chunk[0], chunk[1]);
230            if first == second {
231                // Two runs of the same arm back to back: this is a block, not
232                // an interleaved replicate, and there is no pair to form.
233                return None;
234            }
235            let (a, b) = (&self.replicates[2 * k], &self.replicates[2 * k + 1]);
236            if a.arm != first || b.arm != second {
237                // The numbers are not in the sequence `order` claims, so which
238                // ran first is unknown. Refused rather than guessed.
239                return None;
240            }
241            let (control, change) = if first == ArmId::A { (a, b) } else { (b, a) };
242            pairs.push(ReplicatePair {
243                subject: change.agg,
244                comparator: control.agg,
245                order: if first == ArmId::A {
246                    ArmOrder::ComparatorFirst
247                } else {
248                    ArmOrder::SubjectFirst
249                },
250            });
251        }
252        Some(pairs)
253    }
254
255    fn validate_arms(&self) -> Result<(), String> {
256        if self.arms[0].id != ArmId::A || self.arms[1].id != ArmId::B {
257            return Err(format!(
258                "PP-32: arms are [{:?}, {:?}], expected [A, B]",
259                self.arms[0].id, self.arms[1].id
260            ));
261        }
262        for arm in &self.arms {
263            if arm.sha256.len() != 64
264                || !arm
265                    .sha256
266                    .bytes()
267                    .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
268            {
269                return Err(format!(
270                    "PP-32: arm {:?} sha256 {:?} is not 64 lowercase hex characters",
271                    arm.id, arm.sha256
272                ));
273            }
274        }
275        if self.delta_kind == DeltaKind::Code && self.arms[0].sha256 == self.arms[1].sha256 {
276            return Err(
277                "PP-32: delta_kind=code but both arms carry the same sha256 — a code delta needs \
278                 two binaries, and one binary run twice measures noise"
279                    .to_string(),
280            );
281        }
282        Ok(())
283    }
284
285    fn validate_order(&self) -> Result<(), String> {
286        if self.order.len() < 2 {
287            return Err(format!(
288                "PP-32: order has {} entries — an A/B record needs at least one of each",
289                self.order.len()
290            ));
291        }
292        if let Some(i) = self.order.windows(2).position(|w| w[0] == w[1]) {
293            return Err(format!(
294                "PP-32: order is not strictly alternating — entries {i} and {} are both {:?}",
295                i + 1,
296                self.order[i]
297            ));
298        }
299        if self.replicates.len() != self.order.len() {
300            return Err(format!(
301                "PP-32: {} replicates against {} order entries — every run in the sequence must \
302                 carry its numbers",
303                self.replicates.len(),
304                self.order.len()
305            ));
306        }
307        for (i, (want, got)) in self.order.iter().zip(self.replicates.iter()).enumerate() {
308            if *want != got.arm {
309                return Err(format!(
310                    "PP-32: replicate {i} is arm {:?} but order says {want:?}",
311                    got.arm
312                ));
313            }
314        }
315        Ok(())
316    }
317
318    fn validate_config_diff(&self) -> Result<(), String> {
319        let declared = self.delta_kind.declared_keys();
320        let outside: Vec<&str> = self
321            .effective_config_diff
322            .iter()
323            .map(|d| d.key.as_str())
324            .filter(|k| !declared.contains(k))
325            .collect();
326        if outside.is_empty() {
327            return Ok(());
328        }
329        Err(format!(
330            "PP-32: the arms' effective configs differ on {outside:?}, outside the declared delta \
331             {declared:?} — the run measured more than one change and attributed it to one"
332        ))
333    }
334
335    fn validate_interval(&self) -> Result<(), String> {
336        let derived = self.derived_interval();
337        if intervals_agree(self.interval.as_ref(), derived.as_ref()) {
338            return Ok(());
339        }
340        Err(format!(
341            "PP-32: the stated interval {:?} is not the one these replicates produce ({derived:?}) \
342             — a stated bound its own data does not reproduce is a fabricated measurement",
343            self.interval
344        ))
345    }
346}
347
348/// Does a stated interval match the derived one?
349///
350/// `point` and `lcb95` are compared to a relative tolerance rather than bit for
351/// bit: a record that has been through JSON can differ from the recomputed
352/// value by an ulp, and refusing a record for that would be a rule about
353/// `serde_json`'s float formatting rather than about the measurement. `method`
354/// and `n` are compared exactly — those are claims, not measurements.
355fn intervals_agree(stated: Option<&Ratio>, derived: Option<&Ratio>) -> bool {
356    match (stated, derived) {
357        (None, None) => true,
358        (Some(a), Some(b)) => {
359            a.method == b.method
360                && a.n == b.n
361                && close(Some(a.point), Some(b.point))
362                && close(a.lcb95, b.lcb95)
363        }
364        _ => false,
365    }
366}
367
368fn close(a: Option<f64>, b: Option<f64>) -> bool {
369    match (a, b) {
370        (None, None) => true,
371        (Some(x), Some(y)) => (x - y).abs() <= 1e-9 * x.abs().max(y.abs()).max(1.0),
372        _ => false,
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    // The `<selftest-name>__<sentence>` spelling is load-bearing: PP-29's
379    // `scripts/spec_conformance.sh` joins the §6 invariant table to the test
380    // list on the prefix before the double underscore, so renaming these to
381    // single-underscore snake case would silently unjoin the rows they arm.
382    #![allow(non_snake_case)]
383    use super::*;
384    use serde_json::json;
385
386    fn arm(id: ArmId, commit: &str, sha: char, config: Value) -> Arm {
387        Arm {
388            id,
389            commit: commit.to_string(),
390            sha256: std::iter::repeat_n(sha, 64).collect(),
391            effective_config: config,
392        }
393    }
394
395    fn record(delta_kind: DeltaKind, arms: [Arm; 2]) -> AbRecord {
396        let order = vec![ArmId::A, ArmId::B, ArmId::A, ArmId::B, ArmId::A, ArmId::B];
397        let replicates: Vec<AbReplicate> = order
398            .iter()
399            .enumerate()
400            .map(|(i, arm)| AbReplicate {
401                arm: *arm,
402                agg: if *arm == ArmId::A {
403                    100.0 + i as f64
404                } else {
405                    130.0 + i as f64
406                },
407                dec: Some(40.0),
408                prefill: None,
409            })
410            .collect();
411        let mut r = AbRecord {
412            run_id: RunId::derive("2026-09-02T10:11:12.345Z", "lambda", &"c".repeat(64), 7),
413            started_utc: "2026-09-02T10:11:12.345Z".to_string(),
414            host: "lambda".to_string(),
415            delta_kind,
416            prediction: "batched decode <= 3.5 ms/tok; agg(2) > 1.0x one client".to_string(),
417            arms,
418            interleaved: true,
419            order,
420            effective_config_diff: Vec::new(),
421            replicates,
422            interval: None,
423        };
424        r.interval = r.derived_interval();
425        r
426    }
427
428    fn code_record() -> AbRecord {
429        record(
430            DeltaKind::Code,
431            [
432                arm(ArmId::A, "119f61738", 'a', json!({"max_batch": 11})),
433                arm(ArmId::B, "2f0c9d114", 'b', json!({"max_batch": 11})),
434            ],
435        )
436    }
437
438    /// PP-32's must-not-fire: a `code` delta with two shas parses and validates.
439    #[test]
440    fn abrecord_ok__a_code_delta_with_two_shas_parses() {
441        let r = code_record();
442        let text = serde_json::to_string(&r).expect("serialises");
443        let back = AbRecord::parse(&text).expect("round-trips");
444        back.validate().expect("a conformant record validates");
445        assert_eq!(back.arms, r.arms);
446        assert_eq!(back.order, r.order);
447        assert_eq!(back.replicates, r.replicates);
448        assert_eq!(back.delta_kind, r.delta_kind);
449        assert_eq!(back.run_id, r.run_id);
450        assert!(
451            intervals_agree(back.interval.as_ref(), r.interval.as_ref()),
452            "{:?} vs {:?}",
453            back.interval,
454            r.interval
455        );
456        assert_eq!(back.arms[0].id, ArmId::A);
457        assert_ne!(back.arms[0].sha256, back.arms[1].sha256);
458        assert!(back.interval.expect("interval").point > 1.0);
459    }
460
461    /// The tolerance is a tolerance, not a hole: a bound that is actually
462    /// different is still refused.
463    #[test]
464    fn the_interval_tolerance_admits_an_ulp_and_nothing_more() {
465        let derived = code_record().interval.expect("interval");
466        let one_ulp = Ratio {
467            point: f64::from_bits(derived.point.to_bits() + 1),
468            ..derived.clone()
469        };
470        assert!(intervals_agree(Some(&one_ulp), Some(&derived)));
471        let moved = Ratio {
472            point: derived.point * 1.000_01,
473            ..derived.clone()
474        };
475        assert!(!intervals_agree(Some(&moved), Some(&derived)));
476        assert!(!intervals_agree(None, Some(&derived)));
477        assert!(!intervals_agree(Some(&derived), None));
478    }
479
480    /// PP-32's must-fire: the record has no field able to hold a comparator, and
481    /// `deny_unknown_fields` means a producer cannot add one either.
482    #[test]
483    fn abrecord_comparator__a_comparator_field_does_not_parse() {
484        let mut value = serde_json::to_value(code_record()).expect("serialises");
485        value
486            .as_object_mut()
487            .expect("object")
488            .insert("comparator".to_string(), json!({"runtime": "llama.cpp"}));
489        let err = AbRecord::parse(&value.to_string()).expect_err("comparator must not parse");
490        assert!(err.contains("comparator"), "{err}");
491
492        // The same for every other spelling of a parity claim.
493        for smuggled in ["runtime", "baseline", "parity", "agg_ratio", "llama_agg"] {
494            let mut v = serde_json::to_value(code_record()).expect("serialises");
495            v.as_object_mut()
496                .expect("object")
497                .insert(smuggled.to_string(), json!("llama.cpp"));
498            assert!(
499                AbRecord::parse(&v.to_string()).is_err(),
500                "{smuggled} must not parse"
501            );
502        }
503    }
504
505    /// A block of A followed by a block of B measures the drift as well as the
506    /// change.
507    #[test]
508    fn non_interleaved_ab_is_refused() {
509        let mut r = code_record();
510        r.interleaved = false;
511        let err = r.validate().expect_err("interleaved=false");
512        assert!(err.contains("alternate"), "{err}");
513
514        // And the flag cannot lie about the sequence either.
515        let mut blocked = code_record();
516        blocked.order = vec![ArmId::A, ArmId::A, ArmId::B, ArmId::B];
517        blocked.replicates = blocked
518            .order
519            .iter()
520            .map(|arm| AbReplicate {
521                arm: *arm,
522                agg: 100.0,
523                dec: None,
524                prefill: None,
525            })
526            .collect();
527        blocked.interval = blocked.derived_interval();
528        let err = blocked.validate().expect_err("order does not alternate");
529        assert!(err.contains("strictly alternating"), "{err}");
530    }
531
532    /// §10 — any effective-config difference outside the declared delta is a
533    /// hard error, because the run then measured two changes.
534    #[test]
535    fn a_config_diff_outside_the_declared_delta_is_refused() {
536        let mut r = record(
537            DeltaKind::Config {
538                flag: "FUSED_GATE_UP".to_string(),
539            },
540            [
541                arm(ArmId::A, "119f61738", 'a', json!({"fused_gate_up": false})),
542                arm(ArmId::A, "119f61738", 'a', json!({"fused_gate_up": true})),
543            ],
544        );
545        r.arms[1].id = ArmId::B;
546        r.effective_config_diff = vec![ConfigDiff {
547            key: "FUSED_GATE_UP".to_string(),
548            a: json!(false),
549            b: json!(true),
550        }];
551        r.validate()
552            .expect("the declared flag is allowed to differ");
553
554        r.effective_config_diff.push(ConfigDiff {
555            key: "max_batch".to_string(),
556            a: json!(11),
557            b: json!(16),
558        });
559        let err = r.validate().expect_err("max_batch is outside the delta");
560        assert!(err.contains("max_batch"), "{err}");
561        assert!(err.contains("FUSED_GATE_UP"), "{err}");
562
563        // A `code` delta declares no config keys at all, so ANY diff is outside.
564        let mut c = code_record();
565        c.effective_config_diff = vec![ConfigDiff {
566            key: "max_batch".to_string(),
567            a: json!(11),
568            b: json!(16),
569        }];
570        assert!(c.validate().is_err(), "a code delta permits no config diff");
571    }
572
573    /// Each arm's digest is checked, not just its presence.
574    #[test]
575    fn an_arm_digest_that_is_not_64_lowercase_hex_is_refused() {
576        for bad in ["short", &"A".repeat(64), &"z".repeat(64), &"a".repeat(63)] {
577            let mut r = code_record();
578            r.arms[1].sha256 = (*bad).to_string();
579            let err = r.validate().expect_err("{bad} must be refused");
580            assert!(err.contains("64 lowercase hex"), "{bad}: {err}");
581        }
582        let mut swapped = code_record();
583        swapped.arms.swap(0, 1);
584        let err = swapped.validate().expect_err("arms out of order");
585        assert!(err.contains("expected [A, B]"), "{err}");
586    }
587
588    /// A `code` delta needs two binaries; one binary run twice measures noise.
589    #[test]
590    fn a_code_delta_with_one_binary_is_refused() {
591        let mut r = code_record();
592        r.arms[1].sha256 = r.arms[0].sha256.clone();
593        let err = r.validate().expect_err("one binary");
594        assert!(err.contains("two binaries"), "{err}");
595    }
596
597    /// The stored interval must be the one the replicates produce — the rule
598    /// `bench_receipt.py` applies to ratios, applied here.
599    #[test]
600    fn a_stated_interval_its_replicates_do_not_produce_is_refused() {
601        let mut r = code_record();
602        let mut fake = r.interval.clone().expect("interval");
603        fake.lcb95 = Some(9.99);
604        r.interval = Some(fake);
605        let err = r.validate().expect_err("fabricated interval");
606        assert!(err.contains("fabricated"), "{err}");
607    }
608
609    /// The prediction is written before the run; an empty one is a description.
610    #[test]
611    fn an_empty_prediction_is_refused() {
612        let mut r = code_record();
613        r.prediction = "   ".to_string();
614        let err = r.validate().expect_err("no prediction");
615        assert!(err.contains("predict, then verify"), "{err}");
616    }
617
618    /// The order and the replicates must describe the same sequence.
619    #[test]
620    fn replicates_must_match_the_declared_order() {
621        let mut r = code_record();
622        r.replicates.pop();
623        let err = r.validate().expect_err("counts differ");
624        assert!(err.contains("order entries"), "{err}");
625
626        let mut swapped = code_record();
627        swapped.replicates[0].arm = ArmId::B;
628        let err = swapped.validate().expect_err("arm disagrees with order");
629        assert!(err.contains("order says"), "{err}");
630    }
631
632    /// §4.3 MUST-FIRE, through `derived_interval`: a BLOCKED run —
633    /// `A,A,A,B,B,B` rather than `A,B,A,B,A,B` — yields no interval at all.
634    ///
635    /// `derived_interval` used to pair the k-th `A` with the k-th `B` however
636    /// far apart they ran, and to synthesise each pair's `ArmOrder` from
637    /// `k % 2`, which alternates by construction. So a blocked sweep — the
638    /// exact design §4.3's interleaving exists to reject — produced a bound as
639    /// though it had alternated, and the counterbalancing refusal inside
640    /// `log_ratio_lcb` was unreachable from here.
641    #[test]
642    fn abrecord_blocked__a_blocked_order_produces_no_interval() {
643        let with_order = |order: Vec<ArmId>| -> AbRecord {
644            let replicates: Vec<AbReplicate> = order
645                .iter()
646                .enumerate()
647                .map(|(i, arm)| AbReplicate {
648                    arm: *arm,
649                    agg: if *arm == ArmId::A {
650                        100.0 + i as f64
651                    } else {
652                        130.0 + i as f64
653                    },
654                    dec: Some(40.0),
655                    prefill: None,
656                })
657                .collect();
658            AbRecord {
659                order,
660                replicates,
661                ..code_record()
662            }
663        };
664
665        // MUST-NOT-FIRE: an interleaved sequence pairs, run by adjacent run.
666        let interleaved = with_order(vec![
667            ArmId::A,
668            ArmId::B,
669            ArmId::A,
670            ArmId::B,
671            ArmId::A,
672            ArmId::B,
673        ])
674        .derived_interval()
675        .expect("three adjacent pairs");
676        assert_eq!(interleaved.n, 3);
677
678        // MUST-FIRE: the same six runs, blocked, describe no interleaved pair.
679        let blocked = with_order(vec![
680            ArmId::A,
681            ArmId::A,
682            ArmId::A,
683            ArmId::B,
684            ArmId::B,
685            ArmId::B,
686        ]);
687        assert!(
688            blocked.derived_interval().is_none(),
689            "a blocked sweep measures the drift as well as the change, and pairing its k-th A \
690             with its k-th B pairs two runs minutes apart as though they had alternated"
691        );
692        // …and a record that STATES an interval its blocked replicates cannot
693        // produce is refused through exactly this function.
694        let stated = AbRecord {
695            interval: Some(Ratio::reporting_only(
696                1.3,
697                crate::perf_gate::join::RatioMethod::ReplicateTLower,
698                3,
699            )),
700            ..blocked
701        };
702        let err = stated
703            .validate()
704            .expect_err("a stated interval over a blocked sweep");
705        assert!(err.contains("PP-32"), "{err}");
706    }
707
708    /// A counterbalanced sequence — `AB, BA, AB, BA, AB` — is the design
709    /// `log_ratio_lcb` will bound: the arm that runs first flips every
710    /// replicate, so a first-run order effect cancels instead of loading onto
711    /// one arm.
712    ///
713    /// It is recorded here as the shape a bound REQUIRES. `validate_order`
714    /// asks for the run sequence to alternate (`A,B,A,B,…`), which runs A first
715    /// every time and therefore earns a point estimate and no bound — two
716    /// different disciplines under one word, and a §10 question for the spec
717    /// owner rather than something this function may decide.
718    #[test]
719    fn only_a_counterbalanced_sequence_earns_a_bound() {
720        let order = vec![
721            ArmId::A,
722            ArmId::B,
723            ArmId::B,
724            ArmId::A,
725            ArmId::A,
726            ArmId::B,
727            ArmId::B,
728            ArmId::A,
729            ArmId::A,
730            ArmId::B,
731        ];
732        let replicates: Vec<AbReplicate> = order
733            .iter()
734            .enumerate()
735            .map(|(i, arm)| AbReplicate {
736                arm: *arm,
737                agg: if *arm == ArmId::A {
738                    100.0 + (i % 3) as f64
739                } else {
740                    130.0 + (i % 3) as f64
741                },
742                dec: None,
743                prefill: None,
744            })
745            .collect();
746        let counterbalanced = AbRecord {
747            order,
748            replicates,
749            ..code_record()
750        };
751        let i = counterbalanced
752            .derived_interval()
753            .expect("five adjacent pairs");
754        assert_eq!(i.n, 5);
755        assert!(
756            i.lcb95.is_some(),
757            "five counterbalanced pairs support a bound: {i:?}"
758        );
759
760        // The must-not-fire's mirror: the SAME ten runs in the A,B,A,B,… order
761        // `validate_order` asks for run A first every time, and get no bound.
762        let abab: Vec<ArmId> = (0..10)
763            .map(|i| if i % 2 == 0 { ArmId::A } else { ArmId::B })
764            .collect();
765        let replicates: Vec<AbReplicate> = abab
766            .iter()
767            .enumerate()
768            .map(|(i, arm)| AbReplicate {
769                arm: *arm,
770                agg: if *arm == ArmId::A {
771                    100.0 + (i % 3) as f64
772                } else {
773                    130.0 + (i % 3) as f64
774                },
775                dec: None,
776                prefill: None,
777            })
778            .collect();
779        let never_flips = AbRecord {
780            order: abab,
781            replicates,
782            ..code_record()
783        };
784        let j = never_flips.derived_interval().expect("five adjacent pairs");
785        assert_eq!(j.n, 5);
786        assert!(
787            j.lcb95.is_none(),
788            "A always first is not counterbalanced, so §4.3 gives it no bound: {j:?}"
789        );
790    }
791
792    /// The interval is B over A: a faster change is a ratio above 1.
793    #[test]
794    fn the_interval_is_the_change_over_the_control() {
795        let r = code_record();
796        let i = r.derived_interval().expect("three pairs");
797        assert!(i.point > 1.2, "{i:?}");
798        assert!(
799            i.lcb95.is_none(),
800            "three replicate pairs bound no variance (§4.3)"
801        );
802    }
803}