1use std::collections::BTreeMap;
11
12use serde::{Deserialize, Serialize};
13
14use crate::decimal::Decimal;
15use crate::definition::{ParamSource, Predicate, ProvenanceClass, TypeDecl};
16use crate::grade::Grade;
17use crate::ids::ContentDigest;
18use crate::time::{DurationMs, Timestamp};
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
22#[serde(deny_unknown_fields)]
23pub struct Actionability {
24 pub caller_controlled: Vec<ProvenanceClass>,
27 pub remediations: Vec<RemediationDecl>,
28 pub questions: Vec<QuestionDecl>,
29 pub constraint_groups: Vec<ConstraintGroup>,
30 pub checks: Vec<CheckDecl>,
31}
32
33#[derive(
36 Debug,
37 Clone,
38 Copy,
39 PartialEq,
40 Eq,
41 PartialOrd,
42 Ord,
43 Hash,
44 Serialize,
45 Deserialize,
46 schemars::JsonSchema,
47)]
48#[serde(rename_all = "snake_case")]
49pub enum Party {
50 None,
51 Caller,
52 Host,
53 Upstream,
54}
55
56impl Party {
57 pub fn combine<I: IntoIterator<Item = Party>>(parties: I) -> Party {
60 parties.into_iter().max().unwrap_or(Party::None)
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
66#[serde(deny_unknown_fields)]
67pub struct RemediationDecl {
68 pub code: String,
70 #[serde(rename = "for")]
72 pub for_party: Party,
73 pub params: Vec<RemediationParam>,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
77#[serde(deny_unknown_fields)]
78pub struct RemediationParam {
79 pub name: String,
80 #[serde(rename = "type")]
82 pub ty: TypeDecl,
83}
84
85#[derive(
87 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
88)]
89#[serde(deny_unknown_fields)]
90pub struct QuestionDecl {
91 pub id: String,
92 pub fields: Vec<String>,
94 pub text: String,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
100#[serde(deny_unknown_fields)]
101pub struct ConstraintGroup {
102 pub name: String,
103 pub checks: Vec<String>,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
109#[serde(deny_unknown_fields)]
110pub struct CheckDecl {
111 pub id: String,
112 pub subject: String,
114 pub test: Predicate,
116 pub on_fail: OnFail,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
120#[serde(deny_unknown_fields)]
121pub struct OnFail {
122 pub by_provenance: BTreeMap<ProvenanceClass, Consequence>,
123 pub default: DefaultConsequence,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
128#[serde(rename_all = "snake_case")]
129pub enum DefaultConsequence {
130 None,
131 #[serde(untagged)]
132 Consequence(Consequence),
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
138#[serde(rename_all = "snake_case", deny_unknown_fields)]
139pub enum Consequence {
140 Refuse {
141 remediation: RemediationUse,
142 #[serde(default, skip_serializing_if = "Address::is_computed")]
143 address: Address,
144 },
145 DropField {
146 notice: String,
147 #[serde(default, skip_serializing_if = "Address::is_computed")]
148 address: Address,
149 },
150 Relax {
151 group: String,
152 notice: String,
153 #[serde(default, skip_serializing_if = "Address::is_computed")]
154 address: Address,
155 },
156 DowngradeGrade {
157 to: Grade,
159 notice: String,
160 #[serde(default, skip_serializing_if = "Address::is_computed")]
161 address: Address,
162 },
163 Abstain,
164 Escalate {
165 reason: String,
166 },
167 ContinueWithNotice {
168 notice: String,
169 #[serde(default, skip_serializing_if = "Address::is_computed")]
170 address: Address,
171 },
172}
173
174impl Consequence {
175 pub fn is_terminal(&self) -> bool {
177 matches!(
178 self,
179 Consequence::Refuse { .. } | Consequence::Abstain | Consequence::Escalate { .. }
180 )
181 }
182
183 pub fn kind(&self) -> &'static str {
185 match self {
186 Consequence::Refuse { .. } => "refuse",
187 Consequence::DropField { .. } => "drop_field",
188 Consequence::Relax { .. } => "relax",
189 Consequence::DowngradeGrade { .. } => "downgrade_grade",
190 Consequence::Abstain => "abstain",
191 Consequence::Escalate { .. } => "escalate",
192 Consequence::ContinueWithNotice { .. } => "continue_with_notice",
193 }
194 }
195}
196
197#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
200#[serde(rename_all = "snake_case")]
201pub enum Address {
202 #[default]
203 Computed,
204 #[serde(untagged)]
205 Override(AddressOverride),
206}
207
208impl Address {
209 pub fn is_computed(&self) -> bool {
210 matches!(self, Address::Computed)
211 }
212}
213
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
215#[serde(deny_unknown_fields)]
216pub struct AddressOverride {
217 pub to: Party,
219 pub reason: String,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
226#[serde(untagged)]
227pub enum RemediationUse {
228 Code(String),
229 WithParams(RemediationCall),
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
233#[serde(deny_unknown_fields)]
234pub struct RemediationCall {
235 pub code: String,
236 pub params: BTreeMap<String, ParamSource>,
237}
238
239impl RemediationUse {
240 pub fn code(&self) -> &str {
241 match self {
242 RemediationUse::Code(c) => c,
243 RemediationUse::WithParams(c) => &c.code,
244 }
245 }
246}
247
248pub const BUILTIN_REMEDIATIONS: &[&str] = &[
250 "provide_fields",
251 "refresh_fields",
252 "correct_fields",
253 "resolve_conflict",
254 "improve_evidence",
255 "retry",
256 "report_backend",
257 "raise_budget",
258 "provide_capability",
259 "retry_after",
260];
261
262#[derive(
265 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
266)]
267#[serde(rename_all = "snake_case", deny_unknown_fields)]
268pub enum RemediationValue {
269 Bool(bool),
270 Integer(i64),
271 Decimal(Decimal),
272 Text(String),
273 Enum(String),
274 Timestamp(Timestamp),
275 Duration(DurationMs),
276 List(Vec<RemediationValue>),
277}
278
279#[derive(
282 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
283)]
284#[serde(rename_all = "snake_case")]
285pub enum Remediation {
286 None,
287 #[serde(untagged)]
288 Some {
289 code: String,
290 params: BTreeMap<String, RemediationValue>,
291 },
292}
293
294impl Remediation {
295 pub fn code(code: &str) -> Remediation {
296 Remediation::Some {
297 code: code.into(),
298 params: BTreeMap::new(),
299 }
300 }
301
302 pub fn with(
303 code: &str,
304 params: impl IntoIterator<Item = (String, RemediationValue)>,
305 ) -> Remediation {
306 Remediation::Some {
307 code: code.into(),
308 params: params.into_iter().collect(),
309 }
310 }
311
312 pub fn texts<I: IntoIterator<Item = String>>(items: I) -> RemediationValue {
314 RemediationValue::List(items.into_iter().map(RemediationValue::Text).collect())
315 }
316}
317
318#[derive(
320 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, schemars::JsonSchema,
321)]
322#[serde(rename_all = "snake_case", deny_unknown_fields)]
323pub enum CauseProvenance {
324 Observed { class: ProvenanceClass },
326 Accepted { classes: Vec<ProvenanceClass> },
328}
329
330#[derive(
332 Debug,
333 Clone,
334 Copy,
335 Default,
336 PartialEq,
337 Eq,
338 PartialOrd,
339 Ord,
340 Hash,
341 Serialize,
342 Deserialize,
343 schemars::JsonSchema,
344)]
345#[serde(rename_all = "snake_case")]
346pub enum Origin {
347 #[default]
348 Upstream,
349 Host,
350}
351
352#[derive(
354 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, schemars::JsonSchema,
355)]
356#[serde(rename_all = "snake_case", deny_unknown_fields)]
357pub enum Cause {
358 Input {
359 field: String,
360 provenance: CauseProvenance,
361 },
362 Backend {
363 step: String,
364 origin: Origin,
365 },
366 Budget {
367 resource: String,
368 },
369 Deadline,
370 Capability {
371 name: String,
372 },
373 Policy {
375 site: String,
376 },
377}
378
379#[derive(
381 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
382)]
383#[serde(rename_all = "snake_case")]
384pub enum OverrideBasis {
385 Declared,
386 Remediation,
387}
388
389#[derive(
391 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
392)]
393#[serde(rename_all = "snake_case")]
394pub enum Override {
395 None,
396 #[serde(untagged)]
397 Applied {
398 to: Party,
399 basis: OverrideBasis,
400 reason: String,
401 },
402}
403
404#[derive(
407 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
408)]
409#[serde(deny_unknown_fields)]
410pub struct Action {
411 pub addressed_to: Party,
412 pub computed: Party,
413 #[serde(rename = "override")]
414 pub override_: Override,
415 pub remediation: Remediation,
416 pub caused_by: Vec<Cause>,
418 pub transient: bool,
419}
420
421#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
423#[serde(deny_unknown_fields)]
424pub struct Refusal {
425 pub check: String,
426 pub subject: String,
427 pub action: Action,
428}
429
430pub type Question = QuestionDecl;
433
434#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
436#[serde(deny_unknown_fields)]
437pub struct NeedsInput {
438 pub fields: Vec<String>,
439 pub questions: Vec<Question>,
440 pub continuation: ContentDigest,
442 pub action: Action,
443}
444
445#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
447pub struct ContinuationInputs<'a> {
448 pub plan_id: &'a str,
449 pub snapshot_id: &'a str,
450 pub evaluation_time_ms: i64,
451 pub site: &'a str,
452 pub fields: &'a [String],
453 pub questions: &'a [String],
454}
455
456pub const CONTINUATION_TAG: &str = "rustev.continuation/1";
458
459#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
461#[serde(rename_all = "snake_case", deny_unknown_fields)]
462pub enum CheckStatus {
463 Passed,
464 Failed {
465 class: ProvenanceClass,
466 consequence: String,
468 },
469 NotEvaluated {
470 reason: crate::judgment::Unresolved,
471 },
472 Unevaluable {
473 reason: crate::judgment::Unresolved,
474 },
475 Relaxed {
476 by: String,
477 },
478}
479
480#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
482#[serde(deny_unknown_fields)]
483pub struct CheckRecord {
484 pub check: String,
485 pub subject: String,
486 pub status: CheckStatus,
487}
488
489#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
492#[serde(deny_unknown_fields)]
493pub struct PlanActionability {
494 pub checks: Vec<StaticCheck>,
495 pub sites: Vec<StaticSite>,
496}
497
498#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
499#[serde(deny_unknown_fields)]
500pub struct StaticCheck {
501 pub check: String,
502 pub subject: String,
503 pub branches: Vec<StaticBranch>,
506}
507
508#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
509#[serde(deny_unknown_fields)]
510pub struct StaticBranch {
511 pub classes: Vec<ProvenanceClass>,
512 pub consequence: String,
513 pub causes: Vec<Cause>,
514 pub party: Party,
515}
516
517#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
519#[serde(deny_unknown_fields)]
520pub struct StaticSite {
521 pub site: String,
522 pub causes: Vec<Cause>,
523 pub party: Party,
524}
525
526pub const CALLER_CONTROLLABLE: [ProvenanceClass; 3] = [
528 ProvenanceClass::UserSupplied,
529 ProvenanceClass::AuthenticatedAppField,
530 ProvenanceClass::AttributedClaim,
531];
532
533pub fn is_code(s: &str) -> bool {
535 let b = s.as_bytes();
536 !b.is_empty()
537 && b.len() <= 64
538 && b[0].is_ascii_lowercase()
539 && b.iter()
540 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || *c == b'_')
541}