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 update_callback: Arc<dyn Fn(DateTime<Utc>, DateTime<Utc>) -> Vec<String> + Send + Sync>,
22}
23
24impl LifecycleTimeManager {
25 pub fn new<F>(update_callback: F) -> Self
30 where
31 F: Fn(DateTime<Utc>, DateTime<Utc>) -> Vec<String> + Send + Sync + 'static,
32 {
33 Self {
34 update_callback: Arc::new(update_callback),
35 }
36 }
37
38 pub fn register_with_clock(&self) {
42 if let Some(clock) = get_global_clock() {
43 self.register_with_clock_instance(&clock);
44 } else {
45 warn!("No global virtual clock found, lifecycle time manager not registered");
46 }
47 }
48
49 pub fn register_with_clock_instance(&self, clock: &VirtualClock) {
53 let callback = self.update_callback.clone();
54 clock.on_time_change(move |old_time, new_time| {
55 debug!("Time changed from {} to {}, updating persona lifecycles", old_time, new_time);
56 let updated = callback(old_time, new_time);
57 if !updated.is_empty() {
58 info!("Updated {} persona lifecycle states: {:?}", updated.len(), updated);
59 }
60 });
61 info!("LifecycleTimeManager registered with virtual clock");
62 }
63}
64
65pub fn check_and_update_lifecycle_transitions(
74 lifecycle: &mut PersonaLifecycle,
75 current_time: DateTime<Utc>,
76) -> bool {
77 let old_state = lifecycle.current_state;
78 let elapsed = current_time - lifecycle.state_entered_at;
79
80 for rule in &lifecycle.transition_rules {
82 if let Some(after_days) = rule.after_days {
84 let required_duration = chrono::Duration::days(after_days as i64);
85 if elapsed < required_duration {
86 continue; }
88 }
89
90 if let Some(condition) = &rule.condition {
92 if !evaluate_lifecycle_condition(condition, &lifecycle.metadata) {
93 debug!(
94 "Condition '{}' not met for persona {}, skipping transition",
95 condition, lifecycle.persona_id
96 );
97 continue;
98 }
99 }
100
101 lifecycle.current_state = rule.to;
103 lifecycle.state_entered_at = current_time;
104 lifecycle.state_history.push((current_time, rule.to));
105
106 info!(
107 "Persona {} lifecycle transitioned: {:?} -> {:?}",
108 lifecycle.persona_id, old_state, rule.to
109 );
110
111 return true; }
113
114 false }
116
117fn evaluate_lifecycle_condition(
128 condition: &str,
129 metadata: &std::collections::HashMap<String, serde_json::Value>,
130) -> bool {
131 let expr = condition.trim();
132
133 if expr.eq_ignore_ascii_case("true") {
135 return true;
136 }
137 if expr.eq_ignore_ascii_case("false") {
138 return false;
139 }
140
141 let operators = [">=", "<=", "!=", "==", ">", "<"];
144 let mut parts: Option<(&str, &str, &str)> = None;
145
146 for op in &operators {
147 if let Some(idx) = expr.find(op) {
148 let var = expr[..idx].trim();
149 let val = expr[idx + op.len()..].trim();
150 if !var.is_empty() && !val.is_empty() {
151 parts = Some((var, op, val));
152 break;
153 }
154 }
155 }
156
157 let (variable, operator, threshold_str) = match parts {
158 Some(p) => p,
159 None => {
160 debug!(expression = expr, "Unrecognized condition expression, defaulting to true");
161 return true;
162 }
163 };
164
165 let meta_value = match metadata.get(variable) {
167 Some(val) => val,
168 None => {
169 debug!(
170 variable = variable,
171 "Condition variable not found in persona metadata, defaulting to false"
172 );
173 return false;
174 }
175 };
176
177 if let Some(actual_num) = meta_value.as_f64() {
179 if let Ok(threshold_num) = threshold_str.parse::<f64>() {
180 return match operator {
181 ">" => actual_num > threshold_num,
182 "<" => actual_num < threshold_num,
183 ">=" => actual_num >= threshold_num,
184 "<=" => actual_num <= threshold_num,
185 "==" => (actual_num - threshold_num).abs() < f64::EPSILON,
186 "!=" => (actual_num - threshold_num).abs() >= f64::EPSILON,
187 _ => true,
188 };
189 }
190 }
191
192 let actual_str = match meta_value {
194 serde_json::Value::String(s) => s.as_str(),
195 serde_json::Value::Bool(b) => {
196 if *b {
197 "true"
198 } else {
199 "false"
200 }
201 }
202 _ => {
203 debug!(variable = variable, "Cannot compare non-string/non-numeric metadata value");
204 return false;
205 }
206 };
207
208 match operator {
209 "==" => actual_str == threshold_str,
210 "!=" => actual_str != threshold_str,
211 _ => {
212 debug!(operator = operator, "Operator not supported for string comparison");
213 false
214 }
215 }
216}