wm_core/time.rs
1//! Time — one generation point for every persisted timestamp.
2//!
3//! Three canonical forms exist in the wild (see
4//! `docs/TIMESTAMP_CONVENTIONS.md` for the full per-surface registry):
5//!
6//! - **Epoch milliseconds** (`now_unix_millis`) — the default for new
7//! persisted event records (sessions, friction JSONL, the opencode
8//! corpus). Sub-second ordering matters for parallel sessions, and the
9//! opencode corpus we correlate against (Phase 4 archaeology) is millis.
10//! - **Epoch seconds** (`now_unix_secs`) — legacy schemas already fixed to
11//! seconds (karma chain, write-audit journal, sangha locks). Do not
12//! migrate casually: readers exist.
13//! - **RFC 3339** (`now_rfc3339`) — human-facing coordination surfaces
14//! (lease ledgers, drill reports) where an agent reads the value with a
15//! bare eye.
16//!
17//! Rules:
18//! 1. Never hand-roll `SystemTime::now()` or `Utc::now().timestamp*()` at
19//! a write site — go through this module so the convention has one
20//! implementation.
21//! 2. A persisted timestamp field's doc comment names its unit.
22//! 3. Cross-surface correlation converts through typed `chrono::DateTime`,
23//! never by comparing raw integers (the 1000× ambiguity class).
24
25#![forbid(unsafe_code)]
26
27use std::time::{SystemTime, UNIX_EPOCH};
28
29/// Current Unix time in **milliseconds** — the default for new persisted
30/// event records.
31#[must_use]
32pub fn now_unix_millis() -> i64 {
33 SystemTime::now()
34 .duration_since(UNIX_EPOCH)
35 .map_or(0, |d| d.as_millis() as i64)
36}
37
38/// Current Unix time in **seconds** — legacy schemas fixed to seconds
39/// (karma chain, write-audit journal, sangha locks).
40#[must_use]
41pub fn now_unix_secs() -> u64 {
42 SystemTime::now()
43 .duration_since(UNIX_EPOCH)
44 .map_or(0, |d| d.as_secs())
45}
46
47/// Current time as **RFC 3339** with second precision and `Z` offset —
48/// human-facing coordination surfaces (lease ledgers, drill reports).
49#[must_use]
50pub fn now_rfc3339() -> String {
51 chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
52}
53
54/// Generate a monotonic, time-ordered operation ID (UUIDv7 format) for
55/// end-to-end multi-step write sequence correlation and crash barrier detection (U6).
56#[must_use]
57pub fn new_operation_id() -> String {
58 uuid::Uuid::now_v7().to_string()
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 #[test]
66 fn millis_and_secs_agree() {
67 let secs = i64::try_from(now_unix_secs()).expect("unix seconds fit i64");
68 let millis = now_unix_millis();
69 // The two calls straddle the same wall-clock second in practice;
70 // allow one second of drift between them.
71 assert!(
72 (millis - secs * 1000).abs() <= 1500,
73 "millis {millis} vs secs {secs} disagree by more than a second"
74 );
75 }
76
77 #[test]
78 fn millis_is_thirteen_digits_this_era() {
79 let millis = now_unix_millis();
80 assert!(
81 (1_000_000_000_000..=9_999_999_999_999).contains(&millis),
82 "millis outside the 13-digit era: {millis}"
83 );
84 }
85
86 #[test]
87 fn rfc3339_shape_is_z_terminated() {
88 let ts = now_rfc3339();
89 assert!(ts.ends_with('Z'), "got: {ts}");
90 assert_eq!(ts.len(), 20, "second-precision Z format, got: {ts}");
91 assert!(chrono::DateTime::parse_from_rfc3339(&ts).is_ok());
92 }
93
94 #[test]
95 fn operation_id_is_valid_time_ordered_uuid() {
96 let id1 = new_operation_id();
97 let id2 = new_operation_id();
98 assert_ne!(id1, id2);
99 let parsed1 = uuid::Uuid::parse_str(&id1).expect("valid uuid");
100 let parsed2 = uuid::Uuid::parse_str(&id2).expect("valid uuid");
101 assert_eq!(parsed1.get_version(), Some(uuid::Version::SortRand));
102 assert_eq!(parsed2.get_version(), Some(uuid::Version::SortRand));
103 assert!(id1 <= id2, "monotonic time order");
104 }
105}