Skip to main content

tatara_process/
identity.rs

1//! Content-addressable identity — deterministic naming from spec.
2//!
3//! Every Process gets a 128-bit BLAKE3 hash of its canonical spec,
4//! base32-encoded (26 chars) using an unambiguous alphabet (no 0/1/o/l).
5//!
6//! Ported from convergence-controller/src/identity.rs, generalized over
7//! any `Serialize` spec (not just `ConvergenceProcessSpec`).
8
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11
12/// Length of the truncated hash in bytes (128 bits of collision space).
13const HASH_BYTES: usize = 16;
14
15/// Crockford base32 alphabet — 32 chars, excludes `i/l/o/u` to remove the
16/// most common visual collisions (1/l/i, 0/o, u/v). Matches Douglas
17/// Crockford's published base32 spec.
18const BASE32_ALPHABET: &[u8] = b"0123456789abcdefghjkmnpqrstvwxyz";
19
20/// Resolved identity — human-assigned or content-derived.
21#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
22pub struct Identity {
23    /// The name used in the PID path (e.g., `"seph"` or `"a3f7x9kp2bfhmnqr5tvwxyzabc"`).
24    pub name: String,
25    /// Canonical-JSON BLAKE3 hash, 26-char base32. Always computed, even when overridden.
26    pub content_hash: String,
27    /// True when `name` came from `spec.identity.nameOverride`.
28    pub name_override: bool,
29}
30
31/// Compute the content hash of any serializable spec.
32///
33/// The canonical pillar-bytes input rides through the ONE substrate
34/// primitive [`crate::three_pillar::pillar_bytes`] — peer of the
35/// intent-attestation-pillar consumers (`IntentVariant::canonical_bytes`,
36/// `render::render_{flux,aplicacao,nix}`, `phase_machine::
37/// compute_intent_hash`) that all route through the same
38/// `serde_json::to_vec(v).unwrap_or_default()` shape. A future
39/// upgrade of the pillar-bytes projection (a canonical-JSON
40/// serializer for stable byte ordering, a size-cap guard, a serde-
41/// error trace event before returning empty) lands at the substrate
42/// owner and every content-hash + attestation consumer inherits the
43/// upgrade mechanically.
44pub fn content_hash<T: Serialize>(spec: &T) -> String {
45    let canonical = crate::three_pillar::pillar_bytes(spec);
46    let digest = blake3::hash(&canonical);
47    base32_encode(&digest.as_bytes()[..HASH_BYTES])
48}
49
50/// Derive an identity from a spec + optional human override.
51///
52/// Override wins when non-empty; the content hash is always computed for integrity.
53pub fn derive_identity<T: Serialize>(spec: &T, name_override: Option<&str>) -> Identity {
54    let hash = content_hash(spec);
55    match name_override.map(str::trim).filter(|s| !s.is_empty()) {
56        Some(name) => Identity {
57            name: name.to_string(),
58            content_hash: hash,
59            name_override: true,
60        },
61        None => Identity {
62            name: hash.clone(),
63            content_hash: hash,
64            name_override: false,
65        },
66    }
67}
68
69/// Canonical hierarchical PID-path segment separator.
70///
71/// The ONE substrate owner of the `'.'` char every hierarchical-PID
72/// composer + walker on this file + [`crate::pid`] (via
73/// [`join_pid_segment`]) reaches through, so a future normalization of
74/// the separator (a swap to `'/'` for a Unix-path-shaped rendering, a
75/// per-segment escape for names carrying literal `.`s, a widened
76/// grapheme-boundary walker for unicode-safe splits) lands at ONE
77/// substrate primitive and every hierarchical-PID producer + consumer
78/// picks up the upgrade mechanically.
79///
80/// Pre-lift the bare `'.'` char literal was hand-authored at TWO
81/// production sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
82/// threshold on the [`crate::pid`] side alone
83/// ([`crate::pid::depth`] on `pid_path.split('.')` and
84/// [`crate::pid::parent_of`] on `pid_path.rfind('.')`), plus the THREE
85/// composer sites this module + [`crate::pid`] hand-authored on the
86/// join axis before the [`join_pid_segment`] lift below routed them
87/// through the same const.
88///
89/// Theory grounding: THEORY.md §II.1 invariant 4 (deterministic
90/// identity — hierarchical PIDs are ONE cluster-wide address space
91/// with ONE canonical separator; every producer + consumer of the
92/// address space binds through the same substrate slot).
93pub const PID_PATH_SEPARATOR: char = '.';
94
95/// Compose the canonical `<head><PID_PATH_SEPARATOR><tail>`
96/// hierarchical PID-path segment join.
97///
98/// Owns the fixed 2-slot `format!("{head}.{tail}")` shape as ONE
99/// substrate site, routing the separator through
100/// [`PID_PATH_SEPARATOR`] so a future normalization of the separator
101/// (see the const's doc for the catalog) reaches every hierarchical-
102/// PID producer through this ONE primitive.
103///
104/// Pre-lift the 2-slot join was hand-authored at THREE production
105/// sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold, split
106/// across two crates:
107/// - [`format_process_address`] on the (`identity.name`, `pid_path`)
108///   pair — the root address composer.
109/// - [`crate::pid::allocate_pid`] on the Some(parent) arm's
110///   (`parent`, `next_sequence`) pair — the child PID allocator.
111/// - [`crate::pid::allocate_pid`] on the None arm's
112///   (`identity.name`, `next_sequence`) pair — the root PID allocator.
113///
114/// Post-lift each callsite reads
115/// `join_pid_segment(<head>, <tail>)` and the composed byte shape
116/// matches the pre-lift `format!` chain verbatim. The `tail`
117/// parameter accepts any [`std::fmt::Display`]-able value so the
118/// `u32 next_sequence` slot on [`crate::pid::allocate_pid`] flows
119/// through the primitive without a pre-format `.to_string()` round-
120/// trip, matching the sibling [`crate::boundary::Satisfaction::labeled_diagnostic`]
121/// composer's `tail: impl Display` convention.
122///
123/// Extension: future hierarchical-PID producers (P3 kenshi-runner's
124/// per-suite Job PID allocator, any future placement rule that names
125/// a fresh child under an existing parent) land as ONE new callsite
126/// through this composer instead of another hand-authored `format!(
127/// "{head}.{tail}")` restatement.
128///
129/// Theory grounding: THEORY.md §VI.1 (generation over composition —
130/// the shape recurred at three sites past the PRIME-DIRECTIVE ≥ 2
131/// duplication trigger, and is lifted to ONE owner here). THEORY.md
132/// §II.1 invariant 5 (composition preserves proofs — the three
133/// callsites now compose structurally through ONE primitive; a
134/// regression that drifted the separator at ONE site surfaces at
135/// [`tests::join_pid_segment_*`] rather than as silent operator-
136/// facing skew across the hierarchical-PID address space).
137#[must_use]
138pub fn join_pid_segment(head: &str, tail: impl std::fmt::Display) -> String {
139    format!("{head}{PID_PATH_SEPARATOR}{tail}")
140}
141
142/// Format a hierarchical process address: `{identity}.{pid_path}`.
143///
144/// Examples: `"seph.1"`, `"a3f7x9kp.1.1"`, `"seph.1.7.2"`.
145///
146/// Routes through the ONE substrate composer [`join_pid_segment`] so a
147/// future normalization of the hierarchical-PID join (see the composer's
148/// doc for the catalog) reaches this address renderer mechanically.
149pub fn format_process_address(identity: &Identity, pid_path: &str) -> String {
150    join_pid_segment(&identity.name, pid_path)
151}
152
153fn base32_encode(bytes: &[u8]) -> String {
154    let mut out = String::with_capacity((bytes.len() * 8).div_ceil(5));
155    let mut bits: u64 = 0;
156    let mut n: u32 = 0;
157    for &b in bytes {
158        bits = (bits << 8) | u64::from(b);
159        n += 8;
160        while n >= 5 {
161            n -= 5;
162            out.push(BASE32_ALPHABET[((bits >> n) & 0x1f) as usize] as char);
163        }
164    }
165    if n > 0 {
166        out.push(BASE32_ALPHABET[((bits << (5 - n)) & 0x1f) as usize] as char);
167    }
168    out
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[derive(Serialize)]
176    struct Dummy {
177        a: u32,
178        b: &'static str,
179    }
180
181    #[test]
182    fn content_hash_is_deterministic() {
183        let s = Dummy { a: 1, b: "x" };
184        assert_eq!(content_hash(&s), content_hash(&s));
185    }
186
187    #[test]
188    fn content_hash_differs_for_different_input() {
189        assert_ne!(
190            content_hash(&Dummy { a: 1, b: "x" }),
191            content_hash(&Dummy { a: 2, b: "x" })
192        );
193    }
194
195    #[test]
196    fn content_hash_length_is_26() {
197        assert_eq!(content_hash(&Dummy { a: 0, b: "" }).len(), 26);
198    }
199
200    #[test]
201    fn alphabet_excludes_ambiguous() {
202        // Crockford base32 excludes i/l/o/u to eliminate visual collisions.
203        let h = content_hash(&Dummy {
204            a: u32::MAX,
205            b: "qwertyuiopasdfghjklzxcvbnm",
206        });
207        for c in h.chars() {
208            assert!(!matches!(c, 'i' | 'l' | 'o' | 'u'), "saw {c}");
209        }
210    }
211
212    #[test]
213    fn override_wins() {
214        let id = derive_identity(&Dummy { a: 1, b: "x" }, Some("seph"));
215        assert_eq!(id.name, "seph");
216        assert!(id.name_override);
217        assert_eq!(id.content_hash.len(), 26);
218    }
219
220    #[test]
221    fn empty_override_falls_back_to_hash() {
222        let id = derive_identity(&Dummy { a: 1, b: "x" }, Some("   "));
223        assert!(!id.name_override);
224        assert_eq!(id.name, id.content_hash);
225    }
226
227    #[test]
228    fn address_format() {
229        let id = Identity {
230            name: "seph".into(),
231            content_hash: "a".repeat(26),
232            name_override: true,
233        };
234        assert_eq!(format_process_address(&id, "1.7"), "seph.1.7");
235    }
236
237    // ─── PID_PATH_SEPARATOR + join_pid_segment substrate pins ─────────
238    //
239    // The [`PID_PATH_SEPARATOR`] const + [`join_pid_segment`] composer
240    // own the ONE substrate site every hierarchical-PID producer +
241    // consumer across this module + [`crate::pid`] reaches through.
242    // These pins bind both at fail-before-pass-after granularity so a
243    // regression that drifted the separator char, changed the composer's
244    // slot ordering (`{tail}{sep}{head}` typo), or dropped the routing
245    // through the const surfaces HERE rather than as silent operator-
246    // facing skew across the cluster-wide PID address space.
247
248    #[test]
249    fn pid_path_separator_is_dot() {
250        // Byte-shape pin: the separator is `'.'`. A drift to `'/'`,
251        // `':'`, or a widened grapheme separator would fail here rather
252        // than as silent skew at every hierarchical-PID splitter
253        // (`crate::pid::depth`, `crate::pid::parent_of`) + composer
254        // (`join_pid_segment`).
255        assert_eq!(PID_PATH_SEPARATOR, '.');
256    }
257
258    #[test]
259    fn join_pid_segment_composes_head_then_separator_then_tail() {
260        // Byte-shape pin: `join_pid_segment("seph", "1")` yields
261        // `"seph.1"`, matching the pre-lift `format!("{}.{}",
262        // identity.name, next_sequence)` shape at
263        // `crate::pid::allocate_pid` (None arm).
264        assert_eq!(join_pid_segment("seph", "1"), "seph.1");
265    }
266
267    #[test]
268    fn join_pid_segment_composes_pid_path_tail_verbatim() {
269        // Byte-shape pin: the `tail` slot accepts a `&str` carrying its
270        // own inner separators without escaping — matches the pre-lift
271        // `format_process_address` shape where the passed `pid_path` was
272        // already a dot-delimited chain.
273        assert_eq!(join_pid_segment("seph", "1.7"), "seph.1.7");
274        assert_eq!(join_pid_segment("seph.1", "7"), "seph.1.7");
275    }
276
277    #[test]
278    fn join_pid_segment_accepts_display_tail() {
279        // Byte-shape pin: the `tail: impl Display` slot admits a `u32`
280        // integer directly, matching the pre-lift `format!("{parent}.
281        // {next_sequence}")` shape at `crate::pid::allocate_pid`
282        // (Some(parent) arm) that inlined the `u32` slot without a
283        // `.to_string()` round-trip.
284        assert_eq!(join_pid_segment("seph.1", 7u32), "seph.1.7");
285        // Sweep the whole hierarchical-PID next-sequence axis so a
286        // regression at any single sequence value surfaces here.
287        for seq in [0u32, 1, 42, u32::MAX] {
288            assert_eq!(
289                join_pid_segment("seph.1", seq),
290                format!("seph.1.{seq}"),
291                "join must match pre-lift `format!(\"{{parent}}.{{seq}}\")` for seq={seq}"
292            );
293        }
294    }
295
296    #[test]
297    fn join_pid_segment_routes_through_pid_path_separator_const() {
298        // Cross-primitive coherence pin: the composed body's separator
299        // slot is byte-identical to the `PID_PATH_SEPARATOR` const. A
300        // regression that inlined a bare `'.'` at the composer while
301        // the const was renamed would surface HERE rather than as
302        // silent skew between the two substrate primitives.
303        let composed = join_pid_segment("seph", "1");
304        let mut chars = composed.chars();
305        assert_eq!(chars.next(), Some('s'));
306        assert_eq!(chars.next(), Some('e'));
307        assert_eq!(chars.next(), Some('p'));
308        assert_eq!(chars.next(), Some('h'));
309        assert_eq!(chars.next(), Some(PID_PATH_SEPARATOR));
310        assert_eq!(chars.next(), Some('1'));
311    }
312
313    #[test]
314    fn format_process_address_composes_through_join_pid_segment() {
315        // Post-lift parity pin: `format_process_address` routes through
316        // `join_pid_segment`, so its output for the same input is byte-
317        // identical to the composer's output for the (identity.name,
318        // pid_path) pair. A regression that inlined a bare `format!` at
319        // the address renderer while the composer was upgraded would
320        // silently split the two on the hierarchical-PID axis.
321        let id = Identity {
322            name: "seph".into(),
323            content_hash: "a".repeat(26),
324            name_override: true,
325        };
326        for pid_path in ["1", "1.7", "1.7.3"] {
327            assert_eq!(
328                format_process_address(&id, pid_path),
329                join_pid_segment(&id.name, pid_path),
330            );
331        }
332    }
333}