Skip to main content

constraint_crdt/
state.rs

1//! # Constraint State — Composite CRDT
2//!
3//! The top-level state of a constraint satisfaction system as a single CRDT.
4//! Combines all sub-CRDTs into one mergeable unit.
5
6use crate::merge::Merge;
7use crate::counter::ConstraintGCounter;
8use crate::orset::ConstraintORSet;
9use crate::eisenstein::EisensteinRegister;
10use serde::{Deserialize, Serialize};
11use std::fmt;
12
13/// The complete constraint state of a fleet node, mergeable without coordination.
14///
15/// This is the key data structure: each node maintains one `ConstraintState`,
16/// and periodically merges with other nodes. The result is always consistent.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ConstraintState {
19    /// Node identifier
20    pub node_id: String,
21    /// Active constraints (OR-Set)
22    pub constraints: ConstraintORSet,
23    /// Aggregate metrics (G-Counter)
24    pub metrics: ConstraintGCounter,
25    /// Current position in constraint space
26    pub position: EisensteinRegister,
27    /// State version (increments on each local mutation)
28    pub version: u64,
29}
30
31impl ConstraintState {
32    pub fn new(node_id: &str) -> Self {
33        Self {
34            node_id: node_id.to_string(),
35            constraints: ConstraintORSet::new(),
36            metrics: ConstraintGCounter::new(),
37            position: EisensteinRegister::new((0, 0), node_id),
38            version: 0,
39        }
40    }
41
42    /// Add a constraint
43    pub fn add_constraint(&mut self, id: &str) {
44        self.constraints.add(id, &self.node_id);
45        self.version += 1;
46    }
47
48    /// Remove a constraint
49    pub fn remove_constraint(&mut self, id: &str) {
50        self.constraints.remove(id);
51        self.version += 1;
52    }
53
54    /// Record satisfied constraints
55    pub fn record_satisfied(&mut self, count: u64) {
56        self.metrics.record_satisfied(&self.node_id, count);
57        self.version += 1;
58    }
59
60    /// Record violations
61    pub fn record_violations(&mut self, count: u64) {
62        self.metrics.record_violations(&self.node_id, count);
63        self.version += 1;
64    }
65
66    /// Update lattice position
67    pub fn update_position(&mut self, pos: (i32, i32)) {
68        self.position.update(pos, &self.node_id);
69        self.version += 1;
70    }
71
72    /// Satisfaction rate (0.0 - 1.0)
73    pub fn satisfaction_rate(&self) -> f64 {
74        self.metrics.satisfaction_rate()
75    }
76
77    /// Number of active constraints
78    pub fn active_constraint_count(&self) -> usize {
79        self.constraints.len()
80    }
81
82    /// Serialize full state
83    pub fn to_json(&self) -> String {
84        serde_json::to_string_pretty(self).unwrap_or_default()
85    }
86}
87
88impl Merge for ConstraintState {
89    fn merge(&mut self, other: &Self) {
90        self.constraints.merge(&other.constraints);
91        self.metrics.merge(&other.metrics);
92        self.position.merge(&other.position);
93        // Version: take max, but don't increment (idempotence)
94        self.version = self.version.max(other.version);
95    }
96}
97
98impl fmt::Display for ConstraintState {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        write!(f, "ConstraintState(node={}, v={}, {} active, {:.1}% satisfied, pos={})",
101            self.node_id, self.version,
102            self.active_constraint_count(),
103            self.satisfaction_rate() * 100.0,
104            self.position)
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::merge::laws;
112
113    #[test]
114    fn test_state_creation() {
115        let s = ConstraintState::new("forgemaster");
116        assert_eq!(s.node_id, "forgemaster");
117        assert_eq!(s.version, 0);
118        assert_eq!(s.active_constraint_count(), 0);
119    }
120
121    #[test]
122    fn test_add_remove_constraints() {
123        let mut s = ConstraintState::new("a");
124        s.add_constraint("bounds");
125        s.add_constraint("norm");
126        assert_eq!(s.active_constraint_count(), 2);
127
128        s.remove_constraint("bounds");
129        assert_eq!(s.active_constraint_count(), 1);
130        assert!(s.constraints.contains("norm"));
131    }
132
133    #[test]
134    fn test_merge_two_nodes() {
135        let mut a = ConstraintState::new("forgemaster");
136        a.add_constraint("bounds");
137        a.add_constraint("norm");
138        a.record_satisfied(1000);
139        a.record_violations(5);
140
141        let mut b = ConstraintState::new("oracle1");
142        b.add_constraint("holonomy");
143        b.record_satisfied(2000);
144        b.record_violations(10);
145
146        let merged = a.merged(&b);
147
148        // All constraints present
149        assert!(merged.constraints.contains("bounds"));
150        assert!(merged.constraints.contains("norm"));
151        assert!(merged.constraints.contains("holonomy"));
152
153        // Metrics aggregated
154        assert_eq!(merged.metrics.total_satisfied(), 3000);
155        assert_eq!(merged.metrics.total_violations(), 15);
156
157        // Version takes max
158        assert!(merged.version >= a.version);
159    }
160
161    #[test]
162    fn test_merge_commutative() {
163        let mut a = ConstraintState::new("a");
164        a.add_constraint("c1");
165        a.record_satisfied(100);
166
167        let mut b = ConstraintState::new("b");
168        b.add_constraint("c2");
169        b.record_satisfied(200);
170
171        assert!(laws::check_commutative(&a, &b));
172    }
173
174    #[test]
175    fn test_merge_associative() {
176        let mut a = ConstraintState::new("a");
177        a.add_constraint("c1");
178        let mut b = ConstraintState::new("b");
179        b.add_constraint("c2");
180        let mut c = ConstraintState::new("c");
181        c.add_constraint("c3");
182        assert!(laws::check_associative(&a, &b, &c));
183    }
184
185    #[test]
186    fn test_merge_idempotent() {
187        let mut a = ConstraintState::new("a");
188        a.add_constraint("c1");
189        a.record_satisfied(100);
190        assert!(laws::check_idempotent(&a));
191    }
192
193    #[test]
194    fn test_satisfaction_rate() {
195        let mut s = ConstraintState::new("a");
196        s.record_satisfied(950);
197        s.record_violations(50);
198        assert!((s.satisfaction_rate() - 0.95).abs() < 0.01);
199    }
200
201    #[test]
202    fn test_json_roundtrip() {
203        let mut s = ConstraintState::new("test");
204        s.add_constraint("c1");
205        s.record_satisfied(100);
206        let json = s.to_json();
207        assert!(json.contains("test"));
208        assert!(json.contains("c1"));
209    }
210}
211
212impl PartialEq for ConstraintState {
213    fn eq(&self, other: &Self) -> bool {
214        // CRDT semantics: equality is about the data, not the node_id
215        self.constraints == other.constraints
216            && self.metrics == other.metrics
217            && self.position == other.position
218            && self.version == other.version
219    }
220}