1use sha2::{Digest, Sha256};
8
9const ID_HEX_LEN: usize = 32;
11
12#[derive(Debug, Clone, Copy, Default)]
15pub struct EventIdentity<'a> {
16 pub agent: &'a str,
17 pub session_id: Option<&'a str>,
18 pub source_id: Option<&'a str>,
20 pub ts: i64,
21 pub role: &'a str,
22 pub extra: Option<&'a str>,
24}
25
26pub fn event_id(identity: EventIdentity<'_>) -> String {
28 let mut hasher = Sha256::new();
29 write_field(&mut hasher, identity.agent.as_bytes());
30 write_field(&mut hasher, identity.session_id.unwrap_or("").as_bytes());
31 write_field(&mut hasher, identity.source_id.unwrap_or("").as_bytes());
32 write_field(&mut hasher, &identity.ts.to_be_bytes());
33 write_field(&mut hasher, identity.role.as_bytes());
34 write_field(&mut hasher, identity.extra.unwrap_or("").as_bytes());
35 hex(&hasher.finalize())[..ID_HEX_LEN].to_string()
36}
37
38pub fn text_hash(text: &str) -> String {
40 let mut hasher = Sha256::new();
41 hasher.update(text.as_bytes());
42 hex(&hasher.finalize())[..ID_HEX_LEN].to_string()
43}
44
45fn write_field(hasher: &mut Sha256, bytes: &[u8]) {
48 hasher.update((bytes.len() as u64).to_be_bytes());
49 hasher.update(bytes);
50}
51
52fn hex(bytes: &[u8]) -> String {
53 let mut out = String::with_capacity(bytes.len() * 2);
54 for byte in bytes {
55 use std::fmt::Write as _;
56 let _ = write!(out, "{byte:02x}");
57 }
58 out
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 fn identity() -> EventIdentity<'static> {
66 EventIdentity {
67 agent: "claude-code",
68 session_id: Some("sess-1"),
69 source_id: Some("uuid-1"),
70 ts: 1_754_300_000_000,
71 role: "assistant",
72 extra: None,
73 }
74 }
75
76 #[test]
77 fn same_content_same_id() {
78 assert_eq!(event_id(identity()), event_id(identity()));
79 }
80
81 #[test]
82 fn id_is_stable_across_runs() {
83 assert_eq!(event_id(identity()), "6cbc1e2d028a0bc4fee7ae15c0405df6");
85 }
86
87 #[test]
88 fn different_content_different_id() {
89 let base = event_id(identity());
90 let mut other = identity();
91 other.ts += 1;
92 assert_ne!(event_id(other), base);
93
94 let mut other = identity();
95 other.role = "user";
96 assert_ne!(event_id(other), base);
97
98 let mut other = identity();
99 other.extra = Some("1");
100 assert_ne!(event_id(other), base);
101 }
102
103 #[test]
104 fn field_boundaries_are_unambiguous() {
105 let a = EventIdentity {
106 agent: "ab",
107 session_id: Some("c"),
108 ..EventIdentity::default()
109 };
110 let b = EventIdentity {
111 agent: "a",
112 session_id: Some("bc"),
113 ..EventIdentity::default()
114 };
115 assert_ne!(event_id(a), event_id(b));
116 }
117
118 #[test]
119 fn text_hash_is_deterministic() {
120 assert_eq!(text_hash("run the tests"), text_hash("run the tests"));
121 assert_ne!(text_hash("run the tests"), text_hash("run the test"));
122 assert_eq!(text_hash("").len(), ID_HEX_LEN);
123 }
124}