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 Continuer,
145 Speaker,
150 Custom(u8),
152}
153
154impl fmt::Display for CrossRefType {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 match self {
157 Self::TriggeredBy => write!(f, "TriggeredBy"),
158 Self::EvidenceFor => write!(f, "EvidenceFor"),
159 Self::Elaborates => write!(f, "Elaborates"),
160 Self::EmotionCause => write!(f, "EmotionCause"),
161 Self::GoalMotivation => write!(f, "GoalMotivation"),
162 Self::SceneBoundary => write!(f, "SceneBoundary"),
163 Self::MemoryEncoded => write!(f, "MemoryEncoded"),
164 Self::TomInference => write!(f, "TomInference"),
165 Self::Continuer => write!(f, "Continuer"),
166 Self::Speaker => write!(f, "Speaker"),
167 Self::Custom(v) => write!(f, "Custom(0x{v:02x})"),
168 }
169 }
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct CrossRef {
179 pub source: UniversalNodeId,
181 pub source_structure: StructureTag,
183 pub target: UniversalNodeId,
185 pub target_structure: StructureTag,
187 pub ref_type: CrossRefType,
189 pub created_at: u64,
191 pub chain_seq: u64,
193}
194
195pub struct CrossRefStore {
201 forward: DashMap<UniversalNodeId, Vec<CrossRef>>,
202 reverse: DashMap<UniversalNodeId, Vec<CrossRef>>,
203 count: AtomicU64,
204}
205
206impl CrossRefStore {
207 pub fn new() -> Self {
209 Self {
210 forward: DashMap::new(),
211 reverse: DashMap::new(),
212 count: AtomicU64::new(0),
213 }
214 }
215
216 pub fn insert(&self, crossref: CrossRef) {
218 self.forward
219 .entry(crossref.source.clone())
220 .or_default()
221 .push(crossref.clone());
222 self.reverse
223 .entry(crossref.target.clone())
224 .or_default()
225 .push(crossref);
226 self.count.fetch_add(1, Ordering::Relaxed);
227 }
228
229 pub fn get_forward(&self, id: &UniversalNodeId) -> Vec<CrossRef> {
231 self.forward
232 .get(id)
233 .map(|v| v.value().clone())
234 .unwrap_or_default()
235 }
236
237 pub fn get_reverse(&self, id: &UniversalNodeId) -> Vec<CrossRef> {
239 self.reverse
240 .get(id)
241 .map(|v| v.value().clone())
242 .unwrap_or_default()
243 }
244
245 pub fn get_all(&self, id: &UniversalNodeId) -> Vec<CrossRef> {
247 let mut out = self.get_forward(id);
248 out.extend(self.get_reverse(id));
249 out
250 }
251
252 pub fn count(&self) -> u64 {
254 self.count.load(Ordering::Relaxed)
255 }
256
257 pub fn by_type(&self, id: &UniversalNodeId, ref_type: &CrossRefType) -> Vec<CrossRef> {
259 self.get_all(id)
260 .into_iter()
261 .filter(|cr| &cr.ref_type == ref_type)
262 .collect()
263 }
264
265 pub fn all(&self) -> Vec<CrossRef> {
276 self.forward
277 .iter()
278 .flat_map(|entry| entry.value().clone())
279 .collect()
280 }
281
282 pub fn clear(&self) {
284 self.forward.clear();
285 self.reverse.clear();
286 self.count.store(0, Ordering::Relaxed);
287 }
288}
289
290impl Default for CrossRefStore {
291 fn default() -> Self {
292 Self::new()
293 }
294}
295
296#[cfg(test)]
301mod tests {
302 use super::*;
303
304 fn make_id(tag: &StructureTag, ctx: &[u8], ts: u64) -> UniversalNodeId {
305 UniversalNodeId::new(tag, ctx, ts, b"hash", b"parent")
306 }
307
308 fn sample_crossref(src: UniversalNodeId, tgt: UniversalNodeId, rt: CrossRefType) -> CrossRef {
309 CrossRef {
310 source: src,
311 source_structure: StructureTag::ExoChain,
312 target: tgt,
313 target_structure: StructureTag::ResourceTree,
314 ref_type: rt,
315 created_at: 1000,
316 chain_seq: 42,
317 }
318 }
319
320 #[test]
321 fn universal_node_id_creation() {
322 let id = make_id(&StructureTag::ExoChain, b"ctx", 1);
323 assert_ne!(id.as_bytes(), &[0u8; 32]);
324 }
325
326 #[test]
327 fn universal_node_id_deterministic() {
328 let a = make_id(&StructureTag::ExoChain, b"ctx", 1);
329 let b = make_id(&StructureTag::ExoChain, b"ctx", 1);
330 assert_eq!(a, b);
331
332 let c = make_id(&StructureTag::ExoChain, b"ctx", 2);
333 assert_ne!(a, c);
334 }
335
336 #[test]
337 fn universal_node_id_display_hex() {
338 let id = UniversalNodeId::zero();
339 let s = format!("{id}");
340 assert_eq!(s.len(), 64);
341 assert!(s.chars().all(|c| c.is_ascii_hexdigit()));
342 assert_eq!(s, "0".repeat(64));
343 }
344
345 #[test]
346 fn universal_node_id_zero() {
347 let z = UniversalNodeId::zero();
348 assert_eq!(z.as_bytes(), &[0u8; 32]);
349 assert_eq!(z, UniversalNodeId::from_bytes([0u8; 32]));
350 }
351
352 #[test]
353 fn structure_tag_as_u8() {
354 assert_eq!(StructureTag::ExoChain.as_u8(), 0x01);
355 assert_eq!(StructureTag::ResourceTree.as_u8(), 0x02);
356 assert_eq!(StructureTag::CausalGraph.as_u8(), 0x03);
357 assert_eq!(StructureTag::HnswIndex.as_u8(), 0x04);
358 assert_eq!(StructureTag::Custom(0x10).as_u8(), 0x10);
359 }
360
361 #[test]
362 fn structure_tag_display() {
363 assert_eq!(StructureTag::ExoChain.to_string(), "ExoChain");
364 assert_eq!(StructureTag::ResourceTree.to_string(), "ResourceTree");
365 assert_eq!(StructureTag::Custom(0x10).to_string(), "Custom(0x10)");
366 }
367
368 #[test]
369 fn crossref_type_display() {
370 assert_eq!(CrossRefType::TriggeredBy.to_string(), "TriggeredBy");
371 assert_eq!(CrossRefType::EvidenceFor.to_string(), "EvidenceFor");
372 assert_eq!(CrossRefType::TomInference.to_string(), "TomInference");
373 assert_eq!(CrossRefType::Custom(0xff).to_string(), "Custom(0xff)");
374 }
375
376 #[test]
377 fn crossref_store_insert_and_get_forward() {
378 let store = CrossRefStore::new();
379 let src = make_id(&StructureTag::ExoChain, b"a", 1);
380 let tgt = make_id(&StructureTag::ResourceTree, b"b", 2);
381 store.insert(sample_crossref(src.clone(), tgt, CrossRefType::TriggeredBy));
382
383 let fwd = store.get_forward(&src);
384 assert_eq!(fwd.len(), 1);
385 assert_eq!(fwd[0].ref_type, CrossRefType::TriggeredBy);
386 }
387
388 #[test]
389 fn crossref_store_insert_and_get_reverse() {
390 let store = CrossRefStore::new();
391 let src = make_id(&StructureTag::ExoChain, b"a", 1);
392 let tgt = make_id(&StructureTag::ResourceTree, b"b", 2);
393 store.insert(sample_crossref(src, tgt.clone(), CrossRefType::EvidenceFor));
394
395 let rev = store.get_reverse(&tgt);
396 assert_eq!(rev.len(), 1);
397 assert_eq!(rev[0].ref_type, CrossRefType::EvidenceFor);
398 }
399
400 #[test]
401 fn crossref_store_get_all_both_directions() {
402 let store = CrossRefStore::new();
403 let a = make_id(&StructureTag::ExoChain, b"a", 1);
404 let b = make_id(&StructureTag::ResourceTree, b"b", 2);
405 let c = make_id(&StructureTag::CausalGraph, b"c", 3);
406
407 store.insert(sample_crossref(
409 a.clone(),
410 b.clone(),
411 CrossRefType::TriggeredBy,
412 ));
413 store.insert(sample_crossref(c, a.clone(), CrossRefType::Elaborates));
415
416 let all = store.get_all(&a);
417 assert_eq!(all.len(), 2);
418 assert_eq!(store.count(), 2);
419 }
420
421 #[test]
422 fn crossref_store_by_type() {
423 let store = CrossRefStore::new();
424 let a = make_id(&StructureTag::ExoChain, b"a", 1);
425 let b = make_id(&StructureTag::ResourceTree, b"b", 2);
426 let c = make_id(&StructureTag::CausalGraph, b"c", 3);
427
428 store.insert(sample_crossref(a.clone(), b, CrossRefType::TriggeredBy));
429 store.insert(sample_crossref(a.clone(), c, CrossRefType::EvidenceFor));
430
431 let triggered = store.by_type(&a, &CrossRefType::TriggeredBy);
432 assert_eq!(triggered.len(), 1);
433
434 let evidence = store.by_type(&a, &CrossRefType::EvidenceFor);
435 assert_eq!(evidence.len(), 1);
436
437 let none = store.by_type(&a, &CrossRefType::SceneBoundary);
438 assert!(none.is_empty());
439 }
440
441 #[test]
442 fn crossref_store_resolves_continuer() {
443 let store = CrossRefStore::new();
446 let listener = make_id(&StructureTag::CausalGraph, b"listener", 1);
447 let speaker = make_id(&StructureTag::CausalGraph, b"speaker", 2);
448 store.insert(sample_crossref(
449 listener.clone(),
450 speaker.clone(),
451 CrossRefType::Continuer,
452 ));
453 let other = make_id(&StructureTag::CausalGraph, b"other", 3);
455 store.insert(sample_crossref(
456 listener.clone(),
457 other,
458 CrossRefType::Elaborates,
459 ));
460
461 let continuers = store.by_type(&listener, &CrossRefType::Continuer);
462 assert_eq!(continuers.len(), 1);
463 assert_eq!(continuers[0].target, speaker);
464 assert_eq!(continuers[0].ref_type, CrossRefType::Continuer);
465 assert_eq!(CrossRefType::Continuer.to_string(), "Continuer");
466 }
467
468 #[test]
469 fn crossref_store_clear() {
470 let store = CrossRefStore::new();
471 let a = make_id(&StructureTag::ExoChain, b"a", 1);
472 let b = make_id(&StructureTag::ResourceTree, b"b", 2);
473 store.insert(sample_crossref(a.clone(), b, CrossRefType::TriggeredBy));
474 assert_eq!(store.count(), 1);
475
476 store.clear();
477 assert_eq!(store.count(), 0);
478 assert!(store.get_forward(&a).is_empty());
479 }
480}