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