use crate::ai_contract_diff::semantic_analyzer::{SemanticChangeType, SemanticDriftResult};
use crate::incidents::types::{IncidentSeverity, IncidentStatus};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SemanticIncident {
pub id: String,
pub workspace_id: Option<String>,
pub endpoint: String,
pub method: String,
pub semantic_change_type: SemanticChangeType,
pub severity: IncidentSeverity,
pub status: IncidentStatus,
pub semantic_confidence: f64,
pub soft_breaking_score: f64,
pub llm_analysis: serde_json::Value,
pub before_semantic_state: serde_json::Value,
pub after_semantic_state: serde_json::Value,
pub details: serde_json::Value,
pub related_drift_incident_id: Option<String>,
pub contract_diff_id: Option<String>,
pub external_ticket_id: Option<String>,
pub external_ticket_url: Option<String>,
pub detected_at: i64,
pub created_at: i64,
pub acknowledged_at: Option<i64>,
pub resolved_at: Option<i64>,
pub closed_at: Option<i64>,
pub updated_at: i64,
}
impl SemanticIncident {
pub fn from_drift_result(
result: &SemanticDriftResult,
endpoint: String,
method: String,
workspace_id: Option<String>,
related_drift_incident_id: Option<String>,
contract_diff_id: Option<String>,
) -> Self {
let id = Uuid::new_v4().to_string();
let now = Utc::now().timestamp();
let severity = if result.soft_breaking_score >= 0.8 && result.semantic_confidence >= 0.8 {
IncidentSeverity::Critical
} else if result.soft_breaking_score >= 0.65 || result.semantic_confidence >= 0.75 {
IncidentSeverity::High
} else if result.soft_breaking_score >= 0.5 || result.semantic_confidence >= 0.65 {
IncidentSeverity::Medium
} else {
IncidentSeverity::Low
};
let details = serde_json::json!({
"change_type": result.change_type,
"semantic_confidence": result.semantic_confidence,
"soft_breaking_score": result.soft_breaking_score,
"mismatch_count": result.semantic_mismatches.len(),
});
Self {
id,
workspace_id,
endpoint,
method,
semantic_change_type: result.change_type.clone(),
severity,
status: IncidentStatus::Open,
semantic_confidence: result.semantic_confidence,
soft_breaking_score: result.soft_breaking_score,
llm_analysis: result.llm_analysis.clone(),
before_semantic_state: result.before_semantic_state.clone(),
after_semantic_state: result.after_semantic_state.clone(),
details,
related_drift_incident_id,
contract_diff_id,
external_ticket_id: None,
external_ticket_url: None,
detected_at: now,
created_at: now,
acknowledged_at: None,
resolved_at: None,
closed_at: None,
updated_at: now,
}
}
pub fn acknowledge(&mut self) {
if self.status == IncidentStatus::Open {
self.status = IncidentStatus::Acknowledged;
self.acknowledged_at = Some(Utc::now().timestamp());
self.updated_at = Utc::now().timestamp();
}
}
pub fn resolve(&mut self) {
if self.status != IncidentStatus::Closed {
self.status = IncidentStatus::Resolved;
self.resolved_at = Some(Utc::now().timestamp());
self.updated_at = Utc::now().timestamp();
}
}
pub fn close(&mut self) {
self.status = IncidentStatus::Closed;
self.closed_at = Some(Utc::now().timestamp());
self.updated_at = Utc::now().timestamp();
}
}
pub struct SemanticIncidentManager {
incidents: std::sync::Arc<tokio::sync::RwLock<HashMap<String, SemanticIncident>>>,
webhook_configs: Vec<crate::incidents::integrations::WebhookConfig>,
}
impl SemanticIncidentManager {
pub fn new() -> Self {
Self {
incidents: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::new())),
webhook_configs: Vec::new(),
}
}
pub fn new_with_webhooks(
webhook_configs: Vec<crate::incidents::integrations::WebhookConfig>,
) -> Self {
Self {
incidents: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::new())),
webhook_configs,
}
}
pub fn add_webhook(&mut self, config: crate::incidents::integrations::WebhookConfig) {
self.webhook_configs.push(config);
}
pub async fn create_incident(
&self,
result: &SemanticDriftResult,
endpoint: String,
method: String,
workspace_id: Option<String>,
related_drift_incident_id: Option<String>,
contract_diff_id: Option<String>,
) -> SemanticIncident {
let incident = SemanticIncident::from_drift_result(
result,
endpoint,
method,
workspace_id,
related_drift_incident_id,
contract_diff_id,
);
let id = incident.id.clone();
let mut incidents = self.incidents.write().await;
incidents.insert(id, incident.clone());
self.trigger_webhooks("semantic.incident.created", &incident).await;
incident
}
async fn trigger_webhooks(&self, event_type: &str, incident: &SemanticIncident) {
use crate::incidents::integrations::send_webhook;
use serde_json::json;
for webhook in &self.webhook_configs {
if !webhook.enabled {
continue;
}
if !webhook.events.is_empty() && !webhook.events.contains(&event_type.to_string()) {
continue;
}
let payload = json!({
"event": event_type,
"incident": {
"id": incident.id,
"endpoint": incident.endpoint,
"method": incident.method,
"semantic_change_type": format!("{:?}", incident.semantic_change_type),
"severity": format!("{:?}", incident.severity),
"status": format!("{:?}", incident.status),
"semantic_confidence": incident.semantic_confidence,
"soft_breaking_score": incident.soft_breaking_score,
"details": incident.details,
"created_at": incident.created_at,
}
});
let webhook_clone = webhook.clone();
tokio::spawn(async move {
if let Err(e) = send_webhook(&webhook_clone, &payload).await {
tracing::warn!("Failed to send webhook: {}", e);
}
});
}
}
pub async fn get_incident(&self, id: &str) -> Option<SemanticIncident> {
let incidents = self.incidents.read().await;
incidents.get(id).cloned()
}
pub async fn update_incident(&self, incident: SemanticIncident) {
let mut incidents = self.incidents.write().await;
incidents.insert(incident.id.clone(), incident);
}
pub async fn list_incidents(
&self,
workspace_id: Option<&str>,
endpoint: Option<&str>,
method: Option<&str>,
status: Option<IncidentStatus>,
limit: Option<usize>,
) -> Vec<SemanticIncident> {
let incidents = self.incidents.read().await;
let mut filtered: Vec<_> = incidents
.values()
.filter(|inc| {
if let Some(ws_id) = workspace_id {
if inc.workspace_id.as_deref() != Some(ws_id) {
return false;
}
}
if let Some(ep) = endpoint {
if inc.endpoint != ep {
return false;
}
}
if let Some(m) = method {
if inc.method != m {
return false;
}
}
if let Some(s) = status {
if inc.status != s {
return false;
}
}
true
})
.cloned()
.collect();
filtered.sort_by_key(|inc| std::cmp::Reverse(inc.detected_at));
if let Some(limit) = limit {
filtered.truncate(limit);
}
filtered
}
pub async fn acknowledge_incident(&self, id: &str) -> Option<SemanticIncident> {
let mut incidents = self.incidents.write().await;
if let Some(incident) = incidents.get_mut(id) {
incident.acknowledge();
Some(incident.clone())
} else {
None
}
}
pub async fn resolve_incident(&self, id: &str) -> Option<SemanticIncident> {
let mut incidents = self.incidents.write().await;
if let Some(incident) = incidents.get_mut(id) {
incident.resolve();
Some(incident.clone())
} else {
None
}
}
}
impl Default for SemanticIncidentManager {
fn default() -> Self {
Self::new()
}
}