1use crate::hlc::HlcTimestamp;
57
58pub const INLINE_RELATION_SLOTS: usize = 4;
62
63pub const MAX_CHAIN_DEPTH: usize = 256;
66
67#[derive(
73 Debug, Clone, Copy, PartialEq, Eq, Hash, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize,
74)]
75#[archive(compare(PartialEq))]
76#[repr(u8)]
77pub enum ProvNodeType {
78 Entity = 0,
80 Activity = 1,
82 Agent = 2,
84 Plan = 3,
86}
87
88impl ProvNodeType {
89 pub const fn from_u8(value: u8) -> Option<Self> {
91 match value {
92 0 => Some(Self::Entity),
93 1 => Some(Self::Activity),
94 2 => Some(Self::Agent),
95 3 => Some(Self::Plan),
96 _ => None,
97 }
98 }
99
100 pub const fn as_u8(self) -> u8 {
102 self as u8
103 }
104
105 pub const fn is_entity(self) -> bool {
107 matches!(self, Self::Entity | Self::Plan)
108 }
109}
110
111#[derive(
117 Debug,
118 Clone,
119 Copy,
120 PartialEq,
121 Eq,
122 Hash,
123 Default,
124 rkyv::Archive,
125 rkyv::Serialize,
126 rkyv::Deserialize,
127)]
128#[archive(compare(PartialEq))]
129#[repr(u8)]
130pub enum ProvRelationKind {
131 #[default]
133 None = 0,
134 WasAttributedTo = 1,
136 WasGeneratedBy = 2,
138 WasDerivedFrom = 3,
140 Used = 4,
142 WasInformedBy = 5,
144 WasAssociatedWith = 6,
146 ActedOnBehalfOf = 7,
148}
149
150impl ProvRelationKind {
151 pub const fn from_u8(value: u8) -> Option<Self> {
153 match value {
154 0 => Some(Self::None),
155 1 => Some(Self::WasAttributedTo),
156 2 => Some(Self::WasGeneratedBy),
157 3 => Some(Self::WasDerivedFrom),
158 4 => Some(Self::Used),
159 5 => Some(Self::WasInformedBy),
160 6 => Some(Self::WasAssociatedWith),
161 7 => Some(Self::ActedOnBehalfOf),
162 _ => None,
163 }
164 }
165
166 pub const fn as_u8(self) -> u8 {
168 self as u8
169 }
170
171 pub const fn is_some(self) -> bool {
173 !matches!(self, Self::None)
174 }
175
176 pub const fn expected_source_type(self) -> Option<ProvNodeType> {
180 match self {
181 Self::None => None,
182 Self::WasAttributedTo | Self::WasGeneratedBy | Self::WasDerivedFrom => {
183 Some(ProvNodeType::Entity)
184 }
185 Self::Used | Self::WasInformedBy | Self::WasAssociatedWith => {
186 Some(ProvNodeType::Activity)
187 }
188 Self::ActedOnBehalfOf => Some(ProvNodeType::Agent),
189 }
190 }
191}
192
193#[derive(
198 Debug, Clone, Copy, PartialEq, Eq, Default, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize,
199)]
200#[repr(C)]
201pub struct ProvRelation {
202 pub kind: ProvRelationKind,
204 pub target_id: u64,
206}
207
208impl ProvRelation {
209 pub const EMPTY: Self = Self {
211 kind: ProvRelationKind::None,
212 target_id: 0,
213 };
214
215 pub const fn new(kind: ProvRelationKind, target_id: u64) -> Self {
217 Self { kind, target_id }
218 }
219
220 pub const fn is_some(&self) -> bool {
222 self.kind.is_some()
223 }
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
236#[repr(C)]
237pub struct ProvenanceHeader {
238 pub node_type: ProvNodeType,
240
241 pub node_id: u64,
243
244 pub relations: [ProvRelation; INLINE_RELATION_SLOTS],
247
248 pub overflow_ref: Option<u64>,
251
252 pub prov_timestamp: HlcTimestamp,
254
255 pub plan_id: Option<u64>,
257}
258
259impl ProvenanceHeader {
260 pub const fn new(node_type: ProvNodeType, node_id: u64) -> Self {
262 Self {
263 node_type,
264 node_id,
265 relations: [ProvRelation::EMPTY; INLINE_RELATION_SLOTS],
266 overflow_ref: None,
267 prov_timestamp: HlcTimestamp::zero(),
268 plan_id: None,
269 }
270 }
271
272 pub fn relation_count(&self) -> usize {
274 self.relations.iter().filter(|r| r.is_some()).count()
275 }
276
277 pub const fn has_overflow(&self) -> bool {
279 self.overflow_ref.is_some()
280 }
281
282 pub fn iter_relations(&self) -> impl Iterator<Item = &ProvRelation> {
284 self.relations.iter().filter(|r| r.is_some())
285 }
286
287 pub fn find_relation(&self, kind: ProvRelationKind) -> Option<&ProvRelation> {
289 self.relations.iter().find(|r| r.kind == kind)
290 }
291
292 pub fn validate(&self) -> Result<(), ProvenanceError> {
299 if self.node_id == 0 {
300 return Err(ProvenanceError::InvalidNodeId);
301 }
302
303 for rel in self.iter_relations() {
304 if rel.target_id == self.node_id {
305 return Err(ProvenanceError::SelfLoop {
306 node_id: self.node_id,
307 });
308 }
309 if let Some(expected) = rel.kind.expected_source_type() {
310 if expected != self.node_type
311 && !(expected == ProvNodeType::Entity && self.node_type == ProvNodeType::Plan)
312 {
313 return Err(ProvenanceError::KindTypeMismatch {
314 kind: rel.kind,
315 node_type: self.node_type,
316 });
317 }
318 }
319 }
320
321 Ok(())
322 }
323}
324
325impl Default for ProvenanceHeader {
326 fn default() -> Self {
327 Self::new(ProvNodeType::Entity, 0)
328 }
329}
330
331#[derive(Debug, Clone)]
340pub struct ProvenanceBuilder {
341 node_type: ProvNodeType,
342 node_id: u64,
343 relations: Vec<ProvRelation>,
344 overflow_ref: Option<u64>,
345 prov_timestamp: HlcTimestamp,
346 plan_id: Option<u64>,
347}
348
349impl ProvenanceBuilder {
350 pub fn new(node_type: ProvNodeType, node_id: u64) -> Self {
352 Self {
353 node_type,
354 node_id,
355 relations: Vec::new(),
356 overflow_ref: None,
357 prov_timestamp: HlcTimestamp::zero(),
358 plan_id: None,
359 }
360 }
361
362 pub fn with_timestamp(mut self, ts: HlcTimestamp) -> Self {
364 self.prov_timestamp = ts;
365 self
366 }
367
368 pub fn with_plan(mut self, plan_id: u64) -> Self {
370 self.plan_id = Some(plan_id);
371 self
372 }
373
374 pub fn with_relation(mut self, kind: ProvRelationKind, target_id: u64) -> Self {
377 self.relations.push(ProvRelation::new(kind, target_id));
378 self
379 }
380
381 pub fn with_overflow_ref(mut self, overflow_ref: u64) -> Self {
384 self.overflow_ref = Some(overflow_ref);
385 self
386 }
387
388 pub fn build(self) -> Result<ProvenanceHeader, ProvenanceError> {
395 for rel in &self.relations {
397 if rel.kind == ProvRelationKind::None {
398 return Err(ProvenanceError::InvalidRelationKind);
399 }
400 }
401
402 let mut header = ProvenanceHeader::new(self.node_type, self.node_id);
403 header.prov_timestamp = self.prov_timestamp;
404 header.plan_id = self.plan_id;
405 header.overflow_ref = self.overflow_ref;
406
407 let inline_take = self.relations.len().min(INLINE_RELATION_SLOTS);
408 for (slot, rel) in header.relations[..inline_take]
409 .iter_mut()
410 .zip(self.relations.iter().take(inline_take))
411 {
412 *slot = *rel;
413 }
414
415 if self.relations.len() > INLINE_RELATION_SLOTS && self.overflow_ref.is_none() {
416 return Err(ProvenanceError::OverflowNotSet {
417 excess: self.relations.len() - INLINE_RELATION_SLOTS,
418 });
419 }
420
421 header.validate()?;
422 Ok(header)
423 }
424}
425
426#[derive(Debug, Clone, Copy, PartialEq, Eq)]
428pub enum ProvenanceError {
429 InvalidNodeId,
431 InvalidRelationKind,
433 KindTypeMismatch {
435 kind: ProvRelationKind,
437 node_type: ProvNodeType,
439 },
440 SelfLoop {
442 node_id: u64,
444 },
445 ChainTooDeep {
447 depth: usize,
449 },
450 CycleDetected {
452 at_node: u64,
454 },
455 OverflowNotSet {
457 excess: usize,
459 },
460}
461
462impl std::fmt::Display for ProvenanceError {
463 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464 match self {
465 Self::InvalidNodeId => write!(f, "provenance node_id must be non-zero"),
466 Self::InvalidRelationKind => {
467 write!(f, "relation kind None is reserved for empty slots")
468 }
469 Self::KindTypeMismatch { kind, node_type } => write!(
470 f,
471 "relation kind {:?} is not valid from node type {:?}",
472 kind, node_type
473 ),
474 Self::SelfLoop { node_id } => {
475 write!(f, "relation targets the source node itself ({})", node_id)
476 }
477 Self::ChainTooDeep { depth } => {
478 write!(f, "provenance chain exceeded max depth ({})", depth)
479 }
480 Self::CycleDetected { at_node } => {
481 write!(f, "provenance chain cycle detected at node {}", at_node)
482 }
483 Self::OverflowNotSet { excess } => write!(
484 f,
485 "{} relations overflowed inline slots; call with_overflow_ref",
486 excess
487 ),
488 }
489 }
490}
491
492impl std::error::Error for ProvenanceError {}
493
494pub fn validate_chain<F>(start: u64, mut next: F) -> Result<usize, ProvenanceError>
502where
503 F: FnMut(u64) -> Option<u64>,
504{
505 let mut seen: Vec<u64> = Vec::with_capacity(16);
506 let mut cursor = Some(start);
507 let mut depth = 0usize;
508
509 while let Some(node) = cursor {
510 if seen.contains(&node) {
511 return Err(ProvenanceError::CycleDetected { at_node: node });
512 }
513 seen.push(node);
514 depth += 1;
515 if depth > MAX_CHAIN_DEPTH {
516 return Err(ProvenanceError::ChainTooDeep { depth });
517 }
518 cursor = next(node);
519 }
520 Ok(depth)
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526
527 #[test]
532 fn test_provenance_header_size() {
533 let size = std::mem::size_of::<ProvenanceHeader>();
537 assert!(
538 size <= 160,
539 "ProvenanceHeader size {} exceeds reasonable envelope budget",
540 size
541 );
542 println!("ProvenanceHeader size: {} bytes", size);
544 }
545
546 #[test]
547 fn test_prov_node_type_roundtrip() {
548 for kind in [
549 ProvNodeType::Entity,
550 ProvNodeType::Activity,
551 ProvNodeType::Agent,
552 ProvNodeType::Plan,
553 ] {
554 assert_eq!(ProvNodeType::from_u8(kind.as_u8()), Some(kind));
555 }
556 assert_eq!(ProvNodeType::from_u8(99), None);
557 }
558
559 #[test]
560 fn test_prov_relation_kind_roundtrip() {
561 use ProvRelationKind::*;
562 for kind in [
563 None,
564 WasAttributedTo,
565 WasGeneratedBy,
566 WasDerivedFrom,
567 Used,
568 WasInformedBy,
569 WasAssociatedWith,
570 ActedOnBehalfOf,
571 ] {
572 assert_eq!(ProvRelationKind::from_u8(kind.as_u8()), Some(kind));
573 }
574 assert_eq!(ProvRelationKind::from_u8(200), Option::None);
575 }
576
577 #[test]
578 fn test_expected_source_type() {
579 use ProvRelationKind::*;
580 assert_eq!(
581 WasAttributedTo.expected_source_type(),
582 Some(ProvNodeType::Entity)
583 );
584 assert_eq!(Used.expected_source_type(), Some(ProvNodeType::Activity));
585 assert_eq!(
586 ActedOnBehalfOf.expected_source_type(),
587 Some(ProvNodeType::Agent)
588 );
589 assert_eq!(None.expected_source_type(), Option::None);
590 }
591
592 #[test]
597 fn test_builder_basic() {
598 let hdr = ProvenanceBuilder::new(ProvNodeType::Entity, 0xDEADBEEF)
599 .with_relation(ProvRelationKind::WasAttributedTo, 0xA1)
600 .with_relation(ProvRelationKind::WasGeneratedBy, 0xA2)
601 .build()
602 .expect("valid provenance");
603
604 assert_eq!(hdr.node_type, ProvNodeType::Entity);
605 assert_eq!(hdr.node_id, 0xDEADBEEF);
606 assert_eq!(hdr.relation_count(), 2);
607 assert!(!hdr.has_overflow());
608 assert_eq!(
609 hdr.find_relation(ProvRelationKind::WasAttributedTo)
610 .map(|r| r.target_id),
611 Some(0xA1)
612 );
613 }
614
615 #[test]
616 fn test_builder_with_timestamp_and_plan() {
617 let ts = HlcTimestamp::new(1234, 5, 1);
618 let hdr = ProvenanceBuilder::new(ProvNodeType::Activity, 42)
619 .with_timestamp(ts)
620 .with_plan(99)
621 .with_relation(ProvRelationKind::WasAssociatedWith, 7)
622 .build()
623 .unwrap();
624 assert_eq!(hdr.prov_timestamp, ts);
625 assert_eq!(hdr.plan_id, Some(99));
626 }
627
628 #[test]
629 fn test_builder_fills_inline_slots_sequentially() {
630 let hdr = ProvenanceBuilder::new(ProvNodeType::Entity, 1)
631 .with_relation(ProvRelationKind::WasDerivedFrom, 10)
632 .with_relation(ProvRelationKind::WasDerivedFrom, 11)
633 .with_relation(ProvRelationKind::WasDerivedFrom, 12)
634 .with_relation(ProvRelationKind::WasDerivedFrom, 13)
635 .build()
636 .unwrap();
637
638 assert_eq!(hdr.relation_count(), 4);
639 assert!(!hdr.has_overflow());
640 for (i, r) in hdr.iter_relations().enumerate() {
641 assert_eq!(r.target_id, 10 + i as u64);
642 }
643 }
644
645 #[test]
646 fn test_builder_overflow_without_ref_rejected() {
647 let err = ProvenanceBuilder::new(ProvNodeType::Entity, 1)
648 .with_relation(ProvRelationKind::WasDerivedFrom, 10)
649 .with_relation(ProvRelationKind::WasDerivedFrom, 11)
650 .with_relation(ProvRelationKind::WasDerivedFrom, 12)
651 .with_relation(ProvRelationKind::WasDerivedFrom, 13)
652 .with_relation(ProvRelationKind::WasDerivedFrom, 14)
653 .build()
654 .unwrap_err();
655 assert_eq!(err, ProvenanceError::OverflowNotSet { excess: 1 });
656 }
657
658 #[test]
659 fn test_builder_overflow_with_ref_succeeds() {
660 let hdr = ProvenanceBuilder::new(ProvNodeType::Entity, 1)
661 .with_relation(ProvRelationKind::WasDerivedFrom, 10)
662 .with_relation(ProvRelationKind::WasDerivedFrom, 11)
663 .with_relation(ProvRelationKind::WasDerivedFrom, 12)
664 .with_relation(ProvRelationKind::WasDerivedFrom, 13)
665 .with_relation(ProvRelationKind::WasDerivedFrom, 14)
666 .with_relation(ProvRelationKind::WasDerivedFrom, 15)
667 .with_overflow_ref(0xBEEF)
668 .build()
669 .unwrap();
670 assert!(hdr.has_overflow());
671 assert_eq!(hdr.overflow_ref, Some(0xBEEF));
672 assert_eq!(hdr.relation_count(), INLINE_RELATION_SLOTS);
674 }
675
676 #[test]
677 fn test_builder_rejects_none_relation() {
678 let err = ProvenanceBuilder::new(ProvNodeType::Entity, 1)
679 .with_relation(ProvRelationKind::None, 42)
680 .build()
681 .unwrap_err();
682 assert_eq!(err, ProvenanceError::InvalidRelationKind);
683 }
684
685 #[test]
686 fn test_builder_rejects_zero_node_id() {
687 let err = ProvenanceBuilder::new(ProvNodeType::Entity, 0)
688 .build()
689 .unwrap_err();
690 assert_eq!(err, ProvenanceError::InvalidNodeId);
691 }
692
693 #[test]
694 fn test_builder_rejects_self_loop() {
695 let err = ProvenanceBuilder::new(ProvNodeType::Entity, 7)
696 .with_relation(ProvRelationKind::WasDerivedFrom, 7)
697 .build()
698 .unwrap_err();
699 assert_eq!(err, ProvenanceError::SelfLoop { node_id: 7 });
700 }
701
702 #[test]
703 fn test_builder_rejects_kind_type_mismatch() {
704 let err = ProvenanceBuilder::new(ProvNodeType::Entity, 1)
706 .with_relation(ProvRelationKind::Used, 2)
707 .build()
708 .unwrap_err();
709 assert_eq!(
710 err,
711 ProvenanceError::KindTypeMismatch {
712 kind: ProvRelationKind::Used,
713 node_type: ProvNodeType::Entity
714 }
715 );
716 }
717
718 #[test]
719 fn test_plan_accepts_entity_relations() {
720 let hdr = ProvenanceBuilder::new(ProvNodeType::Plan, 100)
722 .with_relation(ProvRelationKind::WasAttributedTo, 200)
723 .build()
724 .unwrap();
725 assert_eq!(hdr.node_type, ProvNodeType::Plan);
726 }
727
728 #[test]
733 fn test_validate_chain_simple() {
734 let parents = |n: u64| match n {
736 1 => Some(2),
737 2 => Some(3),
738 _ => None,
739 };
740 assert_eq!(validate_chain(1, parents).unwrap(), 3);
741 }
742
743 #[test]
744 fn test_validate_chain_detects_cycle() {
745 let parents = |n: u64| match n {
747 1 => Some(2),
748 2 => Some(1),
749 _ => None,
750 };
751 let err = validate_chain(1, parents).unwrap_err();
752 assert_eq!(err, ProvenanceError::CycleDetected { at_node: 1 });
753 }
754
755 #[test]
756 fn test_validate_chain_depth_bounded() {
757 let parents = |n: u64| Some(n + 1);
759 let err = validate_chain(1, parents).unwrap_err();
760 assert!(matches!(err, ProvenanceError::ChainTooDeep { .. }));
761 }
762
763 #[test]
768 fn test_header_iter_relations_skips_empty() {
769 let mut hdr = ProvenanceHeader::new(ProvNodeType::Entity, 1);
770 hdr.relations[0] = ProvRelation::new(ProvRelationKind::WasAttributedTo, 10);
771 hdr.relations[2] = ProvRelation::new(ProvRelationKind::WasGeneratedBy, 20);
772 let targets: Vec<u64> = hdr.iter_relations().map(|r| r.target_id).collect();
773 assert_eq!(targets, vec![10, 20]);
774 }
775
776 #[test]
777 fn test_header_validate_rejects_zero_node() {
778 let hdr = ProvenanceHeader::new(ProvNodeType::Entity, 0);
779 assert_eq!(hdr.validate().unwrap_err(), ProvenanceError::InvalidNodeId);
780 }
781
782 #[test]
783 fn test_default_header_has_zero_node() {
784 let hdr = ProvenanceHeader::default();
786 assert_eq!(hdr.node_id, 0);
787 assert_eq!(hdr.relation_count(), 0);
788 }
789}