1use crate::merge::Merge;
7use crate::counter::ConstraintGCounter;
8use crate::orset::ConstraintORSet;
9use crate::eisenstein::EisensteinRegister;
10use serde::{Deserialize, Serialize};
11use std::fmt;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ConstraintState {
19 pub node_id: String,
21 pub constraints: ConstraintORSet,
23 pub metrics: ConstraintGCounter,
25 pub position: EisensteinRegister,
27 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 pub fn add_constraint(&mut self, id: &str) {
44 self.constraints.add(id, &self.node_id);
45 self.version += 1;
46 }
47
48 pub fn remove_constraint(&mut self, id: &str) {
50 self.constraints.remove(id);
51 self.version += 1;
52 }
53
54 pub fn record_satisfied(&mut self, count: u64) {
56 self.metrics.record_satisfied(&self.node_id, count);
57 self.version += 1;
58 }
59
60 pub fn record_violations(&mut self, count: u64) {
62 self.metrics.record_violations(&self.node_id, count);
63 self.version += 1;
64 }
65
66 pub fn update_position(&mut self, pos: (i32, i32)) {
68 self.position.update(pos, &self.node_id);
69 self.version += 1;
70 }
71
72 pub fn satisfaction_rate(&self) -> f64 {
74 self.metrics.satisfaction_rate()
75 }
76
77 pub fn active_constraint_count(&self) -> usize {
79 self.constraints.len()
80 }
81
82 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 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 assert!(merged.constraints.contains("bounds"));
150 assert!(merged.constraints.contains("norm"));
151 assert!(merged.constraints.contains("holonomy"));
152
153 assert_eq!(merged.metrics.total_satisfied(), 3000);
155 assert_eq!(merged.metrics.total_violations(), 15);
156
157 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 self.constraints == other.constraints
216 && self.metrics == other.metrics
217 && self.position == other.position
218 && self.version == other.version
219 }
220}