#![forbid(unsafe_code)]
use std::fmt::Write as _;
use ftui_render::diff_strategy::{DiffStrategy, StrategyEvidence};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DiffRegime {
StableFrame,
BurstyChange,
ResizeRegime,
DegradedTerminal,
}
impl DiffRegime {
pub const fn as_str(self) -> &'static str {
match self {
Self::StableFrame => "stable_frame",
Self::BurstyChange => "bursty_change",
Self::ResizeRegime => "resize",
Self::DegradedTerminal => "degraded",
}
}
}
#[derive(Debug, Clone)]
pub struct Observation {
pub metric_name: String,
pub value: f64,
pub prior_contribution: f64,
}
impl Observation {
pub fn new(metric_name: impl Into<String>, value: f64, prior_contribution: f64) -> Self {
Self {
metric_name: metric_name.into(),
value,
prior_contribution,
}
}
}
#[derive(Debug, Clone)]
pub struct DiffStrategyRecord {
pub frame_id: u64,
pub regime: DiffRegime,
pub posterior: Vec<(DiffStrategy, f64)>,
pub chosen_strategy: DiffStrategy,
pub confidence: f64,
pub evidence: StrategyEvidence,
pub fallback_triggered: bool,
pub observations: Vec<Observation>,
}
impl DiffStrategyRecord {
pub fn to_jsonl(&self) -> String {
let mut out = String::with_capacity(512);
out.push_str("{\"type\":\"diff_decision\"");
let _ = write!(out, ",\"frame\":{}", self.frame_id);
let _ = write!(out, ",\"regime\":\"{}\"", self.regime.as_str());
let _ = write!(out, ",\"strategy\":\"{:?}\"", self.chosen_strategy);
let _ = write!(out, ",\"confidence\":{:.6}", self.confidence);
let _ = write!(out, ",\"fallback\":{}", self.fallback_triggered);
let _ = write!(
out,
",\"posterior_mean\":{:.6},\"posterior_var\":{:.6}",
self.evidence.posterior_mean, self.evidence.posterior_variance
);
let _ = write!(
out,
",\"cost_full\":{:.4},\"cost_dirty\":{:.4},\"cost_redraw\":{:.4}",
self.evidence.cost_full, self.evidence.cost_dirty, self.evidence.cost_redraw
);
let _ = write!(
out,
",\"alpha\":{:.4},\"beta\":{:.4}",
self.evidence.alpha, self.evidence.beta
);
out.push_str(",\"obs\":[");
for (i, obs) in self.observations.iter().enumerate() {
if i > 0 {
out.push(',');
}
let _ = write!(
out,
"{{\"m\":\"{}\",\"v\":{:.6},\"c\":{:.6}}}",
obs.metric_name.replace('"', "\\\""),
obs.value,
obs.prior_contribution
);
}
out.push_str("]}");
out
}
}
#[derive(Debug, Clone)]
pub struct RegimeTransition {
pub frame_id: u64,
pub from_regime: DiffRegime,
pub to_regime: DiffRegime,
pub trigger: String,
pub confidence: f64,
}
impl RegimeTransition {
pub fn to_jsonl(&self) -> String {
format!(
"{{\"type\":\"regime_transition\",\"frame\":{},\"from\":\"{}\",\"to\":\"{}\",\"trigger\":\"{}\",\"confidence\":{:.6}}}",
self.frame_id,
self.from_regime.as_str(),
self.to_regime.as_str(),
self.trigger.replace('"', "\\\""),
self.confidence,
)
}
}
pub struct DiffEvidenceLedger {
decisions: Vec<Option<DiffStrategyRecord>>,
transitions: Vec<Option<RegimeTransition>>,
decision_head: usize,
transition_head: usize,
decision_count: usize,
transition_count: usize,
decision_capacity: usize,
transition_capacity: usize,
current_regime: DiffRegime,
}
impl std::fmt::Debug for DiffEvidenceLedger {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DiffEvidenceLedger")
.field("decisions", &self.decision_count)
.field("transitions", &self.transition_count)
.field("capacity", &self.decision_capacity)
.field("regime", &self.current_regime)
.finish()
}
}
impl DiffEvidenceLedger {
pub fn new(decision_capacity: usize) -> Self {
let decision_capacity = decision_capacity.max(1);
let transition_capacity = (decision_capacity / 10).max(16);
Self {
decisions: (0..decision_capacity).map(|_| None).collect(),
transitions: (0..transition_capacity).map(|_| None).collect(),
decision_head: 0,
transition_head: 0,
decision_count: 0,
transition_count: 0,
decision_capacity,
transition_capacity,
current_regime: DiffRegime::StableFrame,
}
}
pub fn record(&mut self, record: DiffStrategyRecord) {
if record.regime != self.current_regime {
let transition = RegimeTransition {
frame_id: record.frame_id,
from_regime: self.current_regime,
to_regime: record.regime,
trigger: format!(
"confidence={:.3} strategy={:?}",
record.confidence, record.chosen_strategy
),
confidence: record.confidence,
};
self.record_transition(transition);
self.current_regime = record.regime;
}
self.decisions[self.decision_head] = Some(record);
self.decision_head = (self.decision_head + 1) % self.decision_capacity;
if self.decision_count < self.decision_capacity {
self.decision_count += 1;
}
}
pub fn record_transition(&mut self, transition: RegimeTransition) {
self.transitions[self.transition_head] = Some(transition);
self.transition_head = (self.transition_head + 1) % self.transition_capacity;
if self.transition_count < self.transition_capacity {
self.transition_count += 1;
}
}
pub fn len(&self) -> usize {
self.decision_count
}
pub fn is_empty(&self) -> bool {
self.decision_count == 0
}
pub fn transition_count(&self) -> usize {
self.transition_count
}
pub fn current_regime(&self) -> DiffRegime {
self.current_regime
}
pub fn decisions(&self) -> impl Iterator<Item = &DiffStrategyRecord> {
let cap = self.decision_capacity;
let count = self.decision_count;
let head = self.decision_head;
let start = if count < cap { 0 } else { head };
(0..count).filter_map(move |i| {
let idx = (start + i) % cap;
self.decisions[idx].as_ref()
})
}
pub fn transitions(&self) -> impl Iterator<Item = &RegimeTransition> {
let cap = self.transition_capacity;
let count = self.transition_count;
let head = self.transition_head;
let start = if count < cap { 0 } else { head };
(0..count).filter_map(move |i| {
let idx = (start + i) % cap;
self.transitions[idx].as_ref()
})
}
pub fn last_decision(&self) -> Option<&DiffStrategyRecord> {
if self.decision_count == 0 {
return None;
}
let idx = if self.decision_head == 0 {
self.decision_capacity - 1
} else {
self.decision_head - 1
};
self.decisions[idx].as_ref()
}
pub fn export_jsonl(&self) -> String {
let mut out = String::new();
for d in self.decisions() {
out.push_str(&d.to_jsonl());
out.push('\n');
}
for t in self.transitions() {
out.push_str(&t.to_jsonl());
out.push('\n');
}
out
}
pub fn flush_to_sink(&self, sink: &crate::evidence_sink::EvidenceSink) -> std::io::Result<()> {
for d in self.decisions() {
sink.write_jsonl(&d.to_jsonl())?;
}
for t in self.transitions() {
sink.write_jsonl(&t.to_jsonl())?;
}
Ok(())
}
pub fn clear(&mut self) {
for slot in &mut self.decisions {
*slot = None;
}
for slot in &mut self.transitions {
*slot = None;
}
self.decision_head = 0;
self.transition_head = 0;
self.decision_count = 0;
self.transition_count = 0;
self.current_regime = DiffRegime::StableFrame;
}
}
#[cfg(test)]
mod tests {
use super::*;
use ftui_render::diff_strategy::StrategyEvidence;
fn make_evidence() -> StrategyEvidence {
StrategyEvidence {
strategy: DiffStrategy::DirtyRows,
cost_full: 1.0,
cost_dirty: 0.5,
cost_redraw: 2.0,
posterior_mean: 0.05,
posterior_variance: 0.001,
alpha: 2.0,
beta: 38.0,
dirty_rows: 3,
total_rows: 24,
total_cells: 1920,
guard_reason: "none",
hysteresis_applied: false,
hysteresis_ratio: 0.05,
}
}
fn make_record(frame_id: u64, regime: DiffRegime) -> DiffStrategyRecord {
DiffStrategyRecord {
frame_id,
regime,
posterior: vec![
(DiffStrategy::Full, 0.3),
(DiffStrategy::DirtyRows, 0.6),
(DiffStrategy::FullRedraw, 0.1),
],
chosen_strategy: DiffStrategy::DirtyRows,
confidence: 0.6,
evidence: make_evidence(),
fallback_triggered: false,
observations: vec![
Observation::new("change_fraction", 0.05, 0.3),
Observation::new("dirty_rows", 3.0, 0.2),
],
}
}
#[test]
fn empty_ledger() {
let ledger = DiffEvidenceLedger::new(100);
assert!(ledger.is_empty());
assert_eq!(ledger.len(), 0);
assert_eq!(ledger.transition_count(), 0);
assert!(ledger.last_decision().is_none());
assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
}
#[test]
fn record_single_decision() {
let mut ledger = DiffEvidenceLedger::new(100);
ledger.record(make_record(1, DiffRegime::StableFrame));
assert_eq!(ledger.len(), 1);
assert_eq!(ledger.last_decision().unwrap().frame_id, 1);
}
#[test]
fn ring_buffer_wraps() {
let mut ledger = DiffEvidenceLedger::new(5);
for i in 0..10 {
ledger.record(make_record(i, DiffRegime::StableFrame));
}
assert_eq!(ledger.len(), 5);
let frames: Vec<u64> = ledger.decisions().map(|d| d.frame_id).collect();
assert_eq!(frames, vec![5, 6, 7, 8, 9]);
}
#[test]
fn regime_transition_auto_detected() {
let mut ledger = DiffEvidenceLedger::new(100);
ledger.record(make_record(1, DiffRegime::StableFrame));
ledger.record(make_record(2, DiffRegime::BurstyChange));
assert_eq!(ledger.transition_count(), 1);
assert_eq!(ledger.current_regime(), DiffRegime::BurstyChange);
let t = ledger.transitions().next().unwrap();
assert_eq!(t.from_regime, DiffRegime::StableFrame);
assert_eq!(t.to_regime, DiffRegime::BurstyChange);
assert_eq!(t.frame_id, 2);
}
#[test]
fn no_transition_on_same_regime() {
let mut ledger = DiffEvidenceLedger::new(100);
ledger.record(make_record(1, DiffRegime::StableFrame));
ledger.record(make_record(2, DiffRegime::StableFrame));
assert_eq!(ledger.transition_count(), 0);
}
#[test]
fn multiple_transitions() {
let mut ledger = DiffEvidenceLedger::new(100);
ledger.record(make_record(1, DiffRegime::StableFrame));
ledger.record(make_record(2, DiffRegime::BurstyChange));
ledger.record(make_record(3, DiffRegime::ResizeRegime));
ledger.record(make_record(4, DiffRegime::StableFrame));
assert_eq!(ledger.transition_count(), 3);
}
#[test]
fn jsonl_round_trip_decision() {
let record = make_record(42, DiffRegime::StableFrame);
let jsonl = record.to_jsonl();
assert!(jsonl.contains("\"type\":\"diff_decision\""));
assert!(jsonl.contains("\"frame\":42"));
assert!(jsonl.contains("\"regime\":\"stable_frame\""));
assert!(jsonl.contains("\"strategy\":\"DirtyRows\""));
assert!(jsonl.contains("\"obs\":["));
assert!(jsonl.contains("\"m\":\"change_fraction\""));
}
#[test]
fn jsonl_round_trip_transition() {
let transition = RegimeTransition {
frame_id: 10,
from_regime: DiffRegime::StableFrame,
to_regime: DiffRegime::BurstyChange,
trigger: "burst detected".to_string(),
confidence: 0.85,
};
let jsonl = transition.to_jsonl();
assert!(jsonl.contains("\"type\":\"regime_transition\""));
assert!(jsonl.contains("\"frame\":10"));
assert!(jsonl.contains("\"from\":\"stable_frame\""));
assert!(jsonl.contains("\"to\":\"bursty_change\""));
}
#[test]
fn export_jsonl_output() {
let mut ledger = DiffEvidenceLedger::new(100);
ledger.record(make_record(1, DiffRegime::StableFrame));
ledger.record(make_record(2, DiffRegime::BurstyChange));
let output = ledger.export_jsonl();
let lines: Vec<&str> = output.lines().collect();
assert_eq!(lines.len(), 3);
assert!(lines[0].contains("\"frame\":1"));
assert!(lines[1].contains("\"frame\":2"));
assert!(lines[2].contains("regime_transition"));
}
#[test]
fn clear_resets_everything() {
let mut ledger = DiffEvidenceLedger::new(100);
ledger.record(make_record(1, DiffRegime::StableFrame));
ledger.record(make_record(2, DiffRegime::BurstyChange));
assert_eq!(ledger.current_regime(), DiffRegime::BurstyChange);
ledger.clear();
assert!(ledger.is_empty());
assert_eq!(ledger.transition_count(), 0);
assert!(ledger.last_decision().is_none());
assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
}
#[test]
fn last_decision_returns_most_recent() {
let mut ledger = DiffEvidenceLedger::new(100);
ledger.record(make_record(1, DiffRegime::StableFrame));
ledger.record(make_record(2, DiffRegime::StableFrame));
ledger.record(make_record(3, DiffRegime::StableFrame));
assert_eq!(ledger.last_decision().unwrap().frame_id, 3);
}
#[test]
fn last_decision_after_wrap() {
let mut ledger = DiffEvidenceLedger::new(3);
for i in 0..10 {
ledger.record(make_record(i, DiffRegime::StableFrame));
}
assert_eq!(ledger.last_decision().unwrap().frame_id, 9);
}
#[test]
fn observation_fields() {
let obs = Observation::new("test_metric", 42.0, 1.5);
assert_eq!(obs.metric_name, "test_metric");
assert!((obs.value - 42.0).abs() < f64::EPSILON);
assert!((obs.prior_contribution - 1.5).abs() < f64::EPSILON);
}
#[test]
fn regime_as_str() {
assert_eq!(DiffRegime::StableFrame.as_str(), "stable_frame");
assert_eq!(DiffRegime::BurstyChange.as_str(), "bursty_change");
assert_eq!(DiffRegime::ResizeRegime.as_str(), "resize");
assert_eq!(DiffRegime::DegradedTerminal.as_str(), "degraded");
}
#[test]
fn transition_ring_buffer_wraps() {
let mut ledger = DiffEvidenceLedger::new(10); let regimes = [
DiffRegime::StableFrame,
DiffRegime::BurstyChange,
DiffRegime::ResizeRegime,
DiffRegime::DegradedTerminal,
];
for i in 0..100 {
ledger.record(make_record(i, regimes[i as usize % regimes.len()]));
}
assert!(ledger.transition_count() <= 16);
}
#[test]
fn decisions_order_before_wrap() {
let mut ledger = DiffEvidenceLedger::new(10);
for i in 0..5 {
ledger.record(make_record(i, DiffRegime::StableFrame));
}
let frames: Vec<u64> = ledger.decisions().map(|d| d.frame_id).collect();
assert_eq!(frames, vec![0, 1, 2, 3, 4]);
}
#[test]
fn flush_to_sink_writes_all() {
let mut ledger = DiffEvidenceLedger::new(100);
ledger.record(make_record(1, DiffRegime::StableFrame));
ledger.record(make_record(2, DiffRegime::BurstyChange));
let config = crate::evidence_sink::EvidenceSinkConfig::enabled_stdout();
if let Ok(Some(sink)) = crate::evidence_sink::EvidenceSink::from_config(&config) {
let result = ledger.flush_to_sink(&sink);
assert!(result.is_ok());
}
}
#[test]
fn simulate_1000_frames() {
let mut ledger = DiffEvidenceLedger::new(10_000);
let regimes = [
DiffRegime::StableFrame,
DiffRegime::BurstyChange,
DiffRegime::ResizeRegime,
DiffRegime::StableFrame,
DiffRegime::DegradedTerminal,
DiffRegime::StableFrame,
];
for i in 0..1000 {
let regime = regimes[(i / 100) % regimes.len()];
ledger.record(make_record(i as u64, regime));
}
assert_eq!(ledger.len(), 1000);
assert!(ledger.transition_count() > 0);
let mut prev_frame = 0u64;
for d in ledger.decisions() {
assert!(d.frame_id >= prev_frame);
prev_frame = d.frame_id;
}
let jsonl = ledger.export_jsonl();
let lines: Vec<&str> = jsonl.lines().collect();
assert_eq!(lines.len(), ledger.len() + ledger.transition_count());
}
#[test]
fn debug_format() {
let ledger = DiffEvidenceLedger::new(100);
let debug = format!("{ledger:?}");
assert!(debug.contains("DiffEvidenceLedger"));
assert!(debug.contains("decisions: 0"));
}
#[test]
fn minimum_capacity() {
let mut ledger = DiffEvidenceLedger::new(0); ledger.record(make_record(1, DiffRegime::StableFrame));
assert_eq!(ledger.len(), 1);
ledger.record(make_record(2, DiffRegime::StableFrame));
assert_eq!(ledger.len(), 1); assert_eq!(ledger.last_decision().unwrap().frame_id, 2);
}
#[test]
fn contract_stable_to_bursty_transition() {
let mut ledger = DiffEvidenceLedger::new(100);
for i in 0..10 {
ledger.record(make_record(i, DiffRegime::StableFrame));
}
assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
assert_eq!(ledger.transition_count(), 0);
ledger.record(make_record(10, DiffRegime::BurstyChange));
assert_eq!(ledger.current_regime(), DiffRegime::BurstyChange);
assert_eq!(ledger.transition_count(), 1);
let t = ledger.transitions().next().unwrap();
assert_eq!(t.from_regime, DiffRegime::StableFrame);
assert_eq!(t.to_regime, DiffRegime::BurstyChange);
assert_eq!(t.frame_id, 10);
}
#[test]
fn contract_bursty_recovery_to_stable() {
let mut ledger = DiffEvidenceLedger::new(100);
ledger.record(make_record(0, DiffRegime::StableFrame));
ledger.record(make_record(1, DiffRegime::BurstyChange));
assert_eq!(ledger.transition_count(), 1);
for i in 2..5 {
ledger.record(make_record(i, DiffRegime::StableFrame));
}
assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
assert_eq!(ledger.transition_count(), 2); }
#[test]
fn contract_resize_returns_to_previous() {
let mut ledger = DiffEvidenceLedger::new(100);
ledger.record(make_record(0, DiffRegime::StableFrame));
ledger.record(make_record(1, DiffRegime::ResizeRegime));
assert_eq!(ledger.current_regime(), DiffRegime::ResizeRegime);
ledger.record(make_record(2, DiffRegime::StableFrame));
assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
assert_eq!(ledger.transition_count(), 2);
}
#[test]
fn contract_degraded_entry_and_recovery() {
let mut ledger = DiffEvidenceLedger::new(100);
ledger.record(make_record(0, DiffRegime::StableFrame));
ledger.record(make_record(1, DiffRegime::DegradedTerminal));
assert_eq!(ledger.current_regime(), DiffRegime::DegradedTerminal);
for i in 2..10 {
ledger.record(make_record(i, DiffRegime::DegradedTerminal));
}
assert_eq!(ledger.current_regime(), DiffRegime::DegradedTerminal);
assert_eq!(ledger.transition_count(), 1);
ledger.record(make_record(10, DiffRegime::StableFrame));
assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
assert_eq!(ledger.transition_count(), 2);
}
#[test]
fn contract_no_flapping() {
let mut ledger = DiffEvidenceLedger::new(100);
let sequence = [
DiffRegime::StableFrame,
DiffRegime::BurstyChange,
DiffRegime::StableFrame,
DiffRegime::BurstyChange,
DiffRegime::StableFrame,
];
for (i, ®ime) in sequence.iter().enumerate() {
ledger.record(make_record(i as u64, regime));
}
assert_eq!(ledger.transition_count(), 4);
let transitions: Vec<(DiffRegime, DiffRegime)> = ledger
.transitions()
.map(|t| (t.from_regime, t.to_regime))
.collect();
assert_eq!(
transitions,
vec![
(DiffRegime::StableFrame, DiffRegime::BurstyChange),
(DiffRegime::BurstyChange, DiffRegime::StableFrame),
(DiffRegime::StableFrame, DiffRegime::BurstyChange),
(DiffRegime::BurstyChange, DiffRegime::StableFrame),
]
);
}
#[test]
fn contract_full_lifecycle() {
let mut ledger = DiffEvidenceLedger::new(100);
let lifecycle = [
(0, DiffRegime::StableFrame),
(1, DiffRegime::StableFrame),
(2, DiffRegime::BurstyChange),
(3, DiffRegime::BurstyChange),
(4, DiffRegime::ResizeRegime),
(5, DiffRegime::StableFrame),
(6, DiffRegime::StableFrame),
(7, DiffRegime::DegradedTerminal),
(8, DiffRegime::DegradedTerminal),
(9, DiffRegime::StableFrame),
];
for &(frame, regime) in &lifecycle {
ledger.record(make_record(frame, regime));
}
assert_eq!(ledger.len(), 10);
assert_eq!(ledger.transition_count(), 5);
assert_eq!(ledger.current_regime(), DiffRegime::StableFrame);
for t in ledger.transitions() {
assert!(t.frame_id <= 9);
assert_ne!(t.from_regime, t.to_regime);
}
}
}