Skip to main content

made_core/value_objects/
rounds.rs

1//! [`Rounds`] value object — number of peer-review rounds in a deliberation.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7use crate::error::DomainError;
8
9/// Upper bound for peer-review rounds. Chosen to keep deliberations
10/// bounded and prevent accidental runaway usage of downstream agents.
11pub const MAX_ROUNDS: u32 = 16;
12
13/// Number of peer-review rounds performed during a deliberation.
14///
15/// Zero rounds is a valid configuration (proposals go straight to
16/// validation without critique/revision). The upper bound
17/// [`MAX_ROUNDS`] is enforced to keep deliberations bounded.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(transparent)]
20pub struct Rounds(u32);
21
22impl Rounds {
23    pub const ZERO: Self = Self(0);
24
25    pub fn new(value: u32) -> Result<Self, DomainError> {
26        if value > MAX_ROUNDS {
27            return Err(DomainError::OutOfRange {
28                field: "rounds",
29                value: f64::from(value),
30                min: 0.0,
31                max: f64::from(MAX_ROUNDS),
32            });
33        }
34        Ok(Self(value))
35    }
36
37    #[must_use]
38    pub fn get(self) -> u32 {
39        self.0
40    }
41}
42
43impl Default for Rounds {
44    /// The default mirrors the Python reference implementation
45    /// (`Deliberate(rounds=1)`).
46    fn default() -> Self {
47        Self(1)
48    }
49}
50
51impl fmt::Display for Rounds {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        write!(f, "{}", self.0)
54    }
55}
56
57impl TryFrom<u32> for Rounds {
58    type Error = DomainError;
59    fn try_from(value: u32) -> Result<Self, Self::Error> {
60        Self::new(value)
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn zero_is_allowed() {
70        assert_eq!(Rounds::new(0).unwrap().get(), 0);
71        assert_eq!(Rounds::ZERO.get(), 0);
72    }
73
74    #[test]
75    fn default_is_one() {
76        assert_eq!(Rounds::default().get(), 1);
77    }
78
79    #[test]
80    fn upper_bound_is_max_rounds() {
81        assert!(Rounds::new(MAX_ROUNDS).is_ok());
82    }
83
84    #[test]
85    fn above_upper_bound_is_rejected() {
86        let err = Rounds::new(MAX_ROUNDS + 1).unwrap_err();
87        assert!(matches!(
88            err,
89            DomainError::OutOfRange {
90                field: "rounds",
91                ..
92            }
93        ));
94    }
95
96    #[test]
97    fn display_is_numeric() {
98        assert_eq!(Rounds::new(3).unwrap().to_string(), "3");
99    }
100
101    #[test]
102    fn serde_is_transparent() {
103        assert_eq!(
104            serde_json::to_string(&Rounds::new(4).unwrap()).unwrap(),
105            "4"
106        );
107    }
108}