1use crate::actor::Actor;
18use crate::date::{Date, DateTime, DateTimeField};
19use crate::yaml::Value;
20use std::fmt;
21
22#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct Generated {
28 pub by: Option<Actor>,
31 pub at: Option<DateTimeField>,
33}
34
35impl Generated {
36 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#[derive(Clone, Debug, PartialEq, Eq)]
69pub struct Verification {
70 pub by: Option<Actor>,
72 pub at: Option<DateTimeField>,
74}
75
76impl Verification {
77 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 #[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 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#[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#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
149pub enum TrustTier {
150 Unverified,
152 MachineConfirmed,
154 HumanReviewed,
156}
157
158impl TrustTier {
159 #[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#[derive(Clone, Debug, PartialEq, Eq)]
186pub enum Status {
187 Draft,
189 Stable,
191 Deprecated,
193 Other(String),
196}
197
198pub const STATUS_VALUES: [&str; 3] = ["draft", "stable", "deprecated"];
200
201impl Status {
202 #[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 #[must_use]
215 pub const fn is_known(&self) -> bool {
216 !matches!(self, Self::Other(_))
217 }
218
219 #[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#[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#[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 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 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}