pr4xis_runtime/definition.rs
1//! Definition-bearing addressing — a node's identity is the content address of
2//! its DEFINITION, not its name.
3//!
4//! This is the head of the build spine: it closes the **G5 wire gap**. Under
5//! name-only addressing (`hash(kind, name)`) two nodes that share a name but
6//! differ in structure collide to the same address, so peers could "agree" on a
7//! node while meaning different things. Addressing the *definition* — kind +
8//! name + outgoing edges + governing axioms + lexical grounding — makes
9//! agreement mean agreement on what the node actually IS.
10//!
11//! References to other nodes (edge targets, axioms) are by NAME at this layer,
12//! resolved within the node's own ontology and bound by the ontology's Merkle
13//! root. The stronger *recursive* form — where this address depends on the
14//! targets' own addresses — is the Merkle-DAG layer, which must additionally
15//! handle cyclic graphs (e.g. symmetric `opposition`) via strongly-connected-
16//! component hashing. This module is the cycle-safe floor that layer builds on.
17
18use serde::{Deserialize, Serialize};
19
20use crate::address::ContentAddress;
21use crate::codec::{self, CodecError};
22
23/// The canonical definition of a node — the structure its content-address is
24/// taken over. Field order is the serialized order; the `edges`/`axioms` rows
25/// are sorted + deduplicated by [`Definition::address`] before encoding, so the
26/// address does not depend on assembly order.
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct Definition {
29 /// The node's kind — a name resolved against the meta-ontology
30 /// (e.g. `"Concept"`, `"Functor"`, `"Lens"`).
31 pub kind: String,
32 /// The node's name within its ontology.
33 pub name: String,
34 /// Outgoing typed edges, each `(relation-kind name, target name)`.
35 pub edges: Vec<(String, String)>,
36 /// Names of the axioms that constrain this node.
37 pub axioms: Vec<String>,
38 /// The node's lexical grounding (canonical English form / Lemon entry), if
39 /// any — part of the definition because in praxis "everything is Lemon".
40 pub lexical: Option<String>,
41}
42
43impl Definition {
44 /// The content address of this node — its definition-bearing identity.
45 ///
46 /// Canonical: the `edges` and `axioms` rows are sorted + deduplicated before
47 /// the DAG-CBOR encoding, so two definitions with the same content assembled
48 /// in different orders share the same address.
49 pub fn address(&self) -> Result<ContentAddress, CodecError> {
50 let mut canon = self.clone();
51 canon.edges.sort();
52 canon.edges.dedup();
53 canon.axioms.sort();
54 canon.axioms.dedup();
55 codec::address_of(&canon)
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 fn base() -> Definition {
64 Definition {
65 kind: "Concept".into(),
66 name: "Employer".into(),
67 edges: vec![("Subsumption".into(), "Agent".into())],
68 axioms: vec!["EmployerIsAgent".into()],
69 lexical: Some("employer".into()),
70 }
71 }
72
73 #[test]
74 fn identical_definitions_share_an_address() {
75 assert_eq!(base().address().unwrap(), base().address().unwrap());
76 }
77
78 #[test]
79 fn changing_an_edge_changes_the_address() {
80 let mut b = base();
81 b.edges = vec![("Subsumption".into(), "Person".into())]; // different target
82 assert_ne!(base().address().unwrap(), b.address().unwrap());
83 }
84
85 #[test]
86 fn changing_an_axiom_changes_the_address() {
87 let mut b = base();
88 b.axioms = vec!["EmployerHiresEmployee".into()];
89 assert_ne!(base().address().unwrap(), b.address().unwrap());
90 }
91
92 #[test]
93 fn changing_the_lexical_changes_the_address() {
94 let mut b = base();
95 b.lexical = Some("boss".into());
96 assert_ne!(base().address().unwrap(), b.address().unwrap());
97 }
98
99 #[test]
100 fn same_name_different_definition_does_not_collide() {
101 // The G5 fix: same name, different structure → different address.
102 let mut b = base();
103 b.edges.push(("Opposition".into(), "Employee".into()));
104 assert_ne!(base().address().unwrap(), b.address().unwrap());
105 }
106
107 #[test]
108 fn address_is_order_independent() {
109 let mut a = base();
110 a.edges = vec![
111 ("Subsumption".into(), "Agent".into()),
112 ("Opposition".into(), "Employee".into()),
113 ];
114 a.axioms = vec!["B".into(), "A".into()];
115 let mut b = base();
116 b.edges = vec![
117 ("Opposition".into(), "Employee".into()),
118 ("Subsumption".into(), "Agent".into()),
119 ];
120 b.axioms = vec!["A".into(), "B".into()];
121 assert_eq!(a.address().unwrap(), b.address().unwrap());
122 }
123}