Skip to main content

datasynth_core/models/
intervention.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// The full taxonomy of supported interventions.
5#[derive(Debug, Clone, Serialize, Deserialize)]
6#[serde(tag = "type", rename_all = "snake_case")]
7pub enum InterventionType {
8    /// Entity-level events (vendor default, customer churn, etc.)
9    EntityEvent(EntityEventIntervention),
10    /// Parameter shifts (config value changes)
11    ParameterShift(ParameterShiftIntervention),
12    /// Control failures (effectiveness reduction, bypass)
13    ControlFailure(ControlFailureIntervention),
14    /// Process changes (approval thresholds, automation)
15    ProcessChange(ProcessChangeIntervention),
16    /// Macroeconomic shocks (recession, inflation spike)
17    MacroShock(MacroShockIntervention),
18    /// Regulatory changes (new standards, threshold changes)
19    RegulatoryChange(RegulatoryChangeIntervention),
20    /// Composite bundle of multiple interventions
21    Composite(CompositeIntervention),
22    /// Custom user-defined intervention
23    Custom(CustomIntervention),
24}
25
26// ── Entity Events ──────────────────────────────────
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct EntityEventIntervention {
30    pub subtype: InterventionEntityEvent,
31    pub target: EntityTarget,
32    #[serde(default)]
33    pub parameters: HashMap<String, serde_json::Value>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37#[serde(rename_all = "snake_case")]
38pub enum InterventionEntityEvent {
39    VendorDefault,
40    CustomerChurn,
41    EmployeeDeparture,
42    NewVendorOnboarding,
43    MergerAcquisition,
44    VendorCollusion,
45    CustomerConsolidation,
46    KeyPersonRisk,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct EntityTarget {
51    /// Target by cluster type.
52    pub cluster: Option<String>,
53    /// Target by specific entity IDs.
54    pub entity_ids: Option<Vec<String>>,
55    /// Target by attribute filter (e.g., country = "US").
56    pub filter: Option<HashMap<String, String>>,
57    /// Number of entities to affect (random selection from filter).
58    pub count: Option<u32>,
59    /// Fraction of entities to affect (alternative to count).
60    pub fraction: Option<f64>,
61}
62
63// ── Parameter Shifts ───────────────────────────────
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct ParameterShiftIntervention {
67    /// Dot-path to config parameter.
68    pub target: String,
69    /// Original value (for documentation; auto-filled from config).
70    pub from: Option<serde_json::Value>,
71    /// New value.
72    pub to: serde_json::Value,
73    /// Interpolation method during ramp.
74    #[serde(default)]
75    pub interpolation: InterpolationType,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, Default)]
79#[serde(rename_all = "snake_case")]
80pub enum InterpolationType {
81    #[default]
82    Linear,
83    Exponential,
84    Logistic {
85        steepness: f64,
86    },
87    Step,
88}
89
90// ── Control Failures ───────────────────────────────
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct ControlFailureIntervention {
94    pub subtype: ControlFailureType,
95    /// Control ID (e.g., "C003") or control category.
96    pub control_target: ControlTarget,
97    /// Effectiveness multiplier (0.0 = complete failure, 1.0 = normal).
98    pub severity: f64,
99    /// Whether the failure is detectable by monitoring.
100    #[serde(default)]
101    pub detectable: bool,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105#[serde(rename_all = "snake_case")]
106pub enum ControlFailureType {
107    EffectivenessReduction,
108    CompleteBypass,
109    IntermittentFailure { failure_probability: f64 },
110    DelayedDetection { detection_lag_months: u32 },
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(untagged)]
115pub enum ControlTarget {
116    ById { control_id: String },
117    ByCategory { coso_component: String },
118    ByScope { scope: String },
119    All,
120}
121
122// ── Process Changes ────────────────────────────────
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct ProcessChangeIntervention {
126    pub subtype: ProcessChangeType,
127    #[serde(default)]
128    pub parameters: HashMap<String, serde_json::Value>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132#[serde(rename_all = "snake_case")]
133pub enum ProcessChangeType {
134    ApprovalThresholdChange,
135    NewApprovalLevel,
136    SystemMigration,
137    ProcessAutomation,
138    OutsourcingTransition,
139    PolicyChange,
140    ReorganizationRestructuring,
141}
142
143// ── Macro Shocks ───────────────────────────────────
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct MacroShockIntervention {
147    pub subtype: MacroShockType,
148    /// Severity multiplier (1.0 = standard severity for the shock type).
149    pub severity: f64,
150    /// Named preset (maps to pre-configured parameter bundles).
151    pub preset: Option<String>,
152    /// Override individual macro parameters.
153    #[serde(default)]
154    pub overrides: HashMap<String, f64>,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
158#[serde(rename_all = "snake_case")]
159pub enum MacroShockType {
160    Recession,
161    InflationSpike,
162    CurrencyCrisis,
163    InterestRateShock,
164    CommodityShock,
165    PandemicDisruption,
166    SupplyChainCrisis,
167    CreditCrunch,
168}
169
170// ── Regulatory Changes ─────────────────────────────
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct RegulatoryChangeIntervention {
174    pub subtype: RegulatoryChangeType,
175    #[serde(default)]
176    pub parameters: HashMap<String, serde_json::Value>,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
180#[serde(rename_all = "snake_case")]
181pub enum RegulatoryChangeType {
182    NewStandardAdoption,
183    MaterialityThresholdChange,
184    ReportingRequirementChange,
185    ComplianceThresholdChange,
186    AuditStandardChange,
187    TaxRateChange,
188}
189
190// ── Composite ──────────────────────────────────────
191
192/// Bundles multiple interventions into a named package.
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct CompositeIntervention {
195    pub name: String,
196    pub description: String,
197    /// Child interventions applied together.
198    pub children: Vec<InterventionType>,
199    /// Conflict resolution: first_wins, last_wins, average, error.
200    #[serde(default)]
201    pub conflict_resolution: ConflictResolution,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize, Default)]
205#[serde(rename_all = "snake_case")]
206pub enum ConflictResolution {
207    #[default]
208    FirstWins,
209    LastWins,
210    Average,
211    Error,
212}
213
214// ── Custom ─────────────────────────────────────────
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct CustomIntervention {
218    pub name: String,
219    /// Config path → value mappings.
220    #[serde(default)]
221    pub config_overrides: HashMap<String, serde_json::Value>,
222    /// Causal downstream effects to trigger.
223    #[serde(default)]
224    pub downstream_triggers: Vec<String>,
225}
226
227#[cfg(test)]
228#[allow(clippy::unwrap_used)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_intervention_type_tagged_serde() {
234        let json = r#"{
235            "type": "parameter_shift",
236            "target": "transactions.count",
237            "from": 1000,
238            "to": 2000,
239            "interpolation": "linear"
240        }"#;
241        let intervention: InterventionType = serde_json::from_str(json).unwrap();
242        assert!(matches!(intervention, InterventionType::ParameterShift(_)));
243
244        // Roundtrip
245        let serialized = serde_json::to_string(&intervention).unwrap();
246        let deserialized: InterventionType = serde_json::from_str(&serialized).unwrap();
247        assert!(matches!(deserialized, InterventionType::ParameterShift(_)));
248    }
249
250    #[test]
251    fn test_entity_event_serde() {
252        let json = r#"{
253            "type": "entity_event",
254            "subtype": "vendor_default",
255            "target": {
256                "cluster": "problematic",
257                "count": 5
258            },
259            "parameters": {}
260        }"#;
261        let intervention: InterventionType = serde_json::from_str(json).unwrap();
262        if let InterventionType::EntityEvent(e) = intervention {
263            assert!(matches!(e.subtype, InterventionEntityEvent::VendorDefault));
264            assert_eq!(e.target.cluster, Some("problematic".to_string()));
265            assert_eq!(e.target.count, Some(5));
266        } else {
267            panic!("Expected EntityEvent");
268        }
269    }
270
271    #[test]
272    fn test_macro_shock_serde() {
273        let json = r#"{
274            "type": "macro_shock",
275            "subtype": "recession",
276            "severity": 1.5,
277            "preset": "2008_financial_crisis",
278            "overrides": {"gdp_growth": -0.03}
279        }"#;
280        let intervention: InterventionType = serde_json::from_str(json).unwrap();
281        if let InterventionType::MacroShock(m) = intervention {
282            assert!(matches!(m.subtype, MacroShockType::Recession));
283            assert_eq!(m.severity, 1.5);
284            assert_eq!(m.preset, Some("2008_financial_crisis".to_string()));
285        } else {
286            panic!("Expected MacroShock");
287        }
288    }
289
290    #[test]
291    fn test_control_failure_serde() {
292        let json = r#"{
293            "type": "control_failure",
294            "subtype": "complete_bypass",
295            "control_target": {"control_id": "C003"},
296            "severity": 0.0,
297            "detectable": false
298        }"#;
299        let intervention: InterventionType = serde_json::from_str(json).unwrap();
300        assert!(matches!(intervention, InterventionType::ControlFailure(_)));
301    }
302
303    #[test]
304    fn test_composite_serde() {
305        let json = r#"{
306            "type": "composite",
307            "name": "recession_scenario",
308            "description": "Combined recession effects",
309            "children": [
310                {
311                    "type": "macro_shock",
312                    "subtype": "recession",
313                    "severity": 1.0,
314                    "overrides": {}
315                }
316            ],
317            "conflict_resolution": "first_wins"
318        }"#;
319        let intervention: InterventionType = serde_json::from_str(json).unwrap();
320        if let InterventionType::Composite(c) = intervention {
321            assert_eq!(c.name, "recession_scenario");
322            assert_eq!(c.children.len(), 1);
323        } else {
324            panic!("Expected Composite");
325        }
326    }
327
328    #[test]
329    fn test_conflict_resolution_default() {
330        let cr = ConflictResolution::default();
331        assert!(matches!(cr, ConflictResolution::FirstWins));
332    }
333}