#![allow(dead_code)]
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::PathBuf;
use crate::brain::client::BrainSuggestion;
#[derive(Debug, Clone)]
pub struct DecisionRecord {
pub timestamp: String,
pub pid: u32,
pub project: String,
pub tool: Option<String>,
pub command: Option<String>,
pub brain_action: String,
pub brain_confidence: f64,
pub brain_reasoning: String,
pub user_action: String, }
fn decisions_dir() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into());
PathBuf::from(home).join(".claudectl").join("brain")
}
fn decisions_path() -> PathBuf {
decisions_dir().join("decisions.jsonl")
}
pub fn log_decision(
pid: u32,
project: &str,
tool: Option<&str>,
command: Option<&str>,
suggestion: &BrainSuggestion,
user_action: &str,
) {
let record = serde_json::json!({
"ts": timestamp_now(),
"pid": pid,
"project": project,
"tool": tool,
"command": command,
"brain_action": suggestion.action.label(),
"brain_confidence": suggestion.confidence,
"brain_reasoning": suggestion.reasoning,
"user_action": user_action,
});
let path = decisions_path();
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent);
}
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&path) {
let _ = writeln!(
file,
"{}",
serde_json::to_string(&record).unwrap_or_default()
);
}
}
pub fn read_stats() -> DecisionStats {
let path = decisions_path();
let content = match fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => return DecisionStats::default(),
};
let mut total = 0u32;
let mut accepted = 0u32;
let mut rejected = 0u32;
let mut auto_executed = 0u32;
for line in content.lines() {
let Ok(json) = serde_json::from_str::<serde_json::Value>(line) else {
continue;
};
total += 1;
match json.get("user_action").and_then(|v| v.as_str()) {
Some("accept") => accepted += 1,
Some("reject") => rejected += 1,
Some("auto") => auto_executed += 1,
_ => {}
}
}
DecisionStats {
total,
accepted,
rejected,
auto_executed,
}
}
pub fn forget() -> Result<(), String> {
let path = decisions_path();
if path.exists() {
fs::remove_file(&path).map_err(|e| format!("failed to delete {}: {e}", path.display()))?;
}
Ok(())
}
#[derive(Debug, Default)]
pub struct DecisionStats {
pub total: u32,
pub accepted: u32,
pub rejected: u32,
pub auto_executed: u32,
}
impl DecisionStats {
pub fn accuracy_pct(&self) -> f64 {
let decided = self.accepted + self.rejected;
if decided == 0 {
return 0.0;
}
(self.accepted as f64 / decided as f64) * 100.0
}
}
fn timestamp_now() -> String {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
format!("{secs}")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rules::RuleAction;
fn make_suggestion() -> BrainSuggestion {
BrainSuggestion {
action: RuleAction::Approve,
message: None,
reasoning: "safe command".into(),
confidence: 0.95,
}
}
#[test]
fn log_and_read_decisions() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("decisions.jsonl");
let record = serde_json::json!({
"user_action": "accept",
"brain_action": "approve",
});
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.unwrap();
writeln!(file, "{}", serde_json::to_string(&record).unwrap()).unwrap();
let record2 = serde_json::json!({
"user_action": "reject",
"brain_action": "approve",
});
writeln!(file, "{}", serde_json::to_string(&record2).unwrap()).unwrap();
drop(file);
let content = fs::read_to_string(&path).unwrap();
let mut accepted = 0;
let mut rejected = 0;
for line in content.lines() {
let json: serde_json::Value = serde_json::from_str(line).unwrap();
match json["user_action"].as_str() {
Some("accept") => accepted += 1,
Some("reject") => rejected += 1,
_ => {}
}
}
assert_eq!(accepted, 1);
assert_eq!(rejected, 1);
}
#[test]
fn stats_accuracy() {
let stats = DecisionStats {
total: 10,
accepted: 8,
rejected: 2,
auto_executed: 0,
};
assert!((stats.accuracy_pct() - 80.0).abs() < f64::EPSILON);
}
#[test]
fn stats_accuracy_no_decisions() {
let stats = DecisionStats::default();
assert!((stats.accuracy_pct() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn suggestion_label_used() {
let s = make_suggestion();
assert_eq!(s.action.label(), "approve");
}
}