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