Skip to main content

orion_error/core/
snapshot.rs

1use crate::{core::DomainReason, StructError};
2
3use super::{
4    context::OperationResult, report::DiagnosticReport, ErrorCategory, ErrorIdentityProvider,
5    ErrorMetadata, OperationContext, SourceFrame,
6};
7
8pub const STABLE_SNAPSHOT_SCHEMA_VERSION: &str = "orion-error.snapshot.v2";
9#[cfg_attr(feature = "serde", derive(serde::Serialize))]
10#[derive(Debug, Clone, PartialEq)]
11pub struct SnapshotContextFrame {
12    /// Stable root operation name.
13    pub target: Option<String>,
14    /// Action/phase captured by `doing(...)`.
15    pub action: Option<String>,
16    /// Resource/location captured by `at(...)`.
17    pub locator: Option<String>,
18    /// Stable path segments captured from runtime context.
19    pub path: Vec<String>,
20    /// Stable machine-readable metadata payload.
21    pub metadata: ErrorMetadata,
22    /// Compatibility projection of ad-hoc context key/value pairs.
23    pub fields: Vec<(String, String)>,
24    /// Compatibility projection of runtime scope result.
25    pub result: OperationResult,
26}
27
28#[cfg_attr(feature = "serde", derive(serde::Serialize))]
29#[derive(Debug, Clone, PartialEq)]
30pub struct StableSnapshotContextFrame {
31    pub target: Option<String>,
32    pub action: Option<String>,
33    pub locator: Option<String>,
34    pub path: Vec<String>,
35    pub metadata: ErrorMetadata,
36}
37
38#[cfg_attr(feature = "serde", derive(serde::Serialize))]
39#[derive(Debug, Clone, PartialEq)]
40pub struct SnapshotSourceFrame {
41    pub index: usize,
42    /// Stable human-facing summary for diagnostics and snapshot assertions.
43    pub message: String,
44    /// Compatibility projection of formatted display output.
45    pub display: Option<String>,
46    /// Compatibility projection of best-effort runtime type name.
47    pub type_name: Option<String>,
48    pub error_code: Option<i32>,
49    pub reason: Option<String>,
50    pub want: Option<String>,
51    pub path: Option<String>,
52    pub detail: Option<String>,
53    pub metadata: ErrorMetadata,
54    pub is_root_cause: bool,
55}
56
57#[cfg_attr(feature = "serde", derive(serde::Serialize))]
58#[derive(Debug, Clone, PartialEq)]
59pub struct StableSnapshotSourceFrame {
60    pub index: usize,
61    pub message: String,
62    pub error_code: Option<i32>,
63    pub reason: Option<String>,
64    pub want: Option<String>,
65    pub path: Option<String>,
66    pub detail: Option<String>,
67    pub metadata: ErrorMetadata,
68    pub is_root_cause: bool,
69}
70
71/// Stable machine-readable snapshot view derived from `StructError`.
72///
73/// This object is intentionally separate from runtime propagation semantics.
74/// It carries exported diagnostic data, but does not implement `StdError`
75/// or own any runtime source object handles.
76#[derive(Debug, Clone, PartialEq)]
77pub struct ErrorSnapshot {
78    pub reason: String,
79    pub detail: Option<String>,
80    pub position: Option<String>,
81    pub want: Option<String>,
82    pub path: Option<String>,
83    pub context: Vec<SnapshotContextFrame>,
84    pub root_metadata: ErrorMetadata,
85    pub source_frames: Vec<SnapshotSourceFrame>,
86}
87
88#[cfg_attr(feature = "serde", derive(serde::Serialize))]
89#[derive(Debug, Clone, PartialEq)]
90pub struct StableErrorSnapshot {
91    pub schema_version: &'static str,
92    pub reason: String,
93    pub detail: Option<String>,
94    pub position: Option<String>,
95    pub want: Option<String>,
96    pub path: Option<String>,
97    pub context: Vec<StableSnapshotContextFrame>,
98    pub root_metadata: ErrorMetadata,
99    pub source_frames: Vec<StableSnapshotSourceFrame>,
100}
101
102/// Identity-first snapshot view.
103///
104/// This view keeps `code` and `category` available for governance, testing,
105/// policy decisions, and protocol projections without changing the stable
106/// snapshot export contract.
107#[cfg_attr(feature = "serde", derive(serde::Serialize))]
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct ErrorIdentity {
110    pub code: String,
111    pub category: ErrorCategory,
112    pub reason: String,
113    pub detail: Option<String>,
114    pub position: Option<String>,
115    pub want: Option<String>,
116    pub path: Option<String>,
117}
118
119#[cfg(feature = "serde")]
120impl serde::Serialize for ErrorSnapshot {
121    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
122    where
123        S: serde::Serializer,
124    {
125        self.stable_export().serialize(serializer)
126    }
127}
128
129impl ErrorSnapshot {
130    pub fn stable_context(&self) -> &[SnapshotContextFrame] {
131        &self.context
132    }
133
134    pub fn stable_source_frames(&self) -> &[SnapshotSourceFrame] {
135        &self.source_frames
136    }
137
138    pub fn root_source_frame(&self) -> Option<&SnapshotSourceFrame> {
139        self.source_frames.iter().find(|frame| frame.is_root_cause)
140    }
141
142    pub fn stable_export(&self) -> StableErrorSnapshot {
143        self.clone().into_stable_export()
144    }
145
146    pub fn into_stable_export(self) -> StableErrorSnapshot {
147        StableErrorSnapshot {
148            schema_version: STABLE_SNAPSHOT_SCHEMA_VERSION,
149            reason: self.reason,
150            detail: self.detail,
151            position: self.position,
152            want: self.want,
153            path: self.path,
154            context: self.context.into_iter().map(Into::into).collect(),
155            root_metadata: self.root_metadata,
156            source_frames: self.source_frames.into_iter().map(Into::into).collect(),
157        }
158    }
159
160    #[cfg(feature = "serde_json")]
161    pub fn to_stable_snapshot_json(&self) -> serde_json::Result<serde_json::Value> {
162        serde_json::to_value(self.stable_export())
163    }
164
165    pub fn report(&self) -> DiagnosticReport {
166        self.clone().into_report()
167    }
168
169    pub fn into_report(self) -> DiagnosticReport {
170        DiagnosticReport {
171            reason: self.reason,
172            detail: self.detail,
173            position: self.position,
174            want: self.want,
175            path: self.path,
176            context: self.context.into_iter().map(Into::into).collect(),
177            root_metadata: self.root_metadata,
178            source_frames: self.source_frames.into_iter().map(Into::into).collect(),
179        }
180    }
181}
182
183impl StableErrorSnapshot {
184    pub fn report(&self) -> DiagnosticReport {
185        DiagnosticReport {
186            reason: self.reason.clone(),
187            detail: self.detail.clone(),
188            position: self.position.clone(),
189            want: self.want.clone(),
190            path: self.path.clone(),
191            context: self.context.iter().cloned().map(Into::into).collect(),
192            root_metadata: self.root_metadata.clone(),
193            source_frames: self.source_frames.iter().cloned().map(Into::into).collect(),
194        }
195    }
196
197    pub fn into_report(self) -> DiagnosticReport {
198        DiagnosticReport {
199            reason: self.reason,
200            detail: self.detail,
201            position: self.position,
202            want: self.want,
203            path: self.path,
204            context: self.context.into_iter().map(Into::into).collect(),
205            root_metadata: self.root_metadata,
206            source_frames: self.source_frames.into_iter().map(Into::into).collect(),
207        }
208    }
209}
210
211impl SnapshotContextFrame {
212    pub fn stable_export(&self) -> StableSnapshotContextFrame {
213        self.clone().into()
214    }
215}
216
217impl SnapshotSourceFrame {
218    pub fn stable_export(&self) -> StableSnapshotSourceFrame {
219        self.clone().into()
220    }
221}
222
223impl From<SnapshotContextFrame> for StableSnapshotContextFrame {
224    fn from(value: SnapshotContextFrame) -> Self {
225        StableSnapshotContextFrame {
226            target: value.target,
227            action: value.action,
228            locator: value.locator,
229            path: value.path,
230            metadata: value.metadata,
231        }
232    }
233}
234
235impl From<&SnapshotContextFrame> for StableSnapshotContextFrame {
236    fn from(value: &SnapshotContextFrame) -> Self {
237        value.clone().into()
238    }
239}
240
241impl From<StableSnapshotContextFrame> for SnapshotContextFrame {
242    fn from(value: StableSnapshotContextFrame) -> Self {
243        SnapshotContextFrame {
244            target: value.target,
245            action: value.action,
246            locator: value.locator,
247            path: value.path,
248            metadata: value.metadata,
249            fields: Vec::new(),
250            result: OperationResult::Fail,
251        }
252    }
253}
254
255impl From<&StableSnapshotContextFrame> for SnapshotContextFrame {
256    fn from(value: &StableSnapshotContextFrame) -> Self {
257        value.clone().into()
258    }
259}
260
261impl From<SnapshotSourceFrame> for StableSnapshotSourceFrame {
262    fn from(value: SnapshotSourceFrame) -> Self {
263        StableSnapshotSourceFrame {
264            index: value.index,
265            message: value.message,
266            error_code: value.error_code,
267            reason: value.reason,
268            want: value.want,
269            path: value.path,
270            detail: value.detail,
271            metadata: value.metadata,
272            is_root_cause: value.is_root_cause,
273        }
274    }
275}
276
277impl From<&SnapshotSourceFrame> for StableSnapshotSourceFrame {
278    fn from(value: &SnapshotSourceFrame) -> Self {
279        value.clone().into()
280    }
281}
282
283impl From<StableSnapshotSourceFrame> for SnapshotSourceFrame {
284    fn from(value: StableSnapshotSourceFrame) -> Self {
285        SnapshotSourceFrame {
286            index: value.index,
287            message: value.message,
288            display: None,
289            type_name: None,
290            error_code: value.error_code,
291            reason: value.reason,
292            want: value.want,
293            path: value.path,
294            detail: value.detail,
295            metadata: value.metadata,
296            is_root_cause: value.is_root_cause,
297        }
298    }
299}
300
301impl From<&StableSnapshotSourceFrame> for SnapshotSourceFrame {
302    fn from(value: &StableSnapshotSourceFrame) -> Self {
303        value.clone().into()
304    }
305}
306
307impl From<OperationContext> for SnapshotContextFrame {
308    fn from(value: OperationContext) -> Self {
309        Self {
310            target: value.target().clone(),
311            action: value.action().clone(),
312            locator: value.locator().clone(),
313            path: value.normalized_path_segments(),
314            metadata: value.metadata().clone(),
315            fields: value.context().items.clone(),
316            result: value.result().clone(),
317        }
318    }
319}
320
321impl From<SnapshotContextFrame> for OperationContext {
322    fn from(value: SnapshotContextFrame) -> Self {
323        let mut ctx = value
324            .target
325            .clone()
326            .map(OperationContext::from_target)
327            .unwrap_or_default();
328        ctx.replace_target_for_report(value.target);
329        ctx.replace_action_for_report(value.action);
330        ctx.replace_locator_for_report(value.locator);
331        ctx.replace_path_for_report(value.path);
332        ctx.context_mut_for_report().items = value.fields;
333        ctx.replace_metadata_for_report(value.metadata);
334        match value.result {
335            OperationResult::Suc => ctx.mark_suc(),
336            OperationResult::Fail => {}
337            OperationResult::Cancel => ctx.mark_cancel(),
338        }
339        ctx
340    }
341}
342
343impl From<StableSnapshotContextFrame> for OperationContext {
344    fn from(value: StableSnapshotContextFrame) -> Self {
345        SnapshotContextFrame::from(value).into()
346    }
347}
348
349impl From<&StableSnapshotContextFrame> for OperationContext {
350    fn from(value: &StableSnapshotContextFrame) -> Self {
351        value.clone().into()
352    }
353}
354
355impl From<SourceFrame> for SnapshotSourceFrame {
356    fn from(value: SourceFrame) -> Self {
357        Self {
358            index: value.index,
359            message: value.message,
360            display: value.display,
361            type_name: value.type_name,
362            error_code: value.error_code,
363            reason: value.reason,
364            want: value.want,
365            path: value.path,
366            detail: value.detail,
367            metadata: value.metadata,
368            is_root_cause: value.is_root_cause,
369        }
370    }
371}
372
373impl From<SnapshotSourceFrame> for SourceFrame {
374    fn from(value: SnapshotSourceFrame) -> Self {
375        Self {
376            index: value.index,
377            message: value.message,
378            display: value.display,
379            debug: String::new(),
380            type_name: value.type_name,
381            error_code: value.error_code,
382            reason: value.reason,
383            want: value.want,
384            path: value.path,
385            detail: value.detail,
386            metadata: value.metadata,
387            is_root_cause: value.is_root_cause,
388        }
389    }
390}
391
392impl From<StableSnapshotSourceFrame> for SourceFrame {
393    fn from(value: StableSnapshotSourceFrame) -> Self {
394        SnapshotSourceFrame::from(value).into()
395    }
396}
397
398impl From<&StableSnapshotSourceFrame> for SourceFrame {
399    fn from(value: &StableSnapshotSourceFrame) -> Self {
400        value.clone().into()
401    }
402}
403
404impl<T: DomainReason> From<&StructError<T>> for ErrorSnapshot {
405    fn from(value: &StructError<T>) -> Self {
406        value.snapshot()
407    }
408}
409
410impl<T: DomainReason> From<StructError<T>> for ErrorSnapshot {
411    fn from(value: StructError<T>) -> Self {
412        value.into_snapshot()
413    }
414}
415
416impl From<&ErrorSnapshot> for StableErrorSnapshot {
417    fn from(value: &ErrorSnapshot) -> Self {
418        value.stable_export()
419    }
420}
421
422impl From<ErrorSnapshot> for StableErrorSnapshot {
423    fn from(value: ErrorSnapshot) -> Self {
424        value.into_stable_export()
425    }
426}
427
428impl<T: DomainReason> From<&StructError<T>> for StableErrorSnapshot {
429    fn from(value: &StructError<T>) -> Self {
430        value.snapshot().into_stable_export()
431    }
432}
433
434impl<T: DomainReason> From<StructError<T>> for StableErrorSnapshot {
435    fn from(value: StructError<T>) -> Self {
436        value.into_snapshot().into_stable_export()
437    }
438}
439
440impl<T: DomainReason> StructError<T> {
441    pub fn snapshot(&self) -> ErrorSnapshot {
442        ErrorSnapshot {
443            reason: self.reason().to_string(),
444            detail: self.detail().clone(),
445            position: self.position().clone(),
446            want: self.target_main(),
447            path: self.target_path(),
448            context: self.contexts().iter().cloned().map(Into::into).collect(),
449            root_metadata: self.context_metadata(),
450            source_frames: self
451                .source_frames()
452                .iter()
453                .cloned()
454                .map(Into::into)
455                .collect(),
456        }
457    }
458
459    pub fn into_snapshot(self) -> ErrorSnapshot {
460        self.snapshot()
461    }
462}
463
464impl<T> StructError<T>
465where
466    T: DomainReason + ErrorIdentityProvider,
467{
468    pub fn identity_snapshot(&self) -> ErrorIdentity {
469        ErrorIdentity {
470            code: self.stable_code().to_string(),
471            category: self.error_category(),
472            reason: self.reason().to_string(),
473            detail: self.detail().clone(),
474            position: self.position().clone(),
475            want: self.target_main(),
476            path: self.target_path(),
477        }
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use crate::{
484        core::{context::ContextRecord, DomainReason, ErrorMetadata, SourceFrame},
485        ErrorCategory, ErrorCode, ErrorIdentityProvider, OperationContext, StructError, UvsReason,
486    };
487
488    use super::{
489        DiagnosticReport, ErrorSnapshot, SnapshotContextFrame, SnapshotSourceFrame,
490        StableErrorSnapshot, StableSnapshotContextFrame, StableSnapshotSourceFrame,
491        STABLE_SNAPSHOT_SCHEMA_VERSION,
492    };
493
494    #[derive(Debug, Clone, PartialEq, thiserror::Error)]
495    enum TestReason {
496        #[error("test error")]
497        TestError,
498        #[error("{0}")]
499        Uvs(UvsReason),
500    }
501
502    impl From<UvsReason> for TestReason {
503        fn from(value: UvsReason) -> Self {
504            Self::Uvs(value)
505        }
506    }
507
508    impl DomainReason for TestReason {}
509
510    impl ErrorCode for TestReason {
511        fn error_code(&self) -> i32 {
512            match self {
513                TestReason::TestError => 1001,
514                TestReason::Uvs(reason) => reason.error_code(),
515            }
516        }
517    }
518
519    impl ErrorIdentityProvider for TestReason {
520        fn stable_code(&self) -> &'static str {
521            match self {
522                TestReason::TestError => "test.test_error",
523                TestReason::Uvs(reason) => reason.stable_code(),
524            }
525        }
526
527        fn error_category(&self) -> ErrorCategory {
528            match self {
529                TestReason::TestError => ErrorCategory::Logic,
530                TestReason::Uvs(reason) => reason.error_category(),
531            }
532        }
533    }
534
535    #[test]
536    fn test_snapshot_captures_runtime_fields_and_source_frames() {
537        let source = StructError::from(TestReason::TestError).with_context(
538            OperationContext::doing("load defaults").with_meta("config.kind", "sink_defaults"),
539        );
540        let err = StructError::from(TestReason::Uvs(UvsReason::system_error()))
541            .with_detail("engine bootstrap failed")
542            .with_position("src/main.rs:42")
543            .with_context(
544                OperationContext::doing("start engine").with_meta("component.name", "engine"),
545            )
546            .with_struct_source(source);
547
548        let snapshot = err.snapshot();
549
550        assert_eq!(snapshot.reason, "system error");
551        assert_eq!(snapshot.detail.as_deref(), Some("engine bootstrap failed"));
552        assert_eq!(snapshot.position.as_deref(), Some("src/main.rs:42"));
553        assert_eq!(snapshot.want.as_deref(), Some("start engine"));
554        assert_eq!(snapshot.context[0].target.as_deref(), Some("start engine"));
555        assert_eq!(
556            snapshot.root_metadata.get_str("component.name"),
557            Some("engine")
558        );
559        assert_eq!(
560            snapshot.source_frames[0].metadata.get_str("config.kind"),
561            Some("sink_defaults")
562        );
563    }
564
565    #[test]
566    fn test_identity_snapshot_captures_stable_identity_fields() {
567        let err = StructError::from(TestReason::Uvs(UvsReason::system_error()))
568            .with_detail("engine bootstrap failed")
569            .with_position("src/main.rs:42")
570            .with_context(OperationContext::doing("start engine"));
571
572        let identity = err.identity_snapshot();
573
574        assert_eq!(identity.code, "sys.io_error");
575        assert_eq!(identity.category, ErrorCategory::Sys);
576        assert_eq!(identity.reason, "system error");
577        assert_eq!(identity.detail.as_deref(), Some("engine bootstrap failed"));
578        assert_eq!(identity.position.as_deref(), Some("src/main.rs:42"));
579        assert_eq!(identity.want.as_deref(), Some("start engine"));
580        assert_eq!(identity.path.as_deref(), Some("start engine"));
581    }
582
583    #[cfg(feature = "serde")]
584    #[test]
585    fn test_identity_snapshot_serialization_includes_code_and_category() {
586        use super::ErrorIdentity;
587
588        let identity = ErrorIdentity {
589            code: "sys.io_error".to_string(),
590            category: ErrorCategory::Sys,
591            reason: "system error".to_string(),
592            detail: Some("engine bootstrap failed".to_string()),
593            position: Some("src/main.rs:42".to_string()),
594            want: Some("start engine".to_string()),
595            path: Some("start engine".to_string()),
596        };
597
598        let value = serde_json::to_value(identity).unwrap();
599
600        assert_eq!(value["code"], serde_json::json!("sys.io_error"));
601        assert_eq!(value["category"], serde_json::json!("Sys"));
602        assert_eq!(value["reason"], serde_json::json!("system error"));
603    }
604
605    #[test]
606    fn test_snapshot_preserves_action_and_locator_context_fields() {
607        let mut ctx = OperationContext::at("config.toml");
608        ctx.with_doing("parse config");
609
610        let err = StructError::from(TestReason::Uvs(UvsReason::system_error()))
611            .with_context(
612                OperationContext::doing("load config").with_meta("component.name", "engine"),
613            )
614            .with_context(ctx);
615
616        let snapshot = err.snapshot();
617
618        assert_eq!(snapshot.context[0].action.as_deref(), Some("load config"));
619        assert_eq!(snapshot.context[1].action.as_deref(), Some("parse config"));
620        assert_eq!(snapshot.context[1].locator.as_deref(), Some("config.toml"));
621
622        let report = snapshot.into_report();
623        assert_eq!(report.context[1].action().as_deref(), Some("parse config"));
624        assert_eq!(report.context[1].locator().as_deref(), Some("config.toml"));
625    }
626
627    #[test]
628    fn test_snapshot_report_conversion_preserves_payload() {
629        let snapshot = ErrorSnapshot {
630            reason: "system error".to_string(),
631            detail: Some("engine bootstrap failed".to_string()),
632            position: Some("src/main.rs:42".to_string()),
633            want: Some("start engine".to_string()),
634            path: Some("start engine / load defaults".to_string()),
635            context: vec![SnapshotContextFrame {
636                target: Some("start engine".to_string()),
637                action: None,
638                locator: None,
639                path: vec!["start engine".to_string()],
640                metadata: ErrorMetadata::new(),
641                fields: vec![],
642                result: crate::core::context::OperationResult::Fail,
643            }],
644            root_metadata: {
645                let mut metadata = ErrorMetadata::new();
646                metadata.insert("component.name", "engine");
647                metadata
648            },
649            source_frames: vec![],
650        };
651
652        let report = snapshot.report();
653
654        assert_eq!(report.reason, snapshot.reason);
655        assert_eq!(report.detail, snapshot.detail);
656        assert_eq!(report.position, snapshot.position);
657        assert_eq!(report.want, snapshot.want);
658        assert_eq!(report.path, snapshot.path);
659        assert_eq!(
660            report.context,
661            snapshot
662                .context
663                .clone()
664                .into_iter()
665                .map(Into::into)
666                .collect::<Vec<OperationContext>>()
667        );
668        assert_eq!(report.root_metadata, snapshot.root_metadata);
669        assert_eq!(
670            report.source_frames,
671            snapshot
672                .source_frames
673                .clone()
674                .into_iter()
675                .map(Into::into)
676                .collect::<Vec<SourceFrame>>()
677        );
678    }
679
680    #[test]
681    fn test_snapshot_from_struct_error_matches_snapshot_method() {
682        let err = StructError::from(TestReason::TestError)
683            .with_detail("engine bootstrap failed")
684            .with_context(OperationContext::doing("start engine"));
685
686        let via_method = err.snapshot();
687        let via_from = ErrorSnapshot::from(&err);
688
689        assert_eq!(via_from, via_method);
690    }
691
692    #[test]
693    fn test_snapshot_from_owned_struct_error_matches_snapshot_method() {
694        let err = StructError::from(TestReason::TestError)
695            .with_detail("engine bootstrap failed")
696            .with_context(OperationContext::doing("start engine"));
697
698        let via_method = err.snapshot();
699        let via_from = ErrorSnapshot::from(err);
700
701        assert_eq!(via_from, via_method);
702    }
703
704    #[test]
705    fn test_struct_error_into_snapshot_matches_snapshot_method() {
706        let err = StructError::from(TestReason::TestError)
707            .with_detail("engine bootstrap failed")
708            .with_context(OperationContext::doing("start engine"));
709
710        let via_method = err.snapshot();
711        let via_into = err.into_snapshot();
712
713        assert_eq!(via_into, via_method);
714    }
715
716    #[test]
717    fn test_snapshot_into_report_matches_borrowed_report() {
718        let snapshot = ErrorSnapshot {
719            reason: "system error".to_string(),
720            detail: Some("engine bootstrap failed".to_string()),
721            position: Some("src/main.rs:42".to_string()),
722            want: Some("start engine".to_string()),
723            path: Some("start engine".to_string()),
724            context: vec![SnapshotContextFrame {
725                target: Some("start engine".to_string()),
726                action: None,
727                locator: None,
728                path: vec!["start engine".to_string()],
729                metadata: ErrorMetadata::new(),
730                fields: vec![("tenant".to_string(), "alpha".to_string())],
731                result: crate::core::context::OperationResult::Fail,
732            }],
733            root_metadata: ErrorMetadata::new(),
734            source_frames: vec![SnapshotSourceFrame {
735                index: 0,
736                message: "db unavailable".to_string(),
737                display: Some("db unavailable".to_string()),
738                type_name: Some("std::io::Error".to_string()),
739                error_code: None,
740                reason: None,
741                want: Some("load config".to_string()),
742                path: Some("load config / read".to_string()),
743                detail: Some("inner detail".to_string()),
744                metadata: ErrorMetadata::new(),
745                is_root_cause: true,
746            }],
747        };
748
749        let via_borrowed = snapshot.report();
750        let via_owned = snapshot.clone().into_report();
751        let via_from = DiagnosticReport::from(snapshot);
752
753        assert_eq!(via_owned, via_borrowed);
754        assert_eq!(via_from, via_borrowed);
755    }
756
757    #[test]
758    fn test_snapshot_stable_helpers_prefer_snapshot_native_frames() {
759        let source = StructError::from(TestReason::TestError)
760            .with_detail("inner detail")
761            .with_context(
762                OperationContext::doing("load defaults").with_meta("config.kind", "sink_defaults"),
763            );
764        let err = StructError::from(TestReason::Uvs(UvsReason::system_error()))
765            .with_detail("outer detail")
766            .with_context(OperationContext::doing("start engine"))
767            .with_struct_source(source);
768
769        let snapshot = err.snapshot();
770
771        assert_eq!(snapshot.stable_context(), snapshot.context.as_slice());
772        assert_eq!(
773            snapshot.stable_source_frames(),
774            snapshot.source_frames.as_slice()
775        );
776        assert_eq!(snapshot.root_source_frame().unwrap().message, "test error");
777        assert_eq!(
778            snapshot
779                .root_source_frame()
780                .unwrap()
781                .metadata
782                .get_str("config.kind"),
783            Some("sink_defaults")
784        );
785    }
786
787    #[test]
788    fn test_snapshot_stable_export_strips_compat_projection_fields() {
789        let source = StructError::from(TestReason::TestError)
790            .with_detail("inner detail")
791            .with_context(
792                OperationContext::doing("load defaults").with_meta("config.kind", "sink_defaults"),
793            );
794        let mut outer = OperationContext::at("engine.toml");
795        outer.with_doing("start engine");
796        let err = StructError::from(TestReason::Uvs(UvsReason::system_error()))
797            .with_detail("outer detail")
798            .with_context(outer)
799            .with_struct_source(source);
800
801        let snapshot = err.snapshot();
802        let stable = snapshot.stable_export();
803
804        assert_eq!(stable.schema_version, STABLE_SNAPSHOT_SCHEMA_VERSION);
805        assert_eq!(stable.reason, snapshot.reason);
806        assert_eq!(stable.context[0].target.as_deref(), Some("start engine"));
807        assert_eq!(stable.context[0].action.as_deref(), Some("start engine"));
808        assert_eq!(stable.context[0].locator.as_deref(), Some("engine.toml"));
809        assert_eq!(
810            stable.context[0].path,
811            vec!["start engine".to_string(), "engine.toml".to_string()]
812        );
813        assert_eq!(
814            stable.source_frames[0].message,
815            snapshot.source_frames[0].message
816        );
817        assert_eq!(
818            stable.source_frames[0].metadata.get_str("config.kind"),
819            Some("sink_defaults")
820        );
821    }
822
823    #[test]
824    fn test_snapshot_into_stable_export_matches_borrowed_stable_export() {
825        let snapshot = ErrorSnapshot {
826            reason: "system error".to_string(),
827            detail: Some("outer detail".to_string()),
828            position: Some("src/main.rs:42".to_string()),
829            want: Some("start engine".to_string()),
830            path: Some("start engine".to_string()),
831            context: vec![SnapshotContextFrame {
832                target: Some("start engine".to_string()),
833                action: None,
834                locator: None,
835                path: vec!["start engine".to_string()],
836                metadata: ErrorMetadata::new(),
837                fields: vec![("tenant".to_string(), "alpha".to_string())],
838                result: crate::core::context::OperationResult::Fail,
839            }],
840            root_metadata: ErrorMetadata::new(),
841            source_frames: vec![SnapshotSourceFrame {
842                index: 0,
843                message: "db unavailable".to_string(),
844                display: Some("db unavailable".to_string()),
845                type_name: Some("std::io::Error".to_string()),
846                error_code: None,
847                reason: None,
848                want: Some("load config".to_string()),
849                path: Some("load config / read".to_string()),
850                detail: Some("inner detail".to_string()),
851                metadata: ErrorMetadata::new(),
852                is_root_cause: true,
853            }],
854        };
855
856        let via_borrowed = snapshot.stable_export();
857        let via_owned = snapshot.clone().into_stable_export();
858        let via_from_borrowed = StableErrorSnapshot::from(&snapshot);
859        let via_from_owned = StableErrorSnapshot::from(snapshot);
860
861        assert_eq!(via_owned, via_borrowed);
862        assert_eq!(via_from_borrowed, via_borrowed);
863        assert_eq!(via_from_owned, via_borrowed);
864        assert_eq!(via_borrowed.schema_version, STABLE_SNAPSHOT_SCHEMA_VERSION);
865    }
866
867    #[test]
868    fn test_stable_snapshot_from_struct_error_matches_snapshot_stable_export() {
869        let source = StructError::from(TestReason::TestError)
870            .with_detail("inner detail")
871            .with_context(
872                OperationContext::doing("load defaults").with_meta("config.kind", "sink_defaults"),
873            );
874        let err = StructError::from(TestReason::Uvs(UvsReason::system_error()))
875            .with_detail("outer detail")
876            .with_context(OperationContext::doing("start engine"))
877            .with_struct_source(source);
878
879        let via_method = err.snapshot().stable_export();
880        let via_borrowed = StableErrorSnapshot::from(&err);
881        let via_owned = StableErrorSnapshot::from(err);
882
883        assert_eq!(via_borrowed, via_method);
884        assert_eq!(via_owned, via_method);
885    }
886
887    #[test]
888    fn test_snapshot_frame_stable_from_matches_stable_export() {
889        let context = SnapshotContextFrame {
890            target: Some("start engine".to_string()),
891            action: None,
892            locator: None,
893            path: vec!["start engine".to_string()],
894            metadata: ErrorMetadata::new(),
895            fields: vec![("tenant".to_string(), "alpha".to_string())],
896            result: crate::core::context::OperationResult::Fail,
897        };
898        let source = SnapshotSourceFrame {
899            index: 0,
900            message: "db unavailable".to_string(),
901            display: Some("db unavailable".to_string()),
902            type_name: Some("std::io::Error".to_string()),
903            error_code: None,
904            reason: None,
905            want: Some("load config".to_string()),
906            path: Some("load config / read".to_string()),
907            detail: Some("inner detail".to_string()),
908            metadata: ErrorMetadata::new(),
909            is_root_cause: true,
910        };
911
912        assert_eq!(
913            StableSnapshotContextFrame::from(&context),
914            context.stable_export()
915        );
916        assert_eq!(
917            StableSnapshotContextFrame::from(context.clone()),
918            context.stable_export()
919        );
920        assert_eq!(
921            StableSnapshotSourceFrame::from(&source),
922            source.stable_export()
923        );
924        assert_eq!(
925            StableSnapshotSourceFrame::from(source.clone()),
926            source.stable_export()
927        );
928    }
929
930    #[test]
931    fn test_stable_snapshot_into_report_matches_report() {
932        let stable = StableErrorSnapshot {
933            schema_version: STABLE_SNAPSHOT_SCHEMA_VERSION,
934            reason: "system error".to_string(),
935            detail: Some("outer detail".to_string()),
936            position: None,
937            want: Some("start engine".to_string()),
938            path: Some("start engine".to_string()),
939            context: vec![StableSnapshotContextFrame {
940                target: Some("start engine".to_string()),
941                action: None,
942                locator: None,
943                path: vec!["start engine".to_string()],
944                metadata: ErrorMetadata::new(),
945            }],
946            root_metadata: ErrorMetadata::new(),
947            source_frames: vec![StableSnapshotSourceFrame {
948                index: 0,
949                message: "db unavailable".to_string(),
950                error_code: None,
951                reason: None,
952                want: Some("load config".to_string()),
953                path: Some("load config / read".to_string()),
954                detail: Some("inner detail".to_string()),
955                metadata: ErrorMetadata::new(),
956                is_root_cause: true,
957            }],
958        };
959
960        let via_method = stable.report();
961        let via_owned = stable.clone().into_report();
962
963        assert_eq!(via_owned, via_method);
964    }
965
966    #[test]
967    fn test_stable_frame_to_compat_frame_defaults_compat_fields() {
968        let context = StableSnapshotContextFrame {
969            target: Some("start engine".to_string()),
970            action: None,
971            locator: None,
972            path: vec!["start engine".to_string()],
973            metadata: ErrorMetadata::new(),
974        };
975        let source = StableSnapshotSourceFrame {
976            index: 0,
977            message: "db unavailable".to_string(),
978            error_code: None,
979            reason: None,
980            want: Some("load config".to_string()),
981            path: Some("load config / read".to_string()),
982            detail: Some("inner detail".to_string()),
983            metadata: ErrorMetadata::new(),
984            is_root_cause: true,
985        };
986
987        let compat_context = SnapshotContextFrame::from(&context);
988        let compat_source = SnapshotSourceFrame::from(&source);
989
990        assert_eq!(compat_context.target, context.target);
991        assert_eq!(compat_context.path, context.path);
992        assert_eq!(compat_context.fields, Vec::<(String, String)>::new());
993        assert_eq!(
994            compat_context.result,
995            crate::core::context::OperationResult::Fail
996        );
997        assert_eq!(compat_source.message, source.message);
998        assert_eq!(compat_source.display, None);
999        assert_eq!(compat_source.type_name, None);
1000    }
1001
1002    #[test]
1003    fn test_snapshot_context_frame_roundtrip_to_operation_context() {
1004        let mut ctx = OperationContext::doing("start engine");
1005        ctx.with_doing("load defaults");
1006        ctx.record("tenant", "alpha");
1007        ctx.record_meta("component.name", "engine");
1008
1009        let snapshot_frame = SnapshotContextFrame::from(ctx.clone());
1010        let roundtrip: OperationContext = snapshot_frame.clone().into();
1011
1012        assert_eq!(snapshot_frame.target.as_deref(), Some("start engine"));
1013        assert_eq!(
1014            snapshot_frame.path,
1015            vec!["start engine".to_string(), "load defaults".to_string()]
1016        );
1017        assert_eq!(roundtrip.target().as_deref(), Some("start engine"));
1018        assert_eq!(
1019            roundtrip.path(),
1020            vec!["start engine".to_string(), "load defaults".to_string()]
1021        );
1022        assert_eq!(
1023            roundtrip.metadata().get_str("component.name"),
1024            Some("engine")
1025        );
1026        assert_eq!(
1027            roundtrip.context().items,
1028            vec![("tenant".to_string(), "alpha".to_string())]
1029        );
1030    }
1031
1032    #[test]
1033    fn test_snapshot_context_frame_roundtrip_normalizes_action_locator_path() {
1034        let mut ctx = OperationContext::at("engine.toml");
1035        ctx.with_doing("start engine");
1036
1037        let snapshot_frame = SnapshotContextFrame::from(ctx);
1038        let roundtrip: OperationContext = snapshot_frame.clone().into();
1039
1040        assert_eq!(snapshot_frame.target.as_deref(), Some("start engine"));
1041        assert_eq!(snapshot_frame.action.as_deref(), Some("start engine"));
1042        assert_eq!(snapshot_frame.locator.as_deref(), Some("engine.toml"));
1043        assert_eq!(
1044            snapshot_frame.path,
1045            vec!["start engine".to_string(), "engine.toml".to_string()]
1046        );
1047        assert_eq!(
1048            roundtrip.path(),
1049            vec!["start engine".to_string(), "engine.toml".to_string()]
1050        );
1051        assert_eq!(
1052            roundtrip.path_string().as_deref(),
1053            Some("start engine / engine.toml")
1054        );
1055    }
1056
1057    #[test]
1058    fn test_snapshot_source_frame_roundtrip_to_report_frame() {
1059        let frame = SnapshotSourceFrame {
1060            index: 0,
1061            message: "db unavailable".to_string(),
1062            display: Some("db unavailable".to_string()),
1063            type_name: Some("std::io::Error".to_string()),
1064            error_code: None,
1065            reason: None,
1066            want: Some("load config".to_string()),
1067            path: Some("load config / read".to_string()),
1068            detail: Some("inner detail".to_string()),
1069            metadata: {
1070                let mut metadata = ErrorMetadata::new();
1071                metadata.insert("config.kind", "sink_defaults");
1072                metadata
1073            },
1074            is_root_cause: true,
1075        };
1076
1077        let report_frame: SourceFrame = frame.clone().into();
1078        let roundtrip = SnapshotSourceFrame::from(report_frame);
1079
1080        assert_eq!(roundtrip, frame);
1081    }
1082
1083    #[cfg(feature = "serde")]
1084    #[test]
1085    fn test_snapshot_default_serialization_uses_stable_export_shape() {
1086        let source = StructError::from(TestReason::TestError)
1087            .with_detail("inner detail")
1088            .with_context(
1089                OperationContext::doing("load defaults").with_meta("config.kind", "sink_defaults"),
1090            );
1091        let err = StructError::from(TestReason::Uvs(UvsReason::system_error()))
1092            .with_detail("engine bootstrap failed")
1093            .with_position("src/main.rs:42")
1094            .with_context(
1095                OperationContext::doing("start engine").with_meta("component.name", "engine"),
1096            )
1097            .with_struct_source(source);
1098
1099        let json_value = serde_json::to_value(err.snapshot()).unwrap();
1100
1101        assert_eq!(
1102            json_value["schema_version"],
1103            serde_json::json!(STABLE_SNAPSHOT_SCHEMA_VERSION)
1104        );
1105        assert_eq!(json_value["reason"], serde_json::json!("system error"));
1106        assert_eq!(
1107            json_value["detail"],
1108            serde_json::json!("engine bootstrap failed")
1109        );
1110        assert_eq!(json_value["position"], serde_json::json!("src/main.rs:42"));
1111        assert_eq!(json_value["want"], serde_json::json!("start engine"));
1112        assert_eq!(json_value["path"], serde_json::json!("start engine"));
1113        assert_eq!(
1114            json_value["root_metadata"]["component.name"],
1115            serde_json::json!("engine")
1116        );
1117        assert_eq!(
1118            json_value["context"][0]["target"],
1119            serde_json::json!("start engine")
1120        );
1121        assert_eq!(
1122            json_value["context"][0]["action"],
1123            serde_json::json!("start engine")
1124        );
1125        assert_eq!(json_value["context"][0]["locator"], serde_json::Value::Null);
1126        assert_eq!(
1127            json_value["context"][0]["path"],
1128            serde_json::json!(["start engine"])
1129        );
1130        assert_eq!(
1131            json_value["context"][0]["metadata"]["component.name"],
1132            serde_json::json!("engine")
1133        );
1134        assert!(json_value["context"][0].get("fields").is_none());
1135        assert!(json_value["context"][0].get("result").is_none());
1136        assert_eq!(
1137            json_value["source_frames"][0]["message"],
1138            serde_json::json!("test error")
1139        );
1140        assert_eq!(
1141            json_value["source_frames"][0]["reason"],
1142            serde_json::json!("test error")
1143        );
1144        assert_eq!(
1145            json_value["source_frames"][0]["want"],
1146            serde_json::json!("load defaults")
1147        );
1148        assert_eq!(
1149            json_value["source_frames"][0]["path"],
1150            serde_json::json!("load defaults")
1151        );
1152        assert_eq!(
1153            json_value["source_frames"][0]["detail"],
1154            serde_json::json!("inner detail")
1155        );
1156        assert_eq!(
1157            json_value["source_frames"][0]["metadata"]["config.kind"],
1158            serde_json::json!("sink_defaults")
1159        );
1160        assert_eq!(
1161            json_value["source_frames"][0]["is_root_cause"],
1162            serde_json::json!(true)
1163        );
1164        assert!(json_value["source_frames"][0].get("debug").is_none());
1165        assert!(json_value["source_frames"][0].get("display").is_none());
1166        assert!(json_value["source_frames"][0].get("type_name").is_none());
1167        assert!(json_value.get("source_message").is_none());
1168        assert!(json_value.get("source_chain").is_none());
1169    }
1170
1171    #[cfg(feature = "serde")]
1172    #[test]
1173    fn test_snapshot_stable_export_serialization_omits_compat_projection_fields() {
1174        let snapshot = ErrorSnapshot {
1175            reason: "system error".to_string(),
1176            detail: Some("outer detail".to_string()),
1177            position: Some("src/main.rs:42".to_string()),
1178            want: Some("start engine".to_string()),
1179            path: Some("start engine".to_string()),
1180            context: vec![SnapshotContextFrame {
1181                target: Some("start engine".to_string()),
1182                action: Some("start engine".to_string()),
1183                locator: Some("engine.toml".to_string()),
1184                path: vec!["start engine".to_string()],
1185                metadata: {
1186                    let mut metadata = ErrorMetadata::new();
1187                    metadata.insert("component.name", "engine");
1188                    metadata
1189                },
1190                fields: vec![("tenant".to_string(), "alpha".to_string())],
1191                result: crate::core::context::OperationResult::Fail,
1192            }],
1193            root_metadata: {
1194                let mut metadata = ErrorMetadata::new();
1195                metadata.insert("component.name", "engine");
1196                metadata
1197            },
1198            source_frames: vec![SnapshotSourceFrame {
1199                index: 0,
1200                message: "db unavailable".to_string(),
1201                display: Some("db unavailable".to_string()),
1202                type_name: Some("std::io::Error".to_string()),
1203                error_code: None,
1204                reason: None,
1205                want: Some("load config".to_string()),
1206                path: Some("load config / read".to_string()),
1207                detail: Some("inner detail".to_string()),
1208                metadata: {
1209                    let mut metadata = ErrorMetadata::new();
1210                    metadata.insert("config.kind", "sink_defaults");
1211                    metadata
1212                },
1213                is_root_cause: true,
1214            }],
1215        };
1216
1217        let stable = snapshot.stable_export();
1218        let json_value = serde_json::to_value(&stable).unwrap();
1219
1220        assert_eq!(StableErrorSnapshot::clone(&stable), stable);
1221        assert_eq!(
1222            json_value["schema_version"],
1223            serde_json::json!(STABLE_SNAPSHOT_SCHEMA_VERSION)
1224        );
1225        assert_eq!(json_value["reason"], serde_json::json!("system error"));
1226        assert_eq!(
1227            json_value["context"][0]["target"],
1228            serde_json::json!("start engine")
1229        );
1230        assert_eq!(
1231            json_value["context"][0]["action"],
1232            serde_json::json!("start engine")
1233        );
1234        assert_eq!(
1235            json_value["context"][0]["locator"],
1236            serde_json::json!("engine.toml")
1237        );
1238        assert_eq!(
1239            json_value["context"][0]["path"],
1240            serde_json::json!(["start engine"])
1241        );
1242        assert_eq!(
1243            json_value["context"][0]["metadata"]["component.name"],
1244            serde_json::json!("engine")
1245        );
1246        assert!(json_value["context"][0].get("fields").is_none());
1247        assert!(json_value["context"][0].get("result").is_none());
1248        assert_eq!(
1249            json_value["source_frames"][0]["message"],
1250            serde_json::json!("db unavailable")
1251        );
1252        assert_eq!(
1253            json_value["source_frames"][0]["path"],
1254            serde_json::json!("load config / read")
1255        );
1256        assert_eq!(
1257            json_value["source_frames"][0]["detail"],
1258            serde_json::json!("inner detail")
1259        );
1260        assert!(json_value["source_frames"][0].get("display").is_none());
1261        assert!(json_value["source_frames"][0].get("type_name").is_none());
1262    }
1263
1264    #[cfg(feature = "serde_json")]
1265    #[test]
1266    fn test_to_stable_snapshot_json_uses_stable_export_shape() {
1267        let snapshot = ErrorSnapshot {
1268            reason: "system error".to_string(),
1269            detail: Some("outer detail".to_string()),
1270            position: None,
1271            want: Some("start engine".to_string()),
1272            path: Some("start engine".to_string()),
1273            context: vec![SnapshotContextFrame {
1274                target: Some("start engine".to_string()),
1275                action: Some("start engine".to_string()),
1276                locator: Some("engine.toml".to_string()),
1277                path: vec!["start engine".to_string()],
1278                metadata: ErrorMetadata::new(),
1279                fields: vec![("tenant".to_string(), "alpha".to_string())],
1280                result: crate::core::context::OperationResult::Fail,
1281            }],
1282            root_metadata: ErrorMetadata::new(),
1283            source_frames: vec![SnapshotSourceFrame {
1284                index: 0,
1285                message: "db unavailable".to_string(),
1286                display: Some("db unavailable".to_string()),
1287                type_name: Some("std::io::Error".to_string()),
1288                error_code: None,
1289                reason: None,
1290                want: Some("load config".to_string()),
1291                path: Some("load config / read".to_string()),
1292                detail: None,
1293                metadata: ErrorMetadata::new(),
1294                is_root_cause: true,
1295            }],
1296        };
1297
1298        let json_value = snapshot.to_stable_snapshot_json().unwrap();
1299
1300        assert_eq!(
1301            json_value,
1302            serde_json::to_value(snapshot.stable_export()).unwrap()
1303        );
1304        assert_eq!(
1305            json_value["schema_version"],
1306            serde_json::json!(STABLE_SNAPSHOT_SCHEMA_VERSION)
1307        );
1308        assert_eq!(
1309            json_value["context"][0]["action"],
1310            serde_json::json!("start engine")
1311        );
1312        assert_eq!(
1313            json_value["context"][0]["locator"],
1314            serde_json::json!("engine.toml")
1315        );
1316        assert!(json_value["context"][0].get("fields").is_none());
1317        assert!(json_value["source_frames"][0].get("display").is_none());
1318    }
1319
1320    #[cfg(feature = "serde_json")]
1321    #[test]
1322    fn test_stable_snapshot_json_fields_match_schema_constants() {
1323        let snapshot = ErrorSnapshot {
1324            reason: "system error".to_string(),
1325            detail: Some("outer detail".to_string()),
1326            position: Some("src/main.rs:42".to_string()),
1327            want: Some("start engine".to_string()),
1328            path: Some("start engine".to_string()),
1329            context: vec![SnapshotContextFrame {
1330                target: Some("start engine".to_string()),
1331                action: None,
1332                locator: None,
1333                path: vec!["start engine".to_string()],
1334                metadata: ErrorMetadata::new(),
1335                fields: vec![("tenant".to_string(), "alpha".to_string())],
1336                result: crate::core::context::OperationResult::Fail,
1337            }],
1338            root_metadata: ErrorMetadata::new(),
1339            source_frames: vec![SnapshotSourceFrame {
1340                index: 0,
1341                message: "db unavailable".to_string(),
1342                display: Some("db unavailable".to_string()),
1343                type_name: Some("std::io::Error".to_string()),
1344                error_code: None,
1345                reason: None,
1346                want: Some("load config".to_string()),
1347                path: Some("load config / read".to_string()),
1348                detail: Some("inner detail".to_string()),
1349                metadata: ErrorMetadata::new(),
1350                is_root_cause: true,
1351            }],
1352        };
1353
1354        let json_value = snapshot.to_stable_snapshot_json().unwrap();
1355        let top_level = json_value.as_object().unwrap();
1356        let context = json_value["context"][0].as_object().unwrap();
1357        let source_frame = json_value["source_frames"][0].as_object().unwrap();
1358
1359        assert_eq!(
1360            sorted_keys(top_level),
1361            sorted_strings(&[
1362                "schema_version",
1363                "reason",
1364                "detail",
1365                "position",
1366                "want",
1367                "path",
1368                "context",
1369                "root_metadata",
1370                "source_frames",
1371            ])
1372        );
1373        assert_eq!(
1374            sorted_keys(context),
1375            sorted_strings(&["target", "action", "locator", "path", "metadata"])
1376        );
1377        assert_eq!(
1378            sorted_keys(source_frame),
1379            sorted_strings(&[
1380                "index",
1381                "message",
1382                "error_code",
1383                "reason",
1384                "want",
1385                "path",
1386                "detail",
1387                "metadata",
1388                "is_root_cause",
1389            ])
1390        );
1391    }
1392
1393    #[cfg(feature = "serde_json")]
1394    fn sorted_keys(map: &serde_json::Map<String, serde_json::Value>) -> Vec<String> {
1395        let mut keys = map.keys().cloned().collect::<Vec<_>>();
1396        keys.sort();
1397        keys
1398    }
1399
1400    #[cfg(feature = "serde_json")]
1401    fn sorted_strings(values: &[&str]) -> Vec<String> {
1402        let mut values = values
1403            .iter()
1404            .map(|value| value.to_string())
1405            .collect::<Vec<_>>();
1406        values.sort();
1407        values
1408    }
1409}
1410
1411#[cfg(doc)]
1412mod stable_snapshot_compile_fail_docs {
1413    //! ```compile_fail
1414    //! use orion_error::{StableErrorSnapshot, ErrorSnapshot};
1415    //!
1416    //! fn must_not_compile(stable: StableErrorSnapshot) -> ErrorSnapshot {
1417    //!     stable.into()
1418    //! }
1419    //! ```
1420}