Skip to main content

mockforge_intelligence/intelligent_behavior/
mutation_analyzer.rs

1//! Request mutation detection and context analysis
2//!
3//! This module analyzes changes between requests to detect mutations, identify
4//! validation issues, and infer appropriate response types based on context.
5
6use super::context::StatefulAiContext;
7use super::types::BehaviorRules;
8use mockforge_foundation::Result;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12/// Analysis of changes between requests
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct MutationAnalysis {
15    /// Fields that were changed
16    pub changed_fields: Vec<FieldChange>,
17    /// Fields that were added
18    pub added_fields: Vec<String>,
19    /// Fields that were removed
20    pub removed_fields: Vec<String>,
21    /// Detected validation issues
22    pub validation_issues: Vec<ValidationIssue>,
23    /// Overall mutation type
24    pub mutation_type: MutationType,
25    /// Confidence score (0.0 to 1.0)
26    pub confidence: f64,
27}
28
29/// Change detected in a field
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct FieldChange {
32    /// Field name
33    pub field: String,
34    /// Previous value
35    pub previous_value: Value,
36    /// New value
37    pub new_value: Value,
38    /// Change type
39    pub change_type: ChangeType,
40}
41
42/// Type of change detected
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
44#[serde(rename_all = "lowercase")]
45pub enum ChangeType {
46    /// Value was modified
47    Modified,
48    /// Value type changed
49    TypeChanged,
50    /// Value was cleared (set to null or empty)
51    Cleared,
52    /// Value was set (from null/undefined to a value)
53    Set,
54}
55
56/// Type of mutation detected
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
58#[serde(rename_all = "lowercase")]
59pub enum MutationType {
60    /// Create operation (new resource)
61    Create,
62    /// Update operation (existing resource modified)
63    Update,
64    /// Delete operation (resource removed)
65    Delete,
66    /// Partial update (only some fields changed)
67    PartialUpdate,
68    /// No significant changes
69    NoChange,
70    /// Invalid mutation (validation errors)
71    Invalid,
72}
73
74/// Validation issue detected from mutation
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct ValidationIssue {
77    /// Field that has the issue (None for object-level issues)
78    pub field: Option<String>,
79    /// Type of validation issue
80    pub issue_type: ValidationIssueType,
81    /// Severity of the issue
82    pub severity: ValidationSeverity,
83    /// Context data for the issue
84    pub context: Value,
85    /// Suggested error message
86    pub error_message: String,
87}
88
89/// Type of validation issue
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
91#[serde(rename_all = "lowercase")]
92pub enum ValidationIssueType {
93    /// Required field is missing
94    Required,
95    /// Invalid format
96    Format,
97    /// Value too short
98    MinLength,
99    /// Value too long
100    MaxLength,
101    /// Invalid pattern
102    Pattern,
103    /// Value out of range
104    Range,
105    /// Invalid type
106    Type,
107    /// Custom validation error
108    Custom,
109}
110
111/// Severity of validation issue
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
113#[serde(rename_all = "lowercase")]
114pub enum ValidationSeverity {
115    /// Info (non-blocking)
116    Info,
117    /// Warning (may cause issues)
118    Warning,
119    /// Error (blocks operation)
120    Error,
121    /// Critical (severe error)
122    Critical,
123}
124
125/// Response type inferred from mutation
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
127#[serde(rename_all = "lowercase")]
128pub enum ResponseType {
129    /// Success response (200/201)
130    Success,
131    /// Validation error (400)
132    ValidationError,
133    /// Not found (404)
134    NotFound,
135    /// Conflict (409)
136    Conflict,
137    /// Unauthorized (401)
138    Unauthorized,
139    /// Forbidden (403)
140    Forbidden,
141    /// Server error (500)
142    ServerError,
143}
144
145/// Mutation analyzer
146pub struct MutationAnalyzer {
147    /// Behavior rules to use for validation
148    rules: Option<BehaviorRules>,
149}
150
151impl MutationAnalyzer {
152    /// Create a new mutation analyzer
153    pub fn new() -> Self {
154        Self { rules: None }
155    }
156
157    /// Create with behavior rules
158    pub fn with_rules(mut self, rules: BehaviorRules) -> Self {
159        self.rules = Some(rules);
160        self
161    }
162
163    /// Analyze mutation between current and previous request
164    ///
165    /// Compares the current request with the previous request in the session
166    /// to detect changes and identify potential validation issues.
167    pub async fn analyze_mutation(
168        &self,
169        current: &Value,
170        previous: Option<&Value>,
171        _context: &StatefulAiContext,
172    ) -> Result<MutationAnalysis> {
173        let mut changed_fields = Vec::new();
174        let mut added_fields = Vec::new();
175        let mut removed_fields = Vec::new();
176
177        // If no previous request, this is likely a create operation
178        if previous.is_none() {
179            if let Value::Object(obj) = current {
180                for (key, _value) in obj {
181                    added_fields.push(key.clone());
182                }
183            }
184
185            return Ok(MutationAnalysis {
186                changed_fields,
187                added_fields,
188                removed_fields,
189                validation_issues: Vec::new(),
190                mutation_type: MutationType::Create,
191                confidence: 0.9,
192            });
193        }
194
195        let previous = previous.unwrap();
196
197        // Compare objects
198        if let (Value::Object(current_obj), Value::Object(prev_obj)) = (current, previous) {
199            // Check for changed fields
200            for (key, current_val) in current_obj {
201                if let Some(prev_val) = prev_obj.get(key) {
202                    if current_val != prev_val {
203                        let change_type = self.determine_change_type(prev_val, current_val);
204                        changed_fields.push(FieldChange {
205                            field: key.clone(),
206                            previous_value: prev_val.clone(),
207                            new_value: current_val.clone(),
208                            change_type,
209                        });
210                    }
211                } else {
212                    // Field was added
213                    added_fields.push(key.clone());
214                }
215            }
216
217            // Check for removed fields
218            for key in prev_obj.keys() {
219                if !current_obj.contains_key(key) {
220                    removed_fields.push(key.clone());
221                }
222            }
223        }
224
225        // Detect validation issues
226        let validation_issues = self
227            .detect_validation_issues(&MutationAnalysis {
228                changed_fields: changed_fields.clone(),
229                added_fields: added_fields.clone(),
230                removed_fields: removed_fields.clone(),
231                validation_issues: Vec::new(),
232                mutation_type: MutationType::NoChange,
233                confidence: 0.0,
234            })
235            .await?;
236
237        // Determine mutation type
238        let mutation_type = self.determine_mutation_type(
239            &changed_fields,
240            &added_fields,
241            &removed_fields,
242            &validation_issues,
243        );
244
245        // Calculate confidence
246        let confidence = self.calculate_confidence(&mutation_type, &validation_issues);
247
248        Ok(MutationAnalysis {
249            changed_fields,
250            added_fields,
251            removed_fields,
252            validation_issues,
253            mutation_type,
254            confidence,
255        })
256    }
257
258    /// Detect validation issues based on mutation and rules
259    pub async fn detect_validation_issues(
260        &self,
261        mutation: &MutationAnalysis,
262    ) -> Result<Vec<ValidationIssue>> {
263        let mut issues = Vec::new();
264
265        // Check for required fields if this is a create operation
266        if mutation.mutation_type == MutationType::Create {
267            // If rules are available, check required fields
268            if let Some(ref rules) = self.rules {
269                for (resource_name, schema) in &rules.schemas {
270                    if let Some(required) = schema.get("required").and_then(|r| r.as_array()) {
271                        for field_name in required {
272                            if let Some(field_str) = field_name.as_str() {
273                                // Check if field is missing in added fields
274                                if !mutation.added_fields.contains(&field_str.to_string()) {
275                                    issues.push(ValidationIssue {
276                                        field: Some(field_str.to_string()),
277                                        issue_type: ValidationIssueType::Required,
278                                        severity: ValidationSeverity::Error,
279                                        context: serde_json::json!({
280                                            "resource": resource_name,
281                                            "field": field_str
282                                        }),
283                                        error_message: format!("Field '{}' is required", field_str),
284                                    });
285                                }
286                            }
287                        }
288                    }
289                }
290            }
291        }
292
293        // Check for invalid field changes
294        for change in &mutation.changed_fields {
295            // Check for type changes
296            if change.change_type == ChangeType::TypeChanged {
297                issues.push(ValidationIssue {
298                    field: Some(change.field.clone()),
299                    issue_type: ValidationIssueType::Type,
300                    severity: ValidationSeverity::Error,
301                    context: serde_json::json!({
302                        "previous_type": self.value_type(&change.previous_value),
303                        "new_type": self.value_type(&change.new_value)
304                    }),
305                    error_message: format!(
306                        "Field '{}' cannot change type from {} to {}",
307                        change.field,
308                        self.value_type(&change.previous_value),
309                        self.value_type(&change.new_value)
310                    ),
311                });
312            }
313
314            // Check for cleared required fields
315            if change.change_type == ChangeType::Cleared {
316                if let Some(ref rules) = self.rules {
317                    // Check if this field is required in any schema
318                    for schema in rules.schemas.values() {
319                        if let Some(required) = schema.get("required").and_then(|r| r.as_array()) {
320                            if required
321                                .iter()
322                                .any(|f| f.as_str().map(|s| s == change.field).unwrap_or(false))
323                            {
324                                issues.push(ValidationIssue {
325                                    field: Some(change.field.clone()),
326                                    issue_type: ValidationIssueType::Required,
327                                    severity: ValidationSeverity::Error,
328                                    context: serde_json::json!({
329                                        "field": change.field
330                                    }),
331                                    error_message: format!(
332                                        "Field '{}' cannot be cleared",
333                                        change.field
334                                    ),
335                                });
336                            }
337                        }
338                    }
339                }
340            }
341        }
342
343        Ok(issues)
344    }
345
346    /// Infer response type from mutation analysis
347    pub fn infer_response_type(
348        &self,
349        mutation: &MutationAnalysis,
350        _context: &StatefulAiContext,
351    ) -> ResponseType {
352        // If there are validation errors, return validation error
353        if !mutation.validation_issues.is_empty() {
354            let has_errors = mutation
355                .validation_issues
356                .iter()
357                .any(|i| i.severity >= ValidationSeverity::Error);
358            if has_errors {
359                return ResponseType::ValidationError;
360            }
361        }
362
363        // Determine based on mutation type
364        match mutation.mutation_type {
365            MutationType::Create => ResponseType::Success,
366            MutationType::Update | MutationType::PartialUpdate => ResponseType::Success,
367            MutationType::Delete => ResponseType::Success,
368            MutationType::Invalid => ResponseType::ValidationError,
369            MutationType::NoChange => ResponseType::Success,
370        }
371    }
372
373    // ===== Private helper methods =====
374
375    /// Determine change type between two values
376    fn determine_change_type(&self, previous: &Value, current: &Value) -> ChangeType {
377        // Check if previous was null/empty and current has value (Set takes precedence)
378        if previous.is_null() || (previous.is_string() && previous.as_str() == Some("")) {
379            return ChangeType::Set;
380        }
381
382        // Check if current is null/empty
383        if current.is_null() || (current.is_string() && current.as_str() == Some("")) {
384            return ChangeType::Cleared;
385        }
386
387        // Check for type change (after null checks)
388        if std::mem::discriminant(previous) != std::mem::discriminant(current) {
389            return ChangeType::TypeChanged;
390        }
391
392        // Otherwise, it's a modification
393        ChangeType::Modified
394    }
395
396    /// Determine mutation type from changes
397    fn determine_mutation_type(
398        &self,
399        changed_fields: &[FieldChange],
400        added_fields: &[String],
401        removed_fields: &[String],
402        validation_issues: &[ValidationIssue],
403    ) -> MutationType {
404        // If there are critical validation issues, it's invalid
405        if validation_issues.iter().any(|i| i.severity == ValidationSeverity::Critical) {
406            return MutationType::Invalid;
407        }
408
409        // If many fields added and few changed, likely create
410        if added_fields.len() > changed_fields.len() && removed_fields.is_empty() {
411            return MutationType::Create;
412        }
413
414        // If fields removed, likely delete or update
415        if !removed_fields.is_empty()
416            && removed_fields.len() > added_fields.len() + changed_fields.len()
417        {
418            return MutationType::Delete;
419        }
420
421        // If fields changed, determine if it's partial or full update
422        if !changed_fields.is_empty() {
423            // If we have added or removed fields along with changes, it's a full update
424            if !added_fields.is_empty() || !removed_fields.is_empty() {
425                return MutationType::Update;
426            }
427            // If a significant portion of fields changed (more than half), it's a full update
428            // For now, treat any update with only changed fields (no adds/removes) as Update
429            // PartialUpdate would be for cases where we're updating a subset of a larger object
430            // Since we don't have the total field count here, default to Update
431            return MutationType::Update;
432        }
433
434        MutationType::NoChange
435    }
436
437    /// Calculate confidence score
438    fn calculate_confidence(
439        &self,
440        mutation_type: &MutationType,
441        validation_issues: &[ValidationIssue],
442    ) -> f64 {
443        let mut confidence = 0.5; // Base confidence
444
445        // Increase confidence for clear mutation types
446        match mutation_type {
447            MutationType::Create | MutationType::Delete => confidence += 0.3,
448            MutationType::Update | MutationType::PartialUpdate => confidence += 0.2,
449            MutationType::Invalid => confidence -= 0.2,
450            MutationType::NoChange => confidence = 0.1,
451        }
452
453        // Decrease confidence if there are validation issues
454        if !validation_issues.is_empty() {
455            confidence -= 0.1 * validation_issues.len() as f64;
456        }
457
458        confidence.clamp(0.0, 1.0)
459    }
460
461    /// Get value type as string
462    fn value_type(&self, value: &Value) -> &str {
463        match value {
464            Value::Null => "null",
465            Value::Bool(_) => "boolean",
466            Value::Number(_) => "number",
467            Value::String(_) => "string",
468            Value::Array(_) => "array",
469            Value::Object(_) => "object",
470        }
471    }
472}
473
474impl Default for MutationAnalyzer {
475    fn default() -> Self {
476        Self::new()
477    }
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483    use serde_json::json;
484
485    #[tokio::test]
486    async fn test_analyze_mutation_create() {
487        let analyzer = MutationAnalyzer::new();
488        let config = super::super::config::IntelligentBehaviorConfig::default();
489        let context = StatefulAiContext::new("test_session", config);
490
491        let current = json!({
492            "name": "Alice",
493            "email": "alice@example.com"
494        });
495
496        let analysis = analyzer.analyze_mutation(&current, None, &context).await.unwrap();
497
498        assert_eq!(analysis.mutation_type, MutationType::Create);
499        assert!(!analysis.added_fields.is_empty());
500    }
501
502    #[tokio::test]
503    async fn test_analyze_mutation_update() {
504        let analyzer = MutationAnalyzer::new();
505        let config = super::super::config::IntelligentBehaviorConfig::default();
506        let context = StatefulAiContext::new("test_session", config);
507
508        let previous = json!({
509            "name": "Alice",
510            "email": "alice@example.com"
511        });
512
513        let current = json!({
514            "name": "Alice Smith",
515            "email": "alice@example.com"
516        });
517
518        let analysis =
519            analyzer.analyze_mutation(&current, Some(&previous), &context).await.unwrap();
520
521        assert_eq!(analysis.mutation_type, MutationType::Update);
522        assert!(!analysis.changed_fields.is_empty());
523    }
524
525    #[tokio::test]
526    async fn test_determine_change_type() {
527        let analyzer = MutationAnalyzer::new();
528
529        let prev = json!("old");
530        let curr = json!("new");
531        assert_eq!(analyzer.determine_change_type(&prev, &curr), ChangeType::Modified);
532
533        let prev = json!(null);
534        let curr = json!("value");
535        assert_eq!(analyzer.determine_change_type(&prev, &curr), ChangeType::Set);
536
537        let prev = json!("value");
538        let curr = json!(null);
539        assert_eq!(analyzer.determine_change_type(&prev, &curr), ChangeType::Cleared);
540    }
541
542    #[tokio::test]
543    async fn test_infer_response_type() {
544        let analyzer = MutationAnalyzer::new();
545        let config = super::super::config::IntelligentBehaviorConfig::default();
546        let context = StatefulAiContext::new("test_session", config);
547
548        let mutation = MutationAnalysis {
549            changed_fields: Vec::new(),
550            added_fields: vec!["name".to_string()],
551            removed_fields: Vec::new(),
552            validation_issues: Vec::new(),
553            mutation_type: MutationType::Create,
554            confidence: 0.9,
555        };
556
557        let response_type = analyzer.infer_response_type(&mutation, &context);
558        assert_eq!(response_type, ResponseType::Success);
559    }
560}