Skip to main content

agent_evaluate_contract/
gate.rs

1//! Report comparison and the declarative release gate.
2
3use serde::{Deserialize, Serialize};
4
5use crate::metric::{MetricClass, MetricDecision};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(rename_all = "camelCase", deny_unknown_fields)]
9pub struct CompareReportsRequest {
10    pub report_id: String,
11    pub baseline_report_id: String,
12    #[serde(default)]
13    pub minimum_pass_rate: f64,
14    #[serde(default = "default_max_infra_rate")]
15    pub maximum_infra_failure_rate: f64,
16    #[serde(default = "default_significance")]
17    pub significance_level: f64,
18    #[serde(default)]
19    pub minimum_effect: f64,
20    /// Per-class gate policy. Absent keeps the v1 aggregate-only behaviour.
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub policy: Option<GatePolicy>,
23}
24
25impl CompareReportsRequest {
26    /// The effective policy: the explicit one, or the aggregate thresholds
27    /// promoted into the class-aware form.
28    pub fn effective_policy(&self) -> GatePolicy {
29        self.policy.clone().unwrap_or_else(|| GatePolicy {
30            minimum_pass_rate: self.minimum_pass_rate,
31            maximum_infra_failure_rate: self.maximum_infra_failure_rate,
32            significance_level: self.significance_level,
33            minimum_effect: self.minimum_effect,
34            ..GatePolicy::default()
35        })
36    }
37}
38
39fn default_max_infra_rate() -> f64 {
40    1.0
41}
42
43fn default_significance() -> f64 {
44    0.05
45}
46
47/// Declarative gate. Guardrails are absolute, goals are statistical, and
48/// diagnostics never gate.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase", deny_unknown_fields)]
51pub struct GatePolicy {
52    /// Aggregate floor for the whole suite.
53    #[serde(default)]
54    pub minimum_pass_rate: f64,
55    #[serde(default = "default_max_infra_rate")]
56    pub maximum_infra_failure_rate: f64,
57    #[serde(default = "default_significance")]
58    pub significance_level: f64,
59    /// Regression tolerated on a goal metric before it can block.
60    #[serde(default)]
61    pub minimum_effect: f64,
62    /// Floor applied to every goal metric individually.
63    #[serde(default)]
64    pub goal_minimum_rate: f64,
65    /// Narrower rules for named evaluators.
66    #[serde(default)]
67    pub overrides: Vec<MetricThreshold>,
68}
69
70impl Default for GatePolicy {
71    fn default() -> Self {
72        Self {
73            minimum_pass_rate: 0.0,
74            maximum_infra_failure_rate: 1.0,
75            significance_level: 0.05,
76            minimum_effect: 0.0,
77            goal_minimum_rate: 0.0,
78            overrides: Vec::new(),
79        }
80    }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase", deny_unknown_fields)]
85pub struct MetricThreshold {
86    pub evaluator_id: String,
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub minimum_rate: Option<f64>,
89    /// Absolute drop against the baseline that blocks regardless of class.
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub maximum_regression: Option<f64>,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(rename_all = "snake_case")]
96pub enum ViolationReason {
97    GuardrailFailed,
98    BelowMinimumRate,
99    SignificantRegression,
100    InfrastructureFailureRate,
101    AggregatePassRate,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105#[serde(rename_all = "camelCase")]
106pub struct MetricViolation {
107    pub evaluator_id: String,
108    #[serde(default, skip_serializing_if = "Option::is_none")]
109    pub evaluator_version: Option<String>,
110    pub class: MetricClass,
111    pub reason: ViolationReason,
112    pub decision: MetricDecision,
113    pub observed_rate: f64,
114    #[serde(default, skip_serializing_if = "Option::is_none")]
115    pub baseline_rate: Option<f64>,
116    pub delta: f64,
117    #[serde(default, skip_serializing_if = "Option::is_none")]
118    pub p_value: Option<f64>,
119    pub detail: String,
120}
121
122/// Actionable gate outcome. Callers need the blocking metric, not a bool.
123#[derive(Debug, Clone, Default, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct GateDecision {
126    pub passed: bool,
127    #[serde(default)]
128    pub blocking_metrics: Vec<MetricViolation>,
129    #[serde(default)]
130    pub warnings: Vec<MetricViolation>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct CaseOutcomeDiff {
136    pub case_id: String,
137    pub case_version: String,
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub variant_id: Option<String>,
140    pub baseline_passed: bool,
141    pub current_passed: bool,
142    pub flaky_signal: bool,
143    #[serde(default)]
144    pub historical_flake_rate: f64,
145    #[serde(default)]
146    pub historical_samples: u64,
147}
148
149/// Per-metric delta between two reports.
150#[derive(Debug, Clone, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct MetricComparison {
153    pub evaluator_id: String,
154    pub evaluator_version: String,
155    pub class: MetricClass,
156    pub current_rate: f64,
157    #[serde(default, skip_serializing_if = "Option::is_none")]
158    pub baseline_rate: Option<f64>,
159    pub delta: f64,
160    pub p_value: f64,
161    pub statistically_significant: bool,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
165#[serde(rename_all = "camelCase")]
166pub struct ReportComparison {
167    pub report_id: String,
168    pub baseline_report_id: String,
169    pub pass_rate_delta: f64,
170    pub infra_failure_rate_delta: f64,
171    pub pass_rate_confidence_low: f64,
172    pub pass_rate_confidence_high: f64,
173    pub effect_confidence_low: f64,
174    pub effect_confidence_high: f64,
175    pub p_value: f64,
176    pub statistically_significant: bool,
177    pub changed_cases: Vec<CaseOutcomeDiff>,
178    #[serde(default)]
179    pub metric_comparisons: Vec<MetricComparison>,
180    /// Retained for wire compatibility; mirrors `gate.passed`.
181    pub gate_passed: bool,
182    #[serde(default)]
183    pub gate: GateDecision,
184}