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