use sha2::{Digest, Sha256}; pub fn sha256_hex(data: &[u8]) -> String { let mut h = Sha256::new(); h.update(data); hex::encode(h.finalize()) } /// Synchronous UTC timestamp helper, ISO 8601 with millisecond precision. pub fn now_iso() -> String { chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true) } pub fn plus_hours(hours: i64) -> String { (chrono::Utc::now() + chrono::Duration::hours(hours)) .to_rfc3339_opts(chrono::SecondsFormat::Millis, true) } pub fn new_id() -> String { uuid::Uuid::new_v4().to_string() } pub fn gen_token() -> String { use rand::RngCore; let mut buf = [0u8; 32]; rand::rngs::OsRng.fill_bytes(&mut buf); hex::encode(buf) } /// Line based unified diff (full document, no hunks) used by the AI draft diff. pub fn unified_diff(a: &str, b: &str) -> String { let x: Vec<&str> = a.lines().collect(); let y: Vec<&str> = b.lines().collect(); let n = x.len(); let m = y.len(); // LCS table let mut dp = vec![vec![0usize; m + 1]; n + 1]; for i in (0..n).rev() { for j in (0..m).rev() { dp[i][j] = if x[i] == y[j] { dp[i + 1][j + 1] + 1 } else { dp[i + 1][j].max(dp[i][j + 1]) }; } } let mut out = String::new(); let mut ctx = std::collections::VecDeque::::new(); let (mut i, mut j) = (0usize, 0usize); while i < n || j < m { if i < n && j < m && x[i] == y[j] { ctx.push_back(i); if ctx.len() > 2 { let keep = ctx.pop_front().unwrap(); out.push_str(&format!(" {}\n", x[keep])); } i += 1; j += 1; } else { // entering a change: emit queued context lines first for k in ctx.drain(..) { out.push_str(&format!(" {}\n", x[k])); } if i < n && (j >= m || dp[i + 1][j] >= dp[i][j + 1]) { out.push_str(&format!("-{}\n", x[i])); i += 1; } else { out.push_str(&format!("+{}\n", y[j])); j += 1; } } } out } /// Deterministic canonical JSON string (recursively sorted keys) for hashing. pub fn canonical_json(v: &serde_json::Value) -> String { fn sort(v: &serde_json::Value) -> serde_json::Value { match v { serde_json::Value::Array(a) => serde_json::Value::Array(a.iter().map(sort).collect()), serde_json::Value::Object(o) => { let mut keys: Vec<(String, serde_json::Value)> = o.iter().map(|(k, v)| (k.clone(), sort(v))).collect(); keys.sort_by(|a, b| a.0.cmp(&b.0)); serde_json::Value::Object(keys.into_iter().collect()) } other => other.clone(), } } sort(v).to_string() } #[cfg(test)] mod tests { use super::*; #[test] fn now_iso_is_synchronous_iso8601() { let t = now_iso(); let parsed = chrono::DateTime::parse_from_rfc3339(&t).expect("ISO timestamp"); assert!(parsed.timestamp() > 1_700_000_000); assert!(t.ends_with('Z')); } #[test] fn plus_hours_and_tokens() { let e = chrono::DateTime::parse_from_rfc3339(&plus_hours(1)).unwrap(); let n = chrono::DateTime::parse_from_rfc3339(&now_iso()).unwrap(); assert!((e - n).num_minutes() >= 59); let a = gen_token(); assert_eq!(a.len(), 64); assert_ne!(a, gen_token()); assert_eq!(new_id().len(), 36); } #[test] fn sha256_is_stable() { assert_eq!(sha256_hex(b"abc"), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); } #[test] fn unified_diff_marks_changes_with_context() { let a = "x\nkeep\ny"; let b = "x\nkeep\nz"; let d = unified_diff(a, b); assert!(d.contains("-y\n+z\n"), "diff: {d}"); assert!(d.contains(" keep")); let same = unified_diff(a, "x\nkeep\ny"); assert!(!same.contains('+') && !same.contains('-'), "no diff: {same}"); } #[test] fn canonical_json_sorts_recursively() { let v: serde_json::Value = serde_json::from_str(r#"{"b":1,"a":{"y":[3,2],"x":true}}"#).unwrap(); assert_eq!(canonical_json(&v), r#"{"a":{"x":true,"y":[3,2]},"b":1}"#); } }