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    #[serde(
64        default,
65        skip_serializing_if = "Option::is_none",
66        deserialize_with = "crate::screening::present"
67    )]
68    pub extraction: Option<Vec<crate::extraction_results::ExtractionEvidence>>,
69    #[serde(
70        default,
71        skip_serializing_if = "Option::is_none",
72        deserialize_with = "crate::screening::present"
73    )]
74    pub acceptance: Option<Vec<crate::extraction_results::AcceptanceEvidence>>,
75}
76
77crate::document::versioned_document!(
78    EvidenceRecord,
79    [schema::EVIDENCE, schema::EVIDENCE_V2],
80    "rustev.evidence/1 or rustev.evidence/2",
81    RECORD_V1,
82    check = EvidenceRecord::screening_version
83);
84
85impl EvidenceRecord {
86    pub(crate) fn screening_version(&self) -> Result<(), String> {
87        self.judgment.extraction_version()?;
88        if self.schema != schema::EVIDENCE_V2
89            && (self.extraction.is_some()
90                || self.acceptance.is_some()
91                || self.inputs.iter().any(|i| i.producer.is_some())
92                || self.judgment.lineage.producers.is_some())
93        {
94            return Err("extraction, acceptance and producers require rustev.evidence/2".into());
95        }
96        if self.schema != schema::EVIDENCE_V2
97            && (self.acquisitions.is_some()
98                || self
99                    .steps
100                    .iter()
101                    .any(|s| s.constructed || matches!(s.status, StepStatus::Skipped { .. })))
102        {
103            return Err(
104                "acquisitions, constructed values and skipped steps require rustev.evidence/2"
105                    .into(),
106            );
107        }
108        if self.schema != schema::EVIDENCE_V2
109            && self
110                .steps
111                .iter()
112                .any(|s| s.ranking.is_some() || s.selection.is_some())
113        {
114            return Err("ranking and selection require rustev.evidence/2".into());
115        }
116        if self.generation.is_some() && self.schema != schema::EVIDENCE_V2 {
117            return Err("generation requires rustev.evidence/2".into());
118        }
119        if self.screening.is_some() && self.schema != schema::EVIDENCE_V2 {
120            return Err("screening requires rustev.evidence/2".into());
121        }
122        Ok(())
123    }
124}
125
126#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
127#[serde(deny_unknown_fields)]
128pub struct InputEvidence {
129    pub field: String,
130    pub status: InputStatus,
131    /// Evidence 2 (spec 032, 3.10.1): the input's value metadata.
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub meta: Option<ValueMeta>,
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub presence: Option<Presence>,
136    /// Evidence 2, graded inputs: every entry considered, in snapshot order.
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub entries: Option<Vec<EntryEvidence>>,
139    /// Evidence 2: how a disagreement was resolved (spec 032, 3.8.4).
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    pub resolution: Option<Resolution>,
142    /// Evidence 2: the check whose `drop_field` made this input absent; its
143    /// entry digest, source and provenance stay recorded (spec 033, 3.4.8).
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub dropped: Option<DroppedBy>,
146    #[serde(
147        default,
148        skip_serializing_if = "Option::is_none",
149        deserialize_with = "crate::screening::present"
150    )]
151    pub producer: Option<crate::snapshot::Producer>,
152}
153
154/// Which check dropped an input (spec 033, 3.4.8).
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
156#[serde(deny_unknown_fields)]
157pub struct DroppedBy {
158    pub check: String,
159}
160
161/// One snapshot entry considered for a graded input.
162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
163#[serde(deny_unknown_fields)]
164pub struct EntryEvidence {
165    pub source: String,
166    pub provenance: ProvenanceClass,
167    #[serde(default, skip_serializing_if = "Option::is_none")]
168    pub claim: Option<Grade>,
169    pub ceiling: ValueGrade,
170    pub effective: ValueGrade,
171    pub digest: String,
172    pub as_of_ms: Timestamp,
173    pub disposition: Disposition,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
177#[serde(rename_all = "snake_case", deny_unknown_fields)]
178pub enum Disposition {
179    Used,
180    Outvoted,
181    Ignored { reason: String },
182    Rejected { reason: String },
183}
184
185/// The rule applied to a disagreement and what it chose.
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
187#[serde(deny_unknown_fields)]
188pub struct Resolution {
189    pub rule: ResolutionRule,
190    pub used: UsedEntry,
191}
192
193/// The conflict rule applied (spec 032, 3.8.1); the source order of
194/// `prefer` and `treat_as_hint` is the plan's.
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
196#[serde(rename_all = "snake_case")]
197pub enum ResolutionRule {
198    RequireAgreement,
199    Prefer,
200    TreatAsHint,
201    Abstain,
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
205#[serde(rename_all = "snake_case", deny_unknown_fields)]
206pub enum UsedEntry {
207    None,
208    /// Index into the input's `entries`.
209    Entry(u64),
210}
211
212#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
213#[serde(rename_all = "snake_case", deny_unknown_fields)]
214pub enum InputStatus {
215    Resolved {
216        provenance: ProvenanceClass,
217        as_of_ms: Timestamp,
218        source: String,
219    },
220    Unresolved(Unresolved),
221}
222
223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
224#[serde(deny_unknown_fields)]
225pub struct StepEvidence {
226    /// Present only for a value derived from a constructed key (036, 3.7.4).
227    #[serde(
228        default,
229        skip_serializing_if = "std::ops::Not::not",
230        deserialize_with = "crate::acquisition_record::constructed"
231    )]
232    #[schemars(schema_with = "crate::acquisition_record::constructed_schema")]
233    pub constructed: bool,
234    pub step: String,
235    pub instance: Vec<String>,
236    pub status: StepStatus,
237    /// Evidence 2: the step value's metadata.
238    #[serde(default, skip_serializing_if = "Option::is_none")]
239    pub meta: Option<ValueMeta>,
240    /// Evidence 2, `join_keyed@1`: each entry's id and match status.
241    #[serde(default, skip_serializing_if = "Option::is_none")]
242    pub join: Option<Vec<JoinEvidence>>,
243    /// Evidence 2, `merge_fields@1`: each merged field.
244    #[serde(default, skip_serializing_if = "Option::is_none")]
245    pub merge: Option<Vec<MergeEvidence>>,
246    /// Evidence 2, `corroborate` (spec 032, 3.7.6).
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    pub corroboration: Option<Corroboration>,
249    /// Spec 039, evidence 2: ids and result metadata only.
250    #[serde(
251        default,
252        skip_serializing_if = "Option::is_none",
253        deserialize_with = "crate::screening::present"
254    )]
255    #[schemars(with = "crate::ranking_result::RankingEvidence")]
256    pub ranking: Option<crate::ranking_result::RankingEvidence>,
257    #[serde(
258        default,
259        skip_serializing_if = "Option::is_none",
260        deserialize_with = "crate::screening::present"
261    )]
262    #[schemars(with = "crate::ranking_result::Selection")]
263    pub selection: Option<crate::ranking_result::Selection>,
264}
265
266#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
267#[serde(rename_all = "snake_case")]
268pub enum MatchStatus {
269    Matched,
270    Unmatched,
271    MatchedAbsent,
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
275#[serde(deny_unknown_fields)]
276pub struct JoinEvidence {
277    pub id: String,
278    pub status: MatchStatus,
279}
280
281/// One merged field: its side (or why it is absent), rule and grade.
282#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
283#[serde(deny_unknown_fields)]
284pub struct MergeEvidence {
285    /// The item id when merging per item; empty for two records.
286    pub item: String,
287    pub field: String,
288    /// `overlay_only`, `base_only` or `overlay_else_base_if_grade_at_least`.
289    pub rule: String,
290    pub result: MergedField,
291}
292
293#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
294#[serde(rename_all = "snake_case", deny_unknown_fields)]
295pub enum MergedField {
296    Taken { side: MergeSide, grade: ValueGrade },
297    Absent { reason: AbsentReason },
298}
299
300#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
301#[serde(rename_all = "snake_case")]
302pub enum CorroborationOutcome {
303    Corroborated,
304    Uncorroborated,
305    Contradicted,
306}
307
308/// A corroboration step's record: the claim before and after, the outcome
309/// and every check.
310#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
311#[serde(deny_unknown_fields)]
312pub struct Corroboration {
313    pub claim: String,
314    pub claim_digest: String,
315    pub grade_before: ValueGrade,
316    pub grade_after: ValueGrade,
317    pub outcome: CorroborationOutcome,
318    pub checks: Vec<CheckEvidence>,
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
322#[serde(rename_all = "snake_case")]
323pub enum CheckResult {
324    Supports,
325    Contradicts,
326    NoEvidence,
327}
328
329#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
330#[serde(deny_unknown_fields)]
331pub struct CheckEvidence {
332    pub result: CheckResult,
333    /// Why a check is `no_evidence`, or its declared `on_false`.
334    pub reason: CheckReason,
335    pub reads: Vec<ReadEvidence>,
336}
337
338#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
339#[serde(rename_all = "snake_case", deny_unknown_fields)]
340pub enum CheckReason {
341    /// The predicate was true.
342    True,
343    /// The predicate was false; the declared `on_false` applied.
344    False(OnFalse),
345    /// A read was unresolved or absent.
346    Unresolved(Unresolved),
347    /// A read's grade is below `upgrade_to`.
348    BelowGrade { read: String },
349}
350
351#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
352#[serde(deny_unknown_fields)]
353pub struct ReadEvidence {
354    pub reference: String,
355    /// Absent when the read has no value (it was unresolved or absent).
356    #[serde(default, skip_serializing_if = "Option::is_none")]
357    pub digest: Option<String>,
358    pub grade: ValueGrade,
359}
360
361#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
362#[serde(rename_all = "snake_case", deny_unknown_fields)]
363pub enum StepStatus {
364    Resolved {
365        kind: String,
366        derivation: Derivation,
367    },
368    Unresolved(Unresolved),
369    Skipped {
370        guard: String,
371    },
372}
373
374impl StepEvidence {
375    /// A version 1 step record: no evidence 2 member.
376    pub fn new(step: String, instance: Vec<String>, status: StepStatus) -> Self {
377        StepEvidence {
378            constructed: false,
379            step,
380            instance,
381            status,
382            meta: None,
383            join: None,
384            merge: None,
385            corroboration: None,
386            ranking: None,
387            selection: None,
388        }
389    }
390}