Skip to main content

okf_core/
trust.rs

1//! Trust and lifecycle frontmatter: `generated`, `verified`, `status`, and
2//! `stale_after`.
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.
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`]).
14//! - The trust tier is *derived* from `verified`, never stored
15//!   ([`TrustTier::derive`]).
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: `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: `{ 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, 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`.
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`.
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, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
149pub enum TrustTier {
150    /// No `verified` key.
151    #[default]
152    Unverified,
153    /// `verified` by non-`human:` actors only.
154    MachineConfirmed,
155    /// `verified` by at least one `human:<id>` actor.
156    HumanReviewed,
157}
158
159impl TrustTier {
160    /// Derives the tier from a concept's verification events.
161    #[must_use]
162    pub fn derive(events: &[Verification]) -> Self {
163        let mut valid_events = events.iter().filter(|event| event.is_valid());
164        if valid_events.clone().next().is_none() {
165            Self::Unverified
166        } else if valid_events.any(|v| v.by.as_ref().is_some_and(Actor::is_human)) {
167            Self::HumanReviewed
168        } else {
169            Self::MachineConfirmed
170        }
171    }
172
173    /// The string representation of this trust tier.
174    #[must_use]
175    pub const fn as_str(&self) -> &'static str {
176        match self {
177            Self::Unverified => "unverified",
178            Self::MachineConfirmed => "machine-confirmed",
179            Self::HumanReviewed => "human-reviewed",
180        }
181    }
182}
183
184impl AsRef<str> for TrustTier {
185    fn as_ref(&self) -> &str {
186        self.as_str()
187    }
188}
189
190/// Error returned when a string cannot be parsed into a [`TrustTier`].
191#[derive(Clone, Debug, PartialEq, Eq)]
192pub struct ParseTrustTierError(pub String);
193
194impl fmt::Display for ParseTrustTierError {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        write!(f, "unknown trust tier: {:?}", self.0)
197    }
198}
199
200impl std::error::Error for ParseTrustTierError {}
201
202impl std::str::FromStr for TrustTier {
203    type Err = ParseTrustTierError;
204    fn from_str(s: &str) -> Result<Self, Self::Err> {
205        match s.trim() {
206            "unverified" => Ok(Self::Unverified),
207            "machine-confirmed" | "machine_confirmed" => Ok(Self::MachineConfirmed),
208            "human-reviewed" | "human_reviewed" => Ok(Self::HumanReviewed),
209            other => Err(ParseTrustTierError(other.to_string())),
210        }
211    }
212}
213
214impl fmt::Display for TrustTier {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        f.write_str(self.as_str())
217    }
218}
219
220/// A concept's lifecycle `status`. An absent key means
221/// [`Status::Stable`].
222#[derive(Clone, Debug, Default, PartialEq, Eq)]
223pub enum Status {
224    /// Not yet reviewed; possibly incomplete.
225    Draft,
226    /// The default: ready for consumption.
227    #[default]
228    Stable,
229    /// Kept for links and history; no longer current.
230    Deprecated,
231    /// A producer-defined value outside the three the spec names. Consumers
232    /// must tolerate it.
233    Other(String),
234}
235
236/// The `status` values the spec defines.
237pub const STATUS_VALUES: [&str; 3] = ["draft", "stable", "deprecated"];
238
239impl Status {
240    /// Parses a `status` scalar. `None` (an absent key) is [`Status::Stable`].
241    #[must_use]
242    pub fn parse(value: Option<&str>) -> Self {
243        value.map_or(Self::Stable, |s| match s.trim() {
244            "draft" => Self::Draft,
245            "stable" | "" => Self::Stable,
246            "deprecated" => Self::Deprecated,
247            other => Self::Other(other.to_string()),
248        })
249    }
250
251    /// Returns the string representation of this status.
252    #[must_use]
253    pub const fn as_str(&self) -> &str {
254        match self {
255            Self::Draft => "draft",
256            Self::Stable => "stable",
257            Self::Deprecated => "deprecated",
258            Self::Other(s) => s.as_str(),
259        }
260    }
261
262    /// `true` for one of the three values the spec defines.
263    #[must_use]
264    pub const fn is_known(&self) -> bool {
265        !matches!(self, Self::Other(_))
266    }
267
268    /// `true` for [`Status::Deprecated`], kept for links and history, but no
269    /// longer current.
270    #[must_use]
271    pub fn is_deprecated(&self) -> bool {
272        *self == Self::Deprecated
273    }
274}
275
276impl AsRef<str> for Status {
277    fn as_ref(&self) -> &str {
278        self.as_str()
279    }
280}
281
282impl std::str::FromStr for Status {
283    type Err = std::convert::Infallible;
284    fn from_str(s: &str) -> Result<Self, Self::Err> {
285        Ok(Self::parse(Some(s)))
286    }
287}
288
289impl fmt::Display for Status {
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        f.write_str(self.as_str())
292    }
293}
294
295/// Whether a concept with this `stale_after` timestamp is stale at `now`.
296///
297/// Staleness rule: "A concept is stale when `now >= stale_after`." An absent,
298/// offset-less, or unparseable `stale_after` is never stale.
299#[must_use]
300pub fn is_stale_at(stale_after: Option<DateTime>, now: DateTime) -> bool {
301    stale_after.is_some_and(|dt| dt.offset_minutes.is_some() && dt.has_time && now >= dt)
302}
303
304/// Whether a concept with this `stale_after` timestamp is stale on `today`.
305///
306/// Evaluates staleness at midnight UTC on `today`.
307#[must_use]
308pub fn is_stale_on(stale_after: Option<DateTime>, today: Date) -> bool {
309    is_stale_at(stale_after, today.to_utc_datetime())
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    use crate::yaml::Value;
316
317    fn v(yaml: &str) -> Value {
318        Value::parse(yaml).unwrap()
319    }
320
321    #[test]
322    fn bare_verified_mapping_is_a_one_element_list() {
323        let bare =
324            Verification::list_from_value(&v("{ by: human:walter, at: 2026-06-25T09:00:00Z }"));
325        assert_eq!(bare.len(), 1);
326        assert!(bare[0].by.as_ref().unwrap().is_human());
327        assert_eq!(TrustTier::derive(&bare), TrustTier::HumanReviewed);
328    }
329
330    #[test]
331    fn trust_tiers_key_off_the_human_prefix() {
332        assert_eq!(TrustTier::derive(&[]), TrustTier::Unverified);
333
334        let machine = Verification::list_from_value(&v(
335            "- { by: process:finance-nightly, at: 2026-06-26T02:00:00Z }",
336        ));
337        assert_eq!(TrustTier::derive(&machine), TrustTier::MachineConfirmed);
338
339        let both =
340            Verification::list_from_value(&v("- { by: human:walter, at: 2026-06-25T09:00:00Z }\n\
341             - { by: process:finance-nightly, at: 2026-06-26T02:00:00Z }"));
342        assert_eq!(both.len(), 2);
343        assert_eq!(TrustTier::derive(&both), TrustTier::HumanReviewed);
344        assert!(TrustTier::HumanReviewed > TrustTier::MachineConfirmed);
345
346        // "How recently" is the latest `at`, not the last entry.
347        let latest = latest_verification(&both).unwrap();
348        assert_eq!(latest.at.as_ref().unwrap().raw, "2026-06-26T02:00:00Z");
349    }
350
351    #[test]
352    fn malformed_verification_events_do_not_raise_trust() {
353        let malformed =
354            Verification::list_from_value(&v("- { by: human:walter, at: yesterday }\n\
355             - { by: human:other, at: 2026-06-26 }\n\
356             - { by: human:third }"));
357        assert_eq!(malformed.len(), 3);
358        assert!(malformed.iter().all(|event| !event.is_valid()));
359        assert_eq!(TrustTier::derive(&malformed), TrustTier::Unverified);
360        assert_eq!(latest_verification(&malformed), None);
361    }
362
363    #[test]
364    fn status_defaults_to_stable_and_keeps_unknown_values() {
365        assert_eq!(Status::parse(None), Status::Stable);
366        assert_eq!(Status::parse(Some("draft")), Status::Draft);
367        assert_eq!(Status::parse(Some("deprecated")), Status::Deprecated);
368        let other = Status::parse(Some("experimental"));
369        assert!(!other.is_known());
370        assert_eq!(other.to_string(), "experimental");
371    }
372
373    #[test]
374    fn staleness_is_an_instant_comparison() {
375        let stale_after = DateTime::parse("2026-09-23T00:00:00Z");
376        assert!(!is_stale_at(
377            stale_after,
378            DateTime::parse("2026-09-22T23:59:59Z").unwrap()
379        ));
380        assert!(is_stale_at(
381            stale_after,
382            DateTime::parse("2026-09-23T00:00:00Z").unwrap()
383        ));
384        assert!(is_stale_at(
385            stale_after,
386            DateTime::parse("2026-09-24T12:00:00Z").unwrap()
387        ));
388        assert!(!is_stale_at(
389            None,
390            DateTime::parse("2099-01-01T00:00:00Z").unwrap()
391        ));
392        // Date-only or offset-less values are ignored
393        assert!(!is_stale_at(
394            DateTime::parse("2026-09-23"),
395            DateTime::parse("2026-09-24T00:00:00Z").unwrap()
396        ));
397        assert!(!is_stale_at(
398            DateTime::parse("2026-09-23T00:00:00"),
399            DateTime::parse("2026-09-24T00:00:00Z").unwrap()
400        ));
401    }
402
403    #[test]
404    fn generated_reads_actor_and_datetime() {
405        let g = Generated::from_value(&v(
406            "{ by: reference_agent/gemini-2.5-pro, at: 2026-06-20T22:53:05Z }",
407        ))
408        .unwrap();
409        assert_eq!(g.by.as_ref().unwrap().producer(), Some("reference_agent"));
410        assert!(g.at.as_ref().unwrap().is_valid());
411        assert!(Generated::from_value(&v("just a string")).is_none());
412    }
413}