use pr4xis::category::connection::{ConnectionFamily, ConnectionGenerators};
use pr4xis::category::{Arrow, Concept, DomainAxiomatized, FinitelyGenerated};
use pr4xis::ontology::connection_constructors;
use crate::archive::Archive;
use crate::connection::{Connection, GeneratorAction};
use crate::definition::Definition;
fn to_connection(g: ConnectionGenerators) -> Connection {
let action = match g.family {
ConnectionFamily::Functor {
map_object,
map_morphism,
} => GeneratorAction::Functor {
map_object,
map_morphism,
},
ConnectionFamily::NaturalTransformation { components } => {
GeneratorAction::NaturalTransformation { components }
}
ConnectionFamily::Adjunction {
left_map_object,
right_map_object,
unit,
counit,
} => GeneratorAction::Adjunction {
left_map_object,
right_map_object,
unit,
counit,
},
};
Connection {
kind: g.kind,
source: g.source.as_str().to_string(),
target: g.target.as_str().to_string(),
action,
laws: g.laws,
}
}
pub fn definition_of<K, O, M>(kind: &K, obj: &O, morphisms: &[M]) -> Definition
where
K: Concept,
O: Concept,
M: Arrow<Object = O>,
{
lower(kind.name(), obj, morphisms)
}
pub fn binding_definition<K: Concept>(kind: &K, name: &str) -> Definition {
Definition {
kind: kind.name().to_string(),
name: name.to_string(),
edges: Vec::new(),
axioms: Vec::new(),
lexical: None,
}
}
fn lower<O, M>(kind: &str, obj: &O, morphisms: &[M]) -> Definition
where
O: Concept,
M: Arrow<Object = O>,
{
let mut edges: Vec<(String, String)> = morphisms
.iter()
.filter(|m| m.target() != *obj) .map(|m| (format!("{:?}", m.kind()), m.target().name().to_string()))
.collect();
edges.sort();
edges.dedup();
let lexical = obj.lexical().map(|lex| lex.definition.as_str().to_string());
Definition {
kind: kind.to_string(),
name: obj.name().to_string(),
edges,
axioms: Vec::new(),
lexical,
}
}
pub fn emit<Cat>() -> Archive
where
Cat: DomainAxiomatized + 'static,
Cat::Object: FinitelyGenerated,
<Cat::Morphism as Arrow>::Kind: core::fmt::Debug + PartialEq + Clone + 'static,
{
let mut nodes: Vec<Definition> = <Cat::Object as FinitelyGenerated>::variants()
.iter()
.map(|obj| lower("Concept", obj, &Cat::morphisms_from(obj)))
.collect();
for axiom in pr4xis::ontology::reasoning::structural_axioms_for::<Cat>() {
nodes.push(Definition {
kind: "Axiom".to_string(),
name: axiom.name().as_str().to_string(),
edges: Vec::new(),
axioms: Vec::new(),
lexical: Some(axiom.description().as_str().to_string()),
});
}
for axiom in <Cat as DomainAxiomatized>::domain_axioms() {
nodes.push(Definition {
kind: "Axiom".to_string(),
name: axiom.name().as_str().to_string(),
edges: Vec::new(),
axioms: Vec::new(),
lexical: Some(axiom.description().as_str().to_string()),
});
}
let own = Cat::ontology_name();
let mut connections: Vec<Connection> = connection_constructors()
.into_iter()
.filter(|g| g.source == own || g.target == own)
.map(to_connection)
.collect();
connections.sort_by_key(|c| c.address().map(|a| *a.as_bytes()).unwrap_or([0u8; 32]));
connections.dedup();
Archive { nodes, connections }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::address::ContentAddress;
use crate::load;
use crate::rebind::{RebindTarget, rebind_nodes};
use std::collections::{BTreeSet, HashMap};
pr4xis::ontology! {
name: "Org",
source: "pr4xis-runtime emit 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),
],
}
pr4xis::ontology! {
name: "Guild",
source: "pr4xis-runtime emit domain-axiom test fixture",
concepts: [Master, Apprentice, Member],
labels: {
Master: ("en", "Master", "A guild member who has completed an apprenticeship."),
Apprentice: ("en", "Apprentice", "A guild member learning the trade."),
Member: ("en", "Member", "One enrolled in the guild."),
},
is_a: [
(Master, Member),
(Apprentice, Member),
],
axioms: {
EveryRoleIsAMember: {
source: "Guarino (2009) The Ontological Level — domain axiomatisation layer",
description: "Every guild role (Master, Apprentice) is-a Member via the Subsumption closure.",
holds: {
use pr4xis::category::Category;
let to_member = |from: GuildConcept| {
GuildCategory::morphisms().iter().any(|m| {
m.from == from
&& m.to == GuildConcept::Member
&& m.kind == GuildRelationKind::Subsumption
})
};
to_member(GuildConcept::Master) && to_member(GuildConcept::Apprentice)
},
},
MemberIsTheRoot: {
source: "Smith et al. (2005) OBO Relation Ontology — Subsumption is antisymmetric",
description: "Member is the taxonomy root: it subsumes no other guild role.",
holds: {
use pr4xis::category::Category;
!GuildCategory::morphisms().iter().any(|m| {
m.from == GuildConcept::Member
&& m.to != GuildConcept::Member
&& m.kind == GuildRelationKind::Subsumption
})
},
},
},
}
pr4xis::ontology! {
name: "Workforce",
source: "pr4xis-runtime emit test fixture",
concepts: [Employer, Employee, Person, Agent, Contractor],
labels: {
Employer: ("en", "Employer", "One who employs (workforce view)."),
Employee: ("en", "Employee", "One who is employed (workforce view)."),
Person: ("en", "Person", "A human being (workforce view)."),
Agent: ("en", "Agent", "One who acts (workforce view)."),
Contractor: ("en", "Contractor", "An external agent under contract."),
},
is_a: [
(Employer, Person),
(Employee, Person),
(Contractor, Agent),
(Person, Agent),
],
}
pr4xis::functor! {
name: OrgIntoWorkforce,
source: OrgCategory,
target: WorkforceCategory,
citation: "Mac Lane (1971) Categories for the Working Mathematician Ch. I §4",
map_object: |obj: &OrgConcept| -> WorkforceConcept {
match obj {
OrgConcept::Employer => WorkforceConcept::Employer,
OrgConcept::Employee => WorkforceConcept::Employee,
OrgConcept::Person => WorkforceConcept::Person,
OrgConcept::Agent => WorkforceConcept::Agent,
}
},
map_morphism: |m: &OrgRelation| -> WorkforceRelation {
let map = |o: &OrgConcept| match o {
OrgConcept::Employer => WorkforceConcept::Employer,
OrgConcept::Employee => WorkforceConcept::Employee,
OrgConcept::Person => WorkforceConcept::Person,
OrgConcept::Agent => WorkforceConcept::Agent,
};
let kind = match m.kind {
OrgRelationKind::Identity => WorkforceRelationKind::Identity,
OrgRelationKind::Subsumption => WorkforceRelationKind::Subsumption,
OrgRelationKind::Parthood => WorkforceRelationKind::Parthood,
OrgRelationKind::Causation => WorkforceRelationKind::Causation,
OrgRelationKind::Opposition => WorkforceRelationKind::Opposition,
};
WorkforceRelation { from: map(&m.from), to: map(&m.to), kind }
},
}
#[test]
fn emits_every_concept_as_a_node() {
let archive = emit::<OrgCategory>();
let names: BTreeSet<&str> = archive.nodes.iter().map(|n| n.name.as_str()).collect();
for c in ["Employer", "Employee", "Person", "Agent"] {
assert!(names.contains(c), "missing concept node {c}");
}
}
#[test]
fn emits_the_subsumption_closure_as_edges() {
let archive = emit::<OrgCategory>();
let employer = archive.nodes.iter().find(|n| n.name == "Employer").unwrap();
let targets: BTreeSet<&str> = employer.edges.iter().map(|(_, t)| t.as_str()).collect();
assert!(targets.contains("Person"), "Employer → Person missing");
assert!(
targets.contains("Agent"),
"closure Employer → Agent missing"
);
}
#[test]
fn emits_each_concepts_gloss_as_its_lexical_and_round_trips() {
let glosses: HashMap<&str, &str> = OrgOntology::labels()
.iter()
.map(|(_, _, surface, gloss)| (*surface, *gloss))
.collect();
let archive = emit::<OrgCategory>();
let concept_nodes: Vec<_> = archive
.nodes
.iter()
.filter(|n| n.kind == "Concept")
.collect();
for node in &concept_nodes {
let expected = glosses.get(node.name.as_str()).copied();
assert_eq!(
node.lexical.as_deref(),
expected,
"node {} must carry its labels-table gloss",
node.name
);
assert!(
node.lexical.is_some(),
"every glossed concept must emit a lexical; {} did not",
node.name
);
}
assert_eq!(
concept_nodes.iter().filter(|n| n.lexical.is_some()).count(),
glosses.len(),
"every labelled concept must contribute a gloss"
);
let bytes = load::emit(&archive).unwrap();
let loaded = load::load(&bytes, archive.root().unwrap()).unwrap();
assert_eq!(loaded, archive);
let employer = loaded.nodes.iter().find(|n| n.name == "Employer").unwrap();
assert_eq!(employer.lexical.as_deref(), Some("One who employs."));
}
#[test]
fn emitted_ontology_round_trips_through_the_runtime() {
let archive = emit::<OrgCategory>();
let bytes = load::emit(&archive).unwrap();
let loaded = load::load(&bytes, archive.root().unwrap()).unwrap();
assert_eq!(loaded, archive);
}
#[test]
fn axioms_field_is_wired_through_the_codec_round_trip() {
let with_axioms = Definition {
kind: "Concept".to_string(),
name: "Employer".to_string(),
edges: vec![("Subsumption".to_string(), "Agent".to_string())],
axioms: vec![
"EmployerIsAgent".to_string(),
"EmployerHiresEmployee".to_string(),
],
lexical: Some("employer".to_string()),
};
let without_axioms = Definition {
axioms: Vec::new(),
..with_axioms.clone()
};
assert_ne!(
with_axioms.address().unwrap(),
without_axioms.address().unwrap(),
"axioms must be load-bearing on the definition address"
);
let archive = Archive {
nodes: vec![with_axioms.clone()],
connections: Vec::new(),
};
let archive_no_axioms = Archive {
nodes: vec![without_axioms],
connections: Vec::new(),
};
assert_ne!(
archive.root().unwrap(),
archive_no_axioms.root().unwrap(),
"the axioms field must reach the archive root"
);
let bytes = load::emit(&archive).unwrap();
let loaded = load::load(&bytes, archive.root().unwrap()).unwrap();
assert_eq!(loaded, archive, "the archive must round-trip faithfully");
let node = loaded.nodes.iter().find(|n| n.name == "Employer").unwrap();
assert_eq!(
node.axioms,
vec![
"EmployerIsAgent".to_string(),
"EmployerHiresEmployee".to_string(),
],
"the non-empty axioms Vec must survive the round-trip byte-exact"
);
}
#[test]
fn emits_declared_domain_axioms_as_nodes_that_round_trip_and_rebind() {
let declared: Vec<(String, String)> = <GuildCategory as DomainAxiomatized>::domain_axioms()
.iter()
.map(|a| {
(
a.name().as_str().to_string(),
a.description().as_str().to_string(),
)
})
.collect();
assert_eq!(
declared.len(),
2,
"the Guild fixture declares exactly two domain axioms"
);
let archive = emit::<GuildCategory>();
let axiom_nodes: HashMap<&str, &Definition> = archive
.nodes
.iter()
.filter(|n| n.kind == "Axiom")
.map(|n| (n.name.as_str(), n))
.collect();
for (name, description) in &declared {
let node = axiom_nodes.get(name.as_str()).unwrap_or_else(|| {
panic!("declared domain axiom {name:?} must be emitted as an Axiom node")
});
assert_eq!(
node.lexical.as_deref(),
Some(description.as_str()),
"the domain-axiom node must carry its description as its lexical gloss"
);
}
let org = emit::<OrgCategory>();
for (name, _) in &declared {
assert!(
!org.nodes.iter().any(|n| &n.name == name),
"an ontology with no axioms: clause must not emit {name:?}"
);
}
let bytes = load::emit(&archive).unwrap();
let loaded = load::load(&bytes, archive.root().unwrap()).unwrap();
assert_eq!(
loaded, archive,
"the archive (incl. domain axioms) round-trips"
);
for (name, _) in &declared {
assert!(
loaded
.nodes
.iter()
.any(|n| n.kind == "Axiom" && &n.name == name),
"the domain-axiom node {name:?} must survive the round-trip byte-exact"
);
}
for (name, _) in &declared {
let rebound = pr4xis::ontology::axiom_by_name(name).unwrap_or_else(|| {
panic!("emitted domain axiom {name:?} must rebind via axiom_by_name on load")
});
assert_eq!(
rebound.name().as_str(),
name.as_str(),
"the rebound axiom must carry the same stable name the node was keyed by"
);
rebound.verify().unwrap_or_else(|c| {
panic!(
"the rebound domain axiom {name:?} must verify against the ontology; got {:?}",
c.meta().name
)
});
}
}
#[test]
fn connections_are_wired_through_the_codec_round_trip() {
let connection = Connection {
kind: "FullyFaithful".to_string(),
source: "Employer".to_string(),
target: "Agent".to_string(),
action: GeneratorAction::Functor {
map_object: vec![("Employer".to_string(), "Agent".to_string())],
map_morphism: vec![("Subsumption".to_string(), "Subsumption".to_string())],
},
laws: vec!["PreservesComposition".to_string()],
};
let node = Definition {
kind: "Concept".to_string(),
name: "Employer".to_string(),
edges: Vec::new(),
axioms: Vec::new(),
lexical: None,
};
let with_conn = Archive {
nodes: vec![node.clone()],
connections: vec![connection.clone()],
};
let without_conn = Archive {
nodes: vec![node],
connections: Vec::new(),
};
assert_ne!(
with_conn.root().unwrap(),
without_conn.root().unwrap(),
"a connection must be load-bearing on the archive root"
);
let bytes = load::emit(&with_conn).unwrap();
let loaded = load::load(&bytes, with_conn.root().unwrap()).unwrap();
assert_eq!(loaded, with_conn, "the archive must round-trip faithfully");
assert_eq!(
loaded.connections,
vec![connection],
"the connection must survive the round-trip byte-exact"
);
}
#[test]
fn emitted_ontology_rebinds_against_itself() {
struct Selfish(HashMap<String, ContentAddress>);
impl RebindTarget for Selfish {
fn address_of(&self, name: &str) -> Option<ContentAddress> {
self.0.get(name).copied()
}
}
let archive = emit::<OrgCategory>();
let known: HashMap<String, ContentAddress> = archive
.nodes
.iter()
.map(|n| (n.name.clone(), n.address().unwrap()))
.collect();
let rebound = rebind_nodes(&archive, &Selfish(known)).unwrap();
assert!(
rebound.iter().all(|r| r.is_bound()),
"a freshly-emitted ontology must rebind to itself"
);
}
fn org_workforce_conn(archive: &Archive) -> &Connection {
archive
.connections
.iter()
.find(|c| c.source == "OrgOntology" && c.target == "WorkforceOntology")
.expect("the registered Org→Workforce functor must be emitted as a connection")
}
#[test]
fn emits_a_registered_functor_as_a_connection() {
let archive = emit::<OrgCategory>();
assert!(
!archive.connections.is_empty(),
"Org participates in the OrgIntoWorkforce functor — connections must be non-empty"
);
let conn = org_workforce_conn(&archive);
match &conn.action {
GeneratorAction::Functor {
map_object,
map_morphism,
} => {
for (s, t) in [
("Employer", "Employer"),
("Employee", "Employee"),
("Person", "Person"),
("Agent", "Agent"),
] {
assert!(
map_object.contains(&(s.to_string(), t.to_string())),
"object map must carry {s} → {t}"
);
}
assert!(
map_morphism.contains(&("Subsumption".to_string(), "Subsumption".to_string())),
"morphism map must carry Subsumption → Subsumption"
);
}
other => panic!("expected a Functor action, got {other:?}"),
}
assert!(conn.laws.contains(&"FunctorIdentityLaw".to_string()));
assert!(conn.laws.contains(&"FunctorCompositionLaw".to_string()));
}
#[test]
fn the_same_functor_is_emitted_from_both_endpoints() {
let from_org = emit::<OrgCategory>();
let from_workforce = emit::<WorkforceCategory>();
let org_conn = org_workforce_conn(&from_org);
let wf_conn = org_workforce_conn(&from_workforce);
assert_eq!(
org_conn.address().unwrap(),
wf_conn.address().unwrap(),
"the same functor must content-address equally from both endpoints"
);
}
#[test]
fn the_connection_is_content_addressed_and_action_sensitive() {
let archive = emit::<OrgCategory>();
let conn = org_workforce_conn(&archive).clone();
let baseline = conn.address().unwrap();
let mut mutated = conn.clone();
if let GeneratorAction::Functor { map_object, .. } = &mut mutated.action {
map_object.retain(|(s, _)| s != "Employer");
map_object.push(("Employer".to_string(), "Agent".to_string()));
}
assert_ne!(
baseline,
mutated.address().unwrap(),
"changing the functor's action must change the connection's content-address"
);
}
#[test]
fn emitted_connections_survive_the_round_trip_byte_exact() {
let archive = emit::<OrgCategory>();
assert!(!archive.connections.is_empty());
let bytes = load::emit(&archive).unwrap();
let loaded = load::load(&bytes, archive.root().unwrap()).unwrap();
assert_eq!(
loaded, archive,
"the archive (incl. connections) must round-trip faithfully"
);
let before = org_workforce_conn(&archive);
let after = org_workforce_conn(&loaded);
assert_eq!(before, after, "the connection must survive byte-exact");
assert_eq!(before.address().unwrap(), after.address().unwrap());
}
#[test]
fn connections_rebind_by_content_address_agreement() {
struct Peer(HashMap<String, ContentAddress>);
impl RebindTarget for Peer {
fn address_of(&self, name: &str) -> Option<ContentAddress> {
self.0.get(name).copied()
}
}
let org = emit::<OrgCategory>();
let workforce = emit::<WorkforceCategory>();
let conn = org_workforce_conn(&org).clone();
let mut known: HashMap<String, ContentAddress> = HashMap::new();
known.insert(conn.source.clone(), org.root().unwrap());
known.insert(conn.target.clone(), workforce.root().unwrap());
let peer = Peer(known);
assert!(
peer.address_of(&conn.source).is_some() && peer.address_of(&conn.target).is_some(),
"both endpoint ontologies must be known to the agreeing peer"
);
let mut disagreeing: HashMap<String, ContentAddress> = HashMap::new();
disagreeing.insert(conn.source.clone(), org.root().unwrap());
disagreeing.insert(conn.target.clone(), org.root().unwrap()); let bad = Peer(disagreeing);
assert_ne!(
bad.address_of(&conn.target),
Some(workforce.root().unwrap()),
"a peer at a different address must NOT agree on the target ontology"
);
}
}