Skip to main content

knowledge_base_extension_framework/
ontology.rs

1//! Composition and verification of extension ontology contracts.
2
3use crate::bindings::ResolvedBindings;
4use crate::contracts::{BindingReference, ExtensionId, PropertyRequirement};
5use crate::registry::ActiveExtensions;
6use knowledge_base_models::{Cardinality, EntityTypeId, Property, PropertyId, PropertyUsage, ValueType};
7use knowledge_base_snapshot::RepositorySnapshot;
8use std::collections::{BTreeMap, BTreeSet};
9use std::fmt;
10
11/// A field constrained by a partial property requirement.
12#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
13pub enum OntologyContractField {
14    ValueType,
15    Usage,
16    Cardinality,
17    SubjectTypes,
18    TargetTypes,
19    AllowedQualifiers,
20}
21
22impl fmt::Display for OntologyContractField {
23    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
24        formatter.write_str(match self {
25            Self::ValueType => "value_type",
26            Self::Usage => "usage",
27            Self::Cardinality => "cardinality",
28            Self::SubjectTypes => "subject_types",
29            Self::TargetTypes => "target_types",
30            Self::AllowedQualifiers => "allowed_qualifiers",
31        })
32    }
33}
34
35/// One deterministic failure while checking activated extension ontology contracts.
36#[derive(Clone, Debug, Eq, PartialEq)]
37pub struct OntologyContractDiagnostic {
38    pub extension: ExtensionId,
39    pub binding: BindingReference,
40    pub id: PropertyId,
41    pub field: OntologyContractField,
42    pub message: String,
43}
44
45impl fmt::Display for OntologyContractDiagnostic {
46    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47        write!(
48            formatter,
49            "extension {} binding {} resolves to property {} with incompatible {}: {}",
50            self.extension, self.binding, self.id, self.field, self.message
51        )
52    }
53}
54
55/// Verifies all partial ontology requirements for an activated extension set.
56///
57/// Requirements are composed by resolved property ID. Scalar expectations must agree;
58/// collection expectations are unioned and checked as subsets of the property record.
59pub fn verify_ontology_contracts(snapshot: &RepositorySnapshot, active: &ActiveExtensions, bindings: &ResolvedBindings) -> Vec<OntologyContractDiagnostic> {
60    let mut diagnostics = Vec::new();
61    let mut requirements = BTreeMap::<PropertyId, MergedPropertyRequirement>::new();
62
63    for extension in active.extensions() {
64        let metadata = extension.metadata();
65        // Manifest activation has already confirmed every entity-type binding resolves
66        // to a canonical record. EntityTypeRequirement has no further fields to check.
67        for requirement in &metadata.ontology_requirements.properties {
68            let id = bindings
69                .property(metadata, &requirement.binding)
70                .expect("active requirements have validated property bindings")
71                .clone();
72            requirements
73                .entry(id.clone())
74                .or_default()
75                .merge(id, metadata.id.clone(), requirement, bindings, metadata, &mut diagnostics);
76        }
77    }
78
79    for (id, requirement) in requirements {
80        let property = snapshot.properties().get(&id).expect("manifest activation confirms all bound properties exist");
81        requirement.verify(property, &mut diagnostics);
82    }
83
84    diagnostics.sort_by(|left, right| {
85        (&left.binding, &left.id, left.field, &left.message, &left.extension).cmp(&(&right.binding, &right.id, right.field, &right.message, &right.extension))
86    });
87    diagnostics
88}
89
90#[derive(Default)]
91struct MergedPropertyRequirement {
92    value_type: Scalar<ValueType>,
93    usage: Scalar<PropertyUsage>,
94    cardinality: Scalar<Cardinality>,
95    subject_types: BTreeMap<EntityTypeId, Contributor>,
96    target_types: BTreeMap<EntityTypeId, Contributor>,
97    allowed_qualifiers: BTreeMap<PropertyId, Contributor>,
98}
99
100struct Scalar<T> {
101    expected: Option<(T, Contributor)>,
102    conflict: bool,
103}
104
105impl<T> Default for Scalar<T> {
106    fn default() -> Self {
107        Self { expected: None, conflict: false }
108    }
109}
110
111#[derive(Clone)]
112struct Contributor {
113    extension: ExtensionId,
114    binding: BindingReference,
115}
116
117impl MergedPropertyRequirement {
118    fn merge(
119        &mut self,
120        id: PropertyId,
121        extension: ExtensionId,
122        requirement: &PropertyRequirement,
123        bindings: &ResolvedBindings,
124        metadata: &crate::contracts::ExtensionMetadata,
125        diagnostics: &mut Vec<OntologyContractDiagnostic>,
126    ) {
127        let contributor = Contributor {
128            extension,
129            binding: requirement.binding.clone(),
130        };
131        self.value_type
132            .merge(id.clone(), requirement.value_type, OntologyContractField::ValueType, &contributor, diagnostics);
133        self.usage.merge(id.clone(), requirement.usage, OntologyContractField::Usage, &contributor, diagnostics);
134        self.cardinality
135            .merge(id, requirement.cardinality, OntologyContractField::Cardinality, &contributor, diagnostics);
136        for binding in &requirement.subject_types {
137            let id = bindings
138                .entity_type(metadata, binding)
139                .expect("active requirements have validated entity-type bindings")
140                .clone();
141            self.subject_types.entry(id).or_insert_with(|| contributor.clone());
142        }
143        if let Some(target_types) = &requirement.target_types {
144            for binding in target_types {
145                let id = bindings
146                    .entity_type(metadata, binding)
147                    .expect("active requirements have validated entity-type bindings")
148                    .clone();
149                self.target_types.entry(id).or_insert_with(|| contributor.clone());
150            }
151        }
152        for binding in &requirement.allowed_qualifiers {
153            let id = bindings.property(metadata, binding).expect("active requirements have validated property bindings").clone();
154            self.allowed_qualifiers.entry(id).or_insert_with(|| contributor.clone());
155        }
156    }
157
158    fn verify(&self, property: &Property, diagnostics: &mut Vec<OntologyContractDiagnostic>) {
159        self.value_type.verify(property, OntologyContractField::ValueType, property.value_type, diagnostics);
160        self.usage.verify(property, OntologyContractField::Usage, property.usage, diagnostics);
161        self.cardinality.verify(property, OntologyContractField::Cardinality, property.cardinality, diagnostics);
162        verify_collection(
163            property,
164            OntologyContractField::SubjectTypes,
165            &self.subject_types,
166            property.subject_types.iter().cloned().collect(),
167            diagnostics,
168        );
169        if !self.target_types.is_empty() && property.value_type != ValueType::Entity {
170            let contributor = self.target_types.values().next().expect("non-empty map has a contributor");
171            diagnostics.push(diagnostic(
172                contributor,
173                property,
174                OntologyContractField::TargetTypes,
175                format!("property value_type is {}, but target types require entity", render(property.value_type)),
176            ));
177        } else {
178            verify_collection(
179                property,
180                OntologyContractField::TargetTypes,
181                &self.target_types,
182                property.target_types.clone().unwrap_or_default().into_iter().collect(),
183                diagnostics,
184            );
185        }
186        verify_collection(
187            property,
188            OntologyContractField::AllowedQualifiers,
189            &self.allowed_qualifiers,
190            property.allowed_qualifiers.iter().cloned().collect(),
191            diagnostics,
192        );
193    }
194}
195
196impl<T> Scalar<T>
197where
198    T: Copy + Eq + fmt::Debug,
199{
200    fn merge(&mut self, id: PropertyId, expected: Option<T>, field: OntologyContractField, contributor: &Contributor, diagnostics: &mut Vec<OntologyContractDiagnostic>) {
201        let Some(expected) = expected else { return };
202        match &self.expected {
203            None => self.expected = Some((expected, contributor.clone())),
204            Some((existing, _first)) if *existing == expected => {}
205            Some((existing, first)) => {
206                self.conflict = true;
207                diagnostics.push(OntologyContractDiagnostic {
208                    extension: contributor.extension.clone(),
209                    binding: contributor.binding.clone(),
210                    id,
211                    field,
212                    message: format!(
213                        "requires {}, conflicting with {} required by extension {} binding {}",
214                        render(expected),
215                        render(*existing),
216                        first.extension,
217                        first.binding
218                    ),
219                });
220            }
221        }
222    }
223
224    fn verify(&self, property: &Property, field: OntologyContractField, actual: T, diagnostics: &mut Vec<OntologyContractDiagnostic>) {
225        let Some((expected, contributor)) = &self.expected else { return };
226        if !self.conflict && *expected != actual {
227            diagnostics.push(diagnostic(
228                contributor,
229                property,
230                field,
231                format!("expected {}, found {}", render(*expected), render(actual)),
232            ));
233        }
234    }
235}
236
237fn verify_collection<T>(
238    property: &Property,
239    field: OntologyContractField,
240    expected: &BTreeMap<T, Contributor>,
241    actual: BTreeSet<T>,
242    diagnostics: &mut Vec<OntologyContractDiagnostic>,
243) where
244    T: Ord + fmt::Display,
245{
246    for (id, contributor) in expected {
247        if !actual.contains(id) {
248            diagnostics.push(diagnostic(contributor, property, field, format!("missing required entry {id}")));
249        }
250    }
251}
252
253fn diagnostic(contributor: &Contributor, property: &Property, field: OntologyContractField, message: String) -> OntologyContractDiagnostic {
254    OntologyContractDiagnostic {
255        extension: contributor.extension.clone(),
256        binding: contributor.binding.clone(),
257        id: property.id.clone(),
258        field,
259        message,
260    }
261}
262
263fn render(value: impl fmt::Debug) -> String {
264    format!("{value:?}").to_lowercase()
265}