Skip to main content

smos_domain/
error.rs

1//! Domain-level errors.
2//!
3//! Every operation that can fail at the domain boundary returns [`DomainError`].
4//! No IO errors live here — the domain layer is pure.
5
6use crate::enums::FactStatus;
7use crate::value_objects::{FactId, Timestamp};
8
9/// All invariants and parse failures the domain layer can report.
10///
11/// Variants are intentionally exhaustive: each one corresponds to a single
12/// well-defined invariant of a value object or aggregate. Callers match
13/// exhaustively so the compiler flags any new invariant we forget to handle.
14#[derive(Debug, thiserror::Error)]
15pub enum DomainError {
16    #[error("fact id format invalid: {0}")]
17    InvalidFactId(String),
18
19    #[error("memory key unsafe or invalid: {0}")]
20    UnsafeMemoryKey(String),
21
22    #[error("session id format invalid: {0}")]
23    InvalidSessionId(String),
24
25    #[error("confidence out of range [0,1]: {0}")]
26    ConfidenceOutOfRange(f32),
27
28    #[error("heat out of range [0,1]: {0}")]
29    HeatOutOfRange(f32),
30
31    #[error("cosine out of range [-1,1]: {0}")]
32    CosineOutOfRange(f32),
33
34    #[error("fact content empty")]
35    EmptyFactContent,
36
37    #[error("embedding empty")]
38    EmptyEmbedding,
39
40    #[error("timestamp out of representable range: {0}")]
41    InvalidTimestamp(String),
42
43    #[error("illegal status transition: {from} -> {to}")]
44    IllegalStatusTransition { from: FactStatus, to: FactStatus },
45
46    #[error("invariant violation: Accepted fact must have confidence >= {threshold}, got {actual}")]
47    ConfidenceBelowAcceptThreshold { threshold: f32, actual: f32 },
48
49    #[error("invariant violation: valid_until ({until}) <= valid_from ({from})")]
50    ValidUntilBeforeValidFrom { from: Timestamp, until: Timestamp },
51
52    #[error("invariant violation: fact cannot conflict with itself: {0}")]
53    SelfConflict(FactId),
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use crate::enums::FactStatus;
60    use crate::value_objects::{FactId, Timestamp};
61
62    #[test]
63    fn display_invalid_fact_id_contains_input() {
64        let err = DomainError::InvalidFactId("bad".to_string());
65        assert_eq!(err.to_string(), "fact id format invalid: bad");
66    }
67
68    #[test]
69    fn display_unsafe_memory_key_contains_input() {
70        let err = DomainError::UnsafeMemoryKey("../etc".to_string());
71        assert_eq!(err.to_string(), "memory key unsafe or invalid: ../etc");
72    }
73
74    #[test]
75    fn display_confidence_out_of_range_includes_value() {
76        let err = DomainError::ConfidenceOutOfRange(1.5);
77        assert_eq!(err.to_string(), "confidence out of range [0,1]: 1.5");
78    }
79
80    #[test]
81    fn display_empty_fact_content_is_static_text() {
82        let err = DomainError::EmptyFactContent;
83        assert_eq!(err.to_string(), "fact content empty");
84    }
85
86    #[test]
87    fn display_illegal_transition_includes_both_statuses() {
88        let err = DomainError::IllegalStatusTransition {
89            from: FactStatus::Accepted,
90            to: FactStatus::Pending,
91        };
92        assert_eq!(
93            err.to_string(),
94            "illegal status transition: accepted -> pending"
95        );
96    }
97
98    #[test]
99    fn display_confidence_below_threshold_includes_threshold_and_actual() {
100        let err = DomainError::ConfidenceBelowAcceptThreshold {
101            threshold: 0.7,
102            actual: 0.5,
103        };
104        assert_eq!(
105            err.to_string(),
106            "invariant violation: Accepted fact must have confidence >= 0.7, got 0.5"
107        );
108    }
109
110    #[test]
111    fn display_self_conflict_includes_fact_id() {
112        let id = FactId::from_content("x");
113        let err = DomainError::SelfConflict(id.clone());
114        assert_eq!(
115            err.to_string(),
116            format!(
117                "invariant violation: fact cannot conflict with itself: {}",
118                id
119            )
120        );
121    }
122
123    #[test]
124    fn display_valid_until_before_valid_from_mentions_invariant() {
125        let from = Timestamp::from_unix_secs(1000).unwrap();
126        let until = Timestamp::from_unix_secs(500).unwrap();
127        let err = DomainError::ValidUntilBeforeValidFrom { from, until };
128        let msg = err.to_string();
129        // Display carries the invariant text and both timestamps (the exact
130        // OffsetDateTime rendering is crate-version dependent, so we assert on
131        // the stable substring rather than a specific datetime format).
132        assert!(msg.contains("valid_until"), "msg = {msg}");
133        assert!(msg.contains("valid_from"), "msg = {msg}");
134        assert!(msg.contains("invariant violation"), "msg = {msg}");
135    }
136}