Skip to main content

type_bridge_schema_compat/
lib.rs

1//! One-way compatibility front-ends for the V2 schema fact graph.
2//!
3//! This public supporting crate is deliberately narrow. Source-language parsers
4//! converge on `type_bridge_schema::FactAssembler`; contract and schema crates
5//! never depend on compatibility parsers or their transitive grammar
6//! dependencies.
7
8#![deny(missing_docs)]
9
10mod adopted_genesis;
11mod descriptor;
12mod function_references;
13mod literal;
14mod live_authority;
15mod released_syntax;
16
17pub use adopted_genesis::{
18    ADOPTED_GENESIS_FILE_NAME, AdoptedGenesisAuthority, LEGACY_LEDGER_SCHEMA_TYPEQL,
19    is_legacy_ledger_label, parse_adopted_genesis, parse_adopted_genesis_authority,
20    parse_adopted_genesis_authority_with_internal,
21};
22pub use function_references::{FunctionBodyReferences, SchemaReference, TypeqlDeclaredSchema};
23pub use live_authority::{
24    LiveLegacyLedgerPresence, LiveQueryAuthorityState, LiveQueryControlPresence,
25    MANAGED_FENCE_SCHEMA_TYPEQL, legacy_ledger_schema_presence, managed_fence_schema_presence,
26    rebuild_live_query_authority, rebuild_live_query_authority_state,
27};
28
29pub use descriptor::{
30    GENERATED_DECLARED_DESCRIPTOR_PATH, GENERATED_DECLARED_DESCRIPTOR_V1,
31    GENERATED_DECLARED_DESCRIPTOR_V2, GeneratedDeclaredDescriptorSetV1,
32    attach_declared_descriptors, empty_generated_declared_descriptors_json,
33    generate_package_with_declared_descriptors, generated_declared_descriptors_json,
34    generated_descriptors_to_declared, released_typeql_to_declared_lossless_projection,
35    released_typeql_to_declared_lossless_projection_with_references,
36    released_typeql_to_declared_projection, released_typeql_to_declared_projection_with_references,
37    typeql_to_generated_descriptors,
38};
39
40/// Comparison-only reporting between the frozen V1 parser and the V2 fact graph.
41pub mod shadow;
42
43pub use shadow::{
44    ShadowCompared, ShadowComparison, ShadowCoverage, ShadowCoverageState, ShadowDimension,
45    ShadowFinding, ShadowLaneNotRun, ShadowLaneOutcome, ShadowLaneRejection, ShadowLaneSummary,
46    ShadowUnavailableLane, ShadowVerdict, V1ShadowInternalError, V1ShadowReport, v1_shadow_report,
47};
48
49use std::collections::{BTreeMap, BTreeSet};
50
51use type_bridge_contract::codec::FormatVersion;
52use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticCode};
53use type_bridge_contract::id::{
54    AttributeId, FunctionId, Label, RoleId, StructId, TypeId, TypeKind,
55};
56use type_bridge_contract::limits::MAX_CANONICAL_COLLECTION_LEN;
57use type_bridge_contract::schema::{
58    AnnotationFact, AnnotationFactId, AnnotationKindId, AnnotationSubjectId, CanonicalValueRange,
59    CanonicalValueSet, CollectionMode, DeclaredSchema, DocText, DocumentId, FunctionBody,
60    FunctionFact, FunctionParameter, FunctionReturnElement, FunctionReturnMode, FunctionSignature,
61    OwnsFact, OwnsFactId, PlaysFactId, RegexPattern, RelatesFactId, SchemaAnnotationValue,
62    SchemaDiagnostic, SchemaDiagnostics, SchemaFact, SourceSpan, StructFact, StructField, SubFact,
63    SubFactId, TypeFact, TypeReference, ValueFact, ValueFactId,
64};
65use type_bridge_contract::value::{CanonicalString, CanonicalValue, Cardinality, ValueTypeTag};
66use type_bridge_schema::FactAssembler;
67use typeql::Annotation;
68use typeql::annotation::CardinalityRange;
69use typeql::common::{Span, Spanned};
70use typeql::query::{QueryStructure, schema::SchemaQuery};
71use typeql::schema::definable::{
72    Definable,
73    function::{Function, Output},
74    struct_::Struct,
75    type_::{Capability, CapabilityBase, Type as TypeDeclaration},
76};
77use typeql::type_::{NamedType, NamedTypeAny, TypeRef, TypeRefAny};
78
79use crate::literal::{canonical_literal, validate_quoted_string};
80use crate::released_syntax::{ReleasedAnnotationTarget, ReleasedSyntax};
81
82/// Defensive source bound applied before entering the third-party parser.
83pub const MAX_TYPEQL_SCHEMA_BYTES: usize = 16 * 1024 * 1024;
84
85#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub(crate) enum TypeqlSourceSizePolicy {
87    Defensive,
88    TrustedGenerator,
89}
90
91impl TypeqlSourceSizePolicy {
92    const fn allows(self, source_len: usize) -> bool {
93        matches!(self, Self::TrustedGenerator) || source_len <= MAX_TYPEQL_SCHEMA_BYTES
94    }
95}
96
97fn ensure_typeql_source_size(
98    source: &str,
99    size_policy: TypeqlSourceSizePolicy,
100) -> Result<(), SchemaDiagnostics> {
101    if size_policy.allows(source.len()) {
102        return Ok(());
103    }
104    Err(error(
105        DiagnosticCategory::InvalidContract,
106        "typeql_schema_size_limit",
107        "TypeQL schema source exceeds the compatibility parser limit",
108        None,
109    ))
110}
111
112/// Parse one TypeQL `define` query into the canonical declared schema graph
113/// and derive neutral references from every function body.
114pub fn typeql_to_declared_with_references(
115    document: DocumentId,
116    source: &str,
117) -> Result<TypeqlDeclaredSchema, SchemaDiagnostics> {
118    typeql_to_declared_with_references_with_size_policy(
119        document,
120        source,
121        TypeqlSourceSizePolicy::Defensive,
122    )
123}
124
125pub(crate) fn typeql_to_declared_with_references_with_size_policy(
126    document: DocumentId,
127    source: &str,
128    size_policy: TypeqlSourceSizePolicy,
129) -> Result<TypeqlDeclaredSchema, SchemaDiagnostics> {
130    ensure_typeql_source_size(source, size_policy)?;
131    typeql_to_declared_with_references_impl(document, source, source, None, None, None, None)
132}
133
134pub(crate) fn released_typeql_to_declared_with_references(
135    document: DocumentId,
136    released: &ReleasedSyntax,
137    size_policy: TypeqlSourceSizePolicy,
138) -> Result<TypeqlDeclaredSchema, SchemaDiagnostics> {
139    ensure_typeql_source_size(released.original_source(), size_policy)?;
140    let reference_projection = released_unresolved_capability_ranges(&document, released)?;
141    typeql_to_declared_with_references_impl(
142        document,
143        released.original_source(),
144        released.source(),
145        Some(released),
146        None,
147        None,
148        Some(&reference_projection.played_role_declarations),
149    )
150}
151
152pub(crate) fn released_typeql_to_declared_with_references_omitting_capabilities(
153    document: DocumentId,
154    released: &ReleasedSyntax,
155    omitted_declarations: &BTreeSet<usize>,
156    omitted_capabilities: &BTreeSet<usize>,
157    played_role_declarations: &BTreeMap<usize, String>,
158    size_policy: TypeqlSourceSizePolicy,
159) -> Result<TypeqlDeclaredSchema, SchemaDiagnostics> {
160    ensure_typeql_source_size(released.original_source(), size_policy)?;
161    typeql_to_declared_with_references_impl(
162        document,
163        released.original_source(),
164        released.source(),
165        Some(released),
166        Some(omitted_declarations),
167        Some(omitted_capabilities),
168        Some(played_role_declarations),
169    )
170}
171
172/// Index every non-portable declaration and causally unresolved capability in
173/// one released-schema pass.
174///
175/// Descriptor generation is intentionally open-world, but retrying the whole
176/// parser once per missing reference makes a small partial export quadratic.
177/// This index mirrors the released merge algebra, validates the surviving
178/// direct role graph in memory, and returns original byte ranges for every
179/// declaration/fact the generator-only projection must omit plus the declaring
180/// role scope for valid inherited plays edges. Descriptor omissions are kept
181/// separate from the older render-only role repair so newly unsupported
182/// canonical identities cannot change released model bytes.
183#[derive(Default)]
184pub(crate) struct ReleasedReferenceProjection {
185    pub(crate) omitted_declarations: BTreeMap<usize, usize>,
186    pub(crate) omitted: BTreeMap<usize, usize>,
187    pub(crate) omitted_from_render: BTreeMap<usize, usize>,
188    pub(crate) played_role_declarations: BTreeMap<usize, String>,
189}
190
191pub(crate) fn released_unresolved_capability_ranges(
192    document: &DocumentId,
193    released: &ReleasedSyntax,
194) -> Result<ReleasedReferenceProjection, SchemaDiagnostics> {
195    let queries = typeql::parse_queries(released.source()).map_err(|parse_error| {
196        error(
197            DiagnosticCategory::InvalidContract,
198            "invalid_typeql_schema",
199            format!("TypeQL schema parsing failed: {parse_error}"),
200            None,
201        )
202    })?;
203    let mut definables = Vec::new();
204    for query in queries {
205        match query.structure {
206            QueryStructure::Schema(SchemaQuery::Define(define)) => {
207                definables.extend(define.definables);
208            }
209            _ => {
210                return Err(error(
211                    DiagnosticCategory::InvalidContract,
212                    "expected_typeql_define",
213                    "schema compatibility input must contain only define queries",
214                    query_span(document, released.original_source(), query.span)?,
215                ));
216            }
217        }
218    }
219    restore_released_labels(released, &mut definables);
220    let declarations = definables
221        .iter()
222        .filter_map(|definable| match definable {
223            Definable::TypeDeclaration(declaration) => Some(declaration),
224            _ => None,
225        })
226        .collect::<Vec<_>>();
227    let kinds = infer_type_kinds(document, released.original_source(), &declarations, true)?;
228    let mut omitted_declarations = BTreeMap::new();
229    let mut invalid_type_labels = BTreeSet::new();
230    for (declaration, kind) in declarations.iter().zip(&kinds) {
231        let label = typeql_label(&declaration.label);
232        let portable = TypeId::new(*kind, label.clone())
233            .and_then(TypeFact::new)
234            .is_ok();
235        if !portable {
236            invalid_type_labels.insert(label);
237            if let Some(span) = declaration.span {
238                omitted_declarations.insert(span.begin_offset, span.end_offset);
239            }
240        }
241    }
242    // Every reopening of a non-portable identity belongs to the same omitted
243    // declaration closure, including a kindless standalone `plays` line.
244    for declaration in &declarations {
245        if invalid_type_labels.contains(&typeql_label(&declaration.label))
246            && let Some(span) = declaration.span
247        {
248            omitted_declarations.insert(span.begin_offset, span.end_offset);
249        }
250    }
251    let ids = declarations
252        .iter()
253        .zip(&kinds)
254        .filter(|(declaration, _)| {
255            !declaration
256                .span
257                .is_some_and(|span| omitted_declarations.contains_key(&span.begin_offset))
258        })
259        .map(|(declaration, kind)| (typeql_label(&declaration.label), *kind))
260        .collect::<BTreeMap<_, _>>();
261
262    let mut last_attribute_declaration = BTreeMap::new();
263    let mut last_sub_capability = BTreeMap::new();
264    let mut last_value_capability = BTreeMap::new();
265    for (declaration_index, (declaration, kind)) in declarations.iter().zip(&kinds).enumerate() {
266        let label = typeql_label(&declaration.label);
267        if *kind == TypeKind::Attribute {
268            last_attribute_declaration.insert(label.clone(), declaration_index);
269        }
270        for (capability_index, capability) in declaration.capabilities.iter().enumerate() {
271            if matches!(capability.base, CapabilityBase::Sub(_)) {
272                last_sub_capability.insert(label.clone(), (declaration_index, capability_index));
273            }
274            if matches!(capability.base, CapabilityBase::ValueType(_)) {
275                last_value_capability.insert(label.clone(), (declaration_index, capability_index));
276            }
277        }
278    }
279
280    struct IndexedCapability<'a> {
281        owner: String,
282        owner_kind: TypeKind,
283        capability: &'a Capability,
284        start: usize,
285        end: usize,
286    }
287
288    let mut effective = Vec::new();
289    let mut first_object_capability = BTreeSet::new();
290    for (declaration_index, (declaration, kind)) in declarations.iter().zip(&kinds).enumerate() {
291        if declaration
292            .span
293            .is_some_and(|span| omitted_declarations.contains_key(&span.begin_offset))
294        {
295            continue;
296        }
297        let owner = typeql_label(&declaration.label);
298        if *kind == TypeKind::Attribute
299            && last_attribute_declaration.get(&owner) != Some(&declaration_index)
300        {
301            continue;
302        }
303        for (capability_index, capability) in declaration.capabilities.iter().enumerate() {
304            if matches!(capability.base, CapabilityBase::Sub(_))
305                && last_sub_capability.get(&owner) != Some(&(declaration_index, capability_index))
306            {
307                continue;
308            }
309            if matches!(capability.base, CapabilityBase::ValueType(_))
310                && last_value_capability.get(&owner) != Some(&(declaration_index, capability_index))
311            {
312                continue;
313            }
314            if *kind != TypeKind::Attribute
315                && let Some(identity) = released_object_capability_identity(capability)
316                && !first_object_capability.insert((owner.clone(), identity))
317            {
318                continue;
319            }
320            let Some(span) = capability.span else {
321                continue;
322            };
323            effective.push(IndexedCapability {
324                owner: owner.clone(),
325                owner_kind: *kind,
326                capability,
327                start: span.begin_offset,
328                end: span.end_offset,
329            });
330        }
331    }
332
333    let mut omitted = BTreeMap::new();
334    let mut parents = BTreeMap::<String, String>::new();
335    for indexed in &effective {
336        match &indexed.capability.base {
337            CapabilityBase::Sub(sub) => {
338                let parent = typeql_label(&sub.supertype_label);
339                if root_kind(&parent).is_some() {
340                    continue;
341                }
342                match ids.get(&parent) {
343                    None => {
344                        omitted.insert(indexed.start, indexed.end);
345                    }
346                    Some(parent_kind) if *parent_kind == indexed.owner_kind => {
347                        parents.insert(indexed.owner.clone(), parent);
348                    }
349                    Some(_) => {}
350                }
351            }
352            CapabilityBase::Owns(owns) => {
353                if let Ok((attribute, _)) = capability_type_ref(&owns.owned)
354                    && !ids.contains_key(&attribute)
355                {
356                    omitted.insert(indexed.start, indexed.end);
357                }
358            }
359            _ => {}
360        }
361    }
362
363    let mut roles = BTreeMap::<(String, String), ReleasedIndexedRole>::new();
364    let mut role_names_by_relation = BTreeMap::<String, Vec<String>>::new();
365    for (capability_index, indexed) in effective.iter().enumerate() {
366        let CapabilityBase::Relates(relates) = &indexed.capability.base else {
367            continue;
368        };
369        let Ok((role, _)) = capability_type_ref(&relates.related) else {
370            continue;
371        };
372        let specializes = relates.specialised.as_ref().and_then(|specialized| {
373            capability_type_ref(specialized)
374                .ok()
375                .map(|(label, _)| label)
376        });
377        let role_is_portable = Label::new(&role).is_ok()
378            && specializes
379                .as_ref()
380                .is_none_or(|label| Label::new(label).is_ok());
381        if !role_is_portable {
382            omitted.insert(indexed.start, indexed.end);
383            continue;
384        }
385        role_names_by_relation
386            .entry(indexed.owner.clone())
387            .or_default()
388            .push(role.clone());
389        roles.insert(
390            (indexed.owner.clone(), role),
391            ReleasedIndexedRole {
392                capability_index,
393                specializes,
394            },
395        );
396    }
397
398    let relation_names = ids
399        .iter()
400        .filter_map(|(name, kind)| (*kind == TypeKind::Relation).then_some(name.clone()))
401        .collect::<BTreeSet<_>>();
402    let mut played_role_names_by_relation = BTreeMap::<String, BTreeSet<String>>::new();
403    for indexed in &effective {
404        if let CapabilityBase::Plays(plays) = &indexed.capability.base {
405            let relation = typeql_label(&plays.role.scope);
406            let role = typeql_label(&plays.role.name);
407            if Label::new(&relation).is_err() || Label::new(&role).is_err() {
408                omitted.insert(indexed.start, indexed.end);
409                continue;
410            }
411            played_role_names_by_relation
412                .entry(relation)
413                .or_default()
414                .insert(role);
415        }
416    }
417    let role_resolution = released_role_validities(
418        &relation_names,
419        &role_names_by_relation,
420        &played_role_names_by_relation,
421        &roles,
422        &parents,
423    );
424    for (key, validity) in &role_resolution.direct {
425        if *validity == ReleasedRoleValidity::Invalid {
426            let indexed = &effective[roles[key].capability_index];
427            omitted.insert(indexed.start, indexed.end);
428        }
429    }
430
431    let mut omitted_from_render = BTreeMap::new();
432    for (key, validity) in &role_resolution.direct {
433        if *validity == ReleasedRoleValidity::Invalid {
434            let indexed = &effective[roles[key].capability_index];
435            omitted_from_render.insert(indexed.start, indexed.end);
436        }
437    }
438
439    let mut played_role_declarations = BTreeMap::new();
440    let mut first_portable_plays = BTreeSet::new();
441    for indexed in &effective {
442        let CapabilityBase::Plays(plays) = &indexed.capability.base else {
443            continue;
444        };
445        if omitted.contains_key(&indexed.start) {
446            continue;
447        }
448        let relation = typeql_label(&plays.role.scope);
449        let role = typeql_label(&plays.role.name);
450        match ids.get(&relation) {
451            None => {
452                omitted.insert(indexed.start, indexed.end);
453            }
454            Some(TypeKind::Relation) => {
455                match role_resolution.plays.get(&(relation.clone(), role.clone())) {
456                    Some(ReleasedRoleValidity::Valid) => {
457                        if let Some(declaration) =
458                            role_resolution.play_declarations.get(&(relation, role))
459                        {
460                            let identity = (
461                                indexed.owner.clone(),
462                                declaration.clone(),
463                                typeql_label(&plays.role.name),
464                            );
465                            if first_portable_plays.insert(identity) {
466                                played_role_declarations.insert(indexed.start, declaration.clone());
467                            } else {
468                                // Distinct released role refs may collapse to
469                                // the same inherited direct role identity.
470                                // Preserve the first frozen capability and
471                                // record the later alias as open-world evidence
472                                // instead of feeding a duplicate fact to the
473                                // canonical assembler.
474                                omitted.insert(indexed.start, indexed.end);
475                            }
476                        }
477                    }
478                    Some(ReleasedRoleValidity::Invalid) | None => {
479                        omitted.insert(indexed.start, indexed.end);
480                    }
481                    Some(ReleasedRoleValidity::Indeterminate) => {}
482                }
483            }
484            Some(_) => {}
485        }
486    }
487
488    Ok(ReleasedReferenceProjection {
489        omitted_declarations,
490        omitted,
491        omitted_from_render,
492        played_role_declarations,
493    })
494}
495
496#[derive(Clone, Copy, Debug, Eq, PartialEq)]
497enum ReleasedRoleValidity {
498    Valid,
499    Invalid,
500    Indeterminate,
501}
502
503#[derive(Clone, Debug)]
504struct ReleasedIndexedRole {
505    capability_index: usize,
506    specializes: Option<String>,
507}
508
509struct ReleasedRoleResolution {
510    direct: BTreeMap<(String, String), ReleasedRoleValidity>,
511    plays: BTreeMap<(String, String), ReleasedRoleValidity>,
512    play_declarations: BTreeMap<(String, String), String>,
513}
514
515/// Resolve direct role specializations in relation-tree order without
516/// recursion or repeated ancestry walks.
517///
518/// `direct_labels` indexes valid direct declarations on the current ancestor
519/// path. Specialization does not remove its target from this index: the
520/// canonical assembler binds `as <label>` to a direct ancestor declaration,
521/// including one already replaced in the effective role view.
522///
523/// `effective_labels` separately mirrors the frozen generator's inherited
524/// local role names. A valid specialization removes its immediate inherited
525/// target name and adds its local name; plays checks query that set after the
526/// relation's own declarations have been applied. Explicit exit frames
527/// restore both views before visiting a sibling.
528///
529/// This is O((relations + roles) log roles), uses a heap work stack for deep
530/// inheritance, and exactly models the released parser after invalid direct
531/// specializations have been omitted: an omitted role contributes no label
532/// for a descendant specialization to bind.
533fn released_role_validities(
534    relation_names: &BTreeSet<String>,
535    role_names_by_relation: &BTreeMap<String, Vec<String>>,
536    played_role_names_by_relation: &BTreeMap<String, BTreeSet<String>>,
537    roles: &BTreeMap<(String, String), ReleasedIndexedRole>,
538    parents: &BTreeMap<String, String>,
539) -> ReleasedRoleResolution {
540    let mut children = BTreeMap::<String, Vec<String>>::new();
541    let mut roots = Vec::new();
542    for relation in relation_names {
543        match parents
544            .get(relation)
545            .filter(|parent| relation_names.contains(*parent))
546        {
547            Some(parent) => children
548                .entry(parent.clone())
549                .or_default()
550                .push(relation.clone()),
551            None => roots.push(relation.clone()),
552        }
553    }
554
555    // Any relation not reachable from a root participates in, or descends
556    // from, an inheritance cycle. Preserve its capabilities as indeterminate
557    // so the canonical assembler reports the cycle instead of this
558    // compatibility index inventing a different omission.
559    let mut direct = roles
560        .keys()
561        .cloned()
562        .map(|key| (key, ReleasedRoleValidity::Indeterminate))
563        .collect::<BTreeMap<_, _>>();
564    let mut plays = played_role_names_by_relation
565        .iter()
566        .flat_map(|(relation, role_names)| {
567            role_names.iter().map(|role_name| {
568                (
569                    (relation.clone(), role_name.clone()),
570                    ReleasedRoleValidity::Indeterminate,
571                )
572            })
573        })
574        .collect::<BTreeMap<_, _>>();
575    let mut direct_labels = BTreeSet::<String>::new();
576    let mut effective_roles = BTreeMap::<String, String>::new();
577    enum Traversal {
578        Enter(String),
579        Exit {
580            direct: Vec<(String, bool)>,
581            effective: Vec<(String, Option<String>)>,
582        },
583    }
584    let mut work = roots
585        .into_iter()
586        .rev()
587        .map(Traversal::Enter)
588        .collect::<Vec<_>>();
589    let mut play_declarations = BTreeMap::new();
590
591    while let Some(frame) = work.pop() {
592        match frame {
593            Traversal::Enter(relation) => {
594                let mut additions = BTreeSet::new();
595                let mut removals = BTreeSet::new();
596                if let Some(role_names) = role_names_by_relation.get(&relation) {
597                    for role_name in role_names {
598                        let key = (relation.clone(), role_name.clone());
599                        let role = &roles[&key];
600                        let resolved = match role.specializes.as_ref() {
601                            Some(specialized) if direct_labels.contains(specialized) => {
602                                removals.insert(specialized.clone());
603                                ReleasedRoleValidity::Valid
604                            }
605                            Some(_) => ReleasedRoleValidity::Invalid,
606                            None => ReleasedRoleValidity::Valid,
607                        };
608                        direct.insert(key, resolved);
609                        if resolved == ReleasedRoleValidity::Valid {
610                            additions.insert(role_name.clone());
611                        }
612                    }
613                }
614
615                let affected = removals.union(&additions).cloned().collect::<Vec<_>>();
616                let mut previous_effective = Vec::with_capacity(affected.len());
617                for role_name in affected {
618                    let previous = effective_roles.get(&role_name).cloned();
619                    previous_effective.push((role_name.clone(), previous));
620                    if additions.contains(&role_name) {
621                        effective_roles.insert(role_name, relation.clone());
622                    } else {
623                        effective_roles.remove(&role_name);
624                    }
625                }
626                let mut previous_direct = Vec::with_capacity(additions.len());
627                for role_name in &additions {
628                    let was_visible = direct_labels.contains(role_name);
629                    previous_direct.push((role_name.clone(), was_visible));
630                    direct_labels.insert(role_name.clone());
631                }
632
633                if let Some(played_role_names) = played_role_names_by_relation.get(&relation) {
634                    for role_name in played_role_names {
635                        let declaration = effective_roles.get(role_name);
636                        plays.insert(
637                            (relation.clone(), role_name.clone()),
638                            if declaration.is_some() {
639                                ReleasedRoleValidity::Valid
640                            } else {
641                                ReleasedRoleValidity::Invalid
642                            },
643                        );
644                        if let Some(declaration) = declaration {
645                            play_declarations
646                                .insert((relation.clone(), role_name.clone()), declaration.clone());
647                        }
648                    }
649                }
650
651                work.push(Traversal::Exit {
652                    direct: previous_direct,
653                    effective: previous_effective,
654                });
655                if let Some(relation_children) = children.get(&relation) {
656                    work.extend(
657                        relation_children
658                            .iter()
659                            .rev()
660                            .cloned()
661                            .map(Traversal::Enter),
662                    );
663                }
664            }
665            Traversal::Exit { direct, effective } => {
666                for (role_name, was_visible) in direct {
667                    if was_visible {
668                        direct_labels.insert(role_name);
669                    } else {
670                        direct_labels.remove(&role_name);
671                    }
672                }
673                for (role_name, previous) in effective {
674                    if let Some(declaration) = previous {
675                        effective_roles.insert(role_name, declaration);
676                    } else {
677                        effective_roles.remove(&role_name);
678                    }
679                }
680            }
681        }
682    }
683
684    ReleasedRoleResolution {
685        direct,
686        plays,
687        play_declarations,
688    }
689}
690
691fn typeql_to_declared_with_references_impl(
692    document: DocumentId,
693    source: &str,
694    parser_source: &str,
695    released: Option<&ReleasedSyntax>,
696    omitted_released_declarations: Option<&BTreeSet<usize>>,
697    omitted_released_capabilities: Option<&BTreeSet<usize>>,
698    played_role_declarations: Option<&BTreeMap<usize, String>>,
699) -> Result<TypeqlDeclaredSchema, SchemaDiagnostics> {
700    // Released schema sources may carry several define blocks; every query
701    // must still be a define, and their definables merge in source order.
702    let queries = typeql::parse_queries(parser_source).map_err(|parse_error| {
703        error(
704            DiagnosticCategory::InvalidContract,
705            "invalid_typeql_schema",
706            format!("TypeQL schema parsing failed: {parse_error}"),
707            None,
708        )
709    })?;
710    if queries.is_empty() {
711        return Err(error(
712            DiagnosticCategory::InvalidContract,
713            "expected_typeql_define",
714            "schema compatibility input must contain at least one define query",
715            None,
716        ));
717    }
718    let mut definables = Vec::new();
719    for query in queries {
720        match query.structure {
721            QueryStructure::Schema(SchemaQuery::Define(define)) => {
722                definables.extend(define.definables);
723            }
724            _ => {
725                return Err(error(
726                    DiagnosticCategory::InvalidContract,
727                    "expected_typeql_define",
728                    "schema compatibility input must contain only define queries",
729                    query_span(&document, source, query.span)?,
730                ));
731            }
732        }
733    }
734    if let Some(released) = released {
735        restore_released_labels(released, &mut definables);
736    }
737
738    let declarations: Vec<&TypeDeclaration> = definables
739        .iter()
740        .filter_map(|definable| match definable {
741            Definable::TypeDeclaration(declaration) => Some(declaration),
742            _ => None,
743        })
744        .collect();
745    let kinds = infer_type_kinds(&document, source, &declarations, released.is_some())?;
746    let mut ids = BTreeMap::new();
747    let mut assembler = FactAssembler::new(FormatVersion::V1);
748    let mut function_body_references = BTreeMap::new();
749
750    for (declaration, kind) in declarations.iter().zip(&kinds) {
751        if released_declaration_is_omitted(declaration, omitted_released_declarations) {
752            continue;
753        }
754        let label = typeql_label(&declaration.label);
755        let declaration_span = source_span(&document, source, declaration.span)?;
756        let id = TypeId::new(*kind, label.clone())
757            .map_err(|diagnostic| contract(diagnostic, declaration_span.clone()))?;
758        // Released renders re-open declared labels freely — kindless
759        // standalone `plays` lines and explicit split declarations alike —
760        // in any order. Every compatible re-opening merges into the one
761        // identity; genuinely conflicting kinds were already rejected by
762        // `infer_type_kinds` with both spans.
763        if ids.contains_key(&label) {
764            continue;
765        }
766        assembler.insert_fact(
767            SchemaFact::Type(
768                TypeFact::new(id.clone())
769                    .map_err(|diagnostic| contract(diagnostic, declaration_span.clone()))?,
770            ),
771            declaration_span,
772        )?;
773        ids.entry(label).or_insert(id);
774    }
775
776    if released.is_none() {
777        // Keep the strict adapter's original fact-insertion order and
778        // duplicate diagnostics. Released merge behavior belongs only to the
779        // compatibility projection below.
780        for declaration in &declarations {
781            let label = typeql_label(&declaration.label);
782            let id = ids.get(&label).cloned().ok_or_else(|| {
783                error(
784                    DiagnosticCategory::InvalidContract,
785                    "unknown_typeql_type",
786                    format!("TypeQL declaration `{label}` has no inferred type identity"),
787                    query_span(&document, source, declaration.span)
788                        .ok()
789                        .flatten(),
790                )
791            })?;
792            insert_annotations(
793                &mut assembler,
794                AnnotationSubjectId::Type(id.clone()),
795                &declaration.annotations,
796                &document,
797                source,
798            )?;
799            for capability in &declaration.capabilities {
800                insert_capability(
801                    &mut assembler,
802                    &ids,
803                    &id,
804                    capability,
805                    CapabilityAnnotations::Strict(&capability.annotations),
806                    &document,
807                    source,
808                )?;
809            }
810        }
811    } else {
812        // Reproduce the released parser's merge algebra before entering the
813        // strict fact assembler. Attribute declarations are map replacements
814        // (the final declaration wins); entity and relation declarations merge,
815        // with type annotations accumulated under their own last-write/OR
816        // identities and `sub` taking the final spelling.
817        let mut last_attribute_declaration = BTreeMap::new();
818        let mut last_sub_capability = BTreeMap::new();
819        let mut last_value_capability = BTreeMap::new();
820        for (declaration_index, (declaration, kind)) in declarations.iter().zip(&kinds).enumerate()
821        {
822            if released_declaration_is_omitted(declaration, omitted_released_declarations) {
823                continue;
824            }
825            let label = typeql_label(&declaration.label);
826            if *kind == TypeKind::Attribute {
827                last_attribute_declaration.insert(label.clone(), declaration_index);
828            }
829            for (capability_index, capability) in declaration.capabilities.iter().enumerate() {
830                if matches!(&capability.base, CapabilityBase::Sub(_)) {
831                    last_sub_capability
832                        .insert(label.clone(), (declaration_index, capability_index));
833                }
834                if matches!(&capability.base, CapabilityBase::ValueType(_)) {
835                    last_value_capability
836                        .insert(label.clone(), (declaration_index, capability_index));
837                }
838            }
839        }
840
841        let mut merged_type_annotations: BTreeMap<String, Vec<Annotation>> = BTreeMap::new();
842        let mut merged_value_annotations: BTreeMap<String, Vec<Annotation>> = BTreeMap::new();
843        for (declaration_index, (declaration, kind)) in declarations.iter().zip(&kinds).enumerate()
844        {
845            if released_declaration_is_omitted(declaration, omitted_released_declarations) {
846                continue;
847            }
848            let label = typeql_label(&declaration.label);
849            if *kind == TypeKind::Attribute
850                && last_attribute_declaration.get(&label) != Some(&declaration_index)
851            {
852                continue;
853            }
854            if let Some(released) = released {
855                for annotation in declaration.annotations.iter().chain(
856                    declaration
857                        .capabilities
858                        .iter()
859                        .flat_map(|capability| &capability.annotations),
860                ) {
861                    match released_annotation_target(released, annotation) {
862                        Some(ReleasedAnnotationTarget::Value) => merged_value_annotations
863                            .entry(label.clone())
864                            .or_default()
865                            .push(annotation.clone()),
866                        Some(ReleasedAnnotationTarget::Type) => merged_type_annotations
867                            .entry(label.clone())
868                            .or_default()
869                            .push(annotation.clone()),
870                        Some(ReleasedAnnotationTarget::Capability) => {}
871                        None if *kind == TypeKind::Attribute => {
872                            let target = released_attribute_annotation_target(annotation);
873                            let annotations = match target {
874                                ReleasedAnnotationTarget::Value => &mut merged_value_annotations,
875                                ReleasedAnnotationTarget::Type => &mut merged_type_annotations,
876                                ReleasedAnnotationTarget::Capability => continue,
877                            };
878                            annotations
879                                .entry(label.clone())
880                                .or_default()
881                                .push(annotation.clone());
882                        }
883                        None => {}
884                    }
885                }
886            } else {
887                merged_type_annotations
888                    .entry(label)
889                    .or_default()
890                    .extend(declaration.annotations.iter().cloned());
891            }
892        }
893        for (label, annotations) in merged_type_annotations {
894            let id = ids.get(&label).cloned().ok_or_else(|| {
895                error(
896                    DiagnosticCategory::InvalidContract,
897                    "unknown_typeql_type",
898                    format!("TypeQL declaration `{label}` has no inferred type identity"),
899                    None,
900                )
901            })?;
902            insert_released_annotations(
903                &mut assembler,
904                AnnotationSubjectId::Type(id.clone()),
905                &annotations,
906                &document,
907                source,
908            )?;
909        }
910
911        // The released generator observes the first owns/plays/relates capability
912        // by identity, including that capability's annotations. Its parser keeps
913        // duplicates in the initial declaration internally, but every released
914        // emitter resolves the ordered name back to the first matching capability;
915        // compatible projection therefore deduplicates both within one declaration
916        // and across later reopenings. Attribute declarations are replacements,
917        // and within the final declaration their last `sub` and `value` clauses win.
918        let mut first_object_capability = BTreeMap::new();
919        for (declaration_index, (declaration, kind)) in declarations.iter().zip(&kinds).enumerate()
920        {
921            if released_declaration_is_omitted(declaration, omitted_released_declarations) {
922                continue;
923            }
924            let label = typeql_label(&declaration.label);
925            if *kind == TypeKind::Attribute
926                && last_attribute_declaration.get(&label) != Some(&declaration_index)
927            {
928                continue;
929            }
930            let id = ids.get(&label).cloned().ok_or_else(|| {
931                error(
932                    DiagnosticCategory::InvalidContract,
933                    "unknown_typeql_type",
934                    format!("TypeQL declaration `{label}` has no inferred type identity"),
935                    query_span(&document, source, declaration.span)
936                        .ok()
937                        .flatten(),
938                )
939            })?;
940            for (capability_index, capability) in declaration.capabilities.iter().enumerate() {
941                if matches!(&capability.base, CapabilityBase::Sub(_))
942                    && last_sub_capability.get(&label)
943                        != Some(&(declaration_index, capability_index))
944                {
945                    continue;
946                }
947                if matches!(&capability.base, CapabilityBase::ValueType(_))
948                    && last_value_capability.get(&label)
949                        != Some(&(declaration_index, capability_index))
950                {
951                    continue;
952                }
953                if *kind != TypeKind::Attribute
954                    && let Some(capability_id) = released_object_capability_identity(capability)
955                {
956                    let key = (label.clone(), capability_id);
957                    if first_object_capability.contains_key(&key) {
958                        continue;
959                    }
960                    first_object_capability.insert(key, (declaration_index, capability_index));
961                }
962                if omitted_released_capabilities.is_some_and(|omitted| {
963                    capability
964                        .span
965                        .is_some_and(|span| omitted.contains(&span.begin_offset))
966                }) {
967                    continue;
968                }
969                let released_annotations;
970                let annotations = if let Some(released) = released {
971                    released_annotations = capability
972                        .annotations
973                        .iter()
974                        .filter(|annotation| {
975                            released_annotation_target(released, annotation)
976                                == Some(ReleasedAnnotationTarget::Capability)
977                        })
978                        .cloned()
979                        .collect::<Vec<_>>();
980                    released_annotations.as_slice()
981                } else {
982                    capability.annotations.as_slice()
983                };
984                insert_capability(
985                    &mut assembler,
986                    &ids,
987                    &id,
988                    capability,
989                    CapabilityAnnotations::Released {
990                        annotations,
991                        played_role_declaration: capability.span.and_then(|span| {
992                            played_role_declarations
993                                .and_then(|declarations| declarations.get(&span.begin_offset))
994                                .map(String::as_str)
995                        }),
996                    },
997                    &document,
998                    source,
999                )?;
1000            }
1001        }
1002
1003        for (label, annotations) in merged_value_annotations {
1004            let Some(first) = annotations.first() else {
1005                continue;
1006            };
1007            let annotation_span = source_span(&document, source, first.span())?;
1008            let attribute = AttributeId::new(&label)
1009                .map_err(|diagnostic| contract(diagnostic, annotation_span))?;
1010            insert_released_annotations(
1011                &mut assembler,
1012                AnnotationSubjectId::Value(ValueFactId::new(attribute)),
1013                &annotations,
1014                &document,
1015                source,
1016            )?;
1017        }
1018    }
1019
1020    for definable in &definables {
1021        match definable {
1022            Definable::TypeDeclaration(_) => {}
1023            Definable::Struct(structure) => {
1024                insert_struct(&mut assembler, structure, &document, source)?;
1025            }
1026            Definable::Function(function) => {
1027                insert_function(&mut assembler, function, &document, source)?;
1028                let function_id = FunctionId::new(function.signature.ident.as_str_unchecked())
1029                    .expect("TypeQL emitted a function identifier rejected by the contract");
1030                let body_span = source_span(&document, source, function.block.span)?;
1031                let references =
1032                    function_references::collect_function_body_references(&function.block)
1033                        .map_err(|diagnostic| contract(diagnostic, body_span))?;
1034                function_body_references.insert(function_id, references);
1035            }
1036        }
1037    }
1038
1039    assembler
1040        .finish()
1041        .map(|declared| TypeqlDeclaredSchema::new(declared, function_body_references))
1042}
1043
1044fn restore_released_labels(released: &ReleasedSyntax, definables: &mut [Definable]) {
1045    for definable in definables {
1046        if let Definable::TypeDeclaration(declaration) = definable {
1047            released.restore_label(&mut declaration.label);
1048        }
1049    }
1050}
1051
1052/// Parse one TypeQL `define` query into the canonical declared schema graph.
1053///
1054/// This compatibility wrapper discards only the derived function-body index;
1055/// declared facts and their fingerprints are unchanged.
1056pub fn typeql_to_declared(
1057    document: DocumentId,
1058    source: &str,
1059) -> Result<DeclaredSchema, SchemaDiagnostics> {
1060    typeql_to_declared_with_references(document, source).map(TypeqlDeclaredSchema::into_declared)
1061}
1062
1063/// Parse TypeQL and return canonical direct facts in identity order.
1064pub fn typeql_to_facts(
1065    document: DocumentId,
1066    source: &str,
1067) -> Result<Vec<SchemaFact>, SchemaDiagnostics> {
1068    let declared = typeql_to_declared(document, source)?;
1069    Ok(declared.facts().cloned().collect())
1070}
1071
1072/// Transpile one legacy TOML schema and adapt its rendered TypeQL into the
1073/// canonical declared schema graph.
1074///
1075/// `rendered_typeql_document` identifies the generated TypeQL, not the input
1076/// TOML. Diagnostics from TypeQL parsing and adaptation therefore contain
1077/// offsets into the rendered source and never claim to locate original TOML
1078/// text. TOML decoding and validation diagnostics have no source span because
1079/// the legacy transpiler does not expose structured TOML locations.
1080pub fn toml_to_declared(
1081    rendered_typeql_document: DocumentId,
1082    toml_source: &str,
1083) -> Result<DeclaredSchema, SchemaDiagnostics> {
1084    let rendered_typeql =
1085        type_bridge_toml_transpiler::toml_to_typeql(toml_source).map_err(|transpile_error| {
1086            error(
1087                DiagnosticCategory::InvalidContract,
1088                "invalid_toml_schema",
1089                format!("TOML schema transpilation failed: {transpile_error}"),
1090                None,
1091            )
1092        })?;
1093    typeql_to_declared(rendered_typeql_document, &rendered_typeql)
1094}
1095
1096/// Transpile legacy TOML and return canonical direct facts in identity order.
1097///
1098/// See [`toml_to_declared`] for the provenance contract of
1099/// `rendered_typeql_document`.
1100pub fn toml_to_facts(
1101    rendered_typeql_document: DocumentId,
1102    toml_source: &str,
1103) -> Result<Vec<SchemaFact>, SchemaDiagnostics> {
1104    let declared = toml_to_declared(rendered_typeql_document, toml_source)?;
1105    Ok(declared.facts().cloned().collect())
1106}
1107
1108#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
1109enum ReleasedObjectCapabilityIdentity {
1110    Owns(String),
1111    Relates(String),
1112    Plays(String, String),
1113}
1114
1115fn released_declaration_is_omitted(
1116    declaration: &TypeDeclaration,
1117    omitted: Option<&BTreeSet<usize>>,
1118) -> bool {
1119    omitted.is_some_and(|omitted| {
1120        declaration
1121            .span
1122            .is_some_and(|span| omitted.contains(&span.begin_offset))
1123    })
1124}
1125
1126fn released_object_capability_identity(
1127    capability: &Capability,
1128) -> Option<ReleasedObjectCapabilityIdentity> {
1129    match &capability.base {
1130        CapabilityBase::Owns(owns) => capability_type_ref(&owns.owned)
1131            .ok()
1132            .map(|(label, _)| label)
1133            .map(ReleasedObjectCapabilityIdentity::Owns),
1134        CapabilityBase::Relates(relates) => capability_type_ref(&relates.related)
1135            .ok()
1136            .map(|(label, _)| label)
1137            .map(ReleasedObjectCapabilityIdentity::Relates),
1138        CapabilityBase::Plays(plays) => Some(ReleasedObjectCapabilityIdentity::Plays(
1139            typeql_label(&plays.role.scope),
1140            typeql_label(&plays.role.name),
1141        )),
1142        _ => None,
1143    }
1144}
1145
1146fn released_annotation_target(
1147    released: &ReleasedSyntax,
1148    annotation: &Annotation,
1149) -> Option<ReleasedAnnotationTarget> {
1150    annotation
1151        .span()
1152        .map(|span| span.begin_offset)
1153        .and_then(|start| released.annotation_target(start))
1154}
1155
1156fn released_attribute_annotation_target(annotation: &Annotation) -> ReleasedAnnotationTarget {
1157    match annotation {
1158        Annotation::Regex(_) | Annotation::Range(_) | Annotation::Values(_) => {
1159            ReleasedAnnotationTarget::Value
1160        }
1161        _ => ReleasedAnnotationTarget::Type,
1162    }
1163}
1164
1165fn infer_type_kinds(
1166    document: &DocumentId,
1167    source: &str,
1168    declarations: &[&TypeDeclaration],
1169    released: bool,
1170) -> Result<Vec<TypeKind>, SchemaDiagnostics> {
1171    let mut inferred = Vec::with_capacity(declarations.len());
1172    for declaration in declarations {
1173        let mut kind = declaration
1174            .kind
1175            .as_ref()
1176            .map(|kind| kind_from_token(&kind.to_string()))
1177            .transpose()
1178            .map_err(|message| {
1179                at(
1180                    document,
1181                    source,
1182                    declaration.span,
1183                    "unsupported_typeql_kind",
1184                    message,
1185                )
1186            })?;
1187        for capability in &declaration.capabilities {
1188            let hint = match &capability.base {
1189                CapabilityBase::ValueType(_) => Some(TypeKind::Attribute),
1190                CapabilityBase::Relates(_) => Some(TypeKind::Relation),
1191                CapabilityBase::Sub(sub) => root_kind(&typeql_label(&sub.supertype_label)),
1192                _ => None,
1193            };
1194            if let Some(hint) = hint {
1195                merge_kind(&mut kind, hint).map_err(|message| {
1196                    at(
1197                        document,
1198                        source,
1199                        capability.span,
1200                        "conflicting_typeql_kind",
1201                        message,
1202                    )
1203                })?;
1204            }
1205        }
1206        inferred.push(kind);
1207    }
1208
1209    loop {
1210        let mut known = BTreeMap::new();
1211        for (declaration, kind) in declarations.iter().zip(&inferred) {
1212            if let Some(kind) = kind {
1213                let label = typeql_label(&declaration.label);
1214                if let Some(previous) = known.insert(label.clone(), *kind)
1215                    && previous != *kind
1216                {
1217                    return Err(at(
1218                        document,
1219                        source,
1220                        declaration.span,
1221                        "conflicting_typeql_kind",
1222                        format!("TypeQL label `{label}` is declared with incompatible kinds"),
1223                    ));
1224                }
1225            }
1226        }
1227        let mut changed = false;
1228        for (index, declaration) in declarations.iter().enumerate() {
1229            if inferred[index].is_some() {
1230                continue;
1231            }
1232            // A kindless statement re-opening an already-classified label
1233            // (released renders emit standalone `plays` lines this way)
1234            // inherits that label's kind.
1235            if let Some(own_kind) = known.get(&typeql_label(&declaration.label)) {
1236                inferred[index] = Some(*own_kind);
1237                changed = true;
1238                continue;
1239            }
1240            for capability in &declaration.capabilities {
1241                let CapabilityBase::Sub(sub) = &capability.base else {
1242                    continue;
1243                };
1244                if let Some(parent_kind) = known.get(&typeql_label(&sub.supertype_label)) {
1245                    inferred[index] = Some(*parent_kind);
1246                    changed = true;
1247                    break;
1248                }
1249            }
1250        }
1251        if !changed {
1252            break;
1253        }
1254    }
1255
1256    if released {
1257        for (declaration, kind) in declarations.iter().zip(&mut inferred) {
1258            let plays_only = declaration.kind.is_none()
1259                && !declaration.capabilities.is_empty()
1260                && declaration
1261                    .capabilities
1262                    .iter()
1263                    .all(|capability| matches!(capability.base, CapabilityBase::Plays(_)));
1264            if !plays_only {
1265                continue;
1266            }
1267            match kind {
1268                Some(TypeKind::Attribute) => {
1269                    return Err(at(
1270                        document,
1271                        source,
1272                        declaration.span,
1273                        "conflicting_typeql_kind",
1274                        format!(
1275                            "released standalone plays label `{}` conflicts with an attribute declaration",
1276                            typeql_label(&declaration.label)
1277                        ),
1278                    ));
1279                }
1280                Some(TypeKind::Entity | TypeKind::Relation) => {}
1281                Some(TypeKind::Struct) => {
1282                    return Err(at(
1283                        document,
1284                        source,
1285                        declaration.span,
1286                        "conflicting_typeql_kind",
1287                        format!(
1288                            "released standalone plays label `{}` cannot resolve to a struct",
1289                            typeql_label(&declaration.label)
1290                        ),
1291                    ));
1292                }
1293                None => *kind = Some(TypeKind::Entity),
1294            }
1295        }
1296    }
1297
1298    declarations
1299        .iter()
1300        .zip(inferred)
1301        .map(|(declaration, kind)| {
1302            kind.ok_or_else(|| {
1303                at(
1304                    document,
1305                    source,
1306                    declaration.span,
1307                    "ambiguous_typeql_kind",
1308                    format!(
1309                        "TypeQL declaration `{}` does not identify an entity, relation, or attribute kind",
1310                        typeql_label(&declaration.label)
1311                    ),
1312                )
1313            })
1314        })
1315        .collect()
1316}
1317
1318#[derive(Clone, Copy)]
1319enum CapabilityAnnotations<'a> {
1320    Strict(&'a [Annotation]),
1321    Released {
1322        annotations: &'a [Annotation],
1323        played_role_declaration: Option<&'a str>,
1324    },
1325}
1326
1327impl CapabilityAnnotations<'_> {
1328    const fn annotations(&self) -> &[Annotation] {
1329        match self {
1330            Self::Strict(annotations) | Self::Released { annotations, .. } => annotations,
1331        }
1332    }
1333
1334    const fn played_role_declaration(&self) -> Option<&str> {
1335        match self {
1336            Self::Strict(_) => None,
1337            Self::Released {
1338                played_role_declaration,
1339                ..
1340            } => *played_role_declaration,
1341        }
1342    }
1343}
1344
1345fn insert_capability(
1346    assembler: &mut FactAssembler,
1347    ids: &BTreeMap<String, TypeId>,
1348    owner: &TypeId,
1349    capability: &Capability,
1350    annotations: CapabilityAnnotations<'_>,
1351    document: &DocumentId,
1352    source: &str,
1353) -> Result<(), SchemaDiagnostics> {
1354    let annotation_slice = annotations.annotations();
1355    let capability_span = source_span(document, source, capability.span)?;
1356    let subject = match &capability.base {
1357        CapabilityBase::Sub(sub) => {
1358            let parent_label = typeql_label(&sub.supertype_label);
1359            if root_kind(&parent_label).is_some() {
1360                reject_annotations_if_present(annotation_slice, document, source)?;
1361                return Ok(());
1362            }
1363            let parent = ids.get(&parent_label).cloned().ok_or_else(|| {
1364                at(
1365                    document,
1366                    source,
1367                    sub.span,
1368                    "unknown_typeql_parent",
1369                    format!("unknown TypeQL parent `{parent_label}`"),
1370                )
1371            })?;
1372            let id = SubFactId::new(owner.clone(), parent)
1373                .map_err(|diagnostic| contract(diagnostic, capability_span.clone()))?;
1374            assembler.insert_fact(SchemaFact::Sub(SubFact::new(id.clone())), capability_span)?;
1375            AnnotationSubjectId::Sub(id)
1376        }
1377        CapabilityBase::ValueType(value) => {
1378            let value_type = named_value_type(&value.value_type).map_err(|message| {
1379                at(
1380                    document,
1381                    source,
1382                    value.span,
1383                    "unsupported_typeql_value_type",
1384                    message,
1385                )
1386            })?;
1387            let attribute = AttributeId::new(owner.label().as_str())
1388                .map_err(|diagnostic| contract(diagnostic, capability_span.clone()))?;
1389            let id = ValueFactId::new(attribute);
1390            assembler.insert_fact(
1391                SchemaFact::Value(ValueFact::new(id.clone(), value_type)),
1392                capability_span,
1393            )?;
1394            AnnotationSubjectId::Value(id)
1395        }
1396        CapabilityBase::Owns(owns) => {
1397            let (attribute_label, collection_mode) =
1398                capability_type_ref(&owns.owned).map_err(|message| {
1399                    at(
1400                        document,
1401                        source,
1402                        owns.span,
1403                        "unsupported_typeql_owns",
1404                        message,
1405                    )
1406                })?;
1407            let attribute_type = ids.get(&attribute_label).ok_or_else(|| {
1408                at(
1409                    document,
1410                    source,
1411                    owns.span,
1412                    "unknown_typeql_attribute",
1413                    format!("unknown owned attribute `{attribute_label}`"),
1414                )
1415            })?;
1416            if attribute_type.kind() != TypeKind::Attribute {
1417                return Err(at(
1418                    document,
1419                    source,
1420                    owns.span,
1421                    "invalid_typeql_owned_kind",
1422                    "TypeQL owns targets must be attribute types",
1423                ));
1424            }
1425            let attribute = AttributeId::new(attribute_label)
1426                .map_err(|diagnostic| contract(diagnostic, capability_span.clone()))?;
1427            let id = OwnsFactId::new(owner.clone(), attribute)
1428                .map_err(|diagnostic| contract(diagnostic, capability_span.clone()))?;
1429            assembler.insert_fact(
1430                SchemaFact::Owns(OwnsFact::new_with_collection_mode(
1431                    id.clone(),
1432                    collection_mode,
1433                )),
1434                capability_span,
1435            )?;
1436            AnnotationSubjectId::Owns(id)
1437        }
1438        CapabilityBase::Relates(relates) => {
1439            let (role_label, collection_mode) =
1440                capability_type_ref(&relates.related).map_err(|message| {
1441                    at(
1442                        document,
1443                        source,
1444                        relates.span,
1445                        "unsupported_typeql_relates",
1446                        message,
1447                    )
1448                })?;
1449            let role = RoleId::new(owner.label().as_str(), &role_label)
1450                .map_err(|diagnostic| contract(diagnostic, capability_span.clone()))?;
1451            let id = RelatesFactId::new(owner.clone(), role)
1452                .map_err(|diagnostic| contract(diagnostic, capability_span.clone()))?;
1453            let specializes = relates
1454                .specialised
1455                .as_ref()
1456                .map(|specialized| {
1457                    capability_type_ref(specialized)
1458                        .map(|(label, _)| label)
1459                        .map_err(|message| {
1460                            at(
1461                                document,
1462                                source,
1463                                specialized.span(),
1464                                "unsupported_typeql_role_specialization",
1465                                message,
1466                            )
1467                        })
1468                        .and_then(|label| {
1469                            let span = source_span(document, source, specialized.span())?;
1470                            let label = Label::new(label)
1471                                .map_err(|diagnostic| contract(diagnostic, span.clone()))?;
1472                            Ok((label, span))
1473                        })
1474                })
1475                .transpose()?;
1476            assembler.insert_relates_with_collection_mode(
1477                id.clone(),
1478                specializes,
1479                collection_mode,
1480                capability_span,
1481            )?;
1482            AnnotationSubjectId::Relates(id)
1483        }
1484        CapabilityBase::Plays(plays) => {
1485            let relation_label = annotations
1486                .played_role_declaration()
1487                .map(str::to_owned)
1488                .unwrap_or_else(|| typeql_label(&plays.role.scope));
1489            let role_label = typeql_label(&plays.role.name);
1490            let relation = Label::new(&relation_label)
1491                .map_err(|diagnostic| contract(diagnostic, capability_span.clone()))?;
1492            let role = Label::new(&role_label)
1493                .map_err(|diagnostic| contract(diagnostic, capability_span.clone()))?;
1494            assembler.insert_plays(
1495                owner.label().clone(),
1496                relation,
1497                role,
1498                capability_span.clone(),
1499            );
1500            let role_id = RoleId::new(relation_label, role_label)
1501                .map_err(|diagnostic| contract(diagnostic, capability_span.clone()))?;
1502            let id = PlaysFactId::new(owner.clone(), role_id)
1503                .map_err(|diagnostic| contract(diagnostic, capability_span.clone()))?;
1504            AnnotationSubjectId::Plays(id)
1505        }
1506        CapabilityBase::Alias(alias) => {
1507            return Err(at(
1508                document,
1509                source,
1510                alias.span,
1511                "unsupported_typeql_alias",
1512                "TypeQL aliases require an explicit capability contract",
1513            ));
1514        }
1515    };
1516
1517    match annotations {
1518        CapabilityAnnotations::Strict(_) => {
1519            insert_annotations(assembler, subject, annotation_slice, document, source)
1520        }
1521        CapabilityAnnotations::Released { .. } => {
1522            insert_released_annotations(assembler, subject, annotation_slice, document, source)
1523        }
1524    }
1525}
1526
1527fn insert_struct(
1528    assembler: &mut FactAssembler,
1529    structure: &Struct,
1530    document: &DocumentId,
1531    source: &str,
1532) -> Result<(), SchemaDiagnostics> {
1533    if let Some(annotation) = structure.annotations.first() {
1534        return Err(at(
1535            document,
1536            source,
1537            annotation.span(),
1538            "unsupported_typeql_struct_annotation",
1539            "struct annotations are not part of the portable V2 fact contract",
1540        ));
1541    }
1542    let structure_span = source_span(document, source, structure.span)?;
1543    let id = StructId::new(structure.ident.as_str_unchecked())
1544        .map_err(|diagnostic| contract(diagnostic, structure_span.clone()))?;
1545    let mut fields = Vec::with_capacity(structure.fields.len());
1546    for field in &structure.fields {
1547        if let Some(annotation) = field.annotations.first() {
1548            return Err(at(
1549                document,
1550                source,
1551                annotation.span(),
1552                "unsupported_typeql_struct_field_annotation",
1553                "struct field annotations are not live-pinned for the portable contract",
1554            ));
1555        }
1556        let (named, optional) = simple_or_optional_named(&field.type_).map_err(|message| {
1557            at(
1558                document,
1559                source,
1560                field.span,
1561                "unsupported_typeql_struct_field",
1562                message,
1563            )
1564        })?;
1565        let value_type = named_value_type(named).map_err(|message| {
1566            at(
1567                document,
1568                source,
1569                field.span,
1570                "unsupported_typeql_struct_field",
1571                message,
1572            )
1573        })?;
1574        let field_span = source_span(document, source, field.span)?;
1575        let name = Label::new(field.key.as_str_unchecked())
1576            .map_err(|diagnostic| contract(diagnostic, field_span))?;
1577        fields.push(StructField::new(name, value_type, optional));
1578    }
1579    let fact = StructFact::new(id, fields)
1580        .map_err(|diagnostic| contract(diagnostic, structure_span.clone()))?;
1581    assembler.insert_fact(SchemaFact::Struct(fact), structure_span)
1582}
1583
1584fn insert_function(
1585    assembler: &mut FactAssembler,
1586    function: &Function,
1587    document: &DocumentId,
1588    source: &str,
1589) -> Result<(), SchemaDiagnostics> {
1590    let function_span = source_span(document, source, function.span)?;
1591    let id = FunctionId::new(function.signature.ident.as_str_unchecked())
1592        .map_err(|diagnostic| contract(diagnostic, function_span.clone()))?;
1593    let mut parameters = Vec::with_capacity(function.signature.args.len());
1594    for argument in &function.signature.args {
1595        let (named, optional) = simple_or_optional_named(&argument.type_).map_err(|message| {
1596            at(
1597                document,
1598                source,
1599                argument.span,
1600                "unsupported_typeql_function_parameter",
1601                message,
1602            )
1603        })?;
1604        if optional {
1605            return Err(at(
1606                document,
1607                source,
1608                argument.span,
1609                "unsupported_typeql_function_parameter",
1610                "optional function parameters are not part of the V2 signature contract",
1611            ));
1612        }
1613        let name = argument.var.name().ok_or_else(|| {
1614            at(
1615                document,
1616                source,
1617                argument.span,
1618                "anonymous_typeql_function_parameter",
1619                "function parameters must use named variables",
1620            )
1621        })?;
1622        let argument_span = source_span(document, source, argument.span)?;
1623        let name = Label::new(name).map_err(|diagnostic| contract(diagnostic, argument_span))?;
1624        parameters.push(FunctionParameter::new(name, type_reference(named)?));
1625    }
1626    let returns = match &function.signature.output {
1627        Output::Single(single) => {
1628            let elements = return_elements(&single.types, document, source, single.span)?;
1629            if elements.len() == 1 {
1630                FunctionReturnMode::scalar(elements.into_iter().next().expect("one return element"))
1631            } else {
1632                FunctionReturnMode::tuple(elements)
1633                    .map_err(|diagnostic| contract(diagnostic, function_span.clone()))?
1634            }
1635        }
1636        Output::Stream(stream) => FunctionReturnMode::stream(return_elements(
1637            &stream.types,
1638            document,
1639            source,
1640            stream.span,
1641        )?)
1642        .map_err(|diagnostic| contract(diagnostic, function_span.clone()))?,
1643    };
1644    let signature = FunctionSignature::new(parameters, returns)
1645        .map_err(|diagnostic| contract(diagnostic, function_span.clone()))?;
1646    let block_span = function.block.span.ok_or_else(|| {
1647        error(
1648            DiagnosticCategory::InvalidContract,
1649            "missing_typeql_function_body_span",
1650            "TypeQL parser did not retain the function body span",
1651            Some(function_span.clone()),
1652        )
1653    })?;
1654    let body_text = source
1655        .get(block_span.begin_offset..block_span.end_offset)
1656        .ok_or_else(|| {
1657            error(
1658                DiagnosticCategory::InvalidContract,
1659                "invalid_typeql_function_body_span",
1660                "TypeQL function body span is outside the original source",
1661                Some(function_span.clone()),
1662            )
1663        })?;
1664    let body_span = source_span(document, source, Some(block_span))?;
1665    let body =
1666        FunctionBody::new(body_text).map_err(|diagnostic| contract(diagnostic, body_span))?;
1667    assembler.insert_fact(
1668        SchemaFact::Function(FunctionFact::new(id.clone(), signature, body)),
1669        function_span,
1670    )?;
1671    insert_annotations(
1672        assembler,
1673        AnnotationSubjectId::Function(id),
1674        &function.annotations,
1675        document,
1676        source,
1677    )
1678}
1679
1680fn return_elements(
1681    types: &[NamedTypeAny],
1682    document: &DocumentId,
1683    source: &str,
1684    span: Option<Span>,
1685) -> Result<Vec<FunctionReturnElement>, SchemaDiagnostics> {
1686    types
1687        .iter()
1688        .map(|type_| {
1689            let (named, optional) = simple_or_optional_named(type_).map_err(|message| {
1690                at(
1691                    document,
1692                    source,
1693                    span,
1694                    "unsupported_typeql_function_return",
1695                    message,
1696                )
1697            })?;
1698            Ok(FunctionReturnElement::new(type_reference(named)?, optional))
1699        })
1700        .collect()
1701}
1702
1703fn insert_annotations(
1704    assembler: &mut FactAssembler,
1705    subject: AnnotationSubjectId,
1706    annotations: &[Annotation],
1707    document: &DocumentId,
1708    source: &str,
1709) -> Result<(), SchemaDiagnostics> {
1710    for annotation in annotations {
1711        insert_released_annotations(
1712            assembler,
1713            subject.clone(),
1714            core::slice::from_ref(annotation),
1715            document,
1716            source,
1717        )?;
1718    }
1719    Ok(())
1720}
1721
1722fn insert_released_annotations(
1723    assembler: &mut FactAssembler,
1724    subject: AnnotationSubjectId,
1725    annotations: &[Annotation],
1726    document: &DocumentId,
1727    source: &str,
1728) -> Result<(), SchemaDiagnostics> {
1729    // The frozen parser stores one value per annotation identity: presence
1730    // flags accumulate, while doc/regex/values/card and each meta key use the
1731    // final spelling. Repeated attribute ranges update only the bounds present
1732    // in each spelling (`@range(1..) @range(..5)` becomes `1..5`). Stage the
1733    // merged facts before handing them to the strict assembler so compatible
1734    // repetitions do not look like direct fact duplication.
1735    let mut merged = BTreeMap::new();
1736    let mut released_range: Option<(
1737        Option<&typeql::value::Literal>,
1738        Option<&typeql::value::Literal>,
1739        SourceSpan,
1740    )> = None;
1741    for annotation in annotations {
1742        let annotation_span = source_span(document, source, annotation.span())?;
1743        if let Annotation::Range(range) = annotation {
1744            let (lower, upper, latest_span) =
1745                released_range.get_or_insert((None, None, annotation_span.clone()));
1746            if let Some(minimum) = range.min.as_ref() {
1747                *lower = Some(minimum);
1748            }
1749            if let Some(maximum) = range.max.as_ref() {
1750                *upper = Some(maximum);
1751            }
1752            *latest_span = annotation_span;
1753            continue;
1754        }
1755        let kind = annotation_identity_kind(annotation, annotation_span.clone())?;
1756        merged.insert(
1757            AnnotationFactId::new(subject.clone(), kind),
1758            (annotation, annotation_span),
1759        );
1760    }
1761    for (id, (annotation, annotation_span)) in merged {
1762        let (kind, value) = match annotation {
1763            Annotation::Abstract(_) => {
1764                (AnnotationKindId::Abstract, SchemaAnnotationValue::Presence)
1765            }
1766            Annotation::Independent(_) => (
1767                AnnotationKindId::Independent,
1768                SchemaAnnotationValue::Presence,
1769            ),
1770            Annotation::Key(_) => (AnnotationKindId::Key, SchemaAnnotationValue::Presence),
1771            Annotation::Unique(_) => (AnnotationKindId::Unique, SchemaAnnotationValue::Presence),
1772            Annotation::Distinct(_) => {
1773                (AnnotationKindId::Distinct, SchemaAnnotationValue::Presence)
1774            }
1775            Annotation::Cardinality(cardinality) => {
1776                let cardinality = match &cardinality.range {
1777                    CardinalityRange::Exact(exact) => {
1778                        let exact = parse_u64(&exact.value, &annotation_span)?;
1779                        Cardinality::new(exact, Some(exact))
1780                    }
1781                    CardinalityRange::Range(minimum, maximum) => Cardinality::new(
1782                        parse_u64(&minimum.value, &annotation_span)?,
1783                        maximum
1784                            .as_ref()
1785                            .map(|maximum| parse_u64(&maximum.value, &annotation_span))
1786                            .transpose()?,
1787                    ),
1788                }
1789                .map_err(|diagnostic| contract(diagnostic, annotation_span.clone()))?;
1790                (
1791                    AnnotationKindId::Card,
1792                    SchemaAnnotationValue::Cardinality(cardinality),
1793                )
1794            }
1795            Annotation::Regex(regex) => {
1796                validate_annotation_string(&regex.regex, "regex", annotation_span.clone())?;
1797                let text = regex.regex.unescape_regex().map_err(|unescape_error| {
1798                    error(
1799                        DiagnosticCategory::InvalidContract,
1800                        "invalid_typeql_regex",
1801                        format!("TypeQL regex decoding failed: {unescape_error}"),
1802                        Some(annotation_span.clone()),
1803                    )
1804                })?;
1805                let pattern = RegexPattern::new(text)
1806                    .map_err(|diagnostic| contract(diagnostic, annotation_span.clone()))?;
1807                (
1808                    AnnotationKindId::Regex,
1809                    SchemaAnnotationValue::Regex(pattern),
1810                )
1811            }
1812            Annotation::Doc(doc) => {
1813                validate_annotation_string(&doc.doc, "doc", annotation_span.clone())?;
1814                let text = doc.doc.unescape().map_err(|unescape_error| {
1815                    error(
1816                        DiagnosticCategory::InvalidContract,
1817                        "invalid_typeql_doc",
1818                        format!("TypeQL documentation decoding failed: {unescape_error}"),
1819                        Some(annotation_span.clone()),
1820                    )
1821                })?;
1822                let text = DocText::new(text)
1823                    .map_err(|diagnostic| contract(diagnostic, annotation_span.clone()))?;
1824                (AnnotationKindId::Doc, SchemaAnnotationValue::Doc(text))
1825            }
1826            Annotation::Meta(meta) => {
1827                validate_annotation_string(&meta.key, "meta_key", annotation_span.clone())?;
1828                let key = meta.key.unescape().map_err(|unescape_error| {
1829                    error(
1830                        DiagnosticCategory::InvalidContract,
1831                        "invalid_typeql_meta_key",
1832                        format!("TypeQL metadata key decoding failed: {unescape_error}"),
1833                        Some(annotation_span.clone()),
1834                    )
1835                })?;
1836                validate_annotation_string(&meta.value, "meta_value", annotation_span.clone())?;
1837                let value = meta.value.unescape().map_err(|unescape_error| {
1838                    error(
1839                        DiagnosticCategory::InvalidContract,
1840                        "invalid_typeql_meta_value",
1841                        format!("TypeQL metadata value decoding failed: {unescape_error}"),
1842                        Some(annotation_span.clone()),
1843                    )
1844                })?;
1845                let kind = AnnotationKindId::meta(key)
1846                    .map_err(|diagnostic| contract(diagnostic, annotation_span.clone()))?;
1847                let value = CanonicalString::new(value)
1848                    .map_err(|diagnostic| contract(diagnostic, annotation_span.clone()))?;
1849                (
1850                    kind,
1851                    SchemaAnnotationValue::Meta(CanonicalValue::String(value)),
1852                )
1853            }
1854            Annotation::Cascade(_) | Annotation::Subkey(_) => {
1855                return Err(error(
1856                    DiagnosticCategory::UnsupportedCapability,
1857                    "unsupported_typeql_annotation",
1858                    "TypeQL annotation has no portable V2 annotation identity",
1859                    Some(annotation_span),
1860                ));
1861            }
1862            Annotation::Range(range) => {
1863                let lower = range
1864                    .min
1865                    .as_ref()
1866                    .map(|literal| annotation_literal(literal, document, source))
1867                    .transpose()?;
1868                let upper = range
1869                    .max
1870                    .as_ref()
1871                    .map(|literal| annotation_literal(literal, document, source))
1872                    .transpose()?;
1873                let range = CanonicalValueRange::new(lower, upper)
1874                    .map_err(|diagnostic| contract(diagnostic, annotation_span.clone()))?;
1875                (AnnotationKindId::Range, SchemaAnnotationValue::Range(range))
1876            }
1877            Annotation::Values(values) => {
1878                if values.values.len() > MAX_CANONICAL_COLLECTION_LEN {
1879                    return Err(error(
1880                        DiagnosticCategory::InvalidContract,
1881                        "values_annotation_member_limit_exceeded",
1882                        format!(
1883                            "@values contains {} members; the maximum is {MAX_CANONICAL_COLLECTION_LEN}",
1884                            values.values.len()
1885                        ),
1886                        Some(annotation_span),
1887                    ));
1888                }
1889                let values = values
1890                    .values
1891                    .iter()
1892                    .map(|literal| annotation_literal(literal, document, source))
1893                    .collect::<Result<Vec<_>, _>>()?;
1894                let values = CanonicalValueSet::new(values)
1895                    .map_err(|diagnostic| contract(diagnostic, annotation_span.clone()))?;
1896                (
1897                    AnnotationKindId::Values,
1898                    SchemaAnnotationValue::Values(values),
1899                )
1900            }
1901        };
1902        debug_assert_eq!(id.kind(), &kind);
1903        let fact = AnnotationFact::new(id, value)
1904            .map_err(|diagnostic| contract(diagnostic, annotation_span.clone()))?;
1905        assembler.insert_fact(SchemaFact::Annotation(fact), annotation_span)?;
1906    }
1907    if let Some((lower, upper, annotation_span)) = released_range
1908        && (lower.is_some() || upper.is_some())
1909    {
1910        let lower = lower
1911            .map(|literal| annotation_literal(literal, document, source))
1912            .transpose()?;
1913        let upper = upper
1914            .map(|literal| annotation_literal(literal, document, source))
1915            .transpose()?;
1916        let range = CanonicalValueRange::new(lower, upper)
1917            .map_err(|diagnostic| contract(diagnostic, annotation_span.clone()))?;
1918        let id = AnnotationFactId::new(subject, AnnotationKindId::Range);
1919        let fact = AnnotationFact::new(id, SchemaAnnotationValue::Range(range))
1920            .map_err(|diagnostic| contract(diagnostic, annotation_span.clone()))?;
1921        assembler.insert_fact(SchemaFact::Annotation(fact), annotation_span)?;
1922    }
1923    Ok(())
1924}
1925
1926fn annotation_identity_kind(
1927    annotation: &Annotation,
1928    annotation_span: SourceSpan,
1929) -> Result<AnnotationKindId, SchemaDiagnostics> {
1930    Ok(match annotation {
1931        Annotation::Abstract(_) => AnnotationKindId::Abstract,
1932        Annotation::Independent(_) => AnnotationKindId::Independent,
1933        Annotation::Key(_) => AnnotationKindId::Key,
1934        Annotation::Unique(_) => AnnotationKindId::Unique,
1935        Annotation::Distinct(_) => AnnotationKindId::Distinct,
1936        Annotation::Cardinality(_) => AnnotationKindId::Card,
1937        Annotation::Regex(_) => AnnotationKindId::Regex,
1938        Annotation::Doc(_) => AnnotationKindId::Doc,
1939        Annotation::Range(_) => AnnotationKindId::Range,
1940        Annotation::Values(_) => AnnotationKindId::Values,
1941        Annotation::Meta(meta) => {
1942            validate_annotation_string(&meta.key, "meta_key", annotation_span.clone())?;
1943            let key = meta.key.unescape().map_err(|unescape_error| {
1944                error(
1945                    DiagnosticCategory::InvalidContract,
1946                    "invalid_typeql_meta_key",
1947                    format!("TypeQL metadata key decoding failed: {unescape_error}"),
1948                    Some(annotation_span.clone()),
1949                )
1950            })?;
1951            AnnotationKindId::meta(key)
1952                .map_err(|diagnostic| contract(diagnostic, annotation_span))?
1953        }
1954        Annotation::Cascade(_) | Annotation::Subkey(_) => {
1955            return Err(error(
1956                DiagnosticCategory::UnsupportedCapability,
1957                "unsupported_typeql_annotation",
1958                "TypeQL annotation has no portable V2 annotation identity",
1959                Some(annotation_span),
1960            ));
1961        }
1962    })
1963}
1964
1965fn validate_annotation_string(
1966    value: &typeql::value::StringLiteral,
1967    domain: &'static str,
1968    span: SourceSpan,
1969) -> Result<(), SchemaDiagnostics> {
1970    validate_quoted_string(value, domain).map_err(|conversion| {
1971        error(
1972            DiagnosticCategory::InvalidContract,
1973            conversion.code(),
1974            conversion.message(),
1975            Some(span),
1976        )
1977    })
1978}
1979
1980fn annotation_literal(
1981    literal: &typeql::value::Literal,
1982    document: &DocumentId,
1983    source: &str,
1984) -> Result<CanonicalValue, SchemaDiagnostics> {
1985    canonical_literal(literal).map_err(|conversion| {
1986        at(
1987            document,
1988            source,
1989            literal.span,
1990            conversion.code(),
1991            conversion.message(),
1992        )
1993    })
1994}
1995
1996fn reject_annotations_if_present(
1997    annotations: &[Annotation],
1998    document: &DocumentId,
1999    source: &str,
2000) -> Result<(), SchemaDiagnostics> {
2001    if let Some(annotation) = annotations.first() {
2002        return Err(at(
2003            document,
2004            source,
2005            annotation.span(),
2006            "unsupported_root_sub_annotation",
2007            "annotations on built-in root sub declarations have no portable subject identity",
2008        ));
2009    }
2010    Ok(())
2011}
2012
2013fn capability_type_ref(reference: &TypeRefAny) -> Result<(String, CollectionMode), String> {
2014    match reference {
2015        TypeRefAny::Type(inner) => {
2016            plain_inner_type_ref(inner).map(|label| (label, CollectionMode::Unordered))
2017        }
2018        TypeRefAny::List(list) => {
2019            plain_inner_type_ref(&list.inner).map(|label| (label, CollectionMode::OrderedList))
2020        }
2021    }
2022}
2023
2024fn plain_inner_type_ref(reference: &TypeRef) -> Result<String, String> {
2025    match reference {
2026        TypeRef::Label(label) => Ok(typeql_label(label)),
2027        TypeRef::Scoped(_) => {
2028            Err("scoped type references are not valid in this capability".to_owned())
2029        }
2030        TypeRef::Variable(_) => {
2031            Err("type variables are not valid in schema declarations".to_owned())
2032        }
2033    }
2034}
2035
2036fn simple_or_optional_named(reference: &NamedTypeAny) -> Result<(&NamedType, bool), String> {
2037    match reference {
2038        NamedTypeAny::Simple(named) => Ok((named, false)),
2039        NamedTypeAny::Optional(optional) => Ok((&optional.inner, true)),
2040        NamedTypeAny::List(_) => Err("list type references are not live-pinned".to_owned()),
2041    }
2042}
2043
2044fn type_reference(named: &NamedType) -> Result<TypeReference, SchemaDiagnostics> {
2045    TypeReference::from_token(named.to_string()).map_err(contract_without_span)
2046}
2047
2048fn named_value_type(named: &NamedType) -> Result<ValueTypeTag, String> {
2049    match named {
2050        NamedType::BuiltinValueType(value_type) => {
2051            value_type_from_token(&value_type.token.to_string())
2052        }
2053        // Released schema text spells some builtins through the frozen
2054        // alias table (`int`, `long`, `bool`); the strict grammar lexes
2055        // those as labels, and this compatibility front-end honors them.
2056        NamedType::Label(label) => value_type_from_token(&typeql_label(label)),
2057    }
2058}
2059
2060fn value_type_from_token(token: &str) -> Result<ValueTypeTag, String> {
2061    match token {
2062        "string" => Ok(ValueTypeTag::String),
2063        "integer" | "int" | "long" => Ok(ValueTypeTag::Long),
2064        "double" => Ok(ValueTypeTag::Double),
2065        "boolean" | "bool" => Ok(ValueTypeTag::Boolean),
2066        "date" => Ok(ValueTypeTag::Date),
2067        "datetime" => Ok(ValueTypeTag::DateTime),
2068        "datetime-tz" => Ok(ValueTypeTag::DateTimeTz),
2069        "decimal" => Ok(ValueTypeTag::Decimal),
2070        "duration" => Ok(ValueTypeTag::Duration),
2071        _ => Err(format!("unsupported TypeQL value type `{token}`")),
2072    }
2073}
2074
2075fn kind_from_token(token: &str) -> Result<TypeKind, String> {
2076    match token {
2077        "entity" => Ok(TypeKind::Entity),
2078        "relation" => Ok(TypeKind::Relation),
2079        "attribute" => Ok(TypeKind::Attribute),
2080        "role" => Err("roles must be declared through relation relates facts".to_owned()),
2081        _ => Err(format!("unsupported TypeQL type kind `{token}`")),
2082    }
2083}
2084
2085fn root_kind(label: &str) -> Option<TypeKind> {
2086    match label {
2087        "entity" => Some(TypeKind::Entity),
2088        "relation" => Some(TypeKind::Relation),
2089        "attribute" => Some(TypeKind::Attribute),
2090        _ => None,
2091    }
2092}
2093
2094fn merge_kind(kind: &mut Option<TypeKind>, hint: TypeKind) -> Result<(), String> {
2095    match kind {
2096        Some(current) if *current != hint => {
2097            Err("TypeQL declaration contains incompatible kind evidence".to_owned())
2098        }
2099        Some(_) => Ok(()),
2100        None => {
2101            *kind = Some(hint);
2102            Ok(())
2103        }
2104    }
2105}
2106
2107fn typeql_label(label: &typeql::type_::Label) -> String {
2108    label.ident.as_str_unchecked().to_owned()
2109}
2110
2111fn parse_u64(value: &str, span: &SourceSpan) -> Result<u64, SchemaDiagnostics> {
2112    value.parse().map_err(|_| {
2113        error(
2114            DiagnosticCategory::InvalidContract,
2115            "invalid_typeql_cardinality",
2116            "TypeQL cardinality is outside the portable unsigned 64-bit domain",
2117            Some(span.clone()),
2118        )
2119    })
2120}
2121
2122fn source_span(
2123    document: &DocumentId,
2124    source: &str,
2125    span: Option<Span>,
2126) -> Result<SourceSpan, SchemaDiagnostics> {
2127    let span = span.unwrap_or(Span {
2128        begin_offset: 0,
2129        end_offset: source.len(),
2130    });
2131    if span.begin_offset > span.end_offset
2132        || span.end_offset > source.len()
2133        || !source.is_char_boundary(span.begin_offset)
2134        || !source.is_char_boundary(span.end_offset)
2135    {
2136        return Err(error(
2137            DiagnosticCategory::InvalidContract,
2138            "invalid_typeql_source_span",
2139            "TypeQL parser returned a source span outside the original input",
2140            None,
2141        ));
2142    }
2143    let (line, column) = line_column(source, span.begin_offset)?;
2144    let (end_line, end_column) = line_column(source, span.end_offset)?;
2145    SourceSpan::new(
2146        document.clone(),
2147        span.begin_offset as u64,
2148        span.end_offset as u64,
2149        line,
2150        column,
2151        end_line,
2152        end_column,
2153    )
2154    .map_err(contract_without_span)
2155}
2156
2157fn query_span(
2158    document: &DocumentId,
2159    source: &str,
2160    span: Option<Span>,
2161) -> Result<Option<SourceSpan>, SchemaDiagnostics> {
2162    source_span(document, source, span).map(Some)
2163}
2164
2165fn line_column(source: &str, offset: usize) -> Result<(u32, u32), SchemaDiagnostics> {
2166    let prefix = &source[..offset];
2167    let line_count = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1;
2168    let column_count = prefix
2169        .rsplit_once('\n')
2170        .map_or(prefix, |(_, tail)| tail)
2171        .chars()
2172        .count()
2173        + 1;
2174    let line = u32::try_from(line_count).map_err(|_| {
2175        error(
2176            DiagnosticCategory::InvalidContract,
2177            "typeql_source_position_overflow",
2178            "TypeQL source line exceeds the portable position domain",
2179            None,
2180        )
2181    })?;
2182    let column = u32::try_from(column_count).map_err(|_| {
2183        error(
2184            DiagnosticCategory::InvalidContract,
2185            "typeql_source_position_overflow",
2186            "TypeQL source column exceeds the portable position domain",
2187            None,
2188        )
2189    })?;
2190    Ok((line, column))
2191}
2192
2193fn at(
2194    document: &DocumentId,
2195    source: &str,
2196    span: Option<Span>,
2197    code: &'static str,
2198    message: impl Into<String>,
2199) -> SchemaDiagnostics {
2200    match source_span(document, source, span) {
2201        Ok(span) => error(
2202            DiagnosticCategory::InvalidContract,
2203            code,
2204            message,
2205            Some(span),
2206        ),
2207        Err(error) => error,
2208    }
2209}
2210
2211fn error(
2212    category: DiagnosticCategory,
2213    code: &'static str,
2214    message: impl Into<String>,
2215    primary: Option<SourceSpan>,
2216) -> SchemaDiagnostics {
2217    let diagnostic = Diagnostic::new(
2218        category,
2219        DiagnosticCode::new(code).expect("static schema compatibility diagnostic code is valid"),
2220        message,
2221    );
2222    SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, primary))
2223}
2224
2225fn contract(diagnostic: Diagnostic, span: SourceSpan) -> SchemaDiagnostics {
2226    SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, Some(span)))
2227}
2228
2229fn contract_without_span(diagnostic: Diagnostic) -> SchemaDiagnostics {
2230    SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, None))
2231}
2232
2233#[cfg(test)]
2234mod tests {
2235    use super::*;
2236
2237    #[test]
2238    fn trusted_generator_size_policy_does_not_widen_defensive_inputs() {
2239        assert!(TypeqlSourceSizePolicy::Defensive.allows(MAX_TYPEQL_SCHEMA_BYTES));
2240        assert!(!TypeqlSourceSizePolicy::Defensive.allows(MAX_TYPEQL_SCHEMA_BYTES + 1));
2241        assert!(TypeqlSourceSizePolicy::TrustedGenerator.allows(MAX_TYPEQL_SCHEMA_BYTES + 1));
2242    }
2243
2244    #[test]
2245    fn deep_role_specialization_index_uses_heap_stack_and_one_tree_walk() {
2246        const DEPTH: usize = 25_000;
2247        let mut relation_names = BTreeSet::new();
2248        let mut role_names_by_relation = BTreeMap::<String, Vec<String>>::new();
2249        let mut roles = BTreeMap::new();
2250        let mut parents = BTreeMap::new();
2251
2252        for index in 0..DEPTH {
2253            let relation = format!("relation-{index}");
2254            let role = if index == 0 {
2255                "root-role".to_owned()
2256            } else {
2257                format!("role-{index}")
2258            };
2259            relation_names.insert(relation.clone());
2260            role_names_by_relation.insert(relation.clone(), vec![role.clone()]);
2261            roles.insert(
2262                (relation.clone(), role),
2263                ReleasedIndexedRole {
2264                    capability_index: index,
2265                    specializes: (index != 0).then(|| "root-role".to_owned()),
2266                },
2267            );
2268            if index != 0 {
2269                parents.insert(relation, format!("relation-{}", index - 1));
2270            }
2271        }
2272
2273        let leaf = format!("relation-{}", DEPTH - 1);
2274        let leaf_role = format!("role-{}", DEPTH - 1);
2275        let played = BTreeMap::from([(
2276            leaf.clone(),
2277            BTreeSet::from(["root-role".to_owned(), leaf_role.clone()]),
2278        )]);
2279        let resolution = released_role_validities(
2280            &relation_names,
2281            &role_names_by_relation,
2282            &played,
2283            &roles,
2284            &parents,
2285        );
2286
2287        assert_eq!(resolution.direct.len(), DEPTH);
2288        assert!(
2289            resolution
2290                .direct
2291                .values()
2292                .all(|validity| *validity == ReleasedRoleValidity::Valid)
2293        );
2294        assert_eq!(
2295            resolution.plays.get(&(leaf.clone(), leaf_role)),
2296            Some(&ReleasedRoleValidity::Valid)
2297        );
2298        assert_eq!(
2299            resolution.plays.get(&(leaf, "root-role".to_owned())),
2300            Some(&ReleasedRoleValidity::Invalid)
2301        );
2302    }
2303}