Skip to main content

forge_engine/
failure.rs

1//! Failure classification, recording, and retry semantics.
2
3use serde::{Deserialize, Serialize};
4
5/// Classification of failures for structured handling.
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum FailureClass {
9    /// Baseline execution timed out.
10    BaselineTimeout,
11    /// Patched execution crashed after baseline succeeded.
12    PatchedCrash,
13    /// Duplicate CEA replay attempted.
14    DuplicateCeaReplay,
15    /// Database was busy during insert.
16    DbBusy,
17    /// Crash after export receipt but before memory ingest.
18    PostReceiptCrash,
19    /// Semantic-memory ingest unavailable.
20    MemoryIngestUnavailable,
21    /// Partial verification completion.
22    PartialVerification,
23    /// Migration was interrupted.
24    MigrationInterrupted,
25    /// Workspace exceeded size limit.
26    WorkspaceSizeExceeded,
27    /// Unknown or unclassified failure.
28    Other,
29}
30
31/// A persisted failure record.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct FailureRecord {
34    pub failure_id: String,
35    pub run_id: String,
36    pub class: FailureClass,
37    pub message: String,
38    /// Phase during which the failure occurred.
39    pub phase: String,
40    /// Whether this failure is retriable.
41    pub retriable: bool,
42    /// Number of retries attempted so far.
43    pub retry_count: u32,
44    pub occurred_at: String,
45}
46
47impl FailureClass {
48    /// Whether this failure class is generally retriable.
49    pub fn is_retriable(&self) -> bool {
50        matches!(
51            self,
52            Self::DbBusy
53                | Self::MemoryIngestUnavailable
54                | Self::PostReceiptCrash
55                | Self::BaselineTimeout
56        )
57    }
58
59    /// Classify an error into a FailureClass.
60    pub fn from_error(error: &crate::error::ForgeError) -> Self {
61        match error {
62            crate::error::ForgeError::CommandTimeout { .. } => Self::BaselineTimeout,
63            crate::error::ForgeError::Database(e) => {
64                if e.to_string().contains("database is locked") {
65                    Self::DbBusy
66                } else {
67                    Self::Other
68                }
69            }
70            _ => Self::Other,
71        }
72    }
73}