dbmd_core/time.rs
1//! Canonical wall-clock for write-surface timestamp seeding.
2//!
3//! Every write surface that stamps `created` / `updated` (or a log entry's
4//! timestamp) seeds it from [`now`] so all of them agree on one representation:
5//! the current instant as a fixed-offset (UTC) [`DateTime`]. This is the type
6//! [`crate::Frontmatter::created`] / [`crate::Frontmatter::updated`] already
7//! hold, so callers assign it directly with no string round-trip.
8//!
9//! Keeping this in `dbmd-core` (rather than re-deriving it per CLI handler)
10//! means `dbmd write`, `dbmd fm init`, `dbmd fm set`, and `dbmd log append` all
11//! compute "now" the same way from one place — and the thin CLI carries no
12//! bespoke calendar logic.
13//!
14//! ## Reproducibility hook: `DBMD_NOW`
15//!
16//! Because every write surface seeds its timestamps from this one function, it
17//! is also the one place to pin the clock for **deterministic, byte-for-byte**
18//! output. When the `DBMD_NOW` environment variable is set to an RFC3339
19//! timestamp, [`now`] returns that instant verbatim instead of the wall clock,
20//! so a scripted sequence of `dbmd write` / `fm set` / `log` invocations
21//! produces identical `created`/`updated` fields, identical `index.md` /
22//! `index.jsonl` (whose own `updated` is the max over their records), and
23//! identical `log.md` headers on every run. This is the same family of build
24//! hook as `SOURCE_DATE_EPOCH`: unset, behaviour is exactly the wall clock it
25//! always was (zero product impact); set, the toolkit is reproducible — which
26//! the agent-eval golden harness (`crates/dbmd-cli/tests/agent_eval.rs`) relies
27//! on to commit `EXPECTED/` trees that pin the curator's output. A malformed
28//! `DBMD_NOW` is ignored (falls back to the wall clock) rather than aborting an
29//! otherwise-valid write.
30
31use chrono::{DateTime, FixedOffset, Utc};
32
33/// Environment variable that pins [`now`] to a fixed RFC3339 instant. See the
34/// module docs (`## Reproducibility hook`). Unset ⇒ wall clock.
35pub const NOW_OVERRIDE_ENV: &str = "DBMD_NOW";
36
37/// The current instant as a fixed-offset (UTC) timestamp.
38///
39/// Returns `DateTime<FixedOffset>` — the canonical type the universal `created`
40/// / `updated` frontmatter fields and the log entry timestamp hold — so write
41/// surfaces seed it without any RFC3339 string round-trip. Resolution is
42/// chrono's native (sub-second); the canonical writers render it to RFC3339 on
43/// the way to disk.
44///
45/// If [`NOW_OVERRIDE_ENV`] (`DBMD_NOW`) is set to a parseable RFC3339 timestamp,
46/// that fixed instant is returned instead of the wall clock — the deterministic
47/// reproducibility hook (see module docs). An unset or unparseable value falls
48/// back to the wall clock, so the default path is unchanged.
49pub fn now() -> DateTime<FixedOffset> {
50 if let Some(fixed) = now_override() {
51 return fixed;
52 }
53 Utc::now().fixed_offset()
54}
55
56/// Read and parse [`NOW_OVERRIDE_ENV`]. `None` when unset or unparseable (the
57/// caller then uses the wall clock). Normalized to the UTC (zero) offset to
58/// match the wall-clock path, so downstream RFC3339 rendering is offset-stable
59/// regardless of the offset the override was written with.
60fn now_override() -> Option<DateTime<FixedOffset>> {
61 let raw = std::env::var(NOW_OVERRIDE_ENV).ok()?;
62 let raw = raw.trim();
63 if raw.is_empty() {
64 return None;
65 }
66 DateTime::parse_from_rfc3339(raw)
67 .ok()
68 .map(|dt| dt.with_timezone(&Utc).fixed_offset())
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn now_is_utc_offset() {
77 // The canonical seed is always emitted at the UTC (zero) offset, so two
78 // calls and any downstream RFC3339 rendering stay timezone-stable. Hold
79 // for both paths: explicitly pin a non-UTC-offset override and confirm
80 // it is normalized to the zero offset (so a `DBMD_NOW` set in the
81 // ambient env can never make the seed timezone-unstable).
82 assert_eq!(
83 now_override_disabled().offset(),
84 &FixedOffset::east_opt(0).unwrap()
85 );
86 let pinned = parse_override("2026-05-29T12:00:00+05:30").expect("valid override");
87 assert_eq!(pinned.offset(), &FixedOffset::east_opt(0).unwrap());
88 assert_eq!(
89 pinned,
90 "2026-05-29T06:30:00Z"
91 .parse::<DateTime<FixedOffset>>()
92 .unwrap()
93 );
94 }
95
96 #[test]
97 fn now_is_monotonic_nondecreasing() {
98 // Wall-clock now must not go backwards between two reads in the same
99 // process (guards against an accidental fixed/zero implementation).
100 // Read the wall clock directly so an ambient `DBMD_NOW` in the test
101 // environment doesn't turn this into a trivially-equal assertion.
102 let a = now_override_disabled();
103 let b = now_override_disabled();
104 assert!(b >= a, "now() went backwards: {a} then {b}");
105 }
106
107 #[test]
108 fn override_parses_rfc3339_and_falls_back_when_absent_or_bad() {
109 // The reproducibility hook: a valid RFC3339 string pins `now()`; an
110 // empty or unparseable value yields `None` (wall-clock fallback). We
111 // exercise the pure parse helper here — the env-var read is the only
112 // thin wrapper around it and is covered end-to-end by the CLI tests
113 // that set `DBMD_NOW` (golden determinism would break loudly otherwise).
114 assert_eq!(
115 parse_override("2026-05-29T10:15:00Z"),
116 Some(
117 "2026-05-29T10:15:00Z"
118 .parse::<DateTime<FixedOffset>>()
119 .unwrap()
120 )
121 );
122 assert_eq!(parse_override(""), None);
123 assert_eq!(parse_override(" "), None);
124 assert_eq!(parse_override("not-a-timestamp"), None);
125 assert_eq!(parse_override("2026-05-29"), None); // date-only is not RFC3339
126 }
127
128 /// The wall-clock instant, bypassing the env override — for tests that must
129 /// observe real time regardless of an ambient `DBMD_NOW`.
130 fn now_override_disabled() -> DateTime<FixedOffset> {
131 Utc::now().fixed_offset()
132 }
133
134 /// Pure parse of an override string (the body of [`now_override`] minus the
135 /// env read), so the parse contract is unit-testable without mutating
136 /// process-global env state from a parallel test.
137 fn parse_override(raw: &str) -> Option<DateTime<FixedOffset>> {
138 let raw = raw.trim();
139 if raw.is_empty() {
140 return None;
141 }
142 DateTime::parse_from_rfc3339(raw)
143 .ok()
144 .map(|dt| dt.with_timezone(&Utc).fixed_offset())
145 }
146}