Skip to main content

verification_control/
v14.rs

1//! V14 experimental verification artifact types.
2//!
3//! Defines experiment cases, comparability matrices, refuter results,
4//! decision traces, and dispute bundles used by the causal experiment
5//! and refutation surfaces of the verification pipeline.
6
7use 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/// Risk classification for an experiment case.
23#[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/// Lifecycle state of an experiment case from scheduled through completion.
32#[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/// Terminal disposition of an experiment after completion.
42#[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/// A causal experiment case linking an intervention, outcome schema, cohort,
53/// comparability matrix, refuter suite, and budget into a single verifiable unit.
54#[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/// A matrix evaluating whether experimental conditions are comparable enough for valid inference.
70#[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/// Kind of refutation check applied to an experiment.
85#[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/// Pass/fail/skip state of a refuter check.
95#[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/// Impact of a refuter result on promotion eligibility.
104#[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/// Result of a single refuter check against a counterfactual slice.
113#[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/// Budget tier of exactness spend for a decision trace.
126#[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/// Decision selected by the decision trace for an experiment outcome.
135#[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/// Trace record capturing the full decision path from triggering artifacts
145/// through refuter outcomes to the selected promotion or rollback decision.
146#[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/// Current disposition of a dispute bundle.
171#[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/// A dispute challenging one or more artifacts with counterevidence and escalation target.
181#[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    /// Rejects experiment cases with invalid schema versions or terminal-state drift.
197    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    /// Rejects comparability matrices with invalid schema versions.
217    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    /// Rejects refuter results whose pass/fail state conflicts with promotion impact.
230    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    /// Rejects decision traces with invalid schema versions or empty policy basis.
251    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    /// Rejects dispute bundles with invalid schema versions or empty challenge scope.
267    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}