Skip to main content

rustev_contract/
grade.rs

1//! Evidence grades, value metadata and presence (spec 032, 3.2 to 3.4).
2//!
3//! A grade states evidence quality, never permission: `authoritative` means
4//! "attested live by the system that owns the fact", not "authorized"
5//! (spec 032, 2.1). Grade is metadata, not a value kind, and is orthogonal
6//! to derivation class.
7
8use std::collections::BTreeMap;
9
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13use crate::canonical::{CanonicalError, canonical_value_bytes, tagged_digest};
14use crate::definition::ProvenanceClass;
15use crate::time::Timestamp;
16
17/// The tag of a value digest (spec 032, 3.1.5).
18pub const VALUE_TAG: &str = "rustev.value/1";
19
20/// `sha256:` and 64 hex digits over `rustev.value/1 || 0x00 || canonical
21/// bytes` of a decoded value (spec 032, 3.1.5).
22pub fn value_digest(value: &Value) -> Result<String, CanonicalError> {
23    Ok(tagged_digest(VALUE_TAG, &canonical_value_bytes(value)?))
24}
25
26/// The closed, ordered evidence grade. The derived order runs weakest to
27/// strongest, so `min` and `max` are the grade order of spec 032, 3.2.1.
28#[derive(
29    Debug,
30    Clone,
31    Copy,
32    PartialEq,
33    Eq,
34    PartialOrd,
35    Ord,
36    Hash,
37    Serialize,
38    Deserialize,
39    schemars::JsonSchema,
40)]
41#[serde(rename_all = "snake_case")]
42pub enum Grade {
43    /// A label from a source declared unreliable for that field, or a
44    /// constructed key not yet corroborated.
45    Hint,
46    /// A computed or crawled approximation that never was authoritative.
47    Estimated,
48    /// Was authoritative at its as-of time and has not been revalidated.
49    Cached,
50    /// Live, attested by the system that owns the fact.
51    Authoritative,
52}
53
54impl Grade {
55    /// Strongest first, the order the spec lists them in.
56    pub const ALL: [Grade; 4] = [
57        Grade::Authoritative,
58        Grade::Cached,
59        Grade::Estimated,
60        Grade::Hint,
61    ];
62
63    pub fn name(self) -> &'static str {
64        match self {
65            Grade::Authoritative => "authoritative",
66            Grade::Cached => "cached",
67            Grade::Estimated => "estimated",
68            Grade::Hint => "hint",
69        }
70    }
71}
72
73/// A value's grade, or `ungraded` when no graded value contributed.
74/// `ungraded` is not a point of the grade order (spec 032, 3.2.3).
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
76#[serde(rename_all = "snake_case")]
77pub enum ValueGrade {
78    Authoritative,
79    Cached,
80    Estimated,
81    Hint,
82    Ungraded,
83}
84
85impl ValueGrade {
86    pub fn graded(self) -> Option<Grade> {
87        match self {
88            ValueGrade::Authoritative => Some(Grade::Authoritative),
89            ValueGrade::Cached => Some(Grade::Cached),
90            ValueGrade::Estimated => Some(Grade::Estimated),
91            ValueGrade::Hint => Some(Grade::Hint),
92            ValueGrade::Ungraded => None,
93        }
94    }
95
96    /// The grade combination of spec 032, 3.2.2: the minimum over graded
97    /// parts; ungraded parts do not participate.
98    pub fn combine(self, other: ValueGrade) -> ValueGrade {
99        match (self.graded(), other.graded()) {
100            (Some(a), Some(b)) => a.min(b).into(),
101            (Some(a), None) | (None, Some(a)) => a.into(),
102            (None, None) => ValueGrade::Ungraded,
103        }
104    }
105
106    pub fn name(self) -> &'static str {
107        match self.graded() {
108            Some(g) => g.name(),
109            None => "ungraded",
110        }
111    }
112}
113
114impl From<Grade> for ValueGrade {
115    fn from(g: Grade) -> Self {
116        match g {
117            Grade::Authoritative => ValueGrade::Authoritative,
118            Grade::Cached => ValueGrade::Cached,
119            Grade::Estimated => ValueGrade::Estimated,
120            Grade::Hint => ValueGrade::Hint,
121        }
122    }
123}
124
125/// The oldest as-of among contributing graded values, or `none`.
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
127#[serde(rename_all = "snake_case", deny_unknown_fields)]
128pub enum AsOf {
129    None,
130    Ms(Timestamp),
131}
132
133impl AsOf {
134    /// The older of two as-of times; `none` contributes nothing.
135    pub fn oldest(self, other: AsOf) -> AsOf {
136        match (self, other) {
137            (AsOf::Ms(a), AsOf::Ms(b)) => AsOf::Ms(a.min(b)),
138            (AsOf::Ms(a), AsOf::None) | (AsOf::None, AsOf::Ms(a)) => AsOf::Ms(a),
139            (AsOf::None, AsOf::None) => AsOf::None,
140        }
141    }
142}
143
144/// Which side of a merge a field came from (spec 032, 3.6.3).
145#[derive(
146    Debug,
147    Clone,
148    Copy,
149    PartialEq,
150    Eq,
151    PartialOrd,
152    Ord,
153    Hash,
154    Serialize,
155    Deserialize,
156    schemars::JsonSchema,
157)]
158#[serde(rename_all = "snake_case")]
159pub enum MergeSide {
160    Base,
161    Overlay,
162}
163
164/// How a value's metadata came about (spec 032, 3.2.2).
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
166#[serde(rename_all = "snake_case", deny_unknown_fields)]
167pub enum Basis {
168    /// An item supplied by an acquisition source (spec 036, 3.4.6).
169    Acquired {
170        step: String,
171    },
172    /// An input.
173    Declared,
174    /// An exact step or the policy.
175    Computed,
176    Corroborated {
177        step: String,
178    },
179    Merged {
180        step: String,
181        side: MergeSide,
182    },
183    /// An input whose disagreement a declared rule resolved.
184    Resolved {
185        input: String,
186    },
187}
188
189/// Value metadata (spec 032, 3.2.2): carried by every input and step value
190/// of a plan 3 evaluation. A record carries one per field, a list one per
191/// item and a map one per entry, in the optional members.
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
193#[serde(deny_unknown_fields)]
194pub struct ValueMeta {
195    pub grade: ValueGrade,
196    /// Sorted, duplicate-free.
197    pub provenance: Vec<ProvenanceClass>,
198    /// Sorted, duplicate-free source names.
199    pub sources: Vec<String>,
200    pub as_of_ms: AsOf,
201    pub basis: Basis,
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub fields: Option<BTreeMap<String, ValueMeta>>,
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub items: Option<Vec<ValueMeta>>,
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub entries: Option<BTreeMap<String, ValueMeta>>,
208}
209
210impl ValueMeta {
211    /// Metadata to which nothing graded contributed.
212    pub fn ungraded(basis: Basis) -> Self {
213        ValueMeta {
214            grade: ValueGrade::Ungraded,
215            provenance: vec![],
216            sources: vec![],
217            as_of_ms: AsOf::None,
218            basis,
219            fields: None,
220            items: None,
221            entries: None,
222        }
223    }
224
225    /// The combination of spec 032, 3.2.2 (minimum graded grade, union of
226    /// provenance and sources, oldest as-of) under `basis`, without parts.
227    pub fn combination<'a>(parts: impl IntoIterator<Item = &'a ValueMeta>, basis: Basis) -> Self {
228        let mut out = ValueMeta::ungraded(basis);
229        for p in parts {
230            out.grade = out.grade.combine(p.grade);
231            out.provenance.extend(p.provenance.iter().copied());
232            out.sources.extend(p.sources.iter().cloned());
233            out.as_of_ms = out.as_of_ms.oldest(p.as_of_ms);
234        }
235        out.provenance.sort();
236        out.provenance.dedup();
237        out.sources.sort();
238        out.sources.dedup();
239        out
240    }
241}
242
243/// A record presence predicate (spec 032, 3.4.2). Each list is non-empty;
244/// the canonical form sorts and deduplicates every field list.
245#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
246#[serde(rename_all = "snake_case", deny_unknown_fields)]
247pub enum PresentWhen {
248    AnyNonNull(Vec<String>),
249    AllNonNull(Vec<String>),
250    All(Vec<PresentWhen>),
251    Any(Vec<PresentWhen>),
252}
253
254impl PresentWhen {
255    /// The declared tree with each field list sorted and deduplicated.
256    pub fn canonical(&self) -> PresentWhen {
257        let sorted = |v: &[String]| {
258            let mut v = v.to_vec();
259            v.sort();
260            v.dedup();
261            v
262        };
263        match self {
264            PresentWhen::AnyNonNull(f) => PresentWhen::AnyNonNull(sorted(f)),
265            PresentWhen::AllNonNull(f) => PresentWhen::AllNonNull(sorted(f)),
266            PresentWhen::All(ps) => PresentWhen::All(ps.iter().map(Self::canonical).collect()),
267            PresentWhen::Any(ps) => PresentWhen::Any(ps.iter().map(Self::canonical).collect()),
268        }
269    }
270
271    /// Nesting depth (a leaf is 1) and the number of leaf predicates.
272    pub fn shape(&self) -> (usize, usize) {
273        match self {
274            PresentWhen::AnyNonNull(_) | PresentWhen::AllNonNull(_) => (1, 1),
275            PresentWhen::All(ps) | PresentWhen::Any(ps) => ps.iter().fold((1, 0), |(d, n), p| {
276                let (pd, pn) = p.shape();
277                (d.max(pd + 1), n.saturating_add(pn))
278            }),
279        }
280    }
281
282    /// Every field the predicate names, in reading order.
283    pub fn fields(&self, out: &mut Vec<String>) {
284        match self {
285            PresentWhen::AnyNonNull(f) | PresentWhen::AllNonNull(f) => {
286                out.extend(f.iter().cloned())
287            }
288            PresentWhen::All(ps) | PresentWhen::Any(ps) => ps.iter().for_each(|p| p.fields(out)),
289        }
290    }
291
292    /// Evaluate over `is_null(field)`.
293    pub fn holds(&self, is_null: &dyn Fn(&str) -> bool) -> bool {
294        match self {
295            PresentWhen::AnyNonNull(f) => f.iter().any(|x| !is_null(x)),
296            PresentWhen::AllNonNull(f) => f.iter().all(|x| !is_null(x)),
297            PresentWhen::All(ps) => ps.iter().all(|p| p.holds(is_null)),
298            PresentWhen::Any(ps) => ps.iter().any(|p| p.holds(is_null)),
299        }
300    }
301}
302
303/// Why a value is absent (spec 032, 3.4.3 and 3.6.2).
304#[derive(
305    Debug,
306    Clone,
307    Copy,
308    PartialEq,
309    Eq,
310    PartialOrd,
311    Ord,
312    Hash,
313    Serialize,
314    Deserialize,
315    schemars::JsonSchema,
316)]
317#[serde(rename_all = "snake_case")]
318pub enum AbsentReason {
319    PredicateFailed,
320    OverlayMissing,
321    BaseMissing,
322    OverlayMissingBaseBelowGrade,
323    OverlayAndBaseMissing,
324}
325
326impl AbsentReason {
327    pub fn name(self) -> &'static str {
328        match self {
329            AbsentReason::PredicateFailed => "predicate_failed",
330            AbsentReason::OverlayMissing => "overlay_missing",
331            AbsentReason::BaseMissing => "base_missing",
332            AbsentReason::OverlayMissingBaseBelowGrade => "overlay_missing_base_below_grade",
333            AbsentReason::OverlayAndBaseMissing => "overlay_and_base_missing",
334        }
335    }
336}
337
338/// A value's presence status. Key existence alone is never presence.
339#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
340#[serde(rename_all = "snake_case", deny_unknown_fields)]
341pub enum Presence {
342    Present,
343    Absent {
344        reason: AbsentReason,
345        /// The canonical predicate that failed, for `predicate_failed`.
346        #[serde(default, skip_serializing_if = "Option::is_none")]
347        predicate: Option<PresentWhen>,
348    },
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[test]
356    fn grade_order_and_combination() {
357        assert!(Grade::Authoritative > Grade::Cached);
358        assert!(Grade::Cached > Grade::Estimated);
359        assert!(Grade::Estimated > Grade::Hint);
360        let a = ValueGrade::Authoritative;
361        assert_eq!(a.combine(ValueGrade::Hint), ValueGrade::Hint);
362        assert_eq!(a.combine(ValueGrade::Ungraded), a, "ungraded never lowers");
363        assert_eq!(
364            ValueGrade::Ungraded.combine(ValueGrade::Ungraded),
365            ValueGrade::Ungraded
366        );
367        assert_eq!(
368            ValueGrade::Hint.combine(ValueGrade::Ungraded),
369            ValueGrade::Hint,
370            "ungraded never raises"
371        );
372    }
373
374    #[test]
375    fn present_when_shape_and_canonical_form() {
376        let p = PresentWhen::All(vec![
377            PresentWhen::AnyNonNull(vec!["b".into(), "a".into(), "a".into()]),
378            PresentWhen::AllNonNull(vec!["c".into()]),
379        ]);
380        assert_eq!(p.shape(), (2, 2));
381        assert_eq!(
382            p.canonical(),
383            PresentWhen::All(vec![
384                PresentWhen::AnyNonNull(vec!["a".into(), "b".into()]),
385                PresentWhen::AllNonNull(vec!["c".into()]),
386            ])
387        );
388        // a null, b set, c null: any(a, b) holds, all(c) does not.
389        let null = |f: &str| f != "b";
390        assert!(!p.holds(&null));
391    }
392
393    #[test]
394    fn value_digest_is_tagged() {
395        let d = value_digest(&serde_json::json!({"b": 1, "a": "x"})).unwrap();
396        assert!(d.starts_with("sha256:") && d.len() == 71);
397        assert_eq!(
398            d,
399            value_digest(&serde_json::json!({"a": "x", "b": 1})).unwrap()
400        );
401    }
402}