1use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7use crate::catalog::{Capability, SnapshotRef};
8use crate::evaluator::{MetricClass, MetricDecision};
9use crate::run::CaseRunState;
10
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct AssertionVerdict {
14 pub rule: String,
15 pub passed: bool,
16 #[serde(default, skip_serializing_if = "Option::is_none")]
17 pub detail: Option<String>,
18}
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub struct MetricValue {
26 pub metric_id: String,
27 pub value: f64,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub min: Option<f64>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub max: Option<f64>,
32 pub passed: bool,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub detail: Option<String>,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41pub enum VerdictDowngrade {
42 LowAgreement,
44 SelfGraded,
46 PartialEnsemble,
48 JudgeNotAllowlisted,
50}
51
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct EvaluatorVerdict {
55 pub evaluator_id: String,
56 pub evaluator_digest: String,
57 pub kind: String,
58 pub passed: bool,
59 #[serde(default)]
60 pub class: MetricClass,
61 #[serde(default)]
62 pub decision: MetricDecision,
63 #[serde(default)]
64 pub assertions: Vec<AssertionVerdict>,
65 #[serde(default)]
66 pub metrics: Vec<MetricValue>,
67 #[serde(default)]
69 pub judge_cost_micros: u64,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub agreement: Option<f64>,
72 #[serde(default)]
73 pub downgrades: Vec<VerdictDowngrade>,
74}
75
76impl EvaluatorVerdict {
77 pub fn gates(&self) -> bool {
80 self.class.affects_case_verdict() && self.downgrades.is_empty()
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85#[serde(rename_all = "camelCase")]
86pub struct CaseResult {
87 pub case_run_id: String,
88 #[serde(rename = "snapshotRef")]
89 pub snapshot_ref: SnapshotRef,
90 pub case_id: String,
91 pub case_digest: String,
92 pub category: String,
93 pub state: CaseRunState,
94 pub passed: bool,
95 #[serde(default)]
96 pub turns_completed: usize,
97 #[serde(default)]
98 pub verdicts: Vec<EvaluatorVerdict>,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub error: Option<String>,
101 #[serde(default)]
102 pub duration_ms: u64,
103 #[serde(default)]
104 pub cost_micros: u64,
105 #[serde(default)]
106 pub sample_index: u32,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub variant_id: Option<String>,
109}
110
111#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "camelCase")]
114pub struct RateSlice {
115 pub observed: u64,
116 pub passed: u64,
117}
118
119impl RateSlice {
120 pub fn rate(&self) -> f64 {
121 if self.observed == 0 {
122 0.0
123 } else {
124 self.passed as f64 / self.observed as f64
125 }
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130#[serde(rename_all = "camelCase")]
131pub struct MetricSummary {
132 pub evaluator_id: String,
133 pub evaluator_digest: String,
134 pub class: MetricClass,
135 #[serde(default)]
136 pub decision: MetricDecision,
137 pub observed: u64,
138 pub passed: u64,
139 pub rate: f64,
140 pub wilson_low: f64,
143 pub wilson_high: f64,
144 #[serde(default)]
145 pub judge_cost_micros: u64,
146 #[serde(default)]
147 pub by_category: BTreeMap<String, RateSlice>,
148 #[serde(default)]
149 pub by_variant: BTreeMap<String, RateSlice>,
150}
151
152#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
155#[serde(rename_all = "camelCase")]
156pub struct EvalReport {
157 pub id: String,
158 pub run_id: String,
159 pub revision: u32,
160 #[serde(default)]
161 pub snapshots: Vec<SnapshotRef>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub capability: Option<Capability>,
164 pub total_cases: usize,
165 pub passed_cases: usize,
166 pub quality_failed_cases: usize,
167 pub infra_failed_cases: usize,
168 pub invalid_cases: usize,
169 pub analysis_failed_cases: usize,
170 pub effective_pass_rate: f64,
172 #[serde(default)]
173 pub metrics: Vec<MetricSummary>,
174 #[serde(default)]
175 pub case_results: Vec<CaseResult>,
176 #[serde(default)]
177 pub cost_micros: u64,
178 #[serde(default)]
179 pub judge_cost_micros: u64,
180 #[serde(default)]
181 pub duration_ms: u64,
182 pub sealed_at_ms: i64,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186#[serde(rename_all = "camelCase", deny_unknown_fields)]
187pub struct GetReportRequest {
188 pub run_id: String,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub revision: Option<u32>,
192}
193
194#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
195#[serde(rename_all = "camelCase", deny_unknown_fields)]
196pub struct MetricThreshold {
197 pub evaluator_id: String,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub minimum_rate: Option<f64>,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub maximum_regression: Option<f64>,
202}
203
204#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
207#[serde(rename_all = "camelCase", deny_unknown_fields)]
208pub struct GatePolicy {
209 #[serde(default)]
210 pub minimum_pass_rate: f64,
211 #[serde(default = "default_max_infra_rate")]
212 pub maximum_infra_failure_rate: f64,
213 #[serde(default = "default_significance")]
214 pub significance_level: f64,
215 #[serde(default)]
217 pub minimum_effect: f64,
218 #[serde(default)]
219 pub goal_minimum_rate: f64,
220 #[serde(default)]
221 pub overrides: Vec<MetricThreshold>,
222}
223
224fn default_max_infra_rate() -> f64 {
225 1.0
226}
227
228fn default_significance() -> f64 {
229 0.05
230}
231
232impl Default for GatePolicy {
233 fn default() -> Self {
234 Self {
235 minimum_pass_rate: 0.0,
236 maximum_infra_failure_rate: default_max_infra_rate(),
237 significance_level: default_significance(),
238 minimum_effect: 0.0,
239 goal_minimum_rate: 0.0,
240 overrides: Vec::new(),
241 }
242 }
243}
244
245#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
246#[serde(rename_all = "camelCase", deny_unknown_fields)]
247pub struct CompareReportsRequest {
248 pub run_id: String,
249 #[serde(default, skip_serializing_if = "Option::is_none")]
250 pub revision: Option<u32>,
251 pub baseline_run_id: String,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
253 pub baseline_revision: Option<u32>,
254 #[serde(default)]
255 pub policy: GatePolicy,
256}
257
258#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
259#[serde(rename_all = "camelCase")]
260pub struct GateViolation {
261 pub evaluator_id: String,
262 pub class: MetricClass,
263 pub decision: MetricDecision,
264 pub reason: String,
265}
266
267#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
268#[serde(rename_all = "camelCase")]
269pub struct GateDecision {
270 pub allowed: bool,
271 #[serde(default)]
272 pub violations: Vec<GateViolation>,
273}