Skip to main content

saddle_core/
bounded_diagnostic.rs

1//! Allocation-free source facts for admitted execution. Not execution authority.
2use crate::{CaptureSite, DiagnosticCategory, DiagnosticCode, DiagnosticStage};
3use serde::{Serialize, Serializer};
4
5/// Audited metadata only, never arbitrary error messages or business values.
6#[derive(Clone, Copy)]
7pub struct InlineDiagnosticText {
8    bytes: [u8; 192],
9    len: u8,
10    truncated: bool,
11    redacted: bool,
12}
13impl InlineDiagnosticText {
14    pub fn metadata(value: &str) -> Self {
15        let mut out = Self {
16            bytes: [0; 192],
17            len: 0,
18            truncated: false,
19            redacted: false,
20        };
21        let mut len = value.len().min(192);
22        while !value.is_char_boundary(len) {
23            len -= 1;
24        }
25        out.truncated = len < value.len();
26        if !value[..len]
27            .chars()
28            .all(|c| c.is_alphanumeric() || "_:<>[],(); &*.-".contains(c))
29        {
30            out.redacted = true;
31            return out;
32        }
33        out.bytes[..len].copy_from_slice(&value.as_bytes()[..len]);
34        out.len = len as u8;
35        out.truncated = len < value.len();
36        out
37    }
38}
39impl Serialize for InlineDiagnosticText {
40    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
41        use serde::ser::SerializeStruct;
42        let mut value = serializer.serialize_struct("InlineDiagnosticText", 3)?;
43        value.serialize_field(
44            "value",
45            std::str::from_utf8(&self.bytes[..self.len as usize]).unwrap_or(""),
46        )?;
47        value.serialize_field("truncated", &self.truncated)?;
48        value.serialize_field("redacted", &self.redacted)?;
49        value.end()
50    }
51}
52
53#[derive(Clone, Copy, Serialize)]
54#[serde(rename_all = "snake_case")]
55pub enum DiagnosticFactUnavailable {
56    OpaqueSource,
57    MetadataUnavailable,
58    NotApplicable,
59}
60
61/// Membership must come from the generated response registry, not an error text.
62#[derive(Clone, Copy, Serialize)]
63pub struct RegisteredDiagnosticCode(&'static str);
64impl RegisteredDiagnosticCode {
65    pub fn from_registered(code: &'static str, registry: &[&'static str]) -> Option<Self> {
66        (!code.is_empty()
67            && code.len() <= 128
68            && code
69                .bytes()
70                .all(|b| b.is_ascii_alphanumeric() || b"._-".contains(&b))
71            && registry.len() <= 256
72            && registry.contains(&code))
73        .then_some(Self(code))
74    }
75}
76
77/// Fixed source facts. Static codes must come from the component's closed mapping.
78#[derive(Clone, Copy, Serialize)]
79pub struct BoundedDiagnosticCause {
80    stage: DiagnosticStage,
81    code: DiagnosticCode,
82    io_kind: Option<DiagnosticCode>,
83    os_code: Option<i32>,
84    database_code: Option<u32>,
85    sqlstate: Option<InlineDiagnosticText>,
86    column_index: Option<u64>,
87    column_count: Option<u64>,
88    target_rust_type: Option<InlineDiagnosticText>,
89    actual_db_type: Option<InlineDiagnosticText>,
90    object: Option<InlineDiagnosticText>,
91    unavailable: Option<DiagnosticFactUnavailable>,
92    target_type_unavailable: Option<DiagnosticFactUnavailable>,
93    actual_type_unavailable: Option<DiagnosticFactUnavailable>,
94}
95impl BoundedDiagnosticCause {
96    pub fn new(stage: DiagnosticStage, code: DiagnosticCode) -> Self {
97        Self {
98            stage,
99            code,
100            io_kind: None,
101            os_code: None,
102            database_code: None,
103            sqlstate: None,
104            column_index: None,
105            column_count: None,
106            target_rust_type: None,
107            actual_db_type: None,
108            object: None,
109            unavailable: None,
110            target_type_unavailable: Some(DiagnosticFactUnavailable::MetadataUnavailable),
111            actual_type_unavailable: Some(DiagnosticFactUnavailable::MetadataUnavailable),
112        }
113    }
114    pub fn with_system(mut self, kind: DiagnosticCode, os: Option<i32>) -> Self {
115        self.io_kind = Some(kind);
116        self.os_code = os;
117        self
118    }
119    pub fn with_database_code(mut self, code: u32) -> Self {
120        self.database_code = Some(code);
121        self
122    }
123    pub fn with_sqlstate(mut self, state: &str) -> Self {
124        self.sqlstate = (state.len() == 5
125            && state
126                .bytes()
127                .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit()))
128        .then(|| InlineDiagnosticText::metadata(state));
129        self
130    }
131    pub fn with_column(mut self, index: Option<u64>, count: Option<u64>) -> Self {
132        self.column_index = index;
133        self.column_count = count;
134        self
135    }
136    pub fn with_types(
137        mut self,
138        target: Option<InlineDiagnosticText>,
139        actual: Option<InlineDiagnosticText>,
140        unavailable: Option<DiagnosticFactUnavailable>,
141    ) -> Self {
142        self.target_rust_type = target;
143        self.actual_db_type = actual;
144        self.unavailable = unavailable;
145        self.target_type_unavailable = if target.is_some() {
146            None
147        } else {
148            Some(unavailable.unwrap_or(DiagnosticFactUnavailable::MetadataUnavailable))
149        };
150        self.actual_type_unavailable = if actual.is_some() {
151            None
152        } else {
153            Some(unavailable.unwrap_or(DiagnosticFactUnavailable::MetadataUnavailable))
154        };
155        self
156    }
157    pub fn with_object(mut self, object: InlineDiagnosticText) -> Self {
158        self.object = Some(object);
159        self
160    }
161}
162
163/// Four fixed causes plus explicit overflow. Capture never invokes Backtrace or
164/// an arbitrary source formatter. Retain alongside the original Result, not in it.
165#[derive(Serialize)]
166pub struct BoundedDiagnostic {
167    diagnostic_id: u64,
168    primary_diagnostic_id: Option<u64>,
169    category: DiagnosticCategory,
170    capture_site: CaptureSite,
171    origin_file: &'static str,
172    origin_line: u32,
173    origin_column: u32,
174    causes: [Option<BoundedDiagnosticCause>; 4],
175    cause_count: usize,
176    omitted_causes: u64,
177    stack_status: &'static str,
178}
179impl BoundedDiagnostic {
180    #[track_caller]
181    pub fn capture(
182        category: DiagnosticCategory,
183        site: CaptureSite,
184        cause: BoundedDiagnosticCause,
185    ) -> Self {
186        let origin = std::panic::Location::caller();
187        let file = origin
188            .file()
189            .rsplit("/crates/")
190            .next()
191            .unwrap_or(origin.file());
192        let file = if file.starts_with('/') || file.contains('\\') {
193            file.rsplit(['/', '\\']).next().unwrap_or("unknown")
194        } else {
195            file
196        };
197        Self {
198            diagnostic_id: crate::diagnostic::NEXT_ID
199                .fetch_add(1, std::sync::atomic::Ordering::Relaxed),
200            primary_diagnostic_id: None,
201            category,
202            capture_site: site,
203            origin_file: file,
204            origin_line: origin.line(),
205            origin_column: origin.column(),
206            causes: [Some(cause), None, None, None],
207            cause_count: 1,
208            omitted_causes: 0,
209            stack_status: "unavailable_bounded_capture",
210        }
211    }
212    pub fn wrap(mut self, cause: BoundedDiagnosticCause) -> Self {
213        if self.cause_count == self.causes.len() {
214            self.omitted_causes = self.omitted_causes.saturating_add(1);
215        } else {
216            self.causes.copy_within(0..self.cause_count, 1);
217            self.causes[0] = Some(cause);
218            self.cause_count += 1;
219        }
220        self
221    }
222    pub fn during_cleanup_of(mut self, primary: &Self) -> Self {
223        self.primary_diagnostic_id = Some(primary.diagnostic_id);
224        self
225    }
226    pub fn id(&self) -> u64 {
227        self.diagnostic_id
228    }
229    pub fn category(&self) -> DiagnosticCategory {
230        self.category
231    }
232    /// Links cleanup to an existing light source reference without retaining its body.
233    pub fn during_cleanup_occurrence(mut self, primary: DiagnosticOccurrence) -> Self {
234        self.primary_diagnostic_id = Some(primary.source_id());
235        self
236    }
237    pub fn occurrence(&self) -> DiagnosticOccurrence {
238        DiagnosticOccurrence {
239            diagnostic_id: self.diagnostic_id,
240            primary_diagnostic_id: self.primary_diagnostic_id,
241        }
242    }
243}
244
245/// Read-only correlation, not delivery confirmation or execution authority.
246///
247/// ```compile_fail
248/// use saddle_core::DiagnosticOccurrence;
249/// let forged = DiagnosticOccurrence { diagnostic_id: 1, primary_diagnostic_id: None };
250/// ```
251#[derive(Clone, Copy, Serialize)]
252pub struct DiagnosticOccurrence {
253    diagnostic_id: u64,
254    primary_diagnostic_id: Option<u64>,
255}
256impl DiagnosticOccurrence {
257    pub(crate) const fn source_id(&self) -> u64 {
258        self.diagnostic_id
259    }
260    pub(crate) fn from_diagnostic(diagnostic: &crate::Diagnostic) -> Self {
261        Self {
262            diagnostic_id: diagnostic.id(),
263            primary_diagnostic_id: diagnostic.primary_id_for_projection(),
264        }
265    }
266}
267
268#[derive(Clone, Copy, Serialize)]
269#[serde(rename_all = "snake_case")]
270pub enum OperationOutcome {
271    Succeeded,
272    Rejected,
273    Failed,
274    Cancelled,
275    TimedOut,
276    Panicked,
277    Unknown,
278}
279#[derive(Clone, Copy, Serialize)]
280#[serde(rename_all = "snake_case")]
281pub enum PhysicalDispositionFact {
282    NotUsed,
283    Returned,
284    Discarded,
285    Unknown,
286}
287#[derive(Clone, Copy, Serialize)]
288#[serde(rename_all = "snake_case")]
289pub enum BusinessOutcome {
290    Success,
291    Failure,
292    Unknown,
293}
294#[derive(Clone, Copy, Serialize)]
295#[serde(rename_all = "snake_case")]
296pub enum ResponseDelivery {
297    NotStarted,
298    Partial,
299    LocalWriteComplete,
300    Failed,
301    Cancelled,
302    TimedOut,
303    Unknown,
304}
305#[derive(Clone, Copy, Serialize)]
306#[serde(rename_all = "snake_case")]
307pub enum CleanupOutcome {
308    NotRun,
309    Succeeded,
310    Failed,
311    Unknown,
312}
313
314/// Each domain reports only facts it owns; defaults are deliberately unknown.
315/// These are observations, not proof of delivery, finalization or execution.
316#[derive(Clone, Copy, Serialize)]
317pub struct DiagnosticOutcomeAxes {
318    pub operation: OperationOutcome,
319    pub physical: PhysicalDispositionFact,
320    pub business: BusinessOutcome,
321    pub business_code: Option<RegisteredDiagnosticCode>,
322    pub delivery: ResponseDelivery,
323    pub bytes_written: Option<u64>,
324    pub cleanup: CleanupOutcome,
325}
326impl Default for DiagnosticOutcomeAxes {
327    fn default() -> Self {
328        Self {
329            operation: OperationOutcome::Unknown,
330            physical: PhysicalDispositionFact::Unknown,
331            business: BusinessOutcome::Unknown,
332            business_code: None,
333            delivery: ResponseDelivery::Unknown,
334            bytes_written: None,
335            cleanup: CleanupOutcome::Unknown,
336        }
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    #[test]
344    fn bounded_causes_primary_cleanup_metadata_and_business_code() {
345        let cause = BoundedDiagnosticCause::new(
346            DiagnosticStage::RequestDb,
347            DiagnosticCode::new("db.decode").unwrap(),
348        )
349        .with_database_code(1064)
350        .with_sqlstate("42000")
351        .with_types(
352            None,
353            Some(InlineDiagnosticText::metadata("VARCHAR")),
354            Some(DiagnosticFactUnavailable::OpaqueSource),
355        );
356        let mut primary = BoundedDiagnostic::capture(
357            DiagnosticCategory::UnexpectedError,
358            CaptureSite::FirstObserved,
359            cause,
360        );
361        let id = primary.id();
362        for _ in 0..6 {
363            primary = primary.wrap(cause);
364        }
365        let p = serde_json::to_value(&primary).unwrap();
366        assert_eq!(p["diagnostic_id"], id);
367        assert_eq!(p["omitted_causes"], 3);
368        assert_eq!(p["causes"][0]["sqlstate"]["value"], "42000");
369        assert_eq!(p["causes"][0]["target_type_unavailable"], "opaque_source");
370        assert!(p["causes"][0]["actual_type_unavailable"].is_null());
371        let cleanup = BoundedDiagnostic::capture(
372            DiagnosticCategory::UnexpectedError,
373            CaptureSite::FirstObserved,
374            cause,
375        )
376        .during_cleanup_of(&primary);
377        assert_ne!(cleanup.id(), id);
378        assert_eq!(
379            serde_json::to_value(cleanup).unwrap()["primary_diagnostic_id"],
380            id
381        );
382        let secret =
383            serde_json::to_value(InlineDiagnosticText::metadata("mysql://SECRET@host")).unwrap();
384        assert_eq!(secret["redacted"], true);
385        assert!(!secret.to_string().contains("SECRET"));
386        let long = serde_json::to_value(InlineDiagnosticText::metadata(&"界".repeat(100))).unwrap();
387        assert_eq!(long["truncated"], true);
388        assert!(
389            RegisteredDiagnosticCode::from_registered("INVALID_VALUE", &["INVALID_VALUE"])
390                .is_some()
391        );
392        assert!(RegisteredDiagnosticCode::from_registered("FOREIGN", &["INVALID_VALUE"]).is_none());
393        assert!(
394            RegisteredDiagnosticCode::from_registered("SECRET@host", &["SECRET@host"]).is_none()
395        );
396    }
397}