dynamo_bench/coding/
common.rs1use anyhow::{Context, Result, bail};
5use chrono::{DateTime, Utc};
6use serde_json::{Map, Value};
7use sha2::{Digest, Sha256};
8use std::collections::HashSet;
9use std::path::{Path, PathBuf};
10
11pub const DEFAULT_TOKENIZER: &str = "deepseek-ai/DeepSeek-R1-Distill-Llama-8B";
12pub const DEFAULT_BLOCK_SIZE: usize = 64;
13pub const DEFAULT_OUTPUT_NAME: &str = "claude_request_trace.jsonl";
14pub const SIDE_CAR_TOKEN: &str = ".sidecar";
15
16pub fn parse_utc_timestamp_ms(value: &str) -> Result<i64> {
17 if value.is_empty() {
18 bail!("missing timestamp");
19 }
20 let parsed = DateTime::parse_from_rfc3339(value)
21 .with_context(|| format!("invalid RFC3339 timestamp: {value}"))?;
22 Ok(parsed.with_timezone(&Utc).timestamp_millis())
23}
24
25pub fn anonymized_session_id(session_id: &str) -> String {
26 let digest = Sha256::digest(session_id.as_bytes());
27 let mut hex = String::with_capacity(12);
28 for byte in digest.iter().take(6) {
29 hex.push_str(&format!("{byte:02x}"));
30 }
31 format!("session_{hex}")
32}
33
34pub fn sidecar_path_for(output_path: &Path) -> PathBuf {
35 match (output_path.file_stem(), output_path.extension()) {
36 (Some(stem), Some(ext)) => output_path.with_file_name(format!(
37 "{}{}{ext_sep}{}",
38 stem.to_string_lossy(),
39 SIDE_CAR_TOKEN,
40 ext.to_string_lossy(),
41 ext_sep = "."
42 )),
43 _ => output_path.with_file_name(format!(
44 "{}{}.jsonl",
45 output_path
46 .file_name()
47 .unwrap_or_default()
48 .to_string_lossy(),
49 SIDE_CAR_TOKEN
50 )),
51 }
52}
53
54pub fn dedupe_paths(paths: Vec<PathBuf>) -> Vec<PathBuf> {
55 let mut seen = HashSet::new();
56 let mut deduped = Vec::new();
57 for path in paths {
58 let resolved = path.canonicalize().unwrap_or(path);
59 if seen.insert(resolved.clone()) {
60 deduped.push(resolved);
61 }
62 }
63 deduped
64}
65
66pub fn expand_user_path(raw: &str) -> PathBuf {
67 if raw == "~" {
68 return home_dir().unwrap_or_else(|| PathBuf::from(raw));
69 }
70 if let Some(rest) = raw.strip_prefix("~/") {
71 return home_dir()
72 .map(|home| home.join(rest))
73 .unwrap_or_else(|| PathBuf::from(raw));
74 }
75 PathBuf::from(raw)
76}
77
78pub fn home_dir() -> Option<PathBuf> {
79 std::env::var_os("HOME").map(PathBuf::from)
80}
81
82pub fn canonical_json_string(value: &Value) -> Result<String> {
83 let mut rendered = String::new();
84 write_canonical_json(&mut rendered, value)?;
85 Ok(rendered)
86}
87
88fn write_canonical_json(buffer: &mut String, value: &Value) -> Result<()> {
89 match value {
90 Value::Null => buffer.push_str("null"),
91 Value::Bool(flag) => {
92 if *flag {
93 buffer.push_str("true");
94 } else {
95 buffer.push_str("false");
96 }
97 }
98 Value::Number(number) => buffer.push_str(&number.to_string()),
99 Value::String(text) => buffer.push_str(&serde_json::to_string(text)?),
100 Value::Array(items) => {
101 buffer.push('[');
102 for (index, item) in items.iter().enumerate() {
103 if index > 0 {
104 buffer.push(',');
105 }
106 write_canonical_json(buffer, item)?;
107 }
108 buffer.push(']');
109 }
110 Value::Object(map) => {
111 buffer.push('{');
112 let mut keys: Vec<&String> = map.keys().collect();
113 keys.sort_unstable();
114 for (index, key) in keys.into_iter().enumerate() {
115 if index > 0 {
116 buffer.push(',');
117 }
118 buffer.push_str(&serde_json::to_string(key)?);
119 buffer.push(':');
120 write_canonical_json(buffer, &map[key])?;
121 }
122 buffer.push('}');
123 }
124 }
125 Ok(())
126}
127
128pub fn content_blocks(content: Option<&Value>) -> Vec<Value> {
129 match content {
130 Some(Value::String(text)) => {
131 vec![Value::Object(
132 [
133 ("type".to_string(), Value::String("text".to_string())),
134 ("text".to_string(), Value::String(text.clone())),
135 ]
136 .into_iter()
137 .collect(),
138 )]
139 }
140 Some(Value::Array(items)) => items
141 .iter()
142 .filter(|item| item.is_object())
143 .cloned()
144 .collect(),
145 _ => Vec::new(),
146 }
147}
148
149pub fn flatten_block_content_text(value: &Value) -> Result<String> {
150 match value {
151 Value::String(text) => Ok(text.clone()),
152 Value::Array(items) => {
153 let mut parts = Vec::new();
154 for item in items {
155 match item {
156 Value::String(text) => parts.push(text.clone()),
157 Value::Object(map) => {
158 if let Some(text) = map.get("text").and_then(Value::as_str) {
159 parts.push(text.to_string());
160 } else {
161 parts.push(canonical_json_string(item)?);
162 }
163 }
164 _ => parts.push(canonical_json_string(item)?),
165 }
166 }
167 Ok(parts
168 .into_iter()
169 .filter(|part| !part.is_empty())
170 .collect::<Vec<_>>()
171 .join("\n"))
172 }
173 Value::Object(map) => {
174 if let Some(text) = map.get("text").and_then(Value::as_str) {
175 return Ok(text.to_string());
176 }
177 canonical_json_string(value)
178 }
179 _ => canonical_json_string(value),
180 }
181}
182
183pub fn object_field<'a>(value: &'a Value, field: &str) -> Option<&'a Map<String, Value>> {
184 value.get(field)?.as_object()
185}