1use std::fmt;
9use std::sync::atomic::{AtomicU64, Ordering};
10
11use dashmap::DashMap;
12use serde::{Deserialize, Serialize};
13
14#[non_exhaustive]
20#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
21pub enum StructureTag {
22 ExoChain,
24 ResourceTree,
26 CausalGraph,
28 HnswIndex,
30 Custom(u8),
32}
33
34impl StructureTag {
35 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#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
66pub struct UniversalNodeId(pub [u8; 32]);
67
68impl UniversalNodeId {
69 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 pub fn zero() -> Self {
89 Self([0u8; 32])
90 }
91
92 pub fn from_bytes(bytes: [u8; 32]) -> Self {
94 Self(bytes)
95 }
96
97 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#[non_exhaustive]
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub enum CrossRefType {
126 TriggeredBy,
128 EvidenceFor,
130 Elaborates,
132 EmotionCause,
134 GoalMotivation,
136 SceneBoundary,
138 MemoryEncoded,
140 TomInference,
142 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#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct CrossRef {
169 pub source: UniversalNodeId,
171 pub source_structure: StructureTag,
173 pub target: UniversalNodeId,
175 pub target_structure: StructureTag,
177 pub ref_type: CrossRefType,
179 pub created_at: u64,
181 pub chain_seq: u64,
183}
184
185pub struct CrossRefStore {
191 forward: DashMap<UniversalNodeId, Vec<CrossRef>>,
192 reverse: DashMap<UniversalNodeId, Vec<CrossRef>>,
193 count: AtomicU64,
194}
195
196impl CrossRefStore {
197 pub fn new() -> Self {
199 Self {
200 forward: DashMap::new(),
201 reverse: DashMap::new(),
202 count: AtomicU64::new(0),
203 }
204 }
205
206 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 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 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 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 pub fn count(&self) -> u64 {
244 self.count.load(Ordering::Relaxed)
245 }
246
247 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 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#[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 store.insert(sample_crossref(a.clone(), b.clone(), CrossRefType::TriggeredBy));
386 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}