use std::collections::{BTreeMap, HashMap};
use std::fs;
use std::path::Path;
use regex::Regex;
use serde_json::Value;
use super::formatting::{number_f64, sequence_tag, short_codegraph_name};
use super::parser::parse_run_file;
use super::types::{BenchRun, ParsedRun, RepoMeta, RunMetrics, SeqMetrics, ToolPayload};
pub(super) fn run_metrics(parsed: &ParsedRun) -> RunMetrics {
let codegraph = parsed
.tool_calls
.iter()
.filter(|call| call.name.contains("codegraph"))
.collect::<Vec<_>>();
RunMetrics {
init_codegraph_tools: parsed.init_codegraph_tools.unwrap_or(0),
reads: parsed
.tool_calls
.iter()
.filter(|call| call.name == "Read")
.count(),
greps: parsed
.tool_calls
.iter()
.filter(|call| call.name == "Grep" || call.name == "Glob")
.count(),
codegraph_calls: codegraph.len(),
codegraph_sequence: codegraph
.iter()
.map(|call| short_codegraph_name(&call.name))
.collect(),
codegraph_output: codegraph.iter().map(|call| call.out_len).sum(),
trace_used: codegraph.iter().any(|call| call.name.contains("trace")),
turns: parsed
.result
.as_ref()
.and_then(|result| number_f64(result.get("num_turns"))),
duration_seconds: parsed
.result
.as_ref()
.and_then(|result| number_f64(result.get("duration_ms")))
.map(|value| (value / 1000.0).round()),
cost: parsed
.result
.as_ref()
.and_then(|result| number_f64(result.get("total_cost_usd")))
.unwrap_or(0.0),
ok: parsed
.result
.as_ref()
.and_then(|result| result.get("subtype"))
.and_then(Value::as_str)
== Some("success"),
}
}
pub(super) fn seq_metrics(parsed: &ParsedRun) -> SeqMetrics {
let base = run_metrics(parsed);
let mut per_tool = BTreeMap::new();
let mut trace_index = None;
for (codegraph_index, call) in parsed
.tool_calls
.iter()
.filter(|call| call.name.contains("codegraph"))
.enumerate()
{
if trace_index.is_none() && call.name.contains("trace") {
trace_index = Some(codegraph_index);
}
let name = short_codegraph_name(&call.name);
let entry: &mut ToolPayload = per_tool.entry(name).or_default();
entry.n += 1;
entry.out += call.out_len;
}
let after_trace = trace_index.map(|index| {
base.codegraph_sequence
.iter()
.skip(index + 1)
.cloned()
.collect()
});
SeqMetrics {
base,
per_tool,
sequence: parsed
.tool_calls
.iter()
.map(|call| sequence_tag(&call.name))
.collect(),
after_trace,
}
}
pub(super) fn bench_run_metrics(file: &Path) -> Result<Option<BenchRun>, String> {
if !file.exists() {
return Ok(None);
}
let parsed = parse_run_file(file)?;
if parsed
.result
.as_ref()
.and_then(|result| result.get("subtype"))
.and_then(Value::as_str)
!= Some("success")
{
return Ok(None);
}
let tools = parsed
.tool_calls
.iter()
.filter(|call| call.name != "ToolSearch")
.count();
let result = parsed.result.as_ref().expect("result checked above");
Ok(Some(BenchRun {
duration: number_f64(result.get("duration_ms")).unwrap_or(0.0) / 1000.0,
tools,
tokens: parsed.assistant_total_tokens,
cost: number_f64(result.get("total_cost_usd")).unwrap_or(0.0),
raced: parsed.raced,
}))
}
pub(super) fn read_repo_meta(file: &Path) -> Result<HashMap<String, RepoMeta>, String> {
let mut meta = HashMap::new();
if !file.exists() {
return Ok(meta);
}
let line_re = Regex::new(r"^\|\s*([^|]+?)\s*\|\s*(S|M|L)\s*\|\s*`([^`]+)`\s*\|\s*(\d+)\s*\|")
.expect("matrix metadata regex should compile");
for line in fs::read_to_string(file)
.map_err(|err| format!("failed to read {}: {err}", file.display()))?
.lines()
{
if let Some(captures) = line_re.captures(line) {
meta.insert(
captures[3].to_string(),
RepoMeta {
files: captures[4].parse().unwrap_or(0),
},
);
}
}
Ok(meta)
}