Skip to main content

providers_kit/
error.rs

1//! Structured failures and explicitly accessed, potentially sensitive native diagnostics.
2//!
3//! [`KitError`] formats only classification, schema labels, and numeric context. Native
4//! messages, identifiers and bodies are available through [`KitError::native_details`]
5//! for deliberate inspection; they must not be copied into ordinary logs.
6
7use std::fmt;
8use std::time::Duration;
9
10use serde::{Deserialize, Serialize};
11
12const DETAIL_STRING_LIMIT: usize = 65_536;
13const DETAIL_TOTAL_LIMIT: usize = 1_048_576;
14const DETAIL_RECORD_LIMIT: usize = 256;
15const ERROR_DEPTH_LIMIT: usize = 8;
16const ERROR_NODE_LIMIT: usize = 32;
17
18/// Evidence used to choose a portable classification; it does not imply retry safety.
19#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
20#[serde(tag = "source", rename_all = "snake_case", deny_unknown_fields)]
21pub enum ClassificationBasis {
22    /// A caller implementation explicitly selected the classification.
23    Explicit,
24    /// The status used for classification, independently of any status in the body.
25    HttpStatus {
26        /// Actual HTTP status used for this classification.
27        status: u16,
28    },
29    /// A recognized native service code.
30    NativeCode,
31    /// A recognized inner native error type, not the envelope or event type.
32    NativeErrorType,
33    /// A recognized canonical status name.
34    CanonicalStatus,
35    /// A recognized numeric RPC status.
36    RpcCode,
37    /// A recognized reason and domain in the first retained reason record.
38    NativeReason,
39    /// A native hosted outcome or an actual nonzero execution exit code.
40    NativeOutcome,
41    /// A remote failure without a more specific supported classification.
42    Unclassified,
43}
44
45/// A Kit-owned input invariant identified without disclosing caller values.
46///
47/// Labels are trusted static schema names and rules, never native diagnostic text.
48/// Caller implementations can attach one with [`KitError::with_validation_violation`].
49///
50/// ```
51/// use providers_kit::error::ValidationViolation;
52/// let violation = ValidationViolation::OutOfRange {
53///     field: "generation.temperature", rule: "finite and non-negative",
54/// };
55/// assert!(format!("{violation:?}").contains("generation.temperature"));
56/// ```
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub enum ValidationViolation {
59    /// A required field was not supplied.
60    MissingField {
61        /// Trusted field name.
62        field: &'static str,
63    },
64    /// A required value or collection was empty.
65    Empty {
66        /// Trusted field name.
67        field: &'static str,
68    },
69    /// A value did not satisfy its required syntax.
70    InvalidFormat {
71        /// Trusted field name.
72        field: &'static str,
73        /// Trusted syntax rule, without the rejected value.
74        rule: &'static str,
75    },
76    /// A value violated a non-range invariant.
77    InvalidValue {
78        /// Trusted field name.
79        field: &'static str,
80        /// Trusted value constraint.
81        rule: &'static str,
82    },
83    /// A value was outside its permitted range.
84    OutOfRange {
85        /// Trusted field name.
86        field: &'static str,
87        /// Trusted range description.
88        rule: &'static str,
89    },
90    /// A value was repeated where uniqueness is required.
91    Duplicate {
92        /// Trusted field name.
93        field: &'static str,
94    },
95    /// A value or collection exceeded a known limit.
96    LimitExceeded {
97        /// Trusted field name.
98        field: &'static str,
99        /// Maximum permitted count or size.
100        limit: usize,
101    },
102    /// Two fields cannot be used together.
103    Incompatible {
104        /// Trusted field name.
105        field: &'static str,
106        /// Trusted name of the incompatible field.
107        other: &'static str,
108    },
109    /// The selected implementation cannot support the requested input feature.
110    Unsupported {
111        /// Trusted field name.
112        field: &'static str,
113        /// Trusted capability constraint.
114        rule: &'static str,
115    },
116}
117
118/// JSON decoding category, retained independently of a formatted parser message.
119#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
120pub enum JsonErrorCategory {
121    /// Input/output failed during decoding.
122    Io,
123    /// Invalid JSON syntax.
124    Syntax,
125    /// Valid JSON did not match the expected schema.
126    Data,
127    /// Input ended before a JSON value was complete.
128    Eof,
129}
130
131/// Observed JSON value shape, including the absence of a required field.
132#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
133pub enum JsonValueKind {
134    /// The field is absent.
135    Absent,
136    /// JSON null.
137    Null,
138    /// A JSON boolean.
139    Boolean,
140    /// A JSON number.
141    Number,
142    /// A JSON string.
143    String,
144    /// A JSON array.
145    Array,
146    /// A JSON object.
147    Object,
148}
149
150impl JsonValueKind {
151    pub(crate) fn of(value: Option<&serde_json::Value>) -> Self {
152        match value {
153            None => Self::Absent,
154            Some(serde_json::Value::Null) => Self::Null,
155            Some(serde_json::Value::Bool(_)) => Self::Boolean,
156            Some(serde_json::Value::Number(_)) => Self::Number,
157            Some(serde_json::Value::String(_)) => Self::String,
158            Some(serde_json::Value::Array(_)) => Self::Array,
159            Some(serde_json::Value::Object(_)) => Self::Object,
160        }
161    }
162}
163
164/// Exact response observations, accessed deliberately because values may be sensitive.
165///
166/// The corresponding trusted field name remains in [`ResponseViolation`].
167/// Formatting this value never prints observed strings.
168#[derive(Clone, Eq, PartialEq, Serialize)]
169#[serde(tag = "type", rename_all = "snake_case")]
170pub enum ResponseDetails {
171    /// Parser location and category; line and column use the parser's numbering.
172    Json {
173        /// Parser error category.
174        category: JsonErrorCategory,
175        /// One-based line, when available from the parser.
176        line: usize,
177        /// Parser column, which may be zero for an immediate input failure.
178        column: usize,
179    },
180    /// A field was absent or had the wrong JSON shape.
181    FieldType {
182        /// Required shape.
183        expected: JsonValueKind,
184        /// Observed shape.
185        actual: JsonValueKind,
186    },
187    /// Unrecognized native discriminator text.
188    Discriminator {
189        /// Exact observed discriminator, subject to documented bounds.
190        actual: String,
191    },
192    /// Two identifiers did not agree.
193    Identity {
194        /// Expected identifier.
195        expected: String,
196        /// Received identifier.
197        actual: String,
198    },
199    /// An index was outside the available collection.
200    Index {
201        /// Received index.
202        index: usize,
203        /// Number of available entries.
204        available: usize,
205    },
206    /// Observed bytes when a response limit was exceeded.
207    Size {
208        /// Observed byte count; this may be a lower bound after early termination.
209        observed: usize,
210    },
211}
212
213impl ResponseDetails {
214    pub(crate) fn json(error: &serde_json::Error) -> Self {
215        Self::Json {
216            category: match error.classify() {
217                serde_json::error::Category::Io => JsonErrorCategory::Io,
218                serde_json::error::Category::Syntax => JsonErrorCategory::Syntax,
219                serde_json::error::Category::Data => JsonErrorCategory::Data,
220                serde_json::error::Category::Eof => JsonErrorCategory::Eof,
221            },
222            line: error.line(),
223            column: error.column(),
224        }
225    }
226
227    fn bound(&mut self, budget: &mut DetailBudget) {
228        match self {
229            Self::Discriminator { actual } => budget.string(actual),
230            Self::Identity { expected, actual } => {
231                budget.string(expected);
232                budget.string(actual);
233            }
234            Self::Json { .. } | Self::FieldType { .. } | Self::Index { .. } | Self::Size { .. } => {
235            }
236        }
237    }
238}
239
240impl fmt::Debug for ResponseDetails {
241    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
242        match self {
243            Self::Json {
244                category,
245                line,
246                column,
247            } => formatter
248                .debug_struct("Json")
249                .field("category", category)
250                .field("line", line)
251                .field("column", column)
252                .finish(),
253            Self::FieldType { expected, actual } => formatter
254                .debug_struct("FieldType")
255                .field("expected", expected)
256                .field("actual", actual)
257                .finish(),
258            Self::Index { index, available } => formatter
259                .debug_struct("Index")
260                .field("index", index)
261                .field("available", available)
262                .finish(),
263            Self::Size { observed } => formatter
264                .debug_struct("Size")
265                .field("observed", observed)
266                .finish(),
267            Self::Discriminator { .. } => {
268                formatter.write_str("Discriminator { actual: <redacted> }")
269            }
270            Self::Identity { .. } => {
271                formatter.write_str("Identity { expected: <redacted>, actual: <redacted> }")
272            }
273        }
274    }
275}
276
277/// Portable failure classification. This classification does not promise retry safety.
278#[derive(Clone, Copy, Debug, Eq, PartialEq)]
279pub enum ErrorKind {
280    /// Invalid provider configuration.
281    Configuration,
282    /// Invalid input detected before provider execution.
283    InvalidInput,
284    /// The implementation cannot perform the requested operation.
285    Unsupported,
286    /// The connection or transport failed without a more authoritative rejection.
287    Transport,
288    /// A local operation deadline elapsed.
289    Timeout,
290    /// The service reported that its deadline elapsed.
291    DeadlineExceeded,
292    /// Lifecycle admission is closed.
293    Closed,
294    /// The operation is incompatible with the current local state.
295    InvalidState,
296    /// The response violates a known schema or protocol invariant.
297    InvalidResponse(ResponseViolation),
298    /// The service rejected the request as invalid.
299    InvalidRequest,
300    /// Authentication was rejected.
301    Authentication,
302    /// The authenticated caller lacks permission.
303    PermissionDenied,
304    /// A requested model or resource was not found.
305    NotFound,
306    /// The request conflicts with current service state.
307    Conflict,
308    /// The request exceeds the service's size limit.
309    RequestTooLarge,
310    /// A service precondition is not satisfied.
311    FailedPrecondition,
312    /// Capacity or quota was exhausted; the service did not distinguish which.
313    ResourceExhausted,
314    /// The service explicitly identified a rate limit.
315    RateLimited,
316    /// The service explicitly identified exhausted quota.
317    QuotaExceeded,
318    /// Billing prevents execution.
319    Billing,
320    /// The service is unavailable or overloaded.
321    ServiceUnavailable,
322    /// The service reported an internal error.
323    Internal,
324    /// The service reported cancellation.
325    Cancelled,
326    /// A remote rejection without a recognized portable category.
327    ProviderRejected,
328}
329
330impl ErrorKind {
331    pub(crate) fn from_http_status(status: u16) -> Self {
332        match status {
333            400 | 422 => Self::InvalidRequest,
334            401 => Self::Authentication,
335            402 => Self::Billing,
336            403 => Self::PermissionDenied,
337            404 => Self::NotFound,
338            408 | 504 => Self::DeadlineExceeded,
339            409 => Self::Conflict,
340            412 => Self::FailedPrecondition,
341            413 => Self::RequestTooLarge,
342            429 => Self::ResourceExhausted,
343            500 => Self::Internal,
344            502 | 503 | 529 => Self::ServiceUnavailable,
345            _ => Self::ProviderRejected,
346        }
347    }
348    pub(crate) fn from_canonical_status(status: Option<&str>) -> ErrorKind {
349        match status {
350            Some("INVALID_ARGUMENT" | "OUT_OF_RANGE") => Self::InvalidRequest,
351            Some("UNAUTHENTICATED") => Self::Authentication,
352            Some("PERMISSION_DENIED") => Self::PermissionDenied,
353            Some("NOT_FOUND") => Self::NotFound,
354            Some("ALREADY_EXISTS" | "ABORTED") => Self::Conflict,
355            Some("RESOURCE_EXHAUSTED") => Self::ResourceExhausted,
356            Some("FAILED_PRECONDITION") => Self::FailedPrecondition,
357            Some("DEADLINE_EXCEEDED") => Self::DeadlineExceeded,
358            Some("CANCELLED") => Self::Cancelled,
359            Some("UNAVAILABLE") => Self::ServiceUnavailable,
360            Some("INTERNAL" | "DATA_LOSS" | "UNKNOWN") => Self::Internal,
361            Some("UNIMPLEMENTED") => Self::Unsupported,
362            _ => Self::ProviderRejected,
363        }
364    }
365
366    pub(crate) fn from_rpc_code(code: Option<i64>) -> ErrorKind {
367        match code {
368            Some(1) => Self::Cancelled,
369            Some(2 | 13 | 15) => Self::Internal,
370            Some(3 | 11) => Self::InvalidRequest,
371            Some(4) => Self::DeadlineExceeded,
372            Some(5) => Self::NotFound,
373            Some(6 | 10) => Self::Conflict,
374            Some(7) => Self::PermissionDenied,
375            Some(8) => Self::ResourceExhausted,
376            Some(9) => Self::FailedPrecondition,
377            Some(12) => Self::Unsupported,
378            Some(14) => Self::ServiceUnavailable,
379            Some(16) => Self::Authentication,
380            _ => Self::ProviderRejected,
381        }
382    }
383}
384
385/// A response invariant, assigned at its detection site without interpreting prose.
386///
387/// Field and rule labels are trusted, static schema descriptions. Implementors must
388/// never place credentials, payloads, URLs or native messages in these labels.
389#[derive(Clone, Copy, Debug, Eq, PartialEq)]
390pub enum ResponseViolation {
391    /// JSON could not be decoded into the expected response schema.
392    MalformedJson,
393    /// Text bytes were not valid UTF-8.
394    InvalidUtf8,
395    /// A required response field was absent.
396    MissingField {
397        /// Static schema field name, never the field's value.
398        field: &'static str,
399    },
400    /// A present response field had an invalid value.
401    InvalidField {
402        /// Static schema field name, never the field's value.
403        field: &'static str,
404    },
405    /// A response discriminator was not recognized.
406    UnknownVariant {
407        /// Static discriminator field name.
408        field: &'static str,
409    },
410    /// Response identifiers did not agree.
411    IdentityMismatch {
412        /// Static identity field name.
413        field: &'static str,
414    },
415    /// The response violated an ordering or semantic invariant.
416    ProtocolViolation {
417        /// Trusted, static description of the invariant.
418        rule: &'static str,
419    },
420    /// A response stream ended before its terminal event.
421    UnexpectedEnd,
422    /// Response data exceeded a byte limit.
423    LimitExceeded {
424        /// Maximum permitted bytes.
425        limit: usize,
426    },
427}
428
429impl ResponseViolation {
430    pub(crate) fn from_json_error(error: &serde_json::Error, field: &'static str) -> Self {
431        match error.classify() {
432            serde_json::error::Category::Data => Self::InvalidField { field },
433            serde_json::error::Category::Syntax | serde_json::error::Category::Eof => {
434                Self::MalformedJson
435            }
436            serde_json::error::Category::Io => Self::ProtocolViolation {
437                rule: "JSON input read failure",
438            },
439        }
440    }
441}
442
443/// Caller-visible operation associated with a failure.
444#[derive(Clone, Copy, Debug, Eq, PartialEq)]
445pub enum Operation {
446    /// No enclosing behavioral call is available (for example, a standalone DTO check).
447    Unspecified,
448    /// Provider construction.
449    Configure,
450    /// Standalone input validation.
451    ValidateInput,
452    /// Exact model selection.
453    SelectModel,
454    /// Model discovery.
455    ListModels,
456    /// Model metadata lookup.
457    ModelInfo,
458    /// File upload.
459    UploadFile,
460    /// File listing.
461    ListFiles,
462    /// File metadata lookup.
463    FileInfo,
464    /// File deletion.
465    DeleteFile,
466    /// Bounded file download.
467    DownloadFile,
468    /// Batch status lookup.
469    BatchStatus,
470    /// Batch cancellation.
471    CancelBatch,
472    /// Bounded batch result retrieval and normalization.
473    BatchResults,
474    /// Batch listing.
475    ListBatches,
476    /// Runtime health check.
477    Health,
478    /// Shared provider shutdown.
479    Shutdown,
480    /// Unary generation.
481    Generate,
482    /// Streaming generation, including stream consumption.
483    GenerateStream,
484    /// Embedding generation.
485    Embed,
486    /// Batch submission.
487    SubmitBatch,
488    /// Stateful session opening.
489    OpenSession,
490    /// Sending a session command.
491    SessionSend,
492    /// Receiving a session event.
493    SessionReceive,
494    /// Session closure.
495    SessionClose,
496}
497
498/// Stage at which the failure was observed; it does not assert that execution is safe to retry.
499#[derive(Clone, Copy, Debug, Eq, PartialEq)]
500pub enum ErrorPhase {
501    /// A caller implementation supplied no finer phase.
502    Unspecified,
503    /// Provider configuration validation.
504    Configuration,
505    /// Local request validation.
506    InputValidation,
507    /// Request encoding or construction.
508    RequestBuild,
509    /// Sending a request or opening its transport.
510    RequestSend,
511    /// Reading or validating the response status and headers.
512    ResponseHeaders,
513    /// Reading response bytes.
514    ReadBody,
515    /// Decoding a response or frame.
516    Decode,
517    /// Converting decoded native data to portable values.
518    Normalize,
519    /// A service error or protocol failure during a generation stream.
520    Stream,
521    /// Session connection negotiation.
522    SessionHandshake,
523    /// Sending a session command.
524    SessionSend,
525    /// Receiving a session event.
526    SessionReceive,
527    /// Closing a session connection.
528    SessionClose,
529    /// Lifecycle admission or shutdown.
530    Lifecycle,
531    /// Releasing owned resources after an operation.
532    Cleanup,
533}
534
535/// Safe operation context, including the location of a failed batch result row.
536///
537/// ```
538/// use providers_kit::error::{ErrorContext, ErrorPhase, Operation};
539/// let context = ErrorContext {
540///     operation: Operation::BatchResults, phase: ErrorPhase::Normalize, row: Some(0),
541/// };
542/// assert_eq!(context.row, Some(0));
543/// ```
544#[derive(Clone, Copy, Debug, Eq, PartialEq)]
545pub struct ErrorContext {
546    /// The enclosing public operation.
547    pub operation: Operation,
548    /// The stage at which this error was detected.
549    pub phase: ErrorPhase,
550    /// Zero-based logical batch result row, excluding blank JSONL lines.
551    pub row: Option<usize>,
552}
553
554/// One structured native fact with its meaning preserved across providers.
555///
556/// Unknown native fields remain in the bounded body instead of an untyped public map.
557#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)]
558#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
559pub enum NativeDetail {
560    /// A reason is meaningful within its reported domain.
561    Reason {
562        /// Native reason domain.
563        domain: String,
564        /// Native reason value.
565        reason: String,
566    },
567    /// A resource explicitly identified by the service, not a caller correlation ID.
568    Resource {
569        /// Native resource type, if supplied.
570        resource_type: Option<String>,
571        /// Exact native resource name.
572        name: String,
573        /// Native resource owner, if supplied.
574        owner: Option<String>,
575        /// Native description, if supplied.
576        description: Option<String>,
577    },
578    /// Native request field validation failure.
579    FieldViolation {
580        /// Native field path.
581        field: String,
582        /// Native reason, when supplied.
583        reason: Option<String>,
584        /// Native diagnostic prose, when supplied.
585        description: Option<String>,
586    },
587    /// Native retry guidance; it does not authorize an automatic retry.
588    RetryDelay {
589        /// Whole nonnegative seconds.
590        seconds: u64,
591        /// Reported fractional nanoseconds.
592        nanos: u32,
593    },
594    /// Native quota violation.
595    QuotaViolation {
596        /// Native quota subject, when supplied.
597        subject: Option<String>,
598        /// Native diagnostic prose, when supplied.
599        description: Option<String>,
600    },
601    /// Native precondition failure.
602    PreconditionViolation {
603        /// Native precondition kind, when supplied.
604        kind: Option<String>,
605        /// Native subject, when supplied.
606        subject: Option<String>,
607        /// Native diagnostic prose, when supplied.
608        description: Option<String>,
609    },
610}
611
612impl NativeDetail {
613    fn strings(&self) -> [Option<&str>; 4] {
614        match self {
615            Self::Reason { domain, reason } => [Some(domain), Some(reason), None, None],
616            Self::Resource {
617                resource_type,
618                name,
619                owner,
620                description,
621            } => [
622                resource_type.as_deref(),
623                Some(name),
624                owner.as_deref(),
625                description.as_deref(),
626            ],
627            Self::FieldViolation {
628                field,
629                reason,
630                description,
631            } => [Some(field), reason.as_deref(), description.as_deref(), None],
632            Self::QuotaViolation {
633                subject,
634                description,
635            } => [subject.as_deref(), description.as_deref(), None, None],
636            Self::PreconditionViolation {
637                kind,
638                subject,
639                description,
640            } => [
641                kind.as_deref(),
642                subject.as_deref(),
643                description.as_deref(),
644                None,
645            ],
646            Self::RetryDelay { .. } => [None, None, None, None],
647        }
648    }
649
650    fn bound(&mut self, budget: &mut DetailBudget) {
651        match self {
652            Self::Reason { domain, reason } => {
653                budget.string(domain);
654                budget.string(reason);
655            }
656            Self::Resource {
657                resource_type,
658                name,
659                owner,
660                description,
661            } => {
662                budget.string(name);
663                for value in [resource_type, owner, description].into_iter().flatten() {
664                    budget.string(value);
665                }
666            }
667            Self::FieldViolation {
668                field,
669                reason,
670                description,
671            } => {
672                budget.string(field);
673                for value in [reason, description].into_iter().flatten() {
674                    budget.string(value);
675                }
676            }
677            Self::QuotaViolation {
678                subject,
679                description,
680            } => {
681                for value in [subject, description].into_iter().flatten() {
682                    budget.string(value);
683                }
684            }
685            Self::PreconditionViolation {
686                kind,
687                subject,
688                description,
689            } => {
690                for value in [kind, subject, description].into_iter().flatten() {
691                    budget.string(value);
692                }
693            }
694            Self::RetryDelay { .. } => {}
695        }
696    }
697}
698
699impl fmt::Debug for NativeDetail {
700    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
701        let name = match self {
702            Self::Reason { .. } => "Reason",
703            Self::Resource { .. } => "Resource",
704            Self::FieldViolation { .. } => "FieldViolation",
705            Self::RetryDelay { .. } => "RetryDelay",
706            Self::QuotaViolation { .. } => "QuotaViolation",
707            Self::PreconditionViolation { .. } => "PreconditionViolation",
708        };
709        formatter.write_str(name)?;
710        formatter.write_str(" { details: <redacted> }")
711    }
712}
713
714struct DetailBudget {
715    remaining: usize,
716    remaining_nodes: usize,
717    truncated: bool,
718}
719
720impl DetailBudget {
721    fn new() -> Self {
722        Self {
723            remaining: DETAIL_TOTAL_LIMIT,
724            remaining_nodes: ERROR_NODE_LIMIT,
725            truncated: false,
726        }
727    }
728
729    fn string(&mut self, value: &mut String) {
730        let limit = DETAIL_STRING_LIMIT.min(self.remaining);
731        if value.len() > limit {
732            let mut end = limit;
733            while !value.is_char_boundary(end) {
734                end -= 1;
735            }
736            value.truncate(end);
737            self.truncated = true;
738        }
739        self.remaining -= value.len();
740    }
741}
742
743/// Potentially sensitive native details, available only through explicit inspection.
744///
745/// ```
746/// use providers_kit::error::NativeErrorDetails;
747/// let details = NativeErrorDetails { message: Some("private payload".into()), ..Default::default() };
748/// assert!(!format!("{details:?}").contains("private payload"));
749/// ```
750///
751/// All string fields are bounded when attached to [`KitError`]. `Debug` deliberately
752/// hides them, including native codes and request IDs, because a service or proxy can
753/// return arbitrary text in any of them. Raw bodies retain provider-specific fields
754/// not covered by the portable metadata vocabulary.
755#[derive(Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
756#[serde(default, deny_unknown_fields)]
757pub struct NativeErrorDetails {
758    /// Native or Kit diagnostic prose, potentially containing caller input.
759    pub message: Option<String>,
760    /// Bounded native response or frame body, potentially containing secrets.
761    pub body: Option<String>,
762    /// Native error code; its meaning depends on the wire protocol.
763    pub code: Option<String>,
764    /// Native error type, distinct from its code.
765    pub error_type: Option<String>,
766    /// Native event discriminator, distinct from the inner error type.
767    pub event_type: Option<String>,
768    /// Native hosted execution outcome.
769    pub outcome: Option<String>,
770    /// Native parameter associated with the failure.
771    pub parameter: Option<String>,
772    /// Native request correlation ID.
773    pub request_id: Option<String>,
774    /// ID of the server event reporting the failure.
775    pub event_id: Option<String>,
776    /// ID of the client event that caused the failure.
777    pub related_event_id: Option<String>,
778    /// Native tool-call correlation, never a missing-resource identifier.
779    pub tool_call_id: Option<String>,
780    /// Native stream event sequence number, when reported.
781    pub sequence_number: Option<u64>,
782    /// Native status name (for example, a Google RPC canonical status).
783    pub native_status: Option<String>,
784    /// HTTP status reported by a native body or embedded result, not an invented outer status.
785    pub reported_http_status: Option<u16>,
786    /// Native RPC canonical numeric code, distinct from HTTP status.
787    pub rpc_code: Option<i64>,
788    /// Native execution exit code, distinct from HTTP and RPC codes.
789    pub execution_exit_code: Option<i64>,
790    /// Native Retry-After header; no automatic retry is implied.
791    pub retry_after: Option<String>,
792    /// Request URL, potentially containing credentials or private resource names.
793    pub url: Option<String>,
794    /// Repeated structured native evidence; at most 256 records are retained.
795    pub details: Vec<NativeDetail>,
796    /// At least one detail was truncated or the body was known to be incomplete.
797    pub truncated: bool,
798    /// Number of structured native records omitted during bounded capture.
799    pub omitted_details: usize,
800}
801
802impl NativeErrorDetails {
803    fn bound(&mut self) {
804        let mut budget = DetailBudget::new();
805        self.bound_with_budget(&mut budget);
806    }
807
808    fn bound_with_budget(&mut self, budget: &mut DetailBudget) {
809        // Reserve the named classification evidence before optional diagnostics.
810        for value in [
811            &mut self.code,
812            &mut self.error_type,
813            &mut self.native_status,
814            &mut self.outcome,
815        ]
816        .into_iter()
817        .flatten()
818        {
819            budget.string(value);
820        }
821        // The first reason is the one used by a NativeReason classifier. Retain
822        // that record even when it occurs after the general record limit.
823        let reason_index = self
824            .details
825            .iter()
826            .position(|detail| matches!(detail, NativeDetail::Reason { .. }));
827        if let Some(index) = reason_index {
828            self.details[index].bound(budget);
829            if index >= DETAIL_RECORD_LIMIT {
830                let reason = self.details.remove(index);
831                self.details.insert(0, reason);
832            }
833        }
834        let omitted = self.details.len().saturating_sub(DETAIL_RECORD_LIMIT);
835        self.omitted_details = self.omitted_details.saturating_add(omitted);
836        self.details.truncate(DETAIL_RECORD_LIMIT);
837        let retained_reason = self
838            .details
839            .iter()
840            .position(|detail| matches!(detail, NativeDetail::Reason { .. }));
841        for (index, detail) in self.details.iter_mut().enumerate() {
842            if Some(index) != retained_reason {
843                detail.bound(budget);
844            }
845        }
846        for value in [
847            &mut self.event_type,
848            &mut self.parameter,
849            &mut self.request_id,
850            &mut self.event_id,
851            &mut self.related_event_id,
852            &mut self.tool_call_id,
853            &mut self.retry_after,
854            &mut self.url,
855            &mut self.message,
856            &mut self.body,
857        ]
858        .into_iter()
859        .flatten()
860        {
861            budget.string(value);
862        }
863        self.truncated |= budget.truncated || self.omitted_details != 0;
864    }
865
866    fn validate(&self) -> Result<(), KitError> {
867        if self.details.len() > DETAIL_RECORD_LIMIT {
868            return Err(KitError::invalid_input(
869                ValidationViolation::LimitExceeded {
870                    field: "native_details.details",
871                    limit: DETAIL_RECORD_LIMIT,
872                },
873                "native details exceed 256 records",
874            ));
875        }
876        if self.omitted_details != 0 && !self.truncated {
877            return Err(KitError::invalid_input(
878                ValidationViolation::InvalidValue {
879                    field: "native_details.truncated",
880                    rule: "required when detail records were omitted",
881                },
882                "omitted native details require a truncation marker",
883            ));
884        }
885        let named = [
886            self.message.as_deref(),
887            self.body.as_deref(),
888            self.code.as_deref(),
889            self.error_type.as_deref(),
890            self.event_type.as_deref(),
891            self.outcome.as_deref(),
892            self.parameter.as_deref(),
893            self.request_id.as_deref(),
894            self.event_id.as_deref(),
895            self.related_event_id.as_deref(),
896            self.tool_call_id.as_deref(),
897            self.native_status.as_deref(),
898            self.retry_after.as_deref(),
899            self.url.as_deref(),
900        ];
901        let strings = named.into_iter().flatten().chain(
902            self.details
903                .iter()
904                .flat_map(|detail| detail.strings().into_iter().flatten()),
905        );
906        let mut total = 0_usize;
907        for value in strings {
908            if value.len() > DETAIL_STRING_LIMIT {
909                return Err(KitError::invalid_input(
910                    ValidationViolation::LimitExceeded {
911                        field: "native_details.string",
912                        limit: DETAIL_STRING_LIMIT,
913                    },
914                    "native detail exceeds 64 KiB",
915                ));
916            }
917            total = total.saturating_add(value.len());
918        }
919        if total > DETAIL_TOTAL_LIMIT {
920            return Err(KitError::invalid_input(
921                ValidationViolation::LimitExceeded {
922                    field: "native_details",
923                    limit: DETAIL_TOTAL_LIMIT,
924                },
925                "native details exceed 1 MiB in total",
926            ));
927        }
928        Ok(())
929    }
930}
931
932impl fmt::Debug for NativeErrorDetails {
933    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
934        formatter
935            .debug_struct("NativeErrorDetails")
936            .field("details", &"<redacted>")
937            .field("truncated", &self.truncated)
938            .field("omitted_details", &self.omitted_details)
939            .finish()
940    }
941}
942
943#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
944#[serde(rename_all = "snake_case")]
945enum RemoteCategory {
946    Unsupported,
947    InvalidRequest,
948    Authentication,
949    PermissionDenied,
950    NotFound,
951    Conflict,
952    RequestTooLarge,
953    FailedPrecondition,
954    ResourceExhausted,
955    RateLimited,
956    QuotaExceeded,
957    Billing,
958    ServiceUnavailable,
959    Internal,
960    DeadlineExceeded,
961    Cancelled,
962    ProviderRejected,
963}
964
965impl RemoteCategory {
966    fn from_kind(kind: ErrorKind) -> Result<Self, KitError> {
967        match kind {
968            ErrorKind::Unsupported => Ok(Self::Unsupported),
969            ErrorKind::InvalidRequest => Ok(Self::InvalidRequest),
970            ErrorKind::Authentication => Ok(Self::Authentication),
971            ErrorKind::PermissionDenied => Ok(Self::PermissionDenied),
972            ErrorKind::NotFound => Ok(Self::NotFound),
973            ErrorKind::Conflict => Ok(Self::Conflict),
974            ErrorKind::RequestTooLarge => Ok(Self::RequestTooLarge),
975            ErrorKind::FailedPrecondition => Ok(Self::FailedPrecondition),
976            ErrorKind::ResourceExhausted => Ok(Self::ResourceExhausted),
977            ErrorKind::RateLimited => Ok(Self::RateLimited),
978            ErrorKind::QuotaExceeded => Ok(Self::QuotaExceeded),
979            ErrorKind::Billing => Ok(Self::Billing),
980            ErrorKind::ServiceUnavailable => Ok(Self::ServiceUnavailable),
981            ErrorKind::Internal => Ok(Self::Internal),
982            ErrorKind::DeadlineExceeded => Ok(Self::DeadlineExceeded),
983            ErrorKind::Cancelled => Ok(Self::Cancelled),
984            ErrorKind::ProviderRejected => Ok(Self::ProviderRejected),
985            ErrorKind::Configuration
986            | ErrorKind::InvalidInput
987            | ErrorKind::Transport
988            | ErrorKind::Timeout
989            | ErrorKind::Closed
990            | ErrorKind::InvalidState
991            | ErrorKind::InvalidResponse(_) => Err(KitError::invalid_input(
992                ValidationViolation::InvalidValue {
993                    field: "remote_failure.kind",
994                    rule: "must classify a remote outcome",
995                },
996                "remote failure cannot use a local or response-schema classification",
997            )),
998        }
999    }
1000
1001    fn kind(self) -> ErrorKind {
1002        match self {
1003            Self::Unsupported => ErrorKind::Unsupported,
1004            Self::InvalidRequest => ErrorKind::InvalidRequest,
1005            Self::Authentication => ErrorKind::Authentication,
1006            Self::PermissionDenied => ErrorKind::PermissionDenied,
1007            Self::NotFound => ErrorKind::NotFound,
1008            Self::Conflict => ErrorKind::Conflict,
1009            Self::RequestTooLarge => ErrorKind::RequestTooLarge,
1010            Self::FailedPrecondition => ErrorKind::FailedPrecondition,
1011            Self::ResourceExhausted => ErrorKind::ResourceExhausted,
1012            Self::RateLimited => ErrorKind::RateLimited,
1013            Self::QuotaExceeded => ErrorKind::QuotaExceeded,
1014            Self::Billing => ErrorKind::Billing,
1015            Self::ServiceUnavailable => ErrorKind::ServiceUnavailable,
1016            Self::Internal => ErrorKind::Internal,
1017            Self::DeadlineExceeded => ErrorKind::DeadlineExceeded,
1018            Self::Cancelled => ErrorKind::Cancelled,
1019            Self::ProviderRejected => ErrorKind::ProviderRejected,
1020        }
1021    }
1022}
1023
1024/// An opaque remote failure, including failures inside successful batch or hosted results.
1025///
1026/// Serialization deliberately exports all retained native evidence, including the
1027/// bounded body. It is not a redacted logging format. Deserialization rejects invalid
1028/// categories, inconsistent evidence and values exceeding the documented bounds.
1029///
1030/// ```
1031/// use providers_kit::error::{ErrorKind, NativeErrorDetails, RemoteFailure};
1032/// let failure = RemoteFailure::new(ErrorKind::NotFound, NativeErrorDetails::default())?;
1033/// assert_eq!(failure.kind(), ErrorKind::NotFound);
1034///
1035/// # Ok::<(), providers_kit::KitError>(())
1036/// ```
1037#[derive(Clone, Eq, PartialEq)]
1038pub struct RemoteFailure {
1039    category: RemoteCategory,
1040    basis: ClassificationBasis,
1041    details: Box<NativeErrorDetails>,
1042}
1043
1044impl RemoteFailure {
1045    /// Constructs an explicitly classified caller failure without claiming native inference.
1046    ///
1047    /// Details must already fit 64 KiB per string, 1 MiB total string bytes, and 256
1048    /// repeated records. Oversized caller evidence is rejected rather than discarded.
1049    ///
1050    /// # Errors
1051    ///
1052    /// Returns an input error for local-only categories, invalid numeric/detail fields, or evidence exceeding the documented capture bounds. Explicit classification does not authenticate origin or establish retry safety.
1053    ///
1054    /// # Examples
1055    ///
1056    /// ```
1057    /// use providers_kit::error::{RemoteFailure, NativeErrorDetails, ErrorKind, ClassificationBasis};
1058    /// let failure = RemoteFailure::new(
1059    ///     ErrorKind::NotFound,
1060    ///     NativeErrorDetails {
1061    ///         message: Some("private detail".into()),
1062    ///         ..Default::default()
1063    ///     },
1064    /// )?;
1065    /// assert_eq!(failure.kind(), ErrorKind::NotFound);
1066    /// assert_eq!(failure.classification_basis(), ClassificationBasis::Explicit);
1067    /// assert_eq!(failure.native_details().message.as_deref(), Some("private detail"));
1068    /// assert!(!format!("{failure:?}").contains("private detail"));
1069    ///
1070    /// # Ok::<(), providers_kit::KitError>(())
1071    /// ```
1072    ///
1073    pub fn new(kind: ErrorKind, details: NativeErrorDetails) -> Result<Self, KitError> {
1074        let category = RemoteCategory::from_kind(kind)?;
1075        details.validate()?;
1076        Ok(Self {
1077            category,
1078            basis: ClassificationBasis::Explicit,
1079            details: Box::new(details),
1080        })
1081    }
1082
1083    /// Returns the same portable classification vocabulary used by [`KitError`].
1084    ///
1085    /// See [`crate::error::RemoteFailure::new`] for a complete example.
1086    ///
1087    pub fn kind(&self) -> ErrorKind {
1088        self.category.kind()
1089    }
1090
1091    /// Returns the evidence used for classification, independently of the retained body.
1092    ///
1093    /// See [`crate::error::RemoteFailure::new`] for a complete example.
1094    ///
1095    pub fn classification_basis(&self) -> ClassificationBasis {
1096        self.basis
1097    }
1098
1099    /// Explicitly exposes potentially sensitive native evidence.
1100    ///
1101    /// See [`crate::error::RemoteFailure::new`] for a complete example.
1102    ///
1103    pub fn native_details(&self) -> &NativeErrorDetails {
1104        &self.details
1105    }
1106
1107    pub(crate) fn from_native(
1108        kind: ErrorKind,
1109        basis: ClassificationBasis,
1110        mut details: NativeErrorDetails,
1111    ) -> Result<Self, KitError> {
1112        let category = RemoteCategory::from_kind(kind)?;
1113        details.bound();
1114        let failure = Self {
1115            category,
1116            basis,
1117            details: Box::new(details),
1118        };
1119        failure.validate()?;
1120        Ok(failure)
1121    }
1122
1123    fn validate(&self) -> Result<(), KitError> {
1124        self.details.validate()?;
1125        let present = match self.basis {
1126            ClassificationBasis::Explicit => true,
1127            ClassificationBasis::HttpStatus { status } => {
1128                (100..=599).contains(&status)
1129                    && !(200..300).contains(&status)
1130                    && self.kind() == ErrorKind::from_http_status(status)
1131            }
1132            ClassificationBasis::NativeCode => self
1133                .details
1134                .code
1135                .as_deref()
1136                .is_some_and(|value| !value.trim().is_empty()),
1137            ClassificationBasis::NativeErrorType => self
1138                .details
1139                .error_type
1140                .as_deref()
1141                .is_some_and(|value| !value.trim().is_empty()),
1142            ClassificationBasis::CanonicalStatus => {
1143                self.details.native_status.as_deref().is_some_and(|value| {
1144                    let kind = ErrorKind::from_canonical_status(Some(value));
1145                    kind != ErrorKind::ProviderRejected && self.kind() == kind
1146                })
1147            }
1148            ClassificationBasis::RpcCode => self.details.rpc_code.is_some_and(|code| {
1149                let kind = ErrorKind::from_rpc_code(Some(code));
1150                kind != ErrorKind::ProviderRejected && self.kind() == kind
1151            }),
1152            ClassificationBasis::NativeReason => self
1153                .details
1154                .details
1155                .iter()
1156                .find_map(|detail| match detail {
1157                    NativeDetail::Reason { domain, reason } => {
1158                        Some(!domain.trim().is_empty() && !reason.trim().is_empty())
1159                    }
1160                    _ => None,
1161                })
1162                .unwrap_or(false),
1163            ClassificationBasis::NativeOutcome => match self.details.outcome.as_deref() {
1164                Some("OUTCOME_FAILED" | "URL_RETRIEVAL_STATUS_ERROR") => {
1165                    self.kind() == ErrorKind::ProviderRejected
1166                }
1167                Some("OUTCOME_DEADLINE_EXCEEDED") => self.kind() == ErrorKind::DeadlineExceeded,
1168                Some("URL_RETRIEVAL_STATUS_PAYWALL" | "URL_RETRIEVAL_STATUS_UNSAFE") => {
1169                    self.kind() == ErrorKind::PermissionDenied
1170                }
1171                None => {
1172                    self.details
1173                        .execution_exit_code
1174                        .is_some_and(|code| code != 0)
1175                        && self.kind() == ErrorKind::ProviderRejected
1176                }
1177                Some(_) => false,
1178            },
1179            ClassificationBasis::Unclassified => self.category == RemoteCategory::ProviderRejected,
1180        };
1181        if !present {
1182            return Err(KitError::invalid_input(
1183                ValidationViolation::InvalidValue {
1184                    field: "remote_failure.basis",
1185                    rule: "requires corresponding classification evidence",
1186                },
1187                "remote classification basis does not have corresponding evidence",
1188            ));
1189        }
1190        Ok(())
1191    }
1192}
1193
1194impl Serialize for RemoteFailure {
1195    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1196    where
1197        S: serde::Serializer,
1198    {
1199        #[derive(Serialize)]
1200        struct Wire<'a> {
1201            kind: RemoteCategory,
1202            basis: ClassificationBasis,
1203            details: &'a NativeErrorDetails,
1204        }
1205        Wire {
1206            kind: self.category,
1207            basis: self.basis,
1208            details: &self.details,
1209        }
1210        .serialize(serializer)
1211    }
1212}
1213
1214impl<'de> Deserialize<'de> for RemoteFailure {
1215    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1216    where
1217        D: serde::Deserializer<'de>,
1218    {
1219        #[derive(Deserialize)]
1220        #[serde(deny_unknown_fields)]
1221        struct Wire {
1222            kind: RemoteCategory,
1223            basis: ClassificationBasis,
1224            details: NativeErrorDetails,
1225        }
1226        let wire = Wire::deserialize(deserializer)?;
1227        let failure = Self {
1228            category: wire.kind,
1229            basis: wire.basis,
1230            details: Box::new(wire.details),
1231        };
1232        failure.validate().map_err(serde::de::Error::custom)?;
1233        Ok(failure)
1234    }
1235}
1236
1237impl fmt::Display for RemoteFailure {
1238    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1239        write!(formatter, "remote {:?} ({:?})", self.kind(), self.basis)
1240    }
1241}
1242
1243impl fmt::Debug for RemoteFailure {
1244    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1245        formatter
1246            .debug_struct("RemoteFailure")
1247            .field("kind", &self.kind())
1248            .field("basis", &self.basis)
1249            .field("details", &self.details)
1250            .finish()
1251    }
1252}
1253
1254impl std::error::Error for RemoteFailure {}
1255
1256#[derive(Clone)]
1257enum ErrorFailure {
1258    Classified {
1259        kind: ErrorKind,
1260        details: Box<NativeErrorDetails>,
1261    },
1262    Remote(RemoteFailure),
1263}
1264
1265impl ErrorFailure {
1266    fn kind(&self) -> ErrorKind {
1267        match self {
1268            Self::Classified { kind, .. } => *kind,
1269            Self::Remote(failure) => failure.kind(),
1270        }
1271    }
1272
1273    fn details(&self) -> &NativeErrorDetails {
1274        match self {
1275            Self::Classified { details, .. } => details,
1276            Self::Remote(failure) => &failure.details,
1277        }
1278    }
1279
1280    fn details_mut(&mut self) -> &mut NativeErrorDetails {
1281        match self {
1282            Self::Classified { details, .. } => details,
1283            Self::Remote(failure) => &mut failure.details,
1284        }
1285    }
1286}
1287
1288#[derive(Clone)]
1289struct ErrorData {
1290    failure: ErrorFailure,
1291    validation: Option<ValidationViolation>,
1292    truncated: bool,
1293    status: Option<u16>,
1294    timeout: Option<Duration>,
1295    response: Option<ResponseDetails>,
1296    request_target: Option<String>,
1297    correlation_id: Option<String>,
1298    cause: Option<Box<KitError>>,
1299    supplementary: Vec<KitError>,
1300}
1301
1302impl ErrorData {
1303    fn bound(&mut self) {
1304        self.bound_tree(&mut DetailBudget::new(), 1);
1305    }
1306
1307    fn bound_tree(&mut self, budget: &mut DetailBudget, depth: usize) {
1308        budget.remaining_nodes -= 1;
1309        let previous_truncation = budget.truncated;
1310        budget.truncated = false;
1311        // Retain this node's observations and local correlation before its native body.
1312        if let Some(response) = self.response.as_mut() {
1313            response.bound(budget);
1314        }
1315        for value in [&mut self.request_target, &mut self.correlation_id]
1316            .into_iter()
1317            .flatten()
1318        {
1319            budget.string(value);
1320        }
1321        self.failure.details_mut().bound_with_budget(budget);
1322        self.truncated |= self.failure.details().truncated;
1323        if depth == ERROR_DEPTH_LIMIT {
1324            self.truncated |= self.cause.is_some() || !self.supplementary.is_empty();
1325            self.cause = None;
1326            self.supplementary.clear();
1327        } else {
1328            if self.cause.as_ref().is_some_and(|cause| {
1329                budget.remaining_nodes == 0 || !cause.data.retains_remote_evidence(budget)
1330            }) {
1331                self.cause = None;
1332                self.truncated = true;
1333            } else if let Some(cause) = self.cause.as_mut() {
1334                cause.data.bound_tree(budget, depth + 1);
1335            }
1336            let mut omitted = false;
1337            self.supplementary.retain_mut(|child| {
1338                if budget.remaining_nodes == 0 || !child.data.retains_remote_evidence(budget) {
1339                    omitted = true;
1340                    false
1341                } else {
1342                    child.data.bound_tree(budget, depth + 1);
1343                    true
1344                }
1345            });
1346            self.truncated |= omitted;
1347        }
1348        self.truncated |= budget.truncated;
1349        budget.truncated |= previous_truncation || self.truncated;
1350    }
1351
1352    fn retains_remote_evidence(&self, available: &DetailBudget) -> bool {
1353        let ErrorFailure::Remote(failure) = &self.failure else {
1354            return true;
1355        };
1356        let mut budget = DetailBudget {
1357            remaining: available.remaining,
1358            remaining_nodes: available.remaining_nodes,
1359            truncated: false,
1360        };
1361        if let Some(mut response) = self.response.clone() {
1362            response.bound(&mut budget);
1363        }
1364        for value in [&self.request_target, &self.correlation_id]
1365            .into_iter()
1366            .flatten()
1367        {
1368            budget.string(&mut value.clone());
1369        }
1370        let mut retained = failure.clone();
1371        retained.details.bound_with_budget(&mut budget);
1372        // A child is optional diagnostic context. Omit its subtree instead of
1373        // exposing an invalid serializable RemoteFailure or changing its basis.
1374        retained.validate().is_ok()
1375    }
1376}
1377
1378/// Structured error returned by the portable provider API.
1379///
1380/// ```
1381/// use providers_kit::KitError;
1382/// use providers_kit::error::{ErrorKind, ErrorPhase, Operation, ResponseViolation};
1383/// let error = KitError::new(
1384///     ErrorKind::InvalidResponse(ResponseViolation::MissingField { field: "modelVersion" }),
1385///     Operation::BatchResults,
1386///     ErrorPhase::Normalize,
1387/// ).with_row(0);
1388/// assert_eq!(error.context().row, Some(0));
1389/// assert!(error.to_string().contains("modelVersion"));
1390/// ```
1391///
1392/// Runtime errors are deliberately not serializable. Accepted remote outcomes use
1393/// [`RemoteFailure`]; runtime errors may contain trusted static schema labels and
1394/// causal Kit failures that must not be reconstructed from untrusted JSON.
1395///
1396/// ```compile_fail
1397/// use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1398/// let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::RequestSend);
1399/// let _ = serde_json::to_string(&error);
1400/// ```
1401#[derive(Clone)]
1402pub struct KitError {
1403    context: ErrorContext,
1404    data: Box<ErrorData>,
1405}
1406
1407impl KitError {
1408    /// Constructs a classified failure without native details.
1409    ///
1410    /// ```
1411    /// use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1412    /// let error = KitError::new(ErrorKind::Unsupported, Operation::Embed, ErrorPhase::InputValidation);
1413    /// assert_eq!(error.kind(), ErrorKind::Unsupported);
1414    /// ```
1415    pub fn new(kind: ErrorKind, operation: Operation, phase: ErrorPhase) -> Self {
1416        Self {
1417            context: ErrorContext {
1418                operation,
1419                phase,
1420                row: None,
1421            },
1422            data: Box::new(ErrorData {
1423                failure: ErrorFailure::Classified {
1424                    kind,
1425                    details: Box::default(),
1426                },
1427                validation: None,
1428                truncated: false,
1429                status: None,
1430                timeout: None,
1431                response: None,
1432                request_target: None,
1433                correlation_id: None,
1434                cause: None,
1435                supplementary: Vec::new(),
1436            }),
1437        }
1438    }
1439
1440    /// Attaches a remote failure to the operation that observed it.
1441    ///
1442    /// This does not invent an outer HTTP status from an embedded result or stream.
1443    ///
1444    /// # Examples
1445    ///
1446    /// ```
1447    /// use providers_kit::error::{RemoteFailure, NativeErrorDetails, ErrorKind, ClassificationBasis};
1448    /// let failure = RemoteFailure::new(
1449    ///     ErrorKind::NotFound,
1450    ///     NativeErrorDetails {
1451    ///         message: Some("private detail".into()),
1452    ///         ..Default::default()
1453    ///     },
1454    /// )?;
1455    /// use providers_kit::error::{KitError, Operation, ErrorPhase};
1456    /// let error = KitError::from_remote(failure, Operation::FileInfo, ErrorPhase::ReadBody);
1457    /// assert_eq!(error.kind(), ErrorKind::NotFound);
1458    ///
1459    /// # Ok::<(), providers_kit::KitError>(())
1460    /// ```
1461    ///
1462    pub fn from_remote(failure: RemoteFailure, operation: Operation, phase: ErrorPhase) -> Self {
1463        Self::new(failure.kind(), operation, phase).with_remote_failure(failure)
1464    }
1465
1466    pub(crate) fn with_remote_failure(mut self, failure: RemoteFailure) -> Self {
1467        self.data.failure = ErrorFailure::Remote(failure);
1468        self.data.bound();
1469        self
1470    }
1471
1472    /// Returns the basis for classification; caller construction remains explicit.
1473    ///
1474    /// # Examples
1475    ///
1476    /// ```
1477    /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1478    /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1479    /// let _basis = error.classification_basis();
1480    /// ```
1481    ///
1482    pub fn classification_basis(&self) -> ClassificationBasis {
1483        match &self.data.failure {
1484            ErrorFailure::Classified { .. } => ClassificationBasis::Explicit,
1485            ErrorFailure::Remote(failure) => failure.classification_basis(),
1486        }
1487    }
1488
1489    /// Returns an attached remote failure, if one was represented explicitly.
1490    ///
1491    /// Absence does not establish that the implementation made no service request.
1492    ///
1493    /// # Examples
1494    ///
1495    /// ```
1496    /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1497    /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1498    /// assert!(error.remote_failure().is_none());
1499    /// ```
1500    ///
1501    pub fn remote_failure(&self) -> Option<&RemoteFailure> {
1502        match &self.data.failure {
1503            ErrorFailure::Classified { .. } => None,
1504            ErrorFailure::Remote(failure) => Some(failure),
1505        }
1506    }
1507
1508    /// Explicitly exposes potentially sensitive observations for a response violation.
1509    ///
1510    /// # Examples
1511    ///
1512    /// ```
1513    /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1514    /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1515    /// assert!(error.response_details().is_none());
1516    /// ```
1517    ///
1518    pub fn response_details(&self) -> Option<&ResponseDetails> {
1519        self.data.response.as_ref()
1520    }
1521
1522    /// Returns the safe, detector-assigned local input invariant, when applicable.
1523    ///
1524    /// ```
1525    /// use providers_kit::{generation::GenerationConfig, error::ValidationViolation};
1526    /// let error = GenerationConfig::default().with_temperature(-1.0).unwrap_err();
1527    /// assert!(matches!(error.validation_violation(), Some(ValidationViolation::OutOfRange {
1528    ///     field: "generation.temperature", ..
1529    /// })));
1530    /// ```
1531    pub fn validation_violation(&self) -> Option<&ValidationViolation> {
1532        self.data.validation.as_ref()
1533    }
1534
1535    /// Attaches trusted field/rule labels without exposing rejected caller values.
1536    ///
1537    /// ```
1538    /// use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation, ValidationViolation}};
1539    /// let error = KitError::new(ErrorKind::InvalidInput, Operation::ValidateInput, ErrorPhase::InputValidation)
1540    ///     .with_validation_violation(ValidationViolation::Empty { field: "messages" });
1541    /// assert_eq!(error.validation_violation(), Some(&ValidationViolation::Empty { field: "messages" }));
1542    /// ```
1543    pub fn with_validation_violation(mut self, violation: ValidationViolation) -> Self {
1544        self.data.validation = Some(violation);
1545        self
1546    }
1547
1548    /// Reports whether this tree lost diagnostics to string, record, depth, or node bounds.
1549    ///
1550    /// A tree retains at most eight levels, 32 nodes, and 1 MiB of diagnostic strings.
1551    /// The primary error is retained before its cause subtree and supplementary errors.
1552    ///
1553    /// ```
1554    /// use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, NativeErrorDetails, Operation}};
1555    /// let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody)
1556    ///     .with_native_details(NativeErrorDetails {
1557    ///         message: Some("x".repeat(65_537)), ..Default::default()
1558    ///     });
1559    /// assert!(error.diagnostics_truncated());
1560    /// ```
1561    pub fn diagnostics_truncated(&self) -> bool {
1562        self.data.truncated
1563    }
1564
1565    /// Returns the cause of this failure, separately from cleanup or diagnostic failures.
1566    ///
1567    /// # Examples
1568    ///
1569    /// ```
1570    /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1571    /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1572    /// assert!(error.cause().is_none());
1573    /// ```
1574    ///
1575    pub fn cause(&self) -> Option<&Self> {
1576        self.data.cause.as_deref()
1577    }
1578
1579    /// Explicitly exposes the local requested target; this is not native attribution.
1580    ///
1581    /// # Examples
1582    ///
1583    /// ```
1584    /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1585    /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1586    /// assert!(error.request_target().is_none());
1587    /// ```
1588    ///
1589    pub fn request_target(&self) -> Option<&str> {
1590        self.data.request_target.as_deref()
1591    }
1592
1593    /// Explicitly exposes caller correlation, such as a batch custom ID.
1594    ///
1595    /// # Examples
1596    ///
1597    /// ```
1598    /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1599    /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1600    /// assert!(error.correlation_id().is_none());
1601    /// ```
1602    ///
1603    pub fn correlation_id(&self) -> Option<&str> {
1604        self.data.correlation_id.as_deref()
1605    }
1606
1607    /// Retains response observations without placing native values in trusted labels.
1608    ///
1609    /// # Examples
1610    ///
1611    /// ```
1612    /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1613    /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1614    /// let details = providers_kit::error::ResponseDetails::Json {
1615    ///     category: providers_kit::error::JsonErrorCategory::Syntax,
1616    ///     line: 1,
1617    ///     column: 2,
1618    /// };
1619    /// let error = error.with_response_details(details);
1620    /// assert!(error.response_details().is_some());
1621    /// ```
1622    ///
1623    pub fn with_response_details(mut self, details: ResponseDetails) -> Self {
1624        self.data.response = Some(details);
1625        self.data.bound();
1626        self
1627    }
1628
1629    /// Retains an actual causal failure. Independent cleanup belongs in supplementary errors.
1630    ///
1631    /// # Examples
1632    ///
1633    /// ```
1634    /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1635    /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1636    /// let cause = KitError::new(ErrorKind::Timeout, Operation::Generate, ErrorPhase::ReadBody);
1637    /// let error = error.with_cause(cause);
1638    /// assert_eq!(error.cause().unwrap().kind(), ErrorKind::Timeout);
1639    /// ```
1640    ///
1641    pub fn with_cause(mut self, cause: Self) -> Self {
1642        self.data.cause = Some(Box::new(cause));
1643        self.data.bound();
1644        self
1645    }
1646
1647    /// Retains a bounded local target without asserting that the provider identified it.
1648    ///
1649    /// # Examples
1650    ///
1651    /// ```
1652    /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1653    /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1654    /// let error = error.with_request_target("private-resource");
1655    /// assert_eq!(error.request_target(), Some("private-resource"));
1656    /// assert!(!format!("{error}").contains("private-resource"));
1657    /// ```
1658    ///
1659    pub fn with_request_target(mut self, target: impl Into<String>) -> Self {
1660        self.data.request_target = Some(target.into());
1661        self.data.bound();
1662        self
1663    }
1664
1665    /// Retains bounded caller correlation without treating it as a missing-resource identity.
1666    ///
1667    /// # Examples
1668    ///
1669    /// ```
1670    /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1671    /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1672    /// let error = error.with_correlation_id("row-1");
1673    /// assert_eq!(error.correlation_id(), Some("row-1"));
1674    /// ```
1675    ///
1676    pub fn with_correlation_id(mut self, id: impl Into<String>) -> Self {
1677        self.data.correlation_id = Some(id.into());
1678        self.data.bound();
1679        self
1680    }
1681
1682    /// Returns the portable classification without interpreting native prose.
1683    ///
1684    /// ```
1685    ///
1686    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1687    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1688    /// assert_eq!(error.kind(), ErrorKind::Transport);
1689    /// ```
1690    pub fn kind(&self) -> ErrorKind {
1691        self.data.failure.kind()
1692    }
1693    /// Returns safe operation, phase and batch row context.
1694    ///
1695    /// ```
1696    ///
1697    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1698    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1699    /// assert_eq!(error.context().operation, Operation::Generate);
1700    /// ```
1701    pub fn context(&self) -> &ErrorContext {
1702        &self.context
1703    }
1704    /// Returns the actual unsuccessful HTTP status, if one was received.
1705    ///
1706    /// ```
1707    ///
1708    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1709    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1710    /// assert_eq!(error.http_status(), None);
1711    /// ```
1712    pub fn http_status(&self) -> Option<u16> {
1713        self.data.status
1714    }
1715    /// Returns the local deadline that expired, if available.
1716    ///
1717    /// ```
1718    ///
1719    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1720    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1721    /// assert_eq!(error.timeout(), None);
1722    /// ```
1723    pub fn timeout(&self) -> Option<Duration> {
1724        self.data.timeout
1725    }
1726    /// Explicitly exposes potentially sensitive native details; do not log them by default.
1727    ///
1728    /// ```
1729    ///
1730    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1731    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1732    /// assert!(error.native_details().message.is_none());
1733    /// ```
1734    pub fn native_details(&self) -> &NativeErrorDetails {
1735        self.data.failure.details()
1736    }
1737    /// Returns failures collecting diagnostics or releasing resources after the primary failure.
1738    ///
1739    /// ```
1740    ///
1741    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1742    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1743    /// assert!(error.supplementary().is_empty());
1744    /// ```
1745    pub fn supplementary(&self) -> &[KitError] {
1746        &self.data.supplementary
1747    }
1748
1749    /// Attaches an observed HTTP status, never a synthetic status inferred from an event code.
1750    ///
1751    /// ```
1752    ///
1753    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1754    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1755    /// let error = error.with_http_status(503);
1756    /// assert_eq!(error.http_status(), Some(503));
1757    /// ```
1758    pub fn with_http_status(mut self, status: u16) -> Self {
1759        self.data.status = Some(status);
1760        self
1761    }
1762    /// Attaches a local deadline; service timeout responses retain their native classification.
1763    ///
1764    /// ```
1765    ///
1766    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1767    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1768    /// let error = error.with_timeout(std::time::Duration::from_secs(10));
1769    /// assert_eq!(error.timeout(), Some(std::time::Duration::from_secs(10)));
1770    /// ```
1771    pub fn with_timeout(mut self, timeout: Duration) -> Self {
1772        self.data.timeout = Some(timeout);
1773        self
1774    }
1775    /// Attaches the zero-based logical batch row, excluding blank JSONL lines.
1776    ///
1777    /// ```
1778    ///
1779    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1780    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1781    /// let error = error.with_row(2);
1782    /// assert_eq!(error.context().row, Some(2));
1783    /// ```
1784    pub fn with_row(mut self, row: usize) -> Self {
1785        self.context.row = Some(row);
1786        self
1787    }
1788    /// Attaches explicit native details, bounding each string to 64 KiB on UTF-8 boundaries.
1789    ///
1790    /// ```
1791    ///
1792    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1793    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1794    /// let details = providers_kit::error::NativeErrorDetails {
1795    ///     request_id: Some("sensitive-correlation-id".into()),
1796    ///     ..Default::default()
1797    /// };
1798    /// let error = error.with_native_details(details);
1799    /// assert!(!error.to_string().contains("sensitive-correlation-id"));
1800    /// ```
1801    pub fn with_native_details(mut self, mut details: NativeErrorDetails) -> Self {
1802        details.bound();
1803        // Replacing classification evidence is explicit caller construction; it
1804        // must not preserve a stale adapter-derived basis over unrelated details.
1805        match &mut self.data.failure {
1806            ErrorFailure::Classified {
1807                details: current, ..
1808            } => **current = details,
1809            ErrorFailure::Remote(failure) => {
1810                *failure.details = details;
1811                failure.basis = ClassificationBasis::Explicit;
1812            }
1813        }
1814        self.data.bound();
1815        self
1816    }
1817    /// Preserves a secondary failure without replacing this failure's classification or context.
1818    ///
1819    /// ```
1820    ///
1821    /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1822    /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1823    /// let secondary = KitError::new(ErrorKind::Timeout, Operation::Generate, ErrorPhase::ReadBody);
1824    /// let error = error.with_supplementary(secondary);
1825    /// assert_eq!(error.kind(), ErrorKind::Transport);
1826    /// assert_eq!(error.supplementary()[0].kind(), ErrorKind::Timeout);
1827    /// ```
1828    pub fn with_supplementary(mut self, error: Self) -> Self {
1829        self.data.supplementary.push(error);
1830        self.data.bound();
1831        self
1832    }
1833
1834    pub(crate) fn at_operation(mut self, operation: Operation) -> Self {
1835        self.context.operation = operation;
1836        if let Some(cause) = self.data.cause.take() {
1837            self.data.cause = Some(Box::new(
1838                if cause.context.operation == Operation::Unspecified {
1839                    cause.at_operation(operation)
1840                } else {
1841                    *cause
1842                },
1843            ));
1844        }
1845        self.data.supplementary = self
1846            .data
1847            .supplementary
1848            .into_iter()
1849            .map(|error| {
1850                if error.context.operation == Operation::Unspecified {
1851                    error.at_operation(operation)
1852                } else {
1853                    error
1854                }
1855            })
1856            .collect();
1857        self
1858    }
1859    pub(crate) fn at_phase(mut self, phase: ErrorPhase) -> Self {
1860        self.context.phase = phase;
1861        self
1862    }
1863    pub(crate) fn with_message(mut self, message: impl Into<String>) -> Self {
1864        self.data.failure.details_mut().message = Some(message.into());
1865        self.data.bound();
1866        self
1867    }
1868    pub(crate) fn with_body(mut self, body: Option<String>) -> Self {
1869        self.data.failure.details_mut().body = body;
1870        self.data.bound();
1871        self
1872    }
1873    pub(crate) fn with_diagnostic(mut self, details: Option<NativeErrorDetails>) -> Self {
1874        if let Some(details) = details {
1875            self.data.failure.details_mut().body = details.body;
1876            self.data.failure.details_mut().truncated |= details.truncated;
1877        }
1878        self.data.bound();
1879        self
1880    }
1881    // Adapter-only enrichment preserves the already-validated native basis.
1882    pub(crate) fn with_native_capture(mut self, details: NativeErrorDetails) -> Self {
1883        *self.data.failure.details_mut() = details;
1884        self.data.bound();
1885        self
1886    }
1887    pub(crate) fn config(violation: ValidationViolation, message: impl Into<String>) -> Self {
1888        Self::new(
1889            ErrorKind::Configuration,
1890            Operation::Configure,
1891            ErrorPhase::Configuration,
1892        )
1893        .with_validation_violation(violation)
1894        .with_message(message)
1895    }
1896    pub(crate) fn invalid_input(
1897        violation: ValidationViolation,
1898        message: impl Into<String>,
1899    ) -> Self {
1900        Self::new(
1901            ErrorKind::InvalidInput,
1902            Operation::ValidateInput,
1903            ErrorPhase::InputValidation,
1904        )
1905        .with_validation_violation(violation)
1906        .with_message(message)
1907    }
1908    pub(crate) fn invalid_response(reason: ResponseViolation) -> Self {
1909        Self::new(
1910            ErrorKind::InvalidResponse(reason),
1911            Operation::Unspecified,
1912            ErrorPhase::Normalize,
1913        )
1914    }
1915    pub(crate) fn unsupported(violation: ValidationViolation, message: impl Into<String>) -> Self {
1916        Self::new(
1917            ErrorKind::Unsupported,
1918            Operation::Unspecified,
1919            ErrorPhase::InputValidation,
1920        )
1921        .with_validation_violation(violation)
1922        .with_message(message)
1923    }
1924    pub(crate) fn invalid_state(message: impl Into<String>) -> Self {
1925        Self::new(
1926            ErrorKind::InvalidState,
1927            Operation::Unspecified,
1928            ErrorPhase::Lifecycle,
1929        )
1930        .with_message(message)
1931    }
1932    pub(crate) fn closed(message: impl Into<String>) -> Self {
1933        Self::new(
1934            ErrorKind::Closed,
1935            Operation::Unspecified,
1936            ErrorPhase::Lifecycle,
1937        )
1938        .with_message(message)
1939    }
1940}
1941
1942impl fmt::Display for KitError {
1943    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1944        write!(
1945            formatter,
1946            "{:?} during {:?}/{:?}",
1947            self.kind(),
1948            self.context.operation,
1949            self.context.phase
1950        )?;
1951        if let Some(violation) = self.validation_violation() {
1952            write!(formatter, " ({violation:?})")?;
1953        }
1954        if self.diagnostics_truncated() {
1955            formatter.write_str(" (diagnostics truncated)")?;
1956        }
1957        if let Some(status) = self.http_status() {
1958            write!(formatter, " (HTTP {status})")?;
1959        }
1960        if let Some(row) = self.context.row {
1961            write!(formatter, " (row {row})")?;
1962        }
1963        if let Some(timeout) = self.timeout() {
1964            write!(formatter, " (deadline {timeout:?})")?;
1965        }
1966        if !self.supplementary().is_empty() {
1967            write!(
1968                formatter,
1969                " ({} supplementary failures)",
1970                self.supplementary().len()
1971            )?;
1972        }
1973        Ok(())
1974    }
1975}
1976
1977impl fmt::Debug for KitError {
1978    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1979        formatter
1980            .debug_struct("KitError")
1981            .field("kind", &self.kind())
1982            .field("basis", &self.classification_basis())
1983            .field("context", &self.context)
1984            .field("validation", &self.validation_violation())
1985            .field("diagnostics_truncated", &self.diagnostics_truncated())
1986            .field("http_status", &self.http_status())
1987            .field("timeout", &self.timeout())
1988            .field("native", &self.native_details())
1989            .field("response", &self.response_details())
1990            .field("cause", &self.cause())
1991            .field("supplementary", &self.supplementary())
1992            .finish()
1993    }
1994}
1995
1996impl std::error::Error for KitError {
1997    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1998        if let Some(cause) = self.cause() {
1999            return Some(cause);
2000        }
2001        self.remote_failure()
2002            .map(|failure| failure as &(dyn std::error::Error + 'static))
2003    }
2004}