Skip to main content

clawft_kernel/
crossref.rs

1//! Universal cross-references between forest structures.
2//!
3//! This module is compiled only when the `ecc` feature is enabled.
4//! It provides [`UniversalNodeId`] (BLAKE3-hashed identity for any node),
5//! [`CrossRef`] (a typed directed edge between two nodes), and
6//! [`CrossRefStore`] (a concurrent forward/reverse index).
7
8use std::fmt;
9use std::sync::atomic::{AtomicU64, Ordering};
10
11use dashmap::DashMap;
12use serde::{Deserialize, Serialize};
13
14// ---------------------------------------------------------------------------
15// StructureTag
16// ---------------------------------------------------------------------------
17
18/// Identifies which forest structure a node belongs to.
19#[non_exhaustive]
20#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
21pub enum StructureTag {
22    /// ExoChain ledger (0x01).
23    ExoChain,
24    /// Resource tree (0x02).
25    ResourceTree,
26    /// Causal graph (0x03).
27    CausalGraph,
28    /// HNSW vector index (0x04).
29    HnswIndex,
30    /// Domain-specific extension (0x10+).
31    Custom(u8),
32}
33
34impl StructureTag {
35    /// Returns the canonical byte discriminant.
36    pub fn as_u8(&self) -> u8 {
37        match self {
38            Self::ExoChain => 0x01,
39            Self::ResourceTree => 0x02,
40            Self::CausalGraph => 0x03,
41            Self::HnswIndex => 0x04,
42            Self::Custom(v) => *v,
43        }
44    }
45}
46
47impl fmt::Display for StructureTag {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            Self::ExoChain => write!(f, "ExoChain"),
51            Self::ResourceTree => write!(f, "ResourceTree"),
52            Self::CausalGraph => write!(f, "CausalGraph"),
53            Self::HnswIndex => write!(f, "HnswIndex"),
54            Self::Custom(v) => write!(f, "Custom(0x{v:02x})"),
55        }
56    }
57}
58
59// ---------------------------------------------------------------------------
60// UniversalNodeId
61// ---------------------------------------------------------------------------
62
63/// A 32-byte BLAKE3 hash that uniquely identifies any node across all
64/// forest structures.
65#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
66pub struct UniversalNodeId(pub [u8; 32]);
67
68impl UniversalNodeId {
69    /// Derive a deterministic identity by hashing the concatenation of all
70    /// constituent fields via BLAKE3.
71    pub fn new(
72        structure_tag: &StructureTag,
73        context_id: &[u8],
74        hlc_timestamp: u64,
75        content_hash: &[u8],
76        parent_id: &[u8],
77    ) -> Self {
78        let mut hasher = blake3::Hasher::new();
79        hasher.update(&[structure_tag.as_u8()]);
80        hasher.update(context_id);
81        hasher.update(&hlc_timestamp.to_le_bytes());
82        hasher.update(content_hash);
83        hasher.update(parent_id);
84        Self(*hasher.finalize().as_bytes())
85    }
86
87    /// The all-zeros sentinel ID.
88    pub fn zero() -> Self {
89        Self([0u8; 32])
90    }
91
92    /// Construct from a raw 32-byte array.
93    pub fn from_bytes(bytes: [u8; 32]) -> Self {
94        Self(bytes)
95    }
96
97    /// Borrow the inner bytes.
98    pub fn as_bytes(&self) -> &[u8; 32] {
99        &self.0
100    }
101}
102
103impl fmt::Display for UniversalNodeId {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        for b in &self.0 {
106            write!(f, "{b:02x}")?;
107        }
108        Ok(())
109    }
110}
111
112impl fmt::Debug for UniversalNodeId {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        write!(f, "UniversalNodeId({self})")
115    }
116}
117
118// ---------------------------------------------------------------------------
119// CrossRefType
120// ---------------------------------------------------------------------------
121
122/// The semantic relationship carried by a [`CrossRef`].
123#[non_exhaustive]
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub enum CrossRefType {
126    /// Source was triggered by target (0x01).
127    TriggeredBy,
128    /// Source is evidence for target (0x02).
129    EvidenceFor,
130    /// Source elaborates on target (0x03).
131    Elaborates,
132    /// Source is the emotional cause of target (0x04).
133    EmotionCause,
134    /// Source provides goal motivation for target (0x05).
135    GoalMotivation,
136    /// Scene boundary marker (0x06).
137    SceneBoundary,
138    /// Memory encoding link (0x09).
139    MemoryEncoded,
140    /// Theory-of-mind inference (0x0A).
141    TomInference,
142    /// Domain-specific extension.
143    Custom(u8),
144}
145
146impl fmt::Display for CrossRefType {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        match self {
149            Self::TriggeredBy => write!(f, "TriggeredBy"),
150            Self::EvidenceFor => write!(f, "EvidenceFor"),
151            Self::Elaborates => write!(f, "Elaborates"),
152            Self::EmotionCause => write!(f, "EmotionCause"),
153            Self::GoalMotivation => write!(f, "GoalMotivation"),
154            Self::SceneBoundary => write!(f, "SceneBoundary"),
155            Self::MemoryEncoded => write!(f, "MemoryEncoded"),
156            Self::TomInference => write!(f, "TomInference"),
157            Self::Custom(v) => write!(f, "Custom(0x{v:02x})"),
158        }
159    }
160}
161
162// ---------------------------------------------------------------------------
163// CrossRef
164// ---------------------------------------------------------------------------
165
166/// A directed, typed cross-reference between two universal nodes.
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct CrossRef {
169    /// The originating node.
170    pub source: UniversalNodeId,
171    /// Which structure the source lives in.
172    pub source_structure: StructureTag,
173    /// The destination node.
174    pub target: UniversalNodeId,
175    /// Which structure the target lives in.
176    pub target_structure: StructureTag,
177    /// Semantic relationship type.
178    pub ref_type: CrossRefType,
179    /// HLC timestamp at creation.
180    pub created_at: u64,
181    /// ExoChain sequence number for provenance.
182    pub chain_seq: u64,
183}
184
185// ---------------------------------------------------------------------------
186// CrossRefStore
187// ---------------------------------------------------------------------------
188
189/// Concurrent forward/reverse index of [`CrossRef`] edges.
190pub struct CrossRefStore {
191    forward: DashMap<UniversalNodeId, Vec<CrossRef>>,
192    reverse: DashMap<UniversalNodeId, Vec<CrossRef>>,
193    count: AtomicU64,
194}
195
196impl CrossRefStore {
197    /// Create an empty store.
198    pub fn new() -> Self {
199        Self {
200            forward: DashMap::new(),
201            reverse: DashMap::new(),
202            count: AtomicU64::new(0),
203        }
204    }
205
206    /// Insert a cross-reference, indexing it in both directions.
207    pub fn insert(&self, crossref: CrossRef) {
208        self.forward
209            .entry(crossref.source.clone())
210            .or_default()
211            .push(crossref.clone());
212        self.reverse
213            .entry(crossref.target.clone())
214            .or_default()
215            .push(crossref);
216        self.count.fetch_add(1, Ordering::Relaxed);
217    }
218
219    /// All cross-refs originating from `id`.
220    pub fn get_forward(&self, id: &UniversalNodeId) -> Vec<CrossRef> {
221        self.forward
222            .get(id)
223            .map(|v| v.value().clone())
224            .unwrap_or_default()
225    }
226
227    /// All cross-refs pointing *to* `id`.
228    pub fn get_reverse(&self, id: &UniversalNodeId) -> Vec<CrossRef> {
229        self.reverse
230            .get(id)
231            .map(|v| v.value().clone())
232            .unwrap_or_default()
233    }
234
235    /// All cross-refs where `id` appears as source **or** target.
236    pub fn get_all(&self, id: &UniversalNodeId) -> Vec<CrossRef> {
237        let mut out = self.get_forward(id);
238        out.extend(self.get_reverse(id));
239        out
240    }
241
242    /// Total number of cross-refs inserted.
243    pub fn count(&self) -> u64 {
244        self.count.load(Ordering::Relaxed)
245    }
246
247    /// Filter cross-refs involving `id` (either direction) by relationship type.
248    pub fn by_type(&self, id: &UniversalNodeId, ref_type: &CrossRefType) -> Vec<CrossRef> {
249        self.get_all(id)
250            .into_iter()
251            .filter(|cr| &cr.ref_type == ref_type)
252            .collect()
253    }
254
255    /// Remove all entries (useful for calibration resets).
256    pub fn clear(&self) {
257        self.forward.clear();
258        self.reverse.clear();
259        self.count.store(0, Ordering::Relaxed);
260    }
261}
262
263impl Default for CrossRefStore {
264    fn default() -> Self {
265        Self::new()
266    }
267}
268
269// ---------------------------------------------------------------------------
270// Tests
271// ---------------------------------------------------------------------------
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    fn make_id(tag: &StructureTag, ctx: &[u8], ts: u64) -> UniversalNodeId {
278        UniversalNodeId::new(tag, ctx, ts, b"hash", b"parent")
279    }
280
281    fn sample_crossref(
282        src: UniversalNodeId,
283        tgt: UniversalNodeId,
284        rt: CrossRefType,
285    ) -> CrossRef {
286        CrossRef {
287            source: src,
288            source_structure: StructureTag::ExoChain,
289            target: tgt,
290            target_structure: StructureTag::ResourceTree,
291            ref_type: rt,
292            created_at: 1000,
293            chain_seq: 42,
294        }
295    }
296
297    #[test]
298    fn universal_node_id_creation() {
299        let id = make_id(&StructureTag::ExoChain, b"ctx", 1);
300        assert_ne!(id.as_bytes(), &[0u8; 32]);
301    }
302
303    #[test]
304    fn universal_node_id_deterministic() {
305        let a = make_id(&StructureTag::ExoChain, b"ctx", 1);
306        let b = make_id(&StructureTag::ExoChain, b"ctx", 1);
307        assert_eq!(a, b);
308
309        let c = make_id(&StructureTag::ExoChain, b"ctx", 2);
310        assert_ne!(a, c);
311    }
312
313    #[test]
314    fn universal_node_id_display_hex() {
315        let id = UniversalNodeId::zero();
316        let s = format!("{id}");
317        assert_eq!(s.len(), 64);
318        assert!(s.chars().all(|c| c.is_ascii_hexdigit()));
319        assert_eq!(s, "0".repeat(64));
320    }
321
322    #[test]
323    fn universal_node_id_zero() {
324        let z = UniversalNodeId::zero();
325        assert_eq!(z.as_bytes(), &[0u8; 32]);
326        assert_eq!(z, UniversalNodeId::from_bytes([0u8; 32]));
327    }
328
329    #[test]
330    fn structure_tag_as_u8() {
331        assert_eq!(StructureTag::ExoChain.as_u8(), 0x01);
332        assert_eq!(StructureTag::ResourceTree.as_u8(), 0x02);
333        assert_eq!(StructureTag::CausalGraph.as_u8(), 0x03);
334        assert_eq!(StructureTag::HnswIndex.as_u8(), 0x04);
335        assert_eq!(StructureTag::Custom(0x10).as_u8(), 0x10);
336    }
337
338    #[test]
339    fn structure_tag_display() {
340        assert_eq!(StructureTag::ExoChain.to_string(), "ExoChain");
341        assert_eq!(StructureTag::ResourceTree.to_string(), "ResourceTree");
342        assert_eq!(StructureTag::Custom(0x10).to_string(), "Custom(0x10)");
343    }
344
345    #[test]
346    fn crossref_type_display() {
347        assert_eq!(CrossRefType::TriggeredBy.to_string(), "TriggeredBy");
348        assert_eq!(CrossRefType::EvidenceFor.to_string(), "EvidenceFor");
349        assert_eq!(CrossRefType::TomInference.to_string(), "TomInference");
350        assert_eq!(CrossRefType::Custom(0xff).to_string(), "Custom(0xff)");
351    }
352
353    #[test]
354    fn crossref_store_insert_and_get_forward() {
355        let store = CrossRefStore::new();
356        let src = make_id(&StructureTag::ExoChain, b"a", 1);
357        let tgt = make_id(&StructureTag::ResourceTree, b"b", 2);
358        store.insert(sample_crossref(src.clone(), tgt, CrossRefType::TriggeredBy));
359
360        let fwd = store.get_forward(&src);
361        assert_eq!(fwd.len(), 1);
362        assert_eq!(fwd[0].ref_type, CrossRefType::TriggeredBy);
363    }
364
365    #[test]
366    fn crossref_store_insert_and_get_reverse() {
367        let store = CrossRefStore::new();
368        let src = make_id(&StructureTag::ExoChain, b"a", 1);
369        let tgt = make_id(&StructureTag::ResourceTree, b"b", 2);
370        store.insert(sample_crossref(src, tgt.clone(), CrossRefType::EvidenceFor));
371
372        let rev = store.get_reverse(&tgt);
373        assert_eq!(rev.len(), 1);
374        assert_eq!(rev[0].ref_type, CrossRefType::EvidenceFor);
375    }
376
377    #[test]
378    fn crossref_store_get_all_both_directions() {
379        let store = CrossRefStore::new();
380        let a = make_id(&StructureTag::ExoChain, b"a", 1);
381        let b = make_id(&StructureTag::ResourceTree, b"b", 2);
382        let c = make_id(&StructureTag::CausalGraph, b"c", 3);
383
384        // a -> b
385        store.insert(sample_crossref(a.clone(), b.clone(), CrossRefType::TriggeredBy));
386        // c -> a
387        store.insert(sample_crossref(c, a.clone(), CrossRefType::Elaborates));
388
389        let all = store.get_all(&a);
390        assert_eq!(all.len(), 2);
391        assert_eq!(store.count(), 2);
392    }
393
394    #[test]
395    fn crossref_store_by_type() {
396        let store = CrossRefStore::new();
397        let a = make_id(&StructureTag::ExoChain, b"a", 1);
398        let b = make_id(&StructureTag::ResourceTree, b"b", 2);
399        let c = make_id(&StructureTag::CausalGraph, b"c", 3);
400
401        store.insert(sample_crossref(a.clone(), b, CrossRefType::TriggeredBy));
402        store.insert(sample_crossref(a.clone(), c, CrossRefType::EvidenceFor));
403
404        let triggered = store.by_type(&a, &CrossRefType::TriggeredBy);
405        assert_eq!(triggered.len(), 1);
406
407        let evidence = store.by_type(&a, &CrossRefType::EvidenceFor);
408        assert_eq!(evidence.len(), 1);
409
410        let none = store.by_type(&a, &CrossRefType::SceneBoundary);
411        assert!(none.is_empty());
412    }
413
414    #[test]
415    fn crossref_store_clear() {
416        let store = CrossRefStore::new();
417        let a = make_id(&StructureTag::ExoChain, b"a", 1);
418        let b = make_id(&StructureTag::ResourceTree, b"b", 2);
419        store.insert(sample_crossref(a.clone(), b, CrossRefType::TriggeredBy));
420        assert_eq!(store.count(), 1);
421
422        store.clear();
423        assert_eq!(store.count(), 0);
424        assert!(store.get_forward(&a).is_empty());
425    }
426}