use crate::hlc::HlcTimestamp;
pub const INLINE_RELATION_SLOTS: usize = 4;
pub const MAX_CHAIN_DEPTH: usize = 256;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize,
)]
#[archive(compare(PartialEq))]
#[repr(u8)]
pub enum ProvNodeType {
Entity = 0,
Activity = 1,
Agent = 2,
Plan = 3,
}
impl ProvNodeType {
pub const fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(Self::Entity),
1 => Some(Self::Activity),
2 => Some(Self::Agent),
3 => Some(Self::Plan),
_ => None,
}
}
pub const fn as_u8(self) -> u8 {
self as u8
}
pub const fn is_entity(self) -> bool {
matches!(self, Self::Entity | Self::Plan)
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Default,
rkyv::Archive,
rkyv::Serialize,
rkyv::Deserialize,
)]
#[archive(compare(PartialEq))]
#[repr(u8)]
pub enum ProvRelationKind {
#[default]
None = 0,
WasAttributedTo = 1,
WasGeneratedBy = 2,
WasDerivedFrom = 3,
Used = 4,
WasInformedBy = 5,
WasAssociatedWith = 6,
ActedOnBehalfOf = 7,
}
impl ProvRelationKind {
pub const fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(Self::None),
1 => Some(Self::WasAttributedTo),
2 => Some(Self::WasGeneratedBy),
3 => Some(Self::WasDerivedFrom),
4 => Some(Self::Used),
5 => Some(Self::WasInformedBy),
6 => Some(Self::WasAssociatedWith),
7 => Some(Self::ActedOnBehalfOf),
_ => None,
}
}
pub const fn as_u8(self) -> u8 {
self as u8
}
pub const fn is_some(self) -> bool {
!matches!(self, Self::None)
}
pub const fn expected_source_type(self) -> Option<ProvNodeType> {
match self {
Self::None => None,
Self::WasAttributedTo | Self::WasGeneratedBy | Self::WasDerivedFrom => {
Some(ProvNodeType::Entity)
}
Self::Used | Self::WasInformedBy | Self::WasAssociatedWith => {
Some(ProvNodeType::Activity)
}
Self::ActedOnBehalfOf => Some(ProvNodeType::Agent),
}
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Default, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize,
)]
#[repr(C)]
pub struct ProvRelation {
pub kind: ProvRelationKind,
pub target_id: u64,
}
impl ProvRelation {
pub const EMPTY: Self = Self {
kind: ProvRelationKind::None,
target_id: 0,
};
pub const fn new(kind: ProvRelationKind, target_id: u64) -> Self {
Self { kind, target_id }
}
pub const fn is_some(&self) -> bool {
self.kind.is_some()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
#[repr(C)]
pub struct ProvenanceHeader {
pub node_type: ProvNodeType,
pub node_id: u64,
pub relations: [ProvRelation; INLINE_RELATION_SLOTS],
pub overflow_ref: Option<u64>,
pub prov_timestamp: HlcTimestamp,
pub plan_id: Option<u64>,
}
impl ProvenanceHeader {
pub const fn new(node_type: ProvNodeType, node_id: u64) -> Self {
Self {
node_type,
node_id,
relations: [ProvRelation::EMPTY; INLINE_RELATION_SLOTS],
overflow_ref: None,
prov_timestamp: HlcTimestamp::zero(),
plan_id: None,
}
}
pub fn relation_count(&self) -> usize {
self.relations.iter().filter(|r| r.is_some()).count()
}
pub const fn has_overflow(&self) -> bool {
self.overflow_ref.is_some()
}
pub fn iter_relations(&self) -> impl Iterator<Item = &ProvRelation> {
self.relations.iter().filter(|r| r.is_some())
}
pub fn find_relation(&self, kind: ProvRelationKind) -> Option<&ProvRelation> {
self.relations.iter().find(|r| r.kind == kind)
}
pub fn validate(&self) -> Result<(), ProvenanceError> {
if self.node_id == 0 {
return Err(ProvenanceError::InvalidNodeId);
}
for rel in self.iter_relations() {
if rel.target_id == self.node_id {
return Err(ProvenanceError::SelfLoop {
node_id: self.node_id,
});
}
if let Some(expected) = rel.kind.expected_source_type() {
if expected != self.node_type
&& !(expected == ProvNodeType::Entity && self.node_type == ProvNodeType::Plan)
{
return Err(ProvenanceError::KindTypeMismatch {
kind: rel.kind,
node_type: self.node_type,
});
}
}
}
Ok(())
}
}
impl Default for ProvenanceHeader {
fn default() -> Self {
Self::new(ProvNodeType::Entity, 0)
}
}
#[derive(Debug, Clone)]
pub struct ProvenanceBuilder {
node_type: ProvNodeType,
node_id: u64,
relations: Vec<ProvRelation>,
overflow_ref: Option<u64>,
prov_timestamp: HlcTimestamp,
plan_id: Option<u64>,
}
impl ProvenanceBuilder {
pub fn new(node_type: ProvNodeType, node_id: u64) -> Self {
Self {
node_type,
node_id,
relations: Vec::new(),
overflow_ref: None,
prov_timestamp: HlcTimestamp::zero(),
plan_id: None,
}
}
pub fn with_timestamp(mut self, ts: HlcTimestamp) -> Self {
self.prov_timestamp = ts;
self
}
pub fn with_plan(mut self, plan_id: u64) -> Self {
self.plan_id = Some(plan_id);
self
}
pub fn with_relation(mut self, kind: ProvRelationKind, target_id: u64) -> Self {
self.relations.push(ProvRelation::new(kind, target_id));
self
}
pub fn with_overflow_ref(mut self, overflow_ref: u64) -> Self {
self.overflow_ref = Some(overflow_ref);
self
}
pub fn build(self) -> Result<ProvenanceHeader, ProvenanceError> {
for rel in &self.relations {
if rel.kind == ProvRelationKind::None {
return Err(ProvenanceError::InvalidRelationKind);
}
}
let mut header = ProvenanceHeader::new(self.node_type, self.node_id);
header.prov_timestamp = self.prov_timestamp;
header.plan_id = self.plan_id;
header.overflow_ref = self.overflow_ref;
let inline_take = self.relations.len().min(INLINE_RELATION_SLOTS);
for (slot, rel) in header.relations[..inline_take]
.iter_mut()
.zip(self.relations.iter().take(inline_take))
{
*slot = *rel;
}
if self.relations.len() > INLINE_RELATION_SLOTS && self.overflow_ref.is_none() {
return Err(ProvenanceError::OverflowNotSet {
excess: self.relations.len() - INLINE_RELATION_SLOTS,
});
}
header.validate()?;
Ok(header)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProvenanceError {
InvalidNodeId,
InvalidRelationKind,
KindTypeMismatch {
kind: ProvRelationKind,
node_type: ProvNodeType,
},
SelfLoop {
node_id: u64,
},
ChainTooDeep {
depth: usize,
},
CycleDetected {
at_node: u64,
},
OverflowNotSet {
excess: usize,
},
}
impl std::fmt::Display for ProvenanceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidNodeId => write!(f, "provenance node_id must be non-zero"),
Self::InvalidRelationKind => {
write!(f, "relation kind None is reserved for empty slots")
}
Self::KindTypeMismatch { kind, node_type } => write!(
f,
"relation kind {:?} is not valid from node type {:?}",
kind, node_type
),
Self::SelfLoop { node_id } => {
write!(f, "relation targets the source node itself ({})", node_id)
}
Self::ChainTooDeep { depth } => {
write!(f, "provenance chain exceeded max depth ({})", depth)
}
Self::CycleDetected { at_node } => {
write!(f, "provenance chain cycle detected at node {}", at_node)
}
Self::OverflowNotSet { excess } => write!(
f,
"{} relations overflowed inline slots; call with_overflow_ref",
excess
),
}
}
}
impl std::error::Error for ProvenanceError {}
pub fn validate_chain<F>(start: u64, mut next: F) -> Result<usize, ProvenanceError>
where
F: FnMut(u64) -> Option<u64>,
{
let mut seen: Vec<u64> = Vec::with_capacity(16);
let mut cursor = Some(start);
let mut depth = 0usize;
while let Some(node) = cursor {
if seen.contains(&node) {
return Err(ProvenanceError::CycleDetected { at_node: node });
}
seen.push(node);
depth += 1;
if depth > MAX_CHAIN_DEPTH {
return Err(ProvenanceError::ChainTooDeep { depth });
}
cursor = next(node);
}
Ok(depth)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_provenance_header_size() {
let size = std::mem::size_of::<ProvenanceHeader>();
assert!(
size <= 160,
"ProvenanceHeader size {} exceeds reasonable envelope budget",
size
);
println!("ProvenanceHeader size: {} bytes", size);
}
#[test]
fn test_prov_node_type_roundtrip() {
for kind in [
ProvNodeType::Entity,
ProvNodeType::Activity,
ProvNodeType::Agent,
ProvNodeType::Plan,
] {
assert_eq!(ProvNodeType::from_u8(kind.as_u8()), Some(kind));
}
assert_eq!(ProvNodeType::from_u8(99), None);
}
#[test]
fn test_prov_relation_kind_roundtrip() {
use ProvRelationKind::*;
for kind in [
None,
WasAttributedTo,
WasGeneratedBy,
WasDerivedFrom,
Used,
WasInformedBy,
WasAssociatedWith,
ActedOnBehalfOf,
] {
assert_eq!(ProvRelationKind::from_u8(kind.as_u8()), Some(kind));
}
assert_eq!(ProvRelationKind::from_u8(200), Option::None);
}
#[test]
fn test_expected_source_type() {
use ProvRelationKind::*;
assert_eq!(
WasAttributedTo.expected_source_type(),
Some(ProvNodeType::Entity)
);
assert_eq!(Used.expected_source_type(), Some(ProvNodeType::Activity));
assert_eq!(
ActedOnBehalfOf.expected_source_type(),
Some(ProvNodeType::Agent)
);
assert_eq!(None.expected_source_type(), Option::None);
}
#[test]
fn test_builder_basic() {
let hdr = ProvenanceBuilder::new(ProvNodeType::Entity, 0xDEADBEEF)
.with_relation(ProvRelationKind::WasAttributedTo, 0xA1)
.with_relation(ProvRelationKind::WasGeneratedBy, 0xA2)
.build()
.expect("valid provenance");
assert_eq!(hdr.node_type, ProvNodeType::Entity);
assert_eq!(hdr.node_id, 0xDEADBEEF);
assert_eq!(hdr.relation_count(), 2);
assert!(!hdr.has_overflow());
assert_eq!(
hdr.find_relation(ProvRelationKind::WasAttributedTo)
.map(|r| r.target_id),
Some(0xA1)
);
}
#[test]
fn test_builder_with_timestamp_and_plan() {
let ts = HlcTimestamp::new(1234, 5, 1);
let hdr = ProvenanceBuilder::new(ProvNodeType::Activity, 42)
.with_timestamp(ts)
.with_plan(99)
.with_relation(ProvRelationKind::WasAssociatedWith, 7)
.build()
.unwrap();
assert_eq!(hdr.prov_timestamp, ts);
assert_eq!(hdr.plan_id, Some(99));
}
#[test]
fn test_builder_fills_inline_slots_sequentially() {
let hdr = ProvenanceBuilder::new(ProvNodeType::Entity, 1)
.with_relation(ProvRelationKind::WasDerivedFrom, 10)
.with_relation(ProvRelationKind::WasDerivedFrom, 11)
.with_relation(ProvRelationKind::WasDerivedFrom, 12)
.with_relation(ProvRelationKind::WasDerivedFrom, 13)
.build()
.unwrap();
assert_eq!(hdr.relation_count(), 4);
assert!(!hdr.has_overflow());
for (i, r) in hdr.iter_relations().enumerate() {
assert_eq!(r.target_id, 10 + i as u64);
}
}
#[test]
fn test_builder_overflow_without_ref_rejected() {
let err = ProvenanceBuilder::new(ProvNodeType::Entity, 1)
.with_relation(ProvRelationKind::WasDerivedFrom, 10)
.with_relation(ProvRelationKind::WasDerivedFrom, 11)
.with_relation(ProvRelationKind::WasDerivedFrom, 12)
.with_relation(ProvRelationKind::WasDerivedFrom, 13)
.with_relation(ProvRelationKind::WasDerivedFrom, 14)
.build()
.unwrap_err();
assert_eq!(err, ProvenanceError::OverflowNotSet { excess: 1 });
}
#[test]
fn test_builder_overflow_with_ref_succeeds() {
let hdr = ProvenanceBuilder::new(ProvNodeType::Entity, 1)
.with_relation(ProvRelationKind::WasDerivedFrom, 10)
.with_relation(ProvRelationKind::WasDerivedFrom, 11)
.with_relation(ProvRelationKind::WasDerivedFrom, 12)
.with_relation(ProvRelationKind::WasDerivedFrom, 13)
.with_relation(ProvRelationKind::WasDerivedFrom, 14)
.with_relation(ProvRelationKind::WasDerivedFrom, 15)
.with_overflow_ref(0xBEEF)
.build()
.unwrap();
assert!(hdr.has_overflow());
assert_eq!(hdr.overflow_ref, Some(0xBEEF));
assert_eq!(hdr.relation_count(), INLINE_RELATION_SLOTS);
}
#[test]
fn test_builder_rejects_none_relation() {
let err = ProvenanceBuilder::new(ProvNodeType::Entity, 1)
.with_relation(ProvRelationKind::None, 42)
.build()
.unwrap_err();
assert_eq!(err, ProvenanceError::InvalidRelationKind);
}
#[test]
fn test_builder_rejects_zero_node_id() {
let err = ProvenanceBuilder::new(ProvNodeType::Entity, 0)
.build()
.unwrap_err();
assert_eq!(err, ProvenanceError::InvalidNodeId);
}
#[test]
fn test_builder_rejects_self_loop() {
let err = ProvenanceBuilder::new(ProvNodeType::Entity, 7)
.with_relation(ProvRelationKind::WasDerivedFrom, 7)
.build()
.unwrap_err();
assert_eq!(err, ProvenanceError::SelfLoop { node_id: 7 });
}
#[test]
fn test_builder_rejects_kind_type_mismatch() {
let err = ProvenanceBuilder::new(ProvNodeType::Entity, 1)
.with_relation(ProvRelationKind::Used, 2)
.build()
.unwrap_err();
assert_eq!(
err,
ProvenanceError::KindTypeMismatch {
kind: ProvRelationKind::Used,
node_type: ProvNodeType::Entity
}
);
}
#[test]
fn test_plan_accepts_entity_relations() {
let hdr = ProvenanceBuilder::new(ProvNodeType::Plan, 100)
.with_relation(ProvRelationKind::WasAttributedTo, 200)
.build()
.unwrap();
assert_eq!(hdr.node_type, ProvNodeType::Plan);
}
#[test]
fn test_validate_chain_simple() {
let parents = |n: u64| match n {
1 => Some(2),
2 => Some(3),
_ => None,
};
assert_eq!(validate_chain(1, parents).unwrap(), 3);
}
#[test]
fn test_validate_chain_detects_cycle() {
let parents = |n: u64| match n {
1 => Some(2),
2 => Some(1),
_ => None,
};
let err = validate_chain(1, parents).unwrap_err();
assert_eq!(err, ProvenanceError::CycleDetected { at_node: 1 });
}
#[test]
fn test_validate_chain_depth_bounded() {
let parents = |n: u64| Some(n + 1);
let err = validate_chain(1, parents).unwrap_err();
assert!(matches!(err, ProvenanceError::ChainTooDeep { .. }));
}
#[test]
fn test_header_iter_relations_skips_empty() {
let mut hdr = ProvenanceHeader::new(ProvNodeType::Entity, 1);
hdr.relations[0] = ProvRelation::new(ProvRelationKind::WasAttributedTo, 10);
hdr.relations[2] = ProvRelation::new(ProvRelationKind::WasGeneratedBy, 20);
let targets: Vec<u64> = hdr.iter_relations().map(|r| r.target_id).collect();
assert_eq!(targets, vec![10, 20]);
}
#[test]
fn test_header_validate_rejects_zero_node() {
let hdr = ProvenanceHeader::new(ProvNodeType::Entity, 0);
assert_eq!(hdr.validate().unwrap_err(), ProvenanceError::InvalidNodeId);
}
#[test]
fn test_default_header_has_zero_node() {
let hdr = ProvenanceHeader::default();
assert_eq!(hdr.node_id, 0);
assert_eq!(hdr.relation_count(), 0);
}
}