1use schemars::JsonSchema;
8use semantic_memory_forge::EvidenceAdmissibilityV1;
9use serde::{Deserialize, Serialize};
10use stack_ids::{
11 CohortContractId, ComparabilityMatrixId, CounterfactualSliceId, DecisionTraceId,
12 DisputeBundleId, ExperimentBudgetId, ExperimentCaseId, InterventionId, OutcomeSchemaId,
13 RefuterResultId, RefuterSuiteId,
14};
15
16pub const EXPERIMENT_CASE_V1_SCHEMA: &str = "experiment_case_v1";
17pub const COMPARABILITY_MATRIX_V1_SCHEMA: &str = "comparability_matrix_v1";
18pub const REFUTER_RESULT_V1_SCHEMA: &str = "refuter_result_v1";
19pub const DECISION_TRACE_V1_SCHEMA: &str = "decision_trace_v1";
20pub const DISPUTE_BUNDLE_V1_SCHEMA: &str = "dispute_bundle_v1";
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
24#[serde(rename_all = "snake_case")]
25pub enum ExperimentRiskClassV1 {
26 PromotionAdjacent,
27 ObservationOnly,
28 ResearchOnly,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
33#[serde(rename_all = "snake_case")]
34pub enum ExperimentLifecycleStateV1 {
35 Scheduled,
36 Running,
37 Completed,
38 Cancelled,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
43#[serde(rename_all = "snake_case")]
44pub enum ExperimentFinalDispositionV1 {
45 Pending,
46 Promoted,
47 Blocked,
48 RolledBack,
49 AdvisoryOnly,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
55pub struct ExperimentCaseV1 {
56 pub schema_version: String,
57 pub experiment_case_id: ExperimentCaseId,
58 pub intervention_id: InterventionId,
59 pub outcome_schema_id: OutcomeSchemaId,
60 pub cohort_contract_id: CohortContractId,
61 pub comparability_matrix_id: ComparabilityMatrixId,
62 pub refuter_suite_id: RefuterSuiteId,
63 pub experiment_budget_id: ExperimentBudgetId,
64 pub risk_class: ExperimentRiskClassV1,
65 pub lifecycle_state: ExperimentLifecycleStateV1,
66 pub final_disposition: ExperimentFinalDispositionV1,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
71pub struct ComparabilityMatrixV1 {
72 pub schema_version: String,
73 pub comparability_matrix_id: ComparabilityMatrixId,
74 pub workload_comparability: String,
75 pub environment_comparability: String,
76 pub config_comparability: String,
77 pub time_window_comparability: String,
78 pub retry_replay_comparability: String,
79 #[serde(default, skip_serializing_if = "Vec::is_empty")]
80 pub missingness_markers: Vec<String>,
81 pub admissibility_judgment: EvidenceAdmissibilityV1,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
86#[serde(rename_all = "snake_case")]
87pub enum RefuterKindV1 {
88 NegativeControl,
89 Placebo,
90 SubsetStability,
91 HumanReview,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
96#[serde(rename_all = "snake_case")]
97pub enum RefuterStateV1 {
98 Pass,
99 Fail,
100 Skipped,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
105#[serde(rename_all = "snake_case")]
106pub enum RefuterPromotionImpactV1 {
107 KeepsCanaryEligible,
108 BlocksPromotion,
109 RequiresHumanReview,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
114pub struct RefuterResultV1 {
115 pub schema_version: String,
116 pub refuter_result_id: RefuterResultId,
117 pub refuter_kind: RefuterKindV1,
118 pub counterfactual_slice_id: CounterfactualSliceId,
119 pub result_summary: String,
120 pub state: RefuterStateV1,
121 pub replay_linkage: String,
122 pub promotion_impact: RefuterPromotionImpactV1,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
127#[serde(rename_all = "snake_case")]
128pub enum ExactnessSpendV1 {
129 Low,
130 Medium,
131 High,
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
136#[serde(rename_all = "snake_case")]
137pub enum SelectedDecisionV1 {
138 CanaryOnly,
139 RetryExact,
140 HumanReview,
141 Rollback,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
147pub struct DecisionTraceV1 {
148 pub schema_version: String,
149 pub decision_trace_id: DecisionTraceId,
150 #[serde(default, skip_serializing_if = "Vec::is_empty")]
151 pub triggering_artifact_refs: Vec<String>,
152 pub intervention_id: InterventionId,
153 pub counterfactual_slice_id: CounterfactualSliceId,
154 #[serde(default, skip_serializing_if = "Vec::is_empty")]
155 pub refuters_satisfied: Vec<String>,
156 #[serde(default, skip_serializing_if = "Vec::is_empty")]
157 pub refuters_failed: Vec<String>,
158 #[serde(default, skip_serializing_if = "Vec::is_empty")]
159 pub refuters_skipped: Vec<String>,
160 pub exactness_spend: ExactnessSpendV1,
161 #[serde(default, skip_serializing_if = "Vec::is_empty")]
162 pub policy_basis: Vec<String>,
163 pub selected_decision: SelectedDecisionV1,
164 #[serde(default, skip_serializing_if = "Vec::is_empty")]
165 pub rollback_prerequisites: Vec<String>,
166 #[serde(default, skip_serializing_if = "Vec::is_empty")]
167 pub cheap_next_checks: Vec<String>,
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
172#[serde(rename_all = "snake_case")]
173pub enum DisputeDispositionV1 {
174 Open,
175 Resolved,
176 Escalated,
177 Rejected,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
182pub struct DisputeBundleV1 {
183 pub schema_version: String,
184 pub dispute_bundle_id: DisputeBundleId,
185 #[serde(default, skip_serializing_if = "Vec::is_empty")]
186 pub challenged_artifact_refs: Vec<String>,
187 pub basis_of_challenge: String,
188 #[serde(default, skip_serializing_if = "Vec::is_empty")]
189 pub counterevidence_refs: Vec<String>,
190 pub replay_or_recheck_request: String,
191 pub escalation_target: String,
192 pub current_disposition: DisputeDispositionV1,
193}
194
195impl ExperimentCaseV1 {
196 pub fn validate(&self) -> Result<(), String> {
198 if self.schema_version != EXPERIMENT_CASE_V1_SCHEMA {
199 return Err(format!(
200 "expected schema_version `{EXPERIMENT_CASE_V1_SCHEMA}`, found `{}`",
201 self.schema_version
202 ));
203 }
204 if matches!(
205 self.final_disposition,
206 ExperimentFinalDispositionV1::Pending
207 ) && matches!(self.lifecycle_state, ExperimentLifecycleStateV1::Completed)
208 {
209 return Err("completed experiment case cannot retain pending disposition".into());
210 }
211 Ok(())
212 }
213}
214
215impl ComparabilityMatrixV1 {
216 pub fn validate(&self) -> Result<(), String> {
218 if self.schema_version != COMPARABILITY_MATRIX_V1_SCHEMA {
219 return Err(format!(
220 "expected schema_version `{COMPARABILITY_MATRIX_V1_SCHEMA}`, found `{}`",
221 self.schema_version
222 ));
223 }
224 Ok(())
225 }
226}
227
228impl RefuterResultV1 {
229 pub fn validate(&self) -> Result<(), String> {
231 if self.schema_version != REFUTER_RESULT_V1_SCHEMA {
232 return Err(format!(
233 "expected schema_version `{REFUTER_RESULT_V1_SCHEMA}`, found `{}`",
234 self.schema_version
235 ));
236 }
237 if matches!(self.state, RefuterStateV1::Pass)
238 && matches!(
239 self.promotion_impact,
240 RefuterPromotionImpactV1::BlocksPromotion
241 )
242 {
243 return Err("passing refuter result cannot directly block promotion".into());
244 }
245 Ok(())
246 }
247}
248
249impl DecisionTraceV1 {
250 pub fn validate(&self) -> Result<(), String> {
252 if self.schema_version != DECISION_TRACE_V1_SCHEMA {
253 return Err(format!(
254 "expected schema_version `{DECISION_TRACE_V1_SCHEMA}`, found `{}`",
255 self.schema_version
256 ));
257 }
258 if self.policy_basis.is_empty() {
259 return Err("decision trace requires at least one policy basis reference".into());
260 }
261 Ok(())
262 }
263}
264
265impl DisputeBundleV1 {
266 pub fn validate(&self) -> Result<(), String> {
268 if self.schema_version != DISPUTE_BUNDLE_V1_SCHEMA {
269 return Err(format!(
270 "expected schema_version `{DISPUTE_BUNDLE_V1_SCHEMA}`, found `{}`",
271 self.schema_version
272 ));
273 }
274 if self.challenged_artifact_refs.is_empty() {
275 return Err("dispute bundle requires at least one challenged artifact ref".into());
276 }
277 Ok(())
278 }
279}