Skip to main content

macula_rust/
petname.rs

1//! Petnames: a deterministic, human-readable label for a mesh node id —
2//! Docker's adjective_color_animal convention with a four-digit suffix
3//! (e.g. "happy_green_rabbit_4831") — so a person skimming a roster,
4//! transcript, or room listing can recognize and remember a specific
5//! identity without reading 64 hex characters. A pure function of the
6//! node id itself, not random per process: the same identity gets the
7//! same petname across restarts, across every tool that shows it, and
8//! on every other agent's own roster too (everyone hashes the same
9//! public bytes). This is a companion label, never a replacement —
10//! every surface that adds one keeps the real node_id right alongside
11//! it, since only the real id is addressable.
12//!
13//! The suffix exists because the mesh is expected to host THOUSANDS of
14//! agents: 40 x 40 x 40 word trios alone collide visibly under the
15//! birthday problem at a few hundred identities, while the trio plus a
16//! 4-digit hash group (640,000,000 combinations) stays effectively
17//! collision-free at fleet scale while remaining scannable.
18//!
19//! The word lists and derivation are shared with macula-mcp's own
20//! `src/petname.ts` (which this supersedes as the SDK-level home; the
21//! desktop and every future consumer pull it from here): same sha256 of
22//! the lowercased hex id, same 16-bit reads modulo the list lengths,
23//! plus the suffix group.
24
25use sha2::{Digest, Sha256};
26
27const ADJECTIVES_A: [&str; 40] = [
28    "bold", "bouncy", "brave", "breezy", "calm", "cheerful", "clever", "curious", "daring",
29    "eager", "elegant", "fierce", "gentle", "graceful", "humble", "jolly", "jovial", "keen",
30    "kind", "lively", "lucky", "mellow", "merry", "nimble", "noble", "plucky", "proud", "quiet",
31    "quirky", "radiant", "silly", "sleepy", "spry", "sturdy", "tranquil", "upbeat", "vivid",
32    "wise", "witty", "zealous",
33];
34
35const ADJECTIVES_B: [&str; 40] = [
36    "amber",
37    "azure",
38    "bronze",
39    "coral",
40    "crimson",
41    "cyan",
42    "emerald",
43    "golden",
44    "green",
45    "indigo",
46    "ivory",
47    "jade",
48    "lavender",
49    "lilac",
50    "magenta",
51    "maroon",
52    "mauve",
53    "navy",
54    "olive",
55    "orange",
56    "peach",
57    "pink",
58    "plum",
59    "purple",
60    "red",
61    "rust",
62    "ruby",
63    "sage",
64    "salmon",
65    "scarlet",
66    "sienna",
67    "silver",
68    "slate",
69    "tan",
70    "teal",
71    "turquoise",
72    "violet",
73    "yellow",
74    "blue",
75    "copper",
76];
77
78const NOUNS: [&str; 40] = [
79    "antelope",
80    "badger",
81    "beetle",
82    "bison",
83    "cricket",
84    "dolphin",
85    "eagle",
86    "elk",
87    "falcon",
88    "ferret",
89    "flamingo",
90    "fox",
91    "gazelle",
92    "gecko",
93    "hare",
94    "heron",
95    "ibex",
96    "iguana",
97    "lynx",
98    "marten",
99    "mongoose",
100    "moose",
101    "narwhal",
102    "orca",
103    "otter",
104    "owl",
105    "panther",
106    "pelican",
107    "penguin",
108    "rabbit",
109    "raven",
110    "salamander",
111    "seal",
112    "sparrow",
113    "tiger",
114    "toucan",
115    "walrus",
116    "weasel",
117    "wolf",
118    "wombat",
119];
120
121/// The stable "adjective_color_animal_0000" label for a node id, e.g.
122/// "happy_green_rabbit_4831". Same input always produces the same
123/// output — derived from a sha256 digest of the id (lowercased first,
124/// so a node id that happens to arrive in mixed case still maps to the
125/// same petname as its lowercase form), not from anything process-local
126/// like a random seed or insertion order.
127pub fn petname(node_id: &str) -> String {
128    let digest = Sha256::digest(node_id.to_ascii_lowercase().as_bytes());
129    let a = ADJECTIVES_A[u16::from_be_bytes([digest[0], digest[1]]) as usize % ADJECTIVES_A.len()];
130    let b = ADJECTIVES_B[u16::from_be_bytes([digest[2], digest[3]]) as usize % ADJECTIVES_B.len()];
131    let n = NOUNS[u16::from_be_bytes([digest[4], digest[5]]) as usize % NOUNS.len()];
132    let suffix = u16::from_be_bytes([digest[6], digest[7]]) % 10_000;
133    format!("{a}_{b}_{n}_{suffix:04}")
134}
135
136#[cfg(test)]
137mod tests {
138    use super::petname;
139
140    #[test]
141    fn petname_is_deterministic_and_shaped() {
142        let id = "7374b0cfab4eea68e271c3815a0f78e21e913397f67345f337ddba7a3a88ab3a";
143        let first = petname(id);
144        assert_eq!(petname(id), first);
145        let parts: Vec<&str> = first.split('_').collect();
146        assert_eq!(parts.len(), 4, "adjective_color_animal_suffix");
147        assert_eq!(parts[3].len(), 4, "zero-padded four-digit suffix");
148        assert!(parts[3].chars().all(|c| c.is_ascii_digit()));
149    }
150}