Skip to main content

verification_policy/
v14.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use stack_ids::{
4    ArtifactAdmissionPolicyId, DisclosureBudgetId, DisclosurePolicyId, ExperimentBudgetId,
5    RefuterSuiteId, TrustRootSetId,
6};
7
8pub const REFUTER_SUITE_V1_SCHEMA: &str = "refuter_suite_v1";
9pub const EXPERIMENT_BUDGET_V1_SCHEMA: &str = "experiment_budget_v1";
10pub const ARTIFACT_ADMISSION_POLICY_V1_SCHEMA: &str = "artifact_admission_policy_v1";
11pub const DISCLOSURE_POLICY_V1_SCHEMA: &str = "disclosure_policy_v1";
12pub const DISCLOSURE_BUDGET_V1_SCHEMA: &str = "disclosure_budget_v1";
13
14fn require_schema_version(
15    found: &str,
16    expected: &'static str,
17    field: &'static str,
18) -> Result<(), &'static str> {
19    if found != expected {
20        return Err(field);
21    }
22    Ok(())
23}
24
25fn require_non_empty(value: &str, field: &'static str) -> Result<(), &'static str> {
26    if value.trim().is_empty() {
27        return Err(field);
28    }
29    Ok(())
30}
31
32#[derive(
33    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
34)]
35#[serde(rename_all = "snake_case")]
36pub enum ExperimentBudgetClassV1 {
37    Small,
38    Medium,
39    Large,
40}
41
42#[derive(
43    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
44)]
45#[serde(rename_all = "snake_case")]
46pub enum ExperimentMaxExactnessV1 {
47    BoundedExact,
48}
49
50#[derive(
51    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
52)]
53#[serde(rename_all = "snake_case")]
54pub enum RefuterAllowanceV1 {
55    #[serde(rename = "up_to_1")]
56    UpTo1,
57    #[serde(rename = "up_to_3")]
58    UpTo3,
59}
60
61#[derive(
62    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
63)]
64#[serde(rename_all = "snake_case")]
65pub enum OracleAllowanceV1 {
66    OneExactSlice,
67}
68
69#[derive(
70    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
71)]
72#[serde(rename_all = "snake_case")]
73pub enum ReplayAllowanceV1 {
74    FullReplayFamily,
75}
76
77#[derive(
78    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
79)]
80#[serde(rename_all = "snake_case")]
81pub enum HumanReviewAllowanceV1 {
82    OneRequiredIfRollout,
83}
84
85#[derive(
86    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
87)]
88#[serde(rename_all = "snake_case")]
89pub enum BudgetExhaustionBehaviorV1 {
90    EmitTypedDegradationAndHoldAdvisory,
91}
92
93#[derive(
94    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
95)]
96#[serde(rename_all = "snake_case")]
97pub enum PolicyReplayabilityClassV1 {
98    ReplayableWithTicket,
99}
100
101#[derive(
102    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
103)]
104#[serde(rename_all = "snake_case")]
105pub enum PolicyDowngradeBehaviorV1 {
106    AdvisoryOnlyIfReplayMissing,
107    AdvisoryOnlyIfExactMaterialCannotBeShared,
108}
109
110#[derive(
111    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
112)]
113#[serde(rename_all = "snake_case")]
114pub enum DisclosureReplayVisibilityV1 {
115    TicketRequired,
116}
117
118#[derive(
119    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
120)]
121#[serde(rename_all = "snake_case")]
122pub enum DisclosureRevealClassV1 {
123    #[serde(rename = "structured-redacted")]
124    StructuredRedacted,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
128pub struct RefuterSuiteV1 {
129    pub schema_version: String,
130    pub refuter_suite_id: RefuterSuiteId,
131    pub treatment_definition_check: bool,
132    pub negative_control_option: bool,
133    pub placebo_option: bool,
134    pub dummy_outcome_option: bool,
135    pub subset_stability_option: bool,
136    pub alternative_comparison_option: bool,
137    #[serde(default, skip_serializing_if = "Vec::is_empty")]
138    pub admissibility_notes: Vec<String>,
139    pub cost_metadata: String,
140}
141
142impl RefuterSuiteV1 {
143    #[allow(clippy::too_many_arguments)]
144    /// Builds a refuter-suite profile with an explicit cost envelope.
145    pub fn new(
146        refuter_suite_id: RefuterSuiteId,
147        treatment_definition_check: bool,
148        negative_control_option: bool,
149        placebo_option: bool,
150        dummy_outcome_option: bool,
151        subset_stability_option: bool,
152        alternative_comparison_option: bool,
153        admissibility_notes: Vec<String>,
154        cost_metadata: impl Into<String>,
155    ) -> Result<Self, &'static str> {
156        let value = Self {
157            schema_version: REFUTER_SUITE_V1_SCHEMA.to_string(),
158            refuter_suite_id,
159            treatment_definition_check,
160            negative_control_option,
161            placebo_option,
162            dummy_outcome_option,
163            subset_stability_option,
164            alternative_comparison_option,
165            admissibility_notes,
166            cost_metadata: cost_metadata.into(),
167        };
168        value.validate()?;
169        Ok(value)
170    }
171
172    /// Validates schema identity, non-empty cost metadata, and that at least
173    /// one refuter option is enabled.
174    pub fn validate(&self) -> Result<(), &'static str> {
175        require_schema_version(
176            &self.schema_version,
177            REFUTER_SUITE_V1_SCHEMA,
178            "schema_version",
179        )?;
180        require_non_empty(&self.cost_metadata, "cost_metadata")?;
181        if !self.treatment_definition_check
182            && !self.negative_control_option
183            && !self.placebo_option
184            && !self.dummy_outcome_option
185            && !self.subset_stability_option
186            && !self.alternative_comparison_option
187        {
188            return Err("refuter_suite_options");
189        }
190        Ok(())
191    }
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
195pub struct ExperimentBudgetV1 {
196    pub schema_version: String,
197    pub experiment_budget_id: ExperimentBudgetId,
198    pub budget_class: ExperimentBudgetClassV1,
199    pub max_exactness: ExperimentMaxExactnessV1,
200    pub refuter_allowance: RefuterAllowanceV1,
201    pub oracle_allowance: OracleAllowanceV1,
202    pub replay_allowance: ReplayAllowanceV1,
203    pub human_review_allowance: HumanReviewAllowanceV1,
204    pub exhaustion_behavior: BudgetExhaustionBehaviorV1,
205}
206
207impl ExperimentBudgetV1 {
208    #[allow(clippy::too_many_arguments)]
209    /// Builds a typed experiment budget profile.
210    pub fn new(
211        experiment_budget_id: ExperimentBudgetId,
212        budget_class: ExperimentBudgetClassV1,
213        max_exactness: ExperimentMaxExactnessV1,
214        refuter_allowance: RefuterAllowanceV1,
215        oracle_allowance: OracleAllowanceV1,
216        replay_allowance: ReplayAllowanceV1,
217        human_review_allowance: HumanReviewAllowanceV1,
218        exhaustion_behavior: BudgetExhaustionBehaviorV1,
219    ) -> Result<Self, &'static str> {
220        let value = Self {
221            schema_version: EXPERIMENT_BUDGET_V1_SCHEMA.to_string(),
222            experiment_budget_id,
223            budget_class,
224            max_exactness,
225            refuter_allowance,
226            oracle_allowance,
227            replay_allowance,
228            human_review_allowance,
229            exhaustion_behavior,
230        };
231        value.validate()?;
232        Ok(value)
233    }
234
235    /// Validates the experiment-budget schema identity.
236    pub fn validate(&self) -> Result<(), &'static str> {
237        require_schema_version(
238            &self.schema_version,
239            EXPERIMENT_BUDGET_V1_SCHEMA,
240            "schema_version",
241        )
242    }
243}
244
245#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
246pub struct ArtifactAdmissionPolicyV1 {
247    pub schema_version: String,
248    pub artifact_admission_policy_id: ArtifactAdmissionPolicyId,
249    #[serde(default, skip_serializing_if = "Vec::is_empty")]
250    pub allowed_artifact_families: Vec<String>,
251    #[serde(default, skip_serializing_if = "Vec::is_empty")]
252    pub required_trust_root_sets: Vec<TrustRootSetId>,
253    #[serde(default, skip_serializing_if = "Vec::is_empty")]
254    pub required_transparency_obligations: Vec<String>,
255    pub required_replayability_class: PolicyReplayabilityClassV1,
256    #[serde(default, skip_serializing_if = "Vec::is_empty")]
257    pub disclosure_constraints: Vec<String>,
258    pub downgrade_behavior: PolicyDowngradeBehaviorV1,
259    #[serde(default, skip_serializing_if = "Vec::is_empty")]
260    pub disqualifying_failures: Vec<String>,
261}
262
263impl ArtifactAdmissionPolicyV1 {
264    #[allow(clippy::too_many_arguments)]
265    /// Builds a typed artifact-admission policy.
266    pub fn new(
267        artifact_admission_policy_id: ArtifactAdmissionPolicyId,
268        allowed_artifact_families: Vec<String>,
269        required_trust_root_sets: Vec<TrustRootSetId>,
270        required_transparency_obligations: Vec<String>,
271        required_replayability_class: PolicyReplayabilityClassV1,
272        disclosure_constraints: Vec<String>,
273        downgrade_behavior: PolicyDowngradeBehaviorV1,
274        disqualifying_failures: Vec<String>,
275    ) -> Result<Self, &'static str> {
276        let value = Self {
277            schema_version: ARTIFACT_ADMISSION_POLICY_V1_SCHEMA.to_string(),
278            artifact_admission_policy_id,
279            allowed_artifact_families,
280            required_trust_root_sets,
281            required_transparency_obligations,
282            required_replayability_class,
283            disclosure_constraints,
284            downgrade_behavior,
285            disqualifying_failures,
286        };
287        value.validate()?;
288        Ok(value)
289    }
290
291    /// Validates the artifact-admission policy schema identity.
292    pub fn validate(&self) -> Result<(), &'static str> {
293        require_schema_version(
294            &self.schema_version,
295            ARTIFACT_ADMISSION_POLICY_V1_SCHEMA,
296            "schema_version",
297        )
298    }
299}
300
301#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
302pub struct DisclosurePolicyV1 {
303    pub schema_version: String,
304    pub disclosure_policy_id: DisclosurePolicyId,
305    #[serde(default, skip_serializing_if = "Vec::is_empty")]
306    pub allowed_consumers: Vec<String>,
307    #[serde(default, skip_serializing_if = "Vec::is_empty")]
308    pub redaction_rules: Vec<String>,
309    pub replay_visibility: DisclosureReplayVisibilityV1,
310    pub retention_window: String,
311    pub downgrade_behavior: PolicyDowngradeBehaviorV1,
312}
313
314impl DisclosurePolicyV1 {
315    /// Builds a typed disclosure policy.
316    pub fn new(
317        disclosure_policy_id: DisclosurePolicyId,
318        allowed_consumers: Vec<String>,
319        redaction_rules: Vec<String>,
320        replay_visibility: DisclosureReplayVisibilityV1,
321        retention_window: impl Into<String>,
322        downgrade_behavior: PolicyDowngradeBehaviorV1,
323    ) -> Result<Self, &'static str> {
324        let value = Self {
325            schema_version: DISCLOSURE_POLICY_V1_SCHEMA.to_string(),
326            disclosure_policy_id,
327            allowed_consumers,
328            redaction_rules,
329            replay_visibility,
330            retention_window: retention_window.into(),
331            downgrade_behavior,
332        };
333        value.validate()?;
334        Ok(value)
335    }
336
337    /// Validates disclosure-policy schema identity and retention metadata.
338    pub fn validate(&self) -> Result<(), &'static str> {
339        require_schema_version(
340            &self.schema_version,
341            DISCLOSURE_POLICY_V1_SCHEMA,
342            "schema_version",
343        )?;
344        require_non_empty(&self.retention_window, "retention_window")?;
345        Ok(())
346    }
347}
348
349#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
350pub struct DisclosureBudgetV1 {
351    pub schema_version: String,
352    pub disclosure_budget_id: DisclosureBudgetId,
353    pub allowed_reveal_class: DisclosureRevealClassV1,
354    pub current_spend: String,
355    pub redaction_ceiling: String,
356    pub escalation_path: String,
357}
358
359impl DisclosureBudgetV1 {
360    /// Builds a typed disclosure budget.
361    pub fn new(
362        disclosure_budget_id: DisclosureBudgetId,
363        allowed_reveal_class: DisclosureRevealClassV1,
364        current_spend: impl Into<String>,
365        redaction_ceiling: impl Into<String>,
366        escalation_path: impl Into<String>,
367    ) -> Result<Self, &'static str> {
368        let value = Self {
369            schema_version: DISCLOSURE_BUDGET_V1_SCHEMA.to_string(),
370            disclosure_budget_id,
371            allowed_reveal_class,
372            current_spend: current_spend.into(),
373            redaction_ceiling: redaction_ceiling.into(),
374            escalation_path: escalation_path.into(),
375        };
376        value.validate()?;
377        Ok(value)
378    }
379
380    /// Validates disclosure-budget schema identity and required budget text
381    /// fields.
382    pub fn validate(&self) -> Result<(), &'static str> {
383        require_schema_version(
384            &self.schema_version,
385            DISCLOSURE_BUDGET_V1_SCHEMA,
386            "schema_version",
387        )?;
388        require_non_empty(&self.current_spend, "current_spend")?;
389        require_non_empty(&self.redaction_ceiling, "redaction_ceiling")?;
390        require_non_empty(&self.escalation_path, "escalation_path")?;
391        Ok(())
392    }
393}