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