Skip to main content

type_bridge_schema/
normalize.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use type_bridge_contract::capability::{CapabilityId, CapabilitySet};
4use type_bridge_contract::codec::FormatVersion;
5use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory};
6use type_bridge_contract::id::{
7    AttributeId, FunctionId, Label, RoleId, StructId, TypeId, TypeKind,
8};
9use type_bridge_contract::schema::{
10    AnnotationFact, AnnotationFactId, AnnotationKindId, AnnotationSubjectId, CanonicalValueRange,
11    CanonicalValueSet, DeclaredSchema, DocText, FunctionBody, FunctionFact, FunctionParameter,
12    FunctionReturnElement, FunctionReturnMode, FunctionSignature, OwnsFact, OwnsFactId, PlaysFact,
13    PlaysFactId, RegexPattern, RelatesFact, RelatesFactId, SchemaAnnotationValue, SchemaDiagnostic,
14    SchemaDiagnostics, SchemaFact, SchemaFactId, SourceSpan, SourcedSchemaFact, StructFact,
15    StructField, SubFact, SubFactId, TypeFact, TypeReference, ValueFact, ValueFactId,
16};
17use type_bridge_contract::temporal::{CanonicalDate, CanonicalDateTime, CanonicalDuration};
18use type_bridge_contract::value::{
19    CanonicalDouble, CanonicalString, CanonicalValue, Cardinality, DecimalValue, ValueTypeTag,
20};
21
22use crate::parse_provider_datetime_tz;
23use crate::{FactAssembler, SchemaDocument, SchemaDocumentSet, YamlMapping, YamlNode, YamlScalar};
24
25/// Exact discriminator for the first YAML Schema V2 document grammar.
26pub const SCHEMA_V2_FORMAT: &str = "typebridge.schema/v2";
27
28/// Normalize a lossless document set into provider-independent direct facts.
29pub fn normalize_documents(
30    documents: &SchemaDocumentSet,
31) -> Result<DeclaredSchema, SchemaDiagnostics> {
32    if documents.is_empty() {
33        return Err(error(
34            "empty_schema_document_set",
35            "a schema document set must contain at least one document",
36            None,
37        ));
38    }
39
40    let mut normalizer = Normalizer::default();
41    for (_, document) in documents.iter() {
42        normalizer.normalize_document(document)?;
43    }
44    normalizer.finish()
45}
46
47#[derive(Default)]
48struct Normalizer {
49    facts: Vec<SourcedSchemaFact>,
50    pending: Vec<PendingAnnotation>,
51    pending_relates: Vec<PendingRelates>,
52    pending_plays: Vec<YamlNode>,
53    capabilities: CapabilitySet,
54    capability_sources: BTreeMap<CapabilityId, SourceSpan>,
55    type_labels: BTreeMap<String, (TypeId, SourceSpan)>,
56}
57
58impl Normalizer {
59    fn normalize_document(&mut self, document: &SchemaDocument) -> Result<(), SchemaDiagnostics> {
60        let root = document.root();
61        check_keys(
62            root,
63            &[
64                "format",
65                "capabilities",
66                "attributes",
67                "entities",
68                "relations",
69                "plays",
70                "functions",
71                "structs",
72                "extensions",
73            ],
74        )?;
75
76        let format = required_entry(root, "format")?;
77        let format = scalar(format.value())?;
78        if format.value() != SCHEMA_V2_FORMAT {
79            return Err(error(
80                "unsupported_schema_document_format",
81                format!("schema document format must be exactly `{SCHEMA_V2_FORMAT}`"),
82                Some(format.span().clone()),
83            ));
84        }
85
86        if let Some(entry) = entry(root, "capabilities") {
87            self.normalize_capabilities(entry.value())?;
88        }
89        if let Some(entry) = entry(root, "extensions") {
90            self.normalize_extensions(entry.value())?;
91        }
92
93        self.normalize_type_section(root, "attributes", TypeKind::Attribute)?;
94        self.normalize_type_section(root, "entities", TypeKind::Entity)?;
95        self.normalize_type_section(root, "relations", TypeKind::Relation)?;
96
97        if let Some(entry) = entry(root, "plays") {
98            self.pending_plays.push(entry.value().clone());
99        }
100        if let Some(entry) = entry(root, "functions") {
101            self.normalize_functions(document, entry.value())?;
102        }
103        if let Some(entry) = entry(root, "structs") {
104            self.normalize_structs(entry.value())?;
105        }
106
107        Ok(())
108    }
109
110    fn normalize_structs(&mut self, node: &YamlNode) -> Result<(), SchemaDiagnostics> {
111        let declarations = mapping(node)?;
112
113        for declaration in declarations.entries() {
114            let id = contract(
115                StructId::new(declaration.key().value()),
116                declaration.key().span(),
117            )?;
118            let body = mapping(declaration.value())?;
119            check_keys(body, &["fields"])?;
120
121            let fields_node = required_entry(body, "fields")?.value();
122            let fields_sequence = sequence(fields_node)?;
123            let mut fields = Vec::with_capacity(fields_sequence.items().len());
124            let mut field_sources = BTreeMap::<Label, SourceSpan>::new();
125
126            for field_node in fields_sequence.items() {
127                let field_body = mapping(field_node)?;
128                check_keys(field_body, &["name", "type", "optional"])?;
129
130                let name = scalar(required_entry(field_body, "name")?.value())?;
131                let name_id = contract(Label::new(name.value()), name.span())?;
132
133                if let Some(previous) = field_sources.insert(name_id.clone(), name.span().clone()) {
134                    return Err(crate::yaml::diagnostic_with_related(
135                        DiagnosticCategory::InvalidContract,
136                        "duplicate_struct_field",
137                        format!("struct field `{}` is declared more than once", name.value()),
138                        name.span().clone(),
139                        previous,
140                        "first field declaration is here",
141                    ));
142                }
143
144                let value_type =
145                    parse_value_type(scalar(required_entry(field_body, "type")?.value())?)?;
146                let optional = entry(field_body, "optional")
147                    .map(|entry| strict_bool(entry.value()))
148                    .transpose()?
149                    .unwrap_or(false);
150
151                fields.push(StructField::new(name_id, value_type, optional));
152            }
153
154            let fact = contract(StructFact::new(id, fields), body.span())?;
155            self.push(SchemaFact::Struct(fact), body.span().clone());
156        }
157
158        Ok(())
159    }
160
161    fn normalize_capabilities(&mut self, node: &YamlNode) -> Result<(), SchemaDiagnostics> {
162        let body = mapping(node)?;
163        check_keys(body, &["required"])?;
164        if let Some(required) = entry(body, "required") {
165            let sequence = sequence(required.value())?;
166            for item in sequence.items() {
167                let capability = scalar(item)?;
168                let id = contract(CapabilityId::new(capability.value()), capability.span())?;
169                self.insert_capability(id, capability.span().clone())?;
170            }
171        }
172        Ok(())
173    }
174
175    fn normalize_extensions(&mut self, node: &YamlNode) -> Result<(), SchemaDiagnostics> {
176        let extensions = mapping(node)?;
177        for extension in extensions.entries() {
178            let id = contract(
179                CapabilityId::new(extension.key().value()),
180                extension.key().span(),
181            )?;
182            let body = mapping(extension.value())?;
183            // V1 extension declarations are requirement-only. Payload-bearing
184            // handlers are intentionally reserved for a future format version;
185            // accepting and ignoring a payload would make it semantic dead data.
186            check_keys(body, &["required"])?;
187            let required = entry(body, "required")
188                .map(|entry| strict_bool(entry.value()))
189                .transpose()?
190                .unwrap_or(false);
191            if required {
192                self.insert_capability(id, extension.key().span().clone())?;
193            }
194        }
195        Ok(())
196    }
197
198    fn insert_capability(
199        &mut self,
200        id: CapabilityId,
201        source: SourceSpan,
202    ) -> Result<(), SchemaDiagnostics> {
203        if let Some(previous) = self.capability_sources.get(&id) {
204            return Err(crate::yaml::diagnostic_with_related(
205                DiagnosticCategory::InvalidContract,
206                "duplicate_required_capability",
207                format!("required capability `{}` is duplicated", id.as_str()),
208                source,
209                previous.clone(),
210                "first requirement is here",
211            ));
212        }
213        self.capabilities.insert(id.clone());
214        self.capability_sources.insert(id, source);
215        Ok(())
216    }
217
218    fn normalize_type_section(
219        &mut self,
220        root: &YamlMapping,
221        section_name: &str,
222        kind: TypeKind,
223    ) -> Result<(), SchemaDiagnostics> {
224        let Some(section) = entry(root, section_name) else {
225            return Ok(());
226        };
227        let section = mapping(section.value())?;
228
229        for declaration in section.entries() {
230            let id = contract(
231                TypeId::new(kind, declaration.key().value()),
232                declaration.key().span(),
233            )?;
234            self.register_type(id.clone(), declaration.key().span().clone())?;
235            self.push(
236                SchemaFact::Type(contract(
237                    TypeFact::new(id.clone()),
238                    declaration.key().span(),
239                )?),
240                declaration.key().span().clone(),
241            );
242
243            let body = mapping(declaration.value())?;
244            let allowed = match kind {
245                TypeKind::Attribute => {
246                    &["sub", "value", "abstract", "independent", "doc", "meta"][..]
247                }
248                TypeKind::Entity => &["sub", "owns", "abstract", "doc", "meta"][..],
249                TypeKind::Relation => &["sub", "owns", "relates", "abstract", "doc", "meta"][..],
250                TypeKind::Struct => &[][..],
251            };
252            check_keys(body, allowed)?;
253
254            self.queue_presence(
255                body,
256                "abstract",
257                AnnotationSubjectId::Type(id.clone()),
258                AnnotationKindId::Abstract,
259            )?;
260            if kind == TypeKind::Attribute {
261                self.queue_presence(
262                    body,
263                    "independent",
264                    AnnotationSubjectId::Type(id.clone()),
265                    AnnotationKindId::Independent,
266                )?;
267            }
268            self.queue_doc_meta(body, AnnotationSubjectId::Type(id.clone()))?;
269
270            if let Some(sub) = entry(body, "sub") {
271                let (parent, annotations) = match sub.value() {
272                    YamlNode::Scalar(parent) => (parent, None),
273                    YamlNode::Mapping(expanded) => {
274                        check_keys(expanded, &["type", "doc", "meta"])?;
275                        let parent = scalar(required_entry(expanded, "type")?.value())?;
276                        (parent, Some(expanded))
277                    }
278                    YamlNode::Sequence(sequence) => {
279                        return Err(error(
280                            "invalid_schema_sub_shape",
281                            "sub must be a scalar or mapping",
282                            Some(sequence.span().clone()),
283                        ));
284                    }
285                };
286                let parent_id = contract(TypeId::new(kind, parent.value()), parent.span())?;
287                let sub_id = contract(SubFactId::new(id.clone(), parent_id), parent.span())?;
288                self.push(
289                    SchemaFact::Sub(SubFact::new(sub_id.clone())),
290                    parent.span().clone(),
291                );
292                if let Some(annotations) = annotations {
293                    self.queue_doc_meta(annotations, AnnotationSubjectId::Sub(sub_id))?;
294                }
295            }
296
297            if kind == TypeKind::Attribute
298                && let Some(value) = entry(body, "value")
299            {
300                self.normalize_value(&id, value.value())?;
301            }
302            if matches!(kind, TypeKind::Entity | TypeKind::Relation)
303                && let Some(owns) = entry(body, "owns")
304            {
305                self.normalize_owns(&id, owns.value())?;
306            }
307            if kind == TypeKind::Relation
308                && let Some(relates) = entry(body, "relates")
309            {
310                self.normalize_relates(&id, relates.value())?;
311            }
312        }
313        Ok(())
314    }
315
316    fn register_type(&mut self, id: TypeId, source: SourceSpan) -> Result<(), SchemaDiagnostics> {
317        let label = id.label().as_str().to_owned();
318        if let Some((_, previous)) = self.type_labels.get(&label) {
319            return Err(crate::yaml::diagnostic_with_related(
320                DiagnosticCategory::InvalidContract,
321                "duplicate_schema_type_label",
322                format!("schema type label `{label}` is duplicated"),
323                source,
324                previous.clone(),
325                "first type label is here",
326            ));
327        }
328        self.type_labels.insert(label, (id, source));
329        Ok(())
330    }
331
332    fn normalize_value(
333        &mut self,
334        attribute: &TypeId,
335        node: &YamlNode,
336    ) -> Result<(), SchemaDiagnostics> {
337        let (value_type, source, annotations) = match node {
338            YamlNode::Scalar(value) => (parse_value_type(value)?, value.span().clone(), None),
339            YamlNode::Mapping(body) => {
340                check_keys(body, &["type", "regex", "range", "values"])?;
341                let value = required_entry(body, "type")?;
342                let value = scalar(value.value())?;
343                (parse_value_type(value)?, value.span().clone(), Some(body))
344            }
345            YamlNode::Sequence(sequence) => {
346                return Err(error(
347                    "invalid_schema_value_shape",
348                    "attribute value must be a scalar or mapping",
349                    Some(sequence.span().clone()),
350                ));
351            }
352        };
353
354        let attribute_id = contract(AttributeId::new(attribute.label().as_str()), &source)?;
355        let value_id = ValueFactId::new(attribute_id);
356        self.push(
357            SchemaFact::Value(ValueFact::new(value_id.clone(), value_type)),
358            source,
359        );
360
361        if let Some(body) = annotations {
362            self.queue_value_annotations(body, AnnotationSubjectId::Value(value_id))?;
363        }
364        Ok(())
365    }
366
367    fn normalize_owns(&mut self, owner: &TypeId, node: &YamlNode) -> Result<(), SchemaDiagnostics> {
368        for named in named_bodies(node)? {
369            let attribute = contract(AttributeId::new(named.name.value()), named.name.span())?;
370            let id = contract(OwnsFactId::new(owner.clone(), attribute), &named.source)?;
371            self.push(
372                SchemaFact::Owns(OwnsFact::new(id.clone())),
373                named.source.clone(),
374            );
375
376            if let Some(body) = &named.body {
377                check_keys(
378                    body,
379                    &[
380                        "key", "unique", "card", "regex", "range", "values", "doc", "meta",
381                    ],
382                )?;
383                let subject = AnnotationSubjectId::Owns(id);
384                self.queue_presence(body, "key", subject.clone(), AnnotationKindId::Key)?;
385                self.queue_presence(body, "unique", subject.clone(), AnnotationKindId::Unique)?;
386                self.queue_if_present(
387                    body,
388                    "card",
389                    subject.clone(),
390                    AnnotationKindId::Card,
391                    PendingInput::Card,
392                )?;
393                self.queue_value_annotations(body, subject.clone())?;
394                self.queue_doc_meta(body, subject)?;
395            }
396        }
397        Ok(())
398    }
399
400    fn normalize_relates(
401        &mut self,
402        relation: &TypeId,
403        node: &YamlNode,
404    ) -> Result<(), SchemaDiagnostics> {
405        for named in named_bodies(node)? {
406            let role = contract(
407                RoleId::new(relation.label().as_str(), named.name.value()),
408                named.name.span(),
409            )?;
410            let specializes = if let Some(body) = &named.body {
411                if let Some(specializes) = entry(body, "as") {
412                    let specializes = scalar(specializes.value())?;
413                    Some((specializes.value().to_owned(), specializes.span().clone()))
414                } else {
415                    None
416                }
417            } else {
418                None
419            };
420            let id = contract(RelatesFactId::new(relation.clone(), role), &named.source)?;
421            self.pending_relates.push(PendingRelates {
422                id: id.clone(),
423                specializes,
424                source: named.source.clone(),
425            });
426
427            if let Some(body) = &named.body {
428                check_keys(body, &["as", "abstract", "card", "doc", "meta"])?;
429                let subject = AnnotationSubjectId::Relates(id);
430                self.queue_presence(
431                    body,
432                    "abstract",
433                    subject.clone(),
434                    AnnotationKindId::Abstract,
435                )?;
436                self.queue_if_present(
437                    body,
438                    "card",
439                    subject.clone(),
440                    AnnotationKindId::Card,
441                    PendingInput::Card,
442                )?;
443                self.queue_doc_meta(body, subject)?;
444            }
445        }
446        Ok(())
447    }
448
449    fn normalize_plays(&mut self, node: &YamlNode) -> Result<(), SchemaDiagnostics> {
450        let players = mapping(node)?;
451        for player_entry in players.entries() {
452            let Some((player, _)) = self.type_labels.get(player_entry.key().value()).cloned()
453            else {
454                return Err(error(
455                    "unknown_schema_player",
456                    "root plays declaration references an unknown player type",
457                    Some(player_entry.key().span().clone()),
458                ));
459            };
460            if !matches!(player.kind(), TypeKind::Entity | TypeKind::Relation) {
461                return Err(error(
462                    "invalid_schema_player_kind",
463                    "only entity and relation types may play roles",
464                    Some(player_entry.key().span().clone()),
465                ));
466            }
467            let relations = mapping(player_entry.value())?;
468            for relation_entry in relations.entries() {
469                let Some((relation, _)) =
470                    self.type_labels.get(relation_entry.key().value()).cloned()
471                else {
472                    return Err(error(
473                        "unknown_schema_relation",
474                        "root plays declaration references an unknown relation type",
475                        Some(relation_entry.key().span().clone()),
476                    ));
477                };
478                if relation.kind() != TypeKind::Relation {
479                    return Err(error(
480                        "invalid_schema_relation_kind",
481                        "root plays relation key must name a relation type",
482                        Some(relation_entry.key().span().clone()),
483                    ));
484                }
485                for named in named_bodies(relation_entry.value())? {
486                    let role = contract(
487                        RoleId::new(relation.label().as_str(), named.name.value()),
488                        named.name.span(),
489                    )?;
490                    let id = contract(PlaysFactId::new(player.clone(), role), &named.source)?;
491                    self.push(
492                        SchemaFact::Plays(PlaysFact::new(id.clone())),
493                        named.source.clone(),
494                    );
495                    if let Some(body) = &named.body {
496                        check_keys(body, &["card", "doc", "meta"])?;
497                        let subject = AnnotationSubjectId::Plays(id);
498                        self.queue_if_present(
499                            body,
500                            "card",
501                            subject.clone(),
502                            AnnotationKindId::Card,
503                            PendingInput::Card,
504                        )?;
505                        self.queue_doc_meta(body, subject)?;
506                    }
507                }
508            }
509        }
510        Ok(())
511    }
512
513    fn normalize_functions(
514        &mut self,
515        _document: &SchemaDocument,
516        node: &YamlNode,
517    ) -> Result<(), SchemaDiagnostics> {
518        let functions = mapping(node)?;
519        for declaration in functions.entries() {
520            let body = mapping(declaration.value())?;
521            check_keys(body, &["parameters", "returns", "body", "doc", "meta"])?;
522            let typeql_body = mapping(required_entry(body, "body")?.value())?;
523            check_keys(typeql_body, &["typeql"])?;
524            let body_text = scalar(required_entry(typeql_body, "typeql")?.value())?;
525
526            let mut parameters_out = Vec::new();
527            let mut parameter_sources = BTreeMap::<Label, SourceSpan>::new();
528            if let Some(parameters) = entry(body, "parameters") {
529                for parameter_node in sequence(parameters.value())?.items() {
530                    let parameter = mapping(parameter_node)?;
531                    check_keys(parameter, &["name", "type"])?;
532                    let name = scalar(required_entry(parameter, "name")?.value())?;
533                    let name_id = contract(Label::new(name.value()), name.span())?;
534                    if let Some(previous) =
535                        parameter_sources.insert(name_id.clone(), name.span().clone())
536                    {
537                        return Err(crate::diagnostic::diagnostic_with_related(
538                            DiagnosticCategory::InvalidContract,
539                            "duplicate_function_parameter",
540                            format!("function parameter `{}` is duplicated", name.value()),
541                            name.span().clone(),
542                            previous,
543                            "first parameter declaration is here",
544                        ));
545                    }
546                    let type_token = scalar(required_entry(parameter, "type")?.value())?;
547                    let type_ref = contract(
548                        TypeReference::from_token(type_token.value()),
549                        type_token.span(),
550                    )?;
551                    parameters_out.push(FunctionParameter::new(name_id, type_ref));
552                }
553            }
554
555            let returns = mapping(required_entry(body, "returns")?.value())?;
556            check_keys(returns, &["stream"])?;
557            let stream = sequence(required_entry(returns, "stream")?.value())?;
558            let mut return_elements = Vec::with_capacity(stream.items().len());
559            for element in stream.items() {
560                let type_token = scalar(element)?;
561                let type_ref = contract(
562                    TypeReference::from_token(type_token.value()),
563                    type_token.span(),
564                )?;
565                return_elements.push(FunctionReturnElement::new(type_ref, false));
566            }
567
568            let id = contract(
569                FunctionId::new(declaration.key().value()),
570                declaration.key().span(),
571            )?;
572            let returns = contract(FunctionReturnMode::stream(return_elements), returns.span())?;
573            let signature = contract(FunctionSignature::new(parameters_out, returns), body.span())?;
574            let function_body = contract(FunctionBody::new(body_text.value()), body_text.span())?;
575            self.push(
576                SchemaFact::Function(FunctionFact::new(id.clone(), signature, function_body)),
577                body.span().clone(),
578            );
579            self.queue_doc_meta(body, AnnotationSubjectId::Function(id))?;
580        }
581        Ok(())
582    }
583
584    fn queue_presence(
585        &mut self,
586        body: &YamlMapping,
587        key: &str,
588        subject: AnnotationSubjectId,
589        kind: AnnotationKindId,
590    ) -> Result<(), SchemaDiagnostics> {
591        let Some(entry) = entry(body, key) else {
592            return Ok(());
593        };
594        if !strict_bool(entry.value())? {
595            return Err(error(
596                "false_presence_annotation",
597                "presence annotations must be omitted rather than set to false",
598                Some(entry.value().span().clone()),
599            ));
600        }
601        self.pending.push(PendingAnnotation {
602            subject,
603            kind,
604            input: PendingInput::Presence,
605            source: entry.value().span().clone(),
606        });
607        Ok(())
608    }
609
610    fn queue_if_present(
611        &mut self,
612        body: &YamlMapping,
613        key: &str,
614        subject: AnnotationSubjectId,
615        kind: AnnotationKindId,
616        input: fn(YamlNode) -> PendingInput,
617    ) -> Result<(), SchemaDiagnostics> {
618        if let Some(entry) = entry(body, key) {
619            self.pending.push(PendingAnnotation {
620                subject,
621                kind,
622                input: input(entry.value().clone()),
623                source: entry.value().span().clone(),
624            });
625        }
626        Ok(())
627    }
628
629    fn queue_value_annotations(
630        &mut self,
631        body: &YamlMapping,
632        subject: AnnotationSubjectId,
633    ) -> Result<(), SchemaDiagnostics> {
634        self.queue_if_present(
635            body,
636            "regex",
637            subject.clone(),
638            AnnotationKindId::Regex,
639            PendingInput::Regex,
640        )?;
641        self.queue_if_present(
642            body,
643            "range",
644            subject.clone(),
645            AnnotationKindId::Range,
646            PendingInput::Range,
647        )?;
648        self.queue_if_present(
649            body,
650            "values",
651            subject,
652            AnnotationKindId::Values,
653            PendingInput::Values,
654        )
655    }
656
657    fn queue_doc_meta(
658        &mut self,
659        body: &YamlMapping,
660        subject: AnnotationSubjectId,
661    ) -> Result<(), SchemaDiagnostics> {
662        self.queue_if_present(
663            body,
664            "doc",
665            subject.clone(),
666            AnnotationKindId::Doc,
667            PendingInput::Doc,
668        )?;
669        if let Some(meta) = entry(body, "meta") {
670            let meta = mapping(meta.value())?;
671            for item in meta.entries() {
672                let kind = contract(
673                    AnnotationKindId::meta(item.key().value()),
674                    item.key().span(),
675                )?;
676                self.pending.push(PendingAnnotation {
677                    subject: subject.clone(),
678                    kind,
679                    input: PendingInput::Meta(item.value().clone()),
680                    source: item.value().span().clone(),
681                });
682            }
683        }
684        Ok(())
685    }
686
687    fn push(&mut self, fact: SchemaFact, source: SourceSpan) {
688        self.facts.push(SourcedSchemaFact::new(fact, source));
689    }
690
691    fn finish(mut self) -> Result<DeclaredSchema, SchemaDiagnostics> {
692        self.materialize_relates()?;
693        for plays in std::mem::take(&mut self.pending_plays) {
694            self.normalize_plays(&plays)?;
695        }
696        let mut assembler = FactAssembler::new(FormatVersion::V1);
697        for (capability, source) in &self.capability_sources {
698            assembler.require_capability(capability.clone(), source.clone())?;
699        }
700        for sourced in &self.facts {
701            match sourced.fact() {
702                SchemaFact::Struct(fact) => {
703                    assembler.insert_struct(fact.clone(), sourced.source().clone())?;
704                }
705                fact => {
706                    assembler.insert_fact(fact.clone(), sourced.source().clone())?;
707                }
708            }
709        }
710        let structural = assembler.finish()?;
711        for pending in self.pending {
712            self.facts.push(pending.resolve(&structural)?);
713        }
714        DeclaredSchema::from_facts(FormatVersion::V1, self.capabilities, self.facts)
715    }
716
717    fn materialize_relates(&mut self) -> Result<(), SchemaDiagnostics> {
718        let declarations = std::mem::take(&mut self.pending_relates);
719        for declaration in &declarations {
720            let specializes = declaration
721                .specializes
722                .as_ref()
723                .map(|(label, source)| {
724                    self.resolve_inherited_role(
725                        declaration.id.relation(),
726                        label,
727                        source,
728                        &declarations,
729                    )
730                })
731                .transpose()?;
732            self.push(
733                SchemaFact::Relates(contract(
734                    RelatesFact::new(declaration.id.clone(), specializes),
735                    &declaration.source,
736                )?),
737                declaration.source.clone(),
738            );
739        }
740        Ok(())
741    }
742
743    fn resolve_inherited_role(
744        &self,
745        relation: &TypeId,
746        role_label: &str,
747        source: &SourceSpan,
748        declarations: &[PendingRelates],
749    ) -> Result<RoleId, SchemaDiagnostics> {
750        let mut current = relation.clone();
751        let mut visited = BTreeSet::new();
752        loop {
753            if !visited.insert(current.clone()) {
754                return Err(error(
755                    "schema_inheritance_cycle",
756                    "relation inheritance contains a cycle",
757                    Some(source.clone()),
758                ));
759            }
760            let Some(parent) = self.direct_parent(&current) else {
761                return Err(error(
762                    "invalid_role_specialization",
763                    "specialized role is not declared by any ancestor relation",
764                    Some(source.clone()),
765                ));
766            };
767            if let Some(role) = declarations.iter().find_map(|candidate| {
768                (candidate.id.relation() == &parent
769                    && candidate.id.role().label().as_str() == role_label)
770                    .then(|| candidate.id.role().clone())
771            }) {
772                return Ok(role);
773            }
774            current = parent;
775        }
776    }
777
778    fn direct_parent(&self, subtype: &TypeId) -> Option<TypeId> {
779        self.facts.iter().find_map(|sourced| {
780            let SchemaFact::Sub(sub) = sourced.fact() else {
781                return None;
782            };
783            (sub.id().subtype() == subtype).then(|| sub.id().supertype().clone())
784        })
785    }
786}
787
788struct PendingRelates {
789    id: RelatesFactId,
790    specializes: Option<(String, SourceSpan)>,
791    source: SourceSpan,
792}
793
794struct PendingAnnotation {
795    subject: AnnotationSubjectId,
796    kind: AnnotationKindId,
797    input: PendingInput,
798    source: SourceSpan,
799}
800
801enum PendingInput {
802    Presence,
803    Card(YamlNode),
804    Regex(YamlNode),
805    Range(YamlNode),
806    Values(YamlNode),
807    Doc(YamlNode),
808    Meta(YamlNode),
809}
810
811impl PendingAnnotation {
812    fn resolve(self, schema: &DeclaredSchema) -> Result<SourcedSchemaFact, SchemaDiagnostics> {
813        let value = match self.input {
814            PendingInput::Presence => SchemaAnnotationValue::Presence,
815            PendingInput::Card(node) => {
816                SchemaAnnotationValue::Cardinality(parse_cardinality(&node)?)
817            }
818            PendingInput::Regex(node) => SchemaAnnotationValue::Regex(contract(
819                RegexPattern::new(scalar(&node)?.value()),
820                node.span(),
821            )?),
822            PendingInput::Range(node) => {
823                let value_type = subject_value_type(schema, &self.subject, &self.source)?;
824                SchemaAnnotationValue::Range(parse_range(&node, value_type)?)
825            }
826            PendingInput::Values(node) => {
827                let value_type = subject_value_type(schema, &self.subject, &self.source)?;
828                SchemaAnnotationValue::Values(parse_values(&node, value_type)?)
829            }
830            PendingInput::Doc(node) => SchemaAnnotationValue::Doc(contract(
831                DocText::new(scalar(&node)?.value()),
832                node.span(),
833            )?),
834            PendingInput::Meta(node) => {
835                let scalar = scalar(&node)?;
836                SchemaAnnotationValue::Meta(CanonicalValue::String(contract(
837                    CanonicalString::new(scalar.value()),
838                    scalar.span(),
839                )?))
840            }
841        };
842        let fact = contract(
843            AnnotationFact::new(AnnotationFactId::new(self.subject, self.kind), value),
844            &self.source,
845        )?;
846        Ok(SourcedSchemaFact::new(
847            SchemaFact::Annotation(fact),
848            self.source,
849        ))
850    }
851}
852
853#[derive(Clone)]
854struct NamedBody {
855    name: YamlScalar,
856    body: Option<YamlMapping>,
857    source: SourceSpan,
858}
859
860fn named_bodies(node: &YamlNode) -> Result<Vec<NamedBody>, SchemaDiagnostics> {
861    match node {
862        YamlNode::Mapping(mapping) => mapping.entries().iter().map(named_mapping_entry).collect(),
863        YamlNode::Sequence(sequence) => sequence
864            .items()
865            .iter()
866            .map(|item| match item {
867                YamlNode::Scalar(name) => Ok(NamedBody {
868                    name: name.clone(),
869                    body: None,
870                    source: name.span().clone(),
871                }),
872                YamlNode::Mapping(mapping) if mapping.entries().len() == 1 => {
873                    named_mapping_entry(&mapping.entries()[0])
874                }
875                other => Err(error(
876                    "invalid_named_schema_fact",
877                    "named facts must be scalars or single-entry mappings",
878                    Some(other.span().clone()),
879                )),
880            })
881            .collect(),
882        YamlNode::Scalar(value) => Err(error(
883            "invalid_named_schema_fact_collection",
884            "named facts must be a sequence or mapping",
885            Some(value.span().clone()),
886        )),
887    }
888}
889
890fn named_mapping_entry(entry: &crate::YamlMappingEntry) -> Result<NamedBody, SchemaDiagnostics> {
891    let (body, source) = match entry.value() {
892        YamlNode::Mapping(body) => (Some(body.clone()), body.span().clone()),
893        other => {
894            return Err(error(
895                "invalid_named_schema_fact_body",
896                "expanded named fact bodies must be mappings",
897                Some(other.span().clone()),
898            ));
899        }
900    };
901    Ok(NamedBody {
902        name: entry.key().clone(),
903        body,
904        source,
905    })
906}
907
908fn subject_value_type(
909    schema: &DeclaredSchema,
910    subject: &AnnotationSubjectId,
911    source: &SourceSpan,
912) -> Result<ValueTypeTag, SchemaDiagnostics> {
913    let mut attribute = match subject {
914        AnnotationSubjectId::Value(id) => id.attribute().clone(),
915        AnnotationSubjectId::Owns(id) => id.attribute().clone(),
916        _ => {
917            return Err(error(
918                "annotation_value_domain_unavailable",
919                "value annotation subject has no attribute domain",
920                Some(source.clone()),
921            ));
922        }
923    };
924    let mut visited = BTreeSet::new();
925    loop {
926        let type_id = contract(
927            TypeId::new(TypeKind::Attribute, attribute.label().as_str()),
928            source,
929        )?;
930        if !visited.insert(type_id.clone()) {
931            return Err(error(
932                "schema_value_inheritance_cycle",
933                "attribute value inheritance contains a cycle",
934                Some(source.clone()),
935            ));
936        }
937        let value_id = ValueFactId::new(attribute.clone());
938        if let Some(SchemaFact::Value(value)) = schema.fact(&SchemaFactId::Value(value_id)) {
939            return Ok(value.value_type());
940        }
941        let Some(parent) = schema.facts().find_map(|fact| {
942            let SchemaFact::Sub(sub) = fact else {
943                return None;
944            };
945            (sub.id().subtype() == &type_id && sub.id().supertype().kind() == TypeKind::Attribute)
946                .then(|| sub.id().supertype().clone())
947        }) else {
948            return Err(error(
949                "schema_value_domain_missing",
950                "attribute has no direct or inherited value domain",
951                Some(source.clone()),
952            ));
953        };
954        attribute = contract(AttributeId::new(parent.label().as_str()), source)?;
955    }
956}
957
958fn parse_cardinality(node: &YamlNode) -> Result<Cardinality, SchemaDiagnostics> {
959    match node {
960        YamlNode::Scalar(value) => {
961            let exact = canonical_u64(value)?;
962            contract(Cardinality::new(exact, Some(exact)), value.span())
963        }
964        YamlNode::Mapping(body) => {
965            check_keys(body, &["min", "max"])?;
966            let min = canonical_u64(scalar(required_entry(body, "min")?.value())?)?;
967            let max = entry(body, "max")
968                .map(|entry| canonical_u64(scalar(entry.value())?))
969                .transpose()?;
970            contract(Cardinality::new(min, max), body.span())
971        }
972        YamlNode::Sequence(sequence) => Err(error(
973            "invalid_cardinality_shape",
974            "cardinality must be an integer or a min/max mapping",
975            Some(sequence.span().clone()),
976        )),
977    }
978}
979
980fn parse_values(
981    node: &YamlNode,
982    value_type: ValueTypeTag,
983) -> Result<CanonicalValueSet, SchemaDiagnostics> {
984    let values = sequence(node)?;
985    let canonical = values
986        .items()
987        .iter()
988        .map(|item| canonical_value(value_type, scalar(item)?))
989        .collect::<Result<Vec<_>, _>>()?;
990    contract(CanonicalValueSet::new(canonical), values.span())
991}
992
993fn parse_range(
994    node: &YamlNode,
995    value_type: ValueTypeTag,
996) -> Result<CanonicalValueRange, SchemaDiagnostics> {
997    let body = mapping(node)?;
998    check_keys(body, &["min", "max"])?;
999    let lower = entry(body, "min")
1000        .map(|entry| canonical_value(value_type, scalar(entry.value())?))
1001        .transpose()?;
1002    let upper = entry(body, "max")
1003        .map(|entry| canonical_value(value_type, scalar(entry.value())?))
1004        .transpose()?;
1005    contract(CanonicalValueRange::new(lower, upper), body.span())
1006}
1007
1008fn canonical_value(
1009    value_type: ValueTypeTag,
1010    scalar: &YamlScalar,
1011) -> Result<CanonicalValue, SchemaDiagnostics> {
1012    let spelling = scalar.value();
1013    match value_type {
1014        ValueTypeTag::String => Ok(CanonicalValue::String(contract(
1015            CanonicalString::new(spelling),
1016            scalar.span(),
1017        )?)),
1018        ValueTypeTag::Long => {
1019            let value = spelling.parse::<i64>().map_err(|_| {
1020                error(
1021                    "invalid_integer_value",
1022                    "integer annotation value is not an i64",
1023                    Some(scalar.span().clone()),
1024                )
1025            })?;
1026            if value.to_string() != spelling {
1027                return Err(error(
1028                    "non_canonical_integer_value",
1029                    "integer annotation value is not canonically spelled",
1030                    Some(scalar.span().clone()),
1031                ));
1032            }
1033            Ok(CanonicalValue::Long(value))
1034        }
1035        ValueTypeTag::Double => {
1036            let value = spelling.parse::<f64>().map_err(|_| {
1037                error(
1038                    "invalid_double_value",
1039                    "double annotation value is not an f64",
1040                    Some(scalar.span().clone()),
1041                )
1042            })?;
1043            Ok(CanonicalValue::Double(contract(
1044                CanonicalDouble::new(value),
1045                scalar.span(),
1046            )?))
1047        }
1048        ValueTypeTag::Boolean => match spelling {
1049            "true" => Ok(CanonicalValue::Boolean(true)),
1050            "false" => Ok(CanonicalValue::Boolean(false)),
1051            _ => Err(error(
1052                "invalid_boolean_value",
1053                "boolean annotation value must be exactly `true` or `false`",
1054                Some(scalar.span().clone()),
1055            )),
1056        },
1057        ValueTypeTag::Date => Ok(CanonicalValue::Date(contract(
1058            spelling.parse::<CanonicalDate>(),
1059            scalar.span(),
1060        )?)),
1061        ValueTypeTag::DateTime => Ok(CanonicalValue::DateTime(contract(
1062            spelling.parse::<CanonicalDateTime>(),
1063            scalar.span(),
1064        )?)),
1065        ValueTypeTag::DateTimeTz => Ok(CanonicalValue::DateTimeTz(contract(
1066            parse_provider_datetime_tz(spelling),
1067            scalar.span(),
1068        )?)),
1069        ValueTypeTag::Decimal => Ok(CanonicalValue::Decimal(contract(
1070            DecimalValue::new(spelling),
1071            scalar.span(),
1072        )?)),
1073        ValueTypeTag::Duration => Ok(CanonicalValue::Duration(contract(
1074            spelling.parse::<CanonicalDuration>(),
1075            scalar.span(),
1076        )?)),
1077    }
1078}
1079
1080fn parse_value_type(value: &YamlScalar) -> Result<ValueTypeTag, SchemaDiagnostics> {
1081    match value.value() {
1082        "string" => Ok(ValueTypeTag::String),
1083        "integer" => Ok(ValueTypeTag::Long),
1084        "double" => Ok(ValueTypeTag::Double),
1085        "boolean" => Ok(ValueTypeTag::Boolean),
1086        "date" => Ok(ValueTypeTag::Date),
1087        "datetime" => Ok(ValueTypeTag::DateTime),
1088        "datetime-tz" => Ok(ValueTypeTag::DateTimeTz),
1089        "decimal" => Ok(ValueTypeTag::Decimal),
1090        "duration" => Ok(ValueTypeTag::Duration),
1091        _ => Err(error(
1092            "unknown_schema_value_type",
1093            "value type must use a canonical TypeQL value-type token",
1094            Some(value.span().clone()),
1095        )),
1096    }
1097}
1098
1099fn canonical_u64(value: &YamlScalar) -> Result<u64, SchemaDiagnostics> {
1100    let parsed = value.value().parse::<u64>().map_err(|_| {
1101        error(
1102            "invalid_unsigned_integer",
1103            "value must be an unsigned integer",
1104            Some(value.span().clone()),
1105        )
1106    })?;
1107    if parsed.to_string() != value.value() {
1108        return Err(error(
1109            "non_canonical_unsigned_integer",
1110            "unsigned integer is not canonically spelled",
1111            Some(value.span().clone()),
1112        ));
1113    }
1114    Ok(parsed)
1115}
1116
1117fn strict_bool(node: &YamlNode) -> Result<bool, SchemaDiagnostics> {
1118    let value = scalar(node)?;
1119    match value.value() {
1120        "true" => Ok(true),
1121        "false" => Ok(false),
1122        _ => Err(error(
1123            "invalid_schema_boolean",
1124            "schema boolean must be exactly `true` or `false`",
1125            Some(value.span().clone()),
1126        )),
1127    }
1128}
1129
1130fn check_keys(mapping: &YamlMapping, allowed: &[&str]) -> Result<(), SchemaDiagnostics> {
1131    for entry in mapping.entries() {
1132        if !allowed.contains(&entry.key().value()) {
1133            return Err(error(
1134                "unknown_schema_document_key",
1135                format!("unknown schema key `{}`", entry.key().value()),
1136                Some(entry.key().span().clone()),
1137            ));
1138        }
1139    }
1140    Ok(())
1141}
1142
1143fn entry<'a>(mapping: &'a YamlMapping, key: &str) -> Option<&'a crate::YamlMappingEntry> {
1144    mapping
1145        .entries()
1146        .iter()
1147        .find(|entry| entry.key().value() == key)
1148}
1149
1150fn required_entry<'a>(
1151    mapping: &'a YamlMapping,
1152    key: &str,
1153) -> Result<&'a crate::YamlMappingEntry, SchemaDiagnostics> {
1154    entry(mapping, key).ok_or_else(|| {
1155        error(
1156            "missing_schema_document_key",
1157            format!("required schema key `{key}` is missing"),
1158            Some(mapping.span().clone()),
1159        )
1160    })
1161}
1162
1163fn scalar(node: &YamlNode) -> Result<&YamlScalar, SchemaDiagnostics> {
1164    node.as_scalar().ok_or_else(|| {
1165        error(
1166            "schema_scalar_required",
1167            "schema value must be a scalar",
1168            Some(node.span().clone()),
1169        )
1170    })
1171}
1172
1173fn mapping(node: &YamlNode) -> Result<&YamlMapping, SchemaDiagnostics> {
1174    node.as_mapping().ok_or_else(|| {
1175        error(
1176            "schema_mapping_required",
1177            "schema value must be a mapping",
1178            Some(node.span().clone()),
1179        )
1180    })
1181}
1182
1183fn sequence(node: &YamlNode) -> Result<&crate::YamlSequence, SchemaDiagnostics> {
1184    node.as_sequence().ok_or_else(|| {
1185        error(
1186            "schema_sequence_required",
1187            "schema value must be a sequence",
1188            Some(node.span().clone()),
1189        )
1190    })
1191}
1192
1193fn contract<T>(result: Result<T, Diagnostic>, span: &SourceSpan) -> Result<T, SchemaDiagnostics> {
1194    result.map_err(|diagnostic| {
1195        SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, Some(span.clone())))
1196    })
1197}
1198
1199fn error(
1200    code: &'static str,
1201    message: impl Into<String>,
1202    primary: Option<SourceSpan>,
1203) -> SchemaDiagnostics {
1204    crate::yaml::diagnostic(DiagnosticCategory::InvalidContract, code, message, primary)
1205}