Skip to main content

okf_core/
trust.rs

1//! Trust and lifecycle frontmatter: `generated`, `verified`, `status`, and
2//! `stale_after` (§5.2 to §5.5).
3//!
4//! These four keys let a consumer answer "how much should I trust this" and "is
5//! it still current" from frontmatter alone. All are optional, and their
6//! *absence* is meaningful rather than invalid: a concept with no trust
7//! frontmatter is [`TrustTier::Unverified`] and `status: stable`, and must
8//! never be rejected (§11).
9//!
10//! Two rules from the spec are encoded here rather than left to callers:
11//!
12//! - A bare `verified: { by, at }` mapping **must** be read as a one-element
13//!   list ([`Verification::list_from_value`], §5.2).
14//! - The trust tier is *derived* from `verified`, never stored
15//!   ([`TrustTier::derive`], §5.3).
16
17use crate::actor::Actor;
18use crate::date::{Date, DateTime, DateTimeField};
19use crate::yaml::Value;
20use std::fmt;
21
22/// How the current content was produced (§5.2): `generated: { by, at }`.
23///
24/// Distinct from [`Verification`] on purpose: who *wrote* a concept need not be
25/// who *confirmed* it.
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct Generated {
28    /// The producing actor. REQUIRED within `generated`; `None` marks a
29    /// malformed block rather than a fatal error.
30    pub by: Option<Actor>,
31    /// When the content last meaningfully changed.
32    pub at: Option<DateTimeField>,
33}
34
35impl Generated {
36    /// Reads a `generated` value. Returns `None` when the value is not a
37    /// mapping.
38    pub fn from_value(value: &Value) -> Option<Self> {
39        let map = value.as_mapping()?;
40        Some(Self {
41            by: map
42                .get("by")
43                .and_then(Value::as_display_string)
44                .map(Actor::parse),
45            at: map
46                .get("at")
47                .and_then(Value::as_display_string)
48                .map(DateTimeField::new),
49        })
50    }
51}
52
53impl fmt::Display for Generated {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        match (&self.by, &self.at) {
56            (Some(by), Some(at)) => write!(f, "{by} at {at}"),
57            (Some(by), None) => write!(f, "{by}"),
58            (None, Some(at)) => write!(f, "(unknown) at {at}"),
59            (None, None) => f.write_str("(unknown)"),
60        }
61    }
62}
63
64/// A single verification event (§5.2): `{ by, at }`.
65///
66/// Multiple entries capture independent checks, a human sign-off plus a
67/// nightly process, say. "How recently" is the latest [`Verification::at`].
68#[derive(Clone, Debug, PartialEq, Eq)]
69pub struct Verification {
70    /// The verifying actor.
71    pub by: Option<Actor>,
72    /// When the verification happened.
73    pub at: Option<DateTimeField>,
74}
75
76impl Verification {
77    /// Reads one `{ by, at }` mapping. Returns `None` when the value is not a
78    /// mapping.
79    pub fn from_value(value: &Value) -> Option<Self> {
80        let map = value.as_mapping()?;
81        Some(Self {
82            by: map
83                .get("by")
84                .and_then(Value::as_display_string)
85                .map(Actor::parse),
86            at: map
87                .get("at")
88                .and_then(Value::as_display_string)
89                .map(DateTimeField::new),
90        })
91    }
92
93    /// `true` when this event has the required actor and a parseable timestamp
94    /// that includes a time of day and an explicit UTC offset.
95    #[must_use]
96    pub fn is_valid(&self) -> bool {
97        self.by
98            .as_ref()
99            .is_some_and(|by| !by.as_str().trim().is_empty())
100            && self.at.as_ref().is_some_and(DateTimeField::is_valid)
101    }
102
103    /// Reads a whole `verified` value into a list of events.
104    ///
105    /// Consumers **MUST** treat a bare `{ by, at }` mapping as a one-element
106    /// list (§5.2, restated as a conformance rule in §11), so that shape is
107    /// accepted here alongside the list form. Any other shape yields an empty
108    /// list.
109    pub fn list_from_value(value: &Value) -> Vec<Self> {
110        match value {
111            Value::Sequence(items) => items.iter().filter_map(Self::from_value).collect(),
112            Value::Mapping(_) => Self::from_value(value).into_iter().collect(),
113            _ => Vec::new(),
114        }
115    }
116}
117
118impl fmt::Display for Verification {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        match (&self.by, &self.at) {
121            (Some(by), Some(at)) => write!(f, "{by} at {at}"),
122            (Some(by), None) => write!(f, "{by}"),
123            (None, Some(at)) => write!(f, "(unknown) at {at}"),
124            (None, None) => f.write_str("(unknown)"),
125        }
126    }
127}
128
129/// Returns the verification with the latest parseable `at` (§5.2).
130///
131/// Events missing a verifier or a valid, parseable `at` cannot be
132/// ordered and are skipped; `None` means no event carries a usable timestamp.
133#[must_use]
134pub fn latest_verification(events: &[Verification]) -> Option<&Verification> {
135    events
136        .iter()
137        .filter(|v| v.is_valid())
138        .filter_map(|v| v.at.as_ref().and_then(|at| at.datetime).map(|dt| (v, dt)))
139        .max_by_key(|(_, dt)| *dt)
140        .map(|(v, _)| v)
141}
142
143/// A concept's trust tier, derived from `verified` (§5.3).
144///
145/// Ordering is by increasing trust, so tiers can be compared directly
146/// (`tier >= TrustTier::MachineConfirmed`). Tiers are advisory signals, not
147/// access control.
148#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
149pub enum TrustTier {
150    /// No `verified` key.
151    Unverified,
152    /// `verified` by non-`human:` actors only.
153    MachineConfirmed,
154    /// `verified` by at least one `human:<id>` actor.
155    HumanReviewed,
156}
157
158impl TrustTier {
159    /// Derives the tier from a concept's verification events (§5.3).
160    #[must_use]
161    pub fn derive(events: &[Verification]) -> Self {
162        let mut valid_events = events.iter().filter(|event| event.is_valid());
163        if valid_events.clone().next().is_none() {
164            Self::Unverified
165        } else if valid_events.any(|v| v.by.as_ref().is_some_and(Actor::is_human)) {
166            Self::HumanReviewed
167        } else {
168            Self::MachineConfirmed
169        }
170    }
171}
172
173impl fmt::Display for TrustTier {
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        f.write_str(match self {
176            Self::Unverified => "unverified",
177            Self::MachineConfirmed => "machine-confirmed",
178            Self::HumanReviewed => "human-reviewed",
179        })
180    }
181}
182
183/// A concept's lifecycle `status` (§5.4). An absent key means
184/// [`Status::Stable`].
185#[derive(Clone, Debug, PartialEq, Eq)]
186pub enum Status {
187    /// Not yet reviewed; possibly incomplete.
188    Draft,
189    /// The default: ready for consumption.
190    Stable,
191    /// Kept for links and history; no longer current.
192    Deprecated,
193    /// A producer-defined value outside the three the spec names. Consumers
194    /// must tolerate it (§11).
195    Other(String),
196}
197
198/// The `status` values §5.4 defines.
199pub const STATUS_VALUES: [&str; 3] = ["draft", "stable", "deprecated"];
200
201impl Status {
202    /// Parses a `status` scalar. `None` (an absent key) is [`Status::Stable`].
203    #[must_use]
204    pub fn parse(value: Option<&str>) -> Self {
205        value.map_or(Self::Stable, |s| match s.trim() {
206            "draft" => Self::Draft,
207            "stable" | "" => Self::Stable,
208            "deprecated" => Self::Deprecated,
209            other => Self::Other(other.to_string()),
210        })
211    }
212
213    /// `true` for one of the three values §5.4 defines.
214    #[must_use]
215    pub const fn is_known(&self) -> bool {
216        !matches!(self, Self::Other(_))
217    }
218
219    /// `true` for [`Status::Deprecated`], kept for links and history, but no
220    /// longer current.
221    #[must_use]
222    pub fn is_deprecated(&self) -> bool {
223        *self == Self::Deprecated
224    }
225}
226
227impl fmt::Display for Status {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        match self {
230            Self::Draft => f.write_str("draft"),
231            Self::Stable => f.write_str("stable"),
232            Self::Deprecated => f.write_str("deprecated"),
233            Self::Other(s) => f.write_str(s),
234        }
235    }
236}
237
238/// Whether a concept with this `stale_after` timestamp is stale at `now`.
239///
240/// §5.5: "A concept is stale when `now >= stale_after`." An absent,
241/// offset-less, or unparseable `stale_after` is never stale.
242#[must_use]
243pub fn is_stale_at(stale_after: Option<DateTime>, now: DateTime) -> bool {
244    stale_after.is_some_and(|dt| dt.offset_minutes.is_some() && dt.has_time && now >= dt)
245}
246
247/// Whether a concept with this `stale_after` timestamp is stale on `today`.
248///
249/// Evaluates staleness at midnight UTC on `today`.
250#[must_use]
251pub fn is_stale_on(stale_after: Option<DateTime>, today: Date) -> bool {
252    is_stale_at(stale_after, today.to_utc_datetime())
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258    use crate::yaml::Value;
259
260    fn v(yaml: &str) -> Value {
261        Value::parse(yaml).unwrap()
262    }
263
264    #[test]
265    fn bare_verified_mapping_is_a_one_element_list() {
266        let bare =
267            Verification::list_from_value(&v("{ by: human:ahormati, at: 2026-06-25T09:00:00Z }"));
268        assert_eq!(bare.len(), 1);
269        assert!(bare[0].by.as_ref().unwrap().is_human());
270        assert_eq!(TrustTier::derive(&bare), TrustTier::HumanReviewed);
271    }
272
273    #[test]
274    fn trust_tiers_key_off_the_human_prefix() {
275        assert_eq!(TrustTier::derive(&[]), TrustTier::Unverified);
276
277        let machine = Verification::list_from_value(&v(
278            "- { by: process:finance-nightly, at: 2026-06-26T02:00:00Z }",
279        ));
280        assert_eq!(TrustTier::derive(&machine), TrustTier::MachineConfirmed);
281
282        let both = Verification::list_from_value(&v(
283            "- { by: human:ahormati, at: 2026-06-25T09:00:00Z }\n\
284             - { by: process:finance-nightly, at: 2026-06-26T02:00:00Z }",
285        ));
286        assert_eq!(both.len(), 2);
287        assert_eq!(TrustTier::derive(&both), TrustTier::HumanReviewed);
288        assert!(TrustTier::HumanReviewed > TrustTier::MachineConfirmed);
289
290        // "How recently" is the latest `at`, not the last entry.
291        let latest = latest_verification(&both).unwrap();
292        assert_eq!(latest.at.as_ref().unwrap().raw, "2026-06-26T02:00:00Z");
293    }
294
295    #[test]
296    fn malformed_verification_events_do_not_raise_trust() {
297        let malformed =
298            Verification::list_from_value(&v("- { by: human:ahormati, at: yesterday }\n\
299             - { by: human:other, at: 2026-06-26 }\n\
300             - { by: human:third }"));
301        assert_eq!(malformed.len(), 3);
302        assert!(malformed.iter().all(|event| !event.is_valid()));
303        assert_eq!(TrustTier::derive(&malformed), TrustTier::Unverified);
304        assert_eq!(latest_verification(&malformed), None);
305    }
306
307    #[test]
308    fn status_defaults_to_stable_and_keeps_unknown_values() {
309        assert_eq!(Status::parse(None), Status::Stable);
310        assert_eq!(Status::parse(Some("draft")), Status::Draft);
311        assert_eq!(Status::parse(Some("deprecated")), Status::Deprecated);
312        let other = Status::parse(Some("experimental"));
313        assert!(!other.is_known());
314        assert_eq!(other.to_string(), "experimental");
315    }
316
317    #[test]
318    fn staleness_is_an_instant_comparison() {
319        let stale_after = DateTime::parse("2026-09-23T00:00:00Z");
320        assert!(!is_stale_at(
321            stale_after,
322            DateTime::parse("2026-09-22T23:59:59Z").unwrap()
323        ));
324        assert!(is_stale_at(
325            stale_after,
326            DateTime::parse("2026-09-23T00:00:00Z").unwrap()
327        ));
328        assert!(is_stale_at(
329            stale_after,
330            DateTime::parse("2026-09-24T12:00:00Z").unwrap()
331        ));
332        assert!(!is_stale_at(
333            None,
334            DateTime::parse("2099-01-01T00:00:00Z").unwrap()
335        ));
336        // Date-only or offset-less values are ignored
337        assert!(!is_stale_at(
338            DateTime::parse("2026-09-23"),
339            DateTime::parse("2026-09-24T00:00:00Z").unwrap()
340        ));
341        assert!(!is_stale_at(
342            DateTime::parse("2026-09-23T00:00:00"),
343            DateTime::parse("2026-09-24T00:00:00Z").unwrap()
344        ));
345    }
346
347    #[test]
348    fn generated_reads_actor_and_datetime() {
349        let g = Generated::from_value(&v(
350            "{ by: reference_agent/gemini-2.5-pro, at: 2026-06-20T22:53:05Z }",
351        ))
352        .unwrap();
353        assert_eq!(g.by.as_ref().unwrap().producer(), Some("reference_agent"));
354        assert!(g.at.as_ref().unwrap().is_valid());
355        assert!(Generated::from_value(&v("just a string")).is_none());
356    }
357}