Skip to main content

kimberlite_types/
lib.rs

1//! # kmb-types: Core types for `Kimberlite`
2//!
3//! This crate contains shared types used across the `Kimberlite` system:
4//! - Entity IDs ([`TenantId`], [`StreamId`], [`Offset`], [`GroupId`])
5//! - Cryptographic types ([`struct@Hash`])
6//! - Temporal types ([`Timestamp`])
7//! - Log record types ([`RecordKind`], [`RecordHeader`], [`Checkpoint`], [`CheckpointPolicy`])
8//! - Projection tracking ([`AppliedIndex`])
9//! - Idempotency ([`IdempotencyId`])
10//! - Recovery tracking ([`Generation`], [`RecoveryRecord`], [`RecoveryReason`])
11//! - Data classification ([`DataClass`])
12//! - Placement rules ([`Placement`], [`Region`])
13//! - Stream metadata ([`StreamMetadata`])
14//! - Audit actions ([`AuditAction`])
15//! - Event persistence ([`EventPersister`], [`PersistError`])
16//!
17//! This crate opts in to strict PRESSURECRAFT clippy lints. Test-only
18//! `unwrap()` / `panic!` are allowed via `cfg_attr(test, ...)`.
19
20#![warn(
21    clippy::unwrap_used,
22    clippy::panic,
23    clippy::todo,
24    clippy::unimplemented,
25    clippy::too_many_lines
26)]
27#![cfg_attr(
28    test,
29    allow(
30        clippy::unwrap_used,
31        clippy::panic,
32        clippy::todo,
33        clippy::unimplemented,
34        clippy::too_many_lines
35    )
36)]
37
38use std::{
39    fmt::{Debug, Display},
40    ops::{Add, AddAssign, Range, Sub},
41    time::{SystemTime, UNIX_EPOCH},
42};
43
44use bytes::Bytes;
45use serde::{Deserialize, Serialize};
46
47// ============================================================================
48// Entity IDs - All Copy (cheap 8-byte values)
49// ============================================================================
50
51/// Unique identifier for a tenant (organization/customer).
52#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
53pub struct TenantId(u64);
54
55impl TenantId {
56    pub fn new(id: u64) -> Self {
57        Self(id)
58    }
59
60    /// Extracts tenant ID from stream ID (upper 32 bits).
61    ///
62    /// **Bit Layout**:
63    /// - Upper 32 bits: `tenant_id` (supports 4.3B tenants)
64    /// - Lower 32 bits: `local_stream_id` (4.3B streams per tenant)
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// # use kimberlite_types::{TenantId, StreamId};
70    /// let stream_id = StreamId::from_tenant_and_local(TenantId::from(5), 1);
71    /// assert_eq!(TenantId::from_stream_id(stream_id), TenantId::from(5));
72    /// ```
73    pub fn from_stream_id(stream_id: StreamId) -> Self {
74        TenantId::from(u64::from(stream_id) >> 32)
75    }
76}
77
78impl Display for TenantId {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        write!(f, "{}", self.0)
81    }
82}
83
84impl From<u64> for TenantId {
85    fn from(value: u64) -> Self {
86        Self(value)
87    }
88}
89
90impl From<TenantId> for u64 {
91    fn from(id: TenantId) -> Self {
92        id.0
93    }
94}
95
96/// Unique identifier for a stream within the system.
97#[derive(
98    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
99)]
100pub struct StreamId(u64);
101
102impl StreamId {
103    pub fn new(id: u64) -> Self {
104        Self(id)
105    }
106
107    /// Creates stream ID from tenant ID and local stream number.
108    ///
109    /// **Bit Layout**:
110    /// - Upper 32 bits: `tenant_id` (supports 4.3B tenants)
111    /// - Lower 32 bits: `local_stream_id` (4.3B streams per tenant)
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// # use kimberlite_types::{TenantId, StreamId};
117    /// let stream_id = StreamId::from_tenant_and_local(TenantId::from(5), 1);
118    /// assert_eq!(u64::from(stream_id), 21474836481); // (5 << 32) | 1
119    /// assert_eq!(TenantId::from_stream_id(stream_id), TenantId::from(5));
120    /// ```
121    pub fn from_tenant_and_local(tenant_id: TenantId, local_id: u32) -> Self {
122        let tenant_bits = u64::from(tenant_id) << 32;
123        let local_bits = u64::from(local_id);
124        StreamId::from(tenant_bits | local_bits)
125    }
126
127    /// Extracts local stream ID (lower 32 bits).
128    ///
129    /// # Examples
130    ///
131    /// ```
132    /// # use kimberlite_types::{TenantId, StreamId};
133    /// let stream_id = StreamId::from_tenant_and_local(TenantId::from(5), 42);
134    /// assert_eq!(stream_id.local_id(), 42);
135    /// ```
136    pub fn local_id(self) -> u32 {
137        (u64::from(self) & 0xFFFF_FFFF) as u32
138    }
139
140    /// Extracts the tenant id this stream belongs to (upper 32 bits).
141    ///
142    /// Convenience over `TenantId::from_stream_id(stream_id)` for the
143    /// common `id.tenant_id() == other` call sites.
144    ///
145    /// # Examples
146    ///
147    /// ```
148    /// # use kimberlite_types::{TenantId, StreamId};
149    /// let stream_id = StreamId::from_tenant_and_local(TenantId::from(5), 1);
150    /// assert_eq!(stream_id.tenant_id(), TenantId::from(5));
151    /// ```
152    pub fn tenant_id(self) -> TenantId {
153        TenantId::from_stream_id(self)
154    }
155}
156
157impl Add for StreamId {
158    type Output = StreamId;
159
160    fn add(self, rhs: Self) -> Self::Output {
161        let v = self.0 + rhs.0;
162        StreamId::new(v)
163    }
164}
165
166impl Display for StreamId {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        write!(f, "{}", self.0)
169    }
170}
171
172impl From<u64> for StreamId {
173    fn from(value: u64) -> Self {
174        Self(value)
175    }
176}
177
178impl From<StreamId> for u64 {
179    fn from(id: StreamId) -> Self {
180        id.0
181    }
182}
183
184/// Position of an event within a stream.
185///
186/// Offsets are zero-indexed and sequential. The first event in a stream
187/// has offset 0, the second has offset 1, and so on.
188///
189/// Uses `u64` internally — offsets are never negative by definition.
190#[derive(
191    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
192)]
193pub struct Offset(u64);
194
195impl Offset {
196    pub const ZERO: Offset = Offset(0);
197
198    pub fn new(offset: u64) -> Self {
199        Self(offset)
200    }
201
202    /// Returns the offset as a `u64`.
203    pub fn as_u64(&self) -> u64 {
204        self.0
205    }
206
207    /// Returns the offset as a `usize` for indexing.
208    ///
209    /// # Panics
210    ///
211    /// Panics on 32-bit platforms if the offset exceeds `usize::MAX`.
212    pub fn as_usize(&self) -> usize {
213        self.0 as usize
214    }
215}
216
217impl Display for Offset {
218    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219        write!(f, "{}", self.0)
220    }
221}
222
223impl Add for Offset {
224    type Output = Self;
225    fn add(self, rhs: Self) -> Self::Output {
226        Self(self.0 + rhs.0)
227    }
228}
229
230impl AddAssign for Offset {
231    fn add_assign(&mut self, rhs: Self) {
232        self.0 += rhs.0;
233    }
234}
235
236impl Sub for Offset {
237    type Output = Self;
238    fn sub(self, rhs: Self) -> Self::Output {
239        Self(self.0 - rhs.0)
240    }
241}
242
243impl From<u64> for Offset {
244    fn from(value: u64) -> Self {
245        Self(value)
246    }
247}
248
249impl From<Offset> for u64 {
250    fn from(offset: Offset) -> Self {
251        offset.0
252    }
253}
254
255/// Unique identifier for a replication group.
256#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
257pub struct GroupId(u64);
258
259impl Display for GroupId {
260    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261        write!(f, "{}", self.0)
262    }
263}
264
265impl GroupId {
266    pub fn new(id: u64) -> Self {
267        Self(id)
268    }
269}
270
271impl From<u64> for GroupId {
272    fn from(value: u64) -> Self {
273        Self(value)
274    }
275}
276
277impl From<GroupId> for u64 {
278    fn from(id: GroupId) -> Self {
279        id.0
280    }
281}
282
283// ============================================================================
284// Cryptographic Hash - Copy (fixed 32-byte value)
285// ============================================================================
286
287/// Length of cryptographic hashes in bytes (SHA-256 / BLAKE3).
288pub const HASH_LENGTH: usize = 32;
289
290/// A 32-byte cryptographic hash.
291///
292/// This is a foundation type used across `Kimberlite` for:
293/// - Hash chain links (`prev_hash` in records)
294/// - Verification anchors (in checkpoints and projections)
295/// - Content addressing
296///
297/// The specific algorithm (SHA-256 for compliance, BLAKE3 for internal)
298/// is determined by the context where the hash is computed. This type
299/// only stores the resulting 32-byte digest.
300#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
301pub struct Hash([u8; HASH_LENGTH]);
302
303impl Hash {
304    /// The genesis hash (all zeros) used as the `prev_hash` for the first record.
305    pub const GENESIS: Hash = Hash([0u8; HASH_LENGTH]);
306
307    /// Creates a hash from raw bytes.
308    pub fn from_bytes(bytes: [u8; HASH_LENGTH]) -> Self {
309        Self(bytes)
310    }
311
312    /// Returns the hash as a byte slice.
313    pub fn as_bytes(&self) -> &[u8; HASH_LENGTH] {
314        &self.0
315    }
316
317    /// Returns true if this is the genesis hash (all zeros).
318    pub fn is_genesis(&self) -> bool {
319        self.0 == [0u8; HASH_LENGTH]
320    }
321}
322
323impl Debug for Hash {
324    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
325        // Show first 8 bytes in hex for debugging without exposing full hash
326        write!(
327            f,
328            "Hash({:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}...)",
329            self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5], self.0[6], self.0[7]
330        )
331    }
332}
333
334impl Display for Hash {
335    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336        // Full hex representation for display
337        for byte in &self.0 {
338            write!(f, "{byte:02x}")?;
339        }
340        Ok(())
341    }
342}
343
344impl Default for Hash {
345    fn default() -> Self {
346        Self::GENESIS
347    }
348}
349
350impl From<[u8; HASH_LENGTH]> for Hash {
351    fn from(bytes: [u8; HASH_LENGTH]) -> Self {
352        Self(bytes)
353    }
354}
355
356impl From<Hash> for [u8; HASH_LENGTH] {
357    fn from(hash: Hash) -> Self {
358        hash.0
359    }
360}
361
362impl AsRef<[u8]> for Hash {
363    fn as_ref(&self) -> &[u8] {
364        &self.0
365    }
366}
367
368// ============================================================================
369// Timestamp - Copy (8-byte value with monotonic guarantee)
370// ============================================================================
371
372/// Wall-clock timestamp with monotonic guarantee within the system.
373///
374/// Compliance requires real-world time for audit trails; monotonicity
375/// prevents ordering issues when system clocks are adjusted.
376///
377/// Stored as nanoseconds since Unix epoch (1970-01-01 00:00:00 UTC).
378/// This gives us ~584 years of range, well beyond any practical use.
379#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
380pub struct Timestamp(u64);
381
382impl Timestamp {
383    /// The Unix epoch (1970-01-01 00:00:00 UTC).
384    pub const EPOCH: Timestamp = Timestamp(0);
385
386    /// Creates a timestamp from nanoseconds since Unix epoch.
387    pub fn from_nanos(nanos: u64) -> Self {
388        Self(nanos)
389    }
390
391    /// Returns the timestamp as nanoseconds since Unix epoch.
392    pub fn as_nanos(&self) -> u64 {
393        self.0
394    }
395
396    /// Returns the timestamp as seconds since Unix epoch (truncates nanoseconds).
397    pub fn as_secs(&self) -> u64 {
398        self.0 / 1_000_000_000
399    }
400
401    /// Creates a timestamp for the current time.
402    ///
403    /// # Panics
404    ///
405    /// Panics if the system clock is before Unix epoch (should never happen).
406    pub fn now() -> Self {
407        let duration = SystemTime::now()
408            .duration_since(UNIX_EPOCH)
409            .expect("system clock is before Unix epoch");
410        Self(duration.as_nanos() as u64)
411    }
412
413    /// Creates a timestamp ensuring monotonicity: `max(now, last + 1ns)`.
414    ///
415    /// This guarantees that each timestamp is strictly greater than the previous,
416    /// even if the system clock moves backwards or two events occur in the same
417    /// nanosecond.
418    ///
419    /// # Arguments
420    ///
421    /// * `last` - The previous timestamp, if any. Pass `None` for the first timestamp.
422    pub fn now_monotonic(last: Option<Timestamp>) -> Self {
423        let now = Self::now();
424        match last {
425            Some(prev) => {
426                // Ensure strictly increasing: at least prev + 1 nanosecond
427                if now.0 <= prev.0 {
428                    Timestamp(prev.0.saturating_add(1))
429                } else {
430                    now
431                }
432            }
433            None => now,
434        }
435    }
436}
437
438impl Display for Timestamp {
439    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
440        // Display as seconds.nanoseconds for readability
441        let secs = self.0 / 1_000_000_000;
442        let nanos = self.0 % 1_000_000_000;
443        write!(f, "{secs}.{nanos:09}")
444    }
445}
446
447impl Default for Timestamp {
448    fn default() -> Self {
449        Self::EPOCH
450    }
451}
452
453impl From<u64> for Timestamp {
454    fn from(nanos: u64) -> Self {
455        Self(nanos)
456    }
457}
458
459impl From<Timestamp> for u64 {
460    fn from(ts: Timestamp) -> Self {
461        ts.0
462    }
463}
464
465// ============================================================================
466// Record Types - Copy (small enum and struct)
467// ============================================================================
468
469/// The kind of record stored in the log.
470///
471/// This enum distinguishes between different record types to enable
472/// efficient processing and verification.
473#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
474pub enum RecordKind {
475    /// Normal application data record.
476    #[default]
477    Data,
478    /// Periodic verification checkpoint (contains cumulative hash).
479    Checkpoint,
480    /// Logical deletion marker (data is not physically deleted).
481    Tombstone,
482}
483
484impl RecordKind {
485    /// Returns the single-byte discriminant for serialization.
486    pub fn as_byte(&self) -> u8 {
487        match self {
488            RecordKind::Data => 0,
489            RecordKind::Checkpoint => 1,
490            RecordKind::Tombstone => 2,
491        }
492    }
493
494    /// Creates a `RecordKind` from its byte discriminant.
495    ///
496    /// # Errors
497    ///
498    /// Returns `None` if the byte is not a valid discriminant.
499    pub fn from_byte(byte: u8) -> Option<Self> {
500        match byte {
501            0 => Some(RecordKind::Data),
502            1 => Some(RecordKind::Checkpoint),
503            2 => Some(RecordKind::Tombstone),
504            _ => None,
505        }
506    }
507}
508
509/// Metadata header for every log record.
510///
511/// This structure contains all metadata needed to verify and process
512/// a log record without reading its payload.
513#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
514pub struct RecordHeader {
515    /// Position in the log (0-indexed).
516    pub offset: Offset,
517    /// SHA-256 link to the previous record's hash (genesis for first record).
518    pub prev_hash: Hash,
519    /// When the record was committed (monotonic wall-clock).
520    pub timestamp: Timestamp,
521    /// Size of the payload in bytes.
522    pub payload_len: u32,
523    /// Type of record (Data, Checkpoint, Tombstone).
524    pub record_kind: RecordKind,
525}
526
527impl RecordHeader {
528    /// Creates a new record header.
529    ///
530    /// # Arguments
531    ///
532    /// * `offset` - Position in the log
533    /// * `prev_hash` - Hash of the previous record (or GENESIS for first)
534    /// * `timestamp` - When this record was committed
535    /// * `payload_len` - Size of the payload in bytes
536    /// * `record_kind` - Type of record
537    pub fn new(
538        offset: Offset,
539        prev_hash: Hash,
540        timestamp: Timestamp,
541        payload_len: u32,
542        record_kind: RecordKind,
543    ) -> Self {
544        Self {
545            offset,
546            prev_hash,
547            timestamp,
548            payload_len,
549            record_kind,
550        }
551    }
552
553    /// Returns true if this is the first record in the log.
554    pub fn is_genesis(&self) -> bool {
555        self.offset == Offset::ZERO && self.prev_hash.is_genesis()
556    }
557}
558
559// ============================================================================
560// Projection Tracking - Copy (small struct for projections)
561// ============================================================================
562
563/// Tracks which log entry a projection row was derived from.
564///
565/// Projections embed this in each row to enable:
566/// - Point-in-time queries (find rows at a specific offset)
567/// - Verification without walking the hash chain (hash provides direct check)
568/// - Audit trails (know exactly which event created/updated a row)
569#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
570pub struct AppliedIndex {
571    /// The log offset this row was derived from.
572    pub offset: Offset,
573    /// The hash at this offset for direct verification.
574    pub hash: Hash,
575}
576
577impl AppliedIndex {
578    /// Creates a new applied index.
579    pub fn new(offset: Offset, hash: Hash) -> Self {
580        Self { offset, hash }
581    }
582
583    /// Creates the initial applied index (before any records).
584    pub fn genesis() -> Self {
585        Self {
586            offset: Offset::ZERO,
587            hash: Hash::GENESIS,
588        }
589    }
590}
591
592impl Default for AppliedIndex {
593    fn default() -> Self {
594        Self::genesis()
595    }
596}
597
598// ============================================================================
599// Checkpoints - Copy (verification anchors in the log)
600// ============================================================================
601
602/// A periodic verification checkpoint stored in the log.
603///
604/// Checkpoints are records IN the log (not separate files), which means:
605/// - They are part of the hash chain (tamper-evident)
606/// - Checkpoint history is immutable
607/// - Single source of truth
608///
609/// Checkpoints enable efficient verified reads by providing trusted
610/// anchor points, reducing verification from O(n) to O(k) where k is
611/// the distance to the nearest checkpoint.
612#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
613pub struct Checkpoint {
614    /// Log position of this checkpoint record.
615    pub offset: Offset,
616    /// Cumulative hash of the chain at this point.
617    pub chain_hash: Hash,
618    /// Total number of records from genesis to this checkpoint.
619    pub record_count: u64,
620    /// When this checkpoint was created.
621    pub created_at: Timestamp,
622}
623
624impl Checkpoint {
625    /// Creates a new checkpoint.
626    ///
627    /// # Preconditions
628    ///
629    /// - `record_count` should equal `offset.as_u64() + 1` (0-indexed offset)
630    pub fn new(offset: Offset, chain_hash: Hash, record_count: u64, created_at: Timestamp) -> Self {
631        debug_assert_eq!(
632            record_count,
633            offset.as_u64() + 1,
634            "record_count should equal offset + 1"
635        );
636        Self {
637            offset,
638            chain_hash,
639            record_count,
640            created_at,
641        }
642    }
643}
644
645/// Policy for when to create checkpoints.
646///
647/// Checkpoints bound the worst-case verification cost. The default policy
648/// creates a checkpoint every 1000 records and on graceful shutdown.
649#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
650pub struct CheckpointPolicy {
651    /// Create a checkpoint every N records. Set to 0 to disable.
652    pub every_n_records: u64,
653    /// Create a checkpoint on graceful shutdown.
654    pub on_shutdown: bool,
655    /// If true, disable automatic checkpoints (only explicit calls).
656    pub explicit_only: bool,
657}
658
659impl CheckpointPolicy {
660    /// Creates a policy that checkpoints every N records.
661    pub fn every(n: u64) -> Self {
662        Self {
663            every_n_records: n,
664            on_shutdown: true,
665            explicit_only: false,
666        }
667    }
668
669    /// Creates a policy that only creates explicit checkpoints.
670    pub fn explicit_only() -> Self {
671        Self {
672            every_n_records: 0,
673            on_shutdown: false,
674            explicit_only: true,
675        }
676    }
677
678    /// Returns true if a checkpoint should be created at this offset.
679    pub fn should_checkpoint(&self, offset: Offset) -> bool {
680        if self.explicit_only {
681            return false;
682        }
683        if self.every_n_records == 0 {
684            return false;
685        }
686        // Checkpoint at offsets that are multiples of every_n_records
687        // (offset 999 for every_n_records=1000, etc.)
688        (offset.as_u64() + 1) % self.every_n_records == 0
689    }
690}
691
692impl Default for CheckpointPolicy {
693    /// Default policy: checkpoint every 1000 records, on shutdown.
694    fn default() -> Self {
695        Self {
696            every_n_records: 1000,
697            on_shutdown: true,
698            explicit_only: false,
699        }
700    }
701}
702
703// ============================================================================
704// Idempotency - Copy (16-byte identifier for duplicate prevention)
705// ============================================================================
706
707/// Length of idempotency IDs in bytes.
708pub const IDEMPOTENCY_ID_LENGTH: usize = 16;
709
710/// Unique identifier for duplicate transaction prevention.
711///
712/// Clients generate an `IdempotencyId` before their first attempt at a
713/// transaction. If the transaction needs to be retried (e.g., network
714/// timeout), the client reuses the same ID. The server tracks committed
715/// IDs to return the same result for duplicate requests.
716///
717/// Inspired by `FoundationDB`'s idempotency key design.
718///
719/// # FCIS Pattern
720///
721/// This type follows the Functional Core / Imperative Shell pattern:
722/// - `from_bytes()`: Pure restoration from storage
723/// - `from_random_bytes()`: Pure construction from bytes (`pub(crate)`)
724/// - `generate()`: Impure shell that invokes CSPRNG
725#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
726pub struct IdempotencyId([u8; IDEMPOTENCY_ID_LENGTH]);
727
728impl IdempotencyId {
729    // ========================================================================
730    // Functional Core (pure, testable)
731    // ========================================================================
732
733    /// Pure construction from random bytes.
734    ///
735    /// Restricted to `pub(crate)` to prevent misuse with weak random sources.
736    /// External callers should use `generate()` or `from_bytes()`.
737    pub(crate) fn from_random_bytes(bytes: [u8; IDEMPOTENCY_ID_LENGTH]) -> Self {
738        debug_assert!(
739            bytes.iter().any(|&b| b != 0),
740            "idempotency ID bytes are all zeros"
741        );
742        Self(bytes)
743    }
744
745    /// Restoration from stored bytes (pure).
746    ///
747    /// Use this when loading an `IdempotencyId` from storage or wire protocol.
748    pub fn from_bytes(bytes: [u8; IDEMPOTENCY_ID_LENGTH]) -> Self {
749        Self(bytes)
750    }
751
752    /// Returns the ID as a byte slice.
753    pub fn as_bytes(&self) -> &[u8; IDEMPOTENCY_ID_LENGTH] {
754        &self.0
755    }
756
757    // ========================================================================
758    // Imperative Shell (IO boundary)
759    // ========================================================================
760
761    /// Generates a new random idempotency ID using the OS CSPRNG.
762    ///
763    /// # Panics
764    ///
765    /// Panics if the OS CSPRNG fails, which indicates a catastrophic
766    /// system error (e.g., no entropy source available).
767    pub fn generate() -> Self {
768        let mut bytes = [0u8; IDEMPOTENCY_ID_LENGTH];
769        getrandom::fill(&mut bytes).expect("CSPRNG failure is catastrophic");
770        Self::from_random_bytes(bytes)
771    }
772}
773
774impl Debug for IdempotencyId {
775    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
776        // Show full hex for debugging (IDs are meant to be logged)
777        write!(f, "IdempotencyId(")?;
778        for byte in &self.0 {
779            write!(f, "{byte:02x}")?;
780        }
781        write!(f, ")")
782    }
783}
784
785impl Display for IdempotencyId {
786    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
787        // Full hex representation
788        for byte in &self.0 {
789            write!(f, "{byte:02x}")?;
790        }
791        Ok(())
792    }
793}
794
795impl From<[u8; IDEMPOTENCY_ID_LENGTH]> for IdempotencyId {
796    fn from(bytes: [u8; IDEMPOTENCY_ID_LENGTH]) -> Self {
797        Self::from_bytes(bytes)
798    }
799}
800
801impl From<IdempotencyId> for [u8; IDEMPOTENCY_ID_LENGTH] {
802    fn from(id: IdempotencyId) -> Self {
803        id.0
804    }
805}
806
807// ============================================================================
808// Recovery Tracking - Copy (generation-based recovery for compliance)
809// ============================================================================
810
811/// Monotonically increasing recovery generation.
812///
813/// Each recovery event creates a new generation. This provides natural
814/// audit checkpoints and explicit tracking of system recovery events.
815///
816/// Inspired by `FoundationDB`'s generation-based recovery tracking.
817#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
818pub struct Generation(u64);
819
820impl Generation {
821    /// The initial generation (before any recovery).
822    pub const INITIAL: Generation = Generation(0);
823
824    /// Creates a generation from a raw value.
825    pub fn new(value: u64) -> Self {
826        Self(value)
827    }
828
829    /// Returns the generation as a u64.
830    pub fn as_u64(&self) -> u64 {
831        self.0
832    }
833
834    /// Returns the next generation (incremented by 1).
835    pub fn next(&self) -> Self {
836        Generation(self.0.saturating_add(1))
837    }
838}
839
840impl Display for Generation {
841    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
842        write!(f, "gen:{}", self.0)
843    }
844}
845
846impl Default for Generation {
847    fn default() -> Self {
848        Self::INITIAL
849    }
850}
851
852impl From<u64> for Generation {
853    fn from(value: u64) -> Self {
854        Self(value)
855    }
856}
857
858impl From<Generation> for u64 {
859    fn from(generation: Generation) -> Self {
860        generation.0
861    }
862}
863
864/// Reason why a recovery was triggered.
865///
866/// This is recorded in the recovery log for compliance auditing.
867#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
868pub enum RecoveryReason {
869    /// Normal node restart (graceful or crash recovery).
870    NodeRestart,
871    /// Lost quorum and had to recover from remaining replicas.
872    QuorumLoss,
873    /// Detected data corruption requiring recovery.
874    CorruptionDetected,
875    /// Operator manually triggered recovery.
876    ManualIntervention,
877}
878
879impl Display for RecoveryReason {
880    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
881        match self {
882            RecoveryReason::NodeRestart => write!(f, "node_restart"),
883            RecoveryReason::QuorumLoss => write!(f, "quorum_loss"),
884            RecoveryReason::CorruptionDetected => write!(f, "corruption_detected"),
885            RecoveryReason::ManualIntervention => write!(f, "manual_intervention"),
886        }
887    }
888}
889
890/// Records a recovery event with explicit tracking of any data loss.
891///
892/// Critical for compliance: auditors can see exactly what happened during
893/// recovery, including any mutations that were discarded.
894///
895/// Inspired by `FoundationDB`'s 9-phase recovery with explicit data loss tracking.
896#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
897pub struct RecoveryRecord {
898    /// New generation after recovery.
899    pub generation: Generation,
900    /// Previous generation before recovery.
901    pub previous_generation: Generation,
902    /// Last known committed offset before recovery.
903    pub known_committed: Offset,
904    /// Offset we recovered to.
905    pub recovery_point: Offset,
906    /// Range of discarded prepares (if any) - EXPLICIT LOSS TRACKING.
907    ///
908    /// If `Some`, this range of offsets contained prepared but uncommitted
909    /// mutations that were discarded during recovery. This is the critical
910    /// compliance field: it explicitly documents any data loss.
911    pub discarded_range: Option<Range<Offset>>,
912    /// When recovery occurred.
913    pub timestamp: Timestamp,
914    /// Why recovery was triggered.
915    pub reason: RecoveryReason,
916}
917
918impl RecoveryRecord {
919    /// Creates a new recovery record.
920    ///
921    /// # Arguments
922    ///
923    /// * `generation` - The new generation after recovery
924    /// * `previous_generation` - The generation before recovery
925    /// * `known_committed` - Last known committed offset
926    /// * `recovery_point` - The offset we recovered to
927    /// * `discarded_range` - Range of discarded uncommitted prepares, if any
928    /// * `timestamp` - When recovery occurred
929    /// * `reason` - Why recovery was triggered
930    ///
931    /// # Preconditions
932    ///
933    /// - `generation` must be greater than `previous_generation`
934    /// - `recovery_point` must be <= `known_committed`
935    pub fn new(
936        generation: Generation,
937        previous_generation: Generation,
938        known_committed: Offset,
939        recovery_point: Offset,
940        discarded_range: Option<Range<Offset>>,
941        timestamp: Timestamp,
942        reason: RecoveryReason,
943    ) -> Self {
944        debug_assert!(
945            generation > previous_generation,
946            "new generation must be greater than previous"
947        );
948        debug_assert!(
949            recovery_point <= known_committed,
950            "recovery point cannot exceed known committed"
951        );
952
953        Self {
954            generation,
955            previous_generation,
956            known_committed,
957            recovery_point,
958            discarded_range,
959            timestamp,
960            reason,
961        }
962    }
963
964    /// Returns true if any data was lost during this recovery.
965    pub fn had_data_loss(&self) -> bool {
966        self.discarded_range.is_some()
967    }
968
969    /// Returns the number of discarded records, if any.
970    pub fn discarded_count(&self) -> u64 {
971        self.discarded_range
972            .as_ref()
973            .map_or(0, |r| r.end.as_u64().saturating_sub(r.start.as_u64()))
974    }
975}
976
977// ============================================================================
978// Stream Name - Clone (contains String, but rarely cloned)
979// ============================================================================
980
981/// Human-readable name for a stream.
982#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
983pub struct StreamName(String);
984
985impl StreamName {
986    pub fn new(name: impl Into<String>) -> Self {
987        Self(name.into())
988    }
989
990    pub fn as_str(&self) -> &str {
991        &self.0
992    }
993}
994
995impl Display for StreamName {
996    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
997        write!(f, "{}", self.0)
998    }
999}
1000
1001impl From<String> for StreamName {
1002    fn from(name: String) -> Self {
1003        Self(name)
1004    }
1005}
1006
1007impl From<&str> for StreamName {
1008    fn from(name: &str) -> Self {
1009        Self(name.to_string())
1010    }
1011}
1012
1013impl From<StreamName> for String {
1014    fn from(value: StreamName) -> Self {
1015        value.0
1016    }
1017}
1018
1019// ============================================================================
1020// Data Classification - Copy (simple enum, no heap data)
1021// ============================================================================
1022
1023/// Classification of data for compliance purposes.
1024///
1025/// Supports multi-framework compliance: HIPAA, GDPR, PCI DSS, SOX, ISO 27001, `FedRAMP`.
1026///
1027/// # Classification Levels (8 total)
1028///
1029/// **Healthcare (HIPAA):**
1030/// - `PHI`: Protected Health Information
1031/// - `Deidentified`: De-identified per HIPAA Safe Harbor
1032///
1033/// **Privacy (GDPR):**
1034/// - `PII`: Personally Identifiable Information (GDPR Article 4)
1035/// - `Sensitive`: Special category data (GDPR Article 9) - race, health, biometrics, etc.
1036///
1037/// **Financial (PCI DSS, SOX):**
1038/// - `PCI`: Payment Card Industry data (card numbers, CVV, etc.)
1039/// - `Financial`: Financial records subject to SOX regulations
1040///
1041/// **General:**
1042/// - `Confidential`: Internal business data, trade secrets
1043/// - `Public`: Publicly available data with no restrictions
1044///
1045/// # Framework Mappings
1046///
1047/// | Level | HIPAA | GDPR | PCI DSS | SOX | ISO 27001 | FedRAMP |
1048/// |-------|-------|------|---------|-----|-----------|---------|
1049/// | PHI | ✓ | ✓ (PII) | — | — | ✓ | ✓ |
1050/// | Deidentified | ✓ | — | — | — | — | — |
1051/// | PII | — | ✓ | — | — | ✓ | ✓ |
1052/// | Sensitive | — | ✓ (Art 9) | — | — | ✓ | ✓ |
1053/// | PCI | — | ✓ (PII) | ✓ | — | ✓ | ✓ |
1054/// | Financial | — | — | — | ✓ | ✓ | ✓ |
1055/// | Confidential | — | — | — | — | ✓ | ✓ |
1056/// | Public | — | — | — | — | — | — |
1057///
1058#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1059pub enum DataClass {
1060    // ========================================================================
1061    // Healthcare (HIPAA)
1062    // ========================================================================
1063    /// Protected Health Information - subject to HIPAA restrictions.
1064    ///
1065    /// **Examples:** Medical records, diagnoses, lab results, prescriptions
1066    ///
1067    /// **Compliance:** HIPAA Privacy Rule, HIPAA Security Rule
1068    ///
1069    /// **Retention:** Minimum 6 years after last treatment (HIPAA § 164.530)
1070    PHI,
1071
1072    /// Data that has been de-identified per HIPAA Safe Harbor.
1073    ///
1074    /// **Requirements:** All 18 HIPAA identifiers removed (§ 164.514(b)(2))
1075    ///
1076    /// **Examples:** Anonymized patient datasets, aggregate statistics
1077    ///
1078    /// **Compliance:** HIPAA Safe Harbor Method
1079    Deidentified,
1080
1081    // ========================================================================
1082    // Privacy (GDPR)
1083    // ========================================================================
1084    /// Personally Identifiable Information (GDPR Article 4).
1085    ///
1086    /// **Examples:** Names, email addresses, IP addresses, location data
1087    ///
1088    /// **Compliance:** GDPR Articles 5-11 (lawfulness, consent, purpose limitation)
1089    ///
1090    /// **Rights:** Access, rectification, erasure, portability (GDPR Articles 15-20)
1091    PII,
1092
1093    /// Special category data (GDPR Article 9).
1094    ///
1095    /// **Examples:** Racial/ethnic origin, political opinions, religious beliefs,
1096    /// trade union membership, genetic data, biometric data, health data, sex life
1097    ///
1098    /// **Compliance:** GDPR Article 9 (explicit consent required, stricter controls)
1099    ///
1100    /// **Restrictions:** Processing prohibited unless explicit exception applies
1101    Sensitive,
1102
1103    // ========================================================================
1104    // Financial (PCI DSS, SOX)
1105    // ========================================================================
1106    /// Payment Card Industry data (PCI DSS).
1107    ///
1108    /// **Examples:** Credit card numbers, CVV codes, cardholder data
1109    ///
1110    /// **Compliance:** PCI DSS Requirements 1-12
1111    ///
1112    /// **Storage:** Never store CVV/CVV2/PIN after authorization
1113    PCI,
1114
1115    /// Financial records subject to SOX regulations.
1116    ///
1117    /// **Examples:** General ledger, financial statements, audit trails
1118    ///
1119    /// **Compliance:** Sarbanes-Oxley Act § 302, § 404
1120    ///
1121    /// **Retention:** 7 years minimum (SOX § 802)
1122    Financial,
1123
1124    // ========================================================================
1125    // General
1126    // ========================================================================
1127    /// Internal business data, trade secrets.
1128    ///
1129    /// **Examples:** Proprietary algorithms, business strategies, internal communications
1130    ///
1131    /// **Compliance:** ISO 27001 Annex A.8 (Asset Management)
1132    ///
1133    /// **Access:** Restricted to authorized personnel
1134    Confidential,
1135
1136    /// Publicly available data with no restrictions.
1137    ///
1138    /// **Examples:** Public website content, press releases, published research
1139    ///
1140    /// **Compliance:** No special restrictions
1141    ///
1142    /// **Access:** Unrestricted
1143    Public,
1144}
1145
1146// ============================================================================
1147// Placement - Clone (Region::Custom contains String)
1148// ============================================================================
1149
1150/// Placement policy for a stream.
1151#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1152pub enum Placement {
1153    /// Data must remain within the specified region.
1154    Region(Region),
1155    /// Data can be replicated globally across all regions.
1156    Global,
1157}
1158
1159/// Geographic region for data placement.
1160#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1161pub enum Region {
1162    /// US East (N. Virginia) - us-east-1
1163    USEast1,
1164    /// Asia Pacific (Sydney) - ap-southeast-2
1165    APSoutheast2,
1166    /// Custom region identifier
1167    Custom(String),
1168}
1169
1170impl Region {
1171    pub fn custom(name: impl Into<String>) -> Self {
1172        Self::Custom(name.into())
1173    }
1174}
1175
1176impl Display for Region {
1177    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1178        match self {
1179            Region::USEast1 => write!(f, "us-east-1"),
1180            Region::APSoutheast2 => write!(f, "ap-southeast-2"),
1181            Region::Custom(custom) => write!(f, "{custom}"),
1182        }
1183    }
1184}
1185
1186// ============================================================================
1187// Stream Metadata - Clone (created once per stream, cloned rarely)
1188// ============================================================================
1189
1190/// Metadata describing a stream's configuration and current state.
1191#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1192pub struct StreamMetadata {
1193    pub stream_id: StreamId,
1194    pub stream_name: StreamName,
1195    pub data_class: DataClass,
1196    pub placement: Placement,
1197    pub current_offset: Offset,
1198}
1199
1200impl StreamMetadata {
1201    /// Creates new stream metadata with offset initialized to 0.
1202    pub fn new(
1203        stream_id: StreamId,
1204        stream_name: StreamName,
1205        data_class: DataClass,
1206        placement: Placement,
1207    ) -> Self {
1208        Self {
1209            stream_id,
1210            stream_name,
1211            data_class,
1212            placement,
1213            current_offset: Offset::default(),
1214        }
1215    }
1216}
1217
1218// ============================================================================
1219// Batch Payload - NOT Clone (contains Vec<Bytes>, move only)
1220// ============================================================================
1221
1222/// A batch of events to append to a stream.
1223#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
1224pub struct BatchPayload {
1225    pub stream_id: StreamId,
1226    /// The events to append (zero-copy Bytes).
1227    pub events: Vec<Bytes>,
1228    /// Expected current offset for optimistic concurrency.
1229    pub expected_offset: Offset,
1230}
1231
1232impl BatchPayload {
1233    pub fn new(stream_id: StreamId, events: Vec<Bytes>, expected_offset: Offset) -> Self {
1234        Self {
1235            stream_id,
1236            events,
1237            expected_offset,
1238        }
1239    }
1240}
1241
1242// ============================================================================
1243// Audit Actions - Clone (for flexibility in logging)
1244// ============================================================================
1245
1246/// Actions recorded in the audit log.
1247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1248pub enum AuditAction {
1249    /// A new stream was created.
1250    StreamCreated {
1251        stream_id: StreamId,
1252        stream_name: StreamName,
1253        data_class: DataClass,
1254        placement: Placement,
1255    },
1256    /// Events were appended to a stream.
1257    EventsAppended {
1258        stream_id: StreamId,
1259        count: u32,
1260        from_offset: Offset,
1261    },
1262    /// **AUDIT-2026-04 H-5** — a tenant was sealed for
1263    /// forensic / audit / legal-hold operations. No mutating
1264    /// commands (DDL, DML, AppendBatch, CreateStream) will be
1265    /// accepted against the tenant until an `Unseal` is applied.
1266    TenantSealed {
1267        tenant_id: TenantId,
1268        reason: SealReason,
1269    },
1270    /// **AUDIT-2026-04 H-5** — a previously-sealed tenant was
1271    /// released. Mutating commands are accepted again. Audit trail
1272    /// retains the seal/unseal pair as structured evidence.
1273    TenantUnsealed { tenant_id: TenantId },
1274}
1275
1276/// **AUDIT-2026-04 H-5** — why a tenant was sealed.
1277///
1278/// This is an enum rather than a free-form string so downstream
1279/// compliance reports can aggregate on specific operational
1280/// categories — healthcare auditors expect to see discrete reasons
1281/// (e.g. `ForensicHold`) not human-written prose.
1282#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1283pub enum SealReason {
1284    /// Forensic preservation during an incident investigation.
1285    ForensicHold,
1286    /// Audit is in progress; writes must freeze until audit completes.
1287    AuditInProgress,
1288    /// Breach investigation — preserve state for forensic analysis.
1289    BreachInvestigation,
1290    /// Legal hold (discovery / litigation-retention).
1291    LegalHold,
1292}
1293
1294// ============================================================================
1295// Event Persistence - Trait for durable event log writes
1296// ============================================================================
1297
1298/// Abstraction for persisting events to the durable event log.
1299///
1300/// This trait is the bridge between the projection layer and the
1301/// `Kimberlite` replication system. Implementations must block until
1302/// persistence is confirmed.
1303///
1304/// # Healthcare Compliance
1305///
1306/// This is the critical path for HIPAA compliance. The implementation must:
1307/// - **Block until VSR consensus** completes (quorum durability)
1308/// - **Return `Err`** if consensus fails (triggers rollback)
1309/// - **Never return `Ok`** unless events are durably stored
1310///
1311/// # Implementation Notes
1312///
1313/// The implementor (typically `Runtime`) must handle the sync→async bridge:
1314///
1315/// ```ignore
1316/// impl EventPersister for RuntimeHandle {
1317///     fn persist_blocking(&self, stream_id: StreamId, events: Vec<Bytes>) -> Result<Offset, PersistError> {
1318///         // Bridge sync callback to async runtime
1319///         tokio::task::block_in_place(|| {
1320///             tokio::runtime::Handle::current().block_on(async {
1321///                 self.inner.append(stream_id, events).await
1322///             })
1323///         })
1324///         .map_err(|e| {
1325///             tracing::error!(error = %e, "VSR persistence failed");
1326///             PersistError::ConsensusFailed
1327///         })
1328///     }
1329/// }
1330/// ```
1331///
1332/// # Why `Vec<Bytes>` instead of typed events?
1333///
1334/// Events are serialized before reaching this trait. This keeps `kmb-types`
1335/// decoupled from domain-specific event schemas.
1336pub trait EventPersister: Send + Sync + Debug {
1337    /// Persist a batch of serialized events to the durable event log.
1338    ///
1339    /// This method **blocks** until VSR consensus confirms the events are
1340    /// durably stored on a quorum of nodes.
1341    ///
1342    /// # Arguments
1343    ///
1344    /// * `stream_id` - The stream to append events to
1345    /// * `events` - Serialized events
1346    ///
1347    /// # Returns
1348    ///
1349    /// * `Ok(offset)` - Events persisted, returns the new stream offset
1350    /// * `Err(PersistError)` - Persistence failed, caller should rollback
1351    ///
1352    /// # Errors
1353    ///
1354    /// * [`PersistError::ConsensusFailed`] - VSR quorum unavailable after retries
1355    /// * [`PersistError::StorageError`] - Disk I/O or serialization failure
1356    /// * [`PersistError::ShuttingDown`] - System is terminating
1357    fn persist_blocking(
1358        &self,
1359        stream_id: StreamId,
1360        events: Vec<Bytes>,
1361    ) -> Result<Offset, PersistError>;
1362}
1363
1364/// Error returned when event persistence fails.
1365///
1366/// The hook uses this to decide whether to rollback the transaction.
1367/// Specific underlying errors are logged by the implementation.
1368#[derive(Debug, Clone, PartialEq, Eq)]
1369pub enum PersistError {
1370    /// VSR consensus failed after retries (quorum unavailable)
1371    ConsensusFailed,
1372    /// Storage I/O error
1373    StorageError,
1374    /// System is shutting down
1375    ShuttingDown,
1376}
1377
1378impl std::fmt::Display for PersistError {
1379    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1380        match self {
1381            Self::ConsensusFailed => write!(f, "consensus failed after retries"),
1382            Self::StorageError => write!(f, "storage I/O error"),
1383            Self::ShuttingDown => write!(f, "system is shutting down"),
1384        }
1385    }
1386}
1387
1388impl std::error::Error for PersistError {}
1389
1390// ============================================================================
1391// Compression - Copy (simple enum for codec selection)
1392// ============================================================================
1393
1394/// Compression algorithm for record payloads.
1395///
1396/// Each record stores its compression kind so that records compressed with
1397/// different algorithms can coexist in the same segment. The `None` variant
1398/// means the payload is stored uncompressed.
1399#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
1400pub enum CompressionKind {
1401    /// No compression (default).
1402    #[default]
1403    None = 0,
1404    /// LZ4 compression (fast, moderate ratio).
1405    Lz4 = 1,
1406    /// Zstandard compression (slower, better ratio).
1407    Zstd = 2,
1408}
1409
1410impl CompressionKind {
1411    /// Returns the single-byte discriminant for serialization.
1412    pub fn as_byte(self) -> u8 {
1413        self as u8
1414    }
1415
1416    /// Creates a `CompressionKind` from its byte discriminant.
1417    pub fn from_byte(byte: u8) -> Option<Self> {
1418        match byte {
1419            0 => Some(Self::None),
1420            1 => Some(Self::Lz4),
1421            2 => Some(Self::Zstd),
1422            _ => None,
1423        }
1424    }
1425}
1426
1427impl Display for CompressionKind {
1428    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1429        match self {
1430            Self::None => write!(f, "none"),
1431            Self::Lz4 => write!(f, "lz4"),
1432            Self::Zstd => write!(f, "zstd"),
1433        }
1434    }
1435}
1436
1437/// Flux refinement type annotations (experimental)
1438///
1439/// These annotations provide compile-time verification when Flux compiler is enabled.
1440/// Currently commented out as Flux is experimental, but documents intended properties.
1441pub mod flux_annotations;
1442
1443/// Typed-domain primitives for making illegal states unrepresentable.
1444///
1445/// Introduced by the fuzz-to-types hardening effort (see
1446/// `docs-internal/contributing/constructor-audit-2026-04.md`). Re-exports
1447/// [`NonEmptyVec`](domain::NonEmptyVec), [`SqlIdentifier`](domain::SqlIdentifier),
1448/// [`BoundedSize`](domain::BoundedSize), and [`ClearanceLevel`](domain::ClearanceLevel).
1449pub mod domain;
1450
1451pub use domain::{
1452    BoundedSize, BoundedSizeError, ClearanceLevel, ClearanceLevelError, EmptyVecError,
1453    NonEmptyVec, SqlIdentifier, SqlIdentifierError,
1454};
1455
1456#[cfg(test)]
1457mod tests;