Skip to main content

type_bridge_contract/
sdk_diagnostic.rs

1//! Versioned in-memory diagnostics for binding-neutral SDK execution.
2//!
3//! This module is deliberately separate from [`crate::diagnostic`]. The
4//! existing diagnostic is part of canonical contract wires; an SDK execution
5//! diagnostic is an in-memory engine value that bindings project through their
6//! own versioned ABI. It therefore implements no serialization contract.
7//!
8//! Dynamic provider messages, credentials, endpoint or custom-root paths,
9//! query values, and internal backtraces have no representation here. Stable
10//! messages borrow implementation-owned static text. Codes and names are
11//! bounded owned identifiers so canonical typed-query fields can cross this
12//! seam without leaking storage or accepting free-form provider text. Dynamic
13//! context remains limited to the closed typed detail vocabulary.
14
15use std::collections::BTreeMap;
16use std::error::Error;
17use std::fmt;
18
19use crate::capability::CapabilityId;
20use crate::fingerprint::Fingerprint;
21use crate::id::{RoleId, TypeId};
22use crate::schema::OwnsFactId;
23use crate::value::ValueTypeTag;
24
25/// Maximum bytes in one stable execution-diagnostic code.
26pub const MAX_SDK_DIAGNOSTIC_CODE_BYTES: usize = 128;
27/// Maximum UTF-8 bytes in one stable execution-diagnostic message.
28pub const MAX_SDK_DIAGNOSTIC_MESSAGE_BYTES: usize = 512;
29/// Maximum bytes in one stable path name or detail key.
30pub const MAX_SDK_DIAGNOSTIC_NAME_BYTES: usize = 128;
31/// Maximum typed path segments in one execution diagnostic.
32pub const MAX_SDK_DIAGNOSTIC_PATH_SEGMENTS: usize = 32;
33/// Maximum typed details in one execution diagnostic.
34pub const MAX_SDK_DIAGNOSTIC_DETAILS: usize = 32;
35/// Maximum UTF-8 bytes in one query-owned diagnostic identity.
36pub const MAX_SDK_QUERY_DIAGNOSTIC_IDENTITY_BYTES: usize = 512;
37/// Maximum identities in one query diagnostic list detail.
38pub const MAX_SDK_QUERY_DIAGNOSTIC_IDENTITY_LIST: usize = 32;
39
40/// The version of the in-memory SDK execution-diagnostic contract.
41///
42/// The numeric value is projected explicitly by each binding. It is not a
43/// Rust layout or a serialization discriminant.
44#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
45pub struct SdkDiagnosticVersion(u16);
46
47impl SdkDiagnosticVersion {
48    /// The first SDK execution-diagnostic contract.
49    pub const V1: Self = Self(1);
50    /// The version produced by constructors in this module.
51    pub const CURRENT: Self = Self::V1;
52
53    /// Return the stable positive version number.
54    #[must_use]
55    pub const fn get(self) -> u16 {
56        self.0
57    }
58}
59
60/// Stable binding-neutral SDK execution failure categories.
61///
62/// This is a new, forward-extensible category vocabulary. It intentionally
63/// does not add variants to the released [`crate::diagnostic::DiagnosticCategory`].
64#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
65#[non_exhaustive]
66pub enum SdkDiagnosticCategory {
67    /// Caller input does not satisfy the generated SDK contract.
68    InvalidInput,
69    /// The selected runtime or provider cannot execute a required capability.
70    UnsupportedCapability,
71    /// A configured or implementation-defined resource ceiling was exceeded.
72    ResourceLimit,
73    /// Schema, projection, database, request, or result identity did not match.
74    Integrity,
75    /// The database provider could not complete an operation.
76    Provider,
77    /// Transaction lifecycle or commit certainty prevents a successful result.
78    Transaction,
79    /// The operation was cancelled before or during execution.
80    Cancelled,
81    /// TypeBridge failed internally without exposing implementation state.
82    Internal,
83}
84
85impl SdkDiagnosticCategory {
86    /// Return the stable language-neutral category spelling.
87    #[must_use]
88    pub const fn as_str(self) -> &'static str {
89        match self {
90            Self::InvalidInput => "invalid_input",
91            Self::UnsupportedCapability => "unsupported_capability",
92            Self::ResourceLimit => "resource_limit",
93            Self::Integrity => "integrity",
94            Self::Provider => "provider",
95            Self::Transaction => "transaction",
96            Self::Cancelled => "cancelled",
97            Self::Internal => "internal",
98        }
99    }
100}
101
102impl fmt::Display for SdkDiagnosticCategory {
103    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
104        formatter.write_str(self.as_str())
105    }
106}
107
108/// Why an SDK diagnostic component or bounded collection was rejected.
109#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110#[non_exhaustive]
111pub enum SdkDiagnosticBuildError {
112    /// A code was not canonical lowercase snake case or exceeded its ceiling.
113    InvalidCode,
114    /// A message was empty, oversized, multiline, path-like, or not trimmed.
115    InvalidMessage,
116    /// A path name or detail key was not canonical lowercase snake case.
117    InvalidName,
118    /// The typed path exceeded [`MAX_SDK_DIAGNOSTIC_PATH_SEGMENTS`].
119    PathLimitExceeded,
120    /// The detail map exceeded [`MAX_SDK_DIAGNOSTIC_DETAILS`].
121    DetailLimitExceeded,
122    /// The same stable detail key was attached more than once.
123    DuplicateDetail,
124}
125
126impl fmt::Display for SdkDiagnosticBuildError {
127    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
128        let message = match self {
129            Self::InvalidCode => {
130                "SDK diagnostic code must be bounded canonical lowercase snake case"
131            }
132            Self::InvalidMessage => {
133                "SDK diagnostic message must be bounded, trimmed, single-line, and path-free static text"
134            }
135            Self::InvalidName => {
136                "SDK diagnostic name must be bounded canonical lowercase snake case"
137            }
138            Self::PathLimitExceeded => "SDK diagnostic path exceeds its segment ceiling",
139            Self::DetailLimitExceeded => "SDK diagnostic details exceed their entry ceiling",
140            Self::DuplicateDetail => "SDK diagnostic detail key is duplicated",
141        };
142        formatter.write_str(message)
143    }
144}
145
146impl Error for SdkDiagnosticBuildError {}
147
148/// A validated stable machine-readable execution-diagnostic code.
149///
150/// Codes are bounded canonical identifiers. Lowering code must still admit
151/// only engine-owned stable codes, never provider messages or query values.
152#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
153pub struct SdkDiagnosticCode(String);
154
155impl SdkDiagnosticCode {
156    /// Validate one implementation-owned code.
157    pub fn new(value: impl Into<String>) -> Result<Self, SdkDiagnosticBuildError> {
158        let value = value.into();
159        if is_canonical_snake_name(&value, MAX_SDK_DIAGNOSTIC_CODE_BYTES) {
160            Ok(Self(value))
161        } else {
162            Err(SdkDiagnosticBuildError::InvalidCode)
163        }
164    }
165
166    /// Return the stable code spelling.
167    #[must_use]
168    pub fn as_str(&self) -> &str {
169        &self.0
170    }
171}
172
173impl fmt::Display for SdkDiagnosticCode {
174    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
175        formatter.write_str(self.as_str())
176    }
177}
178
179/// A validated stable human-readable execution-diagnostic message.
180///
181/// The message is implementation-owned static text, not a formatting target.
182/// Runtime context belongs only in [`SdkDiagnosticDetailValue`]. Path
183/// separators and control characters are rejected so an endpoint, custom-root
184/// path, or backtrace cannot be copied into this field.
185#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
186pub struct SdkDiagnosticMessage(&'static str);
187
188impl SdkDiagnosticMessage {
189    /// Validate one implementation-owned message.
190    pub fn new(value: &'static str) -> Result<Self, SdkDiagnosticBuildError> {
191        let valid = !value.is_empty()
192            && value.len() <= MAX_SDK_DIAGNOSTIC_MESSAGE_BYTES
193            && value.trim() == value
194            && !value
195                .chars()
196                .any(|character| character.is_control() || matches!(character, '/' | '\\'));
197        if valid {
198            Ok(Self(value))
199        } else {
200            Err(SdkDiagnosticBuildError::InvalidMessage)
201        }
202    }
203
204    /// Return the stable message text.
205    #[must_use]
206    pub const fn as_str(self) -> &'static str {
207        self.0
208    }
209}
210
211impl fmt::Display for SdkDiagnosticMessage {
212    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
213        formatter.write_str(self.as_str())
214    }
215}
216
217/// A validated stable argument name or detail key.
218///
219/// Names are bounded canonical identifiers. Dynamic schema identities use the
220/// typed path and detail variants instead of being flattened into names.
221#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
222pub struct SdkDiagnosticName(String);
223
224impl SdkDiagnosticName {
225    /// Validate one implementation-owned name.
226    pub fn new(value: impl Into<String>) -> Result<Self, SdkDiagnosticBuildError> {
227        let value = value.into();
228        if is_canonical_snake_name(&value, MAX_SDK_DIAGNOSTIC_NAME_BYTES) {
229            Ok(Self(value))
230        } else {
231            Err(SdkDiagnosticBuildError::InvalidName)
232        }
233    }
234
235    /// Return the stable name spelling.
236    #[must_use]
237    pub fn as_str(&self) -> &str {
238        &self.0
239    }
240}
241
242impl fmt::Display for SdkDiagnosticName {
243    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
244        formatter.write_str(self.as_str())
245    }
246}
247
248/// A bounded query-owned identity retained by a structured SDK diagnostic.
249///
250/// This is not a free-text diagnostic channel. The constructor accepts only a
251/// single-line, whitespace-free identifier shape and rejects path separators,
252/// control characters, and unbounded text. Query lowering additionally admits
253/// identities only for closed, case-specific detail keys.
254#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
255pub struct SdkQueryDiagnosticIdentity(String);
256
257impl SdkQueryDiagnosticIdentity {
258    /// Validate one bounded query identity.
259    pub fn new(value: impl Into<String>) -> Result<Self, SdkDiagnosticBuildError> {
260        let value = value.into();
261        let valid = !value.is_empty()
262            && value.len() <= MAX_SDK_QUERY_DIAGNOSTIC_IDENTITY_BYTES
263            && !value.chars().any(|character| {
264                character.is_control()
265                    || character.is_whitespace()
266                    || matches!(character, '/' | '\\' | '@')
267            });
268        if valid {
269            Ok(Self(value))
270        } else {
271            Err(SdkDiagnosticBuildError::InvalidName)
272        }
273    }
274
275    /// Return the exact validated identity spelling.
276    #[must_use]
277    pub fn as_str(&self) -> &str {
278        &self.0
279    }
280}
281
282impl fmt::Display for SdkQueryDiagnosticIdentity {
283    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
284        formatter.write_str(self.as_str())
285    }
286}
287
288/// The exact canonical typed-query failure category retained inside an SDK
289/// diagnostic whose outer category is binding-neutral.
290#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
291#[non_exhaustive]
292pub enum SdkQueryDiagnosticCategory {
293    /// The immutable query plan is invalid.
294    InvalidPlan,
295    /// A terminal cardinality contract was not satisfied.
296    Cardinality,
297    /// The provider lacks a required query capability.
298    UnsupportedCapability,
299    /// Request-relevant schema changed after validation.
300    StaleSchema,
301    /// A query processing ceiling was crossed.
302    ResourceLimit,
303    /// Cooperative cancellation interrupted query processing.
304    Cancelled,
305    /// The provider failed before complete evidence was available.
306    Provider,
307    /// Provider evidence did not match the validated invocation.
308    ResultDecode,
309}
310
311impl SdkQueryDiagnosticCategory {
312    /// Return the canonical typed-query category spelling.
313    #[must_use]
314    pub const fn as_str(self) -> &'static str {
315        match self {
316            Self::InvalidPlan => "invalid_plan",
317            Self::Cardinality => "cardinality",
318            Self::UnsupportedCapability => "unsupported_capability",
319            Self::StaleSchema => "stale_schema",
320            Self::ResourceLimit => "resource_limit",
321            Self::Cancelled => "cancelled",
322            Self::Provider => "provider",
323            Self::ResultDecode => "result_decode",
324        }
325    }
326}
327
328/// One closed structural location within a typed query request or result.
329#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
330#[non_exhaustive]
331pub enum SdkQueryDiagnosticPathKind {
332    /// The request envelope.
333    Request,
334    /// The graph plan.
335    Plan,
336    /// The selected terminal operation.
337    Operation,
338    /// The predicate tree.
339    Predicate,
340    /// The declared output shape.
341    Output,
342    /// Provider solution or hydration evidence.
343    ProviderEvidence,
344    /// The validated result envelope.
345    Result,
346}
347
348impl SdkQueryDiagnosticPathKind {
349    /// Return the stable structural spelling.
350    #[must_use]
351    pub const fn as_str(self) -> &'static str {
352        match self {
353            Self::Request => "request",
354            Self::Plan => "plan",
355            Self::Operation => "operation",
356            Self::Predicate => "predicate",
357            Self::Output => "output",
358            Self::ProviderEvidence => "provider_evidence",
359            Self::Result => "result",
360        }
361    }
362}
363
364fn is_canonical_snake_name(value: impl AsRef<str>, maximum_bytes: usize) -> bool {
365    let value = value.as_ref();
366    let bytes = value.as_bytes();
367    if bytes.is_empty()
368        || bytes.len() > maximum_bytes
369        || !bytes[0].is_ascii_lowercase()
370        || !bytes[bytes.len() - 1].is_ascii_lowercase() && !bytes[bytes.len() - 1].is_ascii_digit()
371    {
372        return false;
373    }
374
375    let mut previous_was_underscore = false;
376    for byte in bytes {
377        if *byte == b'_' {
378            if previous_was_underscore {
379                return false;
380            }
381            previous_was_underscore = true;
382        } else if byte.is_ascii_lowercase() || byte.is_ascii_digit() {
383            previous_was_underscore = false;
384        } else {
385            return false;
386        }
387    }
388    true
389}
390
391/// One typed location within an SDK operation input or projected model.
392#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
393#[non_exhaustive]
394pub enum SdkDiagnosticPathSegment {
395    /// A stable SDK operation argument.
396    Argument(SdkDiagnosticName),
397    /// A zero-based item position in a bounded input collection.
398    Index(u64),
399    /// A canonical schema type identity.
400    Type(TypeId),
401    /// A canonical direct ownership-fact identity.
402    Field(OwnsFactId),
403    /// A canonical relation-qualified role identity.
404    Role(RoleId),
405    /// A structural typed-query request or result location.
406    Query(SdkQueryDiagnosticPathKind),
407    /// A plan-local typed-query binding ordinal.
408    QueryBinding(u16),
409    /// A descriptor-qualified, binding-facing typed-query field identity.
410    QueryField {
411        /// Kind-qualified registry descriptor identity.
412        owner: SdkQueryDiagnosticIdentity,
413        /// Binding-facing field name.
414        name: SdkQueryDiagnosticIdentity,
415    },
416    /// A descriptor-qualified typed-query role identity.
417    QueryRole {
418        /// Kind-qualified registry descriptor identity.
419        owner: SdkQueryDiagnosticIdentity,
420        /// Binding-facing role name.
421        name: SdkQueryDiagnosticIdentity,
422    },
423    /// A plan-local typed-query role-edge ordinal.
424    QueryRoleEdge(u16),
425    /// A zero-based positional typed-query output slot.
426    QueryOutputSlot(u64),
427    /// A declared generated typed-query output member.
428    QueryOutputName(SdkQueryDiagnosticIdentity),
429    /// An object field retained from one authenticated contract diagnostic.
430    ContractField(SdkQueryDiagnosticIdentity),
431    /// A typed identifier retained from one authenticated contract diagnostic.
432    ContractIdentity(SdkQueryDiagnosticIdentity),
433}
434
435/// A provider operation class safe to expose across SDK boundaries.
436///
437/// Provider-specific error text, codes, endpoints, and query text are not
438/// retained. Commit is intentionally absent: failed commits use
439/// [`SdkCommitFailureOutcome`] so their durability certainty cannot be lost.
440#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
441#[non_exhaustive]
442pub enum SdkProviderOperation {
443    /// Establish or verify a direct database connection.
444    Connect,
445    /// Read or install schema authority.
446    Schema,
447    /// Open a read transaction.
448    OpenReadTransaction,
449    /// Open a write transaction.
450    OpenWriteTransaction,
451    /// Execute a read operation.
452    Read,
453    /// Execute a write operation before commit.
454    Write,
455    /// Roll back a write transaction.
456    Rollback,
457    /// Close a provider-owned resource.
458    Close,
459}
460
461impl SdkProviderOperation {
462    /// Return the stable language-neutral operation spelling.
463    #[must_use]
464    pub const fn as_str(self) -> &'static str {
465        match self {
466            Self::Connect => "connect",
467            Self::Schema => "schema",
468            Self::OpenReadTransaction => "open_read_transaction",
469            Self::OpenWriteTransaction => "open_write_transaction",
470            Self::Read => "read",
471            Self::Write => "write",
472            Self::Rollback => "rollback",
473            Self::Close => "close",
474        }
475    }
476}
477
478/// Durability certainty for a failed transaction commit.
479#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
480#[non_exhaustive]
481pub enum SdkCommitFailureOutcome {
482    /// The provider proves that the transaction did not commit.
483    DefinitelyAborted,
484    /// The provider cannot determine whether the transaction committed.
485    Unknown,
486}
487
488impl SdkCommitFailureOutcome {
489    /// Return the stable language-neutral outcome spelling.
490    #[must_use]
491    pub const fn as_str(self) -> &'static str {
492        match self {
493            Self::DefinitelyAborted => "definitely_aborted",
494            Self::Unknown => "unknown",
495        }
496    }
497}
498
499/// One bounded typed SDK execution-diagnostic detail value.
500///
501/// There is deliberately no free-text or byte-string variant. Every dynamic
502/// value is either a bounded canonical identity or a non-sensitive scalar
503/// classification.
504#[derive(Clone, Debug, Eq, PartialEq)]
505#[non_exhaustive]
506pub enum SdkDiagnosticDetailValue {
507    /// A boolean contract fact.
508    Boolean(bool),
509    /// A non-negative item or occurrence count.
510    Count(u64),
511    /// A non-negative byte count.
512    ByteCount(u64),
513    /// A canonical capability identity.
514    Capability(CapabilityId),
515    /// A canonical scalar-domain identity.
516    ValueType(ValueTypeTag),
517    /// A canonical schema type identity.
518    Type(TypeId),
519    /// A canonical direct ownership-fact identity.
520    Field(OwnsFactId),
521    /// A canonical relation-qualified role identity.
522    Role(RoleId),
523    /// A complete bounded, domain-separated fingerprint.
524    Fingerprint(Fingerprint),
525    /// A redacted provider operation classification.
526    ProviderOperation(SdkProviderOperation),
527    /// A classified failed-commit outcome.
528    CommitOutcome(SdkCommitFailureOutcome),
529    /// A signed bound or scalar diagnostic value.
530    Signed(i64),
531    /// The exact canonical typed-query category.
532    QueryCategory(SdkQueryDiagnosticCategory),
533    /// One bounded typed-query identity admitted for a closed detail key.
534    QueryIdentity(SdkQueryDiagnosticIdentity),
535    /// A bounded ordered list of typed-query identities.
536    QueryIdentityList(Vec<SdkQueryDiagnosticIdentity>),
537}
538
539/// Raw presence of one binding-supplied projection-evidence slot.
540///
541/// This closed value records presence only. It does not parse evidence bytes,
542/// infer package provenance, or classify arbitrary evidence collections.
543#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
544pub enum SdkProjectionEvidenceSlotPresence {
545    /// The binding received no bytes for the required slot.
546    Absent,
547    /// The binding received one or more bytes for the slot.
548    Present,
549}
550
551/// One stable, binding-neutral SDK execution failure.
552///
553/// This value is versioned but intentionally has no wire representation. A
554/// binding must copy its accessor results into that binding's owned diagnostic
555/// handle and must not expose this Rust layout.
556#[derive(Clone, Debug, Eq, PartialEq)]
557pub struct SdkExecutionDiagnostic {
558    version: SdkDiagnosticVersion,
559    category: SdkDiagnosticCategory,
560    code: SdkDiagnosticCode,
561    message: SdkDiagnosticMessage,
562    path: Vec<SdkDiagnosticPathSegment>,
563    details: BTreeMap<SdkDiagnosticName, SdkDiagnosticDetailValue>,
564}
565
566impl SdkExecutionDiagnostic {
567    fn stable(
568        category: SdkDiagnosticCategory,
569        code: SdkDiagnosticCode,
570        message: SdkDiagnosticMessage,
571    ) -> Self {
572        Self {
573            version: SdkDiagnosticVersion::CURRENT,
574            category,
575            code,
576            message,
577            path: Vec::new(),
578            details: BTreeMap::new(),
579        }
580    }
581
582    /// Construct a stable invalid-input diagnostic.
583    #[must_use]
584    pub fn invalid_input(code: SdkDiagnosticCode, message: SdkDiagnosticMessage) -> Self {
585        Self::stable(SdkDiagnosticCategory::InvalidInput, code, message)
586    }
587
588    /// Construct a stable unsupported-capability diagnostic.
589    #[must_use]
590    pub fn unsupported_capability(code: SdkDiagnosticCode, message: SdkDiagnosticMessage) -> Self {
591        Self::stable(SdkDiagnosticCategory::UnsupportedCapability, code, message)
592    }
593
594    /// Construct a stable resource-limit diagnostic.
595    #[must_use]
596    pub fn resource_limit(code: SdkDiagnosticCode, message: SdkDiagnosticMessage) -> Self {
597        Self::stable(SdkDiagnosticCategory::ResourceLimit, code, message)
598    }
599
600    /// Construct a stable integrity diagnostic.
601    #[must_use]
602    pub fn integrity(code: SdkDiagnosticCode, message: SdkDiagnosticMessage) -> Self {
603        Self::stable(SdkDiagnosticCategory::Integrity, code, message)
604    }
605
606    /// Construct the fixed generated-package projection-evidence diagnostic.
607    ///
608    /// Callers may append the exact rejected evidence index and validated
609    /// evidence identity, plus closed typed count or provenance details. The
610    /// root argument is fixed here so every binding exposes the same path.
611    #[must_use]
612    pub fn projection_evidence_mismatch() -> Self {
613        Self::stable(
614            SdkDiagnosticCategory::Integrity,
615            static_code("projection_evidence_mismatch"),
616            static_message(
617                "Generated projection evidence does not match the verified schema package",
618            ),
619        )
620        .try_at(SdkDiagnosticPathSegment::Argument(static_name(
621            "projection_evidence",
622        )))
623        .expect("the fixed projection-evidence diagnostic path is bounded")
624    }
625
626    /// Construct the fixed diagnostic for one required projection-evidence
627    /// identity that is absent from its expected position.
628    ///
629    /// The caller supplies a validated contract identity and whether the
630    /// rejected package was foreign. The occurrence counts are fixed by the
631    /// missing-evidence case: exactly one occurrence was expected and none was
632    /// present. Other mismatch shapes must use
633    /// [`Self::projection_evidence_mismatch`] and attach their own exact typed
634    /// metadata rather than relabeling them as missing evidence.
635    #[must_use]
636    pub fn projection_evidence_missing(
637        index: u64,
638        identity: SdkQueryDiagnosticIdentity,
639        foreign_package: bool,
640    ) -> Self {
641        Self::projection_evidence_mismatch()
642            .try_at(SdkDiagnosticPathSegment::Index(index))
643            .and_then(|diagnostic| {
644                diagnostic.try_at(SdkDiagnosticPathSegment::ContractIdentity(identity))
645            })
646            .expect("the fixed missing-evidence diagnostic path is bounded")
647            .with_static_detail(
648                static_name("actual_occurrence_count"),
649                SdkDiagnosticDetailValue::Count(0),
650            )
651            .with_static_detail(
652                static_name("expected_occurrence_count"),
653                SdkDiagnosticDetailValue::Count(1),
654            )
655            .with_static_detail(
656                static_name("foreign_package"),
657                SdkDiagnosticDetailValue::Boolean(foreign_package),
658            )
659    }
660
661    /// Classify a rejected package by the raw presence of its detached
662    /// semantic-schema-fingerprint evidence.
663    ///
664    /// The detached semantic fingerprint is the first item in the current
665    /// generated-package admission evidence, so an absent slot has canonical
666    /// index zero and identity `semantic_schema_fingerprint`. Absence alone
667    /// never proves a foreign package. A present slot deliberately retains the
668    /// generic mismatch because malformed, noncanonical, stale, forged, or
669    /// otherwise conflicting bytes cannot honestly be narrowed by presence.
670    #[must_use]
671    pub fn classify_detached_semantic_schema_fingerprint_rejection(
672        presence: SdkProjectionEvidenceSlotPresence,
673    ) -> Self {
674        match presence {
675            SdkProjectionEvidenceSlotPresence::Absent => Self::projection_evidence_missing(
676                0,
677                SdkQueryDiagnosticIdentity::new("semantic_schema_fingerprint")
678                    .expect("the fixed projection-evidence identity is canonical"),
679                false,
680            ),
681            SdkProjectionEvidenceSlotPresence::Present => Self::projection_evidence_mismatch(),
682        }
683    }
684
685    /// Construct the fixed generated-token package-brand diagnostic.
686    ///
687    /// Operation owners append the exact model, field, role, or query path at
688    /// which a token from another installed package was rejected.
689    #[must_use]
690    pub fn generated_token_package_mismatch() -> Self {
691        Self::stable(
692            SdkDiagnosticCategory::Integrity,
693            static_code("generated_token_package_mismatch"),
694            static_message("The generated token belongs to a different installed schema package"),
695        )
696    }
697
698    /// Construct a redacted provider diagnostic.
699    ///
700    /// The constructor accepts only a closed operation class. In particular,
701    /// it cannot receive provider error text, credentials, endpoints, paths,
702    /// query text, or query values.
703    #[must_use]
704    pub fn provider_failure(operation: SdkProviderOperation) -> Self {
705        Self::stable(
706            SdkDiagnosticCategory::Provider,
707            static_code("provider_operation_failed"),
708            static_message("The database provider could not complete the requested operation"),
709        )
710        .with_static_detail(
711            static_name("operation"),
712            SdkDiagnosticDetailValue::ProviderOperation(operation),
713        )
714    }
715
716    /// Construct a classified, redacted commit-failure diagnostic.
717    ///
718    /// The exact provider message is deliberately discarded. An unknown
719    /// outcome requires state reconciliation before any retry.
720    #[must_use]
721    pub fn commit_failure(outcome: SdkCommitFailureOutcome) -> Self {
722        let (code, message) = match outcome {
723            SdkCommitFailureOutcome::DefinitelyAborted => (
724                "commit_definitely_aborted",
725                "The provider proves that the transaction did not commit",
726            ),
727            SdkCommitFailureOutcome::Unknown => (
728                "commit_outcome_unknown",
729                "The transaction commit outcome is unknown and state must be reconciled before retry",
730            ),
731        };
732        Self::stable(
733            SdkDiagnosticCategory::Transaction,
734            static_code(code),
735            static_message(message),
736        )
737        .with_static_detail(
738            static_name("commit_outcome"),
739            SdkDiagnosticDetailValue::CommitOutcome(outcome),
740        )
741    }
742
743    /// Construct a stable transaction-lifecycle diagnostic.
744    ///
745    /// Dynamic transaction or provider text cannot enter this constructor;
746    /// callers supply only implementation-owned stable code and message text.
747    #[must_use]
748    pub fn transaction_failure(code: SdkDiagnosticCode, message: SdkDiagnosticMessage) -> Self {
749        Self::stable(SdkDiagnosticCategory::Transaction, code, message)
750    }
751
752    /// Construct a typed-query diagnostic while retaining its exact canonical
753    /// query category inside the binding-neutral SDK envelope.
754    ///
755    /// The message is selected from a closed redacted vocabulary. Dynamic
756    /// provider text, query operands, and transport context cannot enter this
757    /// constructor.
758    #[must_use]
759    pub fn query_failure(category: SdkQueryDiagnosticCategory, code: SdkDiagnosticCode) -> Self {
760        let (outer, message) = match category {
761            SdkQueryDiagnosticCategory::InvalidPlan => (
762                SdkDiagnosticCategory::InvalidInput,
763                "The typed query plan does not satisfy the generated query contract",
764            ),
765            SdkQueryDiagnosticCategory::Cardinality => (
766                SdkDiagnosticCategory::InvalidInput,
767                "The typed query result does not satisfy the requested cardinality",
768            ),
769            SdkQueryDiagnosticCategory::UnsupportedCapability => (
770                SdkDiagnosticCategory::UnsupportedCapability,
771                "The provider cannot execute a capability required by this typed query",
772            ),
773            SdkQueryDiagnosticCategory::StaleSchema => (
774                SdkDiagnosticCategory::Integrity,
775                "The typed query schema authority changed after request validation",
776            ),
777            SdkQueryDiagnosticCategory::ResourceLimit => (
778                SdkDiagnosticCategory::ResourceLimit,
779                "The typed query exceeded a binding neutral execution limit",
780            ),
781            SdkQueryDiagnosticCategory::Cancelled => (
782                SdkDiagnosticCategory::Cancelled,
783                "The typed query was cancelled during execution",
784            ),
785            SdkQueryDiagnosticCategory::Provider => (
786                SdkDiagnosticCategory::Provider,
787                "The database provider could not complete the typed query",
788            ),
789            SdkQueryDiagnosticCategory::ResultDecode => (
790                SdkDiagnosticCategory::Integrity,
791                "Typed query evidence does not match the validated request invocation",
792            ),
793        };
794        Self::stable(outer, code, static_message(message)).with_static_detail(
795            static_name("query_category"),
796            SdkDiagnosticDetailValue::QueryCategory(category),
797        )
798    }
799
800    /// Construct the fixed diagnostic for pre-dispatch cancellation.
801    #[must_use]
802    pub fn cancelled_before_dispatch() -> Self {
803        Self::stable(
804            SdkDiagnosticCategory::Cancelled,
805            static_code("cancelled_before_dispatch"),
806            static_message("The operation was cancelled before provider dispatch"),
807        )
808    }
809
810    /// Construct the fixed data-operation cancellation diagnostic.
811    #[must_use]
812    pub fn data_operation_cancelled() -> Self {
813        Self::stable(
814            SdkDiagnosticCategory::Cancelled,
815            static_code("provider_cancelled"),
816            static_message("The data operation was cancelled"),
817        )
818    }
819
820    /// Construct the fixed absolute data-operation deadline diagnostic.
821    #[must_use]
822    pub fn data_operation_deadline_exceeded() -> Self {
823        Self::stable(
824            SdkDiagnosticCategory::ResourceLimit,
825            static_code("transaction_deadline_exceeded"),
826            static_message("The data operation exceeded its absolute execution deadline"),
827        )
828    }
829
830    /// Construct the fixed canonical projected-codec cancellation diagnostic.
831    #[must_use]
832    pub fn projected_codec_cancelled() -> Self {
833        Self::stable(
834            SdkDiagnosticCategory::Cancelled,
835            static_code("projected_codec_cancelled"),
836            static_message("Canonical projected codec work was cancelled before publication"),
837        )
838    }
839
840    /// Construct the fixed canonical projected-codec deadline diagnostic.
841    #[must_use]
842    pub fn projected_codec_deadline_exceeded() -> Self {
843        Self::resource_limit(
844            static_code("projected_codec_deadline_exceeded"),
845            static_message("Canonical projected codec work exceeded its absolute deadline"),
846        )
847    }
848
849    fn projected_codec_limit(code: &'static str) -> Self {
850        Self::resource_limit(
851            static_code(code),
852            static_message("Canonical projected codec work exceeded a tightened resource limit"),
853        )
854    }
855
856    /// Construct the fixed canonical projected-codec input-byte limit diagnostic.
857    #[must_use]
858    pub fn projected_codec_input_limit() -> Self {
859        Self::projected_codec_limit("projected_codec_input_limit")
860    }
861
862    /// Construct the fixed canonical projected-codec output-byte limit diagnostic.
863    #[must_use]
864    pub fn projected_codec_output_limit() -> Self {
865        Self::projected_codec_limit("projected_codec_output_limit")
866    }
867
868    /// Construct the fixed canonical projected-codec member limit diagnostic.
869    #[must_use]
870    pub fn projected_codec_member_limit() -> Self {
871        Self::projected_codec_limit("projected_codec_member_limit")
872    }
873
874    /// Construct the fixed canonical projected-codec depth limit diagnostic.
875    #[must_use]
876    pub fn projected_codec_depth_limit() -> Self {
877        Self::projected_codec_limit("projected_codec_depth_limit")
878    }
879
880    /// Construct the fixed declared-schema mismatch diagnostic without payload data.
881    #[must_use]
882    pub fn projected_record_schema_mismatch() -> Self {
883        Self::invalid_input(
884            static_code("projected_record_schema_mismatch"),
885            static_message("Canonical projected record schema identity does not match the package"),
886        )
887        .try_at(SdkDiagnosticPathSegment::ContractField(
888            SdkQueryDiagnosticIdentity::new("declared_schema_identity")
889                .expect("the fixed projected record schema path is canonical"),
890        ))
891        .expect("the fixed projected record schema path is bounded")
892    }
893
894    /// Construct the fixed rejection for mutating a decoded detached snapshot.
895    #[must_use]
896    pub fn projected_snapshot_detached() -> Self {
897        Self::invalid_input(
898            static_code("projected_snapshot_detached"),
899            static_message(
900                "A decoded canonical snapshot is detached and cannot authorize a mutation",
901            ),
902        )
903    }
904
905    /// Construct a redacted internal failure with no implementation details.
906    #[must_use]
907    pub fn internal_failure() -> Self {
908        Self::stable(
909            SdkDiagnosticCategory::Internal,
910            static_code("internal_failure"),
911            static_message("The operation failed inside the TypeBridge runtime"),
912        )
913    }
914
915    /// Append one typed path segment, enforcing the path ceiling.
916    pub fn try_at(
917        mut self,
918        segment: SdkDiagnosticPathSegment,
919    ) -> Result<Self, SdkDiagnosticBuildError> {
920        if self.path.len() == MAX_SDK_DIAGNOSTIC_PATH_SEGMENTS {
921            return Err(SdkDiagnosticBuildError::PathLimitExceeded);
922        }
923        self.path.push(segment);
924        Ok(self)
925    }
926
927    /// Prepend a bounded typed path while preserving the complete diagnostic.
928    ///
929    /// Execution layers use this when a validated child value fails beneath a
930    /// containing argument or collection item. The original category, code,
931    /// message, path suffix, and typed details remain unchanged.
932    pub fn try_with_path_prefix<I>(mut self, prefix: I) -> Result<Self, SdkDiagnosticBuildError>
933    where
934        I: IntoIterator<Item = SdkDiagnosticPathSegment>,
935    {
936        let mut bounded = Vec::new();
937        for segment in prefix {
938            if bounded.len().saturating_add(self.path.len()) == MAX_SDK_DIAGNOSTIC_PATH_SEGMENTS {
939                return Err(SdkDiagnosticBuildError::PathLimitExceeded);
940            }
941            bounded.push(segment);
942        }
943        for segment in bounded.into_iter().rev() {
944            self.path.insert(0, segment);
945        }
946        Ok(self)
947    }
948
949    /// Attach one typed detail, enforcing uniqueness and the detail ceiling.
950    pub fn try_with_detail(
951        mut self,
952        key: SdkDiagnosticName,
953        value: SdkDiagnosticDetailValue,
954    ) -> Result<Self, SdkDiagnosticBuildError> {
955        if self.details.contains_key(&key) {
956            return Err(SdkDiagnosticBuildError::DuplicateDetail);
957        }
958        if self.details.len() == MAX_SDK_DIAGNOSTIC_DETAILS {
959            return Err(SdkDiagnosticBuildError::DetailLimitExceeded);
960        }
961        self.details.insert(key, value);
962        Ok(self)
963    }
964
965    fn with_static_detail(
966        mut self,
967        key: SdkDiagnosticName,
968        value: SdkDiagnosticDetailValue,
969    ) -> Self {
970        let replaced = self.details.insert(key, value);
971        debug_assert!(replaced.is_none());
972        self
973    }
974
975    /// Return the in-memory diagnostic contract version.
976    #[must_use]
977    pub const fn version(&self) -> SdkDiagnosticVersion {
978        self.version
979    }
980
981    /// Return the stable category.
982    #[must_use]
983    pub const fn category(&self) -> SdkDiagnosticCategory {
984        self.category
985    }
986
987    /// Return the validated stable code.
988    #[must_use]
989    pub const fn code(&self) -> &SdkDiagnosticCode {
990        &self.code
991    }
992
993    /// Return the validated stable message.
994    #[must_use]
995    pub const fn message(&self) -> SdkDiagnosticMessage {
996        self.message
997    }
998
999    /// Return the ordered typed path.
1000    #[must_use]
1001    pub fn path(&self) -> &[SdkDiagnosticPathSegment] {
1002        &self.path
1003    }
1004
1005    /// Return deterministic typed details in lexical key order.
1006    #[must_use]
1007    pub const fn details(&self) -> &BTreeMap<SdkDiagnosticName, SdkDiagnosticDetailValue> {
1008        &self.details
1009    }
1010}
1011
1012impl fmt::Display for SdkExecutionDiagnostic {
1013    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1014        write!(
1015            formatter,
1016            "{} [{}]: {}",
1017            self.category, self.code, self.message
1018        )
1019    }
1020}
1021
1022impl Error for SdkExecutionDiagnostic {}
1023
1024fn static_code(value: &'static str) -> SdkDiagnosticCode {
1025    SdkDiagnosticCode::new(value).expect("static SDK diagnostic code is canonical")
1026}
1027
1028fn static_message(value: &'static str) -> SdkDiagnosticMessage {
1029    SdkDiagnosticMessage::new(value).expect("static SDK diagnostic message is valid")
1030}
1031
1032fn static_name(value: &'static str) -> SdkDiagnosticName {
1033    SdkDiagnosticName::new(value).expect("static SDK diagnostic name is canonical")
1034}
1035
1036#[cfg(test)]
1037mod tests {
1038    use super::*;
1039
1040    #[test]
1041    fn generated_package_diagnostics_have_fixed_binding_neutral_vocabulary() {
1042        let evidence = SdkExecutionDiagnostic::projection_evidence_mismatch();
1043        assert_eq!(evidence.category(), SdkDiagnosticCategory::Integrity);
1044        assert_eq!(evidence.code().as_str(), "projection_evidence_mismatch");
1045        assert_eq!(
1046            evidence.message().as_str(),
1047            "Generated projection evidence does not match the verified schema package",
1048        );
1049        assert!(matches!(
1050            evidence.path(),
1051            [SdkDiagnosticPathSegment::Argument(name)]
1052                if name.as_str() == "projection_evidence"
1053        ));
1054        assert!(evidence.details().is_empty());
1055
1056        let token = SdkExecutionDiagnostic::generated_token_package_mismatch();
1057        assert_eq!(token.category(), SdkDiagnosticCategory::Integrity);
1058        assert_eq!(token.code().as_str(), "generated_token_package_mismatch");
1059        assert_eq!(
1060            token.message().as_str(),
1061            "The generated token belongs to a different installed schema package",
1062        );
1063        assert!(token.path().is_empty());
1064        assert!(token.details().is_empty());
1065    }
1066
1067    #[test]
1068    fn detached_semantic_fingerprint_classifier_matches_v3_representative() {
1069        let missing =
1070            SdkExecutionDiagnostic::classify_detached_semantic_schema_fingerprint_rejection(
1071                SdkProjectionEvidenceSlotPresence::Absent,
1072            );
1073        assert_eq!(missing.category(), SdkDiagnosticCategory::Integrity);
1074        assert_eq!(missing.code().as_str(), "projection_evidence_mismatch");
1075        assert!(matches!(
1076            missing.path(),
1077            [
1078                SdkDiagnosticPathSegment::Argument(argument),
1079                SdkDiagnosticPathSegment::Index(0),
1080                SdkDiagnosticPathSegment::ContractIdentity(identity),
1081            ] if argument.as_str() == "projection_evidence"
1082                && identity.as_str() == "semantic_schema_fingerprint"
1083        ));
1084        assert_eq!(
1085            missing
1086                .details()
1087                .iter()
1088                .map(|(name, value)| (name.as_str(), value))
1089                .collect::<Vec<_>>(),
1090            vec![
1091                (
1092                    "actual_occurrence_count",
1093                    &SdkDiagnosticDetailValue::Count(0),
1094                ),
1095                (
1096                    "expected_occurrence_count",
1097                    &SdkDiagnosticDetailValue::Count(1),
1098                ),
1099                ("foreign_package", &SdkDiagnosticDetailValue::Boolean(false),),
1100            ],
1101        );
1102
1103        let present =
1104            SdkExecutionDiagnostic::classify_detached_semantic_schema_fingerprint_rejection(
1105                SdkProjectionEvidenceSlotPresence::Present,
1106            );
1107        assert_eq!(
1108            present,
1109            SdkExecutionDiagnostic::projection_evidence_mismatch()
1110        );
1111    }
1112
1113    #[test]
1114    fn projected_codec_diagnostics_match_the_closed_v5_algebra() {
1115        for (diagnostic, category, code) in [
1116            (
1117                SdkExecutionDiagnostic::projected_codec_cancelled(),
1118                SdkDiagnosticCategory::Cancelled,
1119                "projected_codec_cancelled",
1120            ),
1121            (
1122                SdkExecutionDiagnostic::projected_codec_input_limit(),
1123                SdkDiagnosticCategory::ResourceLimit,
1124                "projected_codec_input_limit",
1125            ),
1126            (
1127                SdkExecutionDiagnostic::projected_codec_output_limit(),
1128                SdkDiagnosticCategory::ResourceLimit,
1129                "projected_codec_output_limit",
1130            ),
1131            (
1132                SdkExecutionDiagnostic::projected_codec_member_limit(),
1133                SdkDiagnosticCategory::ResourceLimit,
1134                "projected_codec_member_limit",
1135            ),
1136            (
1137                SdkExecutionDiagnostic::projected_codec_depth_limit(),
1138                SdkDiagnosticCategory::ResourceLimit,
1139                "projected_codec_depth_limit",
1140            ),
1141        ] {
1142            assert_eq!(diagnostic.category(), category);
1143            assert_eq!(diagnostic.code().as_str(), code);
1144            assert!(diagnostic.path().is_empty());
1145            assert!(diagnostic.details().is_empty());
1146        }
1147
1148        let mismatch = SdkExecutionDiagnostic::projected_record_schema_mismatch();
1149        assert_eq!(mismatch.category(), SdkDiagnosticCategory::InvalidInput);
1150        assert_eq!(mismatch.code().as_str(), "projected_record_schema_mismatch");
1151        assert!(matches!(
1152            mismatch.path(),
1153            [SdkDiagnosticPathSegment::ContractField(field)]
1154                if field.as_str() == "declared_schema_identity"
1155        ));
1156        assert!(mismatch.details().is_empty());
1157
1158        let detached = SdkExecutionDiagnostic::projected_snapshot_detached();
1159        assert_eq!(detached.category(), SdkDiagnosticCategory::InvalidInput);
1160        assert_eq!(detached.code().as_str(), "projected_snapshot_detached");
1161        assert!(detached.path().is_empty());
1162        assert!(detached.details().is_empty());
1163    }
1164}