Skip to main content

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}