made_core/entities/
statistics.rs1use std::collections::BTreeMap;
8
9use serde::{Deserialize, Serialize};
10
11use crate::value_objects::{DurationMs, Specialty};
12
13#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
19pub struct Statistics {
20 total_deliberations: u64,
21 total_orchestrations: u64,
22 total_duration: DurationMs,
23 per_specialty: BTreeMap<Specialty, u64>,
24}
25
26impl Statistics {
27 #[must_use]
28 pub fn new() -> Self {
29 Self::default()
30 }
31
32 #[must_use]
39 pub fn from_counters(
40 total_deliberations: u64,
41 total_orchestrations: u64,
42 total_duration: DurationMs,
43 per_specialty: BTreeMap<Specialty, u64>,
44 ) -> Self {
45 Self {
46 total_deliberations,
47 total_orchestrations,
48 total_duration,
49 per_specialty,
50 }
51 }
52
53 pub fn record_deliberation(&mut self, specialty: &Specialty, duration: DurationMs) {
55 self.total_deliberations = self.total_deliberations.saturating_add(1);
56 self.total_duration = self.total_duration.saturating_add(duration);
57 let entry = self.per_specialty.entry(specialty.clone()).or_insert(0);
58 *entry = entry.saturating_add(1);
59 }
60
61 pub fn record_orchestration(&mut self, duration: DurationMs) {
63 self.total_orchestrations = self.total_orchestrations.saturating_add(1);
64 self.total_duration = self.total_duration.saturating_add(duration);
65 }
66
67 pub fn reset(&mut self) {
69 *self = Self::default();
70 }
71
72 #[must_use]
73 pub fn total_deliberations(&self) -> u64 {
74 self.total_deliberations
75 }
76
77 #[must_use]
78 pub fn total_orchestrations(&self) -> u64 {
79 self.total_orchestrations
80 }
81
82 #[must_use]
83 pub fn total_duration(&self) -> DurationMs {
84 self.total_duration
85 }
86
87 #[must_use]
88 pub fn per_specialty(&self) -> &BTreeMap<Specialty, u64> {
89 &self.per_specialty
90 }
91
92 #[must_use]
95 pub fn average_duration_ms(&self) -> f64 {
96 let ops = self.total_deliberations + self.total_orchestrations;
97 if ops == 0 {
98 0.0
99 } else {
100 self.total_duration.get() as f64 / ops as f64
101 }
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 fn sp(s: &str) -> Specialty {
110 Specialty::new(s).unwrap()
111 }
112
113 #[test]
114 fn fresh_stats_are_zero() {
115 let s = Statistics::new();
116 assert_eq!(s.total_deliberations(), 0);
117 assert_eq!(s.total_orchestrations(), 0);
118 assert_eq!(s.total_duration(), DurationMs::ZERO);
119 assert_eq!(s.average_duration_ms(), 0.0);
120 assert!(s.per_specialty().is_empty());
121 }
122
123 #[test]
124 fn record_deliberation_advances_counters() {
125 let mut s = Statistics::new();
126 s.record_deliberation(&sp("triage"), DurationMs::from_millis(100));
127 s.record_deliberation(&sp("triage"), DurationMs::from_millis(50));
128 s.record_deliberation(&sp("reviewer"), DurationMs::from_millis(200));
129
130 assert_eq!(s.total_deliberations(), 3);
131 assert_eq!(s.total_duration(), DurationMs::from_millis(350));
132 assert_eq!(s.per_specialty().get(&sp("triage")).copied(), Some(2));
133 assert_eq!(s.per_specialty().get(&sp("reviewer")).copied(), Some(1));
134 }
135
136 #[test]
137 fn record_orchestration_advances_counters_but_not_specialty_map() {
138 let mut s = Statistics::new();
139 s.record_orchestration(DurationMs::from_millis(300));
140 assert_eq!(s.total_orchestrations(), 1);
141 assert_eq!(s.total_duration(), DurationMs::from_millis(300));
142 assert!(s.per_specialty().is_empty());
143 }
144
145 #[test]
146 fn average_duration_divides_over_all_ops() {
147 let mut s = Statistics::new();
148 s.record_deliberation(&sp("a"), DurationMs::from_millis(100));
149 s.record_orchestration(DurationMs::from_millis(200));
150 assert_eq!(s.average_duration_ms(), 150.0);
152 }
153
154 #[test]
155 fn reset_clears_state() {
156 let mut s = Statistics::new();
157 s.record_deliberation(&sp("x"), DurationMs::from_millis(10));
158 s.reset();
159 assert_eq!(s, Statistics::default());
160 }
161
162 #[test]
163 fn saturating_counters_do_not_overflow() {
164 let mut s = Statistics::new();
165 s.total_deliberations = u64::MAX;
166 s.total_duration = DurationMs::from_millis(u64::MAX);
167 s.record_deliberation(&sp("x"), DurationMs::from_millis(42));
168 assert_eq!(s.total_deliberations(), u64::MAX);
169 assert_eq!(s.total_duration().get(), u64::MAX);
170 }
171}