Skip to main content

okf/
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, 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    /// Reads a whole `verified` value into a list of events.
94    ///
95    /// Consumers **MUST** treat a bare `{ by, at }` mapping as a one-element
96    /// list (§5.2, restated as a conformance rule in §11), so that shape is
97    /// accepted here alongside the list form. Any other shape yields an empty
98    /// list.
99    pub fn list_from_value(value: &Value) -> Vec<Self> {
100        match value {
101            Value::Sequence(items) => items.iter().filter_map(Self::from_value).collect(),
102            Value::Mapping(_) => Self::from_value(value).into_iter().collect(),
103            _ => Vec::new(),
104        }
105    }
106}
107
108impl fmt::Display for Verification {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        match (&self.by, &self.at) {
111            (Some(by), Some(at)) => write!(f, "{by} at {at}"),
112            (Some(by), None) => write!(f, "{by}"),
113            (None, Some(at)) => write!(f, "(unknown) at {at}"),
114            (None, None) => f.write_str("(unknown)"),
115        }
116    }
117}
118
119/// Returns the verification with the latest parseable `at` (§5.2).
120///
121/// Events whose `at` is missing or unparseable cannot be ordered and are
122/// skipped; `None` means no event carries a usable timestamp.
123#[must_use]
124pub fn latest_verification(events: &[Verification]) -> Option<&Verification> {
125    events
126        .iter()
127        .filter_map(|v| v.at.as_ref().and_then(|a| a.datetime).map(|dt| (v, dt)))
128        .max_by_key(|(_, dt)| *dt)
129        .map(|(v, _)| v)
130}
131
132/// A concept's trust tier, derived from `verified` (§5.3).
133///
134/// Ordering is by increasing trust, so tiers can be compared directly
135/// (`tier >= TrustTier::MachineConfirmed`). Tiers are advisory signals, not
136/// access control.
137#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
138pub enum TrustTier {
139    /// No `verified` key.
140    Unverified,
141    /// `verified` by non-`human:` actors only.
142    MachineConfirmed,
143    /// `verified` by at least one `human:<id>` actor.
144    HumanReviewed,
145}
146
147impl TrustTier {
148    /// Derives the tier from a concept's verification events (§5.3).
149    #[must_use]
150    pub fn derive(events: &[Verification]) -> Self {
151        if events.is_empty() {
152            Self::Unverified
153        } else if events
154            .iter()
155            .any(|v| v.by.as_ref().is_some_and(Actor::is_human))
156        {
157            Self::HumanReviewed
158        } else {
159            Self::MachineConfirmed
160        }
161    }
162}
163
164impl fmt::Display for TrustTier {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        f.write_str(match self {
167            Self::Unverified => "unverified",
168            Self::MachineConfirmed => "machine-confirmed",
169            Self::HumanReviewed => "human-reviewed",
170        })
171    }
172}
173
174/// A concept's lifecycle `status` (§5.4). An absent key means
175/// [`Status::Stable`].
176#[derive(Clone, Debug, PartialEq, Eq)]
177pub enum Status {
178    /// Not yet reviewed; possibly incomplete.
179    Draft,
180    /// The default: ready for consumption.
181    Stable,
182    /// Kept for links and history; no longer current.
183    Deprecated,
184    /// A producer-defined value outside the three the spec names. Consumers
185    /// must tolerate it (§11).
186    Other(String),
187}
188
189/// The `status` values §5.4 defines.
190pub const STATUS_VALUES: [&str; 3] = ["draft", "stable", "deprecated"];
191
192impl Status {
193    /// Parses a `status` scalar. `None` (an absent key) is [`Status::Stable`].
194    #[must_use]
195    pub fn parse(value: Option<&str>) -> Self {
196        value.map_or(Self::Stable, |s| match s.trim() {
197            "draft" => Self::Draft,
198            "stable" | "" => Self::Stable,
199            "deprecated" => Self::Deprecated,
200            other => Self::Other(other.to_string()),
201        })
202    }
203
204    /// `true` for one of the three values §5.4 defines.
205    #[must_use]
206    pub const fn is_known(&self) -> bool {
207        !matches!(self, Self::Other(_))
208    }
209
210    /// `true` for [`Status::Deprecated`], kept for links and history, but no
211    /// longer current.
212    #[must_use]
213    pub fn is_deprecated(&self) -> bool {
214        *self == Self::Deprecated
215    }
216}
217
218impl fmt::Display for Status {
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        match self {
221            Self::Draft => f.write_str("draft"),
222            Self::Stable => f.write_str("stable"),
223            Self::Deprecated => f.write_str("deprecated"),
224            Self::Other(s) => f.write_str(s),
225        }
226    }
227}
228
229/// Whether a concept with this `stale_after` date is stale on `today`.
230///
231/// §5.5: "A concept is stale when `today >= stale_after`." An absent or
232/// unparseable `stale_after` is never stale.
233#[must_use]
234pub fn is_stale_on(stale_after: Option<Date>, today: Date) -> bool {
235    stale_after.is_some_and(|d| today >= d)
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use crate::yaml::Value;
242
243    fn v(yaml: &str) -> Value {
244        Value::parse(yaml).unwrap()
245    }
246
247    #[test]
248    fn bare_verified_mapping_is_a_one_element_list() {
249        let bare =
250            Verification::list_from_value(&v("{ by: human:ahormati, at: 2026-06-25T09:00:00Z }"));
251        assert_eq!(bare.len(), 1);
252        assert!(bare[0].by.as_ref().unwrap().is_human());
253        assert_eq!(TrustTier::derive(&bare), TrustTier::HumanReviewed);
254    }
255
256    #[test]
257    fn trust_tiers_key_off_the_human_prefix() {
258        assert_eq!(TrustTier::derive(&[]), TrustTier::Unverified);
259
260        let machine = Verification::list_from_value(&v(
261            "- { by: process:finance-nightly, at: 2026-06-26T02:00:00Z }",
262        ));
263        assert_eq!(TrustTier::derive(&machine), TrustTier::MachineConfirmed);
264
265        let both = Verification::list_from_value(&v(
266            "- { by: human:ahormati, at: 2026-06-25T09:00:00Z }\n\
267             - { by: process:finance-nightly, at: 2026-06-26T02:00:00Z }",
268        ));
269        assert_eq!(both.len(), 2);
270        assert_eq!(TrustTier::derive(&both), TrustTier::HumanReviewed);
271        assert!(TrustTier::HumanReviewed > TrustTier::MachineConfirmed);
272
273        // "How recently" is the latest `at`, not the last entry.
274        let latest = latest_verification(&both).unwrap();
275        assert_eq!(latest.at.as_ref().unwrap().raw, "2026-06-26T02:00:00Z");
276    }
277
278    #[test]
279    fn status_defaults_to_stable_and_keeps_unknown_values() {
280        assert_eq!(Status::parse(None), Status::Stable);
281        assert_eq!(Status::parse(Some("draft")), Status::Draft);
282        assert_eq!(Status::parse(Some("deprecated")), Status::Deprecated);
283        let other = Status::parse(Some("experimental"));
284        assert!(!other.is_known());
285        assert_eq!(other.to_string(), "experimental");
286    }
287
288    #[test]
289    fn staleness_is_a_plain_date_comparison() {
290        let stale_after = Date::new(2026, 9, 23);
291        assert!(!is_stale_on(stale_after, Date::new(2026, 9, 22).unwrap()));
292        assert!(
293            is_stale_on(stale_after, Date::new(2026, 9, 23).unwrap()),
294            "stale on the day itself"
295        );
296        assert!(is_stale_on(stale_after, Date::new(2026, 9, 24).unwrap()));
297        assert!(!is_stale_on(None, Date::new(2099, 1, 1).unwrap()));
298    }
299
300    #[test]
301    fn generated_reads_actor_and_datetime() {
302        let g = Generated::from_value(&v(
303            "{ by: reference_agent/gemini-2.5-pro, at: 2026-06-20T22:53:05Z }",
304        ))
305        .unwrap();
306        assert_eq!(g.by.as_ref().unwrap().producer(), Some("reference_agent"));
307        assert!(g.at.as_ref().unwrap().is_valid());
308        assert!(Generated::from_value(&v("just a string")).is_none());
309    }
310}