1#![forbid(unsafe_code)]
2
3use std::fmt::Write as _;
24
25use ftui_render::diff_strategy::{DiffStrategy, StrategyEvidence};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum DiffRegime {
30 StableFrame,
32 BurstyChange,
34 ResizeRegime,
36 DegradedTerminal,
38}
39
40impl DiffRegime {
41 pub const fn as_str(self) -> &'static str {
43 match self {
44 Self::StableFrame => "stable_frame",
45 Self::BurstyChange => "bursty_change",
46 Self::ResizeRegime => "resize",
47 Self::DegradedTerminal => "degraded",
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct Observation {
55 pub metric_name: String,
57 pub value: f64,
59 pub prior_contribution: f64,
61}
62
63impl Observation {
64 pub fn new(metric_name: impl Into<String>, value: f64, prior_contribution: f64) -> Self {
66 Self {
67 metric_name: metric_name.into(),
68 value,
69 prior_contribution,
70 }
71 }
72}
73
74#[derive(Debug, Clone)]
76pub struct DiffStrategyRecord {
77 pub frame_id: u64,
79 pub regime: DiffRegime,
81 pub posterior: Vec<(DiffStrategy, f64)>,
83 pub chosen_strategy: DiffStrategy,
85 pub confidence: f64,
87 pub evidence: StrategyEvidence,
89 pub fallback_triggered: bool,
91 pub observations: Vec<Observation>,
93}
94
95impl DiffStrategyRecord {
96 pub fn to_jsonl(&self) -> String {
98 let mut out = String::with_capacity(512);
99 out.push_str("{\"type\":\"diff_decision\"");
100 let _ = write!(out, ",\"frame\":{}", self.frame_id);
101 let _ = write!(out, ",\"regime\":\"{}\"", self.regime.as_str());
102 let _ = write!(out, ",\"strategy\":\"{:?}\"", self.chosen_strategy);
103 let _ = write!(out, ",\"confidence\":{:.6}", self.confidence);
104 let _ = write!(out, ",\"fallback\":{}", self.fallback_triggered);
105 let _ = write!(
106 out,
107 ",\"posterior_mean\":{:.6},\"posterior_var\":{:.6}",
108 self.evidence.posterior_mean, self.evidence.posterior_variance
109 );
110 let _ = write!(
111 out,
112 ",\"cost_full\":{:.4},\"cost_dirty\":{:.4},\"cost_redraw\":{:.4}",
113 self.evidence.cost_full, self.evidence.cost_dirty, self.evidence.cost_redraw
114 );
115 let _ = write!(
116 out,
117 ",\"alpha\":{:.4},\"beta\":{:.4}",
118 self.evidence.alpha, self.evidence.beta
119 );
120
121 out.push_str(",\"obs\":[");
123 for (i, obs) in self.observations.iter().enumerate() {
124 if i > 0 {
125 out.push(',');
126 }
127 let _ = write!(
128 out,
129 "{{\"m\":\"{}\",\"v\":{:.6},\"c\":{:.6}}}",
130 obs.metric_name.replace('"', "\\\""),
131 obs.value,
132 obs.prior_contribution
133 );
134 }
135 out.push_str("]}");
136 out
137 }
138}
139
140#[derive(Debug, Clone)]
142pub struct RegimeTransition {
143 pub frame_id: u64,
145 pub from_regime: DiffRegime,
147 pub to_regime: DiffRegime,
149 pub trigger: String,
151 pub confidence: f64,
153}
154
155impl RegimeTransition {
156 pub fn to_jsonl(&self) -> String {
158 format!(
159 "{{\"type\":\"regime_transition\",\"frame\":{},\"from\":\"{}\",\"to\":\"{}\",\"trigger\":\"{}\",\"confidence\":{:.6}}}",
160 self.frame_id,
161 self.from_regime.as_str(),
162 self.to_regime.as_str(),
163 self.trigger.replace('"', "\\\""),
164 self.confidence,
165 )
166 }
167}
168
169pub struct DiffEvidenceLedger {
174 decisions: Vec<Option<DiffStrategyRecord>>,
175 transitions: Vec<Option<RegimeTransition>>,
176 decision_head: usize,
177 transition_head: usize,
178 decision_count: usize,
179 transition_count: usize,
180 decision_capacity: usize,
181 transition_capacity: usize,
182 current_regime: DiffRegime,
183}
184
185impl std::fmt::Debug for DiffEvidenceLedger {
186 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187 f.debug_struct("DiffEvidenceLedger")
188 .field("decisions", &self.decision_count)
189 .field("transitions", &self.transition_count)
190 .field("capacity", &self.decision_capacity)
191 .field("regime", &self.current_regime)
192 .finish()
193 }
194}
195
196impl DiffEvidenceLedger {
197 pub fn new(decision_capacity: usize) -> Self {
202 let decision_capacity = decision_capacity.max(1);
203 let transition_capacity = (decision_capacity / 10).max(16);
204 Self {
205 decisions: (0..decision_capacity).map(|_| None).collect(),
206 transitions: (0..transition_capacity).map(|_| None).collect(),
207 decision_head: 0,
208 transition_head: 0,
209 decision_count: 0,
210 transition_count: 0,
211 decision_capacity,
212 transition_capacity,
213 current_regime: DiffRegime::StableFrame,
214 }
215 }
216
217 pub fn record(&mut self, record: DiffStrategyRecord) {
219 if record.regime != self.current_regime {
221 let transition = RegimeTransition {
222 frame_id: record.frame_id,
223 from_regime: self.current_regime,
224 to_regime: record.regime,
225 trigger: format!(
226 "confidence={:.3} strategy={:?}",
227 record.confidence, record.chosen_strategy
228 ),
229 confidence: record.confidence,
230 };
231 self.record_transition(transition);
232 self.current_regime = record.regime;
233 }
234
235 self.decisions[self.decision_head] = Some(record);
236 self.decision_head = (self.decision_head + 1) % self.decision_capacity;
237 if self.decision_count < self.decision_capacity {
238 self.decision_count += 1;
239 }
240 }
241
242 pub fn record_transition(&mut self, transition: RegimeTransition) {
244 self.transitions[self.transition_head] = Some(transition);
245 self.transition_head = (self.transition_head + 1) % self.transition_capacity;
246 if self.transition_count < self.transition_capacity {
247 self.transition_count += 1;
248 }
249 }
250
251 pub fn len(&self) -> usize {
253 self.decision_count
254 }
255
256 pub fn is_empty(&self) -> bool {
258 self.decision_count == 0
259 }
260
261 pub fn transition_count(&self) -> usize {
263 self.transition_count
264 }
265
266 pub fn current_regime(&self) -> DiffRegime {
268 self.current_regime
269 }
270
271 pub fn decisions(&self) -> impl Iterator<Item = &DiffStrategyRecord> {
273 let cap = self.decision_capacity;
274 let count = self.decision_count;
275 let head = self.decision_head;
276 let start = if count < cap { 0 } else { head };
277
278 (0..count).filter_map(move |i| {
279 let idx = (start + i) % cap;
280 self.decisions[idx].as_ref()
281 })
282 }
283
284 pub fn transitions(&self) -> impl Iterator<Item = &RegimeTransition> {
286 let cap = self.transition_capacity;
287 let count = self.transition_count;
288 let head = self.transition_head;
289 let start = if count < cap { 0 } else { head };
290
291 (0..count).filter_map(move |i| {
292 let idx = (start + i) % cap;
293 self.transitions[idx].as_ref()
294 })
295 }
296
297 pub fn last_decision(&self) -> Option<&DiffStrategyRecord> {
299 if self.decision_count == 0 {
300 return None;
301 }
302 let idx = if self.decision_head == 0 {
303 self.decision_capacity - 1
304 } else {
305 self.decision_head - 1
306 };
307 self.decisions[idx].as_ref()
308 }
309
310 pub fn export_jsonl(&self) -> String {
312 let mut out = String::new();
313 for d in self.decisions() {
314 out.push_str(&d.to_jsonl());
315 out.push('\n');
316 }
317 for t in self.transitions() {
318 out.push_str(&t.to_jsonl());
319 out.push('\n');
320 }
321 out
322 }
323
324 pub fn flush_to_sink(&self, sink: &crate::evidence_sink::EvidenceSink) -> std::io::Result<()> {
326 for d in self.decisions() {
327 sink.write_jsonl(&d.to_jsonl())?;
328 }
329 for t in self.transitions() {
330 sink.write_jsonl(&t.to_jsonl())?;
331 }
332 Ok(())
333 }
334
335 pub fn clear(&mut self) {
337 self.decisions.fill_with(|| None);
338 self.transitions.fill_with(|| None);
339 self.decision_head = 0;
340 self.transition_head = 0;
341 self.decision_count = 0;
342 self.transition_count = 0;
343 self.current_regime = DiffRegime::StableFrame;
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350 use ftui_render::diff_strategy::StrategyEvidence;
351
352 fn make_evidence() -> StrategyEvidence {
353 StrategyEvidence {
354 strategy: DiffStrategy::DirtyRows,
355 cost_full: 1.0,
356 cost_dirty: 0.5,
357 cost_redraw: 2.0,
358 posterior_mean: 0.05,
359 posterior_variance: 0.001,
360 alpha: 2.0,
361 beta: 38.0,
362 dirty_rows: 3,
363 total_rows: 24,
364 total_cells: 1920,
365 guard_reason: "none",
366 hysteresis_applied: false,
367 hysteresis_ratio: 0.05,
368 }
369 }
370
371 fn make_record(frame_id: u64, regime: DiffRegime) -> DiffStrategyRecord {
372 DiffStrategyRecord {
373 frame_id,
374 regime,
375 posterior: vec![
376 (DiffStrategy::Full, 0.3),
377 (DiffStrategy::DirtyRows, 0.6),
378 (DiffStrategy::FullRedraw, 0.1),
379 ],
380 chosen_strategy: DiffStrategy::DirtyRows,
381 confidence: 0.6,
382 evidence: make_evidence(),
383 fallback_triggered: false,
384 observations: vec![
385 Observation::new("change_fraction", 0.05, 0.3),
386 Observation::new("dirty_rows", 3.0, 0.2),
387 ],
388 }
389 }
390
391 #[test]
392 fn empty_ledger() {
393 let ledger = DiffEvidenceLedger::new(100);
394 assert!(ledger.is_empty());
395 assert_eq!(ledger.len(), 0);
396 assert_eq!(ledger.transition_count(), 0);
397 assert!(ledger.last_decision().is_none());
398 assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
399 }
400
401 #[test]
402 fn record_single_decision() {
403 let mut ledger = DiffEvidenceLedger::new(100);
404 ledger.record(make_record(1, DiffRegime::StableFrame));
405 assert_eq!(ledger.len(), 1);
406 assert_eq!(ledger.last_decision().unwrap().frame_id, 1);
407 }
408
409 #[test]
410 fn ring_buffer_wraps() {
411 let mut ledger = DiffEvidenceLedger::new(5);
412 for i in 0..10 {
413 ledger.record(make_record(i, DiffRegime::StableFrame));
414 }
415 assert_eq!(ledger.len(), 5);
417 let frames: Vec<u64> = ledger.decisions().map(|d| d.frame_id).collect();
419 assert_eq!(frames, vec![5, 6, 7, 8, 9]);
420 }
421
422 #[test]
423 fn regime_transition_auto_detected() {
424 let mut ledger = DiffEvidenceLedger::new(100);
425 ledger.record(make_record(1, DiffRegime::StableFrame));
426 ledger.record(make_record(2, DiffRegime::BurstyChange));
427 assert_eq!(ledger.transition_count(), 1);
428 assert_eq!(ledger.current_regime(), DiffRegime::BurstyChange);
429 let t = ledger.transitions().next().unwrap();
430 assert_eq!(t.from_regime, DiffRegime::StableFrame);
431 assert_eq!(t.to_regime, DiffRegime::BurstyChange);
432 assert_eq!(t.frame_id, 2);
433 }
434
435 #[test]
436 fn no_transition_on_same_regime() {
437 let mut ledger = DiffEvidenceLedger::new(100);
438 ledger.record(make_record(1, DiffRegime::StableFrame));
439 ledger.record(make_record(2, DiffRegime::StableFrame));
440 assert_eq!(ledger.transition_count(), 0);
441 }
442
443 #[test]
444 fn multiple_transitions() {
445 let mut ledger = DiffEvidenceLedger::new(100);
446 ledger.record(make_record(1, DiffRegime::StableFrame));
447 ledger.record(make_record(2, DiffRegime::BurstyChange));
448 ledger.record(make_record(3, DiffRegime::ResizeRegime));
449 ledger.record(make_record(4, DiffRegime::StableFrame));
450 assert_eq!(ledger.transition_count(), 3);
451 }
452
453 #[test]
454 fn jsonl_round_trip_decision() {
455 let record = make_record(42, DiffRegime::StableFrame);
456 let jsonl = record.to_jsonl();
457 assert!(jsonl.contains("\"type\":\"diff_decision\""));
458 assert!(jsonl.contains("\"frame\":42"));
459 assert!(jsonl.contains("\"regime\":\"stable_frame\""));
460 assert!(jsonl.contains("\"strategy\":\"DirtyRows\""));
461 assert!(jsonl.contains("\"obs\":["));
462 assert!(jsonl.contains("\"m\":\"change_fraction\""));
463 }
464
465 #[test]
466 fn jsonl_round_trip_transition() {
467 let transition = RegimeTransition {
468 frame_id: 10,
469 from_regime: DiffRegime::StableFrame,
470 to_regime: DiffRegime::BurstyChange,
471 trigger: "burst detected".to_string(),
472 confidence: 0.85,
473 };
474 let jsonl = transition.to_jsonl();
475 assert!(jsonl.contains("\"type\":\"regime_transition\""));
476 assert!(jsonl.contains("\"frame\":10"));
477 assert!(jsonl.contains("\"from\":\"stable_frame\""));
478 assert!(jsonl.contains("\"to\":\"bursty_change\""));
479 }
480
481 #[test]
482 fn export_jsonl_output() {
483 let mut ledger = DiffEvidenceLedger::new(100);
484 ledger.record(make_record(1, DiffRegime::StableFrame));
485 ledger.record(make_record(2, DiffRegime::BurstyChange));
486 let output = ledger.export_jsonl();
487 let lines: Vec<&str> = output.lines().collect();
488 assert_eq!(lines.len(), 3);
490 assert!(lines[0].contains("\"frame\":1"));
491 assert!(lines[1].contains("\"frame\":2"));
492 assert!(lines[2].contains("regime_transition"));
493 }
494
495 #[test]
496 fn clear_resets_everything() {
497 let mut ledger = DiffEvidenceLedger::new(100);
498 ledger.record(make_record(1, DiffRegime::StableFrame));
499 ledger.record(make_record(2, DiffRegime::BurstyChange));
500 assert_eq!(ledger.current_regime(), DiffRegime::BurstyChange);
501 ledger.clear();
502 assert!(ledger.is_empty());
503 assert_eq!(ledger.transition_count(), 0);
504 assert!(ledger.last_decision().is_none());
505 assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
506 }
507
508 #[test]
509 fn last_decision_returns_most_recent() {
510 let mut ledger = DiffEvidenceLedger::new(100);
511 ledger.record(make_record(1, DiffRegime::StableFrame));
512 ledger.record(make_record(2, DiffRegime::StableFrame));
513 ledger.record(make_record(3, DiffRegime::StableFrame));
514 assert_eq!(ledger.last_decision().unwrap().frame_id, 3);
515 }
516
517 #[test]
518 fn last_decision_after_wrap() {
519 let mut ledger = DiffEvidenceLedger::new(3);
520 for i in 0..10 {
521 ledger.record(make_record(i, DiffRegime::StableFrame));
522 }
523 assert_eq!(ledger.last_decision().unwrap().frame_id, 9);
524 }
525
526 #[test]
527 fn observation_fields() {
528 let obs = Observation::new("test_metric", 42.0, 1.5);
529 assert_eq!(obs.metric_name, "test_metric");
530 assert!((obs.value - 42.0).abs() < f64::EPSILON);
531 assert!((obs.prior_contribution - 1.5).abs() < f64::EPSILON);
532 }
533
534 #[test]
535 fn regime_as_str() {
536 assert_eq!(DiffRegime::StableFrame.as_str(), "stable_frame");
537 assert_eq!(DiffRegime::BurstyChange.as_str(), "bursty_change");
538 assert_eq!(DiffRegime::ResizeRegime.as_str(), "resize");
539 assert_eq!(DiffRegime::DegradedTerminal.as_str(), "degraded");
540 }
541
542 #[test]
543 fn transition_ring_buffer_wraps() {
544 let mut ledger = DiffEvidenceLedger::new(10); let regimes = [
547 DiffRegime::StableFrame,
548 DiffRegime::BurstyChange,
549 DiffRegime::ResizeRegime,
550 DiffRegime::DegradedTerminal,
551 ];
552 for i in 0..100 {
553 ledger.record(make_record(i, regimes[i as usize % regimes.len()]));
554 }
555 assert!(ledger.transition_count() <= 16);
557 }
558
559 #[test]
560 fn decisions_order_before_wrap() {
561 let mut ledger = DiffEvidenceLedger::new(10);
562 for i in 0..5 {
563 ledger.record(make_record(i, DiffRegime::StableFrame));
564 }
565 let frames: Vec<u64> = ledger.decisions().map(|d| d.frame_id).collect();
566 assert_eq!(frames, vec![0, 1, 2, 3, 4]);
567 }
568
569 #[test]
570 fn flush_to_sink_writes_all() {
571 let mut ledger = DiffEvidenceLedger::new(100);
572 ledger.record(make_record(1, DiffRegime::StableFrame));
573 ledger.record(make_record(2, DiffRegime::BurstyChange));
574
575 let config = crate::evidence_sink::EvidenceSinkConfig::enabled_stdout();
576 if let Ok(Some(sink)) = crate::evidence_sink::EvidenceSink::from_config(&config) {
577 let result = ledger.flush_to_sink(&sink);
579 assert!(result.is_ok());
580 }
581 }
582
583 #[test]
584 fn simulate_1000_frames() {
585 let mut ledger = DiffEvidenceLedger::new(10_000);
586 let regimes = [
587 DiffRegime::StableFrame,
588 DiffRegime::BurstyChange,
589 DiffRegime::ResizeRegime,
590 DiffRegime::StableFrame,
591 DiffRegime::DegradedTerminal,
592 DiffRegime::StableFrame,
593 ];
594
595 for i in 0..1000 {
596 let regime = regimes[(i / 100) % regimes.len()];
598 ledger.record(make_record(i as u64, regime));
599 }
600
601 assert_eq!(ledger.len(), 1000);
602 assert!(ledger.transition_count() > 0);
604
605 let mut prev_frame = 0u64;
607 for d in ledger.decisions() {
608 assert!(d.frame_id >= prev_frame);
609 prev_frame = d.frame_id;
610 }
611
612 let jsonl = ledger.export_jsonl();
614 let lines: Vec<&str> = jsonl.lines().collect();
615 assert_eq!(lines.len(), ledger.len() + ledger.transition_count());
616 }
617
618 #[test]
619 fn debug_format() {
620 let ledger = DiffEvidenceLedger::new(100);
621 let debug = format!("{ledger:?}");
622 assert!(debug.contains("DiffEvidenceLedger"));
623 assert!(debug.contains("decisions: 0"));
624 }
625
626 #[test]
627 fn minimum_capacity() {
628 let mut ledger = DiffEvidenceLedger::new(0); ledger.record(make_record(1, DiffRegime::StableFrame));
630 assert_eq!(ledger.len(), 1);
631 ledger.record(make_record(2, DiffRegime::StableFrame));
632 assert_eq!(ledger.len(), 1); assert_eq!(ledger.last_decision().unwrap().frame_id, 2);
634 }
635
636 #[test]
639 fn contract_stable_to_bursty_transition() {
640 let mut ledger = DiffEvidenceLedger::new(100);
642
643 for i in 0..10 {
645 ledger.record(make_record(i, DiffRegime::StableFrame));
646 }
647 assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
648 assert_eq!(ledger.transition_count(), 0);
649
650 ledger.record(make_record(10, DiffRegime::BurstyChange));
652 assert_eq!(ledger.current_regime(), DiffRegime::BurstyChange);
653 assert_eq!(ledger.transition_count(), 1);
654
655 let t = ledger.transitions().next().unwrap();
656 assert_eq!(t.from_regime, DiffRegime::StableFrame);
657 assert_eq!(t.to_regime, DiffRegime::BurstyChange);
658 assert_eq!(t.frame_id, 10);
659 }
660
661 #[test]
662 fn contract_bursty_recovery_to_stable() {
663 let mut ledger = DiffEvidenceLedger::new(100);
665
666 ledger.record(make_record(0, DiffRegime::StableFrame));
668 ledger.record(make_record(1, DiffRegime::BurstyChange));
669 assert_eq!(ledger.transition_count(), 1); for i in 2..5 {
673 ledger.record(make_record(i, DiffRegime::StableFrame));
674 }
675 assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
676 assert_eq!(ledger.transition_count(), 2); }
678
679 #[test]
680 fn contract_resize_returns_to_previous() {
681 let mut ledger = DiffEvidenceLedger::new(100);
683
684 ledger.record(make_record(0, DiffRegime::StableFrame));
686
687 ledger.record(make_record(1, DiffRegime::ResizeRegime));
689 assert_eq!(ledger.current_regime(), DiffRegime::ResizeRegime);
690
691 ledger.record(make_record(2, DiffRegime::StableFrame));
693 assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
694
695 assert_eq!(ledger.transition_count(), 2);
697 }
698
699 #[test]
700 fn contract_degraded_entry_and_recovery() {
701 let mut ledger = DiffEvidenceLedger::new(100);
703
704 ledger.record(make_record(0, DiffRegime::StableFrame));
705 ledger.record(make_record(1, DiffRegime::DegradedTerminal));
706 assert_eq!(ledger.current_regime(), DiffRegime::DegradedTerminal);
707
708 for i in 2..10 {
710 ledger.record(make_record(i, DiffRegime::DegradedTerminal));
711 }
712 assert_eq!(ledger.current_regime(), DiffRegime::DegradedTerminal);
713 assert_eq!(ledger.transition_count(), 1); ledger.record(make_record(10, DiffRegime::StableFrame));
717 assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
718 assert_eq!(ledger.transition_count(), 2);
719 }
720
721 #[test]
722 fn contract_no_flapping() {
723 let mut ledger = DiffEvidenceLedger::new(100);
726
727 let sequence = [
728 DiffRegime::StableFrame,
729 DiffRegime::BurstyChange,
730 DiffRegime::StableFrame,
731 DiffRegime::BurstyChange,
732 DiffRegime::StableFrame,
733 ];
734
735 for (i, ®ime) in sequence.iter().enumerate() {
736 ledger.record(make_record(i as u64, regime));
737 }
738
739 assert_eq!(ledger.transition_count(), 4);
741
742 let transitions: Vec<(DiffRegime, DiffRegime)> = ledger
744 .transitions()
745 .map(|t| (t.from_regime, t.to_regime))
746 .collect();
747 assert_eq!(
748 transitions,
749 vec![
750 (DiffRegime::StableFrame, DiffRegime::BurstyChange),
751 (DiffRegime::BurstyChange, DiffRegime::StableFrame),
752 (DiffRegime::StableFrame, DiffRegime::BurstyChange),
753 (DiffRegime::BurstyChange, DiffRegime::StableFrame),
754 ]
755 );
756 }
757
758 #[test]
759 fn contract_full_lifecycle() {
760 let mut ledger = DiffEvidenceLedger::new(100);
762
763 let lifecycle = [
764 (0, DiffRegime::StableFrame),
765 (1, DiffRegime::StableFrame),
766 (2, DiffRegime::BurstyChange),
767 (3, DiffRegime::BurstyChange),
768 (4, DiffRegime::ResizeRegime),
769 (5, DiffRegime::StableFrame),
770 (6, DiffRegime::StableFrame),
771 (7, DiffRegime::DegradedTerminal),
772 (8, DiffRegime::DegradedTerminal),
773 (9, DiffRegime::StableFrame),
774 ];
775
776 for &(frame, regime) in &lifecycle {
777 ledger.record(make_record(frame, regime));
778 }
779
780 assert_eq!(ledger.len(), 10);
781 assert_eq!(ledger.transition_count(), 5);
784 assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
785
786 for t in ledger.transitions() {
788 assert!(t.frame_id <= 9);
789 assert_ne!(t.from_regime, t.to_regime);
790 }
791 }
792}