Skip to main content

worldinterface_flowspec/
id.rs

1//! Deterministic ID derivation for compilation.
2//!
3//! Uses UUID v5 (SHA-1 namespace-based) to derive stable, deterministic IDs
4//! from a FlowRunId + NodeId pair. This ensures that recompiling the same
5//! FlowSpec with the same FlowRunId always produces identical TaskIds and
6//! StepRunIds.
7
8use actionqueue_core::ids::TaskId;
9use uuid::Uuid;
10use worldinterface_core::id::{FlowRunId, NodeId, StepRunId};
11
12/// Derives a deterministic StepRunId from a FlowRunId and NodeId.
13///
14/// Uses UUID v5 with the FlowRunId as namespace and the NodeId bytes
15/// prefixed with "step_run:" as name. This guarantees:
16/// - Same (flow_run_id, node_id) pair always produces the same StepRunId
17/// - Different pairs produce different StepRunIds (with cryptographic probability)
18/// - No external state or coordination required
19pub fn derive_step_run_id(flow_run_id: FlowRunId, node_id: NodeId) -> StepRunId {
20    let namespace = *flow_run_id.as_ref();
21    let mut name = b"step_run:".to_vec();
22    name.extend_from_slice(node_id.as_ref().as_bytes());
23    let derived = Uuid::new_v5(&namespace, &name);
24    StepRunId::from(derived)
25}
26
27/// Derives a deterministic TaskId for a step from a FlowRunId and NodeId.
28///
29/// Uses UUID v5 with the FlowRunId as namespace and the NodeId bytes
30/// prefixed with "task:" as name. The prefix distinguishes this derivation
31/// from `derive_step_run_id` to avoid collisions.
32pub fn derive_task_id(flow_run_id: FlowRunId, node_id: NodeId) -> TaskId {
33    let namespace = *flow_run_id.as_ref();
34    let mut name = b"task:".to_vec();
35    name.extend_from_slice(node_id.as_ref().as_bytes());
36    let derived = Uuid::new_v5(&namespace, &name);
37    TaskId::from_uuid(derived)
38}
39
40/// Derives a deterministic TaskId for the Coordinator from a FlowRunId.
41///
42/// Uses UUID v5 with the FlowRunId as namespace and `b"coordinator"` as name.
43/// This cannot collide with step TaskIds because NodeId bytes are 16-byte UUIDs,
44/// not ASCII strings.
45pub fn derive_coordinator_task_id(flow_run_id: FlowRunId) -> TaskId {
46    let namespace = *flow_run_id.as_ref();
47    TaskId::from_uuid(Uuid::new_v5(&namespace, b"coordinator"))
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn step_run_id_deterministic() {
56        let fr = FlowRunId::new();
57        let n = NodeId::new();
58        let a = derive_step_run_id(fr, n);
59        let b = derive_step_run_id(fr, n);
60        assert_eq!(a, b);
61    }
62
63    #[test]
64    fn step_run_id_unique_per_node() {
65        let fr = FlowRunId::new();
66        let n1 = NodeId::new();
67        let n2 = NodeId::new();
68        assert_ne!(derive_step_run_id(fr, n1), derive_step_run_id(fr, n2));
69    }
70
71    #[test]
72    fn step_run_id_unique_per_flow_run() {
73        let fr1 = FlowRunId::new();
74        let fr2 = FlowRunId::new();
75        let n = NodeId::new();
76        assert_ne!(derive_step_run_id(fr1, n), derive_step_run_id(fr2, n));
77    }
78
79    #[test]
80    fn step_run_id_not_nil() {
81        let fr = FlowRunId::new();
82        let n = NodeId::new();
83        let id = derive_step_run_id(fr, n);
84        let uuid: &Uuid = id.as_ref();
85        assert!(!uuid.is_nil());
86    }
87
88    #[test]
89    fn task_id_deterministic() {
90        let fr = FlowRunId::new();
91        let n = NodeId::new();
92        assert_eq!(derive_task_id(fr, n), derive_task_id(fr, n));
93    }
94
95    #[test]
96    fn task_id_unique_per_node() {
97        let fr = FlowRunId::new();
98        let n1 = NodeId::new();
99        let n2 = NodeId::new();
100        assert_ne!(derive_task_id(fr, n1), derive_task_id(fr, n2));
101    }
102
103    #[test]
104    fn coordinator_task_id_deterministic() {
105        let fr = FlowRunId::new();
106        assert_eq!(derive_coordinator_task_id(fr), derive_coordinator_task_id(fr));
107    }
108
109    #[test]
110    fn coordinator_task_id_not_nil() {
111        let fr = FlowRunId::new();
112        let id = derive_coordinator_task_id(fr);
113        assert!(!id.is_nil());
114    }
115
116    #[test]
117    fn coordinator_and_step_task_ids_differ() {
118        let fr = FlowRunId::new();
119        let n = NodeId::new();
120        let coord_id = derive_coordinator_task_id(fr);
121        let step_id = derive_task_id(fr, n);
122        assert_ne!(coord_id, step_id);
123    }
124
125    #[test]
126    fn step_run_id_and_task_id_differ() {
127        let fr = FlowRunId::new();
128        let n = NodeId::new();
129        let step_run = derive_step_run_id(fr, n);
130        let task = derive_task_id(fr, n);
131        assert_ne!(step_run.as_ref(), task.as_uuid());
132    }
133}