use crate::actor::Actor;
use crate::date::{Date, DateTimeField};
use crate::yaml::Value;
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Generated {
pub by: Option<Actor>,
pub at: Option<DateTimeField>,
}
impl Generated {
pub fn from_value(value: &Value) -> Option<Generated> {
let map = value.as_mapping()?;
Some(Generated {
by: map
.get("by")
.and_then(Value::as_display_string)
.map(Actor::parse),
at: map
.get("at")
.and_then(Value::as_display_string)
.map(DateTimeField::new),
})
}
}
impl fmt::Display for Generated {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match (&self.by, &self.at) {
(Some(by), Some(at)) => write!(f, "{by} at {at}"),
(Some(by), None) => write!(f, "{by}"),
(None, Some(at)) => write!(f, "(unknown) at {at}"),
(None, None) => f.write_str("(unknown)"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Verification {
pub by: Option<Actor>,
pub at: Option<DateTimeField>,
}
impl Verification {
pub fn from_value(value: &Value) -> Option<Verification> {
let map = value.as_mapping()?;
Some(Verification {
by: map
.get("by")
.and_then(Value::as_display_string)
.map(Actor::parse),
at: map
.get("at")
.and_then(Value::as_display_string)
.map(DateTimeField::new),
})
}
pub fn list_from_value(value: &Value) -> Vec<Verification> {
match value {
Value::Sequence(items) => items.iter().filter_map(Verification::from_value).collect(),
Value::Mapping(_) => Verification::from_value(value).into_iter().collect(),
_ => Vec::new(),
}
}
}
impl fmt::Display for Verification {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match (&self.by, &self.at) {
(Some(by), Some(at)) => write!(f, "{by} at {at}"),
(Some(by), None) => write!(f, "{by}"),
(None, Some(at)) => write!(f, "(unknown) at {at}"),
(None, None) => f.write_str("(unknown)"),
}
}
}
pub fn latest_verification(events: &[Verification]) -> Option<&Verification> {
events
.iter()
.filter(|v| v.at.as_ref().and_then(|a| a.datetime).is_some())
.max_by_key(|v| v.at.as_ref().and_then(|a| a.datetime).unwrap())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TrustTier {
Unverified,
MachineConfirmed,
HumanReviewed,
}
impl TrustTier {
pub fn derive(events: &[Verification]) -> TrustTier {
if events.is_empty() {
TrustTier::Unverified
} else if events
.iter()
.any(|v| v.by.as_ref().is_some_and(Actor::is_human))
{
TrustTier::HumanReviewed
} else {
TrustTier::MachineConfirmed
}
}
}
impl fmt::Display for TrustTier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
TrustTier::Unverified => "unverified",
TrustTier::MachineConfirmed => "machine-confirmed",
TrustTier::HumanReviewed => "human-reviewed",
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Status {
Draft,
Stable,
Deprecated,
Other(String),
}
pub const STATUS_VALUES: [&str; 3] = ["draft", "stable", "deprecated"];
impl Status {
pub fn parse(value: Option<&str>) -> Status {
match value {
None => Status::Stable,
Some(s) => match s.trim() {
"draft" => Status::Draft,
"stable" | "" => Status::Stable,
"deprecated" => Status::Deprecated,
other => Status::Other(other.to_string()),
},
}
}
pub fn is_known(&self) -> bool {
!matches!(self, Status::Other(_))
}
pub fn is_deprecated(&self) -> bool {
*self == Status::Deprecated
}
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Status::Draft => f.write_str("draft"),
Status::Stable => f.write_str("stable"),
Status::Deprecated => f.write_str("deprecated"),
Status::Other(s) => f.write_str(s),
}
}
}
pub fn is_stale_on(stale_after: Option<Date>, today: Date) -> bool {
stale_after.is_some_and(|d| today >= d)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::yaml::Value;
fn v(yaml: &str) -> Value {
Value::parse(yaml).unwrap()
}
#[test]
fn bare_verified_mapping_is_a_one_element_list() {
let bare =
Verification::list_from_value(&v("{ by: human:ahormati, at: 2026-06-25T09:00:00Z }"));
assert_eq!(bare.len(), 1);
assert!(bare[0].by.as_ref().unwrap().is_human());
assert_eq!(TrustTier::derive(&bare), TrustTier::HumanReviewed);
}
#[test]
fn trust_tiers_key_off_the_human_prefix() {
assert_eq!(TrustTier::derive(&[]), TrustTier::Unverified);
let machine = Verification::list_from_value(&v(
"- { by: process:finance-nightly, at: 2026-06-26T02:00:00Z }",
));
assert_eq!(TrustTier::derive(&machine), TrustTier::MachineConfirmed);
let both = Verification::list_from_value(&v(
"- { by: human:ahormati, at: 2026-06-25T09:00:00Z }\n\
- { by: process:finance-nightly, at: 2026-06-26T02:00:00Z }",
));
assert_eq!(both.len(), 2);
assert_eq!(TrustTier::derive(&both), TrustTier::HumanReviewed);
assert!(TrustTier::HumanReviewed > TrustTier::MachineConfirmed);
let latest = latest_verification(&both).unwrap();
assert_eq!(latest.at.as_ref().unwrap().raw, "2026-06-26T02:00:00Z");
}
#[test]
fn status_defaults_to_stable_and_keeps_unknown_values() {
assert_eq!(Status::parse(None), Status::Stable);
assert_eq!(Status::parse(Some("draft")), Status::Draft);
assert_eq!(Status::parse(Some("deprecated")), Status::Deprecated);
let other = Status::parse(Some("experimental"));
assert!(!other.is_known());
assert_eq!(other.to_string(), "experimental");
}
#[test]
fn staleness_is_a_plain_date_comparison() {
let stale_after = Date::new(2026, 9, 23);
assert!(!is_stale_on(stale_after, Date::new(2026, 9, 22).unwrap()));
assert!(
is_stale_on(stale_after, Date::new(2026, 9, 23).unwrap()),
"stale on the day itself"
);
assert!(is_stale_on(stale_after, Date::new(2026, 9, 24).unwrap()));
assert!(!is_stale_on(None, Date::new(2099, 1, 1).unwrap()));
}
#[test]
fn generated_reads_actor_and_datetime() {
let g = Generated::from_value(&v(
"{ by: reference_agent/gemini-2.5-pro, at: 2026-06-20T22:53:05Z }",
))
.unwrap();
assert_eq!(g.by.as_ref().unwrap().producer(), Some("reference_agent"));
assert!(g.at.as_ref().unwrap().is_valid());
assert!(Generated::from_value(&v("just a string")).is_none());
}
}