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