Skip to main content

rustev_contract/
evidence.rs

1//! Evidence records, `rustev.evidence/1`, and `rustev.evidence/2` for plan 3
2//! (spec 032, 3.10): what one decision used and computed. Emitting and storing them is the runtime's job (spec 003); the
3//! pure core builds the record.
4
5use serde::{Deserialize, Serialize};
6
7use crate::actionability::CheckRecord;
8use crate::definition::{OnFalse, ProvenanceClass};
9use crate::grade::{AbsentReason, Grade, MergeSide, Presence, ValueGrade, ValueMeta};
10use crate::ids::{DefinitionId, PlanId, SnapshotId};
11use crate::judgment::{Derivation, Judgment, Unresolved};
12use crate::limits::RECORD_V1;
13use crate::schema;
14use crate::time::Timestamp;
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
17#[serde(deny_unknown_fields)]
18pub struct EvidenceRecord {
19    pub schema: String,
20    /// Per execution, supplied by the caller; not a digest.
21    pub decision_id: String,
22    pub plan_id: PlanId,
23    pub definition_id: DefinitionId,
24    pub snapshot_id: SnapshotId,
25    pub evaluation_time_ms: Timestamp,
26    pub inputs: Vec<InputEvidence>,
27    pub steps: Vec<StepEvidence>,
28    pub judgment: Judgment,
29    /// Evidence 2 with `actionability`: every check, in declaration order
30    /// (spec 033, 3.4.8).
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub checks: Option<Vec<CheckRecord>>,
33    /// One record per screened value, in lowered-step and item order.
34    #[serde(
35        default,
36        skip_serializing_if = "Option::is_none",
37        deserialize_with = "crate::screening::evidence_list"
38    )]
39    #[schemars(with = "Vec<crate::screening::ScreeningEvidence>", length(min = 1))]
40    pub screening: Option<Vec<crate::screening::ScreeningEvidence>>,
41    /// Spec 036: omitted when the plan has no acquisition step.
42    #[serde(
43        default,
44        skip_serializing_if = "Option::is_none",
45        deserialize_with = "crate::acquisition_record::list"
46    )]
47    #[schemars(
48        with = "Vec<crate::acquisition_record::AcquisitionEvidence>",
49        length(min = 1, max = 4096)
50    )]
51    pub acquisitions: Option<Vec<crate::acquisition_record::AcquisitionEvidence>>,
52    /// Spec 037: digest-only field slots, omitted without generate steps.
53    #[serde(
54        default,
55        skip_serializing_if = "Option::is_none",
56        deserialize_with = "crate::generation_result::evidence"
57    )]
58    #[schemars(
59        with = "Vec<crate::generation_result::GenerationEvidence>",
60        length(min = 1, max = 8)
61    )]
62    pub generation: Option<Vec<crate::generation_result::GenerationEvidence>>,
63}
64
65crate::document::versioned_document!(
66    EvidenceRecord,
67    [schema::EVIDENCE, schema::EVIDENCE_V2],
68    "rustev.evidence/1 or rustev.evidence/2",
69    RECORD_V1,
70    check = EvidenceRecord::screening_version
71);
72
73impl EvidenceRecord {
74    fn screening_version(&self) -> Result<(), String> {
75        if self.schema != schema::EVIDENCE_V2
76            && (self.acquisitions.is_some()
77                || self
78                    .steps
79                    .iter()
80                    .any(|s| s.constructed || matches!(s.status, StepStatus::Skipped { .. })))
81        {
82            return Err(
83                "acquisitions, constructed values and skipped steps require rustev.evidence/2"
84                    .into(),
85            );
86        }
87        if self.generation.is_some() && self.schema != schema::EVIDENCE_V2 {
88            return Err("generation requires rustev.evidence/2".into());
89        }
90        if self.screening.is_some() && self.schema != schema::EVIDENCE_V2 {
91            return Err("screening requires rustev.evidence/2".into());
92        }
93        Ok(())
94    }
95}
96
97#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
98#[serde(deny_unknown_fields)]
99pub struct InputEvidence {
100    pub field: String,
101    pub status: InputStatus,
102    /// Evidence 2 (spec 032, 3.10.1): the input's value metadata.
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub meta: Option<ValueMeta>,
105    #[serde(default, skip_serializing_if = "Option::is_none")]
106    pub presence: Option<Presence>,
107    /// Evidence 2, graded inputs: every entry considered, in snapshot order.
108    #[serde(default, skip_serializing_if = "Option::is_none")]
109    pub entries: Option<Vec<EntryEvidence>>,
110    /// Evidence 2: how a disagreement was resolved (spec 032, 3.8.4).
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub resolution: Option<Resolution>,
113    /// Evidence 2: the check whose `drop_field` made this input absent; its
114    /// entry digest, source and provenance stay recorded (spec 033, 3.4.8).
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub dropped: Option<DroppedBy>,
117}
118
119/// Which check dropped an input (spec 033, 3.4.8).
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
121#[serde(deny_unknown_fields)]
122pub struct DroppedBy {
123    pub check: String,
124}
125
126/// One snapshot entry considered for a graded input.
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
128#[serde(deny_unknown_fields)]
129pub struct EntryEvidence {
130    pub source: String,
131    pub provenance: ProvenanceClass,
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub claim: Option<Grade>,
134    pub ceiling: ValueGrade,
135    pub effective: ValueGrade,
136    pub digest: String,
137    pub as_of_ms: Timestamp,
138    pub disposition: Disposition,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
142#[serde(rename_all = "snake_case", deny_unknown_fields)]
143pub enum Disposition {
144    Used,
145    Outvoted,
146    Ignored { reason: String },
147    Rejected { reason: String },
148}
149
150/// The rule applied to a disagreement and what it chose.
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
152#[serde(deny_unknown_fields)]
153pub struct Resolution {
154    pub rule: ResolutionRule,
155    pub used: UsedEntry,
156}
157
158/// The conflict rule applied (spec 032, 3.8.1); the source order of
159/// `prefer` and `treat_as_hint` is the plan's.
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
161#[serde(rename_all = "snake_case")]
162pub enum ResolutionRule {
163    RequireAgreement,
164    Prefer,
165    TreatAsHint,
166    Abstain,
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
170#[serde(rename_all = "snake_case", deny_unknown_fields)]
171pub enum UsedEntry {
172    None,
173    /// Index into the input's `entries`.
174    Entry(u64),
175}
176
177#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
178#[serde(rename_all = "snake_case", deny_unknown_fields)]
179pub enum InputStatus {
180    Resolved {
181        provenance: ProvenanceClass,
182        as_of_ms: Timestamp,
183        source: String,
184    },
185    Unresolved(Unresolved),
186}
187
188#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
189#[serde(deny_unknown_fields)]
190pub struct StepEvidence {
191    /// Present only for a value derived from a constructed key (036, 3.7.4).
192    #[serde(
193        default,
194        skip_serializing_if = "std::ops::Not::not",
195        deserialize_with = "crate::acquisition_record::constructed"
196    )]
197    #[schemars(schema_with = "crate::acquisition_record::constructed_schema")]
198    pub constructed: bool,
199    pub step: String,
200    pub instance: Vec<String>,
201    pub status: StepStatus,
202    /// Evidence 2: the step value's metadata.
203    #[serde(default, skip_serializing_if = "Option::is_none")]
204    pub meta: Option<ValueMeta>,
205    /// Evidence 2, `join_keyed@1`: each entry's id and match status.
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub join: Option<Vec<JoinEvidence>>,
208    /// Evidence 2, `merge_fields@1`: each merged field.
209    #[serde(default, skip_serializing_if = "Option::is_none")]
210    pub merge: Option<Vec<MergeEvidence>>,
211    /// Evidence 2, `corroborate` (spec 032, 3.7.6).
212    #[serde(default, skip_serializing_if = "Option::is_none")]
213    pub corroboration: Option<Corroboration>,
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
217#[serde(rename_all = "snake_case")]
218pub enum MatchStatus {
219    Matched,
220    Unmatched,
221    MatchedAbsent,
222}
223
224#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
225#[serde(deny_unknown_fields)]
226pub struct JoinEvidence {
227    pub id: String,
228    pub status: MatchStatus,
229}
230
231/// One merged field: its side (or why it is absent), rule and grade.
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
233#[serde(deny_unknown_fields)]
234pub struct MergeEvidence {
235    /// The item id when merging per item; empty for two records.
236    pub item: String,
237    pub field: String,
238    /// `overlay_only`, `base_only` or `overlay_else_base_if_grade_at_least`.
239    pub rule: String,
240    pub result: MergedField,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
244#[serde(rename_all = "snake_case", deny_unknown_fields)]
245pub enum MergedField {
246    Taken { side: MergeSide, grade: ValueGrade },
247    Absent { reason: AbsentReason },
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
251#[serde(rename_all = "snake_case")]
252pub enum CorroborationOutcome {
253    Corroborated,
254    Uncorroborated,
255    Contradicted,
256}
257
258/// A corroboration step's record: the claim before and after, the outcome
259/// and every check.
260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
261#[serde(deny_unknown_fields)]
262pub struct Corroboration {
263    pub claim: String,
264    pub claim_digest: String,
265    pub grade_before: ValueGrade,
266    pub grade_after: ValueGrade,
267    pub outcome: CorroborationOutcome,
268    pub checks: Vec<CheckEvidence>,
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
272#[serde(rename_all = "snake_case")]
273pub enum CheckResult {
274    Supports,
275    Contradicts,
276    NoEvidence,
277}
278
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
280#[serde(deny_unknown_fields)]
281pub struct CheckEvidence {
282    pub result: CheckResult,
283    /// Why a check is `no_evidence`, or its declared `on_false`.
284    pub reason: CheckReason,
285    pub reads: Vec<ReadEvidence>,
286}
287
288#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
289#[serde(rename_all = "snake_case", deny_unknown_fields)]
290pub enum CheckReason {
291    /// The predicate was true.
292    True,
293    /// The predicate was false; the declared `on_false` applied.
294    False(OnFalse),
295    /// A read was unresolved or absent.
296    Unresolved(Unresolved),
297    /// A read's grade is below `upgrade_to`.
298    BelowGrade { read: String },
299}
300
301#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
302#[serde(deny_unknown_fields)]
303pub struct ReadEvidence {
304    pub reference: String,
305    /// Absent when the read has no value (it was unresolved or absent).
306    #[serde(default, skip_serializing_if = "Option::is_none")]
307    pub digest: Option<String>,
308    pub grade: ValueGrade,
309}
310
311#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
312#[serde(rename_all = "snake_case", deny_unknown_fields)]
313pub enum StepStatus {
314    Resolved {
315        kind: String,
316        derivation: Derivation,
317    },
318    Unresolved(Unresolved),
319    Skipped {
320        guard: String,
321    },
322}
323
324impl StepEvidence {
325    /// A version 1 step record: no evidence 2 member.
326    pub fn new(step: String, instance: Vec<String>, status: StepStatus) -> Self {
327        StepEvidence {
328            constructed: false,
329            step,
330            instance,
331            status,
332            meta: None,
333            join: None,
334            merge: None,
335            corroboration: None,
336        }
337    }
338}