mockforge_core/
persona_lifecycle_time.rs1use crate::time_travel::{get_global_clock, VirtualClock};
8use chrono::{DateTime, Utc};
9#[cfg(feature = "data")]
10use mockforge_data::persona_lifecycle::PersonaLifecycle;
11use std::sync::Arc;
12use tracing::{debug, info, warn};
13
14pub struct LifecycleTimeManager {
19 #[allow(clippy::type_complexity)]
22 update_callback: Arc<dyn Fn(DateTime<Utc>, DateTime<Utc>) -> Vec<String> + Send + Sync>,
23}
24
25impl LifecycleTimeManager {
26 pub fn new<F>(update_callback: F) -> Self
31 where
32 F: Fn(DateTime<Utc>, DateTime<Utc>) -> Vec<String> + Send + Sync + 'static,
33 {
34 Self {
35 update_callback: Arc::new(update_callback),
36 }
37 }
38
39 pub fn register_with_clock(&self) {
43 if let Some(clock) = get_global_clock() {
44 self.register_with_clock_instance(&clock);
45 } else {
46 warn!("No global virtual clock found, lifecycle time manager not registered");
47 }
48 }
49
50 pub fn register_with_clock_instance(&self, clock: &VirtualClock) {
54 let callback = self.update_callback.clone();
55 clock.on_time_change(move |old_time, new_time| {
56 debug!("Time changed from {} to {}, updating persona lifecycles", old_time, new_time);
57 let updated = callback(old_time, new_time);
58 if !updated.is_empty() {
59 info!("Updated {} persona lifecycle states: {:?}", updated.len(), updated);
60 }
61 });
62 info!("LifecycleTimeManager registered with virtual clock");
63 }
64}
65
66pub fn check_and_update_lifecycle_transitions(
75 lifecycle: &mut PersonaLifecycle,
76 current_time: DateTime<Utc>,
77) -> bool {
78 let old_state = lifecycle.current_state;
79 let elapsed = current_time - lifecycle.state_entered_at;
80
81 for rule in &lifecycle.transition_rules {
83 if let Some(after_days) = rule.after_days {
85 let required_duration = chrono::Duration::days(after_days as i64);
86 if elapsed < required_duration {
87 continue; }
89 }
90
91 if let Some(condition) = &rule.condition {
93 if !evaluate_lifecycle_condition(condition, &lifecycle.metadata) {
94 debug!(
95 "Condition '{}' not met for persona {}, skipping transition",
96 condition, lifecycle.persona_id
97 );
98 continue;
99 }
100 }
101
102 lifecycle.current_state = rule.to;
104 lifecycle.state_entered_at = current_time;
105 lifecycle.state_history.push((current_time, rule.to));
106
107 info!(
108 "Persona {} lifecycle transitioned: {:?} -> {:?}",
109 lifecycle.persona_id, old_state, rule.to
110 );
111
112 return true; }
114
115 false }
117
118fn evaluate_lifecycle_condition(
129 condition: &str,
130 metadata: &std::collections::HashMap<String, serde_json::Value>,
131) -> bool {
132 let expr = condition.trim();
133
134 if expr.eq_ignore_ascii_case("true") {
136 return true;
137 }
138 if expr.eq_ignore_ascii_case("false") {
139 return false;
140 }
141
142 let operators = [">=", "<=", "!=", "==", ">", "<"];
146 let mut parts: Option<(&str, &str, &str)> = None;
147
148 for op in &operators {
149 if let Some(idx) = expr.find(op) {
150 let var = expr[..idx].trim();
151 let val = expr[idx + op.len()..].trim();
152 if !var.is_empty() && !val.is_empty() {
153 parts = Some((var, op, val));
154 break;
155 }
156 }
157 }
158
159 if parts.is_none() {
161 let word_operators = [" contains ", " starts_with ", " ends_with "];
162 for op in &word_operators {
163 if let Some(idx) = expr.find(op) {
164 let var = expr[..idx].trim();
165 let val = expr[idx + op.len()..].trim();
166 if !var.is_empty() && !val.is_empty() {
167 parts = Some((var, op.trim(), val));
168 break;
169 }
170 }
171 }
172 }
173
174 let (variable, operator, threshold_str) = match parts {
175 Some(p) => p,
176 None => {
177 debug!(expression = expr, "Unrecognized condition expression, defaulting to true");
178 return true;
179 }
180 };
181
182 let meta_value = match metadata.get(variable) {
184 Some(val) => val,
185 None => {
186 debug!(
187 variable = variable,
188 "Condition variable not found in persona metadata, defaulting to false"
189 );
190 return false;
191 }
192 };
193
194 if let Some(actual_num) = meta_value.as_f64() {
196 if let Ok(threshold_num) = threshold_str.parse::<f64>() {
197 return match operator {
198 ">" => actual_num > threshold_num,
199 "<" => actual_num < threshold_num,
200 ">=" => actual_num >= threshold_num,
201 "<=" => actual_num <= threshold_num,
202 "==" => (actual_num - threshold_num).abs() < f64::EPSILON,
203 "!=" => (actual_num - threshold_num).abs() >= f64::EPSILON,
204 _ => true,
205 };
206 }
207 }
208
209 let actual_str = match meta_value {
211 serde_json::Value::String(s) => s.as_str(),
212 serde_json::Value::Bool(b) => {
213 if *b {
214 "true"
215 } else {
216 "false"
217 }
218 }
219 _ => {
220 debug!(variable = variable, "Cannot compare non-string/non-numeric metadata value");
221 return false;
222 }
223 };
224
225 match operator {
226 "==" => actual_str == threshold_str,
227 "!=" => actual_str != threshold_str,
228 ">" => actual_str > threshold_str,
229 "<" => actual_str < threshold_str,
230 ">=" => actual_str >= threshold_str,
231 "<=" => actual_str <= threshold_str,
232 "contains" => actual_str.contains(threshold_str),
233 "starts_with" => actual_str.starts_with(threshold_str),
234 "ends_with" => actual_str.ends_with(threshold_str),
235 _ => {
236 debug!(operator = operator, "Operator not supported for string comparison");
237 false
238 }
239 }
240}