1use 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
25pub const MAX_SDK_DIAGNOSTIC_CODE_BYTES: usize = 128;
27pub const MAX_SDK_DIAGNOSTIC_MESSAGE_BYTES: usize = 512;
29pub const MAX_SDK_DIAGNOSTIC_NAME_BYTES: usize = 128;
31pub const MAX_SDK_DIAGNOSTIC_PATH_SEGMENTS: usize = 32;
33pub const MAX_SDK_DIAGNOSTIC_DETAILS: usize = 32;
35pub const MAX_SDK_QUERY_DIAGNOSTIC_IDENTITY_BYTES: usize = 512;
37pub const MAX_SDK_QUERY_DIAGNOSTIC_IDENTITY_LIST: usize = 32;
39
40#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
45pub struct SdkDiagnosticVersion(u16);
46
47impl SdkDiagnosticVersion {
48 pub const V1: Self = Self(1);
50 pub const CURRENT: Self = Self::V1;
52
53 #[must_use]
55 pub const fn get(self) -> u16 {
56 self.0
57 }
58}
59
60#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
65#[non_exhaustive]
66pub enum SdkDiagnosticCategory {
67 InvalidInput,
69 UnsupportedCapability,
71 ResourceLimit,
73 Integrity,
75 Provider,
77 Transaction,
79 Cancelled,
81 Internal,
83}
84
85impl SdkDiagnosticCategory {
86 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110#[non_exhaustive]
111pub enum SdkDiagnosticBuildError {
112 InvalidCode,
114 InvalidMessage,
116 InvalidName,
118 PathLimitExceeded,
120 DetailLimitExceeded,
122 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
153pub struct SdkDiagnosticCode(String);
154
155impl SdkDiagnosticCode {
156 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 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
186pub struct SdkDiagnosticMessage(&'static str);
187
188impl SdkDiagnosticMessage {
189 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 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
222pub struct SdkDiagnosticName(String);
223
224impl SdkDiagnosticName {
225 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 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
255pub struct SdkQueryDiagnosticIdentity(String);
256
257impl SdkQueryDiagnosticIdentity {
258 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 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
291#[non_exhaustive]
292pub enum SdkQueryDiagnosticCategory {
293 InvalidPlan,
295 Cardinality,
297 UnsupportedCapability,
299 StaleSchema,
301 ResourceLimit,
303 Cancelled,
305 Provider,
307 ResultDecode,
309}
310
311impl SdkQueryDiagnosticCategory {
312 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
330#[non_exhaustive]
331pub enum SdkQueryDiagnosticPathKind {
332 Request,
334 Plan,
336 Operation,
338 Predicate,
340 Output,
342 ProviderEvidence,
344 Result,
346}
347
348impl SdkQueryDiagnosticPathKind {
349 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
393#[non_exhaustive]
394pub enum SdkDiagnosticPathSegment {
395 Argument(SdkDiagnosticName),
397 Index(u64),
399 Type(TypeId),
401 Field(OwnsFactId),
403 Role(RoleId),
405 Query(SdkQueryDiagnosticPathKind),
407 QueryBinding(u16),
409 QueryField {
411 owner: SdkQueryDiagnosticIdentity,
413 name: SdkQueryDiagnosticIdentity,
415 },
416 QueryRole {
418 owner: SdkQueryDiagnosticIdentity,
420 name: SdkQueryDiagnosticIdentity,
422 },
423 QueryRoleEdge(u16),
425 QueryOutputSlot(u64),
427 QueryOutputName(SdkQueryDiagnosticIdentity),
429 ContractField(SdkQueryDiagnosticIdentity),
431 ContractIdentity(SdkQueryDiagnosticIdentity),
433}
434
435#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
441#[non_exhaustive]
442pub enum SdkProviderOperation {
443 Connect,
445 Schema,
447 OpenReadTransaction,
449 OpenWriteTransaction,
451 Read,
453 Write,
455 Rollback,
457 Close,
459}
460
461impl SdkProviderOperation {
462 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
480#[non_exhaustive]
481pub enum SdkCommitFailureOutcome {
482 DefinitelyAborted,
484 Unknown,
486}
487
488impl SdkCommitFailureOutcome {
489 #[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#[derive(Clone, Debug, Eq, PartialEq)]
505#[non_exhaustive]
506pub enum SdkDiagnosticDetailValue {
507 Boolean(bool),
509 Count(u64),
511 ByteCount(u64),
513 Capability(CapabilityId),
515 ValueType(ValueTypeTag),
517 Type(TypeId),
519 Field(OwnsFactId),
521 Role(RoleId),
523 Fingerprint(Fingerprint),
525 ProviderOperation(SdkProviderOperation),
527 CommitOutcome(SdkCommitFailureOutcome),
529 Signed(i64),
531 QueryCategory(SdkQueryDiagnosticCategory),
533 QueryIdentity(SdkQueryDiagnosticIdentity),
535 QueryIdentityList(Vec<SdkQueryDiagnosticIdentity>),
537}
538
539#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
544pub enum SdkProjectionEvidenceSlotPresence {
545 Absent,
547 Present,
549}
550
551#[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 #[must_use]
584 pub fn invalid_input(code: SdkDiagnosticCode, message: SdkDiagnosticMessage) -> Self {
585 Self::stable(SdkDiagnosticCategory::InvalidInput, code, message)
586 }
587
588 #[must_use]
590 pub fn unsupported_capability(code: SdkDiagnosticCode, message: SdkDiagnosticMessage) -> Self {
591 Self::stable(SdkDiagnosticCategory::UnsupportedCapability, code, message)
592 }
593
594 #[must_use]
596 pub fn resource_limit(code: SdkDiagnosticCode, message: SdkDiagnosticMessage) -> Self {
597 Self::stable(SdkDiagnosticCategory::ResourceLimit, code, message)
598 }
599
600 #[must_use]
602 pub fn integrity(code: SdkDiagnosticCode, message: SdkDiagnosticMessage) -> Self {
603 Self::stable(SdkDiagnosticCategory::Integrity, code, message)
604 }
605
606 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
748 pub fn transaction_failure(code: SdkDiagnosticCode, message: SdkDiagnosticMessage) -> Self {
749 Self::stable(SdkDiagnosticCategory::Transaction, code, message)
750 }
751
752 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
858 pub fn projected_codec_input_limit() -> Self {
859 Self::projected_codec_limit("projected_codec_input_limit")
860 }
861
862 #[must_use]
864 pub fn projected_codec_output_limit() -> Self {
865 Self::projected_codec_limit("projected_codec_output_limit")
866 }
867
868 #[must_use]
870 pub fn projected_codec_member_limit() -> Self {
871 Self::projected_codec_limit("projected_codec_member_limit")
872 }
873
874 #[must_use]
876 pub fn projected_codec_depth_limit() -> Self {
877 Self::projected_codec_limit("projected_codec_depth_limit")
878 }
879
880 #[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 #[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 #[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 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 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 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 #[must_use]
977 pub const fn version(&self) -> SdkDiagnosticVersion {
978 self.version
979 }
980
981 #[must_use]
983 pub const fn category(&self) -> SdkDiagnosticCategory {
984 self.category
985 }
986
987 #[must_use]
989 pub const fn code(&self) -> &SdkDiagnosticCode {
990 &self.code
991 }
992
993 #[must_use]
995 pub const fn message(&self) -> SdkDiagnosticMessage {
996 self.message
997 }
998
999 #[must_use]
1001 pub fn path(&self) -> &[SdkDiagnosticPathSegment] {
1002 &self.path
1003 }
1004
1005 #[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}