mockforge-intelligence 0.3.234

AI-powered behavior, response generation, and behavioral cloning for MockForge
Documentation
//! Unified contract health timeline
//!
//! This module provides a unified endpoint that combines:
//! - Structural drift incidents
//! - Semantic drift incidents
//! - Threat assessments
//! - Forecast predictions

use super::structural_incidents::StructuralIncidents;
use crate::incidents::SemanticIncidentManager;
use axum::{
    extract::{Query, State},
    http::StatusCode,
    response::Json,
};
use serde::{Deserialize, Serialize};

#[cfg(feature = "database")]
mod persistence;

/// State supplied by downstream HTTP integrations for the contract timeline.
pub trait ContractHealthStateProvider: Clone + Send + Sync + 'static {
    /// Structural incident manager implementation.
    type Incidents: StructuralIncidents;
    /// Access the existing structural incident manager.
    fn incident_manager(&self) -> &Self::Incidents;
    /// Access the Intelligence semantic incident manager.
    fn semantic_manager(&self) -> &SemanticIncidentManager;
    /// Optional database. Default permits feature unification without requiring
    /// a database field in downstream states compiled without that feature.
    #[cfg(feature = "database")]
    fn database(&self) -> Option<&crate::database::Database> {
        None
    }
}

/// Query parameters for timeline
#[derive(Debug, Deserialize)]
pub struct TimelineQuery {
    /// Workspace ID filter
    pub workspace_id: Option<String>,
    /// Endpoint filter
    pub endpoint: Option<String>,
    /// Method filter
    pub method: Option<String>,
    /// Start date (ISO 8601)
    pub start_date: Option<String>,
    /// End date (ISO 8601)
    pub end_date: Option<String>,
    /// Filter by type: "structural", "semantic", "threat", "forecast", or "all"
    pub event_type: Option<String>,
    /// Limit results
    pub limit: Option<usize>,
}

/// Timeline event
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum TimelineEvent {
    /// Structural drift incident
    #[serde(rename = "structural_drift")]
    StructuralDrift {
        /// Incident ID
        id: String,
        /// Endpoint path
        endpoint: String,
        /// HTTP method
        method: String,
        /// Type of incident
        incident_type: String,
        /// Severity level
        severity: String,
        /// Current status
        status: String,
        /// Detection timestamp
        detected_at: i64,
        /// Additional details
        details: serde_json::Value,
    },
    /// Semantic drift incident
    #[serde(rename = "semantic_drift")]
    SemanticDrift {
        /// Incident ID
        id: String,
        /// Endpoint path
        endpoint: String,
        /// HTTP method
        method: String,
        /// Type of semantic change
        change_type: String,
        /// Severity level
        severity: String,
        /// Current status
        status: String,
        /// Semantic confidence score
        semantic_confidence: f64,
        /// Soft-breaking score
        soft_breaking_score: f64,
        /// Detection timestamp
        detected_at: i64,
        /// Additional details
        details: serde_json::Value,
    },
    /// Threat assessment
    #[serde(rename = "threat_assessment")]
    ThreatAssessment {
        /// Assessment ID
        id: String,
        /// Endpoint path (optional for workspace-level)
        endpoint: Option<String>,
        /// HTTP method (optional for workspace-level)
        method: Option<String>,
        /// Threat level
        threat_level: String,
        /// Threat score (0.0-1.0)
        threat_score: f64,
        /// Assessment timestamp
        assessed_at: i64,
        /// Number of findings
        findings_count: usize,
    },
    /// Forecast prediction
    #[serde(rename = "forecast")]
    Forecast {
        /// Forecast ID
        id: String,
        /// Endpoint path
        endpoint: String,
        /// HTTP method
        method: String,
        /// Forecast window in days
        window_days: u32,
        /// Change probability (0.0-1.0)
        change_probability: f64,
        /// Break probability (0.0-1.0)
        break_probability: f64,
        /// Next expected change timestamp (optional)
        next_expected_change: Option<i64>,
        /// Confidence score (0.0-1.0)
        confidence: f64,
        /// Prediction timestamp
        predicted_at: i64,
    },
}

/// Timeline response
#[derive(Debug, Serialize)]
pub struct TimelineResponse {
    /// Timeline events
    pub events: Vec<TimelineEvent>,
    /// Total count
    pub total: usize,
}

/// Get unified contract health timeline
///
/// GET /api/v1/contract-health/timeline
pub async fn get_timeline<S: ContractHealthStateProvider>(
    State(state): State<S>,
    Query(params): Query<TimelineQuery>,
) -> Result<Json<TimelineResponse>, StatusCode> {
    let mut events = Vec::new();

    let event_type_filter = params.event_type.as_deref().unwrap_or("all");

    // Get structural drift incidents
    if event_type_filter == "all" || event_type_filter == "structural" {
        let query = mockforge_foundation::incidents_types::IncidentQuery {
            workspace_id: params.workspace_id.clone(),
            endpoint: params.endpoint.clone(),
            method: params.method.clone(),
            ..mockforge_foundation::incidents_types::IncidentQuery::default()
        };

        let incidents = state.incident_manager().query_incidents(query).await;
        for incident in incidents {
            events.push(TimelineEvent::StructuralDrift {
                id: incident.id,
                endpoint: incident.endpoint,
                method: incident.method,
                incident_type: format!("{:?}", incident.incident_type),
                severity: format!("{:?}", incident.severity),
                status: format!("{:?}", incident.status),
                detected_at: incident.detected_at,
                details: incident.details,
            });
        }
    }

    // Get semantic drift incidents
    if event_type_filter == "all" || event_type_filter == "semantic" {
        let status = None; // Get all statuses for timeline
        let semantic_incidents = state
            .semantic_manager()
            .list_incidents(
                params.workspace_id.as_deref(),
                params.endpoint.as_deref(),
                params.method.as_deref(),
                status,
                params.limit,
            )
            .await;

        for incident in semantic_incidents {
            events.push(TimelineEvent::SemanticDrift {
                id: incident.id,
                endpoint: incident.endpoint,
                method: incident.method,
                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,
                detected_at: incident.detected_at,
                details: incident.details,
            });
        }
    }

    // Add database events through typed schema-specific row decoders.
    #[cfg(feature = "database")]
    if let Some(pool) = state.database().and_then(|db| db.pool()) {
        let workspace = params
            .workspace_id
            .as_deref()
            .map(uuid::Uuid::parse_str)
            .transpose()
            .map_err(|_| StatusCode::BAD_REQUEST)?;
        events.extend(persistence::events(pool, workspace).await);
    }

    // Sort by timestamp (most recent first)
    events.sort_by_key(|e| {
        std::cmp::Reverse(match e {
            TimelineEvent::StructuralDrift { detected_at, .. } => *detected_at,
            TimelineEvent::SemanticDrift { detected_at, .. } => *detected_at,
            TimelineEvent::ThreatAssessment { assessed_at, .. } => *assessed_at,
            TimelineEvent::Forecast { predicted_at, .. } => *predicted_at,
        })
    });

    // Apply limit
    let total = events.len();
    if let Some(limit) = params.limit {
        events.truncate(limit);
    }

    Ok(Json(TimelineResponse { events, total }))
}

/// Create router for contract health endpoints
pub fn contract_health_router<S: ContractHealthStateProvider>(state: S) -> axum::Router {
    use axum::routing::get;
    use axum::Router;

    Router::new()
        .route("/api/v1/contract-health/timeline", get(get_timeline::<S>))
        .with_state(state)
}