Skip to main content

franken_kernel/
lib.rs

1//! Suite-wide type substrate for FrankenSuite (bd-1usdh.1, bd-1usdh.2).
2//!
3//! Canonical identifier, version, and context types used across all
4//! FrankenSuite projects for cross-project tracing, decision logging,
5//! capability management, and schema compatibility.
6//!
7//! # Identifiers
8//!
9//! All identifier types are 128-bit, `Copy`, `Send + Sync`, and
10//! zero-cost abstractions over `[u8; 16]`.
11//!
12//! # Capability Context
13//!
14//! [`Cx`] is the core context type threaded through all operations.
15//! It carries a [`TraceId`], a [`Budget`] (tropical semiring), and
16//! a capability set generic parameter. Child contexts inherit the
17//! parent's trace and enforce budget monotonicity.
18//!
19//! ```
20//! use franken_kernel::{Cx, Budget, NoCaps, TraceId};
21//!
22//! let trace = TraceId::from_parts(1_700_000_000_000, 42);
23//! let cx = Cx::new(trace, Budget::new(5000), NoCaps);
24//! assert_eq!(cx.budget().remaining_ms(), 5000);
25//!
26//! let child = cx.child(NoCaps, Budget::new(3000));
27//! assert_eq!(child.budget().remaining_ms(), 3000);
28//! assert_eq!(child.depth(), 1);
29//! ```
30
31// CANONICAL TYPE ENFORCEMENT (bd-1usdh.3):
32// The types defined in this crate (TraceId, DecisionId, PolicyId,
33// SchemaVersion, Budget, Cx, NoCaps) are the SOLE canonical definitions
34// for the entire FrankenSuite. No other crate may define competing types
35// with the same names. Use `scripts/check_type_forks.sh` to verify.
36// See also: `.type_fork_baseline.json` for known pre-migration forks.
37
38#![forbid(unsafe_code)]
39#![no_std]
40
41extern crate alloc;
42
43use alloc::fmt;
44use alloc::string::String;
45use alloc::vec::Vec;
46use core::marker::PhantomData;
47use core::str::FromStr;
48
49use serde::{Deserialize, Serialize};
50
51// ---------------------------------------------------------------------------
52// TraceId — 128-bit time-ordered unique identifier
53// ---------------------------------------------------------------------------
54
55/// 128-bit unique trace identifier.
56///
57/// Uses UUIDv7-style layout for time-ordered generation: the high 48 bits
58/// encode a millisecond Unix timestamp, the remaining 80 bits are random.
59///
60/// ```
61/// use franken_kernel::TraceId;
62///
63/// let id = TraceId::from_parts(1_700_000_000_000, 0xABCD_EF01_2345_6789_AB);
64/// let hex = id.to_string();
65/// let parsed: TraceId = hex.parse().unwrap();
66/// assert_eq!(id, parsed);
67/// ```
68#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
69#[serde(transparent)]
70pub struct TraceId(
71    /// Hex-encoded 128-bit identifier.
72    #[serde(with = "hex_u128")]
73    u128,
74);
75
76impl TraceId {
77    /// Create a `TraceId` from raw 128-bit value.
78    #[must_use]
79    pub const fn from_raw(raw: u128) -> Self {
80        Self(raw)
81    }
82
83    /// Create a `TraceId` from a millisecond timestamp and random bits.
84    ///
85    /// The high 48 bits store `ts_ms`, the low 80 bits store `random`.
86    /// The `random` value is truncated to 80 bits.
87    #[must_use]
88    pub const fn from_parts(ts_ms: u64, random: u128) -> Self {
89        let ts_bits = (ts_ms as u128) << 80;
90        let rand_bits = random & 0xFFFF_FFFF_FFFF_FFFF_FFFF; // mask to 80 bits
91        Self(ts_bits | rand_bits)
92    }
93
94    /// Extract the millisecond timestamp from the high 48 bits.
95    #[must_use]
96    pub const fn timestamp_ms(self) -> u64 {
97        (self.0 >> 80) as u64
98    }
99
100    /// Return the raw 128-bit value.
101    #[must_use]
102    pub const fn as_u128(self) -> u128 {
103        self.0
104    }
105
106    /// Return the bytes in big-endian order.
107    #[must_use]
108    pub const fn to_bytes(self) -> [u8; 16] {
109        self.0.to_be_bytes()
110    }
111
112    /// Construct from big-endian bytes.
113    #[must_use]
114    pub const fn from_bytes(bytes: [u8; 16]) -> Self {
115        Self(u128::from_be_bytes(bytes))
116    }
117}
118
119impl fmt::Debug for TraceId {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        write!(f, "TraceId({:032x})", self.0)
122    }
123}
124
125impl fmt::Display for TraceId {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        write!(f, "{:032x}", self.0)
128    }
129}
130
131impl FromStr for TraceId {
132    type Err = ParseIdError;
133
134    fn from_str(s: &str) -> Result<Self, Self::Err> {
135        let val = u128::from_str_radix(s, 16).map_err(|_| ParseIdError {
136            kind: "TraceId",
137            input_len: s.len(),
138        })?;
139        Ok(Self(val))
140    }
141}
142
143// ---------------------------------------------------------------------------
144// DecisionId — 128-bit decision identifier
145// ---------------------------------------------------------------------------
146
147/// 128-bit identifier linking a runtime decision to its EvidenceLedger entry.
148///
149/// Structurally identical to [`TraceId`] but semantically distinct.
150#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
151#[serde(transparent)]
152pub struct DecisionId(#[serde(with = "hex_u128")] u128);
153
154impl DecisionId {
155    /// Create from raw 128-bit value.
156    #[must_use]
157    pub const fn from_raw(raw: u128) -> Self {
158        Self(raw)
159    }
160
161    /// Create from millisecond timestamp and random bits.
162    #[must_use]
163    pub const fn from_parts(ts_ms: u64, random: u128) -> Self {
164        let ts_bits = (ts_ms as u128) << 80;
165        let rand_bits = random & 0xFFFF_FFFF_FFFF_FFFF_FFFF;
166        Self(ts_bits | rand_bits)
167    }
168
169    /// Extract the millisecond timestamp.
170    #[must_use]
171    pub const fn timestamp_ms(self) -> u64 {
172        (self.0 >> 80) as u64
173    }
174
175    /// Return the raw 128-bit value.
176    #[must_use]
177    pub const fn as_u128(self) -> u128 {
178        self.0
179    }
180
181    /// Return the bytes in big-endian order.
182    #[must_use]
183    pub const fn to_bytes(self) -> [u8; 16] {
184        self.0.to_be_bytes()
185    }
186
187    /// Construct from big-endian bytes.
188    pub const fn from_bytes(bytes: [u8; 16]) -> Self {
189        Self(u128::from_be_bytes(bytes))
190    }
191}
192
193impl fmt::Debug for DecisionId {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        write!(f, "DecisionId({:032x})", self.0)
196    }
197}
198
199impl fmt::Display for DecisionId {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        write!(f, "{:032x}", self.0)
202    }
203}
204
205impl FromStr for DecisionId {
206    type Err = ParseIdError;
207
208    fn from_str(s: &str) -> Result<Self, Self::Err> {
209        let val = u128::from_str_radix(s, 16).map_err(|_| ParseIdError {
210            kind: "DecisionId",
211            input_len: s.len(),
212        })?;
213        Ok(Self(val))
214    }
215}
216
217// ---------------------------------------------------------------------------
218// PolicyId — identifies a decision policy with version
219// ---------------------------------------------------------------------------
220
221/// Identifies a decision policy (e.g. scheduler, cancellation, budget).
222///
223/// Includes a version number for policy evolution tracking.
224///
225/// ```
226/// use franken_kernel::PolicyId;
227///
228/// let policy = PolicyId::new("scheduler.preempt", 3);
229/// assert_eq!(policy.name(), "scheduler.preempt");
230/// assert_eq!(policy.version(), 3);
231/// ```
232#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
233pub struct PolicyId {
234    /// Dotted policy name (e.g. "scheduler.preempt").
235    #[serde(rename = "n")]
236    name: String,
237    /// Policy version — incremented when the policy logic changes.
238    #[serde(rename = "v")]
239    version: u32,
240}
241
242impl PolicyId {
243    /// Create a new policy identifier.
244    pub fn new(name: impl Into<String>, version: u32) -> Self {
245        Self {
246            name: name.into(),
247            version,
248        }
249    }
250
251    /// Policy name.
252    pub fn name(&self) -> &str {
253        &self.name
254    }
255
256    /// Policy version.
257    pub const fn version(&self) -> u32 {
258        self.version
259    }
260}
261
262impl fmt::Display for PolicyId {
263    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264        write!(f, "{}@v{}", self.name, self.version)
265    }
266}
267
268// ---------------------------------------------------------------------------
269// SchemaVersion — semantic version with compatibility checking
270// ---------------------------------------------------------------------------
271
272/// Semantic version (major.minor.patch) with compatibility checking.
273///
274/// Two versions are compatible iff their major versions match (semver rule).
275///
276/// ```
277/// use franken_kernel::SchemaVersion;
278///
279/// let v1 = SchemaVersion::new(1, 2, 3);
280/// let v1_compat = SchemaVersion::new(1, 5, 0);
281/// let v2 = SchemaVersion::new(2, 0, 0);
282///
283/// assert!(v1.is_compatible(&v1_compat));
284/// assert!(!v1.is_compatible(&v2));
285/// ```
286#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
287pub struct SchemaVersion {
288    /// Major version — breaking changes.
289    pub major: u32,
290    /// Minor version — backwards-compatible additions.
291    pub minor: u32,
292    /// Patch version — backwards-compatible fixes.
293    pub patch: u32,
294}
295
296impl SchemaVersion {
297    /// Create a new schema version.
298    pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
299        Self {
300            major,
301            minor,
302            patch,
303        }
304    }
305
306    /// Returns `true` if `other` is compatible (same major version).
307    pub const fn is_compatible(&self, other: &Self) -> bool {
308        self.major == other.major
309    }
310}
311
312impl fmt::Display for SchemaVersion {
313    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
315    }
316}
317
318impl FromStr for SchemaVersion {
319    type Err = ParseVersionError;
320
321    fn from_str(s: &str) -> Result<Self, Self::Err> {
322        let parts: alloc::vec::Vec<&str> = s.split('.').collect();
323        if parts.len() != 3 {
324            return Err(ParseVersionError);
325        }
326        let major = parts[0].parse().map_err(|_| ParseVersionError)?;
327        let minor = parts[1].parse().map_err(|_| ParseVersionError)?;
328        let patch = parts[2].parse().map_err(|_| ParseVersionError)?;
329        Ok(Self {
330            major,
331            minor,
332            patch,
333        })
334    }
335}
336
337// ---------------------------------------------------------------------------
338// Budget — tropical semiring (min, +)
339// ---------------------------------------------------------------------------
340
341/// Time budget in the tropical semiring (min, +).
342///
343/// Budget decreases additively via [`consume`](Budget::consume) and the
344/// constraint propagates as the minimum of parent and child budgets.
345///
346/// ```
347/// use franken_kernel::Budget;
348///
349/// let b = Budget::new(1000);
350/// let b2 = b.consume(300).unwrap();
351/// assert_eq!(b2.remaining_ms(), 700);
352/// assert!(b2.consume(800).is_none()); // would exceed budget
353/// ```
354#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
355pub struct Budget {
356    remaining_ms: u64,
357}
358
359impl Budget {
360    /// Create a budget with the given milliseconds remaining.
361    pub const fn new(ms: u64) -> Self {
362        Self { remaining_ms: ms }
363    }
364
365    /// Milliseconds remaining.
366    pub const fn remaining_ms(self) -> u64 {
367        self.remaining_ms
368    }
369
370    /// Consume `ms` milliseconds from the budget.
371    ///
372    /// Returns `None` if insufficient budget remains.
373    pub const fn consume(self, ms: u64) -> Option<Self> {
374        if self.remaining_ms >= ms {
375            Some(Self {
376                remaining_ms: self.remaining_ms - ms,
377            })
378        } else {
379            None
380        }
381    }
382
383    /// Whether the budget is fully exhausted.
384    pub const fn is_exhausted(self) -> bool {
385        self.remaining_ms == 0
386    }
387
388    /// Tropical semiring min: returns the tighter (smaller) budget.
389    #[must_use]
390    pub const fn min(self, other: Self) -> Self {
391        if self.remaining_ms <= other.remaining_ms {
392            self
393        } else {
394            other
395        }
396    }
397
398    /// An unlimited budget (max u64 value).
399    pub const UNLIMITED: Self = Self {
400        remaining_ms: u64::MAX,
401    };
402}
403
404// ---------------------------------------------------------------------------
405// CapabilitySet — trait for capability collections
406// ---------------------------------------------------------------------------
407
408/// Trait for capability sets carried by [`Cx`].
409///
410/// Each FrankenSuite project defines its own capability types and
411/// implements this trait. The trait provides introspection for logging
412/// and diagnostics.
413///
414/// Implementations must be `Clone + Send + Sync` to allow context
415/// propagation across async task boundaries.
416pub trait CapabilitySet: Clone + fmt::Debug + Send + Sync {
417    /// Human-readable names of the capabilities in this set.
418    fn capability_names(&self) -> Vec<&str>;
419
420    /// Number of distinct capabilities.
421    fn count(&self) -> usize;
422
423    /// Whether the capability set is empty.
424    fn is_empty(&self) -> bool {
425        self.count() == 0
426    }
427}
428
429/// An empty capability set for contexts that carry no capabilities.
430#[derive(Clone, Debug, Default, PartialEq, Eq)]
431pub struct NoCaps;
432
433impl CapabilitySet for NoCaps {
434    fn capability_names(&self) -> Vec<&str> {
435        Vec::new()
436    }
437
438    fn count(&self) -> usize {
439        0
440    }
441}
442
443// ---------------------------------------------------------------------------
444// Cx — capability context
445// ---------------------------------------------------------------------------
446
447/// Capability context threaded through all FrankenSuite operations.
448///
449/// `Cx` carries:
450/// - A [`TraceId`] for distributed tracing across project boundaries.
451/// - A [`Budget`] in the tropical semiring (min, +) for resource limits.
452/// - A generic [`CapabilitySet`] defining available capabilities.
453/// - Nesting depth for diagnostics.
454///
455/// The lifetime parameter `'a` ensures that child contexts cannot
456/// outlive their parent scope, enforcing structured concurrency
457/// invariants.
458///
459/// # Propagation
460///
461/// Child contexts are created via [`child`](Cx::child), which:
462/// - Inherits the parent's `TraceId`.
463/// - Takes the minimum of parent and child budgets (tropical min).
464/// - Increments the nesting depth.
465pub struct Cx<'a, C: CapabilitySet = NoCaps> {
466    trace_id: TraceId,
467    budget: Budget,
468    capabilities: C,
469    depth: u32,
470    _scope: PhantomData<&'a ()>,
471}
472
473impl<C: CapabilitySet> Cx<'_, C> {
474    /// Create a root context with the given trace, budget, and capabilities.
475    pub fn new(trace_id: TraceId, budget: Budget, capabilities: C) -> Self {
476        Self {
477            trace_id,
478            budget,
479            capabilities,
480            depth: 0,
481            _scope: PhantomData,
482        }
483    }
484
485    /// Create a child context.
486    ///
487    /// The child inherits this context's `TraceId` and takes the minimum
488    /// of this context's budget and the provided `budget`.
489    pub fn child(&self, capabilities: C, budget: Budget) -> Cx<'_, C> {
490        Cx {
491            trace_id: self.trace_id,
492            budget: self.budget.min(budget),
493            capabilities,
494            depth: self.depth + 1,
495            _scope: PhantomData,
496        }
497    }
498
499    /// The trace identifier for this context.
500    pub const fn trace_id(&self) -> TraceId {
501        self.trace_id
502    }
503
504    /// The remaining budget.
505    pub const fn budget(&self) -> Budget {
506        self.budget
507    }
508
509    /// The capability set.
510    pub fn capabilities(&self) -> &C {
511        &self.capabilities
512    }
513
514    /// Nesting depth (0 for root contexts).
515    pub const fn depth(&self) -> u32 {
516        self.depth
517    }
518
519    /// Consume budget from this context in place.
520    ///
521    /// Returns `false` if insufficient budget remains (budget unchanged).
522    pub fn consume_budget(&mut self, ms: u64) -> bool {
523        match self.budget.consume(ms) {
524            Some(new_budget) => {
525                self.budget = new_budget;
526                true
527            }
528            None => false,
529        }
530    }
531}
532
533impl<C: CapabilitySet> fmt::Debug for Cx<'_, C> {
534    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
535        f.debug_struct("Cx")
536            .field("trace_id", &self.trace_id)
537            .field("budget_ms", &self.budget.remaining_ms())
538            .field("capabilities", &self.capabilities)
539            .field("depth", &self.depth)
540            .finish()
541    }
542}
543
544// ---------------------------------------------------------------------------
545// Error types
546// ---------------------------------------------------------------------------
547
548/// Error returned when parsing a hex identifier string fails.
549#[derive(Clone, Debug, PartialEq, Eq)]
550pub struct ParseIdError {
551    /// Which identifier type was being parsed.
552    pub kind: &'static str,
553    /// Length of the input string.
554    pub input_len: usize,
555}
556
557impl fmt::Display for ParseIdError {
558    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
559        write!(
560            f,
561            "invalid {} hex string (length {})",
562            self.kind, self.input_len
563        )
564    }
565}
566
567/// Error returned when parsing a semantic version string fails.
568#[derive(Clone, Debug, PartialEq, Eq)]
569pub struct ParseVersionError;
570
571impl fmt::Display for ParseVersionError {
572    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
573        write!(f, "invalid schema version (expected major.minor.patch)")
574    }
575}
576
577// ---------------------------------------------------------------------------
578// Serde helper: serialize u128 as hex string
579// ---------------------------------------------------------------------------
580
581mod hex_u128 {
582    use alloc::format;
583    use alloc::string::String;
584
585    use serde::{self, Deserialize, Deserializer, Serializer};
586
587    pub fn serialize<S>(value: &u128, serializer: S) -> Result<S::Ok, S::Error>
588    where
589        S: Serializer,
590    {
591        serializer.serialize_str(&format!("{value:032x}"))
592    }
593
594    pub fn deserialize<'de, D>(deserializer: D) -> Result<u128, D::Error>
595    where
596        D: Deserializer<'de>,
597    {
598        let s = String::deserialize(deserializer)?;
599        u128::from_str_radix(&s, 16)
600            .map_err(|_| serde::de::Error::custom(format!("invalid hex u128: {s}")))
601    }
602}
603
604// ---------------------------------------------------------------------------
605// Tests
606// ---------------------------------------------------------------------------
607
608#[cfg(test)]
609mod tests {
610    extern crate std;
611
612    use super::*;
613    use core::hash::{Hash, Hasher};
614    use std::collections::hash_map::DefaultHasher;
615    use std::string::ToString;
616
617    fn hash_of<T: Hash>(val: &T) -> u64 {
618        let mut h = DefaultHasher::new();
619        val.hash(&mut h);
620        h.finish()
621    }
622
623    // -----------------------------------------------------------------------
624    // TraceId tests
625    // -----------------------------------------------------------------------
626
627    #[test]
628    fn trace_id_from_parts_roundtrip() {
629        let ts = 1_700_000_000_000_u64;
630        let random = 0x00AB_CDEF_0123_4567_89AB_u128;
631        let id = TraceId::from_parts(ts, random);
632        assert_eq!(id.timestamp_ms(), ts);
633        assert_eq!(id.as_u128() & 0xFFFF_FFFF_FFFF_FFFF_FFFF, random);
634    }
635
636    #[test]
637    fn trace_id_display_parse_roundtrip() {
638        let id = TraceId::from_raw(0x0123_4567_89AB_CDEF_0123_4567_89AB_CDEF);
639        let hex = id.to_string();
640        assert_eq!(hex, "0123456789abcdef0123456789abcdef");
641        let parsed: TraceId = hex.parse().unwrap();
642        assert_eq!(id, parsed);
643    }
644
645    #[test]
646    fn trace_id_bytes_roundtrip() {
647        let id = TraceId::from_raw(42);
648        let bytes = id.to_bytes();
649        let recovered = TraceId::from_bytes(bytes);
650        assert_eq!(id, recovered);
651    }
652
653    #[test]
654    fn trace_id_ordering() {
655        let earlier = TraceId::from_parts(1000, 0);
656        let later = TraceId::from_parts(2000, 0);
657        assert!(earlier < later);
658    }
659
660    #[test]
661    fn trace_id_uuidv7_monotonic_ordering_10k() {
662        // Generate 10,000 TraceIds with increasing timestamps; verify monotonic order.
663        let ids: std::vec::Vec<TraceId> = (0..10_000)
664            .map(|i| TraceId::from_parts(1_700_000_000_000 + i, 0))
665            .collect();
666        for window in ids.windows(2) {
667            assert!(
668                window[0] < window[1],
669                "TraceId ordering violated: {:?} >= {:?}",
670                window[0],
671                window[1]
672            );
673        }
674    }
675
676    #[test]
677    fn trace_id_display_parse_roundtrip_many() {
678        // Roundtrip 10,000 random-ish TraceIds through Display -> FromStr.
679        for i in 0..10_000_u128 {
680            let raw = i.wrapping_mul(0x0123_4567_89AB_CDEF) ^ (i << 64);
681            let id = TraceId::from_raw(raw);
682            let hex = id.to_string();
683            let parsed: TraceId = hex.parse().unwrap();
684            assert_eq!(id, parsed, "roundtrip failed for raw={raw:#034x}");
685        }
686    }
687
688    #[test]
689    fn trace_id_serde_json() {
690        let id = TraceId::from_raw(0xFF);
691        let json = serde_json::to_string(&id).unwrap();
692        assert_eq!(json, "\"000000000000000000000000000000ff\"");
693        let parsed: TraceId = serde_json::from_str(&json).unwrap();
694        assert_eq!(id, parsed);
695    }
696
697    #[test]
698    fn trace_id_serde_roundtrip_many() {
699        for i in 0..1_000_u128 {
700            let id = TraceId::from_raw(i.wrapping_mul(0xDEAD_BEEF_CAFE_1234));
701            let json = serde_json::to_string(&id).unwrap();
702            let parsed: TraceId = serde_json::from_str(&json).unwrap();
703            assert_eq!(id, parsed);
704        }
705    }
706
707    #[test]
708    fn trace_id_debug_format() {
709        let id = TraceId::from_raw(0xAB);
710        let dbg = std::format!("{id:?}");
711        assert!(dbg.starts_with("TraceId("));
712        assert!(dbg.contains("ab"));
713    }
714
715    #[test]
716    fn trace_id_copy_semantics() {
717        let id = TraceId::from_raw(42);
718        let copy = id;
719        assert_eq!(id, copy); // Both still usable (Copy).
720    }
721
722    #[test]
723    fn trace_id_hash_consistency() {
724        let a = TraceId::from_raw(0xDEAD);
725        let b = TraceId::from_raw(0xDEAD);
726        assert_eq!(a, b);
727        assert_eq!(hash_of(&a), hash_of(&b));
728    }
729
730    #[test]
731    fn trace_id_zero_and_max() {
732        let zero = TraceId::from_raw(0);
733        assert_eq!(zero.timestamp_ms(), 0);
734        assert_eq!(zero.to_string(), "00000000000000000000000000000000");
735        let roundtrip: TraceId = zero.to_string().parse().unwrap();
736        assert_eq!(zero, roundtrip);
737
738        let max = TraceId::from_raw(u128::MAX);
739        assert_eq!(max.to_string(), "ffffffffffffffffffffffffffffffff");
740        let roundtrip: TraceId = max.to_string().parse().unwrap();
741        assert_eq!(max, roundtrip);
742    }
743
744    // -----------------------------------------------------------------------
745    // DecisionId tests
746    // -----------------------------------------------------------------------
747
748    #[test]
749    fn decision_id_from_parts_roundtrip() {
750        let ts = 1_700_000_000_000_u64;
751        let random = 0x0012_3456_789A_BCDE_F012_u128;
752        let id = DecisionId::from_parts(ts, random);
753        assert_eq!(id.timestamp_ms(), ts);
754        assert_eq!(id.as_u128() & 0xFFFF_FFFF_FFFF_FFFF_FFFF, random);
755    }
756
757    #[test]
758    fn decision_id_display_parse_roundtrip() {
759        let id = DecisionId::from_raw(0xDEAD_BEEF);
760        let hex = id.to_string();
761        let parsed: DecisionId = hex.parse().unwrap();
762        assert_eq!(id, parsed);
763    }
764
765    #[test]
766    fn decision_id_display_parse_roundtrip_many() {
767        for i in 0..10_000_u128 {
768            let raw = i.wrapping_mul(0xABCD_EF01_2345_6789) ^ (i << 64);
769            let id = DecisionId::from_raw(raw);
770            let hex = id.to_string();
771            let parsed: DecisionId = hex.parse().unwrap();
772            assert_eq!(id, parsed, "roundtrip failed for raw={raw:#034x}");
773        }
774    }
775
776    #[test]
777    fn decision_id_ordering() {
778        let earlier = DecisionId::from_parts(1000, 0);
779        let later = DecisionId::from_parts(2000, 0);
780        assert!(earlier < later);
781    }
782
783    #[test]
784    fn decision_id_monotonic_ordering_10k() {
785        let ids: std::vec::Vec<DecisionId> = (0..10_000)
786            .map(|i| DecisionId::from_parts(1_700_000_000_000 + i, 0))
787            .collect();
788        for window in ids.windows(2) {
789            assert!(window[0] < window[1]);
790        }
791    }
792
793    #[test]
794    fn decision_id_serde_json() {
795        let id = DecisionId::from_raw(1);
796        let json = serde_json::to_string(&id).unwrap();
797        let parsed: DecisionId = serde_json::from_str(&json).unwrap();
798        assert_eq!(id, parsed);
799    }
800
801    #[test]
802    fn decision_id_debug_format() {
803        let id = DecisionId::from_raw(0xCD);
804        let dbg = std::format!("{id:?}");
805        assert!(dbg.starts_with("DecisionId("));
806        assert!(dbg.contains("cd"));
807    }
808
809    #[test]
810    fn decision_id_copy_semantics() {
811        let id = DecisionId::from_raw(99);
812        let copy = id;
813        assert_eq!(id, copy);
814    }
815
816    #[test]
817    fn decision_id_hash_consistency() {
818        let a = DecisionId::from_raw(0xBEEF);
819        let b = DecisionId::from_raw(0xBEEF);
820        assert_eq!(a, b);
821        assert_eq!(hash_of(&a), hash_of(&b));
822    }
823
824    #[test]
825    fn decision_id_bytes_roundtrip() {
826        let id = DecisionId::from_raw(0x1234_5678_9ABC_DEF0);
827        let bytes = id.to_bytes();
828        let recovered = DecisionId::from_bytes(bytes);
829        assert_eq!(id, recovered);
830    }
831
832    // -----------------------------------------------------------------------
833    // PolicyId tests
834    // -----------------------------------------------------------------------
835
836    #[test]
837    fn policy_id_display() {
838        let policy = PolicyId::new("scheduler.preempt", 3);
839        assert_eq!(policy.to_string(), "scheduler.preempt@v3");
840        assert_eq!(policy.name(), "scheduler.preempt");
841        assert_eq!(policy.version(), 3);
842    }
843
844    #[test]
845    fn policy_id_serde_json() {
846        let policy = PolicyId::new("cancel.budget", 1);
847        let json = serde_json::to_string(&policy).unwrap();
848        assert!(json.contains("\"n\":"));
849        assert!(json.contains("\"v\":"));
850        let parsed: PolicyId = serde_json::from_str(&json).unwrap();
851        assert_eq!(policy, parsed);
852    }
853
854    #[test]
855    fn policy_id_ordering() {
856        let a = PolicyId::new("a.policy", 1);
857        let b = PolicyId::new("b.policy", 1);
858        assert!(a < b, "PolicyId should order lexicographically by name");
859        let v1 = PolicyId::new("same", 1);
860        let v2 = PolicyId::new("same", 2);
861        assert!(v1 < v2, "same name, should order by version");
862    }
863
864    #[test]
865    fn policy_id_hash_consistency() {
866        let a = PolicyId::new("test.policy", 5);
867        let b = PolicyId::new("test.policy", 5);
868        assert_eq!(a, b);
869        assert_eq!(hash_of(&a), hash_of(&b));
870    }
871
872    // -----------------------------------------------------------------------
873    // SchemaVersion tests
874    // -----------------------------------------------------------------------
875
876    #[test]
877    fn schema_version_compatible() {
878        let v1_2_3 = SchemaVersion::new(1, 2, 3);
879        let v1_5_0 = SchemaVersion::new(1, 5, 0);
880        let v2_0_0 = SchemaVersion::new(2, 0, 0);
881        assert!(v1_2_3.is_compatible(&v1_5_0));
882        assert!(!v1_2_3.is_compatible(&v2_0_0));
883    }
884
885    #[test]
886    fn schema_version_0x_edge_cases() {
887        // 0.x versions: 0.1 and 0.2 both have major=0, so they ARE compatible
888        // under our semver rule (same major).
889        let v0_1 = SchemaVersion::new(0, 1, 0);
890        let v0_2 = SchemaVersion::new(0, 2, 0);
891        assert!(
892            v0_1.is_compatible(&v0_2),
893            "0.x versions should be compatible (same major=0)"
894        );
895
896        // 0.x vs 1.x should NOT be compatible.
897        let v1_0 = SchemaVersion::new(1, 0, 0);
898        assert!(!v0_1.is_compatible(&v1_0));
899    }
900
901    #[test]
902    fn schema_version_display_parse_roundtrip() {
903        let v = SchemaVersion::new(1, 2, 3);
904        assert_eq!(v.to_string(), "1.2.3");
905        let parsed: SchemaVersion = "1.2.3".parse().unwrap();
906        assert_eq!(v, parsed);
907    }
908
909    #[test]
910    fn schema_version_ordering_comprehensive() {
911        let versions = [
912            SchemaVersion::new(1, 0, 0),
913            SchemaVersion::new(1, 0, 1),
914            SchemaVersion::new(1, 1, 0),
915            SchemaVersion::new(2, 0, 0),
916            SchemaVersion::new(2, 1, 0),
917            SchemaVersion::new(10, 0, 0),
918        ];
919        for window in versions.windows(2) {
920            assert!(
921                window[0] < window[1],
922                "{} should be < {}",
923                window[0],
924                window[1]
925            );
926        }
927    }
928
929    #[test]
930    fn schema_version_ordering() {
931        let v1 = SchemaVersion::new(1, 0, 0);
932        let v2 = SchemaVersion::new(2, 0, 0);
933        assert!(v1 < v2);
934    }
935
936    #[test]
937    fn schema_version_serde_json() {
938        let v = SchemaVersion::new(3, 1, 4);
939        let json = serde_json::to_string(&v).unwrap();
940        let parsed: SchemaVersion = serde_json::from_str(&json).unwrap();
941        assert_eq!(v, parsed);
942    }
943
944    #[test]
945    fn schema_version_copy_semantics() {
946        let v = SchemaVersion::new(1, 0, 0);
947        let copy = v;
948        assert_eq!(v, copy);
949    }
950
951    #[test]
952    fn schema_version_hash_consistency() {
953        let a = SchemaVersion::new(1, 2, 3);
954        let b = SchemaVersion::new(1, 2, 3);
955        assert_eq!(a, b);
956        assert_eq!(hash_of(&a), hash_of(&b));
957    }
958
959    #[test]
960    fn schema_version_self_compatible() {
961        let v = SchemaVersion::new(5, 3, 1);
962        assert!(
963            v.is_compatible(&v),
964            "version must be compatible with itself"
965        );
966    }
967
968    // -----------------------------------------------------------------------
969    // Error type tests
970    // -----------------------------------------------------------------------
971
972    #[test]
973    fn parse_id_error_display() {
974        let err = ParseIdError {
975            kind: "TraceId",
976            input_len: 5,
977        };
978        let msg = err.to_string();
979        assert!(msg.contains("TraceId"));
980        assert!(msg.contains('5'));
981    }
982
983    #[test]
984    fn parse_version_error_display() {
985        let err = ParseVersionError;
986        let msg = err.to_string();
987        assert!(msg.contains("major.minor.patch"));
988    }
989
990    #[test]
991    fn invalid_hex_parse_fails() {
992        assert!("not-hex".parse::<TraceId>().is_err());
993        assert!("not-hex".parse::<DecisionId>().is_err());
994    }
995
996    #[test]
997    fn invalid_version_parse_fails() {
998        assert!("1.2".parse::<SchemaVersion>().is_err());
999        assert!("a.b.c".parse::<SchemaVersion>().is_err());
1000        assert!("1.2.3.4".parse::<SchemaVersion>().is_err());
1001        assert!("".parse::<SchemaVersion>().is_err());
1002    }
1003
1004    // -----------------------------------------------------------------------
1005    // Send + Sync static assertions
1006    // -----------------------------------------------------------------------
1007
1008    #[test]
1009    fn all_types_send_sync() {
1010        fn assert_send_sync<T: Send + Sync>() {}
1011        assert_send_sync::<TraceId>();
1012        assert_send_sync::<DecisionId>();
1013        assert_send_sync::<PolicyId>();
1014        assert_send_sync::<SchemaVersion>();
1015        assert_send_sync::<Budget>();
1016        assert_send_sync::<NoCaps>();
1017        // Cx requires C: CapabilitySet which requires Send + Sync.
1018        assert_send_sync::<Cx<'_, NoCaps>>();
1019    }
1020
1021    // -----------------------------------------------------------------------
1022    // Budget tests
1023    // -----------------------------------------------------------------------
1024
1025    #[test]
1026    fn budget_new_and_remaining() {
1027        let b = Budget::new(5000);
1028        assert_eq!(b.remaining_ms(), 5000);
1029        assert!(!b.is_exhausted());
1030    }
1031
1032    #[test]
1033    fn budget_consume() {
1034        let b = Budget::new(1000);
1035        let b2 = b.consume(300).unwrap();
1036        assert_eq!(b2.remaining_ms(), 700);
1037        let b3 = b2.consume(700).unwrap();
1038        assert_eq!(b3.remaining_ms(), 0);
1039        assert!(b3.is_exhausted());
1040    }
1041
1042    #[test]
1043    fn budget_consume_insufficient() {
1044        let b = Budget::new(100);
1045        assert!(b.consume(200).is_none());
1046    }
1047
1048    #[test]
1049    fn budget_consume_exact() {
1050        let b = Budget::new(100);
1051        let b2 = b.consume(100).unwrap();
1052        assert!(b2.is_exhausted());
1053    }
1054
1055    #[test]
1056    fn budget_consume_zero() {
1057        let b = Budget::new(100);
1058        let b2 = b.consume(0).unwrap();
1059        assert_eq!(b2.remaining_ms(), 100);
1060    }
1061
1062    #[test]
1063    fn budget_min() {
1064        let b1 = Budget::new(500);
1065        let b2 = Budget::new(300);
1066        assert_eq!(b1.min(b2).remaining_ms(), 300);
1067        assert_eq!(b2.min(b1).remaining_ms(), 300);
1068    }
1069
1070    #[test]
1071    fn budget_min_equal() {
1072        let b = Budget::new(100);
1073        assert_eq!(b.min(b).remaining_ms(), 100);
1074    }
1075
1076    #[test]
1077    fn budget_unlimited() {
1078        let b = Budget::UNLIMITED;
1079        assert_eq!(b.remaining_ms(), u64::MAX);
1080        assert!(!b.is_exhausted());
1081    }
1082
1083    #[test]
1084    fn budget_unlimited_min_with_finite() {
1085        let finite = Budget::new(1000);
1086        assert_eq!(Budget::UNLIMITED.min(finite).remaining_ms(), 1000);
1087        assert_eq!(finite.min(Budget::UNLIMITED).remaining_ms(), 1000);
1088    }
1089
1090    #[test]
1091    fn budget_serde_json() {
1092        let b = Budget::new(42);
1093        let json = serde_json::to_string(&b).unwrap();
1094        let parsed: Budget = serde_json::from_str(&json).unwrap();
1095        assert_eq!(b, parsed);
1096    }
1097
1098    #[test]
1099    fn budget_copy_semantics() {
1100        let b = Budget::new(100);
1101        let copy = b;
1102        assert_eq!(b, copy);
1103    }
1104
1105    #[test]
1106    fn budget_tropical_identity() {
1107        // Identity element of min is UNLIMITED (u64::MAX).
1108        let b = Budget::new(42);
1109        assert_eq!(b.min(Budget::UNLIMITED), b);
1110        assert_eq!(Budget::UNLIMITED.min(b), b);
1111    }
1112
1113    #[test]
1114    fn budget_tropical_commutativity() {
1115        let a = Budget::new(100);
1116        let b = Budget::new(200);
1117        assert_eq!(a.min(b), b.min(a));
1118    }
1119
1120    #[test]
1121    fn budget_tropical_associativity() {
1122        let a = Budget::new(100);
1123        let b = Budget::new(200);
1124        let c = Budget::new(50);
1125        assert_eq!(a.min(b).min(c), a.min(b.min(c)));
1126    }
1127
1128    // -----------------------------------------------------------------------
1129    // NoCaps tests
1130    // -----------------------------------------------------------------------
1131
1132    #[test]
1133    fn no_caps_empty() {
1134        let caps = NoCaps;
1135        assert_eq!(caps.count(), 0);
1136        assert!(caps.is_empty());
1137        assert!(caps.capability_names().is_empty());
1138    }
1139
1140    #[test]
1141    fn no_caps_clone() {
1142        let a = NoCaps;
1143        let b = a.clone();
1144        assert_eq!(a, b);
1145    }
1146
1147    // -----------------------------------------------------------------------
1148    // Custom CapabilitySet for testing
1149    // -----------------------------------------------------------------------
1150
1151    #[derive(Clone, Debug)]
1152    struct TestCaps {
1153        can_read: bool,
1154        can_write: bool,
1155    }
1156
1157    impl CapabilitySet for TestCaps {
1158        fn capability_names(&self) -> alloc::vec::Vec<&str> {
1159            let mut names = alloc::vec::Vec::new();
1160            if self.can_read {
1161                names.push("read");
1162            }
1163            if self.can_write {
1164                names.push("write");
1165            }
1166            names
1167        }
1168
1169        fn count(&self) -> usize {
1170            usize::from(self.can_read) + usize::from(self.can_write)
1171        }
1172    }
1173
1174    /// Layered capability set for testing attenuation chains.
1175    #[derive(Clone, Debug)]
1176    struct LayeredCaps {
1177        level: u32,
1178    }
1179
1180    impl CapabilitySet for LayeredCaps {
1181        fn capability_names(&self) -> alloc::vec::Vec<&str> {
1182            if self.level > 0 {
1183                alloc::vec!["layer"]
1184            } else {
1185                alloc::vec::Vec::new()
1186            }
1187        }
1188
1189        fn count(&self) -> usize {
1190            usize::from(self.level > 0)
1191        }
1192    }
1193
1194    // -----------------------------------------------------------------------
1195    // Cx tests
1196    // -----------------------------------------------------------------------
1197
1198    #[test]
1199    fn cx_root_creation() {
1200        let trace = TraceId::from_parts(1_700_000_000_000, 1);
1201        let cx = Cx::new(trace, Budget::new(5000), NoCaps);
1202        assert_eq!(cx.trace_id(), trace);
1203        assert_eq!(cx.budget().remaining_ms(), 5000);
1204        assert_eq!(cx.depth(), 0);
1205        assert!(cx.capabilities().is_empty());
1206    }
1207
1208    #[test]
1209    fn cx_child_inherits_trace() {
1210        let trace = TraceId::from_parts(1_700_000_000_000, 42);
1211        let cx = Cx::new(trace, Budget::new(5000), NoCaps);
1212        let child = cx.child(NoCaps, Budget::new(3000));
1213        assert_eq!(child.trace_id(), trace);
1214    }
1215
1216    #[test]
1217    fn cx_child_budget_takes_min() {
1218        let cx = Cx::new(TraceId::from_raw(1), Budget::new(2000), NoCaps);
1219        let child1 = cx.child(NoCaps, Budget::new(1000));
1220        assert_eq!(child1.budget().remaining_ms(), 1000);
1221        let child2 = cx.child(NoCaps, Budget::new(5000));
1222        assert_eq!(child2.budget().remaining_ms(), 2000);
1223    }
1224
1225    #[test]
1226    fn cx_child_increments_depth() {
1227        let cx = Cx::new(TraceId::from_raw(1), Budget::new(1000), NoCaps);
1228        let child = cx.child(NoCaps, Budget::new(1000));
1229        assert_eq!(child.depth(), 1);
1230        let grandchild = child.child(NoCaps, Budget::new(1000));
1231        assert_eq!(grandchild.depth(), 2);
1232    }
1233
1234    #[test]
1235    fn cx_consume_budget() {
1236        let mut cx = Cx::new(TraceId::from_raw(1), Budget::new(500), NoCaps);
1237        assert!(cx.consume_budget(200));
1238        assert_eq!(cx.budget().remaining_ms(), 300);
1239        assert!(!cx.consume_budget(400));
1240        assert_eq!(cx.budget().remaining_ms(), 300);
1241    }
1242
1243    #[test]
1244    fn cx_debug_format() {
1245        let cx = Cx::new(TraceId::from_raw(0xAB), Budget::new(100), NoCaps);
1246        let dbg = std::format!("{cx:?}");
1247        assert!(dbg.contains("Cx"));
1248        assert!(dbg.contains("budget_ms"));
1249        assert!(dbg.contains("100"));
1250    }
1251
1252    #[test]
1253    fn cx_with_custom_capabilities() {
1254        let caps = TestCaps {
1255            can_read: true,
1256            can_write: false,
1257        };
1258        let cx = Cx::new(TraceId::from_raw(1), Budget::new(1000), caps);
1259        assert_eq!(cx.capabilities().count(), 1);
1260        assert_eq!(cx.capabilities().capability_names(), &["read"]);
1261    }
1262
1263    #[test]
1264    fn cx_child_with_attenuated_capabilities() {
1265        let full_caps = TestCaps {
1266            can_read: true,
1267            can_write: true,
1268        };
1269        let cx = Cx::new(TraceId::from_raw(1), Budget::new(1000), full_caps);
1270        assert_eq!(cx.capabilities().count(), 2);
1271
1272        let read_only = TestCaps {
1273            can_read: true,
1274            can_write: false,
1275        };
1276        let child = cx.child(read_only, Budget::new(500));
1277        assert_eq!(child.capabilities().count(), 1);
1278        assert!(!child.capabilities().capability_names().contains(&"write"));
1279    }
1280
1281    #[test]
1282    fn cx_capability_attenuation_chain_10x() {
1283        // Create a chain of 10 nested contexts, each with decreasing level.
1284        let trace = TraceId::from_raw(0x42);
1285        let root = Cx::new(trace, Budget::new(10_000), LayeredCaps { level: 10 });
1286        assert_eq!(root.capabilities().level, 10);
1287
1288        let mut prev_level = 10_u32;
1289        let child1 = root.child(LayeredCaps { level: 9 }, Budget::new(9000));
1290        assert!(child1.capabilities().level < prev_level);
1291        prev_level = child1.capabilities().level;
1292
1293        let child2 = child1.child(LayeredCaps { level: 8 }, Budget::new(8000));
1294        assert!(child2.capabilities().level < prev_level);
1295        prev_level = child2.capabilities().level;
1296
1297        let child3 = child2.child(LayeredCaps { level: 7 }, Budget::new(7000));
1298        assert!(child3.capabilities().level < prev_level);
1299        prev_level = child3.capabilities().level;
1300
1301        let child4 = child3.child(LayeredCaps { level: 6 }, Budget::new(6000));
1302        assert!(child4.capabilities().level < prev_level);
1303        prev_level = child4.capabilities().level;
1304
1305        let child5 = child4.child(LayeredCaps { level: 5 }, Budget::new(5000));
1306        assert!(child5.capabilities().level < prev_level);
1307        prev_level = child5.capabilities().level;
1308
1309        let child6 = child5.child(LayeredCaps { level: 4 }, Budget::new(4000));
1310        assert!(child6.capabilities().level < prev_level);
1311        prev_level = child6.capabilities().level;
1312
1313        let child7 = child6.child(LayeredCaps { level: 3 }, Budget::new(3000));
1314        assert!(child7.capabilities().level < prev_level);
1315        prev_level = child7.capabilities().level;
1316
1317        let child8 = child7.child(LayeredCaps { level: 2 }, Budget::new(2000));
1318        assert!(child8.capabilities().level < prev_level);
1319        prev_level = child8.capabilities().level;
1320
1321        let child9 = child8.child(LayeredCaps { level: 1 }, Budget::new(1000));
1322        assert!(child9.capabilities().level < prev_level);
1323        prev_level = child9.capabilities().level;
1324
1325        let child10 = child9.child(LayeredCaps { level: 0 }, Budget::new(500));
1326        assert!(child10.capabilities().level < prev_level);
1327        assert_eq!(child10.capabilities().level, 0);
1328        assert!(child10.capabilities().is_empty());
1329        assert_eq!(child10.depth(), 10);
1330
1331        // Trace propagated through all 10 levels.
1332        assert_eq!(child10.trace_id(), trace);
1333        // Budget capped by minimum in chain: 500 ms.
1334        assert_eq!(child10.budget().remaining_ms(), 500);
1335    }
1336
1337    #[test]
1338    fn cx_deep_nesting_budget_monotonic() {
1339        // Budget can only decrease or stay the same through nesting.
1340        let cx = Cx::new(TraceId::from_raw(1), Budget::new(1000), NoCaps);
1341        let c1 = cx.child(NoCaps, Budget::new(900));
1342        let c2 = c1.child(NoCaps, Budget::new(800));
1343        let c3 = c2.child(NoCaps, Budget::new(700));
1344        let c4 = c3.child(NoCaps, Budget::new(600));
1345
1346        assert!(c1.budget().remaining_ms() <= cx.budget().remaining_ms());
1347        assert!(c2.budget().remaining_ms() <= c1.budget().remaining_ms());
1348        assert!(c3.budget().remaining_ms() <= c2.budget().remaining_ms());
1349        assert!(c4.budget().remaining_ms() <= c3.budget().remaining_ms());
1350    }
1351
1352    #[test]
1353    fn cx_child_cannot_exceed_parent_budget() {
1354        let cx = Cx::new(TraceId::from_raw(1), Budget::new(100), NoCaps);
1355        // Child requests much more — capped at parent's 100.
1356        let child = cx.child(NoCaps, Budget::UNLIMITED);
1357        assert_eq!(child.budget().remaining_ms(), 100);
1358    }
1359
1360    #[test]
1361    fn cx_trace_propagation_through_chain() {
1362        let trace = TraceId::from_parts(1_700_000_000_000, 0xCAFE);
1363        let cx = Cx::new(trace, Budget::UNLIMITED, NoCaps);
1364        let c1 = cx.child(NoCaps, Budget::UNLIMITED);
1365        let c2 = c1.child(NoCaps, Budget::UNLIMITED);
1366        let c3 = c2.child(NoCaps, Budget::UNLIMITED);
1367        assert_eq!(c3.trace_id(), trace);
1368        assert_eq!(c3.depth(), 3);
1369    }
1370}
1371
1372// ---------------------------------------------------------------------------
1373// Property-based tests (proptest)
1374// ---------------------------------------------------------------------------
1375
1376#[cfg(test)]
1377mod proptest_tests {
1378    extern crate std;
1379
1380    use super::*;
1381    use core::hash::{Hash, Hasher};
1382    use proptest::prelude::*;
1383    use std::collections::hash_map::DefaultHasher;
1384    use std::string::ToString;
1385
1386    fn hash_of<T: Hash>(val: &T) -> u64 {
1387        let mut h = DefaultHasher::new();
1388        val.hash(&mut h);
1389        h.finish()
1390    }
1391
1392    // -- TraceId properties --
1393
1394    proptest! {
1395        #[test]
1396        fn trace_id_display_fromstr_roundtrip(raw: u128) {
1397            let id = TraceId::from_raw(raw);
1398            let hex = id.to_string();
1399            let parsed: TraceId = hex.parse().unwrap();
1400            prop_assert_eq!(id, parsed);
1401        }
1402
1403        #[test]
1404        fn trace_id_serde_roundtrip(raw: u128) {
1405            let id = TraceId::from_raw(raw);
1406            let json = serde_json::to_string(&id).unwrap();
1407            let parsed: TraceId = serde_json::from_str(&json).unwrap();
1408            prop_assert_eq!(id, parsed);
1409        }
1410
1411        #[test]
1412        fn trace_id_bytes_roundtrip(raw: u128) {
1413            let id = TraceId::from_raw(raw);
1414            let bytes = id.to_bytes();
1415            let recovered = TraceId::from_bytes(bytes);
1416            prop_assert_eq!(id, recovered);
1417        }
1418
1419        #[test]
1420        fn trace_id_hash_consistency(a: u128, b: u128) {
1421            let id_a = TraceId::from_raw(a);
1422            let id_b = TraceId::from_raw(b);
1423            if id_a == id_b {
1424                prop_assert_eq!(hash_of(&id_a), hash_of(&id_b));
1425            }
1426        }
1427
1428        #[test]
1429        fn trace_id_from_parts_preserves_timestamp(ts_ms: u64, random: u128) {
1430            // Only 48 bits of timestamp are stored.
1431            let ts_masked = ts_ms & 0xFFFF_FFFF_FFFF;
1432            let id = TraceId::from_parts(ts_masked, random);
1433            prop_assert_eq!(id.timestamp_ms(), ts_masked);
1434        }
1435    }
1436
1437    // -- DecisionId properties --
1438
1439    proptest! {
1440        #[test]
1441        fn decision_id_display_fromstr_roundtrip(raw: u128) {
1442            let id = DecisionId::from_raw(raw);
1443            let hex = id.to_string();
1444            let parsed: DecisionId = hex.parse().unwrap();
1445            prop_assert_eq!(id, parsed);
1446        }
1447
1448        #[test]
1449        fn decision_id_serde_roundtrip(raw: u128) {
1450            let id = DecisionId::from_raw(raw);
1451            let json = serde_json::to_string(&id).unwrap();
1452            let parsed: DecisionId = serde_json::from_str(&json).unwrap();
1453            prop_assert_eq!(id, parsed);
1454        }
1455
1456        #[test]
1457        fn decision_id_hash_consistency(a: u128, b: u128) {
1458            let id_a = DecisionId::from_raw(a);
1459            let id_b = DecisionId::from_raw(b);
1460            if id_a == id_b {
1461                prop_assert_eq!(hash_of(&id_a), hash_of(&id_b));
1462            }
1463        }
1464    }
1465
1466    // -- SchemaVersion properties --
1467
1468    proptest! {
1469        #[test]
1470        fn schema_version_parse_roundtrip(major: u32, minor: u32, patch: u32) {
1471            let v = SchemaVersion::new(major, minor, patch);
1472            let s = v.to_string();
1473            let parsed: SchemaVersion = s.parse().unwrap();
1474            prop_assert_eq!(v, parsed);
1475        }
1476
1477        #[test]
1478        fn schema_version_serde_roundtrip(major: u32, minor: u32, patch: u32) {
1479            let v = SchemaVersion::new(major, minor, patch);
1480            let json = serde_json::to_string(&v).unwrap();
1481            let parsed: SchemaVersion = serde_json::from_str(&json).unwrap();
1482            prop_assert_eq!(v, parsed);
1483        }
1484
1485        #[test]
1486        fn schema_version_compatible_reflexive(major: u32, minor: u32, patch: u32) {
1487            let v = SchemaVersion::new(major, minor, patch);
1488            prop_assert!(v.is_compatible(&v));
1489        }
1490
1491        #[test]
1492        fn schema_version_compatible_symmetric(
1493            m1: u32, n1: u32, p1: u32,
1494            m2: u32, n2: u32, p2: u32
1495        ) {
1496            let a = SchemaVersion::new(m1, n1, p1);
1497            let b = SchemaVersion::new(m2, n2, p2);
1498            prop_assert_eq!(a.is_compatible(&b), b.is_compatible(&a));
1499        }
1500
1501        #[test]
1502        fn schema_version_compatible_transitive(
1503            m1: u32, n1: u32, p1: u32,
1504            n2: u32, p2: u32,
1505            n3: u32, p3: u32
1506        ) {
1507            // If a and b share the same major, and b and c share the same major,
1508            // then a and c must share the same major.
1509            let a = SchemaVersion::new(m1, n1, p1);
1510            let b = SchemaVersion::new(m1, n2, p2);
1511            let c = SchemaVersion::new(m1, n3, p3);
1512            if a.is_compatible(&b) && b.is_compatible(&c) {
1513                prop_assert!(a.is_compatible(&c));
1514            }
1515        }
1516
1517        #[test]
1518        fn schema_version_hash_consistency(
1519            m1: u32, n1: u32, p1: u32,
1520            m2: u32, n2: u32, p2: u32
1521        ) {
1522            let a = SchemaVersion::new(m1, n1, p1);
1523            let b = SchemaVersion::new(m2, n2, p2);
1524            if a == b {
1525                prop_assert_eq!(hash_of(&a), hash_of(&b));
1526            }
1527        }
1528    }
1529
1530    // -- Budget tropical semiring properties --
1531
1532    proptest! {
1533        #[test]
1534        fn budget_min_commutative(a: u64, b: u64) {
1535            let ba = Budget::new(a);
1536            let bb = Budget::new(b);
1537            prop_assert_eq!(ba.min(bb), bb.min(ba));
1538        }
1539
1540        #[test]
1541        fn budget_min_associative(a: u64, b: u64, c: u64) {
1542            let ba = Budget::new(a);
1543            let bb = Budget::new(b);
1544            let bc = Budget::new(c);
1545            prop_assert_eq!(ba.min(bb).min(bc), ba.min(bb.min(bc)));
1546        }
1547
1548        #[test]
1549        fn budget_min_identity(a: u64) {
1550            // UNLIMITED is the identity element for min.
1551            let ba = Budget::new(a);
1552            prop_assert_eq!(ba.min(Budget::UNLIMITED), ba);
1553            prop_assert_eq!(Budget::UNLIMITED.min(ba), ba);
1554        }
1555
1556        #[test]
1557        fn budget_min_idempotent(a: u64) {
1558            let ba = Budget::new(a);
1559            prop_assert_eq!(ba.min(ba), ba);
1560        }
1561
1562        #[test]
1563        fn budget_consume_additive(total in 0..=10_000_u64, a in 0..=5_000_u64, b in 0..=5_000_u64) {
1564            // If we can consume a+b, consuming a then b should give the same result.
1565            let budget = Budget::new(total);
1566            if a + b <= total {
1567                let after_both = budget.consume(a + b).unwrap();
1568                let after_a = budget.consume(a).unwrap();
1569                let after_ab = after_a.consume(b).unwrap();
1570                prop_assert_eq!(after_both.remaining_ms(), after_ab.remaining_ms());
1571            }
1572        }
1573
1574        #[test]
1575        fn budget_serde_roundtrip(ms: u64) {
1576            let b = Budget::new(ms);
1577            let json = serde_json::to_string(&b).unwrap();
1578            let parsed: Budget = serde_json::from_str(&json).unwrap();
1579            prop_assert_eq!(b, parsed);
1580        }
1581
1582        #[test]
1583        fn budget_hash_consistency(a: u64, b: u64) {
1584            let ba = Budget::new(a);
1585            let bb = Budget::new(b);
1586            if ba == bb {
1587                prop_assert_eq!(hash_of(&ba), hash_of(&bb));
1588            }
1589        }
1590    }
1591
1592    // -- Cx property tests --
1593
1594    proptest! {
1595        #[test]
1596        fn cx_child_budget_never_exceeds_parent(parent_ms: u64, child_ms: u64) {
1597            let trace = TraceId::from_raw(1);
1598            let cx = Cx::new(trace, Budget::new(parent_ms), NoCaps);
1599            let child = cx.child(NoCaps, Budget::new(child_ms));
1600            prop_assert!(child.budget().remaining_ms() <= cx.budget().remaining_ms());
1601        }
1602
1603        #[test]
1604        fn cx_child_trace_always_inherited(raw: u128, budget_ms: u64) {
1605            let trace = TraceId::from_raw(raw);
1606            let cx = Cx::new(trace, Budget::new(budget_ms), NoCaps);
1607            let child = cx.child(NoCaps, Budget::new(budget_ms));
1608            prop_assert_eq!(child.trace_id(), trace);
1609        }
1610
1611        #[test]
1612        fn cx_child_depth_increments(raw: u128, budget_ms: u64) {
1613            let cx = Cx::new(TraceId::from_raw(raw), Budget::new(budget_ms), NoCaps);
1614            let child = cx.child(NoCaps, Budget::new(budget_ms));
1615            prop_assert_eq!(child.depth(), cx.depth() + 1);
1616        }
1617    }
1618}