Skip to main content

type_bridge_schema/
semantic.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use serde::Serialize;
4use type_bridge_contract::capability::CapabilitySet;
5use type_bridge_contract::codec::{FormatVersion, to_canonical_json};
6use type_bridge_contract::diagnostic::DiagnosticCategory;
7use type_bridge_contract::fingerprint::SemanticProfileId;
8use type_bridge_contract::managed_scope::{
9    ManagedScopeBinding, ManagedScopeId, SemanticProfileFingerprint,
10};
11use type_bridge_contract::schema::{
12    AnnotationFactId, AnnotationKindId, AnnotationSubjectId, CollectionMode, DeclaredSchema,
13    InterfaceKind, ManagedDeclaredIdentityFingerprint, ManagedSemanticSchemaFingerprint,
14    SchemaAnnotationValue, SchemaDiagnostic, SchemaDiagnostics, SchemaFact, SchemaFactId,
15    SemanticProfile, SemanticSchemaFingerprint,
16};
17use type_bridge_contract::value::Cardinality;
18
19use crate::TYPEDB_3_12_1_TIMEZONE_POLICY_ID;
20
21/// Unbound evidence selecting direct fact identities observed as managed.
22///
23/// Selection-only evidence cannot enter persisted managed fingerprints until
24/// an owning schema derives an exclusive bound scope from all declared facts.
25#[derive(Clone, Debug, Default, Eq, PartialEq)]
26pub struct ManagedSchemaScope {
27    facts: BTreeSet<SchemaFactId>,
28}
29
30impl ManagedSchemaScope {
31    /// Create a deterministic managed scope from direct fact identities.
32    #[must_use]
33    pub fn new(facts: impl IntoIterator<Item = SchemaFactId>) -> Self {
34        Self {
35            facts: facts.into_iter().collect(),
36        }
37    }
38
39    /// Derive a complete exclusive managed scope from every direct declared fact.
40    pub fn bind_exclusive(
41        scope_id: ManagedScopeId,
42        declared: &DeclaredSchema,
43    ) -> Result<BoundManagedSchemaScope, SchemaDiagnostics> {
44        let binding = ManagedScopeBinding::exclusive(scope_id).map_err(|diagnostic| {
45            SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, None))
46        })?;
47        Ok(Self::new(declared.facts().map(SchemaFact::id)).bind(binding))
48    }
49
50    fn bind(self, binding: ManagedScopeBinding) -> BoundManagedSchemaScope {
51        BoundManagedSchemaScope {
52            selection: self,
53            binding,
54        }
55    }
56
57    /// Report whether a direct fact identity is managed.
58    #[must_use]
59    pub fn contains(&self, fact: &SchemaFactId) -> bool {
60        self.facts.contains(fact)
61    }
62
63    /// Iterate managed identities in stable order.
64    pub fn iter(&self) -> impl ExactSizeIterator<Item = &SchemaFactId> {
65        self.facts.iter()
66    }
67
68    /// Return the number of managed identities.
69    #[must_use]
70    pub fn len(&self) -> usize {
71        self.facts.len()
72    }
73
74    /// Report whether the scope manages no facts.
75    #[must_use]
76    pub fn is_empty(&self) -> bool {
77        self.facts.is_empty()
78    }
79}
80
81/// A managed fact selection with an explicit durable deployment/profile binding.
82#[derive(Clone, Debug, Eq, PartialEq)]
83pub struct BoundManagedSchemaScope {
84    selection: ManagedSchemaScope,
85    binding: ManagedScopeBinding,
86}
87
88impl BoundManagedSchemaScope {
89    /// Return the deterministic managed fact selection.
90    pub const fn selection(&self) -> &ManagedSchemaScope {
91        &self.selection
92    }
93
94    /// Return the durable scope/profile binding.
95    pub const fn binding(&self) -> &ManagedScopeBinding {
96        &self.binding
97    }
98}
99
100#[derive(Serialize)]
101struct SemanticSchemaView<'a> {
102    format_version: FormatVersion,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    managed_scope: Option<&'a ManagedScopeBinding>,
105    semantic_profile: &'a SemanticProfileId,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    semantic_profile_fingerprint: Option<&'a SemanticProfileFingerprint>,
108    timezone_policy: &'static str,
109    required_capabilities: &'a CapabilitySet,
110    facts: Vec<SemanticFactView<'a>>,
111}
112
113#[derive(Serialize)]
114struct SemanticFactView<'a> {
115    id: SchemaFactId,
116    value: SemanticFactValue<'a>,
117}
118
119#[derive(Serialize)]
120#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
121enum SemanticFactValue<'a> {
122    Direct(&'a SchemaFact),
123    TimeZoneRange {
124        subject: AnnotationSubjectId,
125        lower_utc_nanoseconds: Option<String>,
126        upper_utc_nanoseconds: Option<String>,
127    },
128    TimeZoneValues {
129        subject: AnnotationSubjectId,
130        utc_nanoseconds: Vec<String>,
131    },
132    MaterializedCardinality {
133        subject: AnnotationSubjectId,
134        cardinality: Cardinality,
135    },
136}
137
138#[derive(Serialize)]
139struct ManagedDeclaredView<'a> {
140    format_version: FormatVersion,
141    managed_scope: &'a ManagedScopeBinding,
142    required_capabilities: &'a CapabilitySet,
143    facts: Vec<&'a SchemaFact>,
144}
145
146/// Return canonical direct-semantic bytes with equal explicit defaults normalized.
147pub fn canonical_semantic_schema_bytes(
148    declared: &DeclaredSchema,
149    profile: &SemanticProfileId,
150) -> Result<Vec<u8>, SchemaDiagnostics> {
151    canonical_semantic_schema_bytes_for_scope(declared, profile, None)
152}
153
154/// Fingerprint canonical direct semantics without hashing inherited resolver closure.
155pub fn semantic_schema_fingerprint(
156    declared: &DeclaredSchema,
157    profile: &SemanticProfileId,
158) -> Result<SemanticSchemaFingerprint, SchemaDiagnostics> {
159    let canonical = canonical_semantic_schema_bytes(declared, profile)?;
160    SemanticSchemaFingerprint::compute(profile.clone(), &canonical)
161        .map_err(|diagnostic| SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, None)))
162}
163
164/// Fingerprint declared identity after explicit managed-scope filtering.
165pub fn managed_declared_identity_fingerprint(
166    declared: &DeclaredSchema,
167    scope: &BoundManagedSchemaScope,
168) -> Result<ManagedDeclaredIdentityFingerprint, SchemaDiagnostics> {
169    let canonical = canonical_managed_declared_identity_bytes(declared, scope)?;
170    ManagedDeclaredIdentityFingerprint::compute(&canonical)
171        .map_err(|diagnostic| SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, None)))
172}
173
174/// Return canonical declared-identity bytes for an explicitly bound managed scope.
175pub fn canonical_managed_declared_identity_bytes(
176    declared: &DeclaredSchema,
177    scope: &BoundManagedSchemaScope,
178) -> Result<Vec<u8>, SchemaDiagnostics> {
179    let view = ManagedDeclaredView {
180        format_version: declared.format(),
181        managed_scope: scope.binding(),
182        required_capabilities: declared.required_capabilities(),
183        facts: declared
184            .facts()
185            .filter(|fact| scope.selection().contains(&fact.id()))
186            .collect(),
187    };
188    to_canonical_json(&view)
189        .map_err(|diagnostic| SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, None)))
190}
191
192/// Fingerprint direct semantics after explicit managed-scope filtering.
193pub fn managed_semantic_schema_fingerprint(
194    declared: &DeclaredSchema,
195    profile: &SemanticProfileId,
196    scope: &BoundManagedSchemaScope,
197) -> Result<ManagedSemanticSchemaFingerprint, SchemaDiagnostics> {
198    let canonical = canonical_managed_semantic_schema_bytes(declared, profile, scope)?;
199    ManagedSemanticSchemaFingerprint::compute(profile.clone(), &canonical)
200        .map_err(|diagnostic| SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, None)))
201}
202
203/// Return canonical direct-semantic bytes for an explicitly bound managed scope.
204pub fn canonical_managed_semantic_schema_bytes(
205    declared: &DeclaredSchema,
206    profile: &SemanticProfileId,
207    scope: &BoundManagedSchemaScope,
208) -> Result<Vec<u8>, SchemaDiagnostics> {
209    canonical_semantic_schema_bytes_for_scope(declared, profile, Some(scope))
210}
211
212fn canonical_semantic_schema_bytes_for_scope(
213    declared: &DeclaredSchema,
214    profile: &SemanticProfileId,
215    scope: Option<&BoundManagedSchemaScope>,
216) -> Result<Vec<u8>, SchemaDiagnostics> {
217    let semantic_profile = SemanticProfile::resolve(profile)
218        .map_err(|diagnostic| SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, None)))?;
219    validate_collection_profile(declared, profile)?;
220    let semantic_profile_fingerprint = scope
221        .map(|_| semantic_profile.content_fingerprint())
222        .transpose()
223        .map_err(|diagnostic| SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, None)))?;
224    let facts = semantic_facts(
225        declared,
226        scope.map(BoundManagedSchemaScope::selection),
227        &semantic_profile,
228    );
229    let view = SemanticSchemaView {
230        format_version: declared.format(),
231        managed_scope: scope.map(BoundManagedSchemaScope::binding),
232        semantic_profile: profile,
233        semantic_profile_fingerprint: semantic_profile_fingerprint.as_ref(),
234        timezone_policy: TYPEDB_3_12_1_TIMEZONE_POLICY_ID,
235        required_capabilities: declared.required_capabilities(),
236        facts,
237    };
238    to_canonical_json(&view)
239        .map_err(|diagnostic| SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, None)))
240}
241
242pub(crate) fn validate_collection_profile(
243    declared: &DeclaredSchema,
244    profile: &SemanticProfileId,
245) -> Result<(), SchemaDiagnostics> {
246    if profile.as_str() == "typedb-3.12.1/v1" {
247        return Ok(());
248    }
249    for fact in declared.facts() {
250        let ordered = match fact {
251            SchemaFact::Owns(fact) => fact.collection_mode() == CollectionMode::OrderedList,
252            SchemaFact::Relates(fact) => fact.collection_mode() == CollectionMode::OrderedList,
253            _ => false,
254        };
255        if ordered {
256            return Err(crate::yaml::diagnostic(
257                DiagnosticCategory::UnsupportedCapability,
258                "ordered_collection_profile_unsupported",
259                "ordered collection facts require the typedb-3.12.1/v1 semantic profile",
260                declared.source(&fact.id()).cloned(),
261            ));
262        }
263    }
264    Ok(())
265}
266
267fn semantic_facts<'a>(
268    declared: &'a DeclaredSchema,
269    scope: Option<&ManagedSchemaScope>,
270    profile: &SemanticProfile,
271) -> Vec<SemanticFactView<'a>> {
272    let mut facts = BTreeMap::<SchemaFactId, SemanticFactValue<'a>>::new();
273    let mut interfaces = Vec::<(AnnotationSubjectId, InterfaceKind)>::new();
274    let key_owns = declared
275        .facts()
276        .filter(|fact| scope.is_none_or(|scope| scope.contains(&fact.id())))
277        .filter_map(|fact| match fact {
278            SchemaFact::Annotation(annotation)
279                if annotation.id().kind() == &AnnotationKindId::Key
280                    && matches!(annotation.id().subject(), AnnotationSubjectId::Owns(_)) =>
281            {
282                Some(annotation.id().subject().clone())
283            }
284            _ => None,
285        })
286        .collect::<BTreeSet<_>>();
287
288    for fact in declared
289        .facts()
290        .filter(|fact| scope.is_none_or(|scope| scope.contains(&fact.id())))
291    {
292        let id = fact.id();
293        match fact {
294            SchemaFact::Owns(owns) => interfaces.push((
295                AnnotationSubjectId::Owns(owns.id().clone()),
296                InterfaceKind::Owns,
297            )),
298            SchemaFact::Relates(relates) => interfaces.push((
299                AnnotationSubjectId::Relates(relates.id().clone()),
300                InterfaceKind::Relates,
301            )),
302            SchemaFact::Plays(plays) => interfaces.push((
303                AnnotationSubjectId::Plays(plays.id().clone()),
304                InterfaceKind::Plays,
305            )),
306            _ => {}
307        }
308
309        let value = match fact {
310            SchemaFact::Annotation(annotation)
311                if annotation.id().kind() == &AnnotationKindId::Card =>
312            {
313                let Some(kind) = interface_kind(annotation.id().subject()) else {
314                    facts.insert(id, SemanticFactValue::Direct(fact));
315                    continue;
316                };
317                match annotation.value() {
318                    SchemaAnnotationValue::Cardinality(cardinality)
319                        if *cardinality
320                            == profile.effective_cardinality(
321                                kind,
322                                None,
323                                key_owns.contains(annotation.id().subject()),
324                            ) =>
325                    {
326                        SemanticFactValue::MaterializedCardinality {
327                            subject: annotation.id().subject().clone(),
328                            cardinality: profile.effective_cardinality(
329                                kind,
330                                None,
331                                key_owns.contains(annotation.id().subject()),
332                            ),
333                        }
334                    }
335                    _ => SemanticFactValue::Direct(fact),
336                }
337            }
338            SchemaFact::Annotation(annotation) => {
339                semantic_timezone_value(annotation).unwrap_or(SemanticFactValue::Direct(fact))
340            }
341            _ => SemanticFactValue::Direct(fact),
342        };
343        facts.insert(id, value);
344    }
345
346    for (subject, kind) in interfaces {
347        let id = SchemaFactId::Annotation(AnnotationFactId::new(
348            subject.clone(),
349            AnnotationKindId::Card,
350        ));
351        let cardinality = profile.effective_cardinality(kind, None, key_owns.contains(&subject));
352        facts
353            .entry(id)
354            .or_insert_with(|| SemanticFactValue::MaterializedCardinality {
355                subject,
356                cardinality,
357            });
358    }
359
360    facts
361        .into_iter()
362        .map(|(id, value)| SemanticFactView { id, value })
363        .collect()
364}
365
366fn semantic_timezone_value<'a>(
367    annotation: &'a type_bridge_contract::schema::AnnotationFact,
368) -> Option<SemanticFactValue<'a>> {
369    match annotation.value() {
370        SchemaAnnotationValue::Range(range) => {
371            let lower = range.lower().and_then(timezone_key);
372            let upper = range.upper().and_then(timezone_key);
373            (lower.is_some() || upper.is_some()).then(|| SemanticFactValue::TimeZoneRange {
374                subject: annotation.id().subject().clone(),
375                lower_utc_nanoseconds: lower,
376                upper_utc_nanoseconds: upper,
377            })
378        }
379        SchemaAnnotationValue::Values(values)
380            if values.iter().next().is_some_and(|value| {
381                matches!(
382                    value,
383                    type_bridge_contract::value::CanonicalValue::DateTimeTz(_)
384                )
385            }) =>
386        {
387            let mut keys = values.iter().filter_map(timezone_key).collect::<Vec<_>>();
388            keys.sort_by(|left, right| {
389                left.parse::<i128>()
390                    .expect("semantic timezone key is an i128")
391                    .cmp(
392                        &right
393                            .parse::<i128>()
394                            .expect("semantic timezone key is an i128"),
395                    )
396            });
397            Some(SemanticFactValue::TimeZoneValues {
398                subject: annotation.id().subject().clone(),
399                utc_nanoseconds: keys,
400            })
401        }
402        _ => None,
403    }
404}
405
406fn timezone_key(value: &type_bridge_contract::value::CanonicalValue) -> Option<String> {
407    let type_bridge_contract::value::CanonicalValue::DateTimeTz(value) = value else {
408        return None;
409    };
410    Some(value.semantic_utc_nanoseconds().to_string())
411}
412
413fn interface_kind(subject: &AnnotationSubjectId) -> Option<InterfaceKind> {
414    match subject {
415        AnnotationSubjectId::Owns(_) => Some(InterfaceKind::Owns),
416        AnnotationSubjectId::Relates(_) => Some(InterfaceKind::Relates),
417        AnnotationSubjectId::Plays(_) => Some(InterfaceKind::Plays),
418        _ => None,
419    }
420}