Skip to main content

merman_core/
runtime.rs

1use crate::time::CivilDate;
2#[cfg(feature = "diagram-gantt")]
3use crate::time::CivilDateTime;
4#[cfg(any(test, feature = "diagram-gantt"))]
5use crate::time::OffsetDateTime;
6#[cfg(test)]
7use crate::time::UtcOffset;
8use std::cell::RefCell;
9use std::num::NonZeroU64;
10
11const DETERMINISTIC_UNIX_MILLIS: i64 = 0;
12const DETERMINISTIC_OPERATION_SEED: u64 = 0x6D65_726D_616E_0001;
13
14thread_local! {
15    static OPERATION_CONTEXT: RefCell<Option<OperationContext>> = const { RefCell::new(None) };
16}
17
18/// Optional system adapters that a runtime policy can request.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum RuntimeCapability {
21    SystemClock,
22    SystemTimeZone,
23    SystemRandom,
24    SystemTiming,
25}
26
27impl RuntimeCapability {
28    pub const ALL: [Self; 4] = [
29        Self::SystemClock,
30        Self::SystemTimeZone,
31        Self::SystemRandom,
32        Self::SystemTiming,
33    ];
34
35    pub const fn id(self) -> &'static str {
36        match self {
37            Self::SystemClock => "system-clock",
38            Self::SystemTimeZone => "system-timezone",
39            Self::SystemRandom => "system-random",
40            Self::SystemTiming => "system-timing",
41        }
42    }
43}
44
45const COMPILED_SYSTEM_ADAPTER_IDS: &[&str] = &[
46    #[cfg(feature = "system-clock")]
47    RuntimeCapability::SystemClock.id(),
48    #[cfg(feature = "system-timezone")]
49    RuntimeCapability::SystemTimeZone.id(),
50    #[cfg(feature = "system-random")]
51    RuntimeCapability::SystemRandom.id(),
52    #[cfg(feature = "system-timing")]
53    RuntimeCapability::SystemTiming.id(),
54];
55
56/// Returns the system adapters actually compiled by the owning core crate after Cargo feature
57/// unification.
58pub const fn compiled_system_adapter_ids() -> &'static [&'static str] {
59    COMPILED_SYSTEM_ADAPTER_IDS
60}
61
62/// Failure to materialize an explicitly requested runtime policy.
63#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
64pub enum RuntimePolicyError {
65    #[error("runtime capability `{}` is not compiled into this artifact", .0.id())]
66    MissingCapability(RuntimeCapability),
67    #[error("local UTC offset must be between -1439 and 1439 minutes, got {0}")]
68    InvalidFixedOffset(i32),
69    #[error("system clock instant is outside the supported millisecond range")]
70    SystemClockOutOfRange,
71    #[error("system time-zone adapter failed: {0}")]
72    SystemTimeZone(String),
73    #[error("runtime instant {0} is outside the supported calendar range")]
74    InstantOutOfRange(i64),
75    #[error("fixed_today local datetime {0}T00:00:00 cannot be resolved in the selected time zone")]
76    FixedLocalMidnightOutOfRange(CivilDate),
77    #[error("system random adapter failed: {0}")]
78    SystemRandom(String),
79}
80
81impl RuntimePolicyError {
82    pub const fn missing_capability(&self) -> Option<RuntimeCapability> {
83        match self {
84            Self::MissingCapability(capability) => Some(*capability),
85            _ => None,
86        }
87    }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91enum ClockPolicy {
92    Fixed(i64),
93    System,
94    Captured(i64),
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98enum SeedPolicy {
99    Fixed(u64),
100    System,
101    Captured(u64),
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105enum TimingPolicy {
106    Disabled,
107    System,
108}
109
110#[cfg(feature = "system-timing")]
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112struct SystemTimingAuthority;
113
114#[cfg(not(feature = "system-timing"))]
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116enum SystemTimingAuthority {}
117
118/// Runtime choices used to create one immutable operation context.
119///
120/// The default policy is deterministic. System state is consulted only after callers explicitly
121/// select a system adapter, either individually or through [`RuntimePolicy::try_native`].
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct RuntimePolicy {
124    clock: ClockPolicy,
125    local_time_zone: crate::time::LocalTimeZone,
126    fixed_today_local: Option<CivilDate>,
127    seed: SeedPolicy,
128    timing: TimingPolicy,
129}
130
131impl Default for RuntimePolicy {
132    fn default() -> Self {
133        Self::deterministic()
134    }
135}
136
137impl RuntimePolicy {
138    /// System adapters selected by [`Self::try_native`].
139    ///
140    /// This list is a policy contract, not a report of what Cargo compiled. Consumers that
141    /// advertise a `native` policy should intersect it with [`compiled_system_adapter_ids`].
142    pub const NATIVE_SYSTEM_ADAPTER_IDS: &'static [&'static str] = &[
143        RuntimeCapability::SystemClock.id(),
144        RuntimeCapability::SystemTimeZone.id(),
145        RuntimeCapability::SystemRandom.id(),
146    ];
147
148    /// Returns the deterministic, target-independent core policy.
149    pub fn deterministic() -> Self {
150        Self {
151            clock: ClockPolicy::Fixed(DETERMINISTIC_UNIX_MILLIS),
152            local_time_zone: crate::time::LocalTimeZone::utc(),
153            fixed_today_local: None,
154            seed: SeedPolicy::Fixed(DETERMINISTIC_OPERATION_SEED),
155            timing: TimingPolicy::Disabled,
156        }
157    }
158
159    /// Selects the native clock, complete local-time rules, and random source.
160    ///
161    /// Timing instrumentation remains opt-in through [`RuntimePolicy::try_with_system_timing`]
162    /// because enabling it changes observable diagnostics and adds work to every operation.
163    pub fn try_native() -> Result<Self, RuntimePolicyError> {
164        Self::deterministic()
165            .try_with_system_clock()?
166            .try_with_system_time_zone()?
167            .try_with_system_random()
168    }
169
170    /// Replays an already captured operation without consulting system state again.
171    pub fn from_operation_context(context: OperationContext) -> Self {
172        let timing = if context.timing.is_some() {
173            TimingPolicy::System
174        } else {
175            TimingPolicy::Disabled
176        };
177        Self {
178            clock: ClockPolicy::Captured(context.unix_millis),
179            local_time_zone: context.local_time_zone,
180            fixed_today_local: context.today_is_fixed.then_some(context.today_local),
181            seed: SeedPolicy::Captured(context.seed),
182            timing,
183        }
184    }
185
186    pub fn with_fixed_unix_millis(mut self, unix_millis: i64) -> Self {
187        self.clock = ClockPolicy::Fixed(unix_millis);
188        self
189    }
190
191    pub fn try_with_system_clock(mut self) -> Result<Self, RuntimePolicyError> {
192        require_system_adapter(
193            RuntimeCapability::SystemClock,
194            cfg!(feature = "system-clock"),
195        )?;
196        self.clock = ClockPolicy::System;
197        Ok(self)
198    }
199
200    pub fn with_fixed_today(mut self, today: Option<CivilDate>) -> Self {
201        self.fixed_today_local = today;
202        self
203    }
204
205    /// Freezes both the local calendar day and the operation clock at that day's local midnight.
206    ///
207    /// Use this when a host accepts a date-only configuration value such as `fixed_today`.
208    /// Resolving it through the selected time zone preserves target-date DST behavior and returns
209    /// a typed error when the date cannot be represented instead of overflowing at an offset
210    /// boundary.
211    pub fn try_with_fixed_today_at_local_midnight(
212        mut self,
213        today: CivilDate,
214    ) -> Result<Self, RuntimePolicyError> {
215        let local = self
216            .local_time_zone
217            .resolve_local(today.at_midnight())
218            .ok_or(RuntimePolicyError::FixedLocalMidnightOutOfRange(today))?;
219        self.clock = ClockPolicy::Fixed(local.timestamp_millis());
220        self.fixed_today_local = Some(today);
221        Ok(self)
222    }
223
224    /// Selects a fixed UTC offset without overloading a sentinel value to mean "system".
225    pub fn try_with_fixed_local_offset_minutes(
226        mut self,
227        offset_minutes: i32,
228    ) -> Result<Self, RuntimePolicyError> {
229        self.local_time_zone = crate::time::LocalTimeZone::fixed(offset_minutes)?;
230        Ok(self)
231    }
232
233    pub fn with_local_time_zone(mut self, time_zone: crate::time::LocalTimeZone) -> Self {
234        self.local_time_zone = time_zone;
235        self
236    }
237
238    pub fn try_with_system_time_zone(mut self) -> Result<Self, RuntimePolicyError> {
239        self.local_time_zone = crate::time::LocalTimeZone::try_system()?;
240        Ok(self)
241    }
242
243    pub fn with_fixed_seed(mut self, seed: u64) -> Self {
244        self.seed = SeedPolicy::Fixed(seed);
245        self
246    }
247
248    pub fn try_with_system_random(mut self) -> Result<Self, RuntimePolicyError> {
249        require_system_adapter(
250            RuntimeCapability::SystemRandom,
251            cfg!(feature = "system-random"),
252        )?;
253        self.seed = SeedPolicy::System;
254        Ok(self)
255    }
256
257    /// Enables the compiled system timing adapter.
258    pub fn try_with_system_timing(mut self) -> Result<Self, RuntimePolicyError> {
259        require_system_adapter(
260            RuntimeCapability::SystemTiming,
261            cfg!(feature = "system-timing"),
262        )?;
263        self.timing = TimingPolicy::System;
264        Ok(self)
265    }
266
267    pub fn fixed_local_offset_minutes(&self) -> Option<i32> {
268        self.local_time_zone.fixed_offset_minutes()
269    }
270
271    pub fn fixed_today(&self) -> Option<CivilDate> {
272        self.fixed_today_local
273    }
274
275    pub fn local_time_zone(&self) -> &crate::time::LocalTimeZone {
276        &self.local_time_zone
277    }
278
279    /// Freezes all selected adapters for one parse or render operation.
280    pub fn begin_operation(&self) -> Result<OperationContext, RuntimePolicyError> {
281        let (unix_millis, clock_source) = match self.clock {
282            ClockPolicy::Fixed(unix_millis) => (unix_millis, RuntimeValueSource::Fixed),
283            ClockPolicy::System => (system_unix_millis()?, RuntimeValueSource::System),
284            ClockPolicy::Captured(unix_millis) => (unix_millis, RuntimeValueSource::Captured),
285        };
286        let local_time_zone = self.local_time_zone.clone();
287        let today_local = match self.fixed_today_local {
288            Some(today) => today,
289            None => local_date_at(unix_millis, &local_time_zone)?,
290        };
291        let (seed, random_source) = match self.seed {
292            SeedPolicy::Fixed(seed) => (seed, RuntimeValueSource::Fixed),
293            SeedPolicy::System => (system_seed()?, RuntimeValueSource::System),
294            SeedPolicy::Captured(seed) => (seed, RuntimeValueSource::Captured),
295        };
296        let timing = match self.timing {
297            TimingPolicy::Disabled => None,
298            TimingPolicy::System => Some(system_timing_authority()?),
299        };
300
301        Ok(OperationContext {
302            unix_millis,
303            clock_source,
304            today_local,
305            today_is_fixed: self.fixed_today_local.is_some(),
306            local_time_zone,
307            seed,
308            random_source,
309            timing,
310        })
311    }
312}
313
314/// Immutable environment captured at the start of one operation.
315#[derive(Debug, Clone, PartialEq, Eq)]
316pub struct OperationContext {
317    unix_millis: i64,
318    clock_source: RuntimeValueSource,
319    today_local: CivilDate,
320    today_is_fixed: bool,
321    local_time_zone: crate::time::LocalTimeZone,
322    seed: u64,
323    random_source: RuntimeValueSource,
324    timing: Option<SystemTimingAuthority>,
325}
326
327/// An operation-derived authority to read the monotonic system clock.
328///
329/// The token has no public constructor. Code can only obtain one from an operation whose policy
330/// explicitly enabled system timing.
331#[derive(Debug, Clone, Copy)]
332pub struct OperationTiming {
333    authority: SystemTimingAuthority,
334}
335
336/// A monotonic timer started through [`OperationTiming`].
337#[derive(Debug)]
338pub struct OperationTimer {
339    #[cfg(feature = "system-timing")]
340    started_at: web_time::Instant,
341    #[cfg(not(feature = "system-timing"))]
342    unavailable: std::convert::Infallible,
343}
344
345/// Failure to request timing authority from an operation that did not enable it.
346#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
347#[error("operation timing was not enabled for this operation")]
348pub struct OperationTimingUnavailable;
349
350/// Provenance of a runtime value captured for one operation.
351#[derive(Debug, Clone, Copy, PartialEq, Eq)]
352pub enum RuntimeValueSource {
353    Fixed,
354    System,
355    Captured,
356}
357
358impl RuntimeValueSource {
359    pub const fn id(self) -> &'static str {
360        match self {
361            Self::Fixed => "fixed",
362            Self::System => "system",
363            Self::Captured => "captured",
364        }
365    }
366}
367
368impl OperationContext {
369    pub const fn unix_millis(&self) -> i64 {
370        self.unix_millis
371    }
372
373    pub const fn clock_source(&self) -> RuntimeValueSource {
374        self.clock_source
375    }
376
377    pub const fn today_local(&self) -> CivilDate {
378        self.today_local
379    }
380
381    pub const fn today_is_fixed(&self) -> bool {
382        self.today_is_fixed
383    }
384
385    pub fn local_time_zone(&self) -> &crate::time::LocalTimeZone {
386        &self.local_time_zone
387    }
388
389    pub const fn seed(&self) -> u64 {
390        self.seed
391    }
392
393    pub const fn random_source(&self) -> RuntimeValueSource {
394        self.random_source
395    }
396
397    pub fn timing(&self) -> Option<OperationTiming> {
398        self.timing.map(|authority| OperationTiming { authority })
399    }
400
401    pub fn require_timing(&self) -> Result<OperationTiming, OperationTimingUnavailable> {
402        self.timing().ok_or(OperationTimingUnavailable)
403    }
404
405    /// Derives a stable value from this operation's random key without sharing mutable PRNG state.
406    ///
407    /// Callers must use a durable, owner-qualified domain such as `block.generated-id`. The
408    /// ordinal is local to that domain, so adding random consumers in another family cannot shift
409    /// existing output.
410    pub fn derive_u64(&self, domain: &str, ordinal: u64) -> u64 {
411        derive_random_u64(self.seed, domain, ordinal)
412    }
413
414    pub fn derive_nonzero_u64(&self, domain: &str, ordinal: u64) -> NonZeroU64 {
415        NonZeroU64::new(self.derive_u64(domain, ordinal)).unwrap_or(NonZeroU64::MIN)
416    }
417
418    pub fn derive_hex(&self, domain: &str, ordinal: u64, len: usize) -> String {
419        derive_random_hex(self.seed, domain, ordinal, len)
420    }
421}
422
423impl OperationTiming {
424    /// Starts a timer using the system adapter authorized for this operation.
425    pub fn start(self) -> OperationTimer {
426        #[cfg(feature = "system-timing")]
427        {
428            let _ = self.authority;
429            OperationTimer {
430                started_at: web_time::Instant::now(),
431            }
432        }
433
434        #[cfg(not(feature = "system-timing"))]
435        {
436            match self.authority {}
437        }
438    }
439}
440
441impl OperationTimer {
442    /// Returns the time elapsed since this timer was started.
443    pub fn elapsed(self) -> std::time::Duration {
444        #[cfg(feature = "system-timing")]
445        {
446            self.started_at.elapsed()
447        }
448
449        #[cfg(not(feature = "system-timing"))]
450        {
451            match self.unavailable {}
452        }
453    }
454}
455
456fn require_system_adapter(
457    capability: RuntimeCapability,
458    available: bool,
459) -> Result<(), RuntimePolicyError> {
460    if available {
461        Ok(())
462    } else {
463        Err(RuntimePolicyError::MissingCapability(capability))
464    }
465}
466
467#[cfg(feature = "system-timing")]
468fn system_timing_authority() -> Result<SystemTimingAuthority, RuntimePolicyError> {
469    Ok(SystemTimingAuthority)
470}
471
472#[cfg(not(feature = "system-timing"))]
473fn system_timing_authority() -> Result<SystemTimingAuthority, RuntimePolicyError> {
474    Err(RuntimePolicyError::MissingCapability(
475        RuntimeCapability::SystemTiming,
476    ))
477}
478
479fn local_date_at(
480    unix_millis: i64,
481    time_zone: &crate::time::LocalTimeZone,
482) -> Result<CivilDate, RuntimePolicyError> {
483    time_zone
484        .at_instant(unix_millis)
485        .map(|local| local.local_datetime().date())
486        .ok_or(RuntimePolicyError::InstantOutOfRange(unix_millis))
487}
488
489#[cfg(feature = "system-clock")]
490fn system_unix_millis() -> Result<i64, RuntimePolicyError> {
491    use std::time::{SystemTime, UNIX_EPOCH};
492
493    let millis = match SystemTime::now().duration_since(UNIX_EPOCH) {
494        Ok(duration) => i128::try_from(duration.as_millis())
495            .map_err(|_| RuntimePolicyError::SystemClockOutOfRange)?,
496        Err(error) => -i128::try_from(error.duration().as_millis())
497            .map_err(|_| RuntimePolicyError::SystemClockOutOfRange)?,
498    };
499    millis
500        .try_into()
501        .map_err(|_| RuntimePolicyError::SystemClockOutOfRange)
502}
503
504#[cfg(not(feature = "system-clock"))]
505fn system_unix_millis() -> Result<i64, RuntimePolicyError> {
506    Err(RuntimePolicyError::MissingCapability(
507        RuntimeCapability::SystemClock,
508    ))
509}
510
511#[cfg(feature = "system-random")]
512fn system_seed() -> Result<u64, RuntimePolicyError> {
513    let mut bytes = [0_u8; size_of::<u64>()];
514    getrandom::fill(&mut bytes)
515        .map_err(|error| RuntimePolicyError::SystemRandom(error.to_string()))?;
516    Ok(u64::from_ne_bytes(bytes))
517}
518
519#[cfg(not(feature = "system-random"))]
520fn system_seed() -> Result<u64, RuntimePolicyError> {
521    Err(RuntimePolicyError::MissingCapability(
522        RuntimeCapability::SystemRandom,
523    ))
524}
525
526pub(crate) fn with_operation_context<R>(context: &OperationContext, f: impl FnOnce() -> R) -> R {
527    OPERATION_CONTEXT.with(|cell| {
528        let previous = cell.replace(Some(context.clone()));
529        struct Restore<'a> {
530            cell: &'a RefCell<Option<OperationContext>>,
531            previous: Option<OperationContext>,
532        }
533        impl Drop for Restore<'_> {
534            fn drop(&mut self) {
535                self.cell.replace(self.previous.take());
536            }
537        }
538        let _restore = Restore { cell, previous };
539        f()
540    })
541}
542
543#[cfg(any(test, feature = "diagram-gantt"))]
544pub(crate) fn today_local() -> CivilDate {
545    active_operation_context().today_local
546}
547
548#[cfg(feature = "diagram-gantt")]
549pub(crate) fn resolve_local_datetime(local: CivilDateTime) -> Option<OffsetDateTime> {
550    active_operation_context()
551        .local_time_zone
552        .resolve_local(local)
553}
554
555#[cfg(feature = "diagram-gantt")]
556pub(crate) fn datetime_to_local(datetime: OffsetDateTime) -> OffsetDateTime {
557    active_operation_context()
558        .local_time_zone
559        .at_instant(datetime.timestamp_millis())
560        .unwrap_or(datetime)
561}
562
563#[cfg(feature = "diagram-gantt")]
564pub(crate) fn datetime_to_local_civil(datetime: OffsetDateTime) -> CivilDateTime {
565    datetime_to_local(datetime).local_datetime()
566}
567
568#[cfg(any(
569    test,
570    feature = "diagram-block",
571    feature = "diagram-git-graph",
572    feature = "diagram-mindmap"
573))]
574pub(crate) fn generated_id_hex(domain: &str, counter: u64, len: usize) -> String {
575    let context = active_operation_context();
576    context.derive_hex(domain, counter, len)
577}
578
579fn derive_random_hex(seed: u64, domain: &str, ordinal: u64, len: usize) -> String {
580    const HEX: &[u8; 16] = b"0123456789abcdef";
581
582    let mut out = String::with_capacity(len);
583    let mut state = derive_random_u64(seed, domain, ordinal);
584    while out.len() < len {
585        state = splitmix64(state);
586        for shift in (0..16).rev() {
587            if out.len() == len {
588                break;
589            }
590            let idx = ((state >> (shift * 4)) & 0xF) as usize;
591            out.push(HEX[idx] as char);
592        }
593    }
594    out
595}
596
597fn derive_random_u64(seed: u64, domain: &str, ordinal: u64) -> u64 {
598    const OPERATION_RANDOM_DOMAIN: u64 = 0x6D65_726D_616E_2D72;
599    let domain_hash = domain
600        .as_bytes()
601        .iter()
602        .fold(0xcbf2_9ce4_8422_2325, |hash, byte| {
603            (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3)
604        });
605    splitmix64(
606        seed ^ OPERATION_RANDOM_DOMAIN
607            ^ domain_hash.rotate_left(17)
608            ^ ordinal.wrapping_mul(0x9E37_79B9_7F4A_7C15),
609    )
610}
611
612fn splitmix64(state: u64) -> u64 {
613    let mut z = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
614    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
615    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
616    z ^ (z >> 31)
617}
618
619#[cfg(any(
620    test,
621    feature = "diagram-gantt",
622    feature = "diagram-block",
623    feature = "diagram-git-graph",
624    feature = "diagram-mindmap"
625))]
626fn active_operation_context() -> OperationContext {
627    OPERATION_CONTEXT
628        .with(|cell| cell.borrow().clone())
629        .unwrap_or_else(|| {
630            RuntimePolicy::deterministic()
631                .begin_operation()
632                .expect("the deterministic runtime policy is infallible")
633        })
634}
635
636#[cfg(test)]
637mod tests {
638    use super::*;
639
640    fn date(year: i32, month: u32, day: u32) -> CivilDate {
641        CivilDate::new(year, month, day).expect("valid test date")
642    }
643
644    #[test]
645    fn operation_context_restores_after_panic() {
646        let outer = RuntimePolicy::deterministic()
647            .with_fixed_today(Some(date(2026, 7, 18)))
648            .begin_operation()
649            .unwrap();
650        let inner = RuntimePolicy::deterministic()
651            .with_fixed_today(Some(date(2030, 1, 2)))
652            .begin_operation()
653            .unwrap();
654
655        with_operation_context(&outer, || {
656            let panic = std::panic::catch_unwind(|| {
657                with_operation_context(&inner, || panic!("test panic"));
658            });
659            assert!(panic.is_err());
660            assert_eq!(today_local(), outer.today_local());
661        });
662    }
663
664    #[test]
665    fn deterministic_policy_uses_epoch_utc_and_fixed_seed() {
666        let context = RuntimePolicy::deterministic().begin_operation().unwrap();
667
668        assert_eq!(context.unix_millis(), 0);
669        assert_eq!(context.today_local(), date(1970, 1, 1));
670        assert_eq!(context.local_time_zone().fixed_offset_minutes(), Some(0));
671        assert_eq!(context.seed(), DETERMINISTIC_OPERATION_SEED);
672        assert_eq!(context.clock_source(), RuntimeValueSource::Fixed);
673        assert_eq!(context.random_source(), RuntimeValueSource::Fixed);
674        assert!(context.timing().is_none());
675    }
676
677    #[test]
678    fn fixed_today_local_midnight_rejects_unrepresentable_offset_boundary() {
679        let earliest_day = OffsetDateTime::from_unix_millis(i64::MIN, UtcOffset::UTC)
680            .utc_datetime()
681            .date();
682        let error = RuntimePolicy::deterministic()
683            .try_with_fixed_local_offset_minutes(1439)
684            .expect("valid fixed offset")
685            .try_with_fixed_today_at_local_midnight(earliest_day)
686            .expect_err("minimum date at eastern boundary must be rejected");
687
688        assert_eq!(
689            error,
690            RuntimePolicyError::FixedLocalMidnightOutOfRange(earliest_day)
691        );
692    }
693
694    #[test]
695    fn replayed_context_is_attested_as_captured() {
696        let original = RuntimePolicy::deterministic().begin_operation().unwrap();
697        let replayed = RuntimePolicy::from_operation_context(original)
698            .begin_operation()
699            .unwrap();
700
701        assert_eq!(replayed.clock_source(), RuntimeValueSource::Captured);
702        assert_eq!(replayed.random_source(), RuntimeValueSource::Captured);
703        assert!(!replayed.today_is_fixed());
704    }
705
706    #[test]
707    fn replayed_context_preserves_computed_and_fixed_today_semantics() {
708        let computed = RuntimePolicy::deterministic().begin_operation().unwrap();
709        let recomputed = RuntimePolicy::from_operation_context(computed)
710            .try_with_fixed_local_offset_minutes(-60)
711            .unwrap()
712            .begin_operation()
713            .unwrap();
714        assert_eq!(recomputed.today_local(), date(1969, 12, 31));
715        assert!(!recomputed.today_is_fixed());
716
717        let fixed_today = date(2026, 7, 22);
718        let fixed = RuntimePolicy::deterministic()
719            .with_fixed_today(Some(fixed_today))
720            .begin_operation()
721            .unwrap();
722        let replayed_fixed = RuntimePolicy::from_operation_context(fixed)
723            .try_with_fixed_local_offset_minutes(-60)
724            .unwrap()
725            .begin_operation()
726            .unwrap();
727        assert_eq!(replayed_fixed.today_local(), fixed_today);
728        assert!(replayed_fixed.today_is_fixed());
729    }
730
731    #[test]
732    fn operation_seed_is_domain_separated_and_context_owned() {
733        let first = RuntimePolicy::deterministic()
734            .with_fixed_seed(1)
735            .begin_operation()
736            .unwrap();
737        let second = RuntimePolicy::deterministic()
738            .with_fixed_seed(2)
739            .begin_operation()
740            .unwrap();
741
742        let first_id = with_operation_context(&first, || generated_id_hex("test.first", 7, 12));
743        let repeated = with_operation_context(&first, || generated_id_hex("test.first", 7, 12));
744        let second_id = with_operation_context(&second, || generated_id_hex("test.first", 7, 12));
745        let other_domain =
746            with_operation_context(&first, || generated_id_hex("test.second", 7, 12));
747
748        assert_eq!(first_id, repeated);
749        assert_ne!(first_id, second_id);
750        assert_ne!(first_id, other_domain);
751    }
752
753    #[test]
754    fn native_policy_reports_the_first_missing_required_adapter() {
755        let expected = RuntimePolicy::NATIVE_SYSTEM_ADAPTER_IDS
756            .iter()
757            .map(|id| {
758                RuntimeCapability::ALL
759                    .into_iter()
760                    .find(|capability| capability.id() == *id)
761                    .expect("native policy adapter IDs must use the runtime capability vocabulary")
762            })
763            .map(|capability| {
764                (
765                    capability,
766                    compiled_system_adapter_ids().contains(&capability.id()),
767                )
768            })
769            .into_iter()
770            .find_map(|(capability, available)| (!available).then_some(capability));
771
772        match expected {
773            Some(capability) => assert_eq!(
774                RuntimePolicy::try_native().unwrap_err(),
775                RuntimePolicyError::MissingCapability(capability)
776            ),
777            None => assert!(RuntimePolicy::try_native().is_ok()),
778        }
779    }
780
781    #[test]
782    fn native_policy_adapter_contract_excludes_timing() {
783        assert_eq!(
784            RuntimePolicy::NATIVE_SYSTEM_ADAPTER_IDS,
785            ["system-clock", "system-timezone", "system-random"]
786        );
787        assert!(
788            !RuntimePolicy::NATIVE_SYSTEM_ADAPTER_IDS
789                .contains(&RuntimeCapability::SystemTiming.id())
790        );
791    }
792
793    #[cfg(all(
794        feature = "system-clock",
795        feature = "system-timezone",
796        feature = "system-random"
797    ))]
798    #[test]
799    fn native_policy_does_not_enable_timing_instrumentation() {
800        let context = RuntimePolicy::try_native()
801            .unwrap()
802            .begin_operation()
803            .unwrap();
804
805        assert!(context.timing().is_none());
806    }
807
808    #[test]
809    fn compiled_system_adapter_ids_follow_the_canonical_order() {
810        let expected = RuntimeCapability::ALL
811            .into_iter()
812            .filter(|capability| match capability {
813                RuntimeCapability::SystemClock => cfg!(feature = "system-clock"),
814                RuntimeCapability::SystemTimeZone => cfg!(feature = "system-timezone"),
815                RuntimeCapability::SystemRandom => cfg!(feature = "system-random"),
816                RuntimeCapability::SystemTiming => cfg!(feature = "system-timing"),
817            })
818            .map(RuntimeCapability::id)
819            .collect::<Vec<_>>();
820
821        assert_eq!(compiled_system_adapter_ids(), expected);
822    }
823
824    #[test]
825    fn fixed_offset_changes_the_local_date_without_changing_the_instant() {
826        let west = RuntimePolicy::deterministic()
827            .try_with_fixed_local_offset_minutes(-60)
828            .unwrap()
829            .begin_operation()
830            .unwrap();
831        let utc = RuntimePolicy::deterministic()
832            .try_with_fixed_local_offset_minutes(0)
833            .unwrap()
834            .begin_operation()
835            .unwrap();
836
837        assert_eq!(west.unix_millis(), utc.unix_millis());
838        assert_eq!(west.today_local(), date(1969, 12, 31));
839        assert_eq!(utc.today_local(), date(1970, 1, 1));
840    }
841
842    #[cfg(feature = "system-timing")]
843    #[test]
844    fn explicitly_enabled_system_timing_is_issued_by_the_operation_context() {
845        let context = RuntimePolicy::deterministic()
846            .try_with_system_timing()
847            .unwrap()
848            .begin_operation()
849            .unwrap();
850
851        let timing = context.require_timing().unwrap();
852        let _elapsed = timing.start().elapsed();
853    }
854
855    #[cfg(not(feature = "system-timing"))]
856    #[test]
857    fn deterministic_context_cannot_forge_timing_without_system_timing() {
858        let context = RuntimePolicy::deterministic().begin_operation().unwrap();
859
860        assert_eq!(
861            context.require_timing().unwrap_err(),
862            OperationTimingUnavailable
863        );
864    }
865}