switchyard/types/ids.rs
1//! Deterministic `UUIDv5` identifiers for plans and actions.
2//!
3//! The UUID namespace is derived from a stable tag (`NS_TAG`) so that
4//! `plan_id` and `action_id` are reproducible across runs for the same
5//! serialized action sequence.
6use std::fmt::Write;
7use uuid::Uuid;
8
9use super::{
10 plan::{Action, Plan},
11 safepath::SafePath,
12};
13// UUIDv5 namespace tag for deterministic plan/action IDs.
14// See SPEC Reproducible v1.1 (Determinism) for guidance.
15use crate::constants::NS_TAG;
16
17/// Internal: return the UUID namespace used for deterministic IDs.
18fn namespace() -> Uuid {
19 Uuid::new_v5(&Uuid::NAMESPACE_URL, NS_TAG.as_bytes())
20}
21
22/// Return the relative representation of a `SafePath` to keep IDs independent of roots.
23fn sp_rel(p: &SafePath) -> String {
24 // Use the relative portion only for determinism across roots
25 p.rel().to_string_lossy().to_string()
26}
27
28/// Serialize an action into a stable, human-readable string used for `UUIDv5` input.
29fn serialize_action(a: &Action) -> String {
30 match a {
31 Action::EnsureSymlink { source, target } => {
32 format!("E:{}->{}", sp_rel(source), sp_rel(target))
33 }
34 Action::RestoreFromBackup { target } => {
35 format!("R:{}", sp_rel(target))
36 }
37 }
38}
39
40/// Compute a deterministic `UUIDv5` for a plan by serializing actions in order.
41///
42/// Two plans with identical action sequences (including ordering) will have the
43/// same `plan_id`, independent of the root directories used by `SafePath`.
44#[must_use]
45pub fn plan_id(plan: &Plan) -> Uuid {
46 let ns = namespace();
47 // Deterministic serialization in action order
48 let mut s = String::new();
49 for a in &plan.actions {
50 s.push_str(&serialize_action(a));
51 s.push('\n');
52 }
53 Uuid::new_v5(&ns, s.as_bytes())
54}
55
56/// Compute a deterministic `UUIDv5` for an action as a function of the plan ID and
57/// the action's serialized form, including the stable position index.
58#[must_use]
59pub fn action_id(plan_id: &Uuid, action: &Action, idx: usize) -> Uuid {
60 let mut s = serialize_action(action);
61 let _ = write!(s, "#{idx}"); // Ignore formatting errors as they're unlikely in this context
62 Uuid::new_v5(plan_id, s.as_bytes())
63}