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 for slot in &mut self.decisions {
338 *slot = None;
339 }
340 for slot in &mut self.transitions {
341 *slot = None;
342 }
343 self.decision_head = 0;
344 self.transition_head = 0;
345 self.decision_count = 0;
346 self.transition_count = 0;
347 self.current_regime = DiffRegime::StableFrame;
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use ftui_render::diff_strategy::StrategyEvidence;
355
356 fn make_evidence() -> StrategyEvidence {
357 StrategyEvidence {
358 strategy: DiffStrategy::DirtyRows,
359 cost_full: 1.0,
360 cost_dirty: 0.5,
361 cost_redraw: 2.0,
362 posterior_mean: 0.05,
363 posterior_variance: 0.001,
364 alpha: 2.0,
365 beta: 38.0,
366 dirty_rows: 3,
367 total_rows: 24,
368 total_cells: 1920,
369 guard_reason: "none",
370 hysteresis_applied: false,
371 hysteresis_ratio: 0.05,
372 }
373 }
374
375 fn make_record(frame_id: u64, regime: DiffRegime) -> DiffStrategyRecord {
376 DiffStrategyRecord {
377 frame_id,
378 regime,
379 posterior: vec![
380 (DiffStrategy::Full, 0.3),
381 (DiffStrategy::DirtyRows, 0.6),
382 (DiffStrategy::FullRedraw, 0.1),
383 ],
384 chosen_strategy: DiffStrategy::DirtyRows,
385 confidence: 0.6,
386 evidence: make_evidence(),
387 fallback_triggered: false,
388 observations: vec![
389 Observation::new("change_fraction", 0.05, 0.3),
390 Observation::new("dirty_rows", 3.0, 0.2),
391 ],
392 }
393 }
394
395 #[test]
396 fn empty_ledger() {
397 let ledger = DiffEvidenceLedger::new(100);
398 assert!(ledger.is_empty());
399 assert_eq!(ledger.len(), 0);
400 assert_eq!(ledger.transition_count(), 0);
401 assert!(ledger.last_decision().is_none());
402 assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
403 }
404
405 #[test]
406 fn record_single_decision() {
407 let mut ledger = DiffEvidenceLedger::new(100);
408 ledger.record(make_record(1, DiffRegime::StableFrame));
409 assert_eq!(ledger.len(), 1);
410 assert_eq!(ledger.last_decision().unwrap().frame_id, 1);
411 }
412
413 #[test]
414 fn ring_buffer_wraps() {
415 let mut ledger = DiffEvidenceLedger::new(5);
416 for i in 0..10 {
417 ledger.record(make_record(i, DiffRegime::StableFrame));
418 }
419 assert_eq!(ledger.len(), 5);
421 let frames: Vec<u64> = ledger.decisions().map(|d| d.frame_id).collect();
423 assert_eq!(frames, vec![5, 6, 7, 8, 9]);
424 }
425
426 #[test]
427 fn regime_transition_auto_detected() {
428 let mut ledger = DiffEvidenceLedger::new(100);
429 ledger.record(make_record(1, DiffRegime::StableFrame));
430 ledger.record(make_record(2, DiffRegime::BurstyChange));
431 assert_eq!(ledger.transition_count(), 1);
432 assert_eq!(ledger.current_regime(), DiffRegime::BurstyChange);
433 let t = ledger.transitions().next().unwrap();
434 assert_eq!(t.from_regime, DiffRegime::StableFrame);
435 assert_eq!(t.to_regime, DiffRegime::BurstyChange);
436 assert_eq!(t.frame_id, 2);
437 }
438
439 #[test]
440 fn no_transition_on_same_regime() {
441 let mut ledger = DiffEvidenceLedger::new(100);
442 ledger.record(make_record(1, DiffRegime::StableFrame));
443 ledger.record(make_record(2, DiffRegime::StableFrame));
444 assert_eq!(ledger.transition_count(), 0);
445 }
446
447 #[test]
448 fn multiple_transitions() {
449 let mut ledger = DiffEvidenceLedger::new(100);
450 ledger.record(make_record(1, DiffRegime::StableFrame));
451 ledger.record(make_record(2, DiffRegime::BurstyChange));
452 ledger.record(make_record(3, DiffRegime::ResizeRegime));
453 ledger.record(make_record(4, DiffRegime::StableFrame));
454 assert_eq!(ledger.transition_count(), 3);
455 }
456
457 #[test]
458 fn jsonl_round_trip_decision() {
459 let record = make_record(42, DiffRegime::StableFrame);
460 let jsonl = record.to_jsonl();
461 assert!(jsonl.contains("\"type\":\"diff_decision\""));
462 assert!(jsonl.contains("\"frame\":42"));
463 assert!(jsonl.contains("\"regime\":\"stable_frame\""));
464 assert!(jsonl.contains("\"strategy\":\"DirtyRows\""));
465 assert!(jsonl.contains("\"obs\":["));
466 assert!(jsonl.contains("\"m\":\"change_fraction\""));
467 }
468
469 #[test]
470 fn jsonl_round_trip_transition() {
471 let transition = RegimeTransition {
472 frame_id: 10,
473 from_regime: DiffRegime::StableFrame,
474 to_regime: DiffRegime::BurstyChange,
475 trigger: "burst detected".to_string(),
476 confidence: 0.85,
477 };
478 let jsonl = transition.to_jsonl();
479 assert!(jsonl.contains("\"type\":\"regime_transition\""));
480 assert!(jsonl.contains("\"frame\":10"));
481 assert!(jsonl.contains("\"from\":\"stable_frame\""));
482 assert!(jsonl.contains("\"to\":\"bursty_change\""));
483 }
484
485 #[test]
486 fn export_jsonl_output() {
487 let mut ledger = DiffEvidenceLedger::new(100);
488 ledger.record(make_record(1, DiffRegime::StableFrame));
489 ledger.record(make_record(2, DiffRegime::BurstyChange));
490 let output = ledger.export_jsonl();
491 let lines: Vec<&str> = output.lines().collect();
492 assert_eq!(lines.len(), 3);
494 assert!(lines[0].contains("\"frame\":1"));
495 assert!(lines[1].contains("\"frame\":2"));
496 assert!(lines[2].contains("regime_transition"));
497 }
498
499 #[test]
500 fn clear_resets_everything() {
501 let mut ledger = DiffEvidenceLedger::new(100);
502 ledger.record(make_record(1, DiffRegime::StableFrame));
503 ledger.record(make_record(2, DiffRegime::BurstyChange));
504 assert_eq!(ledger.current_regime(), DiffRegime::BurstyChange);
505 ledger.clear();
506 assert!(ledger.is_empty());
507 assert_eq!(ledger.transition_count(), 0);
508 assert!(ledger.last_decision().is_none());
509 assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
510 }
511
512 #[test]
513 fn last_decision_returns_most_recent() {
514 let mut ledger = DiffEvidenceLedger::new(100);
515 ledger.record(make_record(1, DiffRegime::StableFrame));
516 ledger.record(make_record(2, DiffRegime::StableFrame));
517 ledger.record(make_record(3, DiffRegime::StableFrame));
518 assert_eq!(ledger.last_decision().unwrap().frame_id, 3);
519 }
520
521 #[test]
522 fn last_decision_after_wrap() {
523 let mut ledger = DiffEvidenceLedger::new(3);
524 for i in 0..10 {
525 ledger.record(make_record(i, DiffRegime::StableFrame));
526 }
527 assert_eq!(ledger.last_decision().unwrap().frame_id, 9);
528 }
529
530 #[test]
531 fn observation_fields() {
532 let obs = Observation::new("test_metric", 42.0, 1.5);
533 assert_eq!(obs.metric_name, "test_metric");
534 assert!((obs.value - 42.0).abs() < f64::EPSILON);
535 assert!((obs.prior_contribution - 1.5).abs() < f64::EPSILON);
536 }
537
538 #[test]
539 fn regime_as_str() {
540 assert_eq!(DiffRegime::StableFrame.as_str(), "stable_frame");
541 assert_eq!(DiffRegime::BurstyChange.as_str(), "bursty_change");
542 assert_eq!(DiffRegime::ResizeRegime.as_str(), "resize");
543 assert_eq!(DiffRegime::DegradedTerminal.as_str(), "degraded");
544 }
545
546 #[test]
547 fn transition_ring_buffer_wraps() {
548 let mut ledger = DiffEvidenceLedger::new(10); let regimes = [
551 DiffRegime::StableFrame,
552 DiffRegime::BurstyChange,
553 DiffRegime::ResizeRegime,
554 DiffRegime::DegradedTerminal,
555 ];
556 for i in 0..100 {
557 ledger.record(make_record(i, regimes[i as usize % regimes.len()]));
558 }
559 assert!(ledger.transition_count() <= 16);
561 }
562
563 #[test]
564 fn decisions_order_before_wrap() {
565 let mut ledger = DiffEvidenceLedger::new(10);
566 for i in 0..5 {
567 ledger.record(make_record(i, DiffRegime::StableFrame));
568 }
569 let frames: Vec<u64> = ledger.decisions().map(|d| d.frame_id).collect();
570 assert_eq!(frames, vec![0, 1, 2, 3, 4]);
571 }
572
573 #[test]
574 fn flush_to_sink_writes_all() {
575 let mut ledger = DiffEvidenceLedger::new(100);
576 ledger.record(make_record(1, DiffRegime::StableFrame));
577 ledger.record(make_record(2, DiffRegime::BurstyChange));
578
579 let config = crate::evidence_sink::EvidenceSinkConfig::enabled_stdout();
580 if let Ok(Some(sink)) = crate::evidence_sink::EvidenceSink::from_config(&config) {
581 let result = ledger.flush_to_sink(&sink);
583 assert!(result.is_ok());
584 }
585 }
586
587 #[test]
588 fn simulate_1000_frames() {
589 let mut ledger = DiffEvidenceLedger::new(10_000);
590 let regimes = [
591 DiffRegime::StableFrame,
592 DiffRegime::BurstyChange,
593 DiffRegime::ResizeRegime,
594 DiffRegime::StableFrame,
595 DiffRegime::DegradedTerminal,
596 DiffRegime::StableFrame,
597 ];
598
599 for i in 0..1000 {
600 let regime = regimes[(i / 100) % regimes.len()];
602 ledger.record(make_record(i as u64, regime));
603 }
604
605 assert_eq!(ledger.len(), 1000);
606 assert!(ledger.transition_count() > 0);
608
609 let mut prev_frame = 0u64;
611 for d in ledger.decisions() {
612 assert!(d.frame_id >= prev_frame);
613 prev_frame = d.frame_id;
614 }
615
616 let jsonl = ledger.export_jsonl();
618 let lines: Vec<&str> = jsonl.lines().collect();
619 assert_eq!(lines.len(), ledger.len() + ledger.transition_count());
620 }
621
622 #[test]
623 fn debug_format() {
624 let ledger = DiffEvidenceLedger::new(100);
625 let debug = format!("{ledger:?}");
626 assert!(debug.contains("DiffEvidenceLedger"));
627 assert!(debug.contains("decisions: 0"));
628 }
629
630 #[test]
631 fn minimum_capacity() {
632 let mut ledger = DiffEvidenceLedger::new(0); ledger.record(make_record(1, DiffRegime::StableFrame));
634 assert_eq!(ledger.len(), 1);
635 ledger.record(make_record(2, DiffRegime::StableFrame));
636 assert_eq!(ledger.len(), 1); assert_eq!(ledger.last_decision().unwrap().frame_id, 2);
638 }
639
640 #[test]
643 fn contract_stable_to_bursty_transition() {
644 let mut ledger = DiffEvidenceLedger::new(100);
646
647 for i in 0..10 {
649 ledger.record(make_record(i, DiffRegime::StableFrame));
650 }
651 assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
652 assert_eq!(ledger.transition_count(), 0);
653
654 ledger.record(make_record(10, DiffRegime::BurstyChange));
656 assert_eq!(ledger.current_regime(), DiffRegime::BurstyChange);
657 assert_eq!(ledger.transition_count(), 1);
658
659 let t = ledger.transitions().next().unwrap();
660 assert_eq!(t.from_regime, DiffRegime::StableFrame);
661 assert_eq!(t.to_regime, DiffRegime::BurstyChange);
662 assert_eq!(t.frame_id, 10);
663 }
664
665 #[test]
666 fn contract_bursty_recovery_to_stable() {
667 let mut ledger = DiffEvidenceLedger::new(100);
669
670 ledger.record(make_record(0, DiffRegime::StableFrame));
672 ledger.record(make_record(1, DiffRegime::BurstyChange));
673 assert_eq!(ledger.transition_count(), 1); for i in 2..5 {
677 ledger.record(make_record(i, DiffRegime::StableFrame));
678 }
679 assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
680 assert_eq!(ledger.transition_count(), 2); }
682
683 #[test]
684 fn contract_resize_returns_to_previous() {
685 let mut ledger = DiffEvidenceLedger::new(100);
687
688 ledger.record(make_record(0, DiffRegime::StableFrame));
690
691 ledger.record(make_record(1, DiffRegime::ResizeRegime));
693 assert_eq!(ledger.current_regime(), DiffRegime::ResizeRegime);
694
695 ledger.record(make_record(2, DiffRegime::StableFrame));
697 assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
698
699 assert_eq!(ledger.transition_count(), 2);
701 }
702
703 #[test]
704 fn contract_degraded_entry_and_recovery() {
705 let mut ledger = DiffEvidenceLedger::new(100);
707
708 ledger.record(make_record(0, DiffRegime::StableFrame));
709 ledger.record(make_record(1, DiffRegime::DegradedTerminal));
710 assert_eq!(ledger.current_regime(), DiffRegime::DegradedTerminal);
711
712 for i in 2..10 {
714 ledger.record(make_record(i, DiffRegime::DegradedTerminal));
715 }
716 assert_eq!(ledger.current_regime(), DiffRegime::DegradedTerminal);
717 assert_eq!(ledger.transition_count(), 1); ledger.record(make_record(10, DiffRegime::StableFrame));
721 assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
722 assert_eq!(ledger.transition_count(), 2);
723 }
724
725 #[test]
726 fn contract_no_flapping() {
727 let mut ledger = DiffEvidenceLedger::new(100);
730
731 let sequence = [
732 DiffRegime::StableFrame,
733 DiffRegime::BurstyChange,
734 DiffRegime::StableFrame,
735 DiffRegime::BurstyChange,
736 DiffRegime::StableFrame,
737 ];
738
739 for (i, ®ime) in sequence.iter().enumerate() {
740 ledger.record(make_record(i as u64, regime));
741 }
742
743 assert_eq!(ledger.transition_count(), 4);
745
746 let transitions: Vec<(DiffRegime, DiffRegime)> = ledger
748 .transitions()
749 .map(|t| (t.from_regime, t.to_regime))
750 .collect();
751 assert_eq!(
752 transitions,
753 vec![
754 (DiffRegime::StableFrame, DiffRegime::BurstyChange),
755 (DiffRegime::BurstyChange, DiffRegime::StableFrame),
756 (DiffRegime::StableFrame, DiffRegime::BurstyChange),
757 (DiffRegime::BurstyChange, DiffRegime::StableFrame),
758 ]
759 );
760 }
761
762 #[test]
763 fn contract_full_lifecycle() {
764 let mut ledger = DiffEvidenceLedger::new(100);
766
767 let lifecycle = [
768 (0, DiffRegime::StableFrame),
769 (1, DiffRegime::StableFrame),
770 (2, DiffRegime::BurstyChange),
771 (3, DiffRegime::BurstyChange),
772 (4, DiffRegime::ResizeRegime),
773 (5, DiffRegime::StableFrame),
774 (6, DiffRegime::StableFrame),
775 (7, DiffRegime::DegradedTerminal),
776 (8, DiffRegime::DegradedTerminal),
777 (9, DiffRegime::StableFrame),
778 ];
779
780 for &(frame, regime) in &lifecycle {
781 ledger.record(make_record(frame, regime));
782 }
783
784 assert_eq!(ledger.len(), 10);
785 assert_eq!(ledger.transition_count(), 5);
788 assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
789
790 for t in ledger.transitions() {
792 assert!(t.frame_id <= 9);
793 assert_ne!(t.from_regime, t.to_regime);
794 }
795 }
796}