Skip to main content

frame_conv/
error.rs

1//! Typed refusals and failures: the `CONV_*` slug family, envelope refusals,
2//! attach/publish/call errors, and cursor-commit outcomes.
3//!
4//! Every variant answers "what should the caller do now" (F-3a R1). Nothing
5//! here is a silent fallback; every substrate verdict is surfaced typed.
6
7use std::time::Duration;
8
9use thiserror::Error;
10
11use crate::id::{ConversationSeq, CorrelationId};
12
13/// Closed refusal-slug family for the conversation contract layer (design
14/// §2.2 — golden-vectored in the crate's vector suite).
15pub mod slug {
16    /// Inbound bytes were not a well-formed conversation envelope.
17    pub const CONV_ENVELOPE_MALFORMED: &str = "CONV_ENVELOPE_MALFORMED";
18    /// The envelope named a pattern outside the closed contract set.
19    pub const CONV_PATTERN_UNKNOWN: &str = "CONV_PATTERN_UNKNOWN";
20    /// The envelope's kind is not in the named pattern's closed kind set.
21    pub const CONV_KIND_INVALID: &str = "CONV_KIND_INVALID";
22    /// The envelope's correlation field was not a typed correlation identity.
23    pub const CONV_CORRELATION_INVALID: &str = "CONV_CORRELATION_INVALID";
24    /// A typed payload failed (de)serialization — v1's schema IS the typed
25    /// Rust message and validation IS serde at both ends (F-3a R1).
26    pub const CONV_SCHEMA_INVALID: &str = "CONV_SCHEMA_INVALID";
27}
28
29/// A typed envelope refusal carrying its closed slug and the exact detail.
30#[derive(Debug, Clone, Error)]
31#[error("{slug}: {detail}")]
32pub struct EnvelopeError {
33    /// The closed `CONV_*` refusal slug.
34    pub slug: &'static str,
35    /// Exact refusal detail naming the real source.
36    pub detail: String,
37}
38
39/// Substrate refusal classes surfaced on operations that reached the server.
40///
41/// The class is frame vocabulary over the substrate's closed answer set; the
42/// exact upstream answer is always preserved in the accompanying detail.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum RefusalClass {
45    /// A declared capacity closure refused the operation.
46    Capacity,
47    /// The operation's authority (binding, generation, receipt) was refused.
48    Authority,
49    /// The participant or enrollment identity was refused.
50    Identity,
51    /// The conversation's sequence/order budget refused the operation.
52    Order,
53    /// Any other typed protocol answer, preserved verbatim in the detail.
54    Protocol,
55}
56
57/// Failure opening, joining, or resuming a conversation.
58#[derive(Debug, Error)]
59pub enum AttachError {
60    /// The bus endpoint was rejected or unreachable.
61    #[error("bus endpoint failed: {detail}")]
62    Endpoint {
63        /// Exact transport-layer detail.
64        detail: String,
65    },
66    /// The substrate refused the enrollment or re-attach with a typed answer.
67    #[error("attach refused ({class:?}): {detail}")]
68    Refused {
69        /// Refusal class in frame vocabulary.
70        class: RefusalClass,
71        /// Exact upstream answer.
72        detail: String,
73    },
74    /// The connection was lost before the attach answer arrived.
75    #[error("attach lost the connection: {detail}")]
76    ConnectionLost {
77        /// Exact transport fate.
78        detail: String,
79    },
80    /// The declared answer window elapsed without a correlated attach answer.
81    #[error("attach answer window elapsed after {waited:?}")]
82    AnswerTimeout {
83        /// Total time waited.
84        waited: Duration,
85    },
86    /// The caller-supplied resume state failed decode/validate/restore.
87    #[error("resume state rejected: {detail}")]
88    ResumeRejected {
89        /// Exact typed decode/restore refusal.
90        detail: String,
91    },
92    /// The caller-owned durable store failed a persistence barrier.
93    #[error("resume store failed: {detail}")]
94    Store {
95        /// Exact store failure.
96        detail: String,
97    },
98    /// A protocol-shape violation at the attach seam.
99    #[error("attach protocol violation: {detail}")]
100    Protocol {
101        /// Exact violation detail.
102        detail: String,
103    },
104}
105
106/// Failure publishing a typed message into the conversation.
107#[derive(Debug, Error)]
108pub enum PublishError {
109    /// The typed value failed serialization — refused BEFORE the wire
110    /// (F-3a R1; slug [`slug::CONV_SCHEMA_INVALID`]).
111    #[error("{}: {detail}", slug::CONV_SCHEMA_INVALID)]
112    SchemaInvalid {
113        /// Exact serialization refusal.
114        detail: String,
115    },
116    /// The substrate answered the admission with a typed refusal.
117    #[error("publish refused ({class:?}): {detail}")]
118    Refused {
119        /// Refusal class in frame vocabulary.
120        class: RefusalClass,
121        /// Exact upstream answer.
122        detail: String,
123    },
124    /// The connection was lost. Send outcomes are transport-write testimony
125    /// only (finding G1 — `Sent` is not receipt); the handle drives slot
126    /// recovery through the substrate's transport-loss surface (finding G2)
127    /// and every subsequent operation fails typed until [`resume`] runs.
128    ///
129    /// [`resume`]: crate::ConversationHandle::resume
130    #[error("publish lost the connection: {detail}")]
131    ConnectionLost {
132        /// Exact transport fate, including the operation and reconnect fates
133        /// the substrate recorded.
134        detail: String,
135    },
136    /// The declared answer window elapsed without the admission's correlated
137    /// terminal answer (the G2 wall, bounded loudly instead of spun on).
138    #[error("publish answer window elapsed after {waited:?}")]
139    AnswerTimeout {
140        /// Total time waited.
141        waited: Duration,
142    },
143    /// A protocol-shape violation at the publish seam.
144    #[error("publish protocol violation: {detail}")]
145    Protocol {
146        /// Exact violation detail.
147        detail: String,
148    },
149}
150
151/// Failure leaving a conversation (the F-3b R1 explicit-leave surface,
152/// wrapping the substrate's terminal `LeaveRequest` — brief §Gate fill S1
153/// leave paragraph, probe findings 2026-07-23). The typed already-left
154/// answers are OUTCOMES ([`LeaveOutcome`]), never errors.
155///
156/// [`LeaveOutcome`]: crate::outcome::LeaveOutcome
157#[derive(Debug, Error)]
158pub enum LeaveError {
159    /// The substrate answered the leave with a typed refusal (stale
160    /// authority, generation conflict, identity refusal — the exact
161    /// upstream answer is preserved in the detail).
162    #[error("leave refused ({class:?}): {detail}")]
163    Refused {
164        /// Refusal class in frame vocabulary.
165        class: RefusalClass,
166        /// Exact upstream answer.
167        detail: String,
168    },
169    /// The connection was lost before the leave's correlated answer.
170    #[error("leave lost the connection: {detail}")]
171    ConnectionLost {
172        /// Exact transport fate.
173        detail: String,
174    },
175    /// The declared answer window elapsed without the leave's correlated
176    /// terminal answer.
177    #[error("leave answer window elapsed after {waited:?}")]
178    AnswerTimeout {
179        /// Total time waited.
180        waited: Duration,
181    },
182    /// The bus endpoint was rejected or unreachable (the resumed-leave
183    /// path's connect leg).
184    #[error("bus endpoint failed: {detail}")]
185    Endpoint {
186        /// Exact transport-layer detail.
187        detail: String,
188    },
189    /// The caller-supplied resume state failed decode/validate/restore
190    /// (the resumed-leave path's restore leg).
191    #[error("resume state rejected: {detail}")]
192    ResumeRejected {
193        /// Exact typed decode/restore refusal.
194        detail: String,
195    },
196    /// The caller-owned durable store failed a persistence barrier.
197    #[error("resume store failed: {detail}")]
198    Store {
199        /// Exact store failure.
200        detail: String,
201    },
202    /// A protocol-shape violation at the leave seam.
203    #[error("leave protocol violation: {detail}")]
204    Protocol {
205        /// Exact violation detail.
206        detail: String,
207    },
208}
209
210/// Failure on the receive/wait side of a pattern call.
211#[derive(Debug, Error)]
212pub enum CallError {
213    /// The admission leg of the call failed.
214    #[error(transparent)]
215    Publish(#[from] PublishError),
216    /// The connection died while waiting (a connection fate, distinct from
217    /// every conversation outcome).
218    #[error("connection lost while waiting: {detail}")]
219    ConnectionLost {
220        /// Exact transport fate.
221        detail: String,
222    },
223    /// A correlated reply arrived but failed typed payload validation —
224    /// surfaced typed, never dropped (F-3a R1/R6).
225    #[error(
226        "{}: reply to {correlation} invalid: {detail}",
227        slug::CONV_SCHEMA_INVALID
228    )]
229    ReplySchemaInvalid {
230        /// The exchange whose reply failed validation.
231        correlation: CorrelationId,
232        /// Exact deserialization refusal.
233        detail: String,
234    },
235    /// A protocol-shape violation while waiting.
236    #[error("call protocol violation: {detail}")]
237    Protocol {
238        /// Exact violation detail.
239        detail: String,
240    },
241}
242
243/// Failure from a caller-owned resume store (frame vocabulary over the
244/// caller-owned persistence boundary — F-0c §R4: the caller owns cursor and
245/// resume-state persistence; the substrate owns barrier ordering).
246#[derive(Debug, Clone, Error)]
247#[error("resume store failure: {detail}")]
248pub struct StoreError {
249    /// Exact store failure detail.
250    pub detail: String,
251}
252
253impl StoreError {
254    /// Constructs a store failure with its exact detail.
255    #[must_use]
256    pub fn new(detail: impl Into<String>) -> Self {
257        Self {
258            detail: detail.into(),
259        }
260    }
261}
262
263/// Typed outcome of committing the caller-owned cursor (honest acking is the
264/// consumer's lever on replay volume — F-0c §R4).
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub enum CursorCommit {
267    /// The cursor advanced to the requested boundary.
268    Committed {
269        /// The committed cumulative boundary.
270        through: ConversationSeq,
271    },
272    /// The requested boundary did not advance the cursor.
273    NoOp {
274        /// The unchanged cursor.
275        current: ConversationSeq,
276    },
277    /// The requested boundary skipped undischarged obligations; the cursor
278    /// is unchanged. A joiner's cursor space begins at its membership —
279    /// history before the join is not ackable (F-0c late-joiner finding).
280    Gap {
281        /// The refused boundary.
282        requested: ConversationSeq,
283        /// The unchanged cursor.
284        current: ConversationSeq,
285    },
286    /// The requested boundary regressed below the committed cursor; the
287    /// cursor is unchanged.
288    Regression {
289        /// The refused boundary.
290        requested: ConversationSeq,
291    },
292}