Skip to main content

silk/
ontology.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3
4use crate::entry::Value;
5
6/// The type of a property value.
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum ValueType {
10    String,
11    Int,
12    Float,
13    Bool,
14    List,
15    Map,
16    /// Accept any Value variant.
17    Any,
18}
19
20/// Definition of a single property on a node or edge type.
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct PropertyDef {
23    pub value_type: ValueType,
24    #[serde(default)]
25    pub required: bool,
26    #[serde(default)]
27    pub description: Option<String>,
28    /// Extensible constraints — validated at write time.
29    /// Built-in: "enum" (list of allowed values), "min"/"max" (numeric range).
30    /// Community contributions welcome for additional constraint types.
31    #[serde(default)]
32    pub constraints: Option<BTreeMap<String, serde_json::Value>>,
33}
34
35/// Definition of a subtype within a node type (D-024).
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37pub struct SubtypeDef {
38    #[serde(default)]
39    pub description: Option<String>,
40    #[serde(default)]
41    pub properties: BTreeMap<String, PropertyDef>,
42}
43
44/// Definition of a node type in the ontology.
45///
46/// If `subtypes` is `Some`, then `add_node` requires a `subtype` parameter
47/// and properties are validated against the subtype's definition.
48/// If `subtypes` is `None`, the type works as before (D-024 backward compat).
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50pub struct NodeTypeDef {
51    #[serde(default)]
52    pub description: Option<String>,
53    #[serde(default)]
54    pub properties: BTreeMap<String, PropertyDef>,
55    /// Optional subtype definitions. When present, `add_node` must specify
56    /// a subtype and properties are validated per-subtype (D-024).
57    #[serde(default)]
58    pub subtypes: Option<BTreeMap<String, SubtypeDef>>,
59    /// RDFS-level class hierarchy (Step 2). If set, this type is a subclass
60    /// of `parent_type`. Queries for the parent type include this type.
61    /// Edge constraints accepting the parent type also accept this type.
62    /// Properties are inherited from the parent (child overrides on conflict).
63    #[serde(default)]
64    pub parent_type: Option<String>,
65}
66
67/// Definition of an edge type in the ontology.
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69pub struct EdgeTypeDef {
70    #[serde(default)]
71    pub description: Option<String>,
72    /// Which node types can be the source of this edge.
73    pub source_types: Vec<String>,
74    /// Which node types can be the target of this edge.
75    pub target_types: Vec<String>,
76    #[serde(default)]
77    pub properties: BTreeMap<String, PropertyDef>,
78}
79
80/// Immutable ontology — the vocabulary and rules of a Silk graph.
81///
82/// Defined once at genesis, locked forever. Every operation is validated
83/// against this ontology before being appended to the DAG.
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85pub struct Ontology {
86    pub node_types: BTreeMap<String, NodeTypeDef>,
87    pub edge_types: BTreeMap<String, EdgeTypeDef>,
88}
89
90/// Validation errors returned when an operation violates the ontology.
91#[derive(Debug, Clone, PartialEq)]
92pub enum ValidationError {
93    UnknownNodeType(String),
94    UnknownEdgeType(String),
95    InvalidSource {
96        edge_type: String,
97        node_type: String,
98        allowed: Vec<String>,
99    },
100    InvalidTarget {
101        edge_type: String,
102        node_type: String,
103        allowed: Vec<String>,
104    },
105    MissingRequiredProperty {
106        type_name: String,
107        property: String,
108    },
109    WrongPropertyType {
110        type_name: String,
111        property: String,
112        expected: ValueType,
113        got: String,
114    },
115    UnknownProperty {
116        type_name: String,
117        property: String,
118    },
119    MissingSubtype {
120        node_type: String,
121        allowed: Vec<String>,
122    },
123    UnknownSubtype {
124        node_type: String,
125        subtype: String,
126        allowed: Vec<String>,
127    },
128    UnexpectedSubtype {
129        node_type: String,
130        subtype: String,
131    },
132    /// A property value violates a constraint (enum, range, etc.)
133    ConstraintViolation {
134        type_name: String,
135        property: String,
136        constraint: String,
137        message: String,
138    },
139}
140
141impl std::fmt::Display for ValidationError {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        match self {
144            ValidationError::UnknownNodeType(t) => write!(f, "unknown node type: '{t}'"),
145            ValidationError::UnknownEdgeType(t) => write!(f, "unknown edge type: '{t}'"),
146            ValidationError::InvalidSource {
147                edge_type,
148                node_type,
149                allowed,
150            } => write!(
151                f,
152                "edge '{edge_type}' cannot have source type '{node_type}' (allowed: {allowed:?})"
153            ),
154            ValidationError::InvalidTarget {
155                edge_type,
156                node_type,
157                allowed,
158            } => write!(
159                f,
160                "edge '{edge_type}' cannot have target type '{node_type}' (allowed: {allowed:?})"
161            ),
162            ValidationError::MissingRequiredProperty {
163                type_name,
164                property,
165            } => write!(f, "'{type_name}' requires property '{property}'"),
166            ValidationError::WrongPropertyType {
167                type_name,
168                property,
169                expected,
170                got,
171            } => write!(
172                f,
173                "'{type_name}'.'{property}' expects {expected:?}, got {got}"
174            ),
175            ValidationError::UnknownProperty {
176                type_name,
177                property,
178            } => write!(f, "'{type_name}' has no property '{property}' in ontology"),
179            ValidationError::MissingSubtype { node_type, allowed } => {
180                write!(f, "'{node_type}' requires a subtype (allowed: {allowed:?})")
181            }
182            ValidationError::UnknownSubtype {
183                node_type,
184                subtype,
185                allowed,
186            } => write!(
187                f,
188                "'{node_type}' has no subtype '{subtype}' (allowed: {allowed:?})"
189            ),
190            ValidationError::UnexpectedSubtype { node_type, subtype } => write!(
191                f,
192                "'{node_type}' does not define subtypes, but got subtype '{subtype}'"
193            ),
194            ValidationError::ConstraintViolation {
195                type_name,
196                property,
197                constraint,
198                message,
199            } => write!(
200                f,
201                "'{type_name}'.'{property}' violates constraint '{constraint}': {message}"
202            ),
203        }
204    }
205}
206
207/// An additive ontology extension — monotonic evolution only (R-03).
208#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
209pub struct OntologyExtension {
210    /// New node types to add.
211    #[serde(default)]
212    pub node_types: BTreeMap<String, NodeTypeDef>,
213    /// New edge types to add.
214    #[serde(default)]
215    pub edge_types: BTreeMap<String, EdgeTypeDef>,
216    /// Updates to existing node types (add properties, subtypes, relax required).
217    #[serde(default)]
218    pub node_type_updates: BTreeMap<String, NodeTypeUpdate>,
219}
220
221/// Additive update to an existing node type.
222#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
223pub struct NodeTypeUpdate {
224    /// New optional properties to add.
225    #[serde(default)]
226    pub add_properties: BTreeMap<String, PropertyDef>,
227    /// Properties to relax from required to optional.
228    #[serde(default)]
229    pub relax_properties: Vec<String>,
230    /// New subtypes to add.
231    #[serde(default)]
232    pub add_subtypes: BTreeMap<String, SubtypeDef>,
233}
234
235/// Errors from monotonic ontology extension (R-03).
236#[derive(Debug, Clone, PartialEq)]
237pub enum MonotonicityError {
238    DuplicateNodeType(String),
239    DuplicateEdgeType(String),
240    UnknownNodeType(String),
241    DuplicateProperty {
242        type_name: String,
243        property: String,
244    },
245    UnknownProperty {
246        type_name: String,
247        property: String,
248    },
249    /// Wraps a ValidationError from validate_self() after merge.
250    ValidationFailed(ValidationError),
251}
252
253impl std::fmt::Display for MonotonicityError {
254    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255        match self {
256            MonotonicityError::DuplicateNodeType(t) => {
257                write!(f, "node type '{t}' already exists")
258            }
259            MonotonicityError::DuplicateEdgeType(t) => {
260                write!(f, "edge type '{t}' already exists")
261            }
262            MonotonicityError::UnknownNodeType(t) => {
263                write!(f, "cannot update unknown node type '{t}'")
264            }
265            MonotonicityError::DuplicateProperty {
266                type_name,
267                property,
268            } => {
269                write!(f, "property '{property}' already exists on '{type_name}'")
270            }
271            MonotonicityError::UnknownProperty {
272                type_name,
273                property,
274            } => {
275                write!(
276                    f,
277                    "property '{property}' does not exist on '{type_name}' (cannot relax)"
278                )
279            }
280            MonotonicityError::ValidationFailed(e) => {
281                write!(f, "ontology validation failed after merge: {e}")
282            }
283        }
284    }
285}
286
287impl Ontology {
288    // -- RDFS-level class hierarchy (Step 2) --
289
290    /// Return all ancestor types of `node_type` (transitive parent_type chain).
291    /// Does not include `node_type` itself. Returns empty vec if no parent.
292    pub fn ancestors(&self, node_type: &str) -> Vec<&str> {
293        let mut result = Vec::new();
294        let mut current = node_type;
295        // Guard against cycles (max 100 levels — no real ontology is deeper)
296        for _ in 0..100 {
297            match self
298                .node_types
299                .get(current)
300                .and_then(|d| d.parent_type.as_deref())
301            {
302                Some(parent) => {
303                    result.push(parent);
304                    current = parent;
305                }
306                None => break,
307            }
308        }
309        result
310    }
311
312    /// Return all descendant types of `node_type` (types whose ancestor chain includes it).
313    /// Does not include `node_type` itself.
314    pub fn descendants(&self, node_type: &str) -> Vec<&str> {
315        // Collect all types that have node_type anywhere in their ancestor chain.
316        self.node_types
317            .iter()
318            .filter(|(name, _)| {
319                name.as_str() != node_type && self.ancestors(name).contains(&node_type)
320            })
321            .map(|(name, _)| name.as_str())
322            .collect()
323    }
324
325    /// Check if `child_type` is the same as or a descendant of `parent_type`.
326    pub fn is_subtype_of(&self, child_type: &str, parent_type: &str) -> bool {
327        child_type == parent_type || self.ancestors(child_type).contains(&parent_type)
328    }
329
330    /// Get all properties for a type, including those inherited from ancestors.
331    /// Ancestors' properties are applied first (most general), then overridden
332    /// by more specific types. Same order as Python MRO: parent first, child overrides.
333    pub fn effective_properties(&self, node_type: &str) -> BTreeMap<String, PropertyDef> {
334        let mut chain: Vec<&str> = self.ancestors(node_type);
335        chain.reverse(); // most general first
336        chain.push(node_type);
337
338        let mut props = BTreeMap::new();
339        for t in chain {
340            if let Some(def) = self.node_types.get(t) {
341                for (k, v) in &def.properties {
342                    props.insert(k.clone(), v.clone());
343                }
344            }
345        }
346        props
347    }
348
349    /// Validate that a node type exists and its properties conform.
350    ///
351    /// If the type defines subtypes (D-024), `subtype` must be `Some` and
352    /// properties are validated against the subtype's definition.
353    /// If the type does not define subtypes, `subtype` must be `None`.
354    pub fn validate_node(
355        &self,
356        node_type: &str,
357        subtype: Option<&str>,
358        properties: &BTreeMap<String, Value>,
359    ) -> Result<(), ValidationError> {
360        let def = self
361            .node_types
362            .get(node_type)
363            .ok_or_else(|| ValidationError::UnknownNodeType(node_type.to_string()))?;
364
365        // Step 2: use effective_properties (includes inherited from ancestors)
366        let base_props = self.effective_properties(node_type);
367
368        match (&def.subtypes, subtype) {
369            // Type has subtypes and caller provided one
370            (Some(subtypes), Some(st)) => {
371                match subtypes.get(st) {
372                    Some(st_def) => {
373                        // Known subtype — merge inherited + type-level + subtype-level
374                        let mut merged = base_props;
375                        merged.extend(st_def.properties.clone());
376                        validate_properties(node_type, &merged, properties)
377                    }
378                    None => {
379                        // D-026: unknown subtype — validate inherited + type-level only
380                        validate_properties(node_type, &base_props, properties)
381                    }
382                }
383            }
384            // Type has subtypes but caller didn't provide one — error
385            (Some(subtypes), None) => Err(ValidationError::MissingSubtype {
386                node_type: node_type.to_string(),
387                allowed: subtypes.keys().cloned().collect(),
388            }),
389            // D-026: accept subtypes even if type doesn't declare any
390            (None, Some(_st)) => validate_properties(node_type, &base_props, properties),
391            // Type has no subtypes and caller didn't provide one — validate as before
392            (None, None) => validate_properties(node_type, &base_props, properties),
393        }
394    }
395
396    /// Validate that an edge type exists, source/target types are allowed,
397    /// and properties conform.
398    pub fn validate_edge(
399        &self,
400        edge_type: &str,
401        source_node_type: &str,
402        target_node_type: &str,
403        properties: &BTreeMap<String, Value>,
404    ) -> Result<(), ValidationError> {
405        let def = self
406            .edge_types
407            .get(edge_type)
408            .ok_or_else(|| ValidationError::UnknownEdgeType(edge_type.to_string()))?;
409
410        // Hierarchy-aware: accept if actual type IS one of the allowed types
411        // OR is a descendant of any allowed type (RDFS rdfs9).
412        if !def
413            .source_types
414            .iter()
415            .any(|t| self.is_subtype_of(source_node_type, t))
416        {
417            return Err(ValidationError::InvalidSource {
418                edge_type: edge_type.to_string(),
419                node_type: source_node_type.to_string(),
420                allowed: def.source_types.clone(),
421            });
422        }
423
424        if !def
425            .target_types
426            .iter()
427            .any(|t| self.is_subtype_of(target_node_type, t))
428        {
429            return Err(ValidationError::InvalidTarget {
430                edge_type: edge_type.to_string(),
431                node_type: target_node_type.to_string(),
432                allowed: def.target_types.clone(),
433            });
434        }
435
436        validate_properties(edge_type, &def.properties, properties)
437    }
438
439    /// Validate a single property update against the ontology.
440    /// Checks that the value type matches the property definition.
441    /// Unknown properties are accepted (D-026: ontology defines minimum, not maximum).
442    pub fn validate_property_update(
443        &self,
444        node_type: &str,
445        subtype: Option<&str>,
446        key: &str,
447        value: &Value,
448    ) -> Result<(), ValidationError> {
449        let def = match self.node_types.get(node_type) {
450            Some(d) => d,
451            None => return Ok(()), // Unknown node type — can't validate
452        };
453
454        // Merge type-level + subtype-level property definitions
455        let mut merged = def.properties.clone();
456        if let (Some(subtypes), Some(st)) = (&def.subtypes, subtype) {
457            if let Some(st_def) = subtypes.get(st) {
458                merged.extend(st_def.properties.clone());
459            }
460        }
461
462        // D-026: unknown properties accepted without validation
463        let prop_def = match merged.get(key) {
464            Some(d) => d,
465            None => return Ok(()),
466        };
467
468        // Type check
469        if prop_def.value_type != ValueType::Any && !value_matches_type(value, &prop_def.value_type)
470        {
471            return Err(ValidationError::WrongPropertyType {
472                type_name: node_type.to_string(),
473                property: key.to_string(),
474                expected: prop_def.value_type.clone(),
475                got: value_type_name(value).to_string(),
476            });
477        }
478
479        // Constraint check
480        if let Some(constraints) = &prop_def.constraints {
481            validate_constraints(node_type, key, value, constraints)?;
482        }
483
484        Ok(())
485    }
486
487    /// Validate that the ontology itself is internally consistent.
488    /// All source_types/target_types in edge defs must reference existing node types.
489    pub fn validate_self(&self) -> Result<(), ValidationError> {
490        // Validate edge source/target references
491        for (edge_name, edge_def) in &self.edge_types {
492            for src in &edge_def.source_types {
493                if !self.node_types.contains_key(src) {
494                    return Err(ValidationError::InvalidSource {
495                        edge_type: edge_name.clone(),
496                        node_type: src.clone(),
497                        allowed: self.node_types.keys().cloned().collect(),
498                    });
499                }
500            }
501            for tgt in &edge_def.target_types {
502                if !self.node_types.contains_key(tgt) {
503                    return Err(ValidationError::InvalidTarget {
504                        edge_type: edge_name.clone(),
505                        node_type: tgt.clone(),
506                        allowed: self.node_types.keys().cloned().collect(),
507                    });
508                }
509            }
510        }
511        // Validate parent_type references (Step 2: class hierarchy)
512        for (type_name, type_def) in &self.node_types {
513            if let Some(ref parent) = type_def.parent_type {
514                if !self.node_types.contains_key(parent) {
515                    return Err(ValidationError::UnknownNodeType(format!(
516                        "{}: parent_type '{}' does not exist",
517                        type_name, parent
518                    )));
519                }
520            }
521        }
522        Ok(())
523    }
524
525    /// R-03: Merge an additive extension into this ontology.
526    /// Only monotonic (additive) changes are allowed:
527    /// - New node types (must not already exist)
528    /// - New edge types (must not already exist)
529    /// - Updates to existing node types: add properties, relax required→optional, add subtypes
530    pub fn merge_extension(&mut self, ext: &OntologyExtension) -> Result<(), MonotonicityError> {
531        // Validate: new node types don't already exist
532        for name in ext.node_types.keys() {
533            if self.node_types.contains_key(name) {
534                return Err(MonotonicityError::DuplicateNodeType(name.clone()));
535            }
536        }
537
538        // Validate: new edge types don't already exist
539        for name in ext.edge_types.keys() {
540            if self.edge_types.contains_key(name) {
541                return Err(MonotonicityError::DuplicateEdgeType(name.clone()));
542            }
543        }
544
545        // Validate node_type_updates reference existing types
546        for (type_name, update) in &ext.node_type_updates {
547            let def = self
548                .node_types
549                .get(type_name)
550                .ok_or_else(|| MonotonicityError::UnknownNodeType(type_name.clone()))?;
551
552            // Validate: add_properties don't already exist
553            for prop_name in update.add_properties.keys() {
554                if def.properties.contains_key(prop_name) {
555                    return Err(MonotonicityError::DuplicateProperty {
556                        type_name: type_name.clone(),
557                        property: prop_name.clone(),
558                    });
559                }
560            }
561
562            // Validate: relax_properties exist and are currently required
563            for prop_name in &update.relax_properties {
564                match def.properties.get(prop_name) {
565                    Some(prop_def) if prop_def.required => {} // ok
566                    Some(_) => {} // already optional — idempotent, allow it
567                    None => {
568                        return Err(MonotonicityError::UnknownProperty {
569                            type_name: type_name.clone(),
570                            property: prop_name.clone(),
571                        });
572                    }
573                }
574            }
575
576            // Validate: add_subtypes don't already exist (if subtypes are defined)
577            if !update.add_subtypes.is_empty() {
578                if let Some(ref existing) = def.subtypes {
579                    for st_name in update.add_subtypes.keys() {
580                        if existing.contains_key(st_name) {
581                            return Err(MonotonicityError::DuplicateProperty {
582                                type_name: type_name.clone(),
583                                property: format!("subtype:{st_name}"),
584                            });
585                        }
586                    }
587                }
588            }
589        }
590
591        // Apply: extend node_types
592        self.node_types.extend(ext.node_types.clone());
593
594        // Apply: extend edge_types
595        self.edge_types.extend(ext.edge_types.clone());
596
597        // Apply: update existing node types
598        for (type_name, update) in &ext.node_type_updates {
599            let def = self.node_types.get_mut(type_name).unwrap(); // validated above
600
601            // Add new properties
602            def.properties.extend(update.add_properties.clone());
603
604            // Relax required → optional
605            for prop_name in &update.relax_properties {
606                if let Some(prop_def) = def.properties.get_mut(prop_name) {
607                    prop_def.required = false;
608                }
609            }
610
611            // Add subtypes
612            if !update.add_subtypes.is_empty() {
613                let subtypes = def.subtypes.get_or_insert_with(BTreeMap::new);
614                subtypes.extend(update.add_subtypes.clone());
615            }
616        }
617
618        // Validate the merged ontology is internally consistent
619        self.validate_self()
620            .map_err(MonotonicityError::ValidationFailed)?;
621
622        Ok(())
623    }
624}
625
626/// Validate properties against their definitions.
627fn validate_properties(
628    type_name: &str,
629    defs: &BTreeMap<String, PropertyDef>,
630    values: &BTreeMap<String, Value>,
631) -> Result<(), ValidationError> {
632    // Check required properties are present
633    for (prop_name, prop_def) in defs {
634        if prop_def.required && !values.contains_key(prop_name) {
635            return Err(ValidationError::MissingRequiredProperty {
636                type_name: type_name.to_string(),
637                property: prop_name.clone(),
638            });
639        }
640    }
641
642    // Check all provided properties are known and correctly typed
643    for (prop_name, value) in values {
644        // D-026: accept unknown properties without validation.
645        // The ontology defines the minimum, not the maximum.
646        let prop_def = match defs.get(prop_name) {
647            Some(def) => def,
648            None => continue,
649        };
650
651        if prop_def.value_type != ValueType::Any {
652            let actual_type = value_type_name(value);
653            let expected = &prop_def.value_type;
654            if !value_matches_type(value, expected) {
655                return Err(ValidationError::WrongPropertyType {
656                    type_name: type_name.to_string(),
657                    property: prop_name.clone(),
658                    expected: expected.clone(),
659                    got: actual_type.to_string(),
660                });
661            }
662        }
663
664        // Validate constraints (if any)
665        if let Some(constraints) = &prop_def.constraints {
666            validate_constraints(type_name, prop_name, value, constraints)?;
667        }
668    }
669
670    Ok(())
671}
672
673/// Validate a property value against its constraints.
674/// Built-in constraints: "enum" (allowed values), "min"/"max" (numeric range).
675/// Unknown constraint names are silently ignored — enables forward compatibility
676/// with community-contributed constraint types.
677fn validate_constraints(
678    type_name: &str,
679    prop_name: &str,
680    value: &Value,
681    constraints: &BTreeMap<String, serde_json::Value>,
682) -> Result<(), ValidationError> {
683    // "enum": list of allowed string values
684    if let Some(serde_json::Value::Array(allowed)) = constraints.get("enum") {
685        if let Value::String(s) = value {
686            let allowed_strs: Vec<&str> = allowed.iter().filter_map(|v| v.as_str()).collect();
687            if !allowed_strs.contains(&s.as_str()) {
688                return constraint_err(
689                    type_name,
690                    prop_name,
691                    "enum",
692                    format!("value '{}' not in allowed set {:?}", s, allowed_strs),
693                );
694            }
695        }
696    }
697
698    // Numeric bounds (4 variants share the same extract-compare pattern)
699    check_numeric_bound(
700        type_name,
701        prop_name,
702        value,
703        constraints,
704        "min",
705        |n, b| n < b,
706        |n, b| format!("value {} is less than minimum {}", n, b),
707    )?;
708    check_numeric_bound(
709        type_name,
710        prop_name,
711        value,
712        constraints,
713        "max",
714        |n, b| n > b,
715        |n, b| format!("value {} exceeds maximum {}", n, b),
716    )?;
717    check_numeric_bound(
718        type_name,
719        prop_name,
720        value,
721        constraints,
722        "min_exclusive",
723        |n, b| n <= b,
724        |n, b| format!("value {} must be greater than {}", n, b),
725    )?;
726    check_numeric_bound(
727        type_name,
728        prop_name,
729        value,
730        constraints,
731        "max_exclusive",
732        |n, b| n >= b,
733        |n, b| format!("value {} must be less than {}", n, b),
734    )?;
735
736    // String length bounds
737    check_string_length(
738        type_name,
739        prop_name,
740        value,
741        constraints,
742        "min_length",
743        |len, bound| len < bound,
744        |len, bound| format!("string length {} is less than minimum {}", len, bound),
745    )?;
746    check_string_length(
747        type_name,
748        prop_name,
749        value,
750        constraints,
751        "max_length",
752        |len, bound| len > bound,
753        |len, bound| format!("string length {} exceeds maximum {}", len, bound),
754    )?;
755
756    // "pattern": regex match on string values
757    if let Some(serde_json::Value::String(pattern)) = constraints.get("pattern") {
758        if let Value::String(s) = value {
759            match regex::Regex::new(pattern) {
760                Ok(re) if !re.is_match(s) => {
761                    return constraint_err(
762                        type_name,
763                        prop_name,
764                        "pattern",
765                        format!("value '{}' does not match pattern '{}'", s, pattern),
766                    );
767                }
768                Err(e) => {
769                    return constraint_err(
770                        type_name,
771                        prop_name,
772                        "pattern",
773                        format!("invalid regex pattern '{}': {}", pattern, e),
774                    );
775                }
776                _ => {}
777            }
778        }
779    }
780
781    // Unknown constraint names are silently ignored (forward compat).
782    Ok(())
783}
784
785/// Helper: extract numeric value from a Value.
786fn value_as_f64(value: &Value) -> Option<f64> {
787    match value {
788        Value::Int(n) => Some(*n as f64),
789        Value::Float(n) => Some(*n),
790        _ => None,
791    }
792}
793
794/// Helper: check a numeric bound constraint.
795fn check_numeric_bound(
796    type_name: &str,
797    prop_name: &str,
798    value: &Value,
799    constraints: &BTreeMap<String, serde_json::Value>,
800    key: &str,
801    violates: impl Fn(f64, f64) -> bool,
802    msg: impl Fn(f64, f64) -> String,
803) -> Result<(), ValidationError> {
804    if let Some(bound_val) = constraints.get(key) {
805        if let Some(bound) = bound_val.as_f64() {
806            if let Some(n) = value_as_f64(value) {
807                if violates(n, bound) {
808                    return constraint_err(type_name, prop_name, key, msg(n, bound));
809                }
810            }
811        }
812    }
813    Ok(())
814}
815
816/// Helper: check a string length constraint.
817fn check_string_length(
818    type_name: &str,
819    prop_name: &str,
820    value: &Value,
821    constraints: &BTreeMap<String, serde_json::Value>,
822    key: &str,
823    violates: impl Fn(u64, u64) -> bool,
824    msg: impl Fn(u64, u64) -> String,
825) -> Result<(), ValidationError> {
826    if let Some(serde_json::Value::Number(n)) = constraints.get(key) {
827        if let (Some(bound), Value::String(s)) = (n.as_u64(), value) {
828            if violates(s.len() as u64, bound) {
829                return constraint_err(type_name, prop_name, key, msg(s.len() as u64, bound));
830            }
831        }
832    }
833    Ok(())
834}
835
836/// Helper: construct a ConstraintViolation error.
837fn constraint_err(
838    type_name: &str,
839    prop_name: &str,
840    constraint: &str,
841    message: String,
842) -> Result<(), ValidationError> {
843    Err(ValidationError::ConstraintViolation {
844        type_name: type_name.to_string(),
845        property: prop_name.to_string(),
846        constraint: constraint.to_string(),
847        message,
848    })
849}
850
851fn value_matches_type(value: &Value, expected: &ValueType) -> bool {
852    matches!(
853        (value, expected),
854        (Value::Null, _)
855            | (Value::String(_), ValueType::String)
856            | (Value::Int(_), ValueType::Int)
857            | (Value::Float(_), ValueType::Float)
858            | (Value::Bool(_), ValueType::Bool)
859            | (Value::List(_), ValueType::List)
860            | (Value::Map(_), ValueType::Map)
861            | (_, ValueType::Any)
862    )
863}
864
865fn value_type_name(value: &Value) -> &'static str {
866    match value {
867        Value::Null => "null",
868        Value::Bool(_) => "bool",
869        Value::Int(_) => "int",
870        Value::Float(_) => "float",
871        Value::String(_) => "string",
872        Value::List(_) => "list",
873        Value::Map(_) => "map",
874    }
875}
876
877#[cfg(test)]
878mod tests {
879    use super::*;
880
881    fn devops_ontology() -> Ontology {
882        Ontology {
883            node_types: BTreeMap::from([
884                (
885                    "signal".into(),
886                    NodeTypeDef {
887                        description: Some("Something observed".into()),
888                        properties: BTreeMap::from([(
889                            "severity".into(),
890                            PropertyDef {
891                                value_type: ValueType::String,
892                                required: true,
893                                description: None,
894                                constraints: None,
895                            },
896                        )]),
897                        subtypes: None,
898                        parent_type: None,
899                    },
900                ),
901                (
902                    "entity".into(),
903                    NodeTypeDef {
904                        description: Some("Something that exists".into()),
905                        properties: BTreeMap::from([
906                            (
907                                "status".into(),
908                                PropertyDef {
909                                    value_type: ValueType::String,
910                                    required: false,
911                                    description: None,
912                                    constraints: None,
913                                },
914                            ),
915                            (
916                                "port".into(),
917                                PropertyDef {
918                                    value_type: ValueType::Int,
919                                    required: false,
920                                    description: None,
921                                    constraints: None,
922                                },
923                            ),
924                        ]),
925                        subtypes: None,
926                        parent_type: None,
927                    },
928                ),
929                (
930                    "rule".into(),
931                    NodeTypeDef {
932                        description: None,
933                        properties: BTreeMap::new(),
934                        subtypes: None,
935                        parent_type: None,
936                    },
937                ),
938                (
939                    "action".into(),
940                    NodeTypeDef {
941                        description: None,
942                        properties: BTreeMap::new(),
943                        subtypes: None,
944                        parent_type: None,
945                    },
946                ),
947            ]),
948            edge_types: BTreeMap::from([
949                (
950                    "OBSERVES".into(),
951                    EdgeTypeDef {
952                        description: None,
953                        source_types: vec!["signal".into()],
954                        target_types: vec!["entity".into()],
955                        properties: BTreeMap::new(),
956                    },
957                ),
958                (
959                    "TRIGGERS".into(),
960                    EdgeTypeDef {
961                        description: None,
962                        source_types: vec!["signal".into()],
963                        target_types: vec!["rule".into()],
964                        properties: BTreeMap::new(),
965                    },
966                ),
967                (
968                    "RUNS_ON".into(),
969                    EdgeTypeDef {
970                        description: None,
971                        source_types: vec!["entity".into()],
972                        target_types: vec!["entity".into()],
973                        properties: BTreeMap::new(),
974                    },
975                ),
976            ]),
977        }
978    }
979
980    // --- Node validation ---
981
982    #[test]
983    fn validate_node_valid() {
984        let ont = devops_ontology();
985        let props = BTreeMap::from([("severity".into(), Value::String("critical".into()))]);
986        assert!(ont.validate_node("signal", None, &props).is_ok());
987    }
988
989    #[test]
990    fn validate_node_unknown_type() {
991        let ont = devops_ontology();
992        let err = ont
993            .validate_node("potato", None, &BTreeMap::new())
994            .unwrap_err();
995        assert!(matches!(err, ValidationError::UnknownNodeType(t) if t == "potato"));
996    }
997
998    #[test]
999    fn validate_node_missing_required() {
1000        let ont = devops_ontology();
1001        let err = ont
1002            .validate_node("signal", None, &BTreeMap::new())
1003            .unwrap_err();
1004        assert!(
1005            matches!(err, ValidationError::MissingRequiredProperty { property, .. } if property == "severity")
1006        );
1007    }
1008
1009    #[test]
1010    fn validate_node_wrong_type() {
1011        let ont = devops_ontology();
1012        let props = BTreeMap::from([("severity".into(), Value::Int(5))]);
1013        let err = ont.validate_node("signal", None, &props).unwrap_err();
1014        assert!(
1015            matches!(err, ValidationError::WrongPropertyType { property, .. } if property == "severity")
1016        );
1017    }
1018
1019    #[test]
1020    fn validate_node_unknown_property_accepted() {
1021        // D-026: unknown properties are accepted without validation
1022        let ont = devops_ontology();
1023        let props = BTreeMap::from([
1024            ("severity".into(), Value::String("warn".into())),
1025            ("bogus".into(), Value::Bool(true)),
1026        ]);
1027        assert!(ont.validate_node("signal", None, &props).is_ok());
1028    }
1029
1030    #[test]
1031    fn validate_node_optional_property_absent() {
1032        let ont = devops_ontology();
1033        // entity has optional "status" — omitting it is fine
1034        assert!(ont.validate_node("entity", None, &BTreeMap::new()).is_ok());
1035    }
1036
1037    #[test]
1038    fn validate_node_null_accepted_for_any_type() {
1039        let ont = devops_ontology();
1040        // Null is accepted for any typed property (represents absence)
1041        let props = BTreeMap::from([("severity".into(), Value::Null)]);
1042        assert!(ont.validate_node("signal", None, &props).is_ok());
1043    }
1044
1045    // --- Edge validation ---
1046
1047    #[test]
1048    fn validate_edge_valid() {
1049        let ont = devops_ontology();
1050        assert!(ont
1051            .validate_edge("OBSERVES", "signal", "entity", &BTreeMap::new())
1052            .is_ok());
1053    }
1054
1055    #[test]
1056    fn validate_edge_unknown_type() {
1057        let ont = devops_ontology();
1058        let err = ont
1059            .validate_edge("FLIES_TO", "signal", "entity", &BTreeMap::new())
1060            .unwrap_err();
1061        assert!(matches!(err, ValidationError::UnknownEdgeType(t) if t == "FLIES_TO"));
1062    }
1063
1064    #[test]
1065    fn validate_edge_invalid_source() {
1066        let ont = devops_ontology();
1067        // OBSERVES requires source=signal, not entity
1068        let err = ont
1069            .validate_edge("OBSERVES", "entity", "entity", &BTreeMap::new())
1070            .unwrap_err();
1071        assert!(matches!(err, ValidationError::InvalidSource { .. }));
1072    }
1073
1074    #[test]
1075    fn validate_edge_invalid_target() {
1076        let ont = devops_ontology();
1077        // OBSERVES requires target=entity, not signal
1078        let err = ont
1079            .validate_edge("OBSERVES", "signal", "signal", &BTreeMap::new())
1080            .unwrap_err();
1081        assert!(matches!(err, ValidationError::InvalidTarget { .. }));
1082    }
1083
1084    // --- Self-validation ---
1085
1086    #[test]
1087    fn validate_self_consistent() {
1088        let ont = devops_ontology();
1089        assert!(ont.validate_self().is_ok());
1090    }
1091
1092    #[test]
1093    fn validate_self_dangling_source() {
1094        let ont = Ontology {
1095            node_types: BTreeMap::from([(
1096                "entity".into(),
1097                NodeTypeDef {
1098                    description: None,
1099                    properties: BTreeMap::new(),
1100                    subtypes: None,
1101                    parent_type: None,
1102                },
1103            )]),
1104            edge_types: BTreeMap::from([(
1105                "OBSERVES".into(),
1106                EdgeTypeDef {
1107                    description: None,
1108                    source_types: vec!["ghost".into()], // doesn't exist
1109                    target_types: vec!["entity".into()],
1110                    properties: BTreeMap::new(),
1111                },
1112            )]),
1113        };
1114        let err = ont.validate_self().unwrap_err();
1115        assert!(
1116            matches!(err, ValidationError::InvalidSource { node_type, .. } if node_type == "ghost")
1117        );
1118    }
1119
1120    #[test]
1121    fn validate_self_dangling_target() {
1122        let ont = Ontology {
1123            node_types: BTreeMap::from([(
1124                "signal".into(),
1125                NodeTypeDef {
1126                    description: None,
1127                    properties: BTreeMap::new(),
1128                    subtypes: None,
1129                    parent_type: None,
1130                },
1131            )]),
1132            edge_types: BTreeMap::from([(
1133                "OBSERVES".into(),
1134                EdgeTypeDef {
1135                    description: None,
1136                    source_types: vec!["signal".into()],
1137                    target_types: vec!["phantom".into()], // doesn't exist
1138                    properties: BTreeMap::new(),
1139                },
1140            )]),
1141        };
1142        let err = ont.validate_self().unwrap_err();
1143        assert!(
1144            matches!(err, ValidationError::InvalidTarget { node_type, .. } if node_type == "phantom")
1145        );
1146    }
1147
1148    // --- Serialization ---
1149
1150    // --- New constraint tests (Step 1: SHACL-inspired vocabulary) ---
1151
1152    fn constrained_ontology() -> Ontology {
1153        Ontology {
1154            node_types: BTreeMap::from([(
1155                "item".into(),
1156                NodeTypeDef {
1157                    description: None,
1158                    properties: BTreeMap::from([
1159                        (
1160                            "slug".into(),
1161                            PropertyDef {
1162                                value_type: ValueType::String,
1163                                required: false,
1164                                description: None,
1165                                constraints: Some(BTreeMap::from([
1166                                    (
1167                                        "pattern".to_string(),
1168                                        serde_json::Value::String("^[a-z0-9-]+$".to_string()),
1169                                    ),
1170                                    (
1171                                        "min_length".to_string(),
1172                                        serde_json::Value::Number(1.into()),
1173                                    ),
1174                                    (
1175                                        "max_length".to_string(),
1176                                        serde_json::Value::Number(63.into()),
1177                                    ),
1178                                ])),
1179                            },
1180                        ),
1181                        (
1182                            "score".into(),
1183                            PropertyDef {
1184                                value_type: ValueType::Float,
1185                                required: false,
1186                                description: None,
1187                                constraints: Some(BTreeMap::from([
1188                                    ("min_exclusive".to_string(), serde_json::json!(0.0)),
1189                                    ("max_exclusive".to_string(), serde_json::json!(100.0)),
1190                                ])),
1191                            },
1192                        ),
1193                    ]),
1194                    subtypes: None,
1195                    parent_type: None,
1196                },
1197            )]),
1198            edge_types: BTreeMap::new(),
1199        }
1200    }
1201
1202    #[test]
1203    fn pattern_valid_slug() {
1204        let ont = constrained_ontology();
1205        let props = BTreeMap::from([("slug".into(), Value::String("my-project-1".into()))]);
1206        assert!(ont.validate_node("item", None, &props).is_ok());
1207    }
1208
1209    #[test]
1210    fn pattern_rejects_uppercase() {
1211        let ont = constrained_ontology();
1212        let props = BTreeMap::from([("slug".into(), Value::String("My-Project".into()))]);
1213        assert!(ont.validate_node("item", None, &props).is_err());
1214    }
1215
1216    #[test]
1217    fn pattern_rejects_spaces() {
1218        let ont = constrained_ontology();
1219        let props = BTreeMap::from([("slug".into(), Value::String("has space".into()))]);
1220        assert!(ont.validate_node("item", None, &props).is_err());
1221    }
1222
1223    #[test]
1224    fn min_length_accepts_valid() {
1225        let ont = constrained_ontology();
1226        let props = BTreeMap::from([("slug".into(), Value::String("a".into()))]);
1227        assert!(ont.validate_node("item", None, &props).is_ok());
1228    }
1229
1230    #[test]
1231    fn min_length_rejects_empty() {
1232        let ont = constrained_ontology();
1233        let props = BTreeMap::from([("slug".into(), Value::String("".into()))]);
1234        let err = ont.validate_node("item", None, &props).unwrap_err();
1235        assert!(
1236            matches!(err, ValidationError::ConstraintViolation { constraint, .. } if constraint == "min_length")
1237        );
1238    }
1239
1240    #[test]
1241    fn max_length_rejects_too_long() {
1242        let ont = constrained_ontology();
1243        let long = "a".repeat(64);
1244        let props = BTreeMap::from([("slug".into(), Value::String(long))]);
1245        let err = ont.validate_node("item", None, &props).unwrap_err();
1246        assert!(
1247            matches!(err, ValidationError::ConstraintViolation { constraint, .. } if constraint == "max_length")
1248        );
1249    }
1250
1251    #[test]
1252    fn max_length_accepts_boundary() {
1253        let ont = constrained_ontology();
1254        let exact = "a".repeat(63);
1255        let props = BTreeMap::from([("slug".into(), Value::String(exact))]);
1256        assert!(ont.validate_node("item", None, &props).is_ok());
1257    }
1258
1259    #[test]
1260    fn min_exclusive_rejects_boundary() {
1261        let ont = constrained_ontology();
1262        let props = BTreeMap::from([("score".into(), Value::Float(0.0))]);
1263        let err = ont.validate_node("item", None, &props).unwrap_err();
1264        assert!(
1265            matches!(err, ValidationError::ConstraintViolation { constraint, .. } if constraint == "min_exclusive")
1266        );
1267    }
1268
1269    #[test]
1270    fn min_exclusive_accepts_above() {
1271        let ont = constrained_ontology();
1272        let props = BTreeMap::from([("score".into(), Value::Float(0.001))]);
1273        assert!(ont.validate_node("item", None, &props).is_ok());
1274    }
1275
1276    #[test]
1277    fn max_exclusive_rejects_boundary() {
1278        let ont = constrained_ontology();
1279        let props = BTreeMap::from([("score".into(), Value::Float(100.0))]);
1280        let err = ont.validate_node("item", None, &props).unwrap_err();
1281        assert!(
1282            matches!(err, ValidationError::ConstraintViolation { constraint, .. } if constraint == "max_exclusive")
1283        );
1284    }
1285
1286    #[test]
1287    fn max_exclusive_accepts_below() {
1288        let ont = constrained_ontology();
1289        let props = BTreeMap::from([("score".into(), Value::Float(99.999))]);
1290        assert!(ont.validate_node("item", None, &props).is_ok());
1291    }
1292
1293    // --- Serialization ---
1294
1295    #[test]
1296    fn ontology_roundtrip_msgpack() {
1297        let ont = devops_ontology();
1298        let bytes = rmp_serde::to_vec(&ont).unwrap();
1299        let decoded: Ontology = rmp_serde::from_slice(&bytes).unwrap();
1300        assert_eq!(ont, decoded);
1301    }
1302
1303    #[test]
1304    fn ontology_roundtrip_json() {
1305        let ont = devops_ontology();
1306        let json = serde_json::to_string(&ont).unwrap();
1307        let decoded: Ontology = serde_json::from_str(&json).unwrap();
1308        assert_eq!(ont, decoded);
1309    }
1310
1311    // --- Step 2: RDFS class hierarchy tests ---
1312
1313    fn hierarchy_ontology() -> Ontology {
1314        // thing → entity → server (two levels)
1315        //       → event
1316        Ontology {
1317            node_types: BTreeMap::from([
1318                (
1319                    "thing".into(),
1320                    NodeTypeDef {
1321                        description: None,
1322                        properties: BTreeMap::from([(
1323                            "name".into(),
1324                            PropertyDef {
1325                                value_type: ValueType::String,
1326                                required: true,
1327                                description: None,
1328                                constraints: None,
1329                            },
1330                        )]),
1331                        subtypes: None,
1332                        parent_type: None, // root
1333                    },
1334                ),
1335                (
1336                    "entity".into(),
1337                    NodeTypeDef {
1338                        description: None,
1339                        properties: BTreeMap::from([(
1340                            "status".into(),
1341                            PropertyDef {
1342                                value_type: ValueType::String,
1343                                required: false,
1344                                description: None,
1345                                constraints: None,
1346                            },
1347                        )]),
1348                        subtypes: None,
1349                        parent_type: Some("thing".into()), // entity extends thing
1350                    },
1351                ),
1352                (
1353                    "server".into(),
1354                    NodeTypeDef {
1355                        description: None,
1356                        properties: BTreeMap::from([(
1357                            "ip".into(),
1358                            PropertyDef {
1359                                value_type: ValueType::String,
1360                                required: false,
1361                                description: None,
1362                                constraints: None,
1363                            },
1364                        )]),
1365                        subtypes: None,
1366                        parent_type: Some("entity".into()), // server extends entity
1367                    },
1368                ),
1369                (
1370                    "event".into(),
1371                    NodeTypeDef {
1372                        description: None,
1373                        properties: BTreeMap::new(),
1374                        subtypes: None,
1375                        parent_type: Some("thing".into()), // event extends thing
1376                    },
1377                ),
1378            ]),
1379            edge_types: BTreeMap::from([(
1380                "RELATES_TO".into(),
1381                EdgeTypeDef {
1382                    description: None,
1383                    source_types: vec!["thing".into()], // accepts any thing descendant
1384                    target_types: vec!["entity".into()], // accepts entity or server
1385                    properties: BTreeMap::new(),
1386                },
1387            )]),
1388        }
1389    }
1390
1391    #[test]
1392    fn ancestors_empty_for_root() {
1393        let ont = hierarchy_ontology();
1394        assert!(ont.ancestors("thing").is_empty());
1395    }
1396
1397    #[test]
1398    fn ancestors_single_parent() {
1399        let ont = hierarchy_ontology();
1400        assert_eq!(ont.ancestors("entity"), vec!["thing"]);
1401    }
1402
1403    #[test]
1404    fn ancestors_transitive() {
1405        let ont = hierarchy_ontology();
1406        // server → entity → thing
1407        assert_eq!(ont.ancestors("server"), vec!["entity", "thing"]);
1408    }
1409
1410    #[test]
1411    fn descendants_of_root() {
1412        let ont = hierarchy_ontology();
1413        let mut desc = ont.descendants("thing");
1414        desc.sort();
1415        assert_eq!(desc, vec!["entity", "event", "server"]);
1416    }
1417
1418    #[test]
1419    fn descendants_of_entity() {
1420        let ont = hierarchy_ontology();
1421        assert_eq!(ont.descendants("entity"), vec!["server"]);
1422    }
1423
1424    #[test]
1425    fn descendants_of_leaf() {
1426        let ont = hierarchy_ontology();
1427        assert!(ont.descendants("server").is_empty());
1428    }
1429
1430    #[test]
1431    fn is_subtype_of_self() {
1432        let ont = hierarchy_ontology();
1433        assert!(ont.is_subtype_of("server", "server"));
1434    }
1435
1436    #[test]
1437    fn is_subtype_of_parent() {
1438        let ont = hierarchy_ontology();
1439        assert!(ont.is_subtype_of("server", "entity"));
1440        assert!(ont.is_subtype_of("server", "thing"));
1441    }
1442
1443    #[test]
1444    fn is_not_subtype_of_sibling() {
1445        let ont = hierarchy_ontology();
1446        assert!(!ont.is_subtype_of("server", "event"));
1447    }
1448
1449    #[test]
1450    fn effective_properties_inherits() {
1451        let ont = hierarchy_ontology();
1452        let props = ont.effective_properties("server");
1453        // server should have: name (from thing), status (from entity), ip (own)
1454        assert!(props.contains_key("name"));
1455        assert!(props.contains_key("status"));
1456        assert!(props.contains_key("ip"));
1457    }
1458
1459    #[test]
1460    fn effective_properties_root_has_own_only() {
1461        let ont = hierarchy_ontology();
1462        let props = ont.effective_properties("thing");
1463        assert!(props.contains_key("name"));
1464        assert!(!props.contains_key("status"));
1465    }
1466
1467    #[test]
1468    fn validate_node_inherits_required_from_ancestor() {
1469        let ont = hierarchy_ontology();
1470        // server requires "name" (inherited from thing)
1471        let err = ont.validate_node("server", None, &BTreeMap::new());
1472        assert!(err.is_err());
1473
1474        let props = BTreeMap::from([("name".into(), Value::String("web-01".into()))]);
1475        assert!(ont.validate_node("server", None, &props).is_ok());
1476    }
1477
1478    #[test]
1479    fn validate_edge_hierarchy_aware() {
1480        let ont = hierarchy_ontology();
1481        // RELATES_TO: source=thing, target=entity
1482        // server is-a thing, server is-a entity → both should pass
1483        let empty = BTreeMap::new();
1484        assert!(ont
1485            .validate_edge("RELATES_TO", "server", "server", &empty)
1486            .is_ok());
1487        assert!(ont
1488            .validate_edge("RELATES_TO", "event", "entity", &empty)
1489            .is_ok());
1490        assert!(ont
1491            .validate_edge("RELATES_TO", "thing", "entity", &empty)
1492            .is_ok());
1493    }
1494
1495    #[test]
1496    fn validate_edge_hierarchy_rejects_wrong_branch() {
1497        let ont = hierarchy_ontology();
1498        // RELATES_TO target must be entity or descendant. event is not entity's descendant.
1499        let empty = BTreeMap::new();
1500        assert!(ont
1501            .validate_edge("RELATES_TO", "thing", "event", &empty)
1502            .is_err());
1503    }
1504
1505    #[test]
1506    fn validate_self_rejects_dangling_parent() {
1507        let ont = Ontology {
1508            node_types: BTreeMap::from([(
1509                "orphan".into(),
1510                NodeTypeDef {
1511                    description: None,
1512                    properties: BTreeMap::new(),
1513                    subtypes: None,
1514                    parent_type: Some("ghost".into()), // doesn't exist
1515                },
1516            )]),
1517            edge_types: BTreeMap::new(),
1518        };
1519        assert!(ont.validate_self().is_err());
1520    }
1521}