1use super::{
7 Comparator, PolicyViolation, ReplayError, ReplayMode, ReplayPolicy, ReplayResult, ReplayStats,
8 ReplayStatus,
9};
10use crate::models::DecisionSnapshot;
11use crate::storage::sync::SyncStorageBackend;
12use std::time::Instant;
13
14pub struct SyncReplayEngine<S: SyncStorageBackend> {
16 storage: S,
17 default_mode: ReplayMode,
18}
19
20impl<S: SyncStorageBackend> SyncReplayEngine<S> {
21 pub fn new(storage: S) -> Self {
23 Self {
24 storage,
25 default_mode: ReplayMode::Tolerant,
26 }
27 }
28
29 pub fn with_mode(storage: S, mode: ReplayMode) -> Self {
31 Self {
32 storage,
33 default_mode: mode,
34 }
35 }
36
37 pub fn default_mode(&self) -> ReplayMode {
39 self.default_mode.clone()
40 }
41
42 pub fn replay(
44 &self,
45 snapshot_id: &str,
46 mode: Option<ReplayMode>,
47 _context_overrides: Option<serde_json::Value>,
48 ) -> Result<ReplayResult, ReplayError> {
49 let start_time = Instant::now();
50 let replay_mode = mode.unwrap_or_else(|| self.default_mode());
51
52 let original_snapshot = self
53 .storage
54 .load_decision(snapshot_id)
55 .map_err(|e| ReplayError::StorageError(e.to_string()))?;
56
57 match replay_mode {
58 ReplayMode::ValidationOnly => {
59 Ok(ReplayResult {
61 status: ReplayStatus::Success,
62 original_snapshot,
63 replay_output: None,
64 outputs_match: true, diff: None,
66 policy_violations: Vec::new(),
67 execution_time_ms: start_time.elapsed().as_millis() as f64,
68 })
69 }
70 ReplayMode::Strict | ReplayMode::Tolerant => {
71 let simulated_output = simulate_execution(&original_snapshot);
74 let outputs_match =
75 compare_outputs(&original_snapshot, &simulated_output, &replay_mode);
76
77 Ok(ReplayResult {
78 status: if outputs_match {
79 ReplayStatus::Success
80 } else {
81 ReplayStatus::Failed
82 },
83 original_snapshot,
84 replay_output: Some(simulated_output),
85 outputs_match,
86 diff: None, policy_violations: Vec::new(),
88 execution_time_ms: start_time.elapsed().as_millis() as f64,
89 })
90 }
91 }
92 }
93
94 pub fn replay_with_policy(
96 &self,
97 snapshot_id: &str,
98 policy: &ReplayPolicy,
99 mode: Option<ReplayMode>,
100 ) -> Result<ReplayResult, ReplayError> {
101 let mut result = self.replay(snapshot_id, mode, None)?;
102
103 let violations = self.validate(snapshot_id, policy)?;
105 result.policy_violations = violations;
106
107 if !result.policy_violations.is_empty() {
108 result.status = ReplayStatus::Failed;
109 }
110
111 Ok(result)
112 }
113
114 pub fn validate(
116 &self,
117 snapshot_id: &str,
118 policy: &ReplayPolicy,
119 ) -> Result<Vec<PolicyViolation>, ReplayError> {
120 let snapshot = self
121 .storage
122 .load_decision(snapshot_id)
123 .map_err(|e| ReplayError::StorageError(e.to_string()))?;
124
125 let mut violations = Vec::new();
126
127 for rule in &policy.rules {
129 match rule.comparator {
130 Comparator::ExactMatch => {
131 if rule.field == "function_name" {
134 if snapshot.function_name.is_empty() {
136 violations.push(PolicyViolation {
137 rule_name: format!("exact_match_{}", rule.field),
138 field: rule.field.clone(),
139 expected: "non-empty function name".to_string(),
140 actual: "empty".to_string(),
141 message: "Function name cannot be empty".to_string(),
142 });
143 }
144 }
145 }
146 Comparator::SemanticSimilarity => {
147 if rule.field == "output" && snapshot.outputs.is_empty() {
150 violations.push(PolicyViolation {
151 rule_name: format!("similarity_{}", rule.field),
152 field: rule.field.clone(),
153 expected: "at least one output".to_string(),
154 actual: "no outputs".to_string(),
155 message: "At least one output is required".to_string(),
156 });
157 }
158 }
159 Comparator::MaxIncreasePercent => {
160 }
163 Comparator::MaxDecreasePercent => {
164 }
167 Comparator::WithinRange => {
168 }
171 }
172 }
173
174 Ok(violations)
175 }
176
177 pub fn get_replay_stats(&self, snapshot_ids: &[String]) -> Result<ReplayStats, ReplayError> {
179 let mut total_replays = 0;
180 let mut successful_replays = 0;
181 let mut failed_replays = 0;
182 let mut exact_matches = 0;
183 let mut mismatches = 0;
184 let mut total_execution_time_ms = 0.0;
185
186 for snapshot_id in snapshot_ids {
187 match self.replay(snapshot_id, None, None) {
188 Ok(result) => {
189 total_replays += 1;
190 total_execution_time_ms += result.execution_time_ms;
191
192 match result.status {
193 ReplayStatus::Success => {
194 successful_replays += 1;
195 if result.outputs_match {
196 exact_matches += 1;
197 } else {
198 mismatches += 1;
199 }
200 }
201 _ => {
202 failed_replays += 1;
203 mismatches += 1;
204 }
205 }
206 }
207 Err(_) => {
208 total_replays += 1;
209 failed_replays += 1;
210 mismatches += 1;
211 }
212 }
213 }
214
215 let average_execution_time_ms = if total_replays > 0 {
216 total_execution_time_ms / total_replays as f64
217 } else {
218 0.0
219 };
220
221 Ok(ReplayStats {
222 total_replays,
223 successful_replays,
224 failed_replays,
225 exact_matches,
226 mismatches,
227 average_execution_time_ms,
228 total_execution_time_ms,
229 })
230 }
231}
232
233impl<S: SyncStorageBackend> Clone for SyncReplayEngine<S>
234where
235 S: Clone,
236{
237 fn clone(&self) -> Self {
238 Self {
239 storage: self.storage.clone(),
240 default_mode: self.default_mode.clone(),
241 }
242 }
243}
244
245fn simulate_execution(decision: &DecisionSnapshot) -> serde_json::Value {
248 if let Some(output) = decision.outputs.first() {
250 output.value.clone()
251 } else {
252 serde_json::Value::Null
253 }
254}
255
256fn compare_outputs(
258 decision: &DecisionSnapshot,
259 simulated_output: &serde_json::Value,
260 mode: &ReplayMode,
261) -> bool {
262 if let Some(original_output) = decision.outputs.first() {
263 match mode {
264 ReplayMode::Strict => {
265 original_output.value == *simulated_output
267 }
268 ReplayMode::Tolerant => {
269 if original_output.value == *simulated_output {
271 true
272 } else {
273 false
275 }
276 }
277 ReplayMode::ValidationOnly => {
278 true
280 }
281 }
282 } else {
283 simulated_output.is_null()
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use crate::models::*;
291 use crate::storage::sync::MemoryStorageBackend;
292 use serde_json::json;
293
294 fn create_test_decision() -> DecisionSnapshot {
295 let input = Input::new("test_input", json!("value"), "string");
296 let output = Output::new("test_output", json!("result"), "string");
297 let model_params = ModelParameters::new("gpt-4");
298
299 DecisionSnapshot::new("test_function")
300 .with_module("test_module")
301 .add_input(input)
302 .add_output(output)
303 .with_model_parameters(model_params)
304 .add_tag("env", "test")
305 }
306
307 #[test]
308 fn test_sync_replay_validation_only() {
309 let storage = MemoryStorageBackend::new();
310 let engine = SyncReplayEngine::new(storage);
311
312 let decision = create_test_decision();
313 let decision_id = engine.storage.save_decision(&decision).unwrap();
314
315 let result = engine
316 .replay(&decision_id, Some(ReplayMode::ValidationOnly), None)
317 .unwrap();
318
319 assert_eq!(result.status, ReplayStatus::Success);
320 assert!(result.outputs_match);
321 assert!(result.replay_output.is_none());
322 }
323
324 #[test]
325 fn test_sync_replay_tolerant_mode() {
326 let storage = MemoryStorageBackend::new();
327 let engine = SyncReplayEngine::new(storage);
328
329 let decision = create_test_decision();
330 let decision_id = engine.storage.save_decision(&decision).unwrap();
331
332 let result = engine
333 .replay(&decision_id, Some(ReplayMode::Tolerant), None)
334 .unwrap();
335
336 assert_eq!(result.status, ReplayStatus::Success);
337 assert!(result.replay_output.is_some());
338 }
339
340 #[test]
341 fn test_sync_replay_stats() {
342 let storage = MemoryStorageBackend::new();
343 let engine = SyncReplayEngine::new(storage);
344
345 let decision1 = create_test_decision();
346 let decision2 = create_test_decision();
347
348 let id1 = engine.storage.save_decision(&decision1).unwrap();
349 let id2 = engine.storage.save_decision(&decision2).unwrap();
350
351 let stats = engine.get_replay_stats(&[id1, id2]).unwrap();
352
353 assert_eq!(stats.total_replays, 2);
354 assert!(stats.total_execution_time_ms >= 0.0);
355 assert!(stats.average_execution_time_ms >= 0.0);
356 }
357
358 #[test]
359 fn test_sync_replay_policy_validation() {
360 let storage = MemoryStorageBackend::new();
361 let engine = SyncReplayEngine::new(storage);
362
363 let policy = ReplayPolicy::new("test_policy".to_string())
364 .with_exact_match("function_name".to_string());
365
366 let decision = create_test_decision();
367 let decision_id = engine.storage.save_decision(&decision).unwrap();
368
369 let violations = engine.validate(&decision_id, &policy).unwrap();
370
371 assert!(violations.is_empty());
373 }
374}