1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
|
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::<usize>::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}"#);
}
}
|