Skip to main content

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}