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    pub(crate) fn primary_id_for_projection(&self) -> Option<u64> {
244        self.primary_diagnostic_id
245    }
246    pub(crate) fn simple_projection(&self) -> Option<(DiagnosticCategory, CaptureSite,
247        &'static str, u32, u32, DiagnosticStage, DiagnosticCode)> {
248        let cause = self.causes[0]?;
249        (self.cause_count == 1 && self.omitted_causes == 0
250            && cause.io_kind.is_none() && cause.os_code.is_none()
251            && cause.database_code.is_none() && cause.sqlstate.is_none()
252            && cause.column_index.is_none() && cause.column_count.is_none()
253            && cause.target_rust_type.is_none() && cause.actual_db_type.is_none()
254            && cause.object.is_none() && cause.unavailable.is_none()
255            && matches!(cause.target_type_unavailable,
256                Some(DiagnosticFactUnavailable::MetadataUnavailable))
257            && matches!(cause.actual_type_unavailable,
258                Some(DiagnosticFactUnavailable::MetadataUnavailable)))
259            .then_some((self.category, self.capture_site, self.origin_file,
260                self.origin_line, self.origin_column, cause.stage, cause.code))
261    }
262}
263
264/// Read-only correlation, not delivery confirmation or execution authority.
265///
266/// ```compile_fail
267/// use saddle_core::DiagnosticOccurrence;
268/// let forged = DiagnosticOccurrence { diagnostic_id: 1, primary_diagnostic_id: None };
269/// ```
270#[derive(Clone, Copy, Serialize)]
271pub struct DiagnosticOccurrence {
272    diagnostic_id: u64,
273    primary_diagnostic_id: Option<u64>,
274}
275impl DiagnosticOccurrence {
276    /// Compare a fixed-size capture with its original source submission.
277    pub fn matches_bounded_diagnostic(&self, diagnostic: &BoundedDiagnostic) -> bool {
278        self.diagnostic_id == diagnostic.id()
279    }
280    /// Compare source identity while allowing a later cleanup association.
281    pub fn matches_diagnostic(&self, diagnostic: &crate::Diagnostic) -> bool {
282        self.diagnostic_id == diagnostic.id()
283    }
284    pub(crate) const fn source_id(&self) -> u64 {
285        self.diagnostic_id
286    }
287    pub(crate) fn from_diagnostic(diagnostic: &crate::Diagnostic) -> Self {
288        Self {
289            diagnostic_id: diagnostic.id(),
290            primary_diagnostic_id: diagnostic.primary_id_for_projection(),
291        }
292    }
293}
294
295#[derive(Clone, Copy, Serialize)]
296#[serde(rename_all = "snake_case")]
297pub enum OperationOutcome {
298    Succeeded,
299    Rejected,
300    Failed,
301    Cancelled,
302    TimedOut,
303    Panicked,
304    Unknown,
305}
306#[derive(Clone, Copy, Serialize)]
307#[serde(rename_all = "snake_case")]
308pub enum PhysicalDispositionFact {
309    NotUsed,
310    Returned,
311    Discarded,
312    Unknown,
313}
314#[derive(Clone, Copy, Serialize)]
315#[serde(rename_all = "snake_case")]
316pub enum BusinessOutcome {
317    Success,
318    Failure,
319    Unknown,
320}
321#[derive(Clone, Copy, Serialize)]
322#[serde(rename_all = "snake_case")]
323pub enum ResponseDelivery {
324    NotStarted,
325    Partial,
326    LocalWriteComplete,
327    Failed,
328    Cancelled,
329    TimedOut,
330    Unknown,
331}
332#[derive(Clone, Copy, Serialize)]
333#[serde(rename_all = "snake_case")]
334pub enum CleanupOutcome {
335    NotRun,
336    Succeeded,
337    Failed,
338    Unknown,
339}
340
341/// Each domain reports only facts it owns; defaults are deliberately unknown.
342/// These are observations, not proof of delivery, finalization or execution.
343#[derive(Clone, Copy, Serialize)]
344pub struct DiagnosticOutcomeAxes {
345    pub operation: OperationOutcome,
346    pub physical: PhysicalDispositionFact,
347    pub business: BusinessOutcome,
348    pub business_code: Option<RegisteredDiagnosticCode>,
349    pub delivery: ResponseDelivery,
350    pub bytes_written: Option<u64>,
351    pub cleanup: CleanupOutcome,
352}
353impl Default for DiagnosticOutcomeAxes {
354    fn default() -> Self {
355        Self {
356            operation: OperationOutcome::Unknown,
357            physical: PhysicalDispositionFact::Unknown,
358            business: BusinessOutcome::Unknown,
359            business_code: None,
360            delivery: ResponseDelivery::Unknown,
361            bytes_written: None,
362            cleanup: CleanupOutcome::Unknown,
363        }
364    }
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370    #[test]
371    fn bounded_causes_primary_cleanup_metadata_and_business_code() {
372        let cause = BoundedDiagnosticCause::new(
373            DiagnosticStage::RequestDb,
374            DiagnosticCode::new("db.decode").unwrap(),
375        )
376        .with_database_code(1064)
377        .with_sqlstate("42000")
378        .with_types(
379            None,
380            Some(InlineDiagnosticText::metadata("VARCHAR")),
381            Some(DiagnosticFactUnavailable::OpaqueSource),
382        );
383        let mut primary = BoundedDiagnostic::capture(
384            DiagnosticCategory::UnexpectedError,
385            CaptureSite::FirstObserved,
386            cause,
387        );
388        let id = primary.id();
389        for _ in 0..6 {
390            primary = primary.wrap(cause);
391        }
392        let p = serde_json::to_value(&primary).unwrap();
393        assert_eq!(p["diagnostic_id"], id);
394        assert_eq!(p["omitted_causes"], 3);
395        assert_eq!(p["causes"][0]["sqlstate"]["value"], "42000");
396        assert_eq!(p["causes"][0]["target_type_unavailable"], "opaque_source");
397        assert!(p["causes"][0]["actual_type_unavailable"].is_null());
398        let cleanup = BoundedDiagnostic::capture(
399            DiagnosticCategory::UnexpectedError,
400            CaptureSite::FirstObserved,
401            cause,
402        )
403        .during_cleanup_of(&primary);
404        assert_ne!(cleanup.id(), id);
405        assert_eq!(
406            serde_json::to_value(cleanup).unwrap()["primary_diagnostic_id"],
407            id
408        );
409        let secret =
410            serde_json::to_value(InlineDiagnosticText::metadata("mysql://SECRET@host")).unwrap();
411        assert_eq!(secret["redacted"], true);
412        assert!(!secret.to_string().contains("SECRET"));
413        let long = serde_json::to_value(InlineDiagnosticText::metadata(&"界".repeat(100))).unwrap();
414        assert_eq!(long["truncated"], true);
415        assert!(
416            RegisteredDiagnosticCode::from_registered("INVALID_VALUE", &["INVALID_VALUE"])
417                .is_some()
418        );
419        assert!(RegisteredDiagnosticCode::from_registered("FOREIGN", &["INVALID_VALUE"]).is_none());
420        assert!(
421            RegisteredDiagnosticCode::from_registered("SECRET@host", &["SECRET@host"]).is_none()
422        );
423    }
424}