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    /// `@card`.
461    Card,
462    /// `@regex`.
463    Regex,
464    /// `@range`.
465    Range,
466    /// `@values`.
467    Values,
468    /// `@doc`.
469    Doc,
470    /// One independently identified `@meta` key.
471    Meta(Label),
472}
473
474impl AnnotationKindId {
475    /// Construct an independently identified metadata kind.
476    pub fn meta(key: impl Into<String>) -> Result<Self, Diagnostic> {
477        Label::new(key).map(Self::Meta)
478    }
479}
480
481/// Identity of an independent annotation fact.
482#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
483pub struct AnnotationFactId {
484    subject: AnnotationSubjectId,
485    kind: AnnotationKindId,
486}
487
488impl AnnotationFactId {
489    /// Construct an annotation identity.
490    pub const fn new(subject: AnnotationSubjectId, kind: AnnotationKindId) -> Self {
491        Self { subject, kind }
492    }
493
494    /// Return the annotated subject.
495    pub const fn subject(&self) -> &AnnotationSubjectId {
496        &self.subject
497    }
498
499    /// Return the annotation kind.
500    pub const fn kind(&self) -> &AnnotationKindId {
501        &self.kind
502    }
503}
504
505/// A validated regular-expression payload retained without a regex engine.
506#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
507#[serde(transparent)]
508pub struct RegexPattern(String);
509
510impl RegexPattern {
511    /// Validate a regex source payload.
512    pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
513        let value = value.into();
514        if value.is_empty() || value.len() > MAX_CANONICAL_STRING_BYTES {
515            return Err(schema_diagnostic(
516                DiagnosticCategory::InvalidContract,
517                "invalid_regex_annotation",
518                "regex annotation text must be non-empty and bounded",
519            ));
520        }
521        Ok(Self(value))
522    }
523
524    /// Return the regex source.
525    pub fn as_str(&self) -> &str {
526        &self.0
527    }
528}
529
530/// A validated non-empty documentation payload.
531#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
532#[serde(transparent)]
533pub struct DocText(String);
534
535impl DocText {
536    /// Validate documentation text.
537    pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
538        let value = value.into();
539        if value.is_empty() || value.len() > MAX_CANONICAL_STRING_BYTES {
540            return Err(schema_diagnostic(
541                DiagnosticCategory::InvalidContract,
542                "invalid_doc_annotation",
543                "documentation text must be non-empty and bounded",
544            ));
545        }
546        Ok(Self(value))
547    }
548
549    /// Return the documentation text.
550    pub fn as_str(&self) -> &str {
551        &self.0
552    }
553}
554
555/// A non-empty exact-domain canonical value set.
556#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
557#[serde(transparent)]
558pub struct CanonicalValueSet(BTreeSet<CanonicalValue>);
559
560/// Precise validation failure for a raw `@values` member sequence.
561#[derive(Debug, Clone, PartialEq, Eq)]
562pub enum CanonicalValueSetViolation {
563    /// No members were supplied.
564    Empty,
565    /// The raw member sequence exceeded the canonical collection ceiling.
566    MemberLimitExceeded {
567        /// Maximum accepted raw member count.
568        maximum: usize,
569        /// Index of the first rejected raw member.
570        first_excess_index: usize,
571    },
572    /// One member used a different exact scalar domain.
573    MixedDomain {
574        /// Domain established by the first member.
575        expected: ValueTypeTag,
576        /// Domain of the conflicting member.
577        actual: ValueTypeTag,
578        /// Index of the conflicting member.
579        member_index: usize,
580    },
581    /// One exact canonical value occurred more than once.
582    Duplicate {
583        /// Index of the first occurrence.
584        first_index: usize,
585        /// Index of the duplicate occurrence.
586        duplicate_index: usize,
587    },
588}
589
590impl CanonicalValueSetViolation {
591    /// Convert to the stable compatibility diagnostic returned by `new`.
592    pub fn into_diagnostic(self) -> Diagnostic {
593        match self {
594            Self::Empty => schema_diagnostic(
595                DiagnosticCategory::InvalidContract,
596                "empty_values_annotation",
597                "values annotations must contain at least one value",
598            ),
599            Self::MemberLimitExceeded {
600                maximum,
601                first_excess_index,
602            } => schema_diagnostic(
603                DiagnosticCategory::ResourceLimit,
604                "values_annotation_member_limit_exceeded",
605                "values annotation exceeds the raw member ceiling",
606            )
607            .with_detail(
608                "maximum_members",
609                i64::try_from(maximum).expect("collection limit fits i64"),
610            )
611            .with_detail(
612                "first_excess_index",
613                i64::try_from(first_excess_index).expect("collection index fits i64"),
614            ),
615            Self::MixedDomain {
616                expected,
617                actual,
618                member_index,
619            } => schema_diagnostic(
620                DiagnosticCategory::InvalidContract,
621                "mixed_values_annotation_domain",
622                "values annotations require one exact scalar domain",
623            )
624            .with_detail("expected_value_type", expected.as_str())
625            .with_detail("actual_value_type", actual.as_str())
626            .with_detail(
627                "member_index",
628                i64::try_from(member_index).expect("collection index fits i64"),
629            ),
630            Self::Duplicate {
631                first_index,
632                duplicate_index,
633            } => schema_diagnostic(
634                DiagnosticCategory::InvalidContract,
635                "duplicate_values_annotation_value",
636                "values annotations cannot contain duplicates",
637            )
638            .with_detail(
639                "first_index",
640                i64::try_from(first_index).expect("collection index fits i64"),
641            )
642            .with_detail(
643                "duplicate_index",
644                i64::try_from(duplicate_index).expect("collection index fits i64"),
645            ),
646        }
647    }
648}
649
650impl CanonicalValueSet {
651    /// Validate a set, rejecting empty, mixed-domain, and duplicate input.
652    pub fn new(values: impl IntoIterator<Item = CanonicalValue>) -> Result<Self, Diagnostic> {
653        Self::new_detailed(values).map_err(CanonicalValueSetViolation::into_diagnostic)
654    }
655
656    /// Validate a raw sequence while retaining member indices for source diagnostics.
657    pub fn new_detailed(
658        values: impl IntoIterator<Item = CanonicalValue>,
659    ) -> Result<Self, CanonicalValueSetViolation> {
660        let mut positions = BTreeMap::new();
661        let mut value_type = None;
662        for (member_index, value) in values.into_iter().enumerate() {
663            if member_index >= MAX_CANONICAL_COLLECTION_LEN {
664                return Err(CanonicalValueSetViolation::MemberLimitExceeded {
665                    maximum: MAX_CANONICAL_COLLECTION_LEN,
666                    first_excess_index: member_index,
667                });
668            }
669            if let Some(expected) = value_type {
670                if expected != value.value_type() {
671                    return Err(CanonicalValueSetViolation::MixedDomain {
672                        expected,
673                        actual: value.value_type(),
674                        member_index,
675                    });
676                }
677            } else {
678                value_type = Some(value.value_type());
679            }
680            if let Some(first_index) = positions.get(&value) {
681                return Err(CanonicalValueSetViolation::Duplicate {
682                    first_index: *first_index,
683                    duplicate_index: member_index,
684                });
685            }
686            positions.insert(value, member_index);
687        }
688        if positions.is_empty() {
689            return Err(CanonicalValueSetViolation::Empty);
690        }
691        Ok(Self(positions.into_keys().collect()))
692    }
693
694    /// Return values in canonical order.
695    pub fn iter(&self) -> impl ExactSizeIterator<Item = &CanonicalValue> {
696        self.0.iter()
697    }
698}
699
700/// An exact-domain, non-empty canonical value range.
701#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
702pub struct CanonicalValueRange {
703    lower: Option<CanonicalValue>,
704    upper: Option<CanonicalValue>,
705}
706
707/// Precise validation failure for canonical range bounds.
708#[derive(Debug, Clone, Copy, PartialEq, Eq)]
709pub enum CanonicalValueRangeViolation {
710    /// Neither bound was supplied.
711    Empty,
712    /// The two bounds used different exact scalar domains.
713    MixedDomain {
714        /// Lower-bound domain.
715        lower: ValueTypeTag,
716        /// Upper-bound domain.
717        upper: ValueTypeTag,
718    },
719    /// The scalar domain has no provider range ordering.
720    UnsupportedDomain {
721        /// Unsupported scalar domain.
722        value_type: ValueTypeTag,
723    },
724    /// The lower bound was equal to or greater than the upper bound.
725    InvalidBounds {
726        /// Semantic ordering of lower relative to upper.
727        ordering: Ordering,
728    },
729}
730
731impl CanonicalValueRangeViolation {
732    /// Convert to the stable compatibility diagnostic returned by `new`.
733    pub fn into_diagnostic(self) -> Diagnostic {
734        match self {
735            Self::Empty => schema_diagnostic(
736                DiagnosticCategory::InvalidContract,
737                "empty_range_annotation",
738                "range annotations require at least one bound",
739            ),
740            Self::MixedDomain { lower, upper } => schema_diagnostic(
741                DiagnosticCategory::InvalidContract,
742                "mixed_range_annotation_domain",
743                "range bounds require one exact scalar domain",
744            )
745            .with_detail("lower_value_type", lower.as_str())
746            .with_detail("upper_value_type", upper.as_str()),
747            Self::UnsupportedDomain { value_type } => schema_diagnostic(
748                DiagnosticCategory::InvalidContract,
749                "unsupported_range_annotation_domain",
750                "range annotations require an ordered scalar domain",
751            )
752            .with_detail("value_type", value_type.as_str()),
753            Self::InvalidBounds { ordering } => schema_diagnostic(
754                DiagnosticCategory::InvalidContract,
755                "invalid_range_annotation_bounds",
756                "range lower bounds must be strictly less than upper bounds",
757            )
758            .with_detail(
759                "ordering",
760                match ordering {
761                    Ordering::Less => "less",
762                    Ordering::Equal => "equal",
763                    Ordering::Greater => "greater",
764                },
765            ),
766        }
767    }
768}
769
770impl CanonicalValueRange {
771    /// Validate a range with at least one bound and one exact scalar domain.
772    pub fn new(
773        lower: Option<CanonicalValue>,
774        upper: Option<CanonicalValue>,
775    ) -> Result<Self, Diagnostic> {
776        Self::new_detailed(lower, upper).map_err(CanonicalValueRangeViolation::into_diagnostic)
777    }
778
779    /// Validate bounds while retaining the exact failure for source diagnostics.
780    pub fn new_detailed(
781        lower: Option<CanonicalValue>,
782        upper: Option<CanonicalValue>,
783    ) -> Result<Self, CanonicalValueRangeViolation> {
784        if lower.is_none() && upper.is_none() {
785            return Err(CanonicalValueRangeViolation::Empty);
786        }
787        if let (Some(lower), Some(upper)) = (&lower, &upper)
788            && lower.value_type() != upper.value_type()
789        {
790            return Err(CanonicalValueRangeViolation::MixedDomain {
791                lower: lower.value_type(),
792                upper: upper.value_type(),
793            });
794        }
795        let value_type = lower
796            .as_ref()
797            .or(upper.as_ref())
798            .expect("non-empty range has one bound")
799            .value_type();
800        if matches!(value_type, ValueTypeTag::Duration) {
801            return Err(CanonicalValueRangeViolation::UnsupportedDomain { value_type });
802        }
803        if let (Some(lower), Some(upper)) = (&lower, &upper) {
804            let ordering = lower
805                .semantic_cmp_same_domain(upper)
806                .expect("every supported exact domain has semantic ordering");
807            if ordering != Ordering::Less {
808                return Err(CanonicalValueRangeViolation::InvalidBounds { ordering });
809            }
810        }
811        Ok(Self { lower, upper })
812    }
813
814    /// Return the lower bound.
815    pub const fn lower(&self) -> Option<&CanonicalValue> {
816        self.lower.as_ref()
817    }
818
819    /// Return the upper bound.
820    pub const fn upper(&self) -> Option<&CanonicalValue> {
821        self.upper.as_ref()
822    }
823}
824
825/// A closed, kind-safe schema annotation payload.
826#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
827#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
828pub enum SchemaAnnotationValue {
829    /// A marker annotation with no payload.
830    Presence,
831    /// A cardinality payload.
832    Cardinality(Cardinality),
833    /// A regex source payload.
834    Regex(RegexPattern),
835    /// An exact canonical value range.
836    Range(CanonicalValueRange),
837    /// A non-empty canonical value set.
838    Values(CanonicalValueSet),
839    /// Documentation text.
840    Doc(DocText),
841    /// A typed metadata value.
842    Meta(CanonicalValue),
843}
844
845/// Existence of an entity, relation, or attribute type.
846#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
847pub struct TypeFact {
848    id: TypeId,
849}
850
851impl TypeFact {
852    /// Construct a type-existence fact.
853    pub fn new(id: TypeId) -> Result<Self, Diagnostic> {
854        if id.kind() == TypeKind::Struct {
855            return Err(schema_diagnostic(
856                DiagnosticCategory::InvalidContract,
857                "invalid_type_fact_kind",
858                "struct existence uses StructFact",
859            ));
860        }
861        if value_type_tag(id.label().as_str()).is_some() {
862            return Err(schema_diagnostic(
863                DiagnosticCategory::InvalidContract,
864                "reserved_schema_type_label",
865                "schema type labels cannot collide with built-in value-type tokens",
866            ));
867        }
868        Ok(Self { id })
869    }
870
871    /// Return the type identity.
872    pub const fn id(&self) -> &TypeId {
873        &self.id
874    }
875}
876
877/// A direct subtype fact.
878#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
879pub struct SubFact {
880    id: SubFactId,
881}
882
883impl SubFact {
884    /// Construct a subtype fact.
885    pub const fn new(id: SubFactId) -> Self {
886        Self { id }
887    }
888
889    /// Return the fact identity.
890    pub const fn id(&self) -> &SubFactId {
891        &self.id
892    }
893}
894
895/// An attribute scalar-domain fact.
896#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
897pub struct ValueFact {
898    id: ValueFactId,
899    value_type: ValueTypeTag,
900}
901
902impl ValueFact {
903    /// Construct an attribute scalar-domain fact.
904    pub const fn new(id: ValueFactId, value_type: ValueTypeTag) -> Self {
905        Self { id, value_type }
906    }
907
908    /// Return the fact identity.
909    pub const fn id(&self) -> &ValueFactId {
910        &self.id
911    }
912
913    /// Return the scalar domain.
914    pub const fn value_type(&self) -> ValueTypeTag {
915        self.value_type
916    }
917}
918
919/// A direct ownership fact.
920#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
921pub struct OwnsFact {
922    id: OwnsFactId,
923}
924
925impl OwnsFact {
926    /// Construct an ownership fact.
927    pub const fn new(id: OwnsFactId) -> Self {
928        Self { id }
929    }
930
931    /// Return the fact identity.
932    pub const fn id(&self) -> &OwnsFactId {
933        &self.id
934    }
935}
936
937/// A direct related-role fact, optionally specializing a parent role.
938#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
939pub struct RelatesFact {
940    id: RelatesFactId,
941    specializes: Option<RoleId>,
942}
943
944impl RelatesFact {
945    /// Construct a related-role fact.
946    pub fn new(id: RelatesFactId, specializes: Option<RoleId>) -> Result<Self, Diagnostic> {
947        if specializes.as_ref().is_some_and(|role| role == id.role()) {
948            return Err(schema_diagnostic(
949                DiagnosticCategory::InvalidContract,
950                "self_specializing_role",
951                "a role cannot specialize itself",
952            ));
953        }
954        Ok(Self { id, specializes })
955    }
956
957    /// Return the fact identity.
958    pub const fn id(&self) -> &RelatesFactId {
959        &self.id
960    }
961
962    /// Return the specialized parent role, if any.
963    pub const fn specializes(&self) -> Option<&RoleId> {
964        self.specializes.as_ref()
965    }
966}
967
968/// A direct role-playing fact.
969#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
970pub struct PlaysFact {
971    id: PlaysFactId,
972}
973
974impl PlaysFact {
975    /// Construct a role-playing fact.
976    pub const fn new(id: PlaysFactId) -> Self {
977        Self { id }
978    }
979
980    /// Return the fact identity.
981    pub const fn id(&self) -> &PlaysFactId {
982        &self.id
983    }
984}
985
986/// An independently identified schema annotation fact.
987#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
988pub struct AnnotationFact {
989    id: AnnotationFactId,
990    value: SchemaAnnotationValue,
991}
992
993impl AnnotationFact {
994    /// Construct and validate an annotation fact.
995    pub fn new(id: AnnotationFactId, value: SchemaAnnotationValue) -> Result<Self, Diagnostic> {
996        validate_annotation(id.subject(), id.kind(), &value)?;
997        Ok(Self { id, value })
998    }
999
1000    /// Return the annotation identity.
1001    pub const fn id(&self) -> &AnnotationFactId {
1002        &self.id
1003    }
1004
1005    /// Return the validated payload.
1006    pub const fn value(&self) -> &SchemaAnnotationValue {
1007        &self.value
1008    }
1009}
1010
1011/// A type reference used by a function signature.
1012#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1013#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
1014pub enum TypeReference {
1015    /// One of the closed built-in scalar value types.
1016    Value(ValueTypeTag),
1017    /// A schema type or struct label resolved against the declared graph.
1018    Schema(Label),
1019}
1020
1021impl TypeReference {
1022    /// Parse the unambiguous TypeQL type-position spelling.
1023    pub fn from_token(value: impl Into<String>) -> Result<Self, Diagnostic> {
1024        let value = value.into();
1025        Ok(value_type_tag(&value)
1026            .map(Self::Value)
1027            .unwrap_or(Self::Schema(Label::new(value)?)))
1028    }
1029
1030    /// Return the referenced schema label, if this is not a built-in value type.
1031    pub const fn schema_label(&self) -> Option<&Label> {
1032        match self {
1033            Self::Value(_) => None,
1034            Self::Schema(label) => Some(label),
1035        }
1036    }
1037}
1038
1039/// One ordered function parameter.
1040#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1041pub struct FunctionParameter {
1042    name: Label,
1043    type_ref: TypeReference,
1044}
1045
1046impl FunctionParameter {
1047    /// Construct a parameter from validated contract values.
1048    pub const fn new(name: Label, type_ref: TypeReference) -> Self {
1049        Self { name, type_ref }
1050    }
1051
1052    /// Return the parameter name without a provider variable sigil.
1053    pub const fn name(&self) -> &Label {
1054        &self.name
1055    }
1056
1057    /// Return the parameter type reference.
1058    pub const fn type_ref(&self) -> &TypeReference {
1059        &self.type_ref
1060    }
1061}
1062
1063/// One ordered element in a function return signature.
1064#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1065pub struct FunctionReturnElement {
1066    type_ref: TypeReference,
1067    optional: bool,
1068}
1069
1070impl FunctionReturnElement {
1071    /// Construct one return element.
1072    pub const fn new(type_ref: TypeReference, optional: bool) -> Self {
1073        Self { type_ref, optional }
1074    }
1075
1076    /// Return the element type reference.
1077    pub const fn type_ref(&self) -> &TypeReference {
1078        &self.type_ref
1079    }
1080
1081    /// Report whether this element may be absent.
1082    pub const fn optional(&self) -> bool {
1083        self.optional
1084    }
1085}
1086
1087/// Native function return cardinality and ordered shape.
1088#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1089#[serde(tag = "kind", content = "elements", rename_all = "snake_case")]
1090pub enum FunctionReturnMode {
1091    /// At most one row containing one element.
1092    Scalar(FunctionReturnElement),
1093    /// At most one row containing two or more ordered elements.
1094    Tuple(Vec<FunctionReturnElement>),
1095    /// Any number of rows containing one or more ordered elements.
1096    Stream(Vec<FunctionReturnElement>),
1097}
1098
1099impl FunctionReturnMode {
1100    /// Construct a scalar return.
1101    pub const fn scalar(element: FunctionReturnElement) -> Self {
1102        Self::Scalar(element)
1103    }
1104
1105    /// Construct a non-empty tuple return with at least two elements.
1106    pub fn tuple(elements: Vec<FunctionReturnElement>) -> Result<Self, Diagnostic> {
1107        if !(2..=MAX_CANONICAL_COLLECTION_LEN).contains(&elements.len()) {
1108            return Err(schema_diagnostic(
1109                DiagnosticCategory::InvalidContract,
1110                "invalid_function_tuple_return",
1111                "tuple function returns require between two and the collection limit elements",
1112            ));
1113        }
1114        Ok(Self::Tuple(elements))
1115    }
1116
1117    /// Construct a non-empty stream return.
1118    pub fn stream(elements: Vec<FunctionReturnElement>) -> Result<Self, Diagnostic> {
1119        if elements.is_empty() || elements.len() > MAX_CANONICAL_COLLECTION_LEN {
1120            return Err(schema_diagnostic(
1121                DiagnosticCategory::InvalidContract,
1122                "invalid_function_stream_return",
1123                "stream function returns require a non-empty bounded element list",
1124            ));
1125        }
1126        Ok(Self::Stream(elements))
1127    }
1128
1129    /// Return elements in semantic signature order.
1130    pub fn elements(&self) -> &[FunctionReturnElement] {
1131        match self {
1132            Self::Scalar(element) => std::slice::from_ref(element),
1133            Self::Tuple(elements) | Self::Stream(elements) => elements,
1134        }
1135    }
1136}
1137
1138/// A validated ordered function signature.
1139#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1140pub struct FunctionSignature {
1141    parameters: Vec<FunctionParameter>,
1142    returns: FunctionReturnMode,
1143}
1144
1145impl FunctionSignature {
1146    /// Construct a signature with unique, bounded ordered parameters.
1147    pub fn new(
1148        parameters: Vec<FunctionParameter>,
1149        returns: FunctionReturnMode,
1150    ) -> Result<Self, Diagnostic> {
1151        if parameters.len() > MAX_CANONICAL_COLLECTION_LEN {
1152            return Err(schema_diagnostic(
1153                DiagnosticCategory::ResourceLimit,
1154                "too_many_function_parameters",
1155                "function parameter count exceeds the canonical collection limit",
1156            ));
1157        }
1158        let mut names = BTreeSet::new();
1159        if parameters
1160            .iter()
1161            .any(|parameter| !names.insert(parameter.name().clone()))
1162        {
1163            return Err(schema_diagnostic(
1164                DiagnosticCategory::InvalidContract,
1165                "duplicate_function_parameter",
1166                "function parameter names must be unique",
1167            ));
1168        }
1169        Ok(Self {
1170            parameters,
1171            returns,
1172        })
1173    }
1174
1175    /// Return parameters in semantic declaration order.
1176    pub fn parameters(&self) -> &[FunctionParameter] {
1177        &self.parameters
1178    }
1179
1180    /// Return the native return shape.
1181    pub const fn returns(&self) -> &FunctionReturnMode {
1182        &self.returns
1183    }
1184}
1185
1186/// Decoded provider-native function body text retained verbatim.
1187#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1188#[serde(transparent)]
1189pub struct FunctionBody(String);
1190
1191impl FunctionBody {
1192    /// Construct a non-empty bounded body without trimming or rewriting it.
1193    pub fn new(text: impl Into<String>) -> Result<Self, Diagnostic> {
1194        let text = text.into();
1195        if text.is_empty() || text.len() > MAX_CANONICAL_STRING_BYTES {
1196            return Err(schema_diagnostic(
1197                DiagnosticCategory::InvalidContract,
1198                "invalid_function_body",
1199                "decoded function body must be non-empty and bounded",
1200            ));
1201        }
1202        Ok(Self(text))
1203    }
1204
1205    /// Return exact decoded body text, including comments and trailing newline.
1206    pub fn text(&self) -> &str {
1207        &self.0
1208    }
1209}
1210
1211/// A function declaration with structured signature and decoded body text.
1212#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1213pub struct FunctionFact {
1214    id: FunctionId,
1215    signature: FunctionSignature,
1216    body: FunctionBody,
1217}
1218
1219impl FunctionFact {
1220    /// Construct a validated function declaration.
1221    pub const fn new(id: FunctionId, signature: FunctionSignature, body: FunctionBody) -> Self {
1222        Self {
1223            id,
1224            signature,
1225            body,
1226        }
1227    }
1228
1229    /// Return the function identity.
1230    pub const fn id(&self) -> &FunctionId {
1231        &self.id
1232    }
1233
1234    /// Return the structured signature.
1235    pub const fn signature(&self) -> &FunctionSignature {
1236        &self.signature
1237    }
1238
1239    /// Return exact decoded provider body text.
1240    pub const fn body(&self) -> &FunctionBody {
1241        &self.body
1242    }
1243
1244    /// Iterate schema labels referenced by the signature.
1245    pub fn schema_references(&self) -> impl Iterator<Item = &Label> {
1246        self.signature
1247            .parameters()
1248            .iter()
1249            .filter_map(|parameter| parameter.type_ref().schema_label())
1250            .chain(
1251                self.signature
1252                    .returns()
1253                    .elements()
1254                    .iter()
1255                    .filter_map(|element| element.type_ref().schema_label()),
1256            )
1257    }
1258}
1259
1260/// One ordered field in a struct declaration.
1261#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1262pub struct StructField {
1263    name: Label,
1264    value_type: ValueTypeTag,
1265    optional: bool,
1266}
1267
1268impl StructField {
1269    /// Construct a field from validated contract values.
1270    pub const fn new(name: Label, value_type: ValueTypeTag, optional: bool) -> Self {
1271        Self {
1272            name,
1273            value_type,
1274            optional,
1275        }
1276    }
1277
1278    /// Return the field name.
1279    pub const fn name(&self) -> &Label {
1280        &self.name
1281    }
1282
1283    /// Return the built-in field value type.
1284    pub const fn value_type(&self) -> ValueTypeTag {
1285        self.value_type
1286    }
1287
1288    /// Report whether the field may be absent.
1289    pub const fn optional(&self) -> bool {
1290        self.optional
1291    }
1292}
1293
1294/// A named struct declaration with ordered, non-empty built-in fields.
1295#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1296pub struct StructFact {
1297    id: StructId,
1298    fields: Vec<StructField>,
1299}
1300
1301impl StructFact {
1302    /// Construct and validate a field-bearing struct declaration.
1303    pub fn new(id: StructId, fields: Vec<StructField>) -> Result<Self, Diagnostic> {
1304        if value_type_tag(id.label().as_str()).is_some() {
1305            return Err(schema_diagnostic(
1306                DiagnosticCategory::InvalidContract,
1307                "reserved_schema_type_label",
1308                "struct labels cannot collide with built-in value-type tokens",
1309            ));
1310        }
1311        if fields.is_empty() {
1312            return Err(schema_diagnostic(
1313                DiagnosticCategory::InvalidContract,
1314                "empty_struct_fields",
1315                "struct declarations require at least one field",
1316            ));
1317        }
1318        if fields.len() > MAX_CANONICAL_COLLECTION_LEN {
1319            return Err(schema_diagnostic(
1320                DiagnosticCategory::ResourceLimit,
1321                "too_many_struct_fields",
1322                "struct field count exceeds the canonical collection limit",
1323            ));
1324        }
1325
1326        let mut names = BTreeSet::new();
1327        for field in &fields {
1328            if !names.insert(field.name().clone()) {
1329                return Err(schema_diagnostic(
1330                    DiagnosticCategory::InvalidContract,
1331                    "duplicate_struct_field",
1332                    "struct field names must be unique within the struct",
1333                ));
1334            }
1335        }
1336
1337        Ok(Self { id, fields })
1338    }
1339
1340    /// Return the struct identity.
1341    pub const fn id(&self) -> &StructId {
1342        &self.id
1343    }
1344
1345    /// Return fields in their declared semantic order.
1346    pub fn fields(&self) -> &[StructField] {
1347        &self.fields
1348    }
1349}
1350
1351/// Stable identity of any Phase 2 schema fact.
1352#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
1353#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
1354pub enum SchemaFactId {
1355    /// Type existence.
1356    Type(TypeId),
1357    /// Direct subtype edge.
1358    Sub(SubFactId),
1359    /// Attribute scalar domain.
1360    Value(ValueFactId),
1361    /// Direct ownership.
1362    Owns(OwnsFactId),
1363    /// Direct related role.
1364    Relates(RelatesFactId),
1365    /// Direct role playing.
1366    Plays(PlaysFactId),
1367    /// Independent annotation.
1368    Annotation(AnnotationFactId),
1369    /// Function declaration.
1370    Function(FunctionId),
1371    /// Struct declaration.
1372    Struct(StructId),
1373}
1374
1375/// A validated, atomic direct schema fact.
1376#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1377#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
1378pub enum SchemaFact {
1379    /// Type existence.
1380    Type(TypeFact),
1381    /// Direct subtype edge.
1382    Sub(SubFact),
1383    /// Attribute scalar domain.
1384    Value(ValueFact),
1385    /// Direct ownership.
1386    Owns(OwnsFact),
1387    /// Direct related role.
1388    Relates(RelatesFact),
1389    /// Direct role playing.
1390    Plays(PlaysFact),
1391    /// Independent annotation.
1392    Annotation(AnnotationFact),
1393    /// Function declaration.
1394    Function(FunctionFact),
1395    /// Struct declaration.
1396    Struct(StructFact),
1397}
1398
1399impl SchemaFact {
1400    /// Return the stable structural identity.
1401    pub fn id(&self) -> SchemaFactId {
1402        match self {
1403            Self::Type(fact) => SchemaFactId::Type(fact.id().clone()),
1404            Self::Sub(fact) => SchemaFactId::Sub(fact.id().clone()),
1405            Self::Value(fact) => SchemaFactId::Value(fact.id().clone()),
1406            Self::Owns(fact) => SchemaFactId::Owns(fact.id().clone()),
1407            Self::Relates(fact) => SchemaFactId::Relates(fact.id().clone()),
1408            Self::Plays(fact) => SchemaFactId::Plays(fact.id().clone()),
1409            Self::Annotation(fact) => SchemaFactId::Annotation(fact.id().clone()),
1410            Self::Function(fact) => SchemaFactId::Function(fact.id().clone()),
1411            Self::Struct(fact) => SchemaFactId::Struct(fact.id().clone()),
1412        }
1413    }
1414}
1415
1416/// A direct fact paired with the one source span that owns it.
1417#[derive(Debug, Clone, PartialEq, Eq)]
1418pub struct SourcedSchemaFact {
1419    fact: SchemaFact,
1420    source: SourceSpan,
1421}
1422
1423impl SourcedSchemaFact {
1424    /// Pair a validated fact with its direct source.
1425    pub const fn new(fact: SchemaFact, source: SourceSpan) -> Self {
1426        Self { fact, source }
1427    }
1428
1429    /// Return the fact.
1430    pub const fn fact(&self) -> &SchemaFact {
1431        &self.fact
1432    }
1433
1434    /// Return the owning source span.
1435    pub const fn source(&self) -> &SourceSpan {
1436        &self.source
1437    }
1438}
1439
1440/// A domain-safe source-document fingerprint.
1441#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1442#[serde(transparent)]
1443pub struct DocumentFingerprint(Fingerprint);
1444
1445impl DocumentFingerprint {
1446    /// Fingerprint exact source bytes, including comments and spelling.
1447    pub fn compute(source: &[u8]) -> Result<Self, Diagnostic> {
1448        Ok(Self(Fingerprint::compute(
1449            FingerprintDomain::new("typebridge.schema.document")?,
1450            CanonicalizationVersion::new("typebridge.raw-utf8/v1")?,
1451            None,
1452            source,
1453        )))
1454    }
1455
1456    /// Return the generic fingerprint metadata.
1457    pub const fn as_fingerprint(&self) -> &Fingerprint {
1458        &self.0
1459    }
1460}
1461
1462/// A domain-safe fingerprint of direct fact identity and meaning.
1463#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1464#[serde(transparent)]
1465pub struct DeclaredIdentityFingerprint(Fingerprint);
1466
1467impl DeclaredIdentityFingerprint {
1468    fn compute(canonical_bytes: &[u8]) -> Result<Self, Diagnostic> {
1469        Ok(Self(Fingerprint::compute(
1470            FingerprintDomain::new("typebridge.schema.declared-identity")?,
1471            CanonicalizationVersion::new("typebridge.schema-canonical-json/v1")?,
1472            None,
1473            canonical_bytes,
1474        )))
1475    }
1476
1477    /// Return the generic fingerprint metadata.
1478    pub const fn as_fingerprint(&self) -> &Fingerprint {
1479        &self.0
1480    }
1481
1482    pub(crate) fn from_wire(fingerprint: Fingerprint) -> Result<Self, Diagnostic> {
1483        if fingerprint.domain().as_str() != "typebridge.schema.declared-identity"
1484            || fingerprint.canonicalization().as_str() != "typebridge.schema-canonical-json/v1"
1485            || fingerprint.semantic_profile().is_some()
1486        {
1487            return Err(Diagnostic::stable(
1488                DiagnosticCategory::Integrity,
1489                "invalid_declared_identity_fingerprint",
1490                "declared identity fingerprint metadata is invalid",
1491            ));
1492        }
1493        Ok(Self(fingerprint))
1494    }
1495}
1496
1497/// A validated normalized graph of direct schema facts.
1498#[derive(Debug, Clone, PartialEq, Eq)]
1499pub struct DeclaredSchema {
1500    format: FormatVersion,
1501    required_capabilities: CapabilitySet,
1502    facts: BTreeMap<SchemaFactId, SchemaFact>,
1503    provenance: BTreeMap<SchemaFactId, SourceSpan>,
1504    fingerprint: DeclaredIdentityFingerprint,
1505}
1506
1507impl Serialize for DeclaredSchema {
1508    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1509    where
1510        S: Serializer,
1511    {
1512        #[derive(Serialize)]
1513        struct TrustedDeclaredSchemaView<'a> {
1514            declared_identity: &'a DeclaredIdentityFingerprint,
1515            facts: Vec<&'a SchemaFact>,
1516            format_version: FormatVersion,
1517            required_capabilities: &'a CapabilitySet,
1518        }
1519
1520        TrustedDeclaredSchemaView {
1521            declared_identity: &self.fingerprint,
1522            facts: self.facts.values().collect(),
1523            format_version: self.format,
1524            required_capabilities: &self.required_capabilities,
1525        }
1526        .serialize(serializer)
1527    }
1528}
1529
1530impl DeclaredSchema {
1531    /// Validate direct fact ownership, references, and annotation combinations.
1532    pub fn from_facts(
1533        format: FormatVersion,
1534        required_capabilities: CapabilitySet,
1535        sourced_facts: impl IntoIterator<Item = SourcedSchemaFact>,
1536    ) -> Result<Self, SchemaDiagnostics> {
1537        ensure_format_version(format, FormatVersion::V1)
1538            .map_err(|error| SchemaDiagnostics::one(SchemaDiagnostic::new(error, None)))?;
1539
1540        let mut facts = BTreeMap::new();
1541        let mut provenance = BTreeMap::<SchemaFactId, SourceSpan>::new();
1542        let mut diagnostics = Vec::new();
1543        for sourced in sourced_facts {
1544            let id = sourced.fact.id();
1545            if let Some(previous) = provenance.get(&id) {
1546                diagnostics.push(
1547                    SchemaDiagnostic::new(
1548                        schema_diagnostic(
1549                            DiagnosticCategory::InvalidContract,
1550                            "duplicate_schema_fact",
1551                            "a direct schema fact is declared more than once",
1552                        ),
1553                        Some(sourced.source.clone()),
1554                    )
1555                    .with_related(DiagnosticLabel::new(
1556                        previous.clone(),
1557                        "first declaration is here",
1558                    )),
1559                );
1560                continue;
1561            }
1562            provenance.insert(id.clone(), sourced.source);
1563            facts.insert(id, sourced.fact);
1564        }
1565
1566        if diagnostics.is_empty() {
1567            validate_references(&facts, &provenance, &mut diagnostics);
1568            validate_annotation_combinations(&facts, &provenance, &mut diagnostics);
1569            validate_annotation_value_domains(&facts, &provenance, &mut diagnostics);
1570        }
1571        if !diagnostics.is_empty() {
1572            return Err(SchemaDiagnostics::from_vec(diagnostics));
1573        }
1574
1575        let canonical =
1576            canonical_declared_identity_bytes(format, &required_capabilities, &facts)
1577                .map_err(|error| SchemaDiagnostics::one(SchemaDiagnostic::new(error, None)))?;
1578        let fingerprint = DeclaredIdentityFingerprint::compute(&canonical)
1579            .map_err(|error| SchemaDiagnostics::one(SchemaDiagnostic::new(error, None)))?;
1580        Ok(Self {
1581            format,
1582            required_capabilities,
1583            facts,
1584            provenance,
1585            fingerprint,
1586        })
1587    }
1588
1589    /// Return the schema format version.
1590    pub const fn format(&self) -> FormatVersion {
1591        self.format
1592    }
1593
1594    /// Return the required open capability set.
1595    pub const fn required_capabilities(&self) -> &CapabilitySet {
1596        &self.required_capabilities
1597    }
1598
1599    /// Return a fact by stable identity.
1600    pub fn fact(&self, id: &SchemaFactId) -> Option<&SchemaFact> {
1601        self.facts.get(id)
1602    }
1603
1604    /// Iterate facts in stable identity order.
1605    pub fn facts(&self) -> impl ExactSizeIterator<Item = &SchemaFact> {
1606        self.facts.values()
1607    }
1608
1609    /// Return the direct source owner of a fact.
1610    pub fn source(&self, id: &SchemaFactId) -> Option<&SourceSpan> {
1611        self.provenance.get(id)
1612    }
1613
1614    /// Return canonical identity bytes with presentation provenance excluded.
1615    pub fn canonical_identity_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
1616        canonical_declared_identity_bytes(self.format, &self.required_capabilities, &self.facts)
1617    }
1618
1619    /// Return the declared identity fingerprint.
1620    pub const fn declared_identity_fingerprint(&self) -> &DeclaredIdentityFingerprint {
1621        &self.fingerprint
1622    }
1623}
1624
1625/// Encode only a constructor-validated declared schema as canonical JSON.
1626pub fn encode_declared_schema(schema: &DeclaredSchema) -> Result<Vec<u8>, Diagnostic> {
1627    crate::declared_schema_wire::encode_declared_schema(schema)
1628}
1629
1630/// Decode canonical bytes through private wire DTOs and every fact/schema constructor.
1631pub fn decode_declared_schema(bytes: &[u8]) -> Result<DeclaredSchema, Diagnostic> {
1632    crate::declared_schema_wire::decode_declared_schema(bytes)
1633}
1634
1635#[derive(Serialize)]
1636struct DeclaredIdentityView<'a> {
1637    format_version: FormatVersion,
1638    required_capabilities: &'a CapabilitySet,
1639    facts: Vec<&'a SchemaFact>,
1640}
1641
1642fn canonical_declared_identity_bytes(
1643    format: FormatVersion,
1644    required_capabilities: &CapabilitySet,
1645    facts: &BTreeMap<SchemaFactId, SchemaFact>,
1646) -> Result<Vec<u8>, Diagnostic> {
1647    to_canonical_json(&DeclaredIdentityView {
1648        format_version: format,
1649        required_capabilities,
1650        facts: facts.values().collect(),
1651    })
1652}
1653
1654fn validate_annotation(
1655    subject: &AnnotationSubjectId,
1656    kind: &AnnotationKindId,
1657    value: &SchemaAnnotationValue,
1658) -> Result<(), Diagnostic> {
1659    let payload_matches = matches!(
1660        (kind, value),
1661        (
1662            AnnotationKindId::Abstract
1663                | AnnotationKindId::Independent
1664                | AnnotationKindId::Key
1665                | AnnotationKindId::Unique,
1666            SchemaAnnotationValue::Presence
1667        ) | (
1668            AnnotationKindId::Card,
1669            SchemaAnnotationValue::Cardinality(_)
1670        ) | (AnnotationKindId::Regex, SchemaAnnotationValue::Regex(_))
1671            | (AnnotationKindId::Range, SchemaAnnotationValue::Range(_))
1672            | (AnnotationKindId::Values, SchemaAnnotationValue::Values(_))
1673            | (AnnotationKindId::Doc, SchemaAnnotationValue::Doc(_))
1674            | (
1675                AnnotationKindId::Meta(_),
1676                SchemaAnnotationValue::Meta(CanonicalValue::String(_))
1677            )
1678    );
1679    if !payload_matches {
1680        return Err(schema_diagnostic(
1681            DiagnosticCategory::InvalidContract,
1682            "invalid_annotation_payload",
1683            "annotation kind and payload do not agree",
1684        ));
1685    }
1686    let subject_matches = match kind {
1687        AnnotationKindId::Abstract => match subject {
1688            AnnotationSubjectId::Type(id) => matches!(
1689                id.kind(),
1690                TypeKind::Entity | TypeKind::Relation | TypeKind::Attribute
1691            ),
1692            AnnotationSubjectId::Relates(_) => true,
1693            AnnotationSubjectId::Sub(_)
1694            | AnnotationSubjectId::Value(_)
1695            | AnnotationSubjectId::Owns(_)
1696            | AnnotationSubjectId::Plays(_)
1697            | AnnotationSubjectId::Function(_) => false,
1698        },
1699        AnnotationKindId::Independent => matches!(
1700            subject,
1701            AnnotationSubjectId::Type(id) if id.kind() == TypeKind::Attribute
1702        ),
1703        AnnotationKindId::Key | AnnotationKindId::Unique => {
1704            matches!(subject, AnnotationSubjectId::Owns(_))
1705        }
1706        AnnotationKindId::Card => matches!(
1707            subject,
1708            AnnotationSubjectId::Owns(_)
1709                | AnnotationSubjectId::Relates(_)
1710                | AnnotationSubjectId::Plays(_)
1711        ),
1712        AnnotationKindId::Regex | AnnotationKindId::Range | AnnotationKindId::Values => {
1713            matches!(
1714                subject,
1715                AnnotationSubjectId::Value(_) | AnnotationSubjectId::Owns(_)
1716            )
1717        }
1718        AnnotationKindId::Doc | AnnotationKindId::Meta(_) => matches!(
1719            subject,
1720            AnnotationSubjectId::Type(_)
1721                | AnnotationSubjectId::Sub(_)
1722                | AnnotationSubjectId::Owns(_)
1723                | AnnotationSubjectId::Relates(_)
1724                | AnnotationSubjectId::Plays(_)
1725                | AnnotationSubjectId::Function(_)
1726        ),
1727    };
1728    if !subject_matches {
1729        return Err(schema_diagnostic(
1730            DiagnosticCategory::InvalidContract,
1731            "invalid_annotation_subject",
1732            "annotation kind does not apply to this schema subject",
1733        ));
1734    }
1735    Ok(())
1736}
1737
1738fn validate_annotation_value_domains(
1739    facts: &BTreeMap<SchemaFactId, SchemaFact>,
1740    provenance: &BTreeMap<SchemaFactId, SourceSpan>,
1741    diagnostics: &mut Vec<SchemaDiagnostic>,
1742) {
1743    for (fact_id, fact) in facts {
1744        let SchemaFact::Annotation(annotation) = fact else {
1745            continue;
1746        };
1747
1748        let kind = annotation.id().kind();
1749        if !matches!(
1750            kind,
1751            AnnotationKindId::Key
1752                | AnnotationKindId::Unique
1753                | AnnotationKindId::Regex
1754                | AnnotationKindId::Range
1755                | AnnotationKindId::Values
1756        ) {
1757            continue;
1758        }
1759
1760        let Some((value_type, value_fact_id)) =
1761            annotation_subject_value_type(annotation.id().subject(), facts)
1762        else {
1763            diagnostics.push(SchemaDiagnostic::new(
1764                schema_diagnostic(
1765                    DiagnosticCategory::InvalidContract,
1766                    "unknown_annotation_value_domain",
1767                    "annotation subject has no resolvable attribute value domain",
1768                ),
1769                provenance.get(fact_id).cloned(),
1770            ));
1771            continue;
1772        };
1773
1774        let valid = match (kind, annotation.value()) {
1775            (AnnotationKindId::Key | AnnotationKindId::Unique, _) => {
1776                value_type != ValueTypeTag::Double
1777            }
1778            (AnnotationKindId::Regex, SchemaAnnotationValue::Regex(_)) => {
1779                value_type == ValueTypeTag::String
1780            }
1781            (AnnotationKindId::Range, SchemaAnnotationValue::Range(range)) => {
1782                value_type != ValueTypeTag::Duration
1783                    && range
1784                        .lower()
1785                        .into_iter()
1786                        .chain(range.upper())
1787                        .all(|bound| bound.value_type() == value_type)
1788            }
1789            (AnnotationKindId::Values, SchemaAnnotationValue::Values(values)) => {
1790                values.iter().all(|value| value.value_type() == value_type)
1791            }
1792            _ => false,
1793        };
1794
1795        if !valid {
1796            let mut diagnostic = SchemaDiagnostic::new(
1797                schema_diagnostic(
1798                    DiagnosticCategory::InvalidContract,
1799                    "invalid_annotation_value_domain",
1800                    "annotation payload is incompatible with the attribute value domain",
1801                ),
1802                provenance.get(fact_id).cloned(),
1803            );
1804            if let Some(value_source) = provenance.get(&value_fact_id) {
1805                diagnostic = diagnostic.with_related(DiagnosticLabel::new(
1806                    value_source.clone(),
1807                    "attribute value domain is declared here",
1808                ));
1809            }
1810            diagnostics.push(diagnostic);
1811        }
1812    }
1813}
1814
1815fn annotation_subject_value_type(
1816    subject: &AnnotationSubjectId,
1817    facts: &BTreeMap<SchemaFactId, SchemaFact>,
1818) -> Option<(ValueTypeTag, SchemaFactId)> {
1819    let mut attribute = match subject {
1820        AnnotationSubjectId::Value(id) => id.attribute().clone(),
1821        AnnotationSubjectId::Owns(id) => id.attribute().clone(),
1822        AnnotationSubjectId::Type(_)
1823        | AnnotationSubjectId::Sub(_)
1824        | AnnotationSubjectId::Relates(_)
1825        | AnnotationSubjectId::Plays(_)
1826        | AnnotationSubjectId::Function(_) => return None,
1827    };
1828    let mut visited = BTreeSet::new();
1829
1830    loop {
1831        let attribute_type = TypeId::new(TypeKind::Attribute, attribute.label().as_str()).ok()?;
1832        if !visited.insert(attribute_type.clone()) {
1833            return None;
1834        }
1835
1836        let value_fact_id = ValueFactId::new(attribute.clone());
1837        let schema_fact_id = SchemaFactId::Value(value_fact_id);
1838        if let Some(SchemaFact::Value(value)) = facts.get(&schema_fact_id) {
1839            return Some((value.value_type(), schema_fact_id));
1840        }
1841
1842        let supertype = facts.values().find_map(|fact| {
1843            let SchemaFact::Sub(sub) = fact else {
1844                return None;
1845            };
1846            (sub.id().subtype() == &attribute_type
1847                && sub.id().supertype().kind() == TypeKind::Attribute)
1848                .then(|| sub.id().supertype().clone())
1849        })?;
1850        attribute = AttributeId::new(supertype.label().as_str()).ok()?;
1851    }
1852}
1853
1854fn validate_references(
1855    facts: &BTreeMap<SchemaFactId, SchemaFact>,
1856    provenance: &BTreeMap<SchemaFactId, SourceSpan>,
1857    diagnostics: &mut Vec<SchemaDiagnostic>,
1858) {
1859    let type_ids = facts
1860        .keys()
1861        .filter_map(|id| match id {
1862            SchemaFactId::Type(id) => Some(id.clone()),
1863            _ => None,
1864        })
1865        .collect::<BTreeSet<_>>();
1866    let role_ids = facts
1867        .keys()
1868        .filter_map(|id| match id {
1869            SchemaFactId::Relates(id) => Some(id.role().clone()),
1870            _ => None,
1871        })
1872        .collect::<BTreeSet<_>>();
1873    let struct_labels = facts
1874        .keys()
1875        .filter_map(|id| match id {
1876            SchemaFactId::Struct(id) => Some(id.label().clone()),
1877            _ => None,
1878        })
1879        .collect::<BTreeSet<_>>();
1880
1881    for (id, fact) in facts {
1882        let valid = match fact {
1883            SchemaFact::Type(_) | SchemaFact::Struct(_) => true,
1884            SchemaFact::Function(fact) => fact.schema_references().all(|label| {
1885                type_ids.iter().any(|id| id.label() == label) || struct_labels.contains(label)
1886            }),
1887            SchemaFact::Sub(fact) => {
1888                type_ids.contains(fact.id().subtype()) && type_ids.contains(fact.id().supertype())
1889            }
1890            SchemaFact::Value(fact) => type_ids.contains(&attribute_type_id(fact.id().attribute())),
1891            SchemaFact::Owns(fact) => {
1892                type_ids.contains(fact.id().owner())
1893                    && type_ids.contains(&attribute_type_id(fact.id().attribute()))
1894            }
1895            SchemaFact::Relates(fact) => {
1896                type_ids.contains(fact.id().relation())
1897                    && fact
1898                        .specializes()
1899                        .is_none_or(|role| role_ids.contains(role))
1900            }
1901            SchemaFact::Plays(fact) => {
1902                type_ids.contains(fact.id().player()) && role_ids.contains(fact.id().role())
1903            }
1904            SchemaFact::Annotation(fact) => {
1905                facts.contains_key(&subject_fact_id(fact.id().subject()))
1906            }
1907        };
1908        if !valid {
1909            diagnostics.push(SchemaDiagnostic::new(
1910                schema_diagnostic(
1911                    DiagnosticCategory::InvalidContract,
1912                    "unknown_schema_fact_reference",
1913                    "schema fact references a declaration that does not exist",
1914                ),
1915                provenance.get(id).cloned(),
1916            ));
1917        }
1918    }
1919}
1920
1921fn validate_annotation_combinations(
1922    facts: &BTreeMap<SchemaFactId, SchemaFact>,
1923    provenance: &BTreeMap<SchemaFactId, SourceSpan>,
1924    diagnostics: &mut Vec<SchemaDiagnostic>,
1925) {
1926    let mut by_subject = BTreeMap::<AnnotationSubjectId, BTreeSet<AnnotationKindId>>::new();
1927    for fact in facts.values() {
1928        if let SchemaFact::Annotation(annotation) = fact {
1929            by_subject
1930                .entry(annotation.id().subject().clone())
1931                .or_default()
1932                .insert(annotation.id().kind().clone());
1933        }
1934    }
1935    for (subject, kinds) in by_subject {
1936        if kinds.contains(&AnnotationKindId::Key)
1937            && (kinds.contains(&AnnotationKindId::Unique)
1938                || kinds.contains(&AnnotationKindId::Card))
1939        {
1940            let key_id =
1941                SchemaFactId::Annotation(AnnotationFactId::new(subject, AnnotationKindId::Key));
1942            diagnostics.push(SchemaDiagnostic::new(
1943                schema_diagnostic(
1944                    DiagnosticCategory::InvalidContract,
1945                    "key_annotation_conflict",
1946                    "key cannot be combined with unique or cardinality",
1947                ),
1948                provenance.get(&key_id).cloned(),
1949            ));
1950        }
1951    }
1952}
1953
1954fn attribute_type_id(attribute: &AttributeId) -> TypeId {
1955    TypeId::new(TypeKind::Attribute, attribute.label().as_str())
1956        .expect("validated attribute labels always form attribute type identities")
1957}
1958
1959fn subject_fact_id(subject: &AnnotationSubjectId) -> SchemaFactId {
1960    match subject {
1961        AnnotationSubjectId::Type(id) => SchemaFactId::Type(id.clone()),
1962        AnnotationSubjectId::Sub(id) => SchemaFactId::Sub(id.clone()),
1963        AnnotationSubjectId::Value(id) => SchemaFactId::Value(id.clone()),
1964        AnnotationSubjectId::Owns(id) => SchemaFactId::Owns(id.clone()),
1965        AnnotationSubjectId::Relates(id) => SchemaFactId::Relates(id.clone()),
1966        AnnotationSubjectId::Plays(id) => SchemaFactId::Plays(id.clone()),
1967        AnnotationSubjectId::Function(id) => SchemaFactId::Function(id.clone()),
1968    }
1969}
1970
1971fn value_type_tag(value: &str) -> Option<ValueTypeTag> {
1972    match value {
1973        "string" => Some(ValueTypeTag::String),
1974        "integer" => Some(ValueTypeTag::Long),
1975        "double" => Some(ValueTypeTag::Double),
1976        "boolean" => Some(ValueTypeTag::Boolean),
1977        "date" => Some(ValueTypeTag::Date),
1978        "datetime" => Some(ValueTypeTag::DateTime),
1979        "datetime-tz" => Some(ValueTypeTag::DateTimeTz),
1980        "decimal" => Some(ValueTypeTag::Decimal),
1981        "duration" => Some(ValueTypeTag::Duration),
1982        _ => None,
1983    }
1984}
1985
1986fn schema_diagnostic(
1987    category: DiagnosticCategory,
1988    code: &'static str,
1989    message: &'static str,
1990) -> Diagnostic {
1991    Diagnostic::stable(category, code, message)
1992}