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)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn test_intervention_type_tagged_serde() {
233        let json = r#"{
234            "type": "parameter_shift",
235            "target": "transactions.count",
236            "from": 1000,
237            "to": 2000,
238            "interpolation": "linear"
239        }"#;
240        let intervention: InterventionType = serde_json::from_str(json).unwrap();
241        assert!(matches!(intervention, InterventionType::ParameterShift(_)));
242
243        // Roundtrip
244        let serialized = serde_json::to_string(&intervention).unwrap();
245        let deserialized: InterventionType = serde_json::from_str(&serialized).unwrap();
246        assert!(matches!(deserialized, InterventionType::ParameterShift(_)));
247    }
248
249    #[test]
250    fn test_entity_event_serde() {
251        let json = r#"{
252            "type": "entity_event",
253            "subtype": "vendor_default",
254            "target": {
255                "cluster": "problematic",
256                "count": 5
257            },
258            "parameters": {}
259        }"#;
260        let intervention: InterventionType = serde_json::from_str(json).unwrap();
261        if let InterventionType::EntityEvent(e) = intervention {
262            assert!(matches!(e.subtype, InterventionEntityEvent::VendorDefault));
263            assert_eq!(e.target.cluster, Some("problematic".to_string()));
264            assert_eq!(e.target.count, Some(5));
265        } else {
266            panic!("Expected EntityEvent");
267        }
268    }
269
270    #[test]
271    fn test_macro_shock_serde() {
272        let json = r#"{
273            "type": "macro_shock",
274            "subtype": "recession",
275            "severity": 1.5,
276            "preset": "2008_financial_crisis",
277            "overrides": {"gdp_growth": -0.03}
278        }"#;
279        let intervention: InterventionType = serde_json::from_str(json).unwrap();
280        if let InterventionType::MacroShock(m) = intervention {
281            assert!(matches!(m.subtype, MacroShockType::Recession));
282            assert_eq!(m.severity, 1.5);
283            assert_eq!(m.preset, Some("2008_financial_crisis".to_string()));
284        } else {
285            panic!("Expected MacroShock");
286        }
287    }
288
289    #[test]
290    fn test_control_failure_serde() {
291        let json = r#"{
292            "type": "control_failure",
293            "subtype": "complete_bypass",
294            "control_target": {"control_id": "C003"},
295            "severity": 0.0,
296            "detectable": false
297        }"#;
298        let intervention: InterventionType = serde_json::from_str(json).unwrap();
299        assert!(matches!(intervention, InterventionType::ControlFailure(_)));
300    }
301
302    #[test]
303    fn test_composite_serde() {
304        let json = r#"{
305            "type": "composite",
306            "name": "recession_scenario",
307            "description": "Combined recession effects",
308            "children": [
309                {
310                    "type": "macro_shock",
311                    "subtype": "recession",
312                    "severity": 1.0,
313                    "overrides": {}
314                }
315            ],
316            "conflict_resolution": "first_wins"
317        }"#;
318        let intervention: InterventionType = serde_json::from_str(json).unwrap();
319        if let InterventionType::Composite(c) = intervention {
320            assert_eq!(c.name, "recession_scenario");
321            assert_eq!(c.children.len(), 1);
322        } else {
323            panic!("Expected Composite");
324        }
325    }
326
327    #[test]
328    fn test_conflict_resolution_default() {
329        let cr = ConflictResolution::default();
330        assert!(matches!(cr, ConflictResolution::FirstWins));
331    }
332}