Skip to main content

riffdb_errors/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Transport-neutral, public-safe errors for RiffDB.
4//!
5//! Declared business outcomes are contract values and intentionally do not
6//! appear in this crate. [`PublicError`] describes failures outside that
7//! outcome algebra without retaining untrusted diagnostic text or internal
8//! error sources.
9
10mod replication;
11pub use replication::ReplicationStreamErrorV3;
12
13use std::error::Error;
14use std::fmt;
15
16use riffdb_types::{
17    ContractLineage, ContractVersion, ExecutionFailureCode, FieldId, IncidentId, RequestId,
18};
19
20/// Version of the bounded application-semantic error envelope.
21pub const APPLICATION_ERROR_ENVELOPE_VERSION: u32 = 1;
22/// Maximum encoded application-error details accepted at a public boundary.
23pub const MAX_APPLICATION_ERROR_BYTES: usize = 16 * 1024;
24/// Maximum number of symbolic path segments in an application error.
25pub const MAX_APPLICATION_SYMBOL_PATH_SEGMENTS: usize = 16;
26/// Maximum bytes in one application operation or path symbol.
27pub const MAX_APPLICATION_SYMBOL_BYTES: usize = 256;
28/// Maximum number of closed remediation codes in one application error.
29pub const MAX_APPLICATION_FIXES: usize = 8;
30/// Maximum RiffQL source offset retained in a public error.
31pub const MAX_APPLICATION_SOURCE_OFFSET: u64 = 262_144;
32
33/// Closed application operation inventory used only for public-safe context.
34#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
35pub enum ApplicationOperation {
36    /// Describe a symbolic contract catalog.
37    DescribeContract,
38    /// Check RiffQL source.
39    CheckQuery,
40    /// Explain ad-hoc or named RiffQL.
41    ExplainQuery,
42    /// Execute ad-hoc or named RiffQL.
43    ExecuteQuery,
44    /// Compile and deploy a named-query module.
45    DeployQueryModule,
46    /// Compile and publish an immutable reactive module.
47    DeployReactiveModule,
48    /// Inspect a named-query module.
49    GetQueryModule,
50    /// Execute a symbolic command.
51    ExecuteCommand,
52    /// Execute a bounded command batch.
53    BatchCommand,
54    /// Execute one projected columnar query under a freshness policy.
55    ExecuteProjectedQuery,
56    /// Inspect authoritative vector evidence through symbolic contract names.
57    InspectVectorState,
58}
59
60impl ApplicationOperation {
61    /// Returns the stable application-facing operation name.
62    #[must_use]
63    pub const fn as_str(self) -> &'static str {
64        match self {
65            Self::DescribeContract => "DescribeContract",
66            Self::CheckQuery => "CheckQuery",
67            Self::ExplainQuery => "ExplainQuery",
68            Self::ExecuteQuery => "ExecuteQuery",
69            Self::DeployQueryModule => "DeployQueryModule",
70            Self::DeployReactiveModule => "DeployReactiveModule",
71            Self::GetQueryModule => "GetQueryModule",
72            Self::ExecuteCommand => "ExecuteCommand",
73            Self::BatchCommand => "BatchCommand",
74            Self::ExecuteProjectedQuery => "ExecuteProjectedQuery",
75            Self::InspectVectorState => "InspectVectorState",
76        }
77    }
78}
79
80/// Closed stable code registry for application-semantic failures.
81#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
82pub enum ApplicationErrorCode {
83    /// Public message bytes failed structural validation.
84    InvalidRequest,
85    /// A typed application input failed validation.
86    InputInvalid,
87    /// Current policy denied the operation.
88    AuthorizationDenied,
89    /// Contract identity is absent, stale, or does not match.
90    ContractMismatch,
91    /// RiffQL could not be parsed, resolved, typed, or planned.
92    QueryInvalid,
93    /// A checked query cannot currently be loaded or executed.
94    QueryUnavailable,
95    /// The requested named-query module is absent or stale.
96    ModuleUnavailable,
97    /// A continuation cursor is invalid or stale.
98    CursorInvalid,
99    /// One complete authorized result exceeds a public bound.
100    ResponseTooLarge,
101    /// Authoritative storage is temporarily unavailable.
102    StorageUnavailable,
103    /// A durable mutation may have completed.
104    OutcomeUnknown,
105    /// The request was cancelled at a safe point.
106    OperationCancelled,
107    /// The request deadline elapsed.
108    DeadlineExceeded,
109    /// A redacted internal defect occurred.
110    InternalDefect,
111    /// An idempotency identity was reused with different input.
112    IdempotencyKeyReuse,
113    /// Deterministic command execution failed.
114    CommandExecutionFailed,
115    /// A formerly valid capability is revoked.
116    CapabilityRevoked,
117    /// A public peer violated the application protocol.
118    ProtocolInvalid,
119    /// Observed history predates a database restore.
120    HistoryIncarnationMismatch,
121    /// Requested history was retired by retention prune.
122    HistoryPruned,
123    /// The service is over capacity and rejected admission.
124    Overloaded,
125    /// Exact result-set providers cannot prove one common epoch.
126    ProjectionDiverged,
127    /// The exact snapshot bound to the request is no longer retained.
128    SnapshotRetired,
129    /// No servable exact snapshot satisfies the requested freshness floor.
130    FreshnessUnsatisfied,
131    /// A retained nearest plan predates the required compiler-owned source declaration.
132    ProjectedSourceRequired,
133    /// This node is a follower and cannot execute a write operation.
134    FollowerMode,
135    /// Durable source fencing permanently refuses new command admission.
136    PrimaryFenced,
137}
138
139/// Complete v1 application error code registry in stable wire order.
140pub const APPLICATION_ERROR_CODES: [ApplicationErrorCode; 27] = [
141    ApplicationErrorCode::InvalidRequest,
142    ApplicationErrorCode::InputInvalid,
143    ApplicationErrorCode::AuthorizationDenied,
144    ApplicationErrorCode::ContractMismatch,
145    ApplicationErrorCode::QueryInvalid,
146    ApplicationErrorCode::QueryUnavailable,
147    ApplicationErrorCode::ModuleUnavailable,
148    ApplicationErrorCode::CursorInvalid,
149    ApplicationErrorCode::ResponseTooLarge,
150    ApplicationErrorCode::StorageUnavailable,
151    ApplicationErrorCode::OutcomeUnknown,
152    ApplicationErrorCode::OperationCancelled,
153    ApplicationErrorCode::DeadlineExceeded,
154    ApplicationErrorCode::InternalDefect,
155    ApplicationErrorCode::IdempotencyKeyReuse,
156    ApplicationErrorCode::CommandExecutionFailed,
157    ApplicationErrorCode::CapabilityRevoked,
158    ApplicationErrorCode::ProtocolInvalid,
159    ApplicationErrorCode::HistoryIncarnationMismatch,
160    ApplicationErrorCode::HistoryPruned,
161    ApplicationErrorCode::Overloaded,
162    ApplicationErrorCode::ProjectionDiverged,
163    ApplicationErrorCode::SnapshotRetired,
164    ApplicationErrorCode::FreshnessUnsatisfied,
165    ApplicationErrorCode::ProjectedSourceRequired,
166    ApplicationErrorCode::FollowerMode,
167    ApplicationErrorCode::PrimaryFenced,
168];
169
170impl ApplicationErrorCode {
171    /// Maps the compatible kernel classification into the application registry.
172    #[must_use]
173    pub const fn from_public_kind(kind: PublicErrorKind) -> Self {
174        match kind {
175            PublicErrorKind::Validation => Self::InputInvalid,
176            PublicErrorKind::IdempotencyKeyReuse => Self::IdempotencyKeyReuse,
177            PublicErrorKind::AuthorizationDenied => Self::AuthorizationDenied,
178            PublicErrorKind::ConcurrencyDeadlineExceeded => Self::DeadlineExceeded,
179            PublicErrorKind::ContractMismatch => Self::ContractMismatch,
180            PublicErrorKind::StorageUnavailable => Self::StorageUnavailable,
181            PublicErrorKind::OutcomeUnknown => Self::OutcomeUnknown,
182            PublicErrorKind::InternalDefect => Self::InternalDefect,
183            PublicErrorKind::CommandExecutionFailed => Self::CommandExecutionFailed,
184            PublicErrorKind::HistoryIncarnationMismatch => Self::HistoryIncarnationMismatch,
185            PublicErrorKind::HistoryPruned => Self::HistoryPruned,
186            PublicErrorKind::Overloaded => Self::Overloaded,
187            PublicErrorKind::FollowerMode => Self::FollowerMode,
188            PublicErrorKind::PrimaryFenced => Self::PrimaryFenced,
189        }
190    }
191
192    /// Returns the stable externally documented code.
193    #[must_use]
194    pub const fn as_str(self) -> &'static str {
195        match self {
196            Self::InvalidRequest => "RDB-APP-0001",
197            Self::InputInvalid => "RDB-INPUT-0101",
198            Self::AuthorizationDenied => "RDB-AUTH-0214",
199            Self::ContractMismatch => "RDB-CONTRACT-0101",
200            Self::QueryInvalid => "RDB-QUERY-0101",
201            Self::QueryUnavailable => "RDB-QUERY-0102",
202            Self::ModuleUnavailable => "RDB-MODULE-0101",
203            Self::CursorInvalid => "RDB-CURSOR-0101",
204            Self::ResponseTooLarge => "RDB-RESOURCE-0101",
205            Self::StorageUnavailable => "RDB-STORAGE-0101",
206            Self::OutcomeUnknown => "RDB-UNCERTAIN-0101",
207            Self::OperationCancelled => "RDB-APP-0002",
208            Self::DeadlineExceeded => "RDB-APP-0003",
209            Self::InternalDefect => "RDB-INTERNAL-0001",
210            Self::IdempotencyKeyReuse => "RDB-COMMAND-0101",
211            Self::CommandExecutionFailed => "RDB-COMMAND-0102",
212            Self::CapabilityRevoked => "RDB-AUTH-0215",
213            Self::ProtocolInvalid => "RDB-PROTOCOL-0101",
214            Self::HistoryIncarnationMismatch => "RDB-HISTORY-0101",
215            Self::HistoryPruned => "RDB-HISTORY-0102",
216            Self::Overloaded => "RDB-CAPACITY-0101",
217            Self::ProjectionDiverged => "RDB-PROJECTION-0101",
218            Self::SnapshotRetired => "RDB-PROJECTION-0102",
219            Self::FreshnessUnsatisfied => "RDB-PROJECTION-0103",
220            Self::ProjectedSourceRequired => "RDB-PROJECTION-0104",
221            Self::FollowerMode => "RDB-REP-0101",
222            Self::PrimaryFenced => "RDB-REP-0102",
223        }
224    }
225
226    /// Returns the one registry-owned public message for this code.
227    #[must_use]
228    pub const fn safe_message(self) -> &'static str {
229        match self {
230            Self::InvalidRequest => "application request is structurally invalid",
231            Self::InputInvalid => "application input is invalid",
232            Self::AuthorizationDenied => "application operation is not authorized",
233            Self::ContractMismatch => "application contract does not match",
234            Self::QueryInvalid => "RiffQL query is invalid",
235            Self::QueryUnavailable => "RiffQL query is unavailable",
236            Self::ModuleUnavailable => "query module is unavailable",
237            Self::CursorInvalid => "query cursor is invalid or stale",
238            Self::ResponseTooLarge => "application result exceeds the service limit",
239            Self::StorageUnavailable => "storage is temporarily unavailable",
240            Self::OutcomeUnknown => "command outcome is not yet known",
241            Self::OperationCancelled => "application request was cancelled",
242            Self::DeadlineExceeded => "application request deadline elapsed",
243            Self::InternalDefect => "an internal error occurred",
244            Self::IdempotencyKeyReuse => {
245                "idempotency key was reused with different application input"
246            }
247            Self::CommandExecutionFailed => "command execution failed",
248            Self::CapabilityRevoked => "application capability is revoked",
249            Self::ProtocolInvalid => "the RiffDB peer returned an invalid application response",
250            Self::HistoryIncarnationMismatch => "observed history predates a database restore",
251            Self::HistoryPruned => "requested history has been pruned",
252            Self::Overloaded => "service is over capacity",
253            Self::FollowerMode => "operation is unavailable in follower mode",
254            Self::PrimaryFenced => "primary is durably fenced",
255            Self::ProjectionDiverged => "query projections cannot prove one common snapshot",
256            Self::SnapshotRetired => "the requested query snapshot has been retired",
257            Self::FreshnessUnsatisfied => "no query snapshot satisfies the requested freshness",
258            Self::ProjectedSourceRequired => {
259                "nearest query requires a compiler-owned projected source"
260            }
261        }
262    }
263
264    /// Returns the closed category.
265    #[must_use]
266    pub const fn category(self) -> ApplicationErrorCategory {
267        match self {
268            Self::InvalidRequest | Self::InputInvalid => ApplicationErrorCategory::Input,
269            Self::AuthorizationDenied | Self::CapabilityRevoked => {
270                ApplicationErrorCategory::Authorization
271            }
272            Self::ContractMismatch => ApplicationErrorCategory::Contract,
273            Self::QueryInvalid
274            | Self::QueryUnavailable
275            | Self::ProjectionDiverged
276            | Self::SnapshotRetired
277            | Self::FreshnessUnsatisfied => ApplicationErrorCategory::Query,
278            Self::ProjectedSourceRequired => ApplicationErrorCategory::Query,
279            Self::ModuleUnavailable => ApplicationErrorCategory::Module,
280            Self::CursorInvalid => ApplicationErrorCategory::Cursor,
281            Self::ResponseTooLarge => ApplicationErrorCategory::Resource,
282            Self::StorageUnavailable => ApplicationErrorCategory::Storage,
283            Self::OutcomeUnknown => ApplicationErrorCategory::Uncertainty,
284            Self::OperationCancelled
285            | Self::DeadlineExceeded
286            | Self::FollowerMode
287            | Self::PrimaryFenced => ApplicationErrorCategory::Control,
288            Self::InternalDefect => ApplicationErrorCategory::Internal,
289            Self::IdempotencyKeyReuse | Self::CommandExecutionFailed => {
290                ApplicationErrorCategory::Command
291            }
292            Self::ProtocolInvalid => ApplicationErrorCategory::Protocol,
293            Self::HistoryIncarnationMismatch | Self::HistoryPruned => {
294                ApplicationErrorCategory::History
295            }
296            Self::Overloaded => ApplicationErrorCategory::Capacity,
297        }
298    }
299
300    /// Returns deterministic recovery guidance.
301    #[must_use]
302    pub const fn recovery_action(self) -> ApplicationRecoveryAction {
303        match self {
304            Self::InvalidRequest
305            | Self::InputInvalid
306            | Self::QueryInvalid
307            | Self::CursorInvalid
308            | Self::ResponseTooLarge
309            | Self::IdempotencyKeyReuse
310            | Self::HistoryIncarnationMismatch
311            | Self::HistoryPruned
312            | Self::SnapshotRetired
313            | Self::FollowerMode => ApplicationRecoveryAction::CorrectRequest,
314            Self::ProjectedSourceRequired => ApplicationRecoveryAction::RefreshContract,
315            Self::AuthorizationDenied | Self::CapabilityRevoked => {
316                ApplicationRecoveryAction::ObtainPermission
317            }
318            Self::ContractMismatch | Self::QueryUnavailable | Self::ModuleUnavailable => {
319                ApplicationRecoveryAction::RefreshContract
320            }
321            Self::StorageUnavailable
322            | Self::DeadlineExceeded
323            | Self::Overloaded
324            | Self::ProjectionDiverged
325            | Self::FreshnessUnsatisfied => ApplicationRecoveryAction::Retry,
326            Self::OutcomeUnknown => ApplicationRecoveryAction::ResolveWithSameIdempotencyKey,
327            Self::OperationCancelled => ApplicationRecoveryAction::None,
328            Self::InternalDefect
329            | Self::CommandExecutionFailed
330            | Self::ProtocolInvalid
331            | Self::PrimaryFenced => ApplicationRecoveryAction::ContactOperator,
332        }
333    }
334
335    /// Returns the exact deterministic remediation set.
336    #[must_use]
337    pub const fn fixes(self) -> &'static [ApplicationFixCode] {
338        match self {
339            Self::InvalidRequest
340            | Self::InputInvalid
341            | Self::QueryInvalid
342            | Self::HistoryIncarnationMismatch
343            | Self::HistoryPruned => &[ApplicationFixCode::CorrectInput],
344            Self::AuthorizationDenied | Self::CapabilityRevoked => {
345                &[ApplicationFixCode::BindApplicationRole]
346            }
347            Self::ContractMismatch => &[ApplicationFixCode::RefreshContract],
348            Self::QueryUnavailable | Self::ModuleUnavailable => {
349                &[ApplicationFixCode::PinActiveModule]
350            }
351            Self::ProjectedSourceRequired => &[ApplicationFixCode::PinActiveModule],
352            Self::CursorInvalid => &[ApplicationFixCode::RestartFromFirstPage],
353            Self::SnapshotRetired => &[ApplicationFixCode::RestartFromFirstPage],
354            Self::ResponseTooLarge | Self::IdempotencyKeyReuse => {
355                &[ApplicationFixCode::CorrectInput]
356            }
357            Self::StorageUnavailable
358            | Self::DeadlineExceeded
359            | Self::Overloaded
360            | Self::ProjectionDiverged
361            | Self::FreshnessUnsatisfied => &[ApplicationFixCode::RetryLater],
362            Self::OutcomeUnknown => &[ApplicationFixCode::ResolveWithSameIdempotencyKey],
363            Self::InternalDefect => &[ApplicationFixCode::ContactOperatorWithIncident],
364            Self::OperationCancelled
365            | Self::CommandExecutionFailed
366            | Self::ProtocolInvalid
367            | Self::FollowerMode
368            | Self::PrimaryFenced => &[],
369        }
370    }
371}
372
373/// Coarse application failure category.
374#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
375pub enum ApplicationErrorCategory {
376    /// Caller-controlled application input.
377    Input,
378    /// Authentication, role, or authorization policy.
379    Authorization,
380    /// Contract selection and compatibility.
381    Contract,
382    /// RiffQL compilation or execution.
383    Query,
384    /// Named-query module selection.
385    Module,
386    /// Cursor validation.
387    Cursor,
388    /// Public resource bounds.
389    Resource,
390    /// Authoritative storage availability.
391    Storage,
392    /// Mutation uncertainty.
393    Uncertainty,
394    /// Cancellation, deadline, or redacted defects.
395    Internal,
396    /// Peer protocol conformance.
397    Protocol,
398    /// Symbolic command admission or execution.
399    Command,
400    /// Cancellation and deadline control.
401    Control,
402    /// History incarnation observations.
403    History,
404    /// Admission capacity.
405    Capacity,
406}
407
408impl ApplicationErrorCategory {
409    /// Returns the stable machine name.
410    #[must_use]
411    pub const fn as_str(self) -> &'static str {
412        match self {
413            Self::Input => "input",
414            Self::Authorization => "authorization",
415            Self::Contract => "contract",
416            Self::Query => "query",
417            Self::Module => "module",
418            Self::Cursor => "cursor",
419            Self::Resource => "resource",
420            Self::Storage => "storage",
421            Self::Uncertainty => "uncertainty",
422            Self::Internal => "internal",
423            Self::Protocol => "protocol",
424            Self::Command => "command",
425            Self::Control => "control",
426            Self::History => "history",
427            Self::Capacity => "capacity",
428        }
429    }
430}
431
432/// Closed application recovery action.
433#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
434pub enum ApplicationRecoveryAction {
435    /// Correct the input before retrying.
436    CorrectRequest,
437    /// Retry according to caller policy.
438    Retry,
439    /// Resolve using the same idempotency identity.
440    ResolveWithSameIdempotencyKey,
441    /// Bind or obtain an application role.
442    ObtainPermission,
443    /// Refresh contract and module metadata.
444    RefreshContract,
445    /// Escalate with the incident identifier when present.
446    ContactOperator,
447    /// No recovery is prescribed.
448    None,
449}
450
451impl ApplicationRecoveryAction {
452    /// Returns the stable machine name.
453    #[must_use]
454    pub const fn as_str(self) -> &'static str {
455        match self {
456            Self::CorrectRequest => "correct_request",
457            Self::Retry => "retry",
458            Self::ResolveWithSameIdempotencyKey => "resolve_with_same_idempotency_key",
459            Self::ObtainPermission => "obtain_permission",
460            Self::RefreshContract => "refresh_contract",
461            Self::ContactOperator => "contact_operator",
462            Self::None => "none",
463        }
464    }
465}
466
467/// Closed machine-actionable remediation.
468#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
469pub enum ApplicationFixCode {
470    /// Correct the typed input or query source.
471    CorrectInput,
472    /// Remove a result field not visible to the application role.
473    RemoveForbiddenOutput,
474    /// Bind the symbolic application role.
475    BindApplicationRole,
476    /// Refresh exact contract metadata.
477    RefreshContract,
478    /// Pin or redeploy the active named-query module.
479    PinActiveModule,
480    /// Declare a bounded index that satisfies the diagnostic.
481    AddBoundedIndex,
482    /// Discard a stale cursor and restart.
483    RestartFromFirstPage,
484    /// Retry later using bounded caller policy.
485    RetryLater,
486    /// Resolve or retry with the exact same idempotency identity.
487    ResolveWithSameIdempotencyKey,
488    /// Contact an operator and include only the opaque incident ID.
489    ContactOperatorWithIncident,
490}
491
492impl ApplicationFixCode {
493    /// Returns the stable machine name.
494    #[must_use]
495    pub const fn as_str(self) -> &'static str {
496        match self {
497            Self::CorrectInput => "correct_input",
498            Self::RemoveForbiddenOutput => "remove_forbidden_output",
499            Self::BindApplicationRole => "bind_application_role",
500            Self::RefreshContract => "refresh_contract",
501            Self::PinActiveModule => "pin_active_module",
502            Self::AddBoundedIndex => "add_bounded_index",
503            Self::RestartFromFirstPage => "restart_from_first_page",
504            Self::RetryLater => "retry_later",
505            Self::ResolveWithSameIdempotencyKey => "resolve_with_same_idempotency_key",
506            Self::ContactOperatorWithIncident => "contact_operator_with_incident",
507        }
508    }
509}
510
511/// Checked half-open source span in submitted RiffQL.
512#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
513pub struct ApplicationSourceSpan {
514    start: u64,
515    end: u64,
516}
517
518impl ApplicationSourceSpan {
519    /// Constructs a bounded ordered source span.
520    pub const fn new(start: u64, end: u64) -> Option<Self> {
521        if start <= end && end <= MAX_APPLICATION_SOURCE_OFFSET {
522            Some(Self { start, end })
523        } else {
524            None
525        }
526    }
527
528    /// Inclusive start byte offset.
529    #[must_use]
530    pub const fn start(self) -> u64 {
531        self.start
532    }
533
534    /// Exclusive end byte offset.
535    #[must_use]
536    pub const fn end(self) -> u64 {
537        self.end
538    }
539}
540
541/// Bounded symbolic context for one application failure.
542#[derive(Clone, Debug, Default, Eq, PartialEq)]
543pub struct ApplicationErrorContext {
544    contract: Option<(ContractLineage, ContractVersion)>,
545    operation_symbol: Option<String>,
546    symbol_path: Vec<String>,
547    source_span: Option<ApplicationSourceSpan>,
548    trace_id: Option<RequestId>,
549}
550
551impl ApplicationErrorContext {
552    /// Constructs empty context for a failure that has no honest symbol.
553    #[must_use]
554    pub const fn empty() -> Self {
555        Self {
556            contract: None,
557            operation_symbol: None,
558            symbol_path: Vec::new(),
559            source_span: None,
560            trace_id: None,
561        }
562    }
563
564    /// Adds exact contract identity already visible to the caller.
565    #[must_use]
566    pub fn with_contract(mut self, lineage: ContractLineage, version: ContractVersion) -> Self {
567        self.contract = Some((lineage, version));
568        self
569    }
570
571    /// Adds one checked caller-visible query, module, command, or role symbol.
572    pub fn with_operation_symbol(
573        mut self,
574        symbol: String,
575    ) -> Result<Self, ApplicationErrorContextError> {
576        validate_application_symbol(&symbol)?;
577        self.operation_symbol = Some(symbol);
578        Ok(self)
579    }
580
581    /// Adds a bounded checked caller-visible symbol path.
582    pub fn with_symbol_path(
583        mut self,
584        symbols: Vec<String>,
585    ) -> Result<Self, ApplicationErrorContextError> {
586        if symbols.len() > MAX_APPLICATION_SYMBOL_PATH_SEGMENTS {
587            return Err(ApplicationErrorContextError);
588        }
589        for symbol in &symbols {
590            validate_application_symbol(symbol)?;
591        }
592        self.symbol_path = symbols;
593        Ok(self)
594    }
595
596    /// Adds a checked source span.
597    #[must_use]
598    pub const fn with_source_span(mut self, source_span: ApplicationSourceSpan) -> Self {
599        self.source_span = Some(source_span);
600        self
601    }
602
603    /// Adds the request identity as the safe end-to-end trace identity.
604    #[must_use]
605    pub const fn with_trace_id(mut self, trace_id: RequestId) -> Self {
606        self.trace_id = Some(trace_id);
607        self
608    }
609
610    /// Exact selected contract, when safely known.
611    #[must_use]
612    pub const fn contract(&self) -> Option<&(ContractLineage, ContractVersion)> {
613        self.contract.as_ref()
614    }
615
616    /// Caller-visible operation symbol, when safely known.
617    #[must_use]
618    pub fn operation_symbol(&self) -> Option<&str> {
619        self.operation_symbol.as_deref()
620    }
621
622    /// Caller-visible symbolic path.
623    #[must_use]
624    pub fn symbol_path(&self) -> &[String] {
625        &self.symbol_path
626    }
627
628    /// Source span, when safely known.
629    #[must_use]
630    pub const fn source_span(&self) -> Option<ApplicationSourceSpan> {
631        self.source_span
632    }
633
634    /// Opaque request/trace identity.
635    #[must_use]
636    pub const fn trace_id(&self) -> Option<RequestId> {
637        self.trace_id
638    }
639}
640
641fn validate_application_symbol(symbol: &str) -> Result<(), ApplicationErrorContextError> {
642    if symbol.is_empty()
643        || symbol.len() > MAX_APPLICATION_SYMBOL_BYTES
644        || !symbol
645            .bytes()
646            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
647    {
648        return Err(ApplicationErrorContextError);
649    }
650    Ok(())
651}
652
653/// A symbolic context rejected before it could reach a public error.
654#[derive(Clone, Copy, Debug, Eq, PartialEq)]
655pub struct ApplicationErrorContextError;
656
657impl fmt::Display for ApplicationErrorContextError {
658    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
659        formatter.write_str("application error context is invalid")
660    }
661}
662
663impl Error for ApplicationErrorContextError {}
664
665/// One fully checked bounded application-semantic failure.
666#[derive(Clone, Debug, Eq, PartialEq)]
667pub struct ApplicationError {
668    code: ApplicationErrorCode,
669    operation: ApplicationOperation,
670    context: ApplicationErrorContext,
671    incident_id: Option<IncidentId>,
672}
673
674impl ApplicationError {
675    /// Constructs one registry-owned error with already checked context.
676    #[must_use]
677    pub const fn new(
678        code: ApplicationErrorCode,
679        operation: ApplicationOperation,
680        context: ApplicationErrorContext,
681        incident_id: Option<IncidentId>,
682    ) -> Self {
683        Self {
684            code,
685            operation,
686            context,
687            incident_id,
688        }
689    }
690
691    /// Lifts one compatible kernel failure into symbolic application context.
692    ///
693    /// Kernel validation paths are intentionally not copied because they carry
694    /// compiler IDs rather than application symbols.
695    #[must_use]
696    pub fn from_public_error(
697        error: &PublicError,
698        operation: ApplicationOperation,
699        context: ApplicationErrorContext,
700    ) -> Self {
701        Self::new(
702            error
703                .application_code_hint()
704                .unwrap_or_else(|| ApplicationErrorCode::from_public_kind(error.kind())),
705            operation,
706            context,
707            error.incident_id().copied(),
708        )
709    }
710
711    /// Stable code.
712    #[must_use]
713    pub const fn code(&self) -> ApplicationErrorCode {
714        self.code
715    }
716
717    /// Application operation.
718    #[must_use]
719    pub const fn operation(&self) -> ApplicationOperation {
720        self.operation
721    }
722
723    /// Checked safe context.
724    #[must_use]
725    pub const fn context(&self) -> &ApplicationErrorContext {
726        &self.context
727    }
728
729    /// Opaque incident identity.
730    #[must_use]
731    pub const fn incident_id(&self) -> Option<&IncidentId> {
732        self.incident_id.as_ref()
733    }
734
735    /// Closed category.
736    #[must_use]
737    pub const fn category(&self) -> ApplicationErrorCategory {
738        self.code.category()
739    }
740
741    /// Stable public message.
742    #[must_use]
743    pub const fn safe_message(&self) -> &'static str {
744        self.code.safe_message()
745    }
746
747    /// Recovery action.
748    #[must_use]
749    pub const fn recovery_action(&self) -> ApplicationRecoveryAction {
750        self.code.recovery_action()
751    }
752
753    /// Deterministic remediation codes.
754    #[must_use]
755    pub const fn fixes(&self) -> &'static [ApplicationFixCode] {
756        self.code.fixes()
757    }
758}
759
760impl fmt::Display for ApplicationError {
761    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
762        write!(
763            formatter,
764            "{}: {} [{}]",
765            self.code.as_str(),
766            self.safe_message(),
767            self.operation.as_str()
768        )?;
769        if let Some(symbol) = self.context.operation_symbol() {
770            write!(formatter, " {symbol}")?;
771        }
772        if let Some(incident_id) = &self.incident_id {
773            write!(formatter, " (incident {incident_id})")?;
774        }
775        Ok(())
776    }
777}
778
779impl Error for ApplicationError {}
780
781/// Consumer-owned source for fresh opaque incident identifiers.
782///
783/// Production providers live at the server composition boundary. Core error
784/// handling receives only this synchronous port and must fail closed when it
785/// cannot obtain an identifier.
786pub trait IncidentIdSource: Send + Sync {
787    /// Returns one fresh checked UUIDv7 incident identifier.
788    fn next_incident_id(&self) -> Result<IncidentId, IncidentIdSourceError>;
789}
790
791/// A bounded, public-safe failure to obtain a fresh incident identifier.
792#[derive(Clone, Copy, Debug, Eq, PartialEq)]
793pub struct IncidentIdSourceError;
794
795impl fmt::Display for IncidentIdSourceError {
796    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
797        formatter.write_str("incident identifier source failed")
798    }
799}
800
801impl Error for IncidentIdSourceError {}
802
803/// The maximum number of validation issues returned for one request.
804pub const MAX_VALIDATION_ISSUES: usize = 16;
805
806/// The maximum number of segments in one validation path.
807pub const MAX_VALIDATION_PATH_SEGMENTS: usize = 16;
808
809/// A protocol-neutral classification for a public failure.
810#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
811pub enum ErrorClass {
812    /// The submitted request is invalid and must be corrected.
813    InvalidArgument,
814    /// The request conflicts with previously admitted state.
815    Conflict,
816    /// The principal is not permitted to perform the operation.
817    PermissionDenied,
818    /// The operation did not complete before its deadline.
819    DeadlineExceeded,
820    /// The request targets a contract state that is no longer current.
821    FailedPrecondition,
822    /// A required service is temporarily unavailable.
823    Unavailable,
824    /// The caller must resolve an uncertain command result.
825    Uncertain,
826    /// The server encountered a defect that is not safe to disclose.
827    Internal,
828}
829
830/// Safe, protocol-neutral recovery guidance for a public failure.
831#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
832pub enum RecoveryAction {
833    /// Correct the request before submitting it again.
834    CorrectRequest,
835    /// Retry the operation according to the caller's retry policy.
836    Retry,
837    /// Resolve or retry the command with the original idempotency key.
838    ResolveWithSameIdempotencyKey,
839    /// Obtain an appropriate permission before retrying.
840    ObtainPermission,
841    /// Refresh contract metadata and rebuild the request.
842    RefreshContract,
843    /// Escalate the incident to an operator.
844    ContactOperator,
845}
846
847/// Closed emergency failure when an internal incident cannot be identified.
848///
849/// This value is intentionally distinct from [`PublicError`]: an internal
850/// public error requires a real fresh [`IncidentId`]. It carries no identifier,
851/// arbitrary text, diagnostic source, or protected operation result.
852///
853/// ```compile_fail
854/// use riffdb_errors::{EmergencyInternalFailure, IncidentIdSourceError, PublicError};
855///
856/// let emergency = EmergencyInternalFailure::from(IncidentIdSourceError);
857/// let _: PublicError = emergency.into();
858/// ```
859#[derive(Clone, Copy, Eq, PartialEq)]
860pub struct EmergencyInternalFailure {
861    _private: (),
862}
863
864impl EmergencyInternalFailure {
865    /// Returns the only fixed message safe at an emergency transport boundary.
866    #[must_use]
867    pub const fn safe_message(self) -> &'static str {
868        "an internal error occurred"
869    }
870
871    /// Returns the protocol-neutral emergency classification.
872    #[must_use]
873    pub const fn class(self) -> ErrorClass {
874        ErrorClass::Internal
875    }
876
877    /// Returns the only safe recovery guidance for this readiness failure.
878    #[must_use]
879    pub const fn recovery_action(self) -> RecoveryAction {
880        RecoveryAction::ContactOperator
881    }
882}
883
884impl From<IncidentIdSourceError> for EmergencyInternalFailure {
885    fn from(_: IncidentIdSourceError) -> Self {
886        Self { _private: () }
887    }
888}
889
890impl fmt::Display for EmergencyInternalFailure {
891    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
892        formatter.write_str(self.safe_message())
893    }
894}
895
896impl fmt::Debug for EmergencyInternalFailure {
897    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
898        formatter.write_str("EmergencyInternalFailure")
899    }
900}
901
902impl Error for EmergencyInternalFailure {}
903
904/// The closed set of failures that RiffDB may expose to an untrusted caller.
905#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
906pub enum PublicErrorKind {
907    /// A request failed bounded validation.
908    Validation,
909    /// An idempotency identity was reused with different canonical input.
910    IdempotencyKeyReuse,
911    /// Authorization denied the operation.
912    AuthorizationDenied,
913    /// Logical conflict acquisition exceeded its deadline, or the fixed v1
914    /// command reevaluation budget was exhausted.
915    ConcurrencyDeadlineExceeded,
916    /// The requested contract version or plan is not current.
917    ContractMismatch,
918    /// Authoritative storage is temporarily unavailable.
919    StorageUnavailable,
920    /// A submitted command may have completed, but its outcome is not known.
921    OutcomeUnknown,
922    /// An internal defect occurred and details were retained only internally.
923    InternalDefect,
924    /// Deterministic evaluation failed before an application commit was formed.
925    CommandExecutionFailed,
926    /// Observed history predates a database restore.
927    HistoryIncarnationMismatch,
928    /// Requested history was retired by retention prune.
929    HistoryPruned,
930    /// The service is over capacity and rejected admission.
931    Overloaded,
932    /// This node is a follower and cannot execute a write operation.
933    FollowerMode,
934    /// Durable source fencing permanently refuses new command admission.
935    PrimaryFenced,
936}
937
938impl PublicErrorKind {
939    /// Returns the stable ASCII identifier exposed by every transport.
940    #[must_use]
941    pub const fn code(self) -> &'static str {
942        match self {
943            Self::Validation => "validation_failed",
944            Self::IdempotencyKeyReuse => "idempotency_key_reuse",
945            Self::AuthorizationDenied => "authorization_denied",
946            Self::ConcurrencyDeadlineExceeded => "concurrency_deadline_exceeded",
947            Self::ContractMismatch => "contract_mismatch",
948            Self::StorageUnavailable => "storage_unavailable",
949            Self::OutcomeUnknown => "outcome_unknown",
950            Self::InternalDefect => "internal_defect",
951            Self::CommandExecutionFailed => "command_execution_failed",
952            Self::HistoryIncarnationMismatch => "history_incarnation_mismatch",
953            Self::HistoryPruned => "history_pruned",
954            Self::Overloaded => "overloaded",
955            Self::FollowerMode => "follower_mode",
956            Self::PrimaryFenced => "primary_fenced",
957        }
958    }
959
960    /// Returns the stable, caller-safe ASCII message for this failure.
961    #[must_use]
962    pub const fn safe_message(self) -> &'static str {
963        match self {
964            Self::Validation => "request validation failed",
965            Self::IdempotencyKeyReuse => "idempotency key was reused with different input",
966            Self::AuthorizationDenied => "operation is not authorized",
967            Self::ConcurrencyDeadlineExceeded => "concurrency deadline exceeded",
968            Self::ContractMismatch => "contract version or plan does not match",
969            Self::StorageUnavailable => "storage is temporarily unavailable",
970            Self::OutcomeUnknown => "command outcome is not yet known",
971            Self::InternalDefect => "an internal error occurred",
972            Self::CommandExecutionFailed => "command execution failed",
973            Self::HistoryIncarnationMismatch => "observed history predates a database restore",
974            Self::HistoryPruned => "requested history has been pruned",
975            Self::Overloaded => "service is over capacity",
976            Self::FollowerMode => "operation is unavailable in follower mode",
977            Self::PrimaryFenced => "primary is durably fenced",
978        }
979    }
980
981    /// Returns the protocol-neutral class for this failure.
982    #[must_use]
983    pub const fn class(self) -> ErrorClass {
984        match self {
985            Self::Validation => ErrorClass::InvalidArgument,
986            Self::IdempotencyKeyReuse => ErrorClass::Conflict,
987            Self::AuthorizationDenied => ErrorClass::PermissionDenied,
988            Self::ConcurrencyDeadlineExceeded => ErrorClass::DeadlineExceeded,
989            Self::ContractMismatch
990            | Self::HistoryIncarnationMismatch
991            | Self::HistoryPruned
992            | Self::FollowerMode
993            | Self::PrimaryFenced => ErrorClass::FailedPrecondition,
994            Self::StorageUnavailable | Self::Overloaded => ErrorClass::Unavailable,
995            Self::OutcomeUnknown => ErrorClass::Uncertain,
996            Self::InternalDefect => ErrorClass::Internal,
997            Self::CommandExecutionFailed => ErrorClass::FailedPrecondition,
998        }
999    }
1000
1001    /// Returns the one authoritative public status code for this kind.
1002    ///
1003    /// Server encoding and client validation must both use this mapping. Do not
1004    /// re-derive the wire code from [`Self::class`] alone: some kinds deliberately
1005    /// diverge (for example, overload keeps class [`ErrorClass::Unavailable`] while
1006    /// carrying [`PublicErrorStatusCode::ResourceExhausted`]).
1007    ///
1008    /// This match is the sole wire-code authority and must remain exhaustive over
1009    /// every [`PublicErrorKind`] variant.
1010    #[must_use]
1011    pub const fn status_code(self) -> PublicErrorStatusCode {
1012        match self {
1013            Self::Validation => PublicErrorStatusCode::InvalidArgument,
1014            Self::IdempotencyKeyReuse => PublicErrorStatusCode::AlreadyExists,
1015            Self::AuthorizationDenied => PublicErrorStatusCode::PermissionDenied,
1016            Self::ConcurrencyDeadlineExceeded => PublicErrorStatusCode::DeadlineExceeded,
1017            Self::ContractMismatch
1018            | Self::CommandExecutionFailed
1019            | Self::HistoryIncarnationMismatch
1020            | Self::HistoryPruned
1021            | Self::FollowerMode
1022            | Self::PrimaryFenced => PublicErrorStatusCode::FailedPrecondition,
1023            Self::StorageUnavailable => PublicErrorStatusCode::Unavailable,
1024            Self::Overloaded => PublicErrorStatusCode::ResourceExhausted,
1025            Self::OutcomeUnknown => PublicErrorStatusCode::Unknown,
1026            Self::InternalDefect => PublicErrorStatusCode::Internal,
1027        }
1028    }
1029
1030    /// Returns safe recovery guidance for this failure.
1031    #[must_use]
1032    pub const fn recovery_action(self) -> RecoveryAction {
1033        match self {
1034            Self::Validation
1035            | Self::IdempotencyKeyReuse
1036            | Self::HistoryIncarnationMismatch
1037            | Self::HistoryPruned
1038            | Self::FollowerMode => RecoveryAction::CorrectRequest,
1039            Self::AuthorizationDenied => RecoveryAction::ObtainPermission,
1040            Self::ConcurrencyDeadlineExceeded | Self::StorageUnavailable | Self::Overloaded => {
1041                RecoveryAction::Retry
1042            }
1043            Self::ContractMismatch => RecoveryAction::RefreshContract,
1044            Self::OutcomeUnknown => RecoveryAction::ResolveWithSameIdempotencyKey,
1045            Self::InternalDefect | Self::CommandExecutionFailed | Self::PrimaryFenced => {
1046                RecoveryAction::ContactOperator
1047            }
1048        }
1049    }
1050}
1051
1052/// Authoritative status code carried for a public error on the gRPC wire.
1053///
1054/// Transport adapters map this closed set to their native status type. It is
1055/// intentionally separate from [`ErrorClass`]: class is the protocol-neutral
1056/// classification, while this value is the exact wire status both server and
1057/// client must agree on.
1058#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1059pub enum PublicErrorStatusCode {
1060    /// The submitted request is invalid and must be corrected.
1061    InvalidArgument,
1062    /// The request conflicts with previously admitted state.
1063    AlreadyExists,
1064    /// The principal is not permitted to perform the operation.
1065    PermissionDenied,
1066    /// The operation did not complete before its deadline.
1067    DeadlineExceeded,
1068    /// The request targets state that is no longer current.
1069    FailedPrecondition,
1070    /// Capacity or resource bounds rejected the request without execution.
1071    ResourceExhausted,
1072    /// A required service is temporarily unavailable.
1073    Unavailable,
1074    /// The caller must resolve an uncertain command result.
1075    Unknown,
1076    /// The server encountered a defect that is not safe to disclose.
1077    Internal,
1078}
1079
1080/// A stable, caller-safe explanation for one validation failure.
1081#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1082pub enum ValidationCode {
1083    /// A required value was not provided.
1084    MissingRequiredValue,
1085    /// A value has the wrong contract type.
1086    TypeMismatch,
1087    /// A value has an invalid representation.
1088    InvalidValue,
1089    /// A numeric or temporal value is outside its allowed range.
1090    OutOfRange,
1091    /// A string or byte value exceeds its allowed length.
1092    TooLong,
1093    /// A collection exceeds its allowed item count.
1094    TooManyItems,
1095    /// A submitted field is not part of the active contract.
1096    UnknownField,
1097    /// A submitted record contains a field more than once.
1098    DuplicateField,
1099}
1100
1101impl ValidationCode {
1102    /// Returns the stable ASCII identifier exposed by every transport.
1103    #[must_use]
1104    pub const fn code(self) -> &'static str {
1105        match self {
1106            Self::MissingRequiredValue => "missing_required_value",
1107            Self::TypeMismatch => "type_mismatch",
1108            Self::InvalidValue => "invalid_value",
1109            Self::OutOfRange => "out_of_range",
1110            Self::TooLong => "too_long",
1111            Self::TooManyItems => "too_many_items",
1112            Self::UnknownField => "unknown_field",
1113            Self::DuplicateField => "duplicate_field",
1114        }
1115    }
1116
1117    /// Returns a stable public-safe description of the validation failure.
1118    #[must_use]
1119    pub const fn safe_message(self) -> &'static str {
1120        match self {
1121            Self::MissingRequiredValue => "required value is missing",
1122            Self::TypeMismatch => "value has the wrong type",
1123            Self::InvalidValue => "value is invalid",
1124            Self::OutOfRange => "value is outside the allowed range",
1125            Self::TooLong => "value exceeds the allowed length",
1126            Self::TooManyItems => "collection has too many items",
1127            Self::UnknownField => "field is not defined by the active contract",
1128            Self::DuplicateField => "field occurs more than once",
1129        }
1130    }
1131}
1132
1133/// One non-secret location component in a validation issue.
1134#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1135pub enum ValidationPathSegment {
1136    /// A compiler-assigned field identifier.
1137    Field(FieldId),
1138    /// A zero-based list position.
1139    ListIndex(u32),
1140}
1141
1142/// A bounded path to a value that failed validation.
1143#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
1144pub struct ValidationPath {
1145    segments: Vec<ValidationPathSegment>,
1146}
1147
1148impl ValidationPath {
1149    /// Creates the root path, used when an issue applies to the whole request.
1150    #[must_use]
1151    pub const fn root() -> Self {
1152        Self {
1153            segments: Vec::new(),
1154        }
1155    }
1156
1157    /// Creates a checked validation path.
1158    pub fn new(segments: Vec<ValidationPathSegment>) -> Result<Self, ValidationPathError> {
1159        if segments.len() > MAX_VALIDATION_PATH_SEGMENTS {
1160            return Err(ValidationPathError::TooManySegments);
1161        }
1162        Ok(Self { segments })
1163    }
1164
1165    /// Returns the path components in root-to-leaf order.
1166    #[must_use]
1167    pub fn segments(&self) -> &[ValidationPathSegment] {
1168        &self.segments
1169    }
1170}
1171
1172/// A failure to construct a bounded validation path.
1173#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1174pub enum ValidationPathError {
1175    /// More than [`MAX_VALIDATION_PATH_SEGMENTS`] were supplied.
1176    TooManySegments,
1177}
1178
1179impl fmt::Display for ValidationPathError {
1180    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1181        formatter.write_str("validation path has too many segments")
1182    }
1183}
1184
1185impl Error for ValidationPathError {}
1186
1187/// One stable validation code associated with a structured path.
1188#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1189pub struct ValidationIssue {
1190    code: ValidationCode,
1191    path: ValidationPath,
1192}
1193
1194impl ValidationIssue {
1195    /// Creates a validation issue without accepting caller-controlled text.
1196    #[must_use]
1197    pub const fn new(code: ValidationCode, path: ValidationPath) -> Self {
1198        Self { code, path }
1199    }
1200
1201    /// Returns the stable validation code.
1202    #[must_use]
1203    pub const fn code(&self) -> ValidationCode {
1204        self.code
1205    }
1206
1207    /// Returns the structured location of the invalid value.
1208    #[must_use]
1209    pub const fn path(&self) -> &ValidationPath {
1210        &self.path
1211    }
1212}
1213
1214/// A non-empty bounded collection of validation issues.
1215#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1216pub struct ValidationIssues {
1217    issues: Vec<ValidationIssue>,
1218}
1219
1220impl ValidationIssues {
1221    /// Creates a checked collection of validation issues.
1222    pub fn new(issues: Vec<ValidationIssue>) -> Result<Self, ValidationIssuesError> {
1223        if issues.is_empty() {
1224            return Err(ValidationIssuesError::Empty);
1225        }
1226        if issues.len() > MAX_VALIDATION_ISSUES {
1227            return Err(ValidationIssuesError::TooManyIssues);
1228        }
1229        Ok(Self { issues })
1230    }
1231
1232    /// Creates a collection containing exactly one issue.
1233    #[must_use]
1234    pub fn one(issue: ValidationIssue) -> Self {
1235        Self {
1236            issues: vec![issue],
1237        }
1238    }
1239
1240    /// Returns the validation issues in deterministic producer order.
1241    #[must_use]
1242    pub fn as_slice(&self) -> &[ValidationIssue] {
1243        &self.issues
1244    }
1245}
1246
1247/// A failure to construct a bounded collection of validation issues.
1248#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1249pub enum ValidationIssuesError {
1250    /// At least one validation issue is required.
1251    Empty,
1252    /// More than [`MAX_VALIDATION_ISSUES`] were supplied.
1253    TooManyIssues,
1254}
1255
1256impl fmt::Display for ValidationIssuesError {
1257    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1258        formatter.write_str(match self {
1259            Self::Empty => "validation issue collection is empty",
1260            Self::TooManyIssues => "validation issue collection has too many entries",
1261        })
1262    }
1263}
1264
1265impl Error for ValidationIssuesError {}
1266
1267/// Structured, caller-safe detail for a public failure.
1268#[derive(Clone, Debug, Eq, PartialEq)]
1269pub enum PublicErrorDetails {
1270    /// The failure does not expose structured detail.
1271    None,
1272    /// Bounded validation failures.
1273    Validation(ValidationIssues),
1274    /// The authoritative contract version active at the time of rejection.
1275    ContractMismatch {
1276        /// The contract version the caller must refresh to.
1277        active_contract_version: ContractVersion,
1278    },
1279    /// A closed deterministic evaluation failure code.
1280    CommandExecutionFailed {
1281        /// The arithmetic or fixed resource-limit failure classification.
1282        code: ExecutionFailureCode,
1283    },
1284}
1285
1286/// A failure safe to display, serialize, or map at a public transport boundary.
1287///
1288/// This type cannot carry arbitrary messages or an error source. Construction
1289/// is restricted to named constructors so its kind and details cannot disagree.
1290/// The optional incident identifier is an opaque correlation value, not a
1291/// diagnostic.
1292#[derive(Clone, Debug, Eq, PartialEq)]
1293pub struct PublicError {
1294    kind: PublicErrorKind,
1295    details: PublicErrorDetails,
1296    incident_id: Option<IncidentId>,
1297    application_code_hint: Option<ApplicationErrorCode>,
1298}
1299
1300impl PublicError {
1301    const fn contextless(kind: PublicErrorKind) -> Self {
1302        Self {
1303            kind,
1304            details: PublicErrorDetails::None,
1305            incident_id: None,
1306            application_code_hint: None,
1307        }
1308    }
1309
1310    /// Creates a bounded validation failure.
1311    #[must_use]
1312    pub const fn validation(issues: ValidationIssues) -> Self {
1313        Self {
1314            kind: PublicErrorKind::Validation,
1315            details: PublicErrorDetails::Validation(issues),
1316            incident_id: None,
1317            application_code_hint: None,
1318        }
1319    }
1320
1321    /// Creates an idempotency-key-reuse failure.
1322    #[must_use]
1323    pub const fn idempotency_key_reuse() -> Self {
1324        Self::contextless(PublicErrorKind::IdempotencyKeyReuse)
1325    }
1326
1327    /// Creates an authorization-denied failure.
1328    #[must_use]
1329    pub const fn authorization_denied() -> Self {
1330        Self::contextless(PublicErrorKind::AuthorizationDenied)
1331    }
1332
1333    /// Creates the shared failure for a logical-conflict deadline or exhausted
1334    /// fixed v1 command reevaluation budget.
1335    #[must_use]
1336    pub const fn concurrency_deadline_exceeded() -> Self {
1337        Self::contextless(PublicErrorKind::ConcurrencyDeadlineExceeded)
1338    }
1339
1340    /// Creates a contract mismatch and identifies the active version.
1341    #[must_use]
1342    pub const fn contract_mismatch(active_contract_version: ContractVersion) -> Self {
1343        Self {
1344            kind: PublicErrorKind::ContractMismatch,
1345            details: PublicErrorDetails::ContractMismatch {
1346                active_contract_version,
1347            },
1348            incident_id: None,
1349            application_code_hint: None,
1350        }
1351    }
1352
1353    /// Creates a storage-unavailable failure.
1354    #[must_use]
1355    pub const fn storage_unavailable() -> Self {
1356        Self::contextless(PublicErrorKind::StorageUnavailable)
1357    }
1358
1359    /// Creates an uncertain-outcome failure.
1360    #[must_use]
1361    pub const fn outcome_unknown() -> Self {
1362        Self::contextless(PublicErrorKind::OutcomeUnknown)
1363    }
1364
1365    /// Creates a deterministic command-execution failure.
1366    #[must_use]
1367    pub const fn command_execution_failed(code: ExecutionFailureCode) -> Self {
1368        Self {
1369            kind: PublicErrorKind::CommandExecutionFailed,
1370            details: PublicErrorDetails::CommandExecutionFailed { code },
1371            incident_id: None,
1372            application_code_hint: None,
1373        }
1374    }
1375
1376    /// Creates a redacted internal-defect failure with its incident identifier.
1377    #[must_use]
1378    pub const fn internal_defect(incident_id: IncidentId) -> Self {
1379        Self {
1380            kind: PublicErrorKind::InternalDefect,
1381            details: PublicErrorDetails::None,
1382            incident_id: Some(incident_id),
1383            application_code_hint: None,
1384        }
1385    }
1386
1387    /// Creates a history-incarnation mismatch failure.
1388    #[must_use]
1389    pub const fn history_incarnation_mismatch() -> Self {
1390        Self::contextless(PublicErrorKind::HistoryIncarnationMismatch)
1391    }
1392
1393    /// Creates a history-pruned failure (RDB-HISTORY-0102).
1394    #[must_use]
1395    pub const fn history_pruned() -> Self {
1396        Self::contextless(PublicErrorKind::HistoryPruned)
1397    }
1398
1399    /// Creates the typed follower-mode refusal before any write is admitted.
1400    #[must_use]
1401    pub const fn follower_mode() -> Self {
1402        Self::contextless(PublicErrorKind::FollowerMode)
1403    }
1404
1405    /// Creates the durable primary-fence refusal before command admission.
1406    /// This outcome never recommends retrying or clearing the fence.
1407    #[must_use]
1408    pub const fn primary_fenced() -> Self {
1409        Self::contextless(PublicErrorKind::PrimaryFenced)
1410    }
1411
1412    /// Creates an overload / capacity rejection failure.
1413    #[must_use]
1414    pub const fn overloaded() -> Self {
1415        Self::contextless(PublicErrorKind::Overloaded)
1416    }
1417
1418    /// Adds an opaque incident identifier for trusted diagnostic correlation.
1419    #[must_use]
1420    pub const fn with_incident_id(mut self, incident_id: IncidentId) -> Self {
1421        self.incident_id = Some(incident_id);
1422        self
1423    }
1424
1425    /// Attaches a stricter symbolic classification for the application surface.
1426    ///
1427    /// The compatible kernel serializer deliberately ignores this hint.
1428    pub fn with_application_code_hint(
1429        mut self,
1430        code: ApplicationErrorCode,
1431    ) -> Result<Self, ApplicationErrorHintError> {
1432        let compatible = match self.kind {
1433            PublicErrorKind::Validation => matches!(
1434                code,
1435                ApplicationErrorCode::InvalidRequest
1436                    | ApplicationErrorCode::InputInvalid
1437                    | ApplicationErrorCode::QueryInvalid
1438                    | ApplicationErrorCode::QueryUnavailable
1439                    | ApplicationErrorCode::ModuleUnavailable
1440                    | ApplicationErrorCode::CursorInvalid
1441                    | ApplicationErrorCode::ProjectionDiverged
1442                    | ApplicationErrorCode::SnapshotRetired
1443                    | ApplicationErrorCode::FreshnessUnsatisfied
1444                    | ApplicationErrorCode::ProjectedSourceRequired
1445            ),
1446            PublicErrorKind::AuthorizationDenied => matches!(
1447                code,
1448                ApplicationErrorCode::AuthorizationDenied | ApplicationErrorCode::CapabilityRevoked
1449            ),
1450            PublicErrorKind::ContractMismatch => code == ApplicationErrorCode::ContractMismatch,
1451            PublicErrorKind::StorageUnavailable => code == ApplicationErrorCode::StorageUnavailable,
1452            PublicErrorKind::OutcomeUnknown => code == ApplicationErrorCode::OutcomeUnknown,
1453            PublicErrorKind::InternalDefect => code == ApplicationErrorCode::InternalDefect,
1454            PublicErrorKind::IdempotencyKeyReuse => {
1455                code == ApplicationErrorCode::IdempotencyKeyReuse
1456            }
1457            PublicErrorKind::ConcurrencyDeadlineExceeded => {
1458                code == ApplicationErrorCode::DeadlineExceeded
1459            }
1460            PublicErrorKind::CommandExecutionFailed => {
1461                code == ApplicationErrorCode::CommandExecutionFailed
1462            }
1463            PublicErrorKind::HistoryIncarnationMismatch => {
1464                code == ApplicationErrorCode::HistoryIncarnationMismatch
1465            }
1466            PublicErrorKind::HistoryPruned => code == ApplicationErrorCode::HistoryPruned,
1467            PublicErrorKind::Overloaded => code == ApplicationErrorCode::Overloaded,
1468            PublicErrorKind::FollowerMode => code == ApplicationErrorCode::FollowerMode,
1469            PublicErrorKind::PrimaryFenced => code == ApplicationErrorCode::PrimaryFenced,
1470        };
1471        if !compatible {
1472            return Err(ApplicationErrorHintError);
1473        }
1474        self.application_code_hint = Some(code);
1475        Ok(self)
1476    }
1477
1478    /// Returns the stricter application-only classification, when present.
1479    #[must_use]
1480    pub const fn application_code_hint(&self) -> Option<ApplicationErrorCode> {
1481        self.application_code_hint
1482    }
1483
1484    /// Returns the closed failure kind.
1485    #[must_use]
1486    pub const fn kind(&self) -> PublicErrorKind {
1487        self.kind
1488    }
1489
1490    /// Returns the stable public code.
1491    #[must_use]
1492    pub const fn code(&self) -> &'static str {
1493        self.kind.code()
1494    }
1495
1496    /// Returns the stable public-safe message.
1497    #[must_use]
1498    pub const fn safe_message(&self) -> &'static str {
1499        self.kind.safe_message()
1500    }
1501
1502    /// Returns the protocol-neutral failure class.
1503    #[must_use]
1504    pub const fn class(&self) -> ErrorClass {
1505        self.kind.class()
1506    }
1507
1508    /// Returns the authoritative public status code for this error.
1509    #[must_use]
1510    pub const fn status_code(&self) -> PublicErrorStatusCode {
1511        self.kind.status_code()
1512    }
1513
1514    /// Returns safe recovery guidance.
1515    #[must_use]
1516    pub const fn recovery_action(&self) -> RecoveryAction {
1517        self.kind.recovery_action()
1518    }
1519
1520    /// Returns structured, caller-safe detail for this failure.
1521    #[must_use]
1522    pub const fn details(&self) -> &PublicErrorDetails {
1523        &self.details
1524    }
1525
1526    /// Returns the opaque incident identifier, when one is available.
1527    #[must_use]
1528    pub const fn incident_id(&self) -> Option<&IncidentId> {
1529        self.incident_id.as_ref()
1530    }
1531}
1532
1533/// An application hint did not refine the compatible kernel classification.
1534#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1535pub struct ApplicationErrorHintError;
1536
1537impl fmt::Display for ApplicationErrorHintError {
1538    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1539        formatter.write_str("application error hint is incompatible")
1540    }
1541}
1542
1543impl Error for ApplicationErrorHintError {}
1544
1545impl fmt::Display for PublicError {
1546    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1547        write!(formatter, "{}: {}", self.code(), self.safe_message())?;
1548        if let Some(incident_id) = &self.incident_id {
1549            write!(formatter, " (incident {incident_id})")?;
1550        }
1551        Ok(())
1552    }
1553}
1554
1555impl Error for PublicError {}
1556
1557/// What a defect puts in doubt (ADR-0250).
1558///
1559/// The scope is a judgment made where the defect is raised, because that is the
1560/// only place that knows it. A supervision sink receives a boxed source and can
1561/// recover nothing from it, so a defect that does not carry its scope is
1562/// indistinguishable from every other, and a process-level breaker fed by all of
1563/// them counts requests as though they were process faults.
1564#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1565pub enum DefectScope {
1566    /// The process's own state is in doubt: a contained panic, an omitted
1567    /// terminal audit, a lower-integrity failure. These feed the runtime's
1568    /// defect breaker.
1569    Process,
1570    /// This request could not be completed, and nothing follows about the next
1571    /// one. These never stop the runtime, however often a caller repeats them.
1572    Request,
1573}
1574
1575/// An internal failure retaining its source for trusted tracing.
1576///
1577/// Its default [`fmt::Display`] and [`fmt::Debug`] representations are
1578/// redacted. Converting this type into [`PublicError`] deliberately discards
1579/// the source.
1580pub struct InternalError {
1581    incident_id: IncidentId,
1582    scope: DefectScope,
1583    source: Box<dyn Error + Send + Sync + 'static>,
1584}
1585
1586impl InternalError {
1587    /// Creates an internal failure associated with an opaque incident.
1588    ///
1589    /// There is deliberately no scope default and no second constructor that
1590    /// omits it: a new defect path must decide what its defect implies rather
1591    /// than inherit an answer nobody made (ADR-0250 decision 1).
1592    pub fn new(
1593        incident_id: IncidentId,
1594        scope: DefectScope,
1595        source: impl Error + Send + Sync + 'static,
1596    ) -> Self {
1597        Self {
1598            incident_id,
1599            scope,
1600            source: Box::new(source),
1601        }
1602    }
1603
1604    /// Returns what this defect puts in doubt.
1605    #[must_use]
1606    pub const fn scope(&self) -> DefectScope {
1607        self.scope
1608    }
1609
1610    /// Returns the opaque identifier used to correlate trusted diagnostics.
1611    #[must_use]
1612    pub const fn incident_id(&self) -> &IncidentId {
1613        &self.incident_id
1614    }
1615
1616    /// Converts this internal failure into a redacted public failure.
1617    #[must_use]
1618    pub fn into_public(self) -> PublicError {
1619        PublicError::internal_defect(self.incident_id)
1620    }
1621}
1622
1623impl fmt::Display for InternalError {
1624    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1625        write!(formatter, "internal defect (incident {})", self.incident_id)
1626    }
1627}
1628
1629impl fmt::Debug for InternalError {
1630    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1631        formatter
1632            .debug_struct("InternalError")
1633            .field("incident_id", &self.incident_id)
1634            .field("source", &"[REDACTED]")
1635            .finish()
1636    }
1637}
1638
1639impl Error for InternalError {
1640    fn source(&self) -> Option<&(dyn Error + 'static)> {
1641        Some(self.source.as_ref())
1642    }
1643}
1644
1645impl From<InternalError> for PublicError {
1646    fn from(error: InternalError) -> Self {
1647        error.into_public()
1648    }
1649}
1650
1651#[cfg(test)]
1652mod tests {
1653    use std::fmt::Write as _;
1654
1655    use super::*;
1656
1657    const KINDS: [PublicErrorKind; 14] = [
1658        PublicErrorKind::Validation,
1659        PublicErrorKind::IdempotencyKeyReuse,
1660        PublicErrorKind::AuthorizationDenied,
1661        PublicErrorKind::ConcurrencyDeadlineExceeded,
1662        PublicErrorKind::ContractMismatch,
1663        PublicErrorKind::StorageUnavailable,
1664        PublicErrorKind::OutcomeUnknown,
1665        PublicErrorKind::InternalDefect,
1666        PublicErrorKind::CommandExecutionFailed,
1667        PublicErrorKind::HistoryIncarnationMismatch,
1668        PublicErrorKind::HistoryPruned,
1669        PublicErrorKind::Overloaded,
1670        PublicErrorKind::FollowerMode,
1671        PublicErrorKind::PrimaryFenced,
1672    ];
1673
1674    const VALIDATION_CODES: [ValidationCode; 8] = [
1675        ValidationCode::MissingRequiredValue,
1676        ValidationCode::TypeMismatch,
1677        ValidationCode::InvalidValue,
1678        ValidationCode::OutOfRange,
1679        ValidationCode::TooLong,
1680        ValidationCode::TooManyItems,
1681        ValidationCode::UnknownField,
1682        ValidationCode::DuplicateField,
1683    ];
1684
1685    #[derive(Debug)]
1686    struct SecretSource;
1687
1688    impl fmt::Display for SecretSource {
1689        fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1690            formatter.write_str("secret-canary-internal-context")
1691        }
1692    }
1693
1694    impl Error for SecretSource {}
1695
1696    #[test]
1697    fn application_error_registry_matches_the_checked_fixture() {
1698        let mut rendered = "code\tcategory\trecovery_action\tfixes\tmessage\n".to_owned();
1699        for code in APPLICATION_ERROR_CODES {
1700            let fixes = code
1701                .fixes()
1702                .iter()
1703                .map(|fix| fix.as_str())
1704                .collect::<Vec<_>>()
1705                .join(",");
1706            writeln!(
1707                rendered,
1708                "{}\t{}\t{}\t{}\t{}",
1709                code.as_str(),
1710                code.category().as_str(),
1711                code.recovery_action().as_str(),
1712                fixes,
1713                code.safe_message()
1714            )
1715            .expect("string");
1716        }
1717        assert_eq!(
1718            rendered,
1719            include_str!("../../../fixtures/application-errors/registry-v1.tsv")
1720        );
1721    }
1722
1723    #[test]
1724    fn application_hints_only_refine_compatible_kernel_failures() {
1725        let validation = PublicError::validation(ValidationIssues::one(ValidationIssue::new(
1726            ValidationCode::InvalidValue,
1727            ValidationPath::root(),
1728        )));
1729        let cursor = validation
1730            .clone()
1731            .with_application_code_hint(ApplicationErrorCode::CursorInvalid)
1732            .expect("compatible cursor refinement");
1733        assert_eq!(
1734            cursor.application_code_hint(),
1735            Some(ApplicationErrorCode::CursorInvalid)
1736        );
1737        assert_eq!(
1738            validation.with_application_code_hint(ApplicationErrorCode::StorageUnavailable),
1739            Err(ApplicationErrorHintError)
1740        );
1741        assert_eq!(
1742            PublicError::authorization_denied()
1743                .with_application_code_hint(ApplicationErrorCode::QueryInvalid),
1744            Err(ApplicationErrorHintError)
1745        );
1746    }
1747
1748    fn incident_id() -> IncidentId {
1749        let mut bytes = [0x42; 16];
1750        bytes[6] = 0x72;
1751        bytes[8] = 0x82;
1752        IncidentId::from_bytes(bytes).expect("fixture is a valid UUIDv7")
1753    }
1754
1755    fn issue() -> ValidationIssue {
1756        ValidationIssue::new(
1757            ValidationCode::TypeMismatch,
1758            ValidationPath::new(vec![
1759                ValidationPathSegment::Field(FieldId::new(7).expect("field ID is nonzero")),
1760                ValidationPathSegment::ListIndex(3),
1761            ])
1762            .expect("fixture path is bounded"),
1763        )
1764    }
1765
1766    #[test]
1767    fn codes_messages_classes_and_recovery_actions_are_stable() {
1768        let expected = [
1769            (
1770                "validation_failed",
1771                "request validation failed",
1772                ErrorClass::InvalidArgument,
1773                RecoveryAction::CorrectRequest,
1774            ),
1775            (
1776                "idempotency_key_reuse",
1777                "idempotency key was reused with different input",
1778                ErrorClass::Conflict,
1779                RecoveryAction::CorrectRequest,
1780            ),
1781            (
1782                "authorization_denied",
1783                "operation is not authorized",
1784                ErrorClass::PermissionDenied,
1785                RecoveryAction::ObtainPermission,
1786            ),
1787            (
1788                "concurrency_deadline_exceeded",
1789                "concurrency deadline exceeded",
1790                ErrorClass::DeadlineExceeded,
1791                RecoveryAction::Retry,
1792            ),
1793            (
1794                "contract_mismatch",
1795                "contract version or plan does not match",
1796                ErrorClass::FailedPrecondition,
1797                RecoveryAction::RefreshContract,
1798            ),
1799            (
1800                "storage_unavailable",
1801                "storage is temporarily unavailable",
1802                ErrorClass::Unavailable,
1803                RecoveryAction::Retry,
1804            ),
1805            (
1806                "outcome_unknown",
1807                "command outcome is not yet known",
1808                ErrorClass::Uncertain,
1809                RecoveryAction::ResolveWithSameIdempotencyKey,
1810            ),
1811            (
1812                "internal_defect",
1813                "an internal error occurred",
1814                ErrorClass::Internal,
1815                RecoveryAction::ContactOperator,
1816            ),
1817            (
1818                "command_execution_failed",
1819                "command execution failed",
1820                ErrorClass::FailedPrecondition,
1821                RecoveryAction::ContactOperator,
1822            ),
1823            (
1824                "history_incarnation_mismatch",
1825                "observed history predates a database restore",
1826                ErrorClass::FailedPrecondition,
1827                RecoveryAction::CorrectRequest,
1828            ),
1829            (
1830                "history_pruned",
1831                "requested history has been pruned",
1832                ErrorClass::FailedPrecondition,
1833                RecoveryAction::CorrectRequest,
1834            ),
1835            (
1836                "overloaded",
1837                "service is over capacity",
1838                ErrorClass::Unavailable,
1839                RecoveryAction::Retry,
1840            ),
1841            (
1842                "follower_mode",
1843                "operation is unavailable in follower mode",
1844                ErrorClass::FailedPrecondition,
1845                RecoveryAction::CorrectRequest,
1846            ),
1847            (
1848                "primary_fenced",
1849                "primary is durably fenced",
1850                ErrorClass::FailedPrecondition,
1851                RecoveryAction::ContactOperator,
1852            ),
1853        ];
1854
1855        assert_eq!(KINDS.len(), 14);
1856        assert_eq!(KINDS[9], PublicErrorKind::HistoryIncarnationMismatch);
1857        assert_eq!(KINDS[10], PublicErrorKind::HistoryPruned);
1858        assert_eq!(KINDS[11], PublicErrorKind::Overloaded);
1859        assert_eq!(APPLICATION_ERROR_CODES.len(), 27);
1860        assert_eq!(
1861            APPLICATION_ERROR_CODES[18],
1862            ApplicationErrorCode::HistoryIncarnationMismatch
1863        );
1864        assert_eq!(
1865            APPLICATION_ERROR_CODES[19],
1866            ApplicationErrorCode::HistoryPruned
1867        );
1868        assert_eq!(
1869            APPLICATION_ERROR_CODES[20],
1870            ApplicationErrorCode::Overloaded
1871        );
1872        assert_eq!(
1873            APPLICATION_ERROR_CODES[21],
1874            ApplicationErrorCode::ProjectionDiverged
1875        );
1876        assert_eq!(
1877            APPLICATION_ERROR_CODES[22],
1878            ApplicationErrorCode::SnapshotRetired
1879        );
1880        assert_eq!(
1881            APPLICATION_ERROR_CODES[23],
1882            ApplicationErrorCode::FreshnessUnsatisfied
1883        );
1884        assert_eq!(
1885            APPLICATION_ERROR_CODES[24],
1886            ApplicationErrorCode::ProjectedSourceRequired
1887        );
1888
1889        for (kind, (code, message, class, recovery_action)) in KINDS.into_iter().zip(expected) {
1890            assert_eq!(kind.code(), code);
1891            assert_eq!(kind.safe_message(), message);
1892            assert_eq!(kind.class(), class);
1893            assert_eq!(kind.recovery_action(), recovery_action);
1894            assert!(code.is_ascii());
1895            assert!(message.is_ascii());
1896        }
1897    }
1898
1899    #[test]
1900    fn every_kind_has_an_authoritative_wire_status_code() {
1901        let expected = [
1902            PublicErrorStatusCode::InvalidArgument,
1903            PublicErrorStatusCode::AlreadyExists,
1904            PublicErrorStatusCode::PermissionDenied,
1905            PublicErrorStatusCode::DeadlineExceeded,
1906            PublicErrorStatusCode::FailedPrecondition,
1907            PublicErrorStatusCode::Unavailable,
1908            PublicErrorStatusCode::Unknown,
1909            PublicErrorStatusCode::Internal,
1910            PublicErrorStatusCode::FailedPrecondition,
1911            PublicErrorStatusCode::FailedPrecondition,
1912            PublicErrorStatusCode::FailedPrecondition,
1913            PublicErrorStatusCode::ResourceExhausted,
1914            PublicErrorStatusCode::FailedPrecondition,
1915        ];
1916        for (kind, expected_code) in KINDS.into_iter().zip(expected) {
1917            assert_eq!(kind.status_code(), expected_code);
1918            assert_eq!(PublicError::contextless(kind).status_code(), expected_code);
1919        }
1920        assert_eq!(PublicErrorKind::Overloaded.class(), ErrorClass::Unavailable);
1921        assert_eq!(
1922            PublicErrorKind::Overloaded.status_code(),
1923            PublicErrorStatusCode::ResourceExhausted
1924        );
1925    }
1926
1927    #[test]
1928    fn staged_public_constructors_are_contextless_and_registry_aligned() {
1929        let mismatch = PublicError::history_incarnation_mismatch();
1930        assert_eq!(mismatch.kind(), PublicErrorKind::HistoryIncarnationMismatch);
1931        assert_eq!(mismatch.details(), &PublicErrorDetails::None);
1932        assert_eq!(mismatch.incident_id(), None);
1933        assert_eq!(
1934            ApplicationErrorCode::from_public_kind(mismatch.kind()),
1935            ApplicationErrorCode::HistoryIncarnationMismatch
1936        );
1937
1938        let pruned = PublicError::history_pruned();
1939        assert_eq!(pruned.kind(), PublicErrorKind::HistoryPruned);
1940        assert_eq!(pruned.details(), &PublicErrorDetails::None);
1941        assert_eq!(pruned.incident_id(), None);
1942        assert_eq!(
1943            ApplicationErrorCode::from_public_kind(pruned.kind()),
1944            ApplicationErrorCode::HistoryPruned
1945        );
1946
1947        let overloaded = PublicError::overloaded();
1948        assert_eq!(overloaded.kind(), PublicErrorKind::Overloaded);
1949        assert_eq!(overloaded.details(), &PublicErrorDetails::None);
1950        assert_eq!(overloaded.incident_id(), None);
1951        assert_eq!(
1952            ApplicationErrorCode::from_public_kind(overloaded.kind()),
1953            ApplicationErrorCode::Overloaded
1954        );
1955    }
1956
1957    #[test]
1958    fn concurrency_deadline_error_represents_both_v1_retryable_causes() {
1959        #[derive(Clone, Copy)]
1960        enum Cause {
1961            LogicalConflictDeadline,
1962            ReevaluationBudgetExhausted,
1963        }
1964
1965        const fn public_error_for(cause: Cause) -> PublicError {
1966            match cause {
1967                Cause::LogicalConflictDeadline | Cause::ReevaluationBudgetExhausted => {
1968                    PublicError::concurrency_deadline_exceeded()
1969                }
1970            }
1971        }
1972
1973        for cause in [
1974            Cause::LogicalConflictDeadline,
1975            Cause::ReevaluationBudgetExhausted,
1976        ] {
1977            let error = public_error_for(cause);
1978            assert_eq!(error.kind(), PublicErrorKind::ConcurrencyDeadlineExceeded);
1979            assert_eq!(error.kind().code(), "concurrency_deadline_exceeded");
1980            assert_eq!(error.kind().safe_message(), "concurrency deadline exceeded");
1981            assert_eq!(error.kind().class(), ErrorClass::DeadlineExceeded);
1982            assert_eq!(error.kind().recovery_action(), RecoveryAction::Retry);
1983            assert_eq!(error.details(), &PublicErrorDetails::None);
1984        }
1985    }
1986
1987    #[test]
1988    fn validation_codes_and_messages_are_stable_and_safe() {
1989        let expected = [
1990            ("missing_required_value", "required value is missing"),
1991            ("type_mismatch", "value has the wrong type"),
1992            ("invalid_value", "value is invalid"),
1993            ("out_of_range", "value is outside the allowed range"),
1994            ("too_long", "value exceeds the allowed length"),
1995            ("too_many_items", "collection has too many items"),
1996            (
1997                "unknown_field",
1998                "field is not defined by the active contract",
1999            ),
2000            ("duplicate_field", "field occurs more than once"),
2001        ];
2002
2003        for (code, (stable_code, stable_message)) in VALIDATION_CODES.into_iter().zip(expected) {
2004            assert_eq!(code.code(), stable_code);
2005            assert_eq!(code.safe_message(), stable_message);
2006            assert!(stable_code.is_ascii());
2007            assert!(stable_message.is_ascii());
2008        }
2009    }
2010
2011    #[test]
2012    fn validation_paths_are_structured_and_bounded() {
2013        let boundary =
2014            vec![
2015                ValidationPathSegment::Field(FieldId::new(1).expect("field ID is nonzero"));
2016                MAX_VALIDATION_PATH_SEGMENTS
2017            ];
2018        let path = ValidationPath::new(boundary).expect("boundary is accepted");
2019        assert_eq!(path.segments().len(), MAX_VALIDATION_PATH_SEGMENTS);
2020        assert!(ValidationPath::root().segments().is_empty());
2021
2022        let above_boundary =
2023            vec![ValidationPathSegment::ListIndex(0); MAX_VALIDATION_PATH_SEGMENTS + 1];
2024        assert_eq!(
2025            ValidationPath::new(above_boundary),
2026            Err(ValidationPathError::TooManySegments)
2027        );
2028    }
2029
2030    #[test]
2031    fn validation_issue_collections_are_non_empty_and_bounded() {
2032        assert_eq!(
2033            ValidationIssues::new(Vec::new()),
2034            Err(ValidationIssuesError::Empty)
2035        );
2036
2037        let issues = ValidationIssues::new(vec![issue(); MAX_VALIDATION_ISSUES])
2038            .expect("boundary is accepted");
2039        assert_eq!(issues.as_slice().len(), MAX_VALIDATION_ISSUES);
2040
2041        assert_eq!(
2042            ValidationIssues::new(vec![issue(); MAX_VALIDATION_ISSUES + 1]),
2043            Err(ValidationIssuesError::TooManyIssues)
2044        );
2045    }
2046
2047    #[test]
2048    fn named_constructors_enforce_kind_and_detail_consistency() {
2049        let validation = PublicError::validation(ValidationIssues::one(issue()));
2050        assert_eq!(validation.kind(), PublicErrorKind::Validation);
2051        assert!(matches!(
2052            validation.details(),
2053            PublicErrorDetails::Validation(issues) if issues.as_slice().len() == 1
2054        ));
2055
2056        let active_version = ContractVersion::new(41).expect("contract version is nonzero");
2057        let mismatch = PublicError::contract_mismatch(active_version);
2058        assert_eq!(mismatch.kind(), PublicErrorKind::ContractMismatch);
2059        assert_eq!(
2060            mismatch.details(),
2061            &PublicErrorDetails::ContractMismatch {
2062                active_contract_version: active_version,
2063            }
2064        );
2065
2066        let contextless = [
2067            PublicError::idempotency_key_reuse(),
2068            PublicError::authorization_denied(),
2069            PublicError::concurrency_deadline_exceeded(),
2070            PublicError::storage_unavailable(),
2071            PublicError::outcome_unknown(),
2072        ];
2073        let expected = [
2074            PublicErrorKind::IdempotencyKeyReuse,
2075            PublicErrorKind::AuthorizationDenied,
2076            PublicErrorKind::ConcurrencyDeadlineExceeded,
2077            PublicErrorKind::StorageUnavailable,
2078            PublicErrorKind::OutcomeUnknown,
2079        ];
2080        for (error, kind) in contextless.into_iter().zip(expected) {
2081            assert_eq!(error.kind(), kind);
2082            assert_eq!(error.details(), &PublicErrorDetails::None);
2083            assert_eq!(error.incident_id(), None);
2084        }
2085
2086        let execution =
2087            PublicError::command_execution_failed(ExecutionFailureCode::ArithmeticFault);
2088        assert_eq!(execution.kind(), PublicErrorKind::CommandExecutionFailed);
2089        assert_eq!(
2090            execution.details(),
2091            &PublicErrorDetails::CommandExecutionFailed {
2092                code: ExecutionFailureCode::ArithmeticFault,
2093            }
2094        );
2095        assert_eq!(execution.incident_id(), None);
2096    }
2097
2098    #[test]
2099    fn public_error_never_exposes_a_source() {
2100        let error = PublicError::storage_unavailable();
2101
2102        assert!(error.source().is_none());
2103    }
2104
2105    #[test]
2106    fn internal_conversion_redacts_secret_source_everywhere_public() {
2107        let internal = InternalError::new(incident_id(), DefectScope::Process, SecretSource);
2108        assert!(internal.source().is_some());
2109        assert!(!internal.to_string().contains("secret-canary"));
2110        assert!(!format!("{internal:?}").contains("SecretSource"));
2111        assert_eq!(
2112            internal.source().map(ToString::to_string).as_deref(),
2113            Some("secret-canary-internal-context")
2114        );
2115
2116        let public = PublicError::from(internal);
2117        let display = public.to_string();
2118        let debug = format!("{public:?}");
2119
2120        assert_eq!(public.kind(), PublicErrorKind::InternalDefect);
2121        assert_eq!(public.details(), &PublicErrorDetails::None);
2122        assert_eq!(public.incident_id(), Some(&incident_id()));
2123        assert!(public.source().is_none());
2124        assert!(!display.contains("secret-canary"));
2125        assert!(!debug.contains("secret-canary"));
2126        assert!(!display.contains("SecretSource"));
2127        assert!(!debug.contains("SecretSource"));
2128    }
2129
2130    #[test]
2131    fn validation_details_cannot_carry_secret_canaries() {
2132        let error = PublicError::validation(ValidationIssues::one(issue()));
2133        let display = error.to_string();
2134        let debug = format!("{error:?}");
2135
2136        assert!(!display.contains("secret-canary"));
2137        assert!(!debug.contains("secret-canary"));
2138        assert_eq!(display, "validation_failed: request validation failed");
2139    }
2140
2141    #[test]
2142    fn public_display_contains_only_stable_fields() {
2143        let error = PublicError::internal_defect(incident_id());
2144
2145        assert_eq!(
2146            error.to_string(),
2147            "internal_defect: an internal error occurred (incident 42424242-4242-7242-8242-424242424242)"
2148        );
2149    }
2150
2151    /// ADR-0118 sweep anchor: the public error surface is structurally
2152    /// incapable of echoing a secret-classified field's VALUE, because no
2153    /// details shape carries caller or stored text at all — locations are
2154    /// stable IDs, messages are `&'static str`. This match is exhaustive on
2155    /// purpose: a new details variant carrying dynamic payload must red here
2156    /// and be reviewed against the secret-field guarantee before it ships.
2157    #[test]
2158    fn public_error_details_shapes_carry_no_dynamic_payload_for_secret_fields() {
2159        let error = PublicError::validation(ValidationIssues::one(issue()));
2160        // The channel is live: the diagnostic names the field's stable ID…
2161        let PublicErrorDetails::Validation(issues) = error.details() else {
2162            panic!("validation details expected");
2163        };
2164        assert_eq!(
2165            issues.as_slice()[0].path().segments()[0],
2166            ValidationPathSegment::Field(FieldId::new(7).expect("nonzero field"))
2167        );
2168        // …while every details shape is closed over value-free payloads.
2169        for details in [
2170            PublicErrorDetails::None,
2171            error.details().clone(),
2172            PublicErrorDetails::ContractMismatch {
2173                active_contract_version: ContractVersion::new(3).expect("nonzero version"),
2174            },
2175            PublicErrorDetails::CommandExecutionFailed {
2176                code: ExecutionFailureCode::ArithmeticFault,
2177            },
2178        ] {
2179            match details {
2180                PublicErrorDetails::None => {}
2181                PublicErrorDetails::Validation(issues) => {
2182                    for issue in issues.as_slice() {
2183                        for segment in issue.path().segments() {
2184                            match segment {
2185                                ValidationPathSegment::Field(_)
2186                                | ValidationPathSegment::ListIndex(_) => {}
2187                            }
2188                        }
2189                    }
2190                }
2191                PublicErrorDetails::ContractMismatch {
2192                    active_contract_version: _,
2193                } => {}
2194                PublicErrorDetails::CommandExecutionFailed { code: _ } => {}
2195            }
2196        }
2197    }
2198
2199    #[test]
2200    fn incident_source_is_synchronous_fallible_and_has_no_fallback() {
2201        struct FixedSource(IncidentId);
2202
2203        impl IncidentIdSource for FixedSource {
2204            fn next_incident_id(&self) -> Result<IncidentId, IncidentIdSourceError> {
2205                Ok(self.0)
2206            }
2207        }
2208
2209        struct FailingSource;
2210
2211        impl IncidentIdSource for FailingSource {
2212            fn next_incident_id(&self) -> Result<IncidentId, IncidentIdSourceError> {
2213                Err(IncidentIdSourceError)
2214            }
2215        }
2216
2217        assert_eq!(
2218            FixedSource(incident_id()).next_incident_id(),
2219            Ok(incident_id())
2220        );
2221        assert_eq!(FailingSource.next_incident_id(), Err(IncidentIdSourceError));
2222        assert_eq!(
2223            IncidentIdSourceError.to_string(),
2224            "incident identifier source failed"
2225        );
2226    }
2227
2228    #[test]
2229    fn emergency_internal_failure_has_no_identifier_or_disclosive_source() {
2230        let failure = EmergencyInternalFailure::from(IncidentIdSourceError);
2231
2232        assert_eq!(failure.safe_message(), "an internal error occurred");
2233        assert_eq!(failure.class(), ErrorClass::Internal);
2234        assert_eq!(failure.recovery_action(), RecoveryAction::ContactOperator);
2235        assert_eq!(failure.to_string(), "an internal error occurred");
2236        assert_eq!(format!("{failure:?}"), "EmergencyInternalFailure");
2237        assert!(failure.source().is_none());
2238        assert!(!failure.to_string().contains("incident"));
2239    }
2240}