1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(rename_all = "lowercase")]
45pub enum ArmId {
46 A,
48 B,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "snake_case", deny_unknown_fields)]
55pub enum DeltaKind {
56 Config {
58 flag: String,
60 },
61 Code,
64}
65
66impl DeltaKind {
67 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
79#[serde(deny_unknown_fields)]
80pub struct Arm {
81 pub id: ArmId,
83 pub commit: String,
85 pub sha256: String,
87 pub effective_config: Value,
89}
90
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
93#[serde(deny_unknown_fields)]
94pub struct ConfigDiff {
95 pub key: String,
97 pub a: Value,
99 pub b: Value,
101}
102
103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
105#[serde(deny_unknown_fields)]
106pub struct AbReplicate {
107 pub arm: ArmId,
109 pub agg: f64,
111 pub dec: Option<f64>,
113 pub prefill: Option<f64>,
115}
116
117#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
123#[serde(deny_unknown_fields)]
124pub struct AbRecord {
125 pub run_id: RunId,
127 pub started_utc: String,
129 pub host: String,
131 pub delta_kind: DeltaKind,
133 pub prediction: String,
135 pub arms: [Arm; 2],
137 pub interleaved: bool,
139 pub order: Vec<ArmId>,
141 pub effective_config_diff: Vec<ConfigDiff>,
143 pub replicates: Vec<AbReplicate>,
145 pub interval: Option<Ratio>,
148}
149
150impl AbRecord {
151 pub fn parse(text: &str) -> Result<Self, String> {
157 serde_json::from_str(text).map_err(|e| format!("parsing AbRecord: {e}"))
158 }
159
160 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 #[must_use]
217 pub fn derived_interval(&self) -> Option<Ratio> {
218 log_ratio_bound_or_point(&self.replicate_pairs()?)
219 }
220
221 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 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 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
348fn 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 #![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 #[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 #[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 #[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 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 #[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 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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 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 #[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 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 #[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}