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, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
149pub enum TrustTier {
150 #[default]
152 Unverified,
153 MachineConfirmed,
155 HumanReviewed,
157}
158
159impl TrustTier {
160 #[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 #[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#[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#[derive(Clone, Debug, Default, PartialEq, Eq)]
223pub enum Status {
224 Draft,
226 #[default]
228 Stable,
229 Deprecated,
231 Other(String),
234}
235
236pub const STATUS_VALUES: [&str; 3] = ["draft", "stable", "deprecated"];
238
239impl Status {
240 #[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 #[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 #[must_use]
264 pub const fn is_known(&self) -> bool {
265 !matches!(self, Self::Other(_))
266 }
267
268 #[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#[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#[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 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 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}