#[allow(unused_imports)]
use alloc::{boxed::Box, format, string::String, string::ToString, vec, vec::Vec};
use alloc::collections::BTreeMap;
use alloc::collections::BTreeSet;
use crate::ontology::Vocabulary;
pub use crate::ontology::Lexical;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct RuntimeConcept {
pub name: String,
pub lexical: Option<Lexical>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Edge {
pub from: String,
pub to: String,
pub kind: EdgeKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum EdgeKind {
Identity,
Subsumption,
Parthood,
Causation,
Opposition,
Custom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Staging {
Embedded,
Async,
Mmap,
Composed,
}
#[derive(Debug, Clone)]
pub struct Ontology {
name: String,
source: String,
concepts: BTreeMap<String, RuntimeConcept>,
edges: BTreeSet<Edge>,
level: usize,
staging: Staging,
provenance: Vec<String>,
}
impl Ontology {
pub fn create(name: &str) -> OntologyBuilder {
OntologyBuilder {
name: String::from(name),
source: String::new(),
concepts: BTreeMap::new(),
edges: BTreeSet::new(),
base_level: 0,
provenance: Vec::new(),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn source(&self) -> &str {
&self.source
}
pub fn concepts(&self) -> &BTreeMap<String, RuntimeConcept> {
&self.concepts
}
pub fn concept(&self, name: &str) -> Option<&RuntimeConcept> {
self.concepts.get(name)
}
pub fn edges(&self) -> &BTreeSet<Edge> {
&self.edges
}
pub fn level(&self) -> usize {
self.level
}
pub fn staging(&self) -> Staging {
self.staging
}
pub fn provenance(&self) -> &[String] {
&self.provenance
}
pub fn couple(&self, other: &Ontology) -> Ontology {
let mut concepts = self.concepts.clone();
for (k, v) in &other.concepts {
concepts
.entry(k.clone())
.and_modify(|existing| merge_lexical(existing, v))
.or_insert_with(|| v.clone());
}
let mut edges = self.edges.clone();
edges.extend(other.edges.iter().cloned());
let provenance =
merge_provenance(&self.provenance, &self.name, &other.provenance, &other.name);
Ontology {
name: format!("{}||{}", self.name, other.name),
source: format!("{} + {}", self.source, other.source),
concepts,
edges,
level: core::cmp::max(self.level, other.level) + 1,
staging: Staging::Composed,
provenance,
}
}
pub fn compose(&self, other: &Ontology) -> Ontology {
let shared: BTreeSet<String> = self
.concepts
.keys()
.filter(|k| other.concepts.contains_key(*k))
.cloned()
.collect();
let mut concepts = self.concepts.clone();
for (k, v) in &other.concepts {
concepts
.entry(k.clone())
.and_modify(|existing| merge_lexical(existing, v))
.or_insert_with(|| v.clone());
}
let mut edges = self.edges.clone();
edges.extend(other.edges.iter().cloned());
let provenance =
merge_provenance(&self.provenance, &self.name, &other.provenance, &other.name);
let suffix = if shared.is_empty() {
format!("{}||{}", self.name, other.name)
} else {
format!("{}&{}", self.name, other.name)
};
Ontology {
name: suffix,
source: format!("{} + {}", self.source, other.source),
concepts,
edges,
level: core::cmp::max(self.level, other.level) + 1,
staging: Staging::Composed,
provenance,
}
}
pub fn couple_partial(&self, other: &Ontology, selected: &[&str]) -> Ontology {
let selected_set: BTreeSet<String> = selected.iter().map(|s| String::from(*s)).collect();
let mut concepts = self.concepts.clone();
for (k, v) in &other.concepts {
if selected_set.contains(k) {
concepts
.entry(k.clone())
.and_modify(|existing| merge_lexical(existing, v))
.or_insert_with(|| v.clone());
}
}
let all_kept: BTreeSet<String> = concepts.keys().cloned().collect();
let mut edges = self.edges.clone();
for e in &other.edges {
if all_kept.contains(&e.from) && all_kept.contains(&e.to) {
edges.insert(e.clone());
}
}
let provenance =
merge_provenance(&self.provenance, &self.name, &other.provenance, &other.name);
Ontology {
name: format!("{}+partial({})", self.name, other.name),
source: format!("{} + {} (partial)", self.source, other.source),
concepts,
edges,
level: core::cmp::max(self.level, other.level) + 1,
staging: Staging::Composed,
provenance,
}
}
pub fn specialize(&self, root: &str) -> Option<Ontology> {
if !self.concepts.contains_key(root) {
return None;
}
let mut reachable = BTreeSet::new();
reachable.insert(String::from(root));
let mut changed = true;
while changed {
changed = false;
for edge in &self.edges {
if edge.kind == EdgeKind::Subsumption
&& reachable.contains(&edge.to)
&& reachable.insert(edge.from.clone())
{
changed = true;
}
}
}
let concepts: BTreeMap<String, RuntimeConcept> = self
.concepts
.iter()
.filter(|(k, _)| reachable.contains(*k))
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let edges: BTreeSet<Edge> = self
.edges
.iter()
.filter(|e| reachable.contains(&e.from) && reachable.contains(&e.to))
.cloned()
.collect();
Some(Ontology {
name: format!("{}[{}]", self.name, root),
source: self.source.clone(),
concepts,
edges,
level: self.level,
staging: self.staging,
provenance: self.provenance.clone(),
})
}
pub fn query(&self, name: &str) -> Vec<&Edge> {
self.edges
.iter()
.filter(|e| e.from == name || e.to == name)
.collect()
}
pub fn vocabulary(&self) -> Vocabulary {
use crate::ontology::{ConceptName, Morphism, MorphismKind};
let concepts: Vec<ConceptName> = self
.concepts
.iter()
.map(|(name, c)| {
let mut cn = ConceptName::new(name.clone());
if let Some(lex) = &c.lexical {
cn = cn.with_lexical(lex.clone());
}
cn
})
.collect();
let morphisms: Vec<Morphism> = self
.edges
.iter()
.map(|e| {
let kind = match e.kind {
EdgeKind::Identity => MorphismKind::Identity,
EdgeKind::Subsumption => MorphismKind::Subsumption,
EdgeKind::Parthood => MorphismKind::Parthood,
EdgeKind::Causation => MorphismKind::Causation,
EdgeKind::Opposition => MorphismKind::Opposition,
EdgeKind::Custom => MorphismKind::Custom(std::borrow::Cow::Borrowed("Custom")),
};
Morphism::new(e.from.clone(), e.to.clone(), kind)
})
.collect();
Vocabulary::from_captured(
self.name.clone(),
"runtime::compose",
self.source.clone(),
concepts,
morphisms,
)
}
pub fn shared_with(&self, other: &Ontology) -> BTreeSet<String> {
self.concepts
.keys()
.filter(|k| other.concepts.contains_key(*k))
.cloned()
.collect()
}
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
for edge in &self.edges {
if !self.concepts.contains_key(&edge.from) {
errors.push(format!(
"edge references unknown source concept: {}",
edge.from
));
}
if !self.concepts.contains_key(&edge.to) {
errors.push(format!(
"edge references unknown target concept: {}",
edge.to
));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub fn extend(&self) -> OntologyBuilder {
OntologyBuilder {
name: self.name.clone(),
source: self.source.clone(),
concepts: self.concepts.clone(),
edges: self
.edges
.iter()
.filter(|e| e.kind != EdgeKind::Identity)
.cloned()
.collect(),
base_level: self.level,
provenance: {
let mut p = self.provenance.clone();
if !p.contains(&self.name) {
p.push(self.name.clone());
}
p
},
}
}
pub fn without(&self, concept: &str) -> Ontology {
let mut concepts = self.concepts.clone();
concepts.remove(concept);
let edges: BTreeSet<Edge> = self
.edges
.iter()
.filter(|e| e.from != concept && e.to != concept)
.cloned()
.collect();
let mut provenance = self.provenance.clone();
if !provenance.contains(&self.name) {
provenance.push(self.name.clone());
}
Ontology {
name: format!("{}-{}", self.name, concept),
source: self.source.clone(),
concepts,
edges,
level: self.level,
staging: Staging::Composed,
provenance,
}
}
pub fn restrict(&self, keep: &[&str]) -> Ontology {
let keep_set: BTreeSet<String> = keep.iter().map(|s| String::from(*s)).collect();
let concepts: BTreeMap<String, RuntimeConcept> = self
.concepts
.iter()
.filter(|(k, _)| keep_set.contains(*k))
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let edges: BTreeSet<Edge> = self
.edges
.iter()
.filter(|e| keep_set.contains(&e.from) && keep_set.contains(&e.to))
.cloned()
.collect();
let mut provenance = self.provenance.clone();
if !provenance.contains(&self.name) {
provenance.push(self.name.clone());
}
Ontology {
name: format!("{}|restricted", self.name),
source: self.source.clone(),
concepts,
edges,
level: self.level,
staging: Staging::Composed,
provenance,
}
}
pub fn rename(&self, old: &str, new: &str) -> Ontology {
assert!(
self.concepts.contains_key(old),
"rename: source concept '{old}' does not exist"
);
assert!(
old == new || !self.concepts.contains_key(new),
"rename: target concept '{new}' already exists — use evolve() explicitly for merging"
);
let mut concepts = BTreeMap::new();
for (k, v) in &self.concepts {
let key = if k == old {
String::from(new)
} else {
k.clone()
};
let mut concept = v.clone();
if concept.name == old {
concept.name = String::from(new);
}
concepts.insert(key, concept);
}
let edges: BTreeSet<Edge> = self
.edges
.iter()
.map(|e| Edge {
from: if e.from == old {
String::from(new)
} else {
e.from.clone()
},
to: if e.to == old {
String::from(new)
} else {
e.to.clone()
},
kind: e.kind,
})
.collect();
let mut provenance = self.provenance.clone();
if !provenance.contains(&self.name) {
provenance.push(self.name.clone());
}
Ontology {
name: format!("{}[{}→{}]", self.name, old, new),
source: self.source.clone(),
concepts,
edges,
level: self.level,
staging: Staging::Composed,
provenance,
}
}
pub fn evolve(&self, mapping: &[(&str, &str)]) -> Ontology {
let map: BTreeMap<&str, &str> = mapping.iter().copied().collect();
let resolve = |name: &str| -> String {
map.get(name)
.map(|s| String::from(*s))
.unwrap_or_else(|| String::from(name))
};
let mut concepts = BTreeMap::new();
for (k, v) in &self.concepts {
let new_name = resolve(k);
assert!(
!concepts.contains_key(&new_name),
"evolve: mapping causes collision at '{new_name}' — would silently collapse concepts"
);
let mut concept = v.clone();
concept.name = new_name.clone();
concepts.insert(new_name, concept);
}
let edges: BTreeSet<Edge> = self
.edges
.iter()
.map(|e| Edge {
from: resolve(&e.from),
to: resolve(&e.to),
kind: e.kind,
})
.collect();
let mut provenance = self.provenance.clone();
if !provenance.contains(&self.name) {
provenance.push(self.name.clone());
}
Ontology {
name: format!("{}→evolved", self.name),
source: self.source.clone(),
concepts,
edges,
level: self.level,
staging: Staging::Composed,
provenance,
}
}
}
fn merge_lexical(target: &mut RuntimeConcept, source: &RuntimeConcept) {
match (&mut target.lexical, &source.lexical) {
(None, Some(src_lex)) => {
target.lexical = Some(src_lex.clone());
}
(Some(tgt_lex), Some(src_lex)) => {
if tgt_lex.label.is_empty() && !src_lex.label.is_empty() {
tgt_lex.label = src_lex.label.clone();
}
if tgt_lex.definition.is_empty() && !src_lex.definition.is_empty() {
tgt_lex.definition = src_lex.definition.clone();
}
if tgt_lex.language.is_empty() && !src_lex.language.is_empty() {
tgt_lex.language = src_lex.language.clone();
}
}
_ => {}
}
}
fn merge_provenance(
left: &[String],
left_name: &str,
right: &[String],
right_name: &str,
) -> Vec<String> {
let mut provenance = left.to_vec();
provenance.extend(right.iter().cloned());
if !provenance.contains(&String::from(left_name)) {
provenance.insert(0, String::from(left_name));
}
if !provenance.contains(&String::from(right_name)) {
provenance.push(String::from(right_name));
}
provenance
}
pub struct OntologyBuilder {
name: String,
source: String,
concepts: BTreeMap<String, RuntimeConcept>,
edges: BTreeSet<Edge>,
base_level: usize,
provenance: Vec<String>,
}
impl OntologyBuilder {
pub fn source(mut self, source: &str) -> Self {
self.source = String::from(source);
self
}
pub fn concept(mut self, name: &str) -> Self {
self.concepts
.entry(String::from(name))
.or_insert(RuntimeConcept {
name: String::from(name),
lexical: None,
});
self
}
pub fn concepts(mut self, names: &[&str]) -> Self {
for name in names {
self.concepts
.entry(String::from(*name))
.or_insert(RuntimeConcept {
name: String::from(*name),
lexical: None,
});
}
self
}
pub fn label(mut self, concept: &str, lang: &str, label: &str) -> Self {
use crate::ontology::{Label, LanguageCode};
let entry = self
.concepts
.entry(String::from(concept))
.or_insert(RuntimeConcept {
name: String::from(concept),
lexical: None,
});
if let Some(ref mut lex) = entry.lexical {
lex.label = Label::new(String::from(label));
lex.language = LanguageCode::new(String::from(lang));
} else {
entry.lexical = Some(Lexical::new(
Label::new(String::from(label)),
crate::ontology::Definition::new(String::new()),
LanguageCode::new(String::from(lang)),
));
}
self
}
pub fn definition(mut self, concept: &str, lang: &str, def: &str) -> Self {
use crate::ontology::{Definition, LanguageCode};
let entry = self
.concepts
.entry(String::from(concept))
.or_insert(RuntimeConcept {
name: String::from(concept),
lexical: None,
});
if let Some(ref mut lex) = entry.lexical {
lex.definition = Definition::new(String::from(def));
lex.language = LanguageCode::new(String::from(lang));
} else {
entry.lexical = Some(Lexical::new(
crate::ontology::Label::new(String::new()),
Definition::new(String::from(def)),
LanguageCode::new(String::from(lang)),
));
}
self
}
pub fn is_a(mut self, child: &str, parent: &str) -> Self {
self.ensure_concept(child);
self.ensure_concept(parent);
self.edges.insert(Edge {
from: String::from(child),
to: String::from(parent),
kind: EdgeKind::Subsumption,
});
self
}
pub fn has_a(mut self, whole: &str, part: &str) -> Self {
self.ensure_concept(whole);
self.ensure_concept(part);
self.edges.insert(Edge {
from: String::from(whole),
to: String::from(part),
kind: EdgeKind::Parthood,
});
self
}
pub fn causes(mut self, cause: &str, effect: &str) -> Self {
self.ensure_concept(cause);
self.ensure_concept(effect);
self.edges.insert(Edge {
from: String::from(cause),
to: String::from(effect),
kind: EdgeKind::Causation,
});
self
}
pub fn opposes(mut self, a: &str, b: &str) -> Self {
self.ensure_concept(a);
self.ensure_concept(b);
self.edges.insert(Edge {
from: String::from(a),
to: String::from(b),
kind: EdgeKind::Opposition,
});
self
}
pub fn edge(mut self, from: &str, to: &str, kind: EdgeKind) -> Self {
self.ensure_concept(from);
self.ensure_concept(to);
self.edges.insert(Edge {
from: String::from(from),
to: String::from(to),
kind,
});
self
}
pub fn build(mut self) -> Ontology {
for name in self.concepts.keys().cloned().collect::<Vec<_>>() {
self.edges.insert(Edge {
from: name.clone(),
to: name,
kind: EdgeKind::Identity,
});
}
Ontology {
name: self.name,
source: self.source,
concepts: self.concepts,
edges: self.edges,
level: self.base_level,
staging: Staging::Composed,
provenance: self.provenance,
}
}
fn ensure_concept(&mut self, name: &str) {
self.concepts
.entry(String::from(name))
.or_insert(RuntimeConcept {
name: String::from(name),
lexical: None,
});
}
}
pub fn from_vocabulary(vocab: &Vocabulary) -> Ontology {
Ontology {
name: vocab.ontology_name.as_str().to_string(),
source: vocab.source.as_str().to_string(),
concepts: BTreeMap::new(),
edges: BTreeSet::new(),
level: 0,
staging: Staging::Embedded,
provenance: Vec::new(),
}
}
#[derive(Debug, Clone)]
pub struct Metroplex {
name: String,
grades: BTreeMap<usize, Vec<Ontology>>,
}
impl Metroplex {
pub fn new(name: &str) -> Self {
Self {
name: String::from(name),
grades: BTreeMap::new(),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn add(&mut self, syntrix: Ontology) {
let level = syntrix.level();
self.grades.entry(level).or_default().push(syntrix);
}
pub fn grade(&self, level: usize) -> &[Ontology] {
self.grades.get(&level).map(|v| v.as_slice()).unwrap_or(&[])
}
pub fn grade_count(&self) -> usize {
self.grades.len()
}
pub fn total_ontologies(&self) -> usize {
self.grades.values().map(|v| v.len()).sum()
}
pub fn vocabularies(&self) -> Vec<Vocabulary> {
self.grades
.values()
.flat_map(|syntrices| syntrices.iter().map(|s| s.vocabulary()))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_and_build() {
let bio = Ontology::create("Biology")
.source("Mayr (1982)")
.concept("Animal")
.concept("Mammal")
.concept("Dog")
.is_a("Dog", "Mammal")
.is_a("Mammal", "Animal")
.build();
assert_eq!(bio.name(), "Biology");
assert_eq!(bio.concepts().len(), 3);
assert_eq!(bio.level(), 0);
assert_eq!(bio.staging(), Staging::Composed);
assert!(bio.validate().is_ok());
}
#[test]
fn concept_with_lexical_metadata() {
let bio = Ontology::create("Biology")
.source("Mayr (1982)")
.concept("Cell")
.label("Cell", "en", "Cell")
.definition("Cell", "en", "The basic structural unit of all organisms")
.build();
let cell = bio.concept("Cell").unwrap();
let lex = cell.lexical.as_ref().unwrap();
assert_eq!(lex.label.as_str(), "Cell");
assert_eq!(lex.language.as_str(), "en");
assert!(lex.definition.as_str().contains("structural unit"));
}
#[test]
fn couple_is_coproduct() {
let bio = Ontology::create("Biology")
.concept("Animal")
.concept("Cell")
.is_a("Cell", "Animal")
.build();
let chem = Ontology::create("Chemistry")
.concept("Molecule")
.concept("Atom")
.has_a("Molecule", "Atom")
.build();
let coupled = bio.couple(&chem);
assert_eq!(coupled.concepts().len(), 4);
assert!(coupled.concept("Animal").is_some());
assert!(coupled.concept("Molecule").is_some());
assert_eq!(coupled.level(), 1);
assert_eq!(coupled.staging(), Staging::Composed);
assert!(coupled.name().contains("||"));
}
#[test]
fn compose_identifies_shared() {
let bio = Ontology::create("Biology")
.concept("Cell")
.concept("Organism")
.is_a("Cell", "Organism")
.build();
let molecular = Ontology::create("Molecular")
.concept("Cell")
.concept("Protein")
.has_a("Cell", "Protein")
.build();
let shared = bio.shared_with(&molecular);
assert!(shared.contains("Cell"));
assert_eq!(shared.len(), 1);
let composed = bio.compose(&molecular);
assert_eq!(composed.concepts().len(), 3);
assert!(composed.name().contains("&"));
}
#[test]
fn partial_couple() {
let full = Ontology::create("Full")
.concept("A")
.concept("B")
.concept("C")
.is_a("A", "B")
.is_a("B", "C")
.build();
let base = Ontology::create("Base").concept("X").build();
let partial = base.couple_partial(&full, &["A", "B"]);
assert!(partial.concept("A").is_some());
assert!(partial.concept("B").is_some());
assert!(partial.concept("X").is_some());
assert!(partial.concept("C").is_none());
}
#[test]
fn specialize_follows_taxonomy() {
let onto = Ontology::create("Taxonomy")
.concept("Animal")
.concept("Mammal")
.concept("Dog")
.concept("Cat")
.concept("Plant")
.is_a("Dog", "Mammal")
.is_a("Cat", "Mammal")
.is_a("Mammal", "Animal")
.build();
let mammals = onto.specialize("Mammal").unwrap();
assert!(mammals.concept("Mammal").is_some());
assert!(mammals.concept("Dog").is_some());
assert!(mammals.concept("Cat").is_some());
assert!(mammals.concept("Animal").is_none());
assert!(mammals.concept("Plant").is_none());
}
#[test]
fn query_returns_related_edges() {
let onto = Ontology::create("Test")
.concept("A")
.concept("B")
.concept("C")
.is_a("A", "B")
.has_a("B", "C")
.build();
let edges = onto.query("B");
let non_identity: Vec<_> = edges
.iter()
.filter(|e| e.kind != EdgeKind::Identity)
.collect();
assert_eq!(non_identity.len(), 2);
}
#[test]
fn metroplex_grades() {
let bio = Ontology::create("Biology").concept("Cell").build();
let chem = Ontology::create("Chemistry").concept("Atom").build();
let biochem = bio.couple(&chem);
let mut mplex = Metroplex::new("Science");
mplex.add(bio.clone());
mplex.add(chem.clone());
mplex.add(biochem);
assert_eq!(mplex.grade(0).len(), 2);
assert_eq!(mplex.grade(1).len(), 1);
assert_eq!(mplex.grade_count(), 2);
assert_eq!(mplex.total_ontologies(), 3);
}
#[test]
fn vocabulary_bridge() {
let onto = Ontology::create("TestOntology")
.source("Test (2024)")
.concept("X")
.concept("Y")
.is_a("X", "Y")
.build();
let vocab = onto.vocabulary();
assert_eq!(vocab.ontology_name.as_str(), "TestOntology");
assert_eq!(vocab.concepts().len(), 2);
assert!(!vocab.morphisms().is_empty());
}
#[test]
fn validate_catches_orphan_edges() {
let mut s = Ontology::create("Bad").concept("A").build();
s.edges.insert(Edge {
from: String::from("A"),
to: String::from("Z"),
kind: EdgeKind::Subsumption,
});
assert!(s.validate().is_err());
}
#[test]
fn compose_level_increments() {
let a = Ontology::create("A").concept("X").build();
let b = Ontology::create("B").concept("Y").build();
let ab = a.couple(&b);
assert_eq!(ab.level(), 1);
let c = Ontology::create("C").concept("Z").build();
let abc = ab.couple(&c);
assert_eq!(abc.level(), 2);
}
#[test]
fn provenance_tracks_composition() {
let a = Ontology::create("Alpha").concept("X").build();
let b = Ontology::create("Beta").concept("Y").build();
let composed = a.couple(&b);
assert!(composed.provenance().contains(&String::from("Alpha")));
assert!(composed.provenance().contains(&String::from("Beta")));
}
#[test]
fn from_vocabulary_is_embedded_staging() {
use crate::ontology::{ConceptName, Morphism, MorphismKind};
let concepts: Vec<ConceptName> =
(0..5).map(|i| ConceptName::new(format!("c{i}"))).collect();
let morphisms: Vec<Morphism> = (0..10)
.map(|i| {
Morphism::new(
ConceptName::new(format!("c{i}")),
ConceptName::new(format!("c{i}")),
MorphismKind::Identity,
)
})
.collect();
let vocab =
Vocabulary::from_captured("Test", "test::path", "Test (2024)", concepts, morphisms);
let s = from_vocabulary(&vocab);
assert_eq!(s.staging(), Staging::Embedded);
assert_eq!(s.name(), "Test");
}
#[test]
fn lexical_metadata_preserved_through_composition() {
let a = Ontology::create("A")
.concept("Cell")
.label("Cell", "en", "Cell")
.definition("Cell", "en", "Basic unit of life")
.build();
let b = Ontology::create("B")
.concept("Atom")
.label("Atom", "en", "Atom")
.build();
let composed = a.couple(&b);
let cell = composed.concept("Cell").unwrap();
assert!(cell.lexical.is_some());
assert_eq!(cell.lexical.as_ref().unwrap().label.as_str(), "Cell");
let atom = composed.concept("Atom").unwrap();
assert!(atom.lexical.is_some());
assert_eq!(atom.lexical.as_ref().unwrap().label.as_str(), "Atom");
}
#[test]
fn extend_adds_to_existing() {
let bio = Ontology::create("Biology")
.concept("Cell")
.concept("Tissue")
.is_a("Cell", "Tissue")
.build();
let extended = bio
.extend()
.concept("Organ")
.is_a("Tissue", "Organ")
.build();
assert_eq!(extended.concepts().len(), 3);
assert!(extended.concept("Organ").is_some());
assert!(extended.concept("Cell").is_some());
}
#[test]
fn without_removes_concept_and_edges() {
let onto = Ontology::create("Test")
.concept("A")
.concept("B")
.concept("C")
.is_a("A", "B")
.is_a("B", "C")
.build();
let reduced = onto.without("B");
assert_eq!(reduced.concepts().len(), 2);
assert!(reduced.concept("B").is_none());
let b_edges: Vec<_> = reduced
.edges()
.iter()
.filter(|e| e.from == "B" || e.to == "B")
.collect();
assert!(b_edges.is_empty());
}
#[test]
fn restrict_keeps_only_specified() {
let onto = Ontology::create("Test")
.concepts(&["A", "B", "C", "D"])
.is_a("A", "B")
.is_a("C", "D")
.build();
let restricted = onto.restrict(&["A", "B"]);
assert_eq!(restricted.concepts().len(), 2);
assert!(restricted.concept("A").is_some());
assert!(restricted.concept("C").is_none());
}
#[test]
fn rename_updates_concept_and_edges() {
let onto = Ontology::create("Test")
.concept("OldName")
.concept("Other")
.is_a("OldName", "Other")
.build();
let renamed = onto.rename("OldName", "NewName");
assert!(renamed.concept("NewName").is_some());
assert!(renamed.concept("OldName").is_none());
let edges: Vec<_> = renamed
.edges()
.iter()
.filter(|e| e.kind == EdgeKind::Subsumption)
.collect();
assert_eq!(edges[0].from, "NewName");
}
#[test]
fn evolve_applies_mapping() {
let onto = Ontology::create("Communication")
.concept("Sender")
.concept("Receiver")
.concept("Feedback")
.is_a("Feedback", "Sender")
.build();
let evolved = onto.evolve(&[
("Sender", "Controller"),
("Receiver", "Plant"),
("Feedback", "Sensor"),
]);
assert!(evolved.concept("Controller").is_some());
assert!(evolved.concept("Plant").is_some());
assert!(evolved.concept("Sensor").is_some());
assert!(evolved.concept("Sender").is_none());
}
#[test]
fn couple_partial_produces_valid_ontology() {
let full = Ontology::create("Full")
.concepts(&["A", "B", "C"])
.is_a("A", "B")
.is_a("B", "C")
.build();
let base = Ontology::create("Base").concept("X").build();
let partial = base.couple_partial(&full, &["A", "B"]);
partial
.validate()
.expect("partial Korporator must produce valid ontology");
}
#[test]
fn compose_merges_lexical_on_shared_concepts() {
let english = Ontology::create("English")
.concept("Cell")
.label("Cell", "en", "Cell")
.build();
let bio = Ontology::create("Biology")
.concept("Cell")
.definition("Cell", "en", "The basic unit of life")
.build();
let merged = english.compose(&bio);
let cell = merged.concept("Cell").unwrap();
let lex = cell.lexical.as_ref().unwrap();
assert_eq!(lex.label.as_str(), "Cell");
assert_eq!(lex.definition.as_str(), "The basic unit of life");
}
#[test]
#[should_panic(expected = "collision")]
fn evolve_rejects_collision() {
let onto = Ontology::create("Test").concept("A").concept("B").build();
let _ = onto.evolve(&[("A", "X"), ("B", "X")]);
}
#[test]
#[should_panic(expected = "already exists")]
fn rename_rejects_existing_target() {
let onto = Ontology::create("Test").concept("A").concept("B").build();
let _ = onto.rename("A", "B");
}
#[test]
fn extend_preserves_level() {
let a = Ontology::create("A").concept("X").build();
let b = Ontology::create("B").concept("Y").build();
let composed = a.couple(&b);
assert_eq!(composed.level(), 1);
let extended = composed.extend().concept("Z").build();
assert_eq!(
extended.level(),
1,
"extend must preserve level, not reset to 0"
);
}
#[test]
fn original_unchanged_after_modification() {
let original = Ontology::create("Original")
.concept("A")
.concept("B")
.build();
let _ = original.without("A");
let _ = original.rename("A", "Z");
let _ = original.extend().concept("C").build();
assert_eq!(original.concepts().len(), 2);
assert!(original.concept("A").is_some());
}
}