agent-evaluate-contract 0.5.0

Transport-neutral contracts for Agent Infra evaluation APIs
Documentation
//! Report comparison and the declarative release gate.

use serde::{Deserialize, Serialize};

use crate::metric::{MetricClass, MetricDecision};

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CompareReportsRequest {
    pub report_id: String,
    pub baseline_report_id: String,
    #[serde(default)]
    pub minimum_pass_rate: f64,
    #[serde(default = "default_max_infra_rate")]
    pub maximum_infra_failure_rate: f64,
    #[serde(default = "default_significance")]
    pub significance_level: f64,
    #[serde(default)]
    pub minimum_effect: f64,
    /// Per-class gate policy. Absent keeps the v1 aggregate-only behaviour.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub policy: Option<GatePolicy>,
}

impl CompareReportsRequest {
    /// The effective policy: the explicit one, or the aggregate thresholds
    /// promoted into the class-aware form.
    pub fn effective_policy(&self) -> GatePolicy {
        self.policy.clone().unwrap_or_else(|| GatePolicy {
            minimum_pass_rate: self.minimum_pass_rate,
            maximum_infra_failure_rate: self.maximum_infra_failure_rate,
            significance_level: self.significance_level,
            minimum_effect: self.minimum_effect,
            ..GatePolicy::default()
        })
    }
}

fn default_max_infra_rate() -> f64 {
    1.0
}

fn default_significance() -> f64 {
    0.05
}

/// Declarative gate. Guardrails are absolute, goals are statistical, and
/// diagnostics never gate.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct GatePolicy {
    /// Aggregate floor for the whole suite.
    #[serde(default)]
    pub minimum_pass_rate: f64,
    #[serde(default = "default_max_infra_rate")]
    pub maximum_infra_failure_rate: f64,
    #[serde(default = "default_significance")]
    pub significance_level: f64,
    /// Regression tolerated on a goal metric before it can block.
    #[serde(default)]
    pub minimum_effect: f64,
    /// Floor applied to every goal metric individually.
    #[serde(default)]
    pub goal_minimum_rate: f64,
    /// Narrower rules for named evaluators.
    #[serde(default)]
    pub overrides: Vec<MetricThreshold>,
}

impl Default for GatePolicy {
    fn default() -> Self {
        Self {
            minimum_pass_rate: 0.0,
            maximum_infra_failure_rate: 1.0,
            significance_level: 0.05,
            minimum_effect: 0.0,
            goal_minimum_rate: 0.0,
            overrides: Vec::new(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct MetricThreshold {
    pub evaluator_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub minimum_rate: Option<f64>,
    /// Absolute drop against the baseline that blocks regardless of class.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub maximum_regression: Option<f64>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ViolationReason {
    GuardrailFailed,
    BelowMinimumRate,
    SignificantRegression,
    InfrastructureFailureRate,
    AggregatePassRate,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MetricViolation {
    pub evaluator_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub evaluator_version: Option<String>,
    pub class: MetricClass,
    pub reason: ViolationReason,
    pub decision: MetricDecision,
    pub observed_rate: f64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub baseline_rate: Option<f64>,
    pub delta: f64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub p_value: Option<f64>,
    pub detail: String,
}

/// Actionable gate outcome. Callers need the blocking metric, not a bool.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GateDecision {
    pub passed: bool,
    #[serde(default)]
    pub blocking_metrics: Vec<MetricViolation>,
    #[serde(default)]
    pub warnings: Vec<MetricViolation>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CaseOutcomeDiff {
    pub case_id: String,
    pub case_version: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub variant_id: Option<String>,
    pub baseline_passed: bool,
    pub current_passed: bool,
    pub flaky_signal: bool,
    #[serde(default)]
    pub historical_flake_rate: f64,
    #[serde(default)]
    pub historical_samples: u64,
}

/// Per-metric delta between two reports.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MetricComparison {
    pub evaluator_id: String,
    pub evaluator_version: String,
    pub class: MetricClass,
    pub current_rate: f64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub baseline_rate: Option<f64>,
    pub delta: f64,
    pub p_value: f64,
    pub statistically_significant: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReportComparison {
    pub report_id: String,
    pub baseline_report_id: String,
    pub pass_rate_delta: f64,
    pub infra_failure_rate_delta: f64,
    pub pass_rate_confidence_low: f64,
    pub pass_rate_confidence_high: f64,
    pub effect_confidence_low: f64,
    pub effect_confidence_high: f64,
    pub p_value: f64,
    pub statistically_significant: bool,
    pub changed_cases: Vec<CaseOutcomeDiff>,
    #[serde(default)]
    pub metric_comparisons: Vec<MetricComparison>,
    /// Retained for wire compatibility; mirrors `gate.passed`.
    pub gate_passed: bool,
    #[serde(default)]
    pub gate: GateDecision,
}