zynk 1.0.1

Portable protocol and helper CLI for multi-agent collaboration.
//! Canonical timestamp handling for v0.2.1 (ADR 026 D1).
//!
//! Every timestamp stored in the live-state database is RFC3339 UTC with
//! seconds precision and a `Z` suffix (`YYYY-MM-DDTHH:MM:SSZ`). Storing one
//! canonical form makes lexicographic `ORDER BY timestamp` equal to
//! chronological order; mixed-offset forms (e.g. `+07:00`) would sort
//! incorrectly.

use chrono::{DateTime, SecondsFormat, Utc};

/// Current wall-clock time in canonical UTC-`Z` seconds form.
pub fn now_utc_seconds() -> String {
    Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true)
}

/// Parse any RFC3339 timestamp (with offset or `Z`), convert to UTC, and
/// re-emit the canonical `Z` seconds form. Returns `None` when the input is not
/// valid RFC3339, so callers can decide whether to keep the original or warn.
pub fn canonicalize(value: &str) -> Option<String> {
    DateTime::parse_from_rfc3339(value).ok().map(|dt| {
        dt.with_timezone(&Utc)
            .to_rfc3339_opts(SecondsFormat::Secs, true)
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn canonicalizes_positive_offset_to_utc_z() {
        assert_eq!(
            canonicalize("2026-05-28T18:00:00+07:00").as_deref(),
            Some("2026-05-28T11:00:00Z")
        );
    }

    #[test]
    fn canonicalizes_zero_offset_to_z() {
        assert_eq!(
            canonicalize("2026-05-28T18:00:00+00:00").as_deref(),
            Some("2026-05-28T18:00:00Z")
        );
    }

    #[test]
    fn passes_through_already_canonical() {
        assert_eq!(
            canonicalize("2026-05-28T18:00:00Z").as_deref(),
            Some("2026-05-28T18:00:00Z")
        );
    }

    #[test]
    fn rejects_non_rfc3339() {
        assert_eq!(canonicalize("not-a-timestamp"), None);
    }

    #[test]
    fn rejects_calendar_invalid_shape() {
        // shape-valid but not a real date: real RFC3339 parsing must reject it.
        assert_eq!(canonicalize("2026-99-99T99:99:99Z"), None);
    }
}