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}