mockforge_core/incidents/
semantic_manager.rs1use crate::ai_contract_diff::semantic_analyzer::{SemanticChangeType, SemanticDriftResult};
8use crate::incidents::types::{IncidentSeverity, IncidentStatus};
9use chrono::Utc;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use uuid::Uuid;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct SemanticIncident {
17 pub id: String,
19 pub workspace_id: Option<String>,
21 pub endpoint: String,
23 pub method: String,
25 pub semantic_change_type: SemanticChangeType,
27 pub severity: IncidentSeverity,
29 pub status: IncidentStatus,
31 pub semantic_confidence: f64,
33 pub soft_breaking_score: f64,
35 pub llm_analysis: serde_json::Value,
37 pub before_semantic_state: serde_json::Value,
39 pub after_semantic_state: serde_json::Value,
41 pub details: serde_json::Value,
43 pub related_drift_incident_id: Option<String>,
45 pub contract_diff_id: Option<String>,
47 pub external_ticket_id: Option<String>,
49 pub external_ticket_url: Option<String>,
51 pub detected_at: i64,
53 pub created_at: i64,
55 pub acknowledged_at: Option<i64>,
57 pub resolved_at: Option<i64>,
59 pub closed_at: Option<i64>,
61 pub updated_at: i64,
63}
64
65impl SemanticIncident {
66 pub fn from_drift_result(
68 result: &SemanticDriftResult,
69 endpoint: String,
70 method: String,
71 workspace_id: Option<String>,
72 related_drift_incident_id: Option<String>,
73 contract_diff_id: Option<String>,
74 ) -> Self {
75 let id = Uuid::new_v4().to_string();
76 let now = Utc::now().timestamp();
77
78 let severity = if result.soft_breaking_score >= 0.8 && result.semantic_confidence >= 0.8 {
80 IncidentSeverity::Critical
81 } else if result.soft_breaking_score >= 0.65 || result.semantic_confidence >= 0.75 {
82 IncidentSeverity::High
83 } else if result.soft_breaking_score >= 0.5 || result.semantic_confidence >= 0.65 {
84 IncidentSeverity::Medium
85 } else {
86 IncidentSeverity::Low
87 };
88
89 let details = serde_json::json!({
90 "change_type": result.change_type,
91 "semantic_confidence": result.semantic_confidence,
92 "soft_breaking_score": result.soft_breaking_score,
93 "mismatch_count": result.semantic_mismatches.len(),
94 });
95
96 Self {
97 id,
98 workspace_id,
99 endpoint,
100 method,
101 semantic_change_type: result.change_type.clone(),
102 severity,
103 status: IncidentStatus::Open,
104 semantic_confidence: result.semantic_confidence,
105 soft_breaking_score: result.soft_breaking_score,
106 llm_analysis: result.llm_analysis.clone(),
107 before_semantic_state: result.before_semantic_state.clone(),
108 after_semantic_state: result.after_semantic_state.clone(),
109 details,
110 related_drift_incident_id,
111 contract_diff_id,
112 external_ticket_id: None,
113 external_ticket_url: None,
114 detected_at: now,
115 created_at: now,
116 acknowledged_at: None,
117 resolved_at: None,
118 closed_at: None,
119 updated_at: now,
120 }
121 }
122
123 pub fn acknowledge(&mut self) {
125 if self.status == IncidentStatus::Open {
126 self.status = IncidentStatus::Acknowledged;
127 self.acknowledged_at = Some(Utc::now().timestamp());
128 self.updated_at = Utc::now().timestamp();
129 }
130 }
131
132 pub fn resolve(&mut self) {
134 if self.status != IncidentStatus::Closed {
135 self.status = IncidentStatus::Resolved;
136 self.resolved_at = Some(Utc::now().timestamp());
137 self.updated_at = Utc::now().timestamp();
138 }
139 }
140
141 pub fn close(&mut self) {
143 self.status = IncidentStatus::Closed;
144 self.closed_at = Some(Utc::now().timestamp());
145 self.updated_at = Utc::now().timestamp();
146 }
147}
148
149pub struct SemanticIncidentManager {
151 incidents: std::sync::Arc<tokio::sync::RwLock<HashMap<String, SemanticIncident>>>,
153 webhook_configs: Vec<crate::incidents::integrations::WebhookConfig>,
155}
156
157impl SemanticIncidentManager {
158 pub fn new() -> Self {
160 Self {
161 incidents: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::new())),
162 webhook_configs: Vec::new(),
163 }
164 }
165
166 pub fn new_with_webhooks(
168 webhook_configs: Vec<crate::incidents::integrations::WebhookConfig>,
169 ) -> Self {
170 Self {
171 incidents: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::new())),
172 webhook_configs,
173 }
174 }
175
176 pub fn add_webhook(&mut self, config: crate::incidents::integrations::WebhookConfig) {
178 self.webhook_configs.push(config);
179 }
180
181 pub async fn create_incident(
183 &self,
184 result: &SemanticDriftResult,
185 endpoint: String,
186 method: String,
187 workspace_id: Option<String>,
188 related_drift_incident_id: Option<String>,
189 contract_diff_id: Option<String>,
190 ) -> SemanticIncident {
191 let incident = SemanticIncident::from_drift_result(
192 result,
193 endpoint,
194 method,
195 workspace_id,
196 related_drift_incident_id,
197 contract_diff_id,
198 );
199
200 let id = incident.id.clone();
201 let mut incidents = self.incidents.write().await;
202 incidents.insert(id, incident.clone());
203
204 self.trigger_webhooks("semantic.incident.created", &incident).await;
206
207 incident
208 }
209
210 async fn trigger_webhooks(&self, event_type: &str, incident: &SemanticIncident) {
212 use crate::incidents::integrations::send_webhook;
213 use serde_json::json;
214
215 for webhook in &self.webhook_configs {
216 if !webhook.enabled {
217 continue;
218 }
219
220 if !webhook.events.is_empty() && !webhook.events.contains(&event_type.to_string()) {
221 continue;
222 }
223
224 let payload = json!({
225 "event": event_type,
226 "incident": {
227 "id": incident.id,
228 "endpoint": incident.endpoint,
229 "method": incident.method,
230 "semantic_change_type": format!("{:?}", incident.semantic_change_type),
231 "severity": format!("{:?}", incident.severity),
232 "status": format!("{:?}", incident.status),
233 "semantic_confidence": incident.semantic_confidence,
234 "soft_breaking_score": incident.soft_breaking_score,
235 "details": incident.details,
236 "created_at": incident.created_at,
237 }
238 });
239
240 let webhook_clone = webhook.clone();
241 tokio::spawn(async move {
242 if let Err(e) = send_webhook(&webhook_clone, &payload).await {
243 tracing::warn!("Failed to send webhook: {}", e);
244 }
245 });
246 }
247 }
248
249 pub async fn get_incident(&self, id: &str) -> Option<SemanticIncident> {
251 let incidents = self.incidents.read().await;
252 incidents.get(id).cloned()
253 }
254
255 pub async fn update_incident(&self, incident: SemanticIncident) {
257 let mut incidents = self.incidents.write().await;
258 incidents.insert(incident.id.clone(), incident);
259 }
260
261 pub async fn list_incidents(
263 &self,
264 workspace_id: Option<&str>,
265 endpoint: Option<&str>,
266 method: Option<&str>,
267 status: Option<IncidentStatus>,
268 limit: Option<usize>,
269 ) -> Vec<SemanticIncident> {
270 let incidents = self.incidents.read().await;
271 let mut filtered: Vec<_> = incidents
272 .values()
273 .filter(|inc| {
274 if let Some(ws_id) = workspace_id {
275 if inc.workspace_id.as_deref() != Some(ws_id) {
276 return false;
277 }
278 }
279 if let Some(ep) = endpoint {
280 if inc.endpoint != ep {
281 return false;
282 }
283 }
284 if let Some(m) = method {
285 if inc.method != m {
286 return false;
287 }
288 }
289 if let Some(s) = status {
290 if inc.status != s {
291 return false;
292 }
293 }
294 true
295 })
296 .cloned()
297 .collect();
298
299 filtered.sort_by_key(|inc| std::cmp::Reverse(inc.detected_at));
301
302 if let Some(limit) = limit {
303 filtered.truncate(limit);
304 }
305
306 filtered
307 }
308
309 pub async fn acknowledge_incident(&self, id: &str) -> Option<SemanticIncident> {
311 let mut incidents = self.incidents.write().await;
312 if let Some(incident) = incidents.get_mut(id) {
313 incident.acknowledge();
314 Some(incident.clone())
315 } else {
316 None
317 }
318 }
319
320 pub async fn resolve_incident(&self, id: &str) -> Option<SemanticIncident> {
322 let mut incidents = self.incidents.write().await;
323 if let Some(incident) = incidents.get_mut(id) {
324 incident.resolve();
325 Some(incident.clone())
326 } else {
327 None
328 }
329 }
330}
331
332impl Default for SemanticIncidentManager {
333 fn default() -> Self {
334 Self::new()
335 }
336}