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}