1use crate::actor::Actor;
18use crate::date::{Date, 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 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#[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#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
138pub enum TrustTier {
139 Unverified,
141 MachineConfirmed,
143 HumanReviewed,
145}
146
147impl TrustTier {
148 #[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#[derive(Clone, Debug, PartialEq, Eq)]
177pub enum Status {
178 Draft,
180 Stable,
182 Deprecated,
184 Other(String),
187}
188
189pub const STATUS_VALUES: [&str; 3] = ["draft", "stable", "deprecated"];
191
192impl Status {
193 #[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 #[must_use]
206 pub const fn is_known(&self) -> bool {
207 !matches!(self, Self::Other(_))
208 }
209
210 #[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#[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 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}