Skip to main content

type_bridge_contract/
schema.rs

1//! Versioned schema identities, facts, provenance, and declared fingerprints.
2//!
3//! This module contains no parser, filesystem, provider, binding, query, or
4//! migration dependencies. Trusted values are created only through validating
5//! constructors; canonical decoding will use explicit validated wire types.
6
7use std::cmp::Ordering;
8use std::collections::{BTreeMap, BTreeSet};
9use std::error::Error;
10use std::fmt;
11
12use serde::{Serialize, Serializer};
13
14use crate::capability::CapabilitySet;
15use crate::codec::{FormatVersion, ensure_format_version, to_canonical_json};
16use crate::diagnostic::{Diagnostic, DiagnosticCategory};
17use crate::fingerprint::{CanonicalizationVersion, Fingerprint, FingerprintDomain};
18use crate::id::{AttributeId, FunctionId, Label, RoleId, StructId, TypeId, TypeKind};
19use crate::limits::{MAX_CANONICAL_COLLECTION_LEN, MAX_CANONICAL_STRING_BYTES};
20use crate::value::{CanonicalValue, Cardinality, ValueTypeTag};
21
22pub use crate::managed_scope::{
23    ManagedScopeBinding, ManagedScopeId, ManagedScopeProfileBinding,
24    ManagedScopeProfileFingerprint, ManagedScopeProfileId, SemanticProfileFingerprint,
25};
26pub use crate::schema_delta::{
27    ManagedFactSelection, ManagedSchemaState, PatchFormatVersion, SchemaDelta, SchemaOperation,
28    SchemaOperationKind, decode_schema_delta, encode_schema_delta,
29};
30pub use crate::schema_fingerprint::{
31    ManagedDeclaredIdentityFingerprint, ManagedSemanticSchemaFingerprint,
32    SchemaDocumentSetFingerprint, SemanticSchemaFingerprint,
33};
34pub use crate::semantic_profile::{InterfaceKind, SemanticProfile};
35
36/// Maximum UTF-8 length of a normalized schema document identifier.
37pub const MAX_DOCUMENT_ID_BYTES: usize = 4096;
38
39/// A normalized, relative, forward-slash schema source identifier.
40#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
41#[serde(transparent)]
42pub struct DocumentId(String);
43
44impl DocumentId {
45    /// Validate a schema document identifier.
46    pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
47        let value = value.into();
48        let valid_segments = value
49            .split('/')
50            .all(|segment| !segment.is_empty() && segment != "." && segment != "..");
51        if value.is_empty()
52            || value.len() > MAX_DOCUMENT_ID_BYTES
53            || value.starts_with('/')
54            || value.contains('\\')
55            || value.contains('\0')
56            || !valid_segments
57        {
58            return Err(schema_diagnostic(
59                DiagnosticCategory::InvalidContract,
60                "invalid_schema_document_id",
61                "schema document identifiers must be normalized relative paths",
62            )
63            .with_detail("document", value));
64        }
65        Ok(Self(value))
66    }
67
68    /// Return the normalized identifier.
69    pub fn as_str(&self) -> &str {
70        &self.0
71    }
72}
73
74impl fmt::Display for DocumentId {
75    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
76        formatter.write_str(self.as_str())
77    }
78}
79
80/// A byte-exact source span with one-based line and column positions.
81#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
82pub struct SourceSpan {
83    document: DocumentId,
84    byte_start: u64,
85    byte_end: u64,
86    line: u32,
87    column: u32,
88    end_line: u32,
89    end_column: u32,
90}
91
92impl SourceSpan {
93    /// Construct a validated source span.
94    #[allow(clippy::too_many_arguments)]
95    pub fn new(
96        document: DocumentId,
97        byte_start: u64,
98        byte_end: u64,
99        line: u32,
100        column: u32,
101        end_line: u32,
102        end_column: u32,
103    ) -> Result<Self, Diagnostic> {
104        if byte_start > byte_end
105            || line == 0
106            || column == 0
107            || end_line == 0
108            || end_column == 0
109            || (end_line, end_column) < (line, column)
110        {
111            return Err(schema_diagnostic(
112                DiagnosticCategory::InvalidContract,
113                "invalid_schema_source_span",
114                "schema source spans must be ordered and one-based",
115            ));
116        }
117        Ok(Self {
118            document,
119            byte_start,
120            byte_end,
121            line,
122            column,
123            end_line,
124            end_column,
125        })
126    }
127
128    /// Return the source document identifier.
129    pub fn document(&self) -> &DocumentId {
130        &self.document
131    }
132
133    /// Return the inclusive byte start.
134    pub const fn byte_start(&self) -> u64 {
135        self.byte_start
136    }
137
138    /// Return the exclusive byte end.
139    pub const fn byte_end(&self) -> u64 {
140        self.byte_end
141    }
142
143    /// Return the one-based start line.
144    pub const fn line(&self) -> u32 {
145        self.line
146    }
147
148    /// Return the one-based start column.
149    pub const fn column(&self) -> u32 {
150        self.column
151    }
152
153    /// Return the one-based end line.
154    pub const fn end_line(&self) -> u32 {
155        self.end_line
156    }
157
158    /// Return the one-based end column.
159    pub const fn end_column(&self) -> u32 {
160        self.end_column
161    }
162}
163
164/// A secondary source label attached to a schema diagnostic.
165#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
166pub struct DiagnosticLabel {
167    span: SourceSpan,
168    message: String,
169}
170
171impl DiagnosticLabel {
172    /// Construct a related diagnostic label.
173    pub fn new(span: SourceSpan, message: impl Into<String>) -> Self {
174        Self {
175            span,
176            message: message.into(),
177        }
178    }
179
180    /// Return the related source span.
181    pub const fn span(&self) -> &SourceSpan {
182        &self.span
183    }
184
185    /// Return the related-label message.
186    pub fn message(&self) -> &str {
187        &self.message
188    }
189}
190
191/// A stable contract diagnostic enriched with schema source locations.
192#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
193pub struct SchemaDiagnostic {
194    diagnostic: Diagnostic,
195    primary: Option<SourceSpan>,
196    related: Vec<DiagnosticLabel>,
197}
198
199impl SchemaDiagnostic {
200    /// Construct a source-aware schema diagnostic.
201    pub fn new(diagnostic: Diagnostic, primary: Option<SourceSpan>) -> Self {
202        Self {
203            diagnostic,
204            primary,
205            related: Vec::new(),
206        }
207    }
208
209    /// Attach a related source label.
210    pub fn with_related(mut self, label: DiagnosticLabel) -> Self {
211        self.related.push(label);
212        self
213    }
214
215    /// Return the stable diagnostic payload.
216    pub const fn diagnostic(&self) -> &Diagnostic {
217        &self.diagnostic
218    }
219
220    /// Return the primary source span, if known.
221    pub fn primary(&self) -> Option<&SourceSpan> {
222        self.primary.as_ref()
223    }
224
225    /// Return related source labels.
226    pub fn related(&self) -> &[DiagnosticLabel] {
227        &self.related
228    }
229}
230
231/// One or more schema diagnostics produced by a fail-closed operation.
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct SchemaDiagnostics(Vec<SchemaDiagnostic>);
234
235impl SchemaDiagnostics {
236    /// Construct a non-empty diagnostic collection.
237    pub fn one(diagnostic: SchemaDiagnostic) -> Self {
238        Self(vec![diagnostic])
239    }
240
241    /// Construct a diagnostic collection from accumulated errors.
242    pub fn from_vec(diagnostics: Vec<SchemaDiagnostic>) -> Self {
243        Self(diagnostics)
244    }
245
246    /// Return all diagnostics in stable order.
247    pub fn iter(&self) -> impl ExactSizeIterator<Item = &SchemaDiagnostic> {
248        self.0.iter()
249    }
250
251    /// Return the number of diagnostics.
252    pub fn len(&self) -> usize {
253        self.0.len()
254    }
255
256    /// Return whether the collection is empty.
257    pub fn is_empty(&self) -> bool {
258        self.0.is_empty()
259    }
260
261    /// Consume the collection.
262    pub fn into_vec(self) -> Vec<SchemaDiagnostic> {
263        self.0
264    }
265}
266
267impl fmt::Display for SchemaDiagnostics {
268    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
269        if let Some(first) = self.0.first() {
270            write!(formatter, "{}", first.diagnostic())?;
271            if self.0.len() > 1 {
272                write!(formatter, " (and {} more)", self.0.len() - 1)?;
273            }
274            Ok(())
275        } else {
276            formatter.write_str("schema validation failed")
277        }
278    }
279}
280
281impl Error for SchemaDiagnostics {}
282
283/// Identity of a direct subtype edge.
284#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
285pub struct SubFactId {
286    subtype: TypeId,
287    supertype: TypeId,
288}
289
290impl SubFactId {
291    /// Construct a subtype-edge identity.
292    pub fn new(subtype: TypeId, supertype: TypeId) -> Result<Self, Diagnostic> {
293        if subtype.kind() == TypeKind::Struct
294            || supertype.kind() == TypeKind::Struct
295            || subtype.kind() != supertype.kind()
296            || subtype == supertype
297        {
298            return Err(schema_diagnostic(
299                DiagnosticCategory::InvalidContract,
300                "invalid_sub_fact",
301                "subtype edges require distinct types of the same non-struct kind",
302            ));
303        }
304        Ok(Self { subtype, supertype })
305    }
306
307    /// Return the subtype.
308    pub const fn subtype(&self) -> &TypeId {
309        &self.subtype
310    }
311
312    /// Return the direct supertype.
313    pub const fn supertype(&self) -> &TypeId {
314        &self.supertype
315    }
316}
317
318/// Identity of an attribute value declaration.
319#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
320#[serde(transparent)]
321pub struct ValueFactId(AttributeId);
322
323impl ValueFactId {
324    /// Construct an attribute value-fact identity.
325    pub const fn new(attribute: AttributeId) -> Self {
326        Self(attribute)
327    }
328
329    /// Return the attribute identity.
330    pub const fn attribute(&self) -> &AttributeId {
331        &self.0
332    }
333}
334
335/// Identity of a direct ownership declaration.
336#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
337pub struct OwnsFactId {
338    owner: TypeId,
339    attribute: AttributeId,
340}
341
342impl OwnsFactId {
343    /// Construct an ownership identity.
344    pub fn new(owner: TypeId, attribute: AttributeId) -> Result<Self, Diagnostic> {
345        if !matches!(owner.kind(), TypeKind::Entity | TypeKind::Relation) {
346            return Err(schema_diagnostic(
347                DiagnosticCategory::InvalidContract,
348                "invalid_owns_owner",
349                "only entity and relation types can own attributes",
350            ));
351        }
352        Ok(Self { owner, attribute })
353    }
354
355    /// Return the owning type.
356    pub const fn owner(&self) -> &TypeId {
357        &self.owner
358    }
359
360    /// Return the owned attribute.
361    pub const fn attribute(&self) -> &AttributeId {
362        &self.attribute
363    }
364}
365
366/// Identity of a direct related-role declaration.
367#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
368pub struct RelatesFactId {
369    relation: TypeId,
370    role: RoleId,
371}
372
373impl RelatesFactId {
374    /// Construct a related-role identity.
375    pub fn new(relation: TypeId, role: RoleId) -> Result<Self, Diagnostic> {
376        if relation.kind() != TypeKind::Relation || relation.label() != role.declaring_relation() {
377            return Err(schema_diagnostic(
378                DiagnosticCategory::InvalidContract,
379                "invalid_relates_identity",
380                "a related role must be declared by its relation type",
381            ));
382        }
383        Ok(Self { relation, role })
384    }
385
386    /// Return the declaring relation.
387    pub const fn relation(&self) -> &TypeId {
388        &self.relation
389    }
390
391    /// Return the declared role.
392    pub const fn role(&self) -> &RoleId {
393        &self.role
394    }
395}
396
397/// Identity of a direct role-playing declaration.
398#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
399pub struct PlaysFactId {
400    player: TypeId,
401    role: RoleId,
402}
403
404impl PlaysFactId {
405    /// Construct a role-playing identity.
406    pub fn new(player: TypeId, role: RoleId) -> Result<Self, Diagnostic> {
407        if !matches!(player.kind(), TypeKind::Entity | TypeKind::Relation) {
408            return Err(schema_diagnostic(
409                DiagnosticCategory::InvalidContract,
410                "invalid_plays_player",
411                "only entity and relation types can play roles",
412            ));
413        }
414        Ok(Self { player, role })
415    }
416
417    /// Return the player type.
418    pub const fn player(&self) -> &TypeId {
419        &self.player
420    }
421
422    /// Return the played role.
423    pub const fn role(&self) -> &RoleId {
424        &self.role
425    }
426}
427
428/// A structural fact that may carry an independent annotation.
429#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
430#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
431pub enum AnnotationSubjectId {
432    /// A type declaration.
433    Type(TypeId),
434    /// A subtype edge.
435    Sub(SubFactId),
436    /// An attribute value declaration.
437    Value(ValueFactId),
438    /// An ownership declaration.
439    Owns(OwnsFactId),
440    /// A related-role declaration.
441    Relates(RelatesFactId),
442    /// A role-playing declaration.
443    Plays(PlaysFactId),
444    /// A function declaration.
445    Function(FunctionId),
446}
447
448/// Stable identity of a schema annotation kind.
449#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
450#[serde(tag = "kind", content = "key", rename_all = "snake_case")]
451pub enum AnnotationKindId {
452    /// `@abstract`.
453    Abstract,
454    /// `@independent`.
455    Independent,
456    /// `@key`.
457    Key,
458    /// `@unique`.
459    Unique,
460    /// `@distinct` on an ordered ownership or related-role collection.
461    Distinct,
462    /// `@card`.
463    Card,
464    /// `@regex`.
465    Regex,
466    /// `@range`.
467    Range,
468    /// `@values`.
469    Values,
470    /// `@doc`.
471    Doc,
472    /// One independently identified `@meta` key.
473    Meta(Label),
474}
475
476impl AnnotationKindId {
477    /// Construct an independently identified metadata kind.
478    pub fn meta(key: impl Into<String>) -> Result<Self, Diagnostic> {
479        Label::new(key).map(Self::Meta)
480    }
481}
482
483/// Identity of an independent annotation fact.
484#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
485pub struct AnnotationFactId {
486    subject: AnnotationSubjectId,
487    kind: AnnotationKindId,
488}
489
490impl AnnotationFactId {
491    /// Construct an annotation identity.
492    pub const fn new(subject: AnnotationSubjectId, kind: AnnotationKindId) -> Self {
493        Self { subject, kind }
494    }
495
496    /// Return the annotated subject.
497    pub const fn subject(&self) -> &AnnotationSubjectId {
498        &self.subject
499    }
500
501    /// Return the annotation kind.
502    pub const fn kind(&self) -> &AnnotationKindId {
503        &self.kind
504    }
505}
506
507/// A validated regular-expression payload retained without a regex engine.
508#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
509#[serde(transparent)]
510pub struct RegexPattern(String);
511
512impl RegexPattern {
513    /// Validate a regex source payload.
514    pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
515        let value = value.into();
516        if value.is_empty() || value.len() > MAX_CANONICAL_STRING_BYTES {
517            return Err(schema_diagnostic(
518                DiagnosticCategory::InvalidContract,
519                "invalid_regex_annotation",
520                "regex annotation text must be non-empty and bounded",
521            ));
522        }
523        Ok(Self(value))
524    }
525
526    /// Return the regex source.
527    pub fn as_str(&self) -> &str {
528        &self.0
529    }
530}
531
532/// A validated non-empty documentation payload.
533#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
534#[serde(transparent)]
535pub struct DocText(String);
536
537impl DocText {
538    /// Validate documentation text.
539    pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
540        let value = value.into();
541        if value.is_empty() || value.len() > MAX_CANONICAL_STRING_BYTES {
542            return Err(schema_diagnostic(
543                DiagnosticCategory::InvalidContract,
544                "invalid_doc_annotation",
545                "documentation text must be non-empty and bounded",
546            ));
547        }
548        Ok(Self(value))
549    }
550
551    /// Return the documentation text.
552    pub fn as_str(&self) -> &str {
553        &self.0
554    }
555}
556
557/// A non-empty exact-domain canonical value set.
558#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
559#[serde(transparent)]
560pub struct CanonicalValueSet(BTreeSet<CanonicalValue>);
561
562/// Precise validation failure for a raw `@values` member sequence.
563#[derive(Debug, Clone, PartialEq, Eq)]
564pub enum CanonicalValueSetViolation {
565    /// No members were supplied.
566    Empty,
567    /// The raw member sequence exceeded the canonical collection ceiling.
568    MemberLimitExceeded {
569        /// Maximum accepted raw member count.
570        maximum: usize,
571        /// Index of the first rejected raw member.
572        first_excess_index: usize,
573    },
574    /// One member used a different exact scalar domain.
575    MixedDomain {
576        /// Domain established by the first member.
577        expected: ValueTypeTag,
578        /// Domain of the conflicting member.
579        actual: ValueTypeTag,
580        /// Index of the conflicting member.
581        member_index: usize,
582    },
583    /// One exact canonical value occurred more than once.
584    Duplicate {
585        /// Index of the first occurrence.
586        first_index: usize,
587        /// Index of the duplicate occurrence.
588        duplicate_index: usize,
589    },
590}
591
592impl CanonicalValueSetViolation {
593    /// Convert to the stable compatibility diagnostic returned by `new`.
594    pub fn into_diagnostic(self) -> Diagnostic {
595        match self {
596            Self::Empty => schema_diagnostic(
597                DiagnosticCategory::InvalidContract,
598                "empty_values_annotation",
599                "values annotations must contain at least one value",
600            ),
601            Self::MemberLimitExceeded {
602                maximum,
603                first_excess_index,
604            } => schema_diagnostic(
605                DiagnosticCategory::ResourceLimit,
606                "values_annotation_member_limit_exceeded",
607                "values annotation exceeds the raw member ceiling",
608            )
609            .with_detail(
610                "maximum_members",
611                i64::try_from(maximum).expect("collection limit fits i64"),
612            )
613            .with_detail(
614                "first_excess_index",
615                i64::try_from(first_excess_index).expect("collection index fits i64"),
616            ),
617            Self::MixedDomain {
618                expected,
619                actual,
620                member_index,
621            } => schema_diagnostic(
622                DiagnosticCategory::InvalidContract,
623                "mixed_values_annotation_domain",
624                "values annotations require one exact scalar domain",
625            )
626            .with_detail("expected_value_type", expected.as_str())
627            .with_detail("actual_value_type", actual.as_str())
628            .with_detail(
629                "member_index",
630                i64::try_from(member_index).expect("collection index fits i64"),
631            ),
632            Self::Duplicate {
633                first_index,
634                duplicate_index,
635            } => schema_diagnostic(
636                DiagnosticCategory::InvalidContract,
637                "duplicate_values_annotation_value",
638                "values annotations cannot contain duplicates",
639            )
640            .with_detail(
641                "first_index",
642                i64::try_from(first_index).expect("collection index fits i64"),
643            )
644            .with_detail(
645                "duplicate_index",
646                i64::try_from(duplicate_index).expect("collection index fits i64"),
647            ),
648        }
649    }
650}
651
652impl CanonicalValueSet {
653    /// Validate a set, rejecting empty, mixed-domain, and duplicate input.
654    pub fn new(values: impl IntoIterator<Item = CanonicalValue>) -> Result<Self, Diagnostic> {
655        Self::new_detailed(values).map_err(CanonicalValueSetViolation::into_diagnostic)
656    }
657
658    /// Validate a raw sequence while retaining member indices for source diagnostics.
659    pub fn new_detailed(
660        values: impl IntoIterator<Item = CanonicalValue>,
661    ) -> Result<Self, CanonicalValueSetViolation> {
662        let mut positions = BTreeMap::new();
663        let mut value_type = None;
664        for (member_index, value) in values.into_iter().enumerate() {
665            if member_index >= MAX_CANONICAL_COLLECTION_LEN {
666                return Err(CanonicalValueSetViolation::MemberLimitExceeded {
667                    maximum: MAX_CANONICAL_COLLECTION_LEN,
668                    first_excess_index: member_index,
669                });
670            }
671            if let Some(expected) = value_type {
672                if expected != value.value_type() {
673                    return Err(CanonicalValueSetViolation::MixedDomain {
674                        expected,
675                        actual: value.value_type(),
676                        member_index,
677                    });
678                }
679            } else {
680                value_type = Some(value.value_type());
681            }
682            if let Some(first_index) = positions.get(&value) {
683                return Err(CanonicalValueSetViolation::Duplicate {
684                    first_index: *first_index,
685                    duplicate_index: member_index,
686                });
687            }
688            positions.insert(value, member_index);
689        }
690        if positions.is_empty() {
691            return Err(CanonicalValueSetViolation::Empty);
692        }
693        Ok(Self(positions.into_keys().collect()))
694    }
695
696    /// Return values in canonical order.
697    pub fn iter(&self) -> impl ExactSizeIterator<Item = &CanonicalValue> {
698        self.0.iter()
699    }
700}
701
702/// An exact-domain, non-empty canonical value range.
703#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
704pub struct CanonicalValueRange {
705    lower: Option<CanonicalValue>,
706    upper: Option<CanonicalValue>,
707}
708
709/// Precise validation failure for canonical range bounds.
710#[derive(Debug, Clone, Copy, PartialEq, Eq)]
711pub enum CanonicalValueRangeViolation {
712    /// Neither bound was supplied.
713    Empty,
714    /// The two bounds used different exact scalar domains.
715    MixedDomain {
716        /// Lower-bound domain.
717        lower: ValueTypeTag,
718        /// Upper-bound domain.
719        upper: ValueTypeTag,
720    },
721    /// The scalar domain has no provider range ordering.
722    UnsupportedDomain {
723        /// Unsupported scalar domain.
724        value_type: ValueTypeTag,
725    },
726    /// The lower bound was equal to or greater than the upper bound.
727    InvalidBounds {
728        /// Semantic ordering of lower relative to upper.
729        ordering: Ordering,
730    },
731}
732
733impl CanonicalValueRangeViolation {
734    /// Convert to the stable compatibility diagnostic returned by `new`.
735    pub fn into_diagnostic(self) -> Diagnostic {
736        match self {
737            Self::Empty => schema_diagnostic(
738                DiagnosticCategory::InvalidContract,
739                "empty_range_annotation",
740                "range annotations require at least one bound",
741            ),
742            Self::MixedDomain { lower, upper } => schema_diagnostic(
743                DiagnosticCategory::InvalidContract,
744                "mixed_range_annotation_domain",
745                "range bounds require one exact scalar domain",
746            )
747            .with_detail("lower_value_type", lower.as_str())
748            .with_detail("upper_value_type", upper.as_str()),
749            Self::UnsupportedDomain { value_type } => schema_diagnostic(
750                DiagnosticCategory::InvalidContract,
751                "unsupported_range_annotation_domain",
752                "range annotations require an ordered scalar domain",
753            )
754            .with_detail("value_type", value_type.as_str()),
755            Self::InvalidBounds { ordering } => schema_diagnostic(
756                DiagnosticCategory::InvalidContract,
757                "invalid_range_annotation_bounds",
758                "range lower bounds must be strictly less than upper bounds",
759            )
760            .with_detail(
761                "ordering",
762                match ordering {
763                    Ordering::Less => "less",
764                    Ordering::Equal => "equal",
765                    Ordering::Greater => "greater",
766                },
767            ),
768        }
769    }
770}
771
772impl CanonicalValueRange {
773    /// Validate a range with at least one bound and one exact scalar domain.
774    pub fn new(
775        lower: Option<CanonicalValue>,
776        upper: Option<CanonicalValue>,
777    ) -> Result<Self, Diagnostic> {
778        Self::new_detailed(lower, upper).map_err(CanonicalValueRangeViolation::into_diagnostic)
779    }
780
781    /// Validate bounds while retaining the exact failure for source diagnostics.
782    pub fn new_detailed(
783        lower: Option<CanonicalValue>,
784        upper: Option<CanonicalValue>,
785    ) -> Result<Self, CanonicalValueRangeViolation> {
786        if lower.is_none() && upper.is_none() {
787            return Err(CanonicalValueRangeViolation::Empty);
788        }
789        if let (Some(lower), Some(upper)) = (&lower, &upper)
790            && lower.value_type() != upper.value_type()
791        {
792            return Err(CanonicalValueRangeViolation::MixedDomain {
793                lower: lower.value_type(),
794                upper: upper.value_type(),
795            });
796        }
797        let value_type = lower
798            .as_ref()
799            .or(upper.as_ref())
800            .expect("non-empty range has one bound")
801            .value_type();
802        if matches!(value_type, ValueTypeTag::Duration) {
803            return Err(CanonicalValueRangeViolation::UnsupportedDomain { value_type });
804        }
805        if let (Some(lower), Some(upper)) = (&lower, &upper) {
806            let ordering = lower
807                .semantic_cmp_same_domain(upper)
808                .expect("every supported exact domain has semantic ordering");
809            if ordering != Ordering::Less {
810                return Err(CanonicalValueRangeViolation::InvalidBounds { ordering });
811            }
812        }
813        Ok(Self { lower, upper })
814    }
815
816    /// Return the lower bound.
817    pub const fn lower(&self) -> Option<&CanonicalValue> {
818        self.lower.as_ref()
819    }
820
821    /// Return the upper bound.
822    pub const fn upper(&self) -> Option<&CanonicalValue> {
823        self.upper.as_ref()
824    }
825}
826
827/// A closed, kind-safe schema annotation payload.
828#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
829#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
830pub enum SchemaAnnotationValue {
831    /// A marker annotation with no payload.
832    Presence,
833    /// A cardinality payload.
834    Cardinality(Cardinality),
835    /// A regex source payload.
836    Regex(RegexPattern),
837    /// An exact canonical value range.
838    Range(CanonicalValueRange),
839    /// A non-empty canonical value set.
840    Values(CanonicalValueSet),
841    /// Documentation text.
842    Doc(DocText),
843    /// A typed metadata value.
844    Meta(CanonicalValue),
845}
846
847/// Existence of an entity, relation, or attribute type.
848#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
849pub struct TypeFact {
850    id: TypeId,
851}
852
853impl TypeFact {
854    /// Construct a type-existence fact.
855    pub fn new(id: TypeId) -> Result<Self, Diagnostic> {
856        if id.kind() == TypeKind::Struct {
857            return Err(schema_diagnostic(
858                DiagnosticCategory::InvalidContract,
859                "invalid_type_fact_kind",
860                "struct existence uses StructFact",
861            ));
862        }
863        if value_type_tag(id.label().as_str()).is_some() {
864            return Err(schema_diagnostic(
865                DiagnosticCategory::InvalidContract,
866                "reserved_schema_type_label",
867                "schema type labels cannot collide with built-in value-type tokens",
868            ));
869        }
870        Ok(Self { id })
871    }
872
873    /// Return the type identity.
874    pub const fn id(&self) -> &TypeId {
875        &self.id
876    }
877}
878
879/// A direct subtype fact.
880#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
881pub struct SubFact {
882    id: SubFactId,
883}
884
885impl SubFact {
886    /// Construct a subtype fact.
887    pub const fn new(id: SubFactId) -> Self {
888        Self { id }
889    }
890
891    /// Return the fact identity.
892    pub const fn id(&self) -> &SubFactId {
893        &self.id
894    }
895}
896
897/// An attribute scalar-domain fact.
898#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
899pub struct ValueFact {
900    id: ValueFactId,
901    value_type: ValueTypeTag,
902}
903
904impl ValueFact {
905    /// Construct an attribute scalar-domain fact.
906    pub const fn new(id: ValueFactId, value_type: ValueTypeTag) -> Self {
907        Self { id, value_type }
908    }
909
910    /// Return the fact identity.
911    pub const fn id(&self) -> &ValueFactId {
912        &self.id
913    }
914
915    /// Return the scalar domain.
916    pub const fn value_type(&self) -> ValueTypeTag {
917        self.value_type
918    }
919}
920
921/// The closed collection semantics of an ownership or related-role fact.
922#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
923#[serde(rename_all = "snake_case")]
924pub enum CollectionMode {
925    /// Multiplicity is retained, but member order is not semantic.
926    #[default]
927    Unordered,
928    /// Members form a semantic ordered list, including at scalar cardinality.
929    OrderedList,
930}
931
932impl CollectionMode {
933    /// Report whether this is the compatibility-default unordered mode.
934    #[must_use]
935    pub const fn is_unordered(&self) -> bool {
936        matches!(self, Self::Unordered)
937    }
938}
939
940/// A direct ownership fact.
941#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
942pub struct OwnsFact {
943    id: OwnsFactId,
944    #[serde(skip_serializing_if = "CollectionMode::is_unordered")]
945    collection_mode: CollectionMode,
946}
947
948impl OwnsFact {
949    /// Construct an unordered ownership fact.
950    pub const fn new(id: OwnsFactId) -> Self {
951        Self::new_with_collection_mode(id, CollectionMode::Unordered)
952    }
953
954    /// Construct an ownership fact with explicit collection semantics.
955    pub const fn new_with_collection_mode(id: OwnsFactId, collection_mode: CollectionMode) -> Self {
956        Self {
957            id,
958            collection_mode,
959        }
960    }
961
962    /// Replace this fact's collection semantics without changing its identity.
963    #[must_use]
964    pub const fn with_collection_mode(mut self, collection_mode: CollectionMode) -> Self {
965        self.collection_mode = collection_mode;
966        self
967    }
968
969    /// Return the fact identity.
970    pub const fn id(&self) -> &OwnsFactId {
971        &self.id
972    }
973
974    /// Return the canonical collection semantics.
975    pub const fn collection_mode(&self) -> CollectionMode {
976        self.collection_mode
977    }
978}
979
980/// A direct related-role fact, optionally specializing a parent role.
981#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
982pub struct RelatesFact {
983    id: RelatesFactId,
984    specializes: Option<RoleId>,
985    #[serde(skip_serializing_if = "CollectionMode::is_unordered")]
986    collection_mode: CollectionMode,
987}
988
989impl RelatesFact {
990    /// Construct an unordered related-role fact.
991    pub fn new(id: RelatesFactId, specializes: Option<RoleId>) -> Result<Self, Diagnostic> {
992        Self::new_with_collection_mode(id, specializes, CollectionMode::Unordered)
993    }
994
995    /// Construct a related-role fact with explicit collection semantics.
996    pub fn new_with_collection_mode(
997        id: RelatesFactId,
998        specializes: Option<RoleId>,
999        collection_mode: CollectionMode,
1000    ) -> Result<Self, Diagnostic> {
1001        if specializes.as_ref().is_some_and(|role| role == id.role()) {
1002            return Err(schema_diagnostic(
1003                DiagnosticCategory::InvalidContract,
1004                "self_specializing_role",
1005                "a role cannot specialize itself",
1006            ));
1007        }
1008        Ok(Self {
1009            id,
1010            specializes,
1011            collection_mode,
1012        })
1013    }
1014
1015    /// Replace this fact's collection semantics without changing its identity.
1016    #[must_use]
1017    pub const fn with_collection_mode(mut self, collection_mode: CollectionMode) -> Self {
1018        self.collection_mode = collection_mode;
1019        self
1020    }
1021
1022    /// Return the fact identity.
1023    pub const fn id(&self) -> &RelatesFactId {
1024        &self.id
1025    }
1026
1027    /// Return the specialized parent role, if any.
1028    pub const fn specializes(&self) -> Option<&RoleId> {
1029        self.specializes.as_ref()
1030    }
1031
1032    /// Return the canonical collection semantics.
1033    pub const fn collection_mode(&self) -> CollectionMode {
1034        self.collection_mode
1035    }
1036}
1037
1038/// A direct role-playing fact.
1039#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1040pub struct PlaysFact {
1041    id: PlaysFactId,
1042}
1043
1044impl PlaysFact {
1045    /// Construct a role-playing fact.
1046    pub const fn new(id: PlaysFactId) -> Self {
1047        Self { id }
1048    }
1049
1050    /// Return the fact identity.
1051    pub const fn id(&self) -> &PlaysFactId {
1052        &self.id
1053    }
1054}
1055
1056/// An independently identified schema annotation fact.
1057#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1058pub struct AnnotationFact {
1059    id: AnnotationFactId,
1060    value: SchemaAnnotationValue,
1061}
1062
1063impl AnnotationFact {
1064    /// Construct and validate an annotation fact.
1065    pub fn new(id: AnnotationFactId, value: SchemaAnnotationValue) -> Result<Self, Diagnostic> {
1066        validate_annotation(id.subject(), id.kind(), &value)?;
1067        Ok(Self { id, value })
1068    }
1069
1070    /// Return the annotation identity.
1071    pub const fn id(&self) -> &AnnotationFactId {
1072        &self.id
1073    }
1074
1075    /// Return the validated payload.
1076    pub const fn value(&self) -> &SchemaAnnotationValue {
1077        &self.value
1078    }
1079}
1080
1081/// A type reference used by a function signature.
1082#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1083#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
1084pub enum TypeReference {
1085    /// One of the closed built-in scalar value types.
1086    Value(ValueTypeTag),
1087    /// A schema type or struct label resolved against the declared graph.
1088    Schema(Label),
1089}
1090
1091impl TypeReference {
1092    /// Parse the unambiguous TypeQL type-position spelling.
1093    pub fn from_token(value: impl Into<String>) -> Result<Self, Diagnostic> {
1094        let value = value.into();
1095        Ok(value_type_tag(&value)
1096            .map(Self::Value)
1097            .unwrap_or(Self::Schema(Label::new(value)?)))
1098    }
1099
1100    /// Return the referenced schema label, if this is not a built-in value type.
1101    pub const fn schema_label(&self) -> Option<&Label> {
1102        match self {
1103            Self::Value(_) => None,
1104            Self::Schema(label) => Some(label),
1105        }
1106    }
1107}
1108
1109/// One ordered function parameter.
1110#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1111pub struct FunctionParameter {
1112    name: Label,
1113    type_ref: TypeReference,
1114}
1115
1116impl FunctionParameter {
1117    /// Construct a parameter from validated contract values.
1118    pub const fn new(name: Label, type_ref: TypeReference) -> Self {
1119        Self { name, type_ref }
1120    }
1121
1122    /// Return the parameter name without a provider variable sigil.
1123    pub const fn name(&self) -> &Label {
1124        &self.name
1125    }
1126
1127    /// Return the parameter type reference.
1128    pub const fn type_ref(&self) -> &TypeReference {
1129        &self.type_ref
1130    }
1131}
1132
1133/// One ordered element in a function return signature.
1134#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1135pub struct FunctionReturnElement {
1136    type_ref: TypeReference,
1137    optional: bool,
1138}
1139
1140impl FunctionReturnElement {
1141    /// Construct one return element.
1142    pub const fn new(type_ref: TypeReference, optional: bool) -> Self {
1143        Self { type_ref, optional }
1144    }
1145
1146    /// Return the element type reference.
1147    pub const fn type_ref(&self) -> &TypeReference {
1148        &self.type_ref
1149    }
1150
1151    /// Report whether this element may be absent.
1152    pub const fn optional(&self) -> bool {
1153        self.optional
1154    }
1155}
1156
1157/// Native function return cardinality and ordered shape.
1158#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1159#[serde(tag = "kind", content = "elements", rename_all = "snake_case")]
1160pub enum FunctionReturnMode {
1161    /// At most one row containing one element.
1162    Scalar(FunctionReturnElement),
1163    /// At most one row containing two or more ordered elements.
1164    Tuple(Vec<FunctionReturnElement>),
1165    /// Any number of rows containing one or more ordered elements.
1166    Stream(Vec<FunctionReturnElement>),
1167}
1168
1169impl FunctionReturnMode {
1170    /// Construct a scalar return.
1171    pub const fn scalar(element: FunctionReturnElement) -> Self {
1172        Self::Scalar(element)
1173    }
1174
1175    /// Construct a non-empty tuple return with at least two elements.
1176    pub fn tuple(elements: Vec<FunctionReturnElement>) -> Result<Self, Diagnostic> {
1177        if !(2..=MAX_CANONICAL_COLLECTION_LEN).contains(&elements.len()) {
1178            return Err(schema_diagnostic(
1179                DiagnosticCategory::InvalidContract,
1180                "invalid_function_tuple_return",
1181                "tuple function returns require between two and the collection limit elements",
1182            ));
1183        }
1184        Ok(Self::Tuple(elements))
1185    }
1186
1187    /// Construct a non-empty stream return.
1188    pub fn stream(elements: Vec<FunctionReturnElement>) -> Result<Self, Diagnostic> {
1189        if elements.is_empty() || elements.len() > MAX_CANONICAL_COLLECTION_LEN {
1190            return Err(schema_diagnostic(
1191                DiagnosticCategory::InvalidContract,
1192                "invalid_function_stream_return",
1193                "stream function returns require a non-empty bounded element list",
1194            ));
1195        }
1196        Ok(Self::Stream(elements))
1197    }
1198
1199    /// Return elements in semantic signature order.
1200    pub fn elements(&self) -> &[FunctionReturnElement] {
1201        match self {
1202            Self::Scalar(element) => std::slice::from_ref(element),
1203            Self::Tuple(elements) | Self::Stream(elements) => elements,
1204        }
1205    }
1206}
1207
1208/// A validated ordered function signature.
1209#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1210pub struct FunctionSignature {
1211    parameters: Vec<FunctionParameter>,
1212    returns: FunctionReturnMode,
1213}
1214
1215impl FunctionSignature {
1216    /// Construct a signature with unique, bounded ordered parameters.
1217    pub fn new(
1218        parameters: Vec<FunctionParameter>,
1219        returns: FunctionReturnMode,
1220    ) -> Result<Self, Diagnostic> {
1221        if parameters.len() > MAX_CANONICAL_COLLECTION_LEN {
1222            return Err(schema_diagnostic(
1223                DiagnosticCategory::ResourceLimit,
1224                "too_many_function_parameters",
1225                "function parameter count exceeds the canonical collection limit",
1226            ));
1227        }
1228        let mut names = BTreeSet::new();
1229        if parameters
1230            .iter()
1231            .any(|parameter| !names.insert(parameter.name().clone()))
1232        {
1233            return Err(schema_diagnostic(
1234                DiagnosticCategory::InvalidContract,
1235                "duplicate_function_parameter",
1236                "function parameter names must be unique",
1237            ));
1238        }
1239        Ok(Self {
1240            parameters,
1241            returns,
1242        })
1243    }
1244
1245    /// Return parameters in semantic declaration order.
1246    pub fn parameters(&self) -> &[FunctionParameter] {
1247        &self.parameters
1248    }
1249
1250    /// Return the native return shape.
1251    pub const fn returns(&self) -> &FunctionReturnMode {
1252        &self.returns
1253    }
1254}
1255
1256/// Decoded provider-native function body text retained verbatim.
1257#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1258#[serde(transparent)]
1259pub struct FunctionBody(String);
1260
1261impl FunctionBody {
1262    /// Construct a non-empty bounded body without trimming or rewriting it.
1263    pub fn new(text: impl Into<String>) -> Result<Self, Diagnostic> {
1264        let text = text.into();
1265        if text.is_empty() || text.len() > MAX_CANONICAL_STRING_BYTES {
1266            return Err(schema_diagnostic(
1267                DiagnosticCategory::InvalidContract,
1268                "invalid_function_body",
1269                "decoded function body must be non-empty and bounded",
1270            ));
1271        }
1272        Ok(Self(text))
1273    }
1274
1275    /// Return exact decoded body text, including comments and trailing newline.
1276    pub fn text(&self) -> &str {
1277        &self.0
1278    }
1279}
1280
1281/// A function declaration with structured signature and decoded body text.
1282#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1283pub struct FunctionFact {
1284    id: FunctionId,
1285    signature: FunctionSignature,
1286    body: FunctionBody,
1287}
1288
1289impl FunctionFact {
1290    /// Construct a validated function declaration.
1291    pub const fn new(id: FunctionId, signature: FunctionSignature, body: FunctionBody) -> Self {
1292        Self {
1293            id,
1294            signature,
1295            body,
1296        }
1297    }
1298
1299    /// Return the function identity.
1300    pub const fn id(&self) -> &FunctionId {
1301        &self.id
1302    }
1303
1304    /// Return the structured signature.
1305    pub const fn signature(&self) -> &FunctionSignature {
1306        &self.signature
1307    }
1308
1309    /// Return exact decoded provider body text.
1310    pub const fn body(&self) -> &FunctionBody {
1311        &self.body
1312    }
1313
1314    /// Iterate schema labels referenced by the signature.
1315    pub fn schema_references(&self) -> impl Iterator<Item = &Label> {
1316        self.signature
1317            .parameters()
1318            .iter()
1319            .filter_map(|parameter| parameter.type_ref().schema_label())
1320            .chain(
1321                self.signature
1322                    .returns()
1323                    .elements()
1324                    .iter()
1325                    .filter_map(|element| element.type_ref().schema_label()),
1326            )
1327    }
1328}
1329
1330/// One ordered field in a struct declaration.
1331#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1332pub struct StructField {
1333    name: Label,
1334    value_type: ValueTypeTag,
1335    optional: bool,
1336}
1337
1338impl StructField {
1339    /// Construct a field from validated contract values.
1340    pub const fn new(name: Label, value_type: ValueTypeTag, optional: bool) -> Self {
1341        Self {
1342            name,
1343            value_type,
1344            optional,
1345        }
1346    }
1347
1348    /// Return the field name.
1349    pub const fn name(&self) -> &Label {
1350        &self.name
1351    }
1352
1353    /// Return the built-in field value type.
1354    pub const fn value_type(&self) -> ValueTypeTag {
1355        self.value_type
1356    }
1357
1358    /// Report whether the field may be absent.
1359    pub const fn optional(&self) -> bool {
1360        self.optional
1361    }
1362}
1363
1364/// A named struct declaration with ordered, non-empty built-in fields.
1365#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1366pub struct StructFact {
1367    id: StructId,
1368    fields: Vec<StructField>,
1369}
1370
1371impl StructFact {
1372    /// Construct and validate a field-bearing struct declaration.
1373    pub fn new(id: StructId, fields: Vec<StructField>) -> Result<Self, Diagnostic> {
1374        if value_type_tag(id.label().as_str()).is_some() {
1375            return Err(schema_diagnostic(
1376                DiagnosticCategory::InvalidContract,
1377                "reserved_schema_type_label",
1378                "struct labels cannot collide with built-in value-type tokens",
1379            ));
1380        }
1381        if fields.is_empty() {
1382            return Err(schema_diagnostic(
1383                DiagnosticCategory::InvalidContract,
1384                "empty_struct_fields",
1385                "struct declarations require at least one field",
1386            ));
1387        }
1388        if fields.len() > MAX_CANONICAL_COLLECTION_LEN {
1389            return Err(schema_diagnostic(
1390                DiagnosticCategory::ResourceLimit,
1391                "too_many_struct_fields",
1392                "struct field count exceeds the canonical collection limit",
1393            ));
1394        }
1395
1396        let mut names = BTreeSet::new();
1397        for field in &fields {
1398            if !names.insert(field.name().clone()) {
1399                return Err(schema_diagnostic(
1400                    DiagnosticCategory::InvalidContract,
1401                    "duplicate_struct_field",
1402                    "struct field names must be unique within the struct",
1403                ));
1404            }
1405        }
1406
1407        Ok(Self { id, fields })
1408    }
1409
1410    /// Return the struct identity.
1411    pub const fn id(&self) -> &StructId {
1412        &self.id
1413    }
1414
1415    /// Return fields in their declared semantic order.
1416    pub fn fields(&self) -> &[StructField] {
1417        &self.fields
1418    }
1419}
1420
1421/// Stable identity of any Phase 2 schema fact.
1422#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
1423#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
1424pub enum SchemaFactId {
1425    /// Type existence.
1426    Type(TypeId),
1427    /// Direct subtype edge.
1428    Sub(SubFactId),
1429    /// Attribute scalar domain.
1430    Value(ValueFactId),
1431    /// Direct ownership.
1432    Owns(OwnsFactId),
1433    /// Direct related role.
1434    Relates(RelatesFactId),
1435    /// Direct role playing.
1436    Plays(PlaysFactId),
1437    /// Independent annotation.
1438    Annotation(AnnotationFactId),
1439    /// Function declaration.
1440    Function(FunctionId),
1441    /// Struct declaration.
1442    Struct(StructId),
1443}
1444
1445/// A validated, atomic direct schema fact.
1446#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1447#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
1448pub enum SchemaFact {
1449    /// Type existence.
1450    Type(TypeFact),
1451    /// Direct subtype edge.
1452    Sub(SubFact),
1453    /// Attribute scalar domain.
1454    Value(ValueFact),
1455    /// Direct ownership.
1456    Owns(OwnsFact),
1457    /// Direct related role.
1458    Relates(RelatesFact),
1459    /// Direct role playing.
1460    Plays(PlaysFact),
1461    /// Independent annotation.
1462    Annotation(AnnotationFact),
1463    /// Function declaration.
1464    Function(FunctionFact),
1465    /// Struct declaration.
1466    Struct(StructFact),
1467}
1468
1469impl SchemaFact {
1470    /// Return the stable structural identity.
1471    pub fn id(&self) -> SchemaFactId {
1472        match self {
1473            Self::Type(fact) => SchemaFactId::Type(fact.id().clone()),
1474            Self::Sub(fact) => SchemaFactId::Sub(fact.id().clone()),
1475            Self::Value(fact) => SchemaFactId::Value(fact.id().clone()),
1476            Self::Owns(fact) => SchemaFactId::Owns(fact.id().clone()),
1477            Self::Relates(fact) => SchemaFactId::Relates(fact.id().clone()),
1478            Self::Plays(fact) => SchemaFactId::Plays(fact.id().clone()),
1479            Self::Annotation(fact) => SchemaFactId::Annotation(fact.id().clone()),
1480            Self::Function(fact) => SchemaFactId::Function(fact.id().clone()),
1481            Self::Struct(fact) => SchemaFactId::Struct(fact.id().clone()),
1482        }
1483    }
1484}
1485
1486/// A direct fact paired with the one source span that owns it.
1487#[derive(Debug, Clone, PartialEq, Eq)]
1488pub struct SourcedSchemaFact {
1489    fact: SchemaFact,
1490    source: SourceSpan,
1491}
1492
1493impl SourcedSchemaFact {
1494    /// Pair a validated fact with its direct source.
1495    pub const fn new(fact: SchemaFact, source: SourceSpan) -> Self {
1496        Self { fact, source }
1497    }
1498
1499    /// Return the fact.
1500    pub const fn fact(&self) -> &SchemaFact {
1501        &self.fact
1502    }
1503
1504    /// Return the owning source span.
1505    pub const fn source(&self) -> &SourceSpan {
1506        &self.source
1507    }
1508}
1509
1510/// A domain-safe source-document fingerprint.
1511#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1512#[serde(transparent)]
1513pub struct DocumentFingerprint(Fingerprint);
1514
1515impl DocumentFingerprint {
1516    /// Fingerprint exact source bytes, including comments and spelling.
1517    pub fn compute(source: &[u8]) -> Result<Self, Diagnostic> {
1518        Ok(Self(Fingerprint::compute(
1519            FingerprintDomain::new("typebridge.schema.document")?,
1520            CanonicalizationVersion::new("typebridge.raw-utf8/v1")?,
1521            None,
1522            source,
1523        )))
1524    }
1525
1526    /// Return the generic fingerprint metadata.
1527    pub const fn as_fingerprint(&self) -> &Fingerprint {
1528        &self.0
1529    }
1530}
1531
1532/// A domain-safe fingerprint of direct fact identity and meaning.
1533#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1534#[serde(transparent)]
1535pub struct DeclaredIdentityFingerprint(Fingerprint);
1536
1537impl DeclaredIdentityFingerprint {
1538    fn compute(canonical_bytes: &[u8]) -> Result<Self, Diagnostic> {
1539        Ok(Self(Fingerprint::compute(
1540            FingerprintDomain::new("typebridge.schema.declared-identity")?,
1541            CanonicalizationVersion::new("typebridge.schema-canonical-json/v1")?,
1542            None,
1543            canonical_bytes,
1544        )))
1545    }
1546
1547    /// Return the generic fingerprint metadata.
1548    pub const fn as_fingerprint(&self) -> &Fingerprint {
1549        &self.0
1550    }
1551
1552    pub(crate) fn from_wire(fingerprint: Fingerprint) -> Result<Self, Diagnostic> {
1553        if fingerprint.domain().as_str() != "typebridge.schema.declared-identity"
1554            || fingerprint.canonicalization().as_str() != "typebridge.schema-canonical-json/v1"
1555            || fingerprint.semantic_profile().is_some()
1556        {
1557            return Err(Diagnostic::stable(
1558                DiagnosticCategory::Integrity,
1559                "invalid_declared_identity_fingerprint",
1560                "declared identity fingerprint metadata is invalid",
1561            ));
1562        }
1563        Ok(Self(fingerprint))
1564    }
1565}
1566
1567/// A validated normalized graph of direct schema facts.
1568#[derive(Debug, Clone, PartialEq, Eq)]
1569pub struct DeclaredSchema {
1570    format: FormatVersion,
1571    required_capabilities: CapabilitySet,
1572    facts: BTreeMap<SchemaFactId, SchemaFact>,
1573    provenance: BTreeMap<SchemaFactId, SourceSpan>,
1574    fingerprint: DeclaredIdentityFingerprint,
1575}
1576
1577impl Serialize for DeclaredSchema {
1578    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1579    where
1580        S: Serializer,
1581    {
1582        #[derive(Serialize)]
1583        struct TrustedDeclaredSchemaView<'a> {
1584            declared_identity: &'a DeclaredIdentityFingerprint,
1585            facts: Vec<&'a SchemaFact>,
1586            format_version: FormatVersion,
1587            required_capabilities: &'a CapabilitySet,
1588        }
1589
1590        TrustedDeclaredSchemaView {
1591            declared_identity: &self.fingerprint,
1592            facts: self.facts.values().collect(),
1593            format_version: self.format,
1594            required_capabilities: &self.required_capabilities,
1595        }
1596        .serialize(serializer)
1597    }
1598}
1599
1600impl DeclaredSchema {
1601    /// Validate direct fact ownership, references, and annotation combinations.
1602    pub fn from_facts(
1603        format: FormatVersion,
1604        required_capabilities: CapabilitySet,
1605        sourced_facts: impl IntoIterator<Item = SourcedSchemaFact>,
1606    ) -> Result<Self, SchemaDiagnostics> {
1607        ensure_format_version(format, FormatVersion::V1)
1608            .map_err(|error| SchemaDiagnostics::one(SchemaDiagnostic::new(error, None)))?;
1609
1610        let mut facts = BTreeMap::new();
1611        let mut provenance = BTreeMap::<SchemaFactId, SourceSpan>::new();
1612        let mut diagnostics = Vec::new();
1613        for sourced in sourced_facts {
1614            let id = sourced.fact.id();
1615            if let Some(previous) = provenance.get(&id) {
1616                diagnostics.push(
1617                    SchemaDiagnostic::new(
1618                        schema_diagnostic(
1619                            DiagnosticCategory::InvalidContract,
1620                            "duplicate_schema_fact",
1621                            "a direct schema fact is declared more than once",
1622                        ),
1623                        Some(sourced.source.clone()),
1624                    )
1625                    .with_related(DiagnosticLabel::new(
1626                        previous.clone(),
1627                        "first declaration is here",
1628                    )),
1629                );
1630                continue;
1631            }
1632            provenance.insert(id.clone(), sourced.source);
1633            facts.insert(id, sourced.fact);
1634        }
1635
1636        if diagnostics.is_empty() {
1637            validate_references(&facts, &provenance, &mut diagnostics);
1638            validate_annotation_combinations(&facts, &provenance, &mut diagnostics);
1639            validate_annotation_value_domains(&facts, &provenance, &mut diagnostics);
1640        }
1641        if !diagnostics.is_empty() {
1642            return Err(SchemaDiagnostics::from_vec(diagnostics));
1643        }
1644
1645        let canonical =
1646            canonical_declared_identity_bytes(format, &required_capabilities, &facts)
1647                .map_err(|error| SchemaDiagnostics::one(SchemaDiagnostic::new(error, None)))?;
1648        let fingerprint = DeclaredIdentityFingerprint::compute(&canonical)
1649            .map_err(|error| SchemaDiagnostics::one(SchemaDiagnostic::new(error, None)))?;
1650        Ok(Self {
1651            format,
1652            required_capabilities,
1653            facts,
1654            provenance,
1655            fingerprint,
1656        })
1657    }
1658
1659    /// Return the schema format version.
1660    pub const fn format(&self) -> FormatVersion {
1661        self.format
1662    }
1663
1664    /// Return the required open capability set.
1665    pub const fn required_capabilities(&self) -> &CapabilitySet {
1666        &self.required_capabilities
1667    }
1668
1669    /// Return a fact by stable identity.
1670    pub fn fact(&self, id: &SchemaFactId) -> Option<&SchemaFact> {
1671        self.facts.get(id)
1672    }
1673
1674    /// Iterate facts in stable identity order.
1675    pub fn facts(&self) -> impl ExactSizeIterator<Item = &SchemaFact> {
1676        self.facts.values()
1677    }
1678
1679    /// Return the direct source owner of a fact.
1680    pub fn source(&self, id: &SchemaFactId) -> Option<&SourceSpan> {
1681        self.provenance.get(id)
1682    }
1683
1684    /// Return canonical identity bytes with presentation provenance excluded.
1685    pub fn canonical_identity_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
1686        canonical_declared_identity_bytes(self.format, &self.required_capabilities, &self.facts)
1687    }
1688
1689    /// Return the declared identity fingerprint.
1690    pub const fn declared_identity_fingerprint(&self) -> &DeclaredIdentityFingerprint {
1691        &self.fingerprint
1692    }
1693}
1694
1695/// Encode only a constructor-validated declared schema as canonical JSON.
1696pub fn encode_declared_schema(schema: &DeclaredSchema) -> Result<Vec<u8>, Diagnostic> {
1697    crate::declared_schema_wire::encode_declared_schema(schema)
1698}
1699
1700/// Decode canonical bytes through private wire DTOs and every fact/schema constructor.
1701pub fn decode_declared_schema(bytes: &[u8]) -> Result<DeclaredSchema, Diagnostic> {
1702    crate::declared_schema_wire::decode_declared_schema(bytes)
1703}
1704
1705#[derive(Serialize)]
1706struct DeclaredIdentityView<'a> {
1707    format_version: FormatVersion,
1708    required_capabilities: &'a CapabilitySet,
1709    facts: Vec<&'a SchemaFact>,
1710}
1711
1712fn canonical_declared_identity_bytes(
1713    format: FormatVersion,
1714    required_capabilities: &CapabilitySet,
1715    facts: &BTreeMap<SchemaFactId, SchemaFact>,
1716) -> Result<Vec<u8>, Diagnostic> {
1717    to_canonical_json(&DeclaredIdentityView {
1718        format_version: format,
1719        required_capabilities,
1720        facts: facts.values().collect(),
1721    })
1722}
1723
1724fn validate_annotation(
1725    subject: &AnnotationSubjectId,
1726    kind: &AnnotationKindId,
1727    value: &SchemaAnnotationValue,
1728) -> Result<(), Diagnostic> {
1729    let payload_matches = matches!(
1730        (kind, value),
1731        (
1732            AnnotationKindId::Abstract
1733                | AnnotationKindId::Independent
1734                | AnnotationKindId::Key
1735                | AnnotationKindId::Unique
1736                | AnnotationKindId::Distinct,
1737            SchemaAnnotationValue::Presence
1738        ) | (
1739            AnnotationKindId::Card,
1740            SchemaAnnotationValue::Cardinality(_)
1741        ) | (AnnotationKindId::Regex, SchemaAnnotationValue::Regex(_))
1742            | (AnnotationKindId::Range, SchemaAnnotationValue::Range(_))
1743            | (AnnotationKindId::Values, SchemaAnnotationValue::Values(_))
1744            | (AnnotationKindId::Doc, SchemaAnnotationValue::Doc(_))
1745            | (
1746                AnnotationKindId::Meta(_),
1747                SchemaAnnotationValue::Meta(CanonicalValue::String(_))
1748            )
1749    );
1750    if !payload_matches {
1751        return Err(schema_diagnostic(
1752            DiagnosticCategory::InvalidContract,
1753            "invalid_annotation_payload",
1754            "annotation kind and payload do not agree",
1755        ));
1756    }
1757    let subject_matches = match kind {
1758        AnnotationKindId::Abstract => match subject {
1759            AnnotationSubjectId::Type(id) => matches!(
1760                id.kind(),
1761                TypeKind::Entity | TypeKind::Relation | TypeKind::Attribute
1762            ),
1763            AnnotationSubjectId::Relates(_) => true,
1764            AnnotationSubjectId::Sub(_)
1765            | AnnotationSubjectId::Value(_)
1766            | AnnotationSubjectId::Owns(_)
1767            | AnnotationSubjectId::Plays(_)
1768            | AnnotationSubjectId::Function(_) => false,
1769        },
1770        AnnotationKindId::Independent => matches!(
1771            subject,
1772            AnnotationSubjectId::Type(id) if id.kind() == TypeKind::Attribute
1773        ),
1774        AnnotationKindId::Key | AnnotationKindId::Unique => {
1775            matches!(subject, AnnotationSubjectId::Owns(_))
1776        }
1777        AnnotationKindId::Distinct => matches!(
1778            subject,
1779            AnnotationSubjectId::Owns(_) | AnnotationSubjectId::Relates(_)
1780        ),
1781        AnnotationKindId::Card => matches!(
1782            subject,
1783            AnnotationSubjectId::Owns(_)
1784                | AnnotationSubjectId::Relates(_)
1785                | AnnotationSubjectId::Plays(_)
1786        ),
1787        AnnotationKindId::Regex | AnnotationKindId::Range | AnnotationKindId::Values => {
1788            matches!(
1789                subject,
1790                AnnotationSubjectId::Value(_) | AnnotationSubjectId::Owns(_)
1791            )
1792        }
1793        AnnotationKindId::Doc | AnnotationKindId::Meta(_) => matches!(
1794            subject,
1795            AnnotationSubjectId::Type(_)
1796                | AnnotationSubjectId::Sub(_)
1797                | AnnotationSubjectId::Owns(_)
1798                | AnnotationSubjectId::Relates(_)
1799                | AnnotationSubjectId::Plays(_)
1800                | AnnotationSubjectId::Function(_)
1801        ),
1802    };
1803    if !subject_matches {
1804        return Err(schema_diagnostic(
1805            DiagnosticCategory::InvalidContract,
1806            "invalid_annotation_subject",
1807            "annotation kind does not apply to this schema subject",
1808        ));
1809    }
1810    Ok(())
1811}
1812
1813fn validate_annotation_value_domains(
1814    facts: &BTreeMap<SchemaFactId, SchemaFact>,
1815    provenance: &BTreeMap<SchemaFactId, SourceSpan>,
1816    diagnostics: &mut Vec<SchemaDiagnostic>,
1817) {
1818    for (fact_id, fact) in facts {
1819        let SchemaFact::Annotation(annotation) = fact else {
1820            continue;
1821        };
1822
1823        let kind = annotation.id().kind();
1824        if !matches!(
1825            kind,
1826            AnnotationKindId::Key
1827                | AnnotationKindId::Unique
1828                | AnnotationKindId::Regex
1829                | AnnotationKindId::Range
1830                | AnnotationKindId::Values
1831        ) {
1832            continue;
1833        }
1834
1835        let Some((value_type, value_fact_id)) =
1836            annotation_subject_value_type(annotation.id().subject(), facts)
1837        else {
1838            diagnostics.push(SchemaDiagnostic::new(
1839                schema_diagnostic(
1840                    DiagnosticCategory::InvalidContract,
1841                    "unknown_annotation_value_domain",
1842                    "annotation subject has no resolvable attribute value domain",
1843                ),
1844                provenance.get(fact_id).cloned(),
1845            ));
1846            continue;
1847        };
1848
1849        let valid = match (kind, annotation.value()) {
1850            (AnnotationKindId::Key | AnnotationKindId::Unique, _) => {
1851                value_type != ValueTypeTag::Double
1852            }
1853            (AnnotationKindId::Regex, SchemaAnnotationValue::Regex(_)) => {
1854                value_type == ValueTypeTag::String
1855            }
1856            (AnnotationKindId::Range, SchemaAnnotationValue::Range(range)) => {
1857                value_type != ValueTypeTag::Duration
1858                    && range
1859                        .lower()
1860                        .into_iter()
1861                        .chain(range.upper())
1862                        .all(|bound| bound.value_type() == value_type)
1863            }
1864            (AnnotationKindId::Values, SchemaAnnotationValue::Values(values)) => {
1865                values.iter().all(|value| value.value_type() == value_type)
1866            }
1867            _ => false,
1868        };
1869
1870        if !valid {
1871            let mut diagnostic = SchemaDiagnostic::new(
1872                schema_diagnostic(
1873                    DiagnosticCategory::InvalidContract,
1874                    "invalid_annotation_value_domain",
1875                    "annotation payload is incompatible with the attribute value domain",
1876                ),
1877                provenance.get(fact_id).cloned(),
1878            );
1879            if let Some(value_source) = provenance.get(&value_fact_id) {
1880                diagnostic = diagnostic.with_related(DiagnosticLabel::new(
1881                    value_source.clone(),
1882                    "attribute value domain is declared here",
1883                ));
1884            }
1885            diagnostics.push(diagnostic);
1886        }
1887    }
1888}
1889
1890fn annotation_subject_value_type(
1891    subject: &AnnotationSubjectId,
1892    facts: &BTreeMap<SchemaFactId, SchemaFact>,
1893) -> Option<(ValueTypeTag, SchemaFactId)> {
1894    let mut attribute = match subject {
1895        AnnotationSubjectId::Value(id) => id.attribute().clone(),
1896        AnnotationSubjectId::Owns(id) => id.attribute().clone(),
1897        AnnotationSubjectId::Type(_)
1898        | AnnotationSubjectId::Sub(_)
1899        | AnnotationSubjectId::Relates(_)
1900        | AnnotationSubjectId::Plays(_)
1901        | AnnotationSubjectId::Function(_) => return None,
1902    };
1903    let mut visited = BTreeSet::new();
1904
1905    loop {
1906        let attribute_type = TypeId::new(TypeKind::Attribute, attribute.label().as_str()).ok()?;
1907        if !visited.insert(attribute_type.clone()) {
1908            return None;
1909        }
1910
1911        let value_fact_id = ValueFactId::new(attribute.clone());
1912        let schema_fact_id = SchemaFactId::Value(value_fact_id);
1913        if let Some(SchemaFact::Value(value)) = facts.get(&schema_fact_id) {
1914            return Some((value.value_type(), schema_fact_id));
1915        }
1916
1917        let supertype = facts.values().find_map(|fact| {
1918            let SchemaFact::Sub(sub) = fact else {
1919                return None;
1920            };
1921            (sub.id().subtype() == &attribute_type
1922                && sub.id().supertype().kind() == TypeKind::Attribute)
1923                .then(|| sub.id().supertype().clone())
1924        })?;
1925        attribute = AttributeId::new(supertype.label().as_str()).ok()?;
1926    }
1927}
1928
1929fn validate_references(
1930    facts: &BTreeMap<SchemaFactId, SchemaFact>,
1931    provenance: &BTreeMap<SchemaFactId, SourceSpan>,
1932    diagnostics: &mut Vec<SchemaDiagnostic>,
1933) {
1934    let type_ids = facts
1935        .keys()
1936        .filter_map(|id| match id {
1937            SchemaFactId::Type(id) => Some(id.clone()),
1938            _ => None,
1939        })
1940        .collect::<BTreeSet<_>>();
1941    let role_ids = facts
1942        .keys()
1943        .filter_map(|id| match id {
1944            SchemaFactId::Relates(id) => Some(id.role().clone()),
1945            _ => None,
1946        })
1947        .collect::<BTreeSet<_>>();
1948    let struct_labels = facts
1949        .keys()
1950        .filter_map(|id| match id {
1951            SchemaFactId::Struct(id) => Some(id.label().clone()),
1952            _ => None,
1953        })
1954        .collect::<BTreeSet<_>>();
1955
1956    for (id, fact) in facts {
1957        let valid = match fact {
1958            SchemaFact::Type(_) | SchemaFact::Struct(_) => true,
1959            SchemaFact::Function(fact) => fact.schema_references().all(|label| {
1960                type_ids.iter().any(|id| id.label() == label) || struct_labels.contains(label)
1961            }),
1962            SchemaFact::Sub(fact) => {
1963                type_ids.contains(fact.id().subtype()) && type_ids.contains(fact.id().supertype())
1964            }
1965            SchemaFact::Value(fact) => type_ids.contains(&attribute_type_id(fact.id().attribute())),
1966            SchemaFact::Owns(fact) => {
1967                type_ids.contains(fact.id().owner())
1968                    && type_ids.contains(&attribute_type_id(fact.id().attribute()))
1969            }
1970            SchemaFact::Relates(fact) => {
1971                type_ids.contains(fact.id().relation())
1972                    && fact
1973                        .specializes()
1974                        .is_none_or(|role| role_ids.contains(role))
1975            }
1976            SchemaFact::Plays(fact) => {
1977                type_ids.contains(fact.id().player()) && role_ids.contains(fact.id().role())
1978            }
1979            SchemaFact::Annotation(fact) => {
1980                facts.contains_key(&subject_fact_id(fact.id().subject()))
1981            }
1982        };
1983        if !valid {
1984            diagnostics.push(SchemaDiagnostic::new(
1985                schema_diagnostic(
1986                    DiagnosticCategory::InvalidContract,
1987                    "unknown_schema_fact_reference",
1988                    "schema fact references a declaration that does not exist",
1989                ),
1990                provenance.get(id).cloned(),
1991            ));
1992        }
1993    }
1994}
1995
1996fn validate_annotation_combinations(
1997    facts: &BTreeMap<SchemaFactId, SchemaFact>,
1998    provenance: &BTreeMap<SchemaFactId, SourceSpan>,
1999    diagnostics: &mut Vec<SchemaDiagnostic>,
2000) {
2001    let mut by_subject = BTreeMap::<AnnotationSubjectId, BTreeSet<AnnotationKindId>>::new();
2002    for fact in facts.values() {
2003        if let SchemaFact::Annotation(annotation) = fact {
2004            by_subject
2005                .entry(annotation.id().subject().clone())
2006                .or_default()
2007                .insert(annotation.id().kind().clone());
2008        }
2009    }
2010    for (subject, kinds) in by_subject {
2011        if kinds.contains(&AnnotationKindId::Distinct) {
2012            let subject_id = subject_fact_id(&subject);
2013            let ordered = match facts.get(&subject_id) {
2014                Some(SchemaFact::Owns(fact)) => {
2015                    fact.collection_mode() == CollectionMode::OrderedList
2016                }
2017                Some(SchemaFact::Relates(fact)) => {
2018                    fact.collection_mode() == CollectionMode::OrderedList
2019                }
2020                _ => true,
2021            };
2022            if !ordered {
2023                let distinct_id = SchemaFactId::Annotation(AnnotationFactId::new(
2024                    subject.clone(),
2025                    AnnotationKindId::Distinct,
2026                ));
2027                let mut diagnostic = SchemaDiagnostic::new(
2028                    schema_diagnostic(
2029                        DiagnosticCategory::InvalidContract,
2030                        "distinct_requires_ordered_collection",
2031                        "distinct applies only to an ordered ownership or related-role collection",
2032                    ),
2033                    provenance.get(&distinct_id).cloned(),
2034                );
2035                if let Some(subject_source) = provenance.get(&subject_id) {
2036                    diagnostic = diagnostic.with_related(DiagnosticLabel::new(
2037                        subject_source.clone(),
2038                        "unordered collection fact is declared here",
2039                    ));
2040                }
2041                diagnostics.push(diagnostic);
2042            }
2043        }
2044        if kinds.contains(&AnnotationKindId::Key)
2045            && (kinds.contains(&AnnotationKindId::Unique)
2046                || kinds.contains(&AnnotationKindId::Card))
2047        {
2048            let key_id =
2049                SchemaFactId::Annotation(AnnotationFactId::new(subject, AnnotationKindId::Key));
2050            diagnostics.push(SchemaDiagnostic::new(
2051                schema_diagnostic(
2052                    DiagnosticCategory::InvalidContract,
2053                    "key_annotation_conflict",
2054                    "key cannot be combined with unique or cardinality",
2055                ),
2056                provenance.get(&key_id).cloned(),
2057            ));
2058        }
2059    }
2060}
2061
2062fn attribute_type_id(attribute: &AttributeId) -> TypeId {
2063    TypeId::new(TypeKind::Attribute, attribute.label().as_str())
2064        .expect("validated attribute labels always form attribute type identities")
2065}
2066
2067fn subject_fact_id(subject: &AnnotationSubjectId) -> SchemaFactId {
2068    match subject {
2069        AnnotationSubjectId::Type(id) => SchemaFactId::Type(id.clone()),
2070        AnnotationSubjectId::Sub(id) => SchemaFactId::Sub(id.clone()),
2071        AnnotationSubjectId::Value(id) => SchemaFactId::Value(id.clone()),
2072        AnnotationSubjectId::Owns(id) => SchemaFactId::Owns(id.clone()),
2073        AnnotationSubjectId::Relates(id) => SchemaFactId::Relates(id.clone()),
2074        AnnotationSubjectId::Plays(id) => SchemaFactId::Plays(id.clone()),
2075        AnnotationSubjectId::Function(id) => SchemaFactId::Function(id.clone()),
2076    }
2077}
2078
2079fn value_type_tag(value: &str) -> Option<ValueTypeTag> {
2080    match value {
2081        "string" => Some(ValueTypeTag::String),
2082        "integer" => Some(ValueTypeTag::Long),
2083        "double" => Some(ValueTypeTag::Double),
2084        "boolean" => Some(ValueTypeTag::Boolean),
2085        "date" => Some(ValueTypeTag::Date),
2086        "datetime" => Some(ValueTypeTag::DateTime),
2087        "datetime-tz" => Some(ValueTypeTag::DateTimeTz),
2088        "decimal" => Some(ValueTypeTag::Decimal),
2089        "duration" => Some(ValueTypeTag::Duration),
2090        _ => None,
2091    }
2092}
2093
2094fn schema_diagnostic(
2095    category: DiagnosticCategory,
2096    code: &'static str,
2097    message: &'static str,
2098) -> Diagnostic {
2099    Diagnostic::stable(category, code, message)
2100}