use alloc::collections::{BTreeMap, BTreeSet};
use pr4xis::category::Concept;
use pr4xis::category::quiver::ReachabilityClosure;
use pr4xis::logic::proof::{SimpleCounterexample, SimpleProof, Verdict};
use pr4xis::ontology::meta::{Citation, Label, ModulePath, OntologyName, Provenance};
use crate::address::ContentAddress;
use crate::apply::apply;
use crate::archive::Archive;
use crate::codec::CodecError;
use crate::connection::GeneratorAction;
use crate::definition::EdgeTarget;
extern crate alloc;
#[allow(unused_imports)]
use alloc::{
string::{String, ToString},
vec::Vec,
};
const RELATIONS_VOCAB: OntologyName = OntologyName::new_static("Relations");
pub fn relations_kind(name: impl Into<String>) -> ConceptRef {
ConceptRef::new(RELATIONS_VOCAB.clone(), name)
}
pub fn subsumption_kind() -> ConceptRef {
relations_kind("Subsumption")
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ConceptRef {
pub ontology: OntologyName,
pub name: String,
}
impl ConceptRef {
pub fn new(ontology: OntologyName, name: impl Into<String>) -> Self {
Self {
ontology,
name: name.into(),
}
}
}
impl Concept for ConceptRef {
fn name(&self) -> &'static str {
""
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RuntimeEdge {
pub source: ConceptRef,
pub kind: ConceptRef,
pub target: ConceptRef,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MaterializedClosure {
reachable: BTreeMap<ConceptRef, ReachabilityClosure<ConceptRef>>,
}
impl MaterializedClosure {
pub fn fold(edges: &[RuntimeEdge], transitive: &BTreeSet<ConceptRef>) -> Self {
let mut reachable: BTreeMap<ConceptRef, ReachabilityClosure<ConceptRef>> = BTreeMap::new();
for kind in transitive {
let closure = ReachabilityClosure::fold(
edges
.iter()
.filter(|e| &e.kind == kind)
.map(|e| (e.source.clone(), e.target.clone())),
);
reachable.insert(kind.clone(), closure);
}
Self { reachable }
}
pub fn reachable_from(&self, source: &ConceptRef, kind: ConceptRef) -> BTreeSet<ConceptRef> {
self.reachable
.get(&kind)
.map(|closure| {
closure
.strict_image(source)
.into_iter()
.map(|(v, _)| v)
.collect()
})
.unwrap_or_default()
}
pub fn reaches(&self, source: &ConceptRef, target: &ConceptRef, kind: ConceptRef) -> bool {
self.reachable
.get(&kind)
.is_some_and(|closure| source != target && closure.reaches(source, target))
}
pub fn populated_kinds(&self) -> Vec<ConceptRef> {
self.reachable
.iter()
.filter(|(_, closure)| closure.edges_iter().next().is_some())
.map(|(kind, _)| kind.clone())
.collect()
}
pub fn image(&self, c: &ConceptRef, kind: &ConceptRef) -> Vec<(ConceptRef, u32)> {
self.reachable
.get(kind)
.map(|closure| closure.strict_image(c))
.unwrap_or_default()
}
pub fn subsumption_image(&self, c: &ConceptRef) -> Vec<(ConceptRef, u32)> {
self.image(c, &subsumption_kind())
}
pub fn meet(&self, a: &ConceptRef, b: &ConceptRef, kind: &ConceptRef) -> Option<ConceptRef> {
self.reachable.get(kind).and_then(|closure| {
closure.meet_by(a, b, |c| (c.ontology.as_str().to_string(), c.name.clone()))
})
}
pub fn subsumption_meet(&self, a: &ConceptRef, b: &ConceptRef) -> Option<ConceptRef> {
self.meet(a, b, &subsumption_kind())
}
pub fn chain(
&self,
child: &ConceptRef,
ancestor: &ConceptRef,
kind: &ConceptRef,
) -> Option<Vec<ConceptRef>> {
let closure = self.reachable.get(kind)?;
if child != ancestor && !closure.reaches(child, ancestor) {
return None;
}
let mut chain: Vec<(ConceptRef, u32)> = closure
.reflexive_image(child)
.into_iter()
.filter(|(x, _)| x == ancestor || closure.reaches(x, ancestor))
.collect();
chain.sort_unstable_by(|(a, da), (b, db)| {
da.cmp(db)
.then_with(|| a.ontology.as_str().cmp(b.ontology.as_str()))
.then_with(|| a.name.cmp(&b.name))
});
Some(chain.into_iter().map(|(v, _)| v).collect())
}
pub fn subsumption_chain(
&self,
child: &ConceptRef,
ancestor: &ConceptRef,
) -> Option<Vec<ConceptRef>> {
self.chain(child, ancestor, &subsumption_kind())
}
pub fn union(&mut self, other: &MaterializedClosure) {
for (kind, other_closure) in &other.reachable {
let into = self.reachable.entry(kind.clone()).or_default();
let merged =
ReachabilityClosure::fold(into.edges_iter().chain(other_closure.edges_iter()));
*into = merged;
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MaterializeError {
Root(CodecError),
DanglingEdge(DanglingEdge),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DanglingEdge {
pub source: String,
pub kind: String,
pub orphan: String,
}
impl core::fmt::Display for MaterializeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
MaterializeError::Root(e) => write!(f, "materialize: derive root: {e}"),
MaterializeError::DanglingEdge(d) => write!(
f,
"materialize: dangling edge {}--{}-->{}: target node {:?} is not declared",
d.source, d.kind, d.orphan, d.orphan
),
}
}
}
impl std::error::Error for MaterializeError {}
#[derive(Debug, Clone)]
pub struct RuntimeOntology {
id: OntologyName,
root: ContentAddress,
archive: Archive,
closure: MaterializedClosure,
}
impl PartialEq for RuntimeOntology {
fn eq(&self, other: &Self) -> bool {
self.root == other.root
}
}
impl Eq for RuntimeOntology {}
impl RuntimeOntology {
pub fn id(&self) -> &OntologyName {
&self.id
}
pub fn root(&self) -> ContentAddress {
self.root
}
pub fn archive(&self) -> &Archive {
&self.archive
}
pub fn closure(&self) -> &MaterializedClosure {
&self.closure
}
pub fn concept(&self, name: impl Into<String>) -> ConceptRef {
ConceptRef::new(self.id.clone(), name)
}
pub fn morphisms_from(&self, c: &ConceptRef) -> Vec<RuntimeEdge> {
self.archive
.nodes
.iter()
.filter(|n| n.name == c.name)
.flat_map(|n| {
n.edges.iter().filter_map(move |(kind_name, target)| {
let name = target.local_name()?;
Some(RuntimeEdge {
source: c.clone(),
kind: relations_kind(kind_name.clone()),
target: ConceptRef::new(self.id.clone(), name.to_string()),
})
})
})
.collect()
}
pub fn reachable_from(&self, c: &ConceptRef, kind: ConceptRef) -> BTreeSet<ConceptRef> {
self.closure.reachable_from(c, kind)
}
pub fn is_a(&self, child: &ConceptRef, ancestor: &ConceptRef) -> Verdict {
let holds = self.closure.reaches(child, ancestor, subsumption_kind());
let meta = self.is_a_meta(child, ancestor);
if holds {
Ok(Box::new(SimpleProof::new(meta)))
} else {
Err(Box::new(SimpleCounterexample::new(meta)))
}
}
pub fn lexical(&self, c: &ConceptRef) -> Option<&str> {
self.archive
.nodes
.iter()
.find(|n| n.name == c.name)
.and_then(|n| n.lexical.as_deref())
}
fn is_a_meta(&self, child: &ConceptRef, ancestor: &ConceptRef) -> Provenance {
Provenance {
name: OntologyName::new(alloc::format!(
"IsA[{}/{} ⊑ {}/{}]",
child.ontology,
child.name,
ancestor.ontology,
ancestor.name
)),
description: Label::new(alloc::format!(
"{} is-a {} via the Subsumption transitive closure",
child.name,
ancestor.name
)),
citation: Citation::parse_static(
"Guarino (2009); Smith et al. (2005) OBO Relation Ontology (transitive_over); Mac Lane (1971) CWM II.7",
),
module_path: ModulePath::new_static(module_path!()),
}
}
}
pub fn materialize(
archive: Archive,
id: OntologyName,
) -> Result<RuntimeOntology, MaterializeError> {
let root = archive.root().map_err(MaterializeError::Root)?;
let declared: BTreeSet<&str> = archive.nodes.iter().map(|n| n.name.as_str()).collect();
let mut edges: Vec<RuntimeEdge> = Vec::new();
for node in &archive.nodes {
for (kind_name, target) in &node.edges {
let local = match target {
EdgeTarget::Local(name) => name,
EdgeTarget::Grounded { .. } => continue,
};
if !declared.contains(local.as_str()) {
return Err(MaterializeError::DanglingEdge(DanglingEdge {
source: node.name.clone(),
kind: kind_name.clone(),
orphan: local.clone(),
}));
}
edges.push(RuntimeEdge {
source: ConceptRef::new(id.clone(), node.name.clone()),
kind: relations_kind(kind_name.clone()),
target: ConceptRef::new(id.clone(), local.clone()),
});
}
}
let closure = MaterializedClosure::fold(&edges, &declared_transitive_kinds());
Ok(RuntimeOntology {
id,
root,
archive,
closure,
})
}
pub fn apply_then_materialize(
action: &GeneratorAction,
source: &Archive,
id: OntologyName,
) -> Result<RuntimeOntology, MaterializeError> {
let praxis = apply(action, source).expect(
"a Functor action, which `apply` always interprets (fail-closed only on non-Functor)",
);
materialize(praxis, id)
}
fn declared_transitive_kinds() -> BTreeSet<ConceptRef> {
const RELATIONS_TRANSITIVE_KINDS_SRC: &str = include_str!("relations_transitive_kinds.txt");
RELATIONS_TRANSITIVE_KINDS_SRC
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(relations_kind)
.collect()
}
const HAS_PROPERTY_REL: &str = "HasProperty";
const TRANSITIVE_CONCEPT: &str = "Transitive";
pub fn transitive_kinds(relations: &RuntimeOntology) -> BTreeSet<ConceptRef> {
relations
.archive()
.nodes
.iter()
.filter(|node| {
node.edges.iter().any(|(rel, target)| {
rel == HAS_PROPERTY_REL
&& matches!(target, EdgeTarget::Local(name) if name == TRANSITIVE_CONCEPT)
})
})
.map(|node| ConceptRef::new(relations.id().clone(), node.name.clone()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::definition::Definition;
use crate::emit;
#[test]
fn apply_then_materialize_is_its_two_steps() {
let synset = |name: &str, hyper: Option<&str>, gloss: &str| Definition {
kind: "Synset".into(),
name: name.into(),
edges: match hyper {
Some(h) => vec![("hypernym".to_string(), EdgeTarget::Local(h.to_string()))],
None => vec![],
},
axioms: vec![],
lexical: Some(gloss.into()),
};
let source = Archive {
nodes: vec![
synset("dog", Some("animal"), "a dog"),
synset("animal", None, "an animal"),
],
connections: vec![],
};
let action = GeneratorAction::Functor {
map_object: vec![("Synset".to_string(), "Concept".to_string())],
map_morphism: vec![("hypernym".to_string(), "Subsumption".to_string())],
};
let id = OntologyName::new_static("MiniLoaded");
let via = apply_then_materialize(&action, &source, id.clone()).unwrap();
let by_hand = materialize(apply(&action, &source).unwrap(), id).unwrap();
assert_eq!(
via.root(),
by_hand.root(),
"the loader is exactly its two steps"
);
assert_eq!(via.id(), by_hand.id());
}
pr4xis::ontology! {
name: "Org",
source: "pr4xis-runtime ontology materialize test fixture",
concepts: [Employer, Employee, Person, Agent],
labels: {
Employer: ("en", "Employer", "One who employs."),
Employee: ("en", "Employee", "One who is employed."),
Person: ("en", "Person", "A human being."),
Agent: ("en", "Agent", "One who acts."),
},
is_a: [
(Employer, Person),
(Employee, Person),
(Person, Agent),
],
}
fn org() -> RuntimeOntology {
let archive = emit::emit::<OrgCategory>();
materialize(archive, OntologyName::new_static("Org")).expect("Org materializes")
}
#[test]
fn employer_reaches_agent_via_the_subsumption_closure() {
let onto = org();
let employer = onto.concept("Employer");
let agent = onto.concept("Agent");
let descendants = onto.reachable_from(&employer, subsumption_kind());
assert!(
descendants.contains(&agent),
"Employer must reach Agent through the Subsumption closure; got {descendants:?}"
);
assert!(descendants.contains(&onto.concept("Person")));
}
#[test]
fn is_a_returns_a_verdict_carrying_the_claim() {
let onto = org();
let employer = onto.concept("Employer");
let agent = onto.concept("Agent");
match onto.is_a(&employer, &agent) {
Ok(proof) => {
let name = proof.meta().name;
assert!(
name.as_str().contains("Employer") && name.as_str().contains("Agent"),
"the proof must name the witnessed Employer ⊑ Agent claim; got {name}"
);
}
Err(c) => panic!("expected Employer is-a Agent to be proven; got {:?}", c),
}
match onto.is_a(&agent, &employer) {
Err(_) => {}
Ok(p) => panic!("Agent is-a Employer must refute; got proof {:?}", p),
}
}
#[test]
fn content_address_identity_equality() {
let archive = emit::emit::<OrgCategory>();
let a = materialize(archive.clone(), OntologyName::new_static("Org")).unwrap();
let b = materialize(archive, OntologyName::new_static("Org")).unwrap();
assert_eq!(a.root(), b.root());
assert_eq!(a, b);
let mut other_archive = emit::emit::<OrgCategory>();
other_archive.nodes.push(Definition {
kind: "Concept".into(),
name: "Stranger".into(),
edges: alloc::vec![],
axioms: alloc::vec![],
lexical: None,
});
let other = materialize(other_archive, OntologyName::new_static("Org")).unwrap();
assert_ne!(a, other);
}
#[test]
fn referential_closure_counterexample_on_a_dangling_edge() {
let archive = Archive {
nodes: alloc::vec![Definition {
kind: "Concept".into(),
name: "Employer".into(),
edges: alloc::vec![("Subsumption".into(), "Ghost".into())],
axioms: alloc::vec![],
lexical: None,
}],
connections: alloc::vec![],
};
match materialize(archive, OntologyName::new_static("Broken")) {
Err(MaterializeError::DanglingEdge(d)) => {
assert_eq!(d.source, "Employer");
assert_eq!(d.kind, "Subsumption");
assert_eq!(d.orphan, "Ghost", "the counterexample must name the orphan");
}
other => panic!("expected a DanglingEdge counterexample; got {other:?}"),
}
}
#[test]
fn transitive_kinds_reads_the_loaded_owl_transitive_property() {
let archive = Archive {
nodes: alloc::vec![
Definition {
kind: "Concept".into(),
name: "Subsumption".into(),
edges: alloc::vec![("HasProperty".into(), "Transitive".into())],
axioms: alloc::vec![],
lexical: None,
},
Definition {
kind: "Concept".into(),
name: "Opposition".into(),
edges: alloc::vec![("HasProperty".into(), "Symmetric".into())],
axioms: alloc::vec![],
lexical: None,
},
Definition {
kind: "Concept".into(),
name: "Transitive".into(),
edges: alloc::vec![],
axioms: alloc::vec![],
lexical: None,
},
Definition {
kind: "Concept".into(),
name: "Symmetric".into(),
edges: alloc::vec![],
axioms: alloc::vec![],
lexical: None,
},
],
connections: alloc::vec![],
};
let relations =
materialize(archive, OntologyName::new_static("Relations")).expect("materializes");
let kinds = transitive_kinds(&relations);
let relations_id = OntologyName::new_static("Relations");
assert!(
kinds.contains(&ConceptRef::new(relations_id.clone(), "Subsumption")),
"Subsumption asserts Transitive(R) → must be a transitive kind; got {kinds:?}"
);
assert!(
!kinds.contains(&ConceptRef::new(relations_id.clone(), "Opposition")),
"Opposition is symmetric, not transitive → must be excluded; got {kinds:?}"
);
assert!(
!kinds.contains(&ConceptRef::new(relations_id, "Transitive")),
"the Transitive marker is not itself a transitive relation kind; got {kinds:?}"
);
assert_eq!(
kinds.len(),
1,
"exactly one transitive kind in this fixture; got {kinds:?}"
);
}
#[test]
fn lexical_lookup_returns_a_nodes_gloss() {
let archive = Archive {
nodes: alloc::vec![Definition {
kind: "Concept".into(),
name: "Employer".into(),
edges: alloc::vec![],
axioms: alloc::vec![],
lexical: Some("One who employs.".into()),
}],
connections: alloc::vec![],
};
let onto = materialize(archive, OntologyName::new_static("Org")).unwrap();
assert_eq!(
onto.lexical(&onto.concept("Employer")),
Some("One who employs.")
);
assert_eq!(onto.lexical(&onto.concept("Nobody")), None);
}
#[test]
fn closure_refold_is_idempotent_on_a_prefolded_input() {
let onto = org();
let employer = onto.concept("Employer");
let direct_then_closed = onto.reachable_from(&employer, subsumption_kind());
let refolded = MaterializedClosure::fold(
&onto
.archive()
.nodes
.iter()
.flat_map(|n| onto.morphisms_from(&onto.concept(n.name.clone())))
.collect::<Vec<_>>(),
&declared_transitive_kinds(),
);
assert_eq!(
refolded.reachable_from(&employer, subsumption_kind()),
direct_then_closed
);
}
}