use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileOp {
Write,
Edit,
NotebookEdit,
MultiEdit,
BashMutation,
ExternalWrite,
}
impl FileOp {
#[must_use]
pub fn label(self) -> &'static str {
match self {
FileOp::Write => "write",
FileOp::Edit => "edit",
FileOp::NotebookEdit => "notebook-edit",
FileOp::MultiEdit => "multi-edit",
FileOp::BashMutation => "bash",
FileOp::ExternalWrite => "external write",
}
}
#[must_use]
pub fn json_key(self) -> &'static str {
match self {
FileOp::Write => "write",
FileOp::Edit => "edit",
FileOp::NotebookEdit => "notebook_edit",
FileOp::MultiEdit => "multi_edit",
FileOp::BashMutation => "bash",
FileOp::ExternalWrite => "external_write",
}
}
#[must_use]
pub fn is_heuristic(self) -> bool {
matches!(self, FileOp::BashMutation)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileMutation {
pub path: String,
pub op: FileOp,
pub timestamp_utc: Option<String>,
pub is_create: bool,
pub path_verbatim: Option<String>,
pub resolution: Option<&'static str>,
pub command_errored: bool,
pub detail: Option<String>,
}
impl Record {
#[must_use]
pub fn structured_tool_mutations(&self) -> Vec<FileMutation> {
let Some(blocks) = self.blocks() else {
return Vec::new();
};
let self_is_create = self
.tur_probe()
.as_ref()
.and_then(|p| p.r#type.as_ref())
.and_then(serde_json::Value::as_str)
== Some("create");
let mut out = Vec::new();
for block in blocks {
let Block::ToolUse { name, input, .. } = block else {
continue;
};
let Some(name) = name.as_deref() else {
continue;
};
let (op, key) = match name {
"Write" => (FileOp::Write, "file_path"),
"Edit" => (FileOp::Edit, "file_path"),
"MultiEdit" => (FileOp::MultiEdit, "file_path"),
"NotebookEdit" => (FileOp::NotebookEdit, "notebook_path"),
_ => continue,
};
let path = input
.as_ref()
.and_then(|v| v.get(key))
.and_then(serde_json::Value::as_str)
.unwrap_or_default();
if path.is_empty() {
continue; }
out.push(FileMutation {
path: path.to_string(),
op,
timestamp_utc: self.timestamp.clone(),
is_create: self_is_create,
path_verbatim: None,
resolution: None,
detail: None,
command_errored: false,
});
}
out
}
#[must_use]
pub fn carrier_create_paths(&self) -> Vec<(String, String, bool)> {
let Some(probe) = self.tur_probe() else {
return Vec::new();
};
let Some(file_path) = probe.file_path.as_ref().and_then(serde_json::Value::as_str) else {
return Vec::new();
};
if file_path.is_empty() {
return Vec::new();
}
let is_create = probe.r#type.as_ref().and_then(serde_json::Value::as_str) == Some("create");
let mut out = Vec::new();
if let Some(blocks) = self.blocks() {
for block in blocks {
if let Block::ToolResult {
tool_use_id: Some(id),
..
} = block
{
out.push((id.clone(), file_path.to_string(), is_create));
}
}
}
out
}
#[must_use]
pub fn bash_command(&self) -> Option<&str> {
let blocks = self.blocks()?;
for block in blocks {
if let Block::ToolUse { name, input, .. } = block {
if name.as_deref() == Some("Bash") {
if let Some(cmd) = input
.as_ref()
.and_then(|v| v.get("command"))
.and_then(serde_json::Value::as_str)
{
return Some(cmd);
}
}
}
}
None
}
}