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;