#![allow(dead_code)]
use std::collections::HashMap;
use super::decisions::{DecisionRecord, read_all_decisions};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RiskTier {
Low,
Medium,
High,
Critical,
}
impl RiskTier {
pub fn label(&self) -> &'static str {
match self {
RiskTier::Low => "low",
RiskTier::Medium => "medium",
RiskTier::High => "high",
RiskTier::Critical => "critical",
}
}
}
impl std::fmt::Display for RiskTier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.label())
}
}
pub fn classify_risk(tool: Option<&str>, command: Option<&str>) -> RiskTier {
let tool = tool.unwrap_or("");
let cmd = command.unwrap_or("").to_lowercase();
const CRITICAL_PATTERNS: &[&str] = &[
"rm -rf",
"rm -fr",
"git push --force",
"git push -f",
"git reset --hard",
"drop table",
"drop database",
"truncate table",
"kubectl delete",
"docker rm",
"format c:",
"> /dev/",
":(){ :|:& };:",
"chmod -r 777",
"chmod 777",
"--no-verify",
];
for pat in CRITICAL_PATTERNS {
if cmd.contains(pat) {
return RiskTier::Critical;
}
}
match tool {
"Read" | "Glob" | "Grep" | "LS" | "Explore" => RiskTier::Low,
"Edit" | "Write" | "NotebookEdit" => {
if cmd.contains("config")
|| cmd.contains(".env")
|| cmd.contains("deploy")
|| cmd.contains("production")
|| cmd.contains("Dockerfile")
|| cmd.contains("ci.yml")
|| cmd.contains("ci.yaml")
{
RiskTier::High
} else {
RiskTier::Medium
}
}
"Bash" => {
const HIGH_RISK_BASH: &[&str] = &[
"git push",
"git merge",
"git rebase",
"npm publish",
"cargo publish",
"pip install",
"npm install -g",
"brew install",
"sudo ",
"curl ",
"wget ",
];
for pat in HIGH_RISK_BASH {
if cmd.contains(pat) {
return RiskTier::High;
}
}
const SAFE_BASH: &[&str] = &[
"cargo test",
"cargo build",
"cargo check",
"cargo clippy",
"cargo fmt",
"npm test",
"npm run",
"pytest",
"go test",
"make test",
"ls",
"pwd",
"cat ",
"head ",
"tail ",
"wc ",
"git status",
"git log",
"git diff",
"git branch",
"echo ",
];
for pat in SAFE_BASH {
if cmd.starts_with(pat) || cmd.contains(pat) {
return RiskTier::Low;
}
}
RiskTier::Medium
}
_ => RiskTier::Medium,
}
}
#[derive(Debug, Clone)]
pub struct CurvePoint {
pub index: usize,
pub correction_rate: f64,
pub window_size: usize,
}
fn rolling_correction_rate(decisions: &[DecisionRecord], window: usize) -> Vec<CurvePoint> {
if decisions.len() < window {
return Vec::new();
}
let mut points = Vec::new();
for i in window..=decisions.len() {
let window_slice = &decisions[i - window..i];
let corrections = window_slice.iter().filter(|d| d.is_negative()).count();
let rate = corrections as f64 / window as f64;
points.push(CurvePoint {
index: i,
correction_rate: rate,
window_size: window,
});
}
points
}
pub fn print_learning_curve() {
let decisions = read_all_decisions();
let total = decisions.len();
println!("Brain Learning Curve");
println!("====================");
println!();
if total < 10 {
println!(" Not enough decisions yet ({total}). Need at least 10.");
println!(" Use claudectl with --brain and accept/reject suggestions to build history.");
return;
}
let window = if total < 50 { 10 } else { 50.min(total / 5) };
let points = rolling_correction_rate(&decisions, window);
if points.is_empty() {
println!(" Not enough decisions for window size {window}.");
return;
}
println!(" Total decisions: {total}");
println!(" Window size: {window}");
println!();
println!(" Correction rate over time (lower = brain is learning):");
println!();
let step = (points.len() / 20).max(1);
let sampled: Vec<&CurvePoint> = points.iter().step_by(step).collect();
let max_rate = sampled
.iter()
.map(|p| p.correction_rate)
.fold(0.0f64, f64::max)
.max(0.01);
for point in &sampled {
let bar_len = ((point.correction_rate / max_rate) * 40.0) as usize;
let bar: String = "#".repeat(bar_len);
println!(
" {:>5} | {:<40} {:.0}%",
point.index,
bar,
point.correction_rate * 100.0,
);
}
println!();
let first_rate = points.first().map(|p| p.correction_rate).unwrap_or(0.0);
let last_rate = points.last().map(|p| p.correction_rate).unwrap_or(0.0);
let delta = first_rate - last_rate;
println!(" Early correction rate: {:.1}%", first_rate * 100.0);
println!(" Current correction rate: {:.1}%", last_rate * 100.0);
if delta > 0.05 {
println!(
" Improvement: {:.1}pp (brain is learning)",
delta * 100.0
);
} else if delta < -0.05 {
println!(
" Regression: {:.1}pp (accuracy declining)",
delta.abs() * 100.0
);
} else {
println!(
" Stable: {:.1}pp change",
delta.abs() * 100.0
);
}
println!();
println!(" Phase transitions:");
let mut prev_rate = first_rate;
for point in points.iter().skip(window) {
let change = (point.correction_rate - prev_rate).abs();
if change > 0.15 {
let direction = if point.correction_rate < prev_rate {
"improved"
} else {
"regressed"
};
println!(
" Decision ~{}: {direction} by {:.0}pp",
point.index,
change * 100.0,
);
}
prev_rate = point.correction_rate;
}
}
#[derive(Debug, Clone)]
pub struct CategoryAccuracy {
pub name: String,
pub total: u32,
pub correct: u32,
pub rejected: u32,
}
impl CategoryAccuracy {
fn accuracy_pct(&self) -> f64 {
let decided = self.correct + self.rejected;
if decided == 0 {
return 0.0;
}
(self.correct as f64 / decided as f64) * 100.0
}
}
pub fn print_accuracy() {
let decisions = read_all_decisions();
let total = decisions.len();
println!("Brain Accuracy Breakdown");
println!("========================");
println!();
if total < 5 {
println!(" Not enough decisions yet ({total}). Need at least 5.");
return;
}
let mut by_tool: HashMap<String, CategoryAccuracy> = HashMap::new();
let mut by_risk: HashMap<String, CategoryAccuracy> = HashMap::new();
let mut by_project: HashMap<String, CategoryAccuracy> = HashMap::new();
for d in &decisions {
let tool = d.tool.clone().unwrap_or_else(|| "unknown".into());
let risk = classify_risk(d.tool.as_deref(), d.command.as_deref());
let project = d.project.clone();
let keys_and_maps: Vec<(String, &mut HashMap<String, CategoryAccuracy>)> = vec![
(tool, &mut by_tool),
(risk.label().to_string(), &mut by_risk),
(project, &mut by_project),
];
for (key, map) in keys_and_maps {
let entry = map.entry(key.clone()).or_insert_with(|| CategoryAccuracy {
name: key,
total: 0,
correct: 0,
rejected: 0,
});
entry.total += 1;
if d.is_positive() {
entry.correct += 1;
} else if d.is_negative() {
entry.rejected += 1;
}
}
}
println!(" By tool:");
print_accuracy_table(&mut by_tool.into_values().collect());
println!();
println!(" By risk tier:");
print_accuracy_table(&mut by_risk.into_values().collect());
println!();
println!(" By project:");
let mut project_list: Vec<CategoryAccuracy> = by_project.into_values().collect();
project_list.sort_by_key(|p| std::cmp::Reverse(p.total));
project_list.truncate(10);
print_accuracy_table(&mut project_list);
println!();
println!(" By phase:");
print_temporal_accuracy(&decisions);
}
fn print_accuracy_table(entries: &mut Vec<CategoryAccuracy>) {
entries.sort_by_key(|e| std::cmp::Reverse(e.total));
println!(
" {:<20} {:>6} {:>8} {:>8} {:>8}",
"Category", "Total", "Correct", "Rejected", "Accuracy"
);
println!(" {}", "-".repeat(54));
for entry in entries {
let decided = entry.correct + entry.rejected;
if decided == 0 {
println!(
" {:<20} {:>6} {:>8} {:>8} {:>7}",
entry.name, entry.total, "-", "-", "n/a"
);
} else {
println!(
" {:<20} {:>6} {:>8} {:>8} {:>7.1}%",
entry.name,
entry.total,
entry.correct,
entry.rejected,
entry.accuracy_pct(),
);
}
}
}
fn print_temporal_accuracy(decisions: &[DecisionRecord]) {
let total = decisions.len();
let phases: Vec<(&str, usize, usize)> = if total >= 500 {
vec![
("early (0-100)", 0, 100),
("mid (100-500)", 100, 500),
("late (500+)", 500, total),
]
} else if total >= 100 {
let mid = total / 2;
vec![("early", 0, mid), ("late", mid, total)]
} else {
vec![("all", 0, total)]
};
println!(
" {:<20} {:>6} {:>8} {:>8} {:>8}",
"Phase", "Total", "Correct", "Rejected", "Accuracy"
);
println!(" {}", "-".repeat(54));
for (label, start, end) in phases {
let slice = &decisions[start..end];
let correct = slice.iter().filter(|d| d.is_positive()).count() as u32;
let rejected = slice.iter().filter(|d| d.is_negative()).count() as u32;
let decided = correct + rejected;
let accuracy = if decided > 0 {
(correct as f64 / decided as f64) * 100.0
} else {
0.0
};
println!(
" {:<20} {:>6} {:>8} {:>8} {:>7.1}%",
label,
slice.len(),
correct,
rejected,
accuracy,
);
}
}
fn rules_baseline_classify(tool: Option<&str>, command: Option<&str>) -> &'static str {
let tool = tool.unwrap_or("");
let cmd = command.unwrap_or("").to_lowercase();
if matches!(tool, "Read" | "Glob" | "Grep" | "LS" | "Explore") {
return "approve";
}
const DENY_PATTERNS: &[&str] = &[
"rm -rf",
"rm -fr",
"git push --force",
"git push -f",
"git reset --hard",
"drop table",
"drop database",
"--no-verify",
"chmod 777",
];
for pat in DENY_PATTERNS {
if cmd.contains(pat) {
return "deny";
}
}
if tool == "Bash" {
const SAFE_CMDS: &[&str] = &[
"cargo test",
"cargo build",
"cargo check",
"cargo clippy",
"cargo fmt",
"npm test",
"npm run",
"pytest",
"go test",
"make",
"git status",
"git log",
"git diff",
"git branch",
"ls",
"pwd",
"echo",
"cat ",
"head ",
"tail ",
];
for pat in SAFE_CMDS {
if cmd.starts_with(pat) || cmd.contains(pat) {
return "approve";
}
}
}
if matches!(tool, "Edit" | "Write") {
if cmd.contains("test") || cmd.contains("spec") || cmd.contains("_test.") {
return "approve";
}
}
"abstain"
}
pub fn print_baseline() {
let decisions = read_all_decisions();
let total = decisions.len();
println!("Rules Baseline Comparison");
println!("=========================");
println!();
if total < 10 {
println!(" Not enough decisions yet ({total}). Need at least 10.");
return;
}
let mut brain_correct = 0u32;
let mut brain_wrong = 0u32;
let mut rules_correct = 0u32;
let mut rules_wrong = 0u32;
let mut rules_abstain = 0u32;
let mut both_correct = 0u32;
let mut brain_only = 0u32;
let mut rules_only = 0u32;
let mut both_wrong = 0u32;
let mut risk_stats: HashMap<RiskTier, (u32, u32, u32, u32)> = HashMap::new();
for d in &decisions {
let user_wanted = if d.is_positive() {
&d.brain_action } else if d.is_negative() {
if d.brain_action == "approve" {
"deny"
} else {
"approve"
}
} else {
continue; };
let rules_said = rules_baseline_classify(d.tool.as_deref(), d.command.as_deref());
let brain_said = d.brain_action.as_str();
let risk = classify_risk(d.tool.as_deref(), d.command.as_deref());
let brain_right = brain_said == user_wanted;
let rules_right = rules_said == user_wanted;
let rules_skipped = rules_said == "abstain";
if brain_right {
brain_correct += 1;
} else {
brain_wrong += 1;
}
if rules_skipped {
rules_abstain += 1;
} else if rules_right {
rules_correct += 1;
} else {
rules_wrong += 1;
}
match (brain_right, rules_right || rules_skipped) {
(true, true) if !rules_skipped => both_correct += 1,
(true, _) => brain_only += 1,
(false, true) if !rules_skipped => rules_only += 1,
_ => both_wrong += 1,
}
let rs = risk_stats.entry(risk).or_insert((0, 0, 0, 0));
if brain_right {
rs.0 += 1;
} else {
rs.1 += 1;
}
if !rules_skipped {
if rules_right {
rs.2 += 1;
} else {
rs.3 += 1;
}
}
}
let decided = brain_correct + brain_wrong;
let rules_decided = rules_correct + rules_wrong;
println!(" Overall ({decided} decisions with feedback):");
println!();
println!(
" {:<25} {:>8} {:>8} {:>8}",
"", "Correct", "Wrong", "Accuracy"
);
println!(" {}", "-".repeat(49));
if decided > 0 {
println!(
" {:<25} {:>8} {:>8} {:>7.1}%",
"Brain (LLM)",
brain_correct,
brain_wrong,
(brain_correct as f64 / decided as f64) * 100.0,
);
}
if rules_decided > 0 {
println!(
" {:<25} {:>8} {:>8} {:>7.1}%",
"Rules baseline",
rules_correct,
rules_wrong,
(rules_correct as f64 / rules_decided as f64) * 100.0,
);
}
println!(
" {:<25} {:>8}",
"Rules abstained (no match)", rules_abstain,
);
println!();
println!(" Agreement:");
println!(" Both correct: {both_correct}");
println!(" Brain only correct: {brain_only}");
println!(" Rules only correct: {rules_only}");
println!(" Both wrong: {both_wrong}");
println!();
println!(" By risk tier:");
println!(
" {:<12} {:>12} {:>12} {:>8}",
"Risk", "Brain acc.", "Rules acc.", "Delta"
);
println!(" {}", "-".repeat(48));
for risk in &[
RiskTier::Low,
RiskTier::Medium,
RiskTier::High,
RiskTier::Critical,
] {
if let Some(&(bc, bw, rc, rw)) = risk_stats.get(risk) {
let b_total = bc + bw;
let r_total = rc + rw;
let b_acc = if b_total > 0 {
(bc as f64 / b_total as f64) * 100.0
} else {
0.0
};
let r_acc = if r_total > 0 {
(rc as f64 / r_total as f64) * 100.0
} else {
0.0
};
let delta = b_acc - r_acc;
let delta_str = if r_total == 0 {
"n/a".to_string()
} else {
format!("{delta:+.1}pp")
};
println!(
" {:<12} {:>11.1}% {:>11.1}% {:>8}",
risk.label(),
b_acc,
r_acc,
delta_str,
);
}
}
}
pub fn print_false_approve() {
let decisions = read_all_decisions();
let total = decisions.len();
println!("False-Approve Rate (Risky Actions)");
println!("===================================");
println!();
if total < 5 {
println!(" Not enough decisions yet ({total}). Need at least 5.");
return;
}
let mut tier_stats: HashMap<RiskTier, FalseApproveStats> = HashMap::new();
let mut worst_cases: Vec<FalseApproveCase> = Vec::new();
for d in &decisions {
let risk = classify_risk(d.tool.as_deref(), d.command.as_deref());
let stats = tier_stats.entry(risk).or_default();
let brain_approved = d.brain_action == "approve";
let user_rejected = d.is_negative();
if brain_approved {
stats.brain_approved += 1;
if user_rejected {
stats.false_approved += 1;
if matches!(risk, RiskTier::High | RiskTier::Critical) {
worst_cases.push(FalseApproveCase {
risk,
tool: d.tool.clone().unwrap_or_default(),
command: d.command.clone().unwrap_or_default(),
confidence: d.brain_confidence,
});
}
}
}
stats.total += 1;
}
println!(
" {:<12} {:>10} {:>12} {:>12} {:>12}",
"Risk tier", "Decisions", "Approved", "False-approve", "FA rate"
);
println!(" {}", "-".repeat(62));
for risk in &[
RiskTier::Low,
RiskTier::Medium,
RiskTier::High,
RiskTier::Critical,
] {
let stats = tier_stats.get(risk).copied().unwrap_or_default();
let fa_rate = if stats.brain_approved > 0 {
(stats.false_approved as f64 / stats.brain_approved as f64) * 100.0
} else {
0.0
};
let rate_str = if stats.brain_approved == 0 {
"n/a".to_string()
} else {
format!("{fa_rate:.1}%")
};
println!(
" {:<12} {:>10} {:>12} {:>12} {:>12}",
risk.label(),
stats.total,
stats.brain_approved,
stats.false_approved,
rate_str,
);
}
let total_approved: u32 = tier_stats.values().map(|s| s.brain_approved).sum();
let total_false: u32 = tier_stats.values().map(|s| s.false_approved).sum();
let overall_rate = if total_approved > 0 {
(total_false as f64 / total_approved as f64) * 100.0
} else {
0.0
};
println!(" {}", "-".repeat(62));
println!(
" {:<12} {:>10} {:>12} {:>12} {:>12}",
"OVERALL",
total,
total_approved,
total_false,
format!("{overall_rate:.1}%"),
);
let high_critical_approved: u32 = [RiskTier::High, RiskTier::Critical]
.iter()
.filter_map(|r| tier_stats.get(r))
.map(|s| s.brain_approved)
.sum();
let high_critical_false: u32 = [RiskTier::High, RiskTier::Critical]
.iter()
.filter_map(|r| tier_stats.get(r))
.map(|s| s.false_approved)
.sum();
println!();
if high_critical_approved > 0 {
let hc_rate = (high_critical_false as f64 / high_critical_approved as f64) * 100.0;
println!(
" High+Critical false-approve rate: {:.1}% ({high_critical_false}/{high_critical_approved})",
hc_rate
);
if hc_rate > 5.0 {
println!(" WARNING: exceeds 5% target for high-risk actions");
} else if hc_rate <= 1.0 {
println!(" GOOD: within 1% target for high-risk actions");
}
} else {
println!(" No high/critical risk approvals recorded yet.");
}
if !worst_cases.is_empty() {
println!();
println!(" Worst cases (high/critical risk, brain approved, user rejected):");
for (i, case) in worst_cases.iter().take(10).enumerate() {
let cmd_preview = if case.command.len() > 60 {
format!("{}...", &case.command[..60])
} else {
case.command.clone()
};
println!(
" {}. [{}] {} \"{}\" (confidence: {:.0}%)",
i + 1,
case.risk,
case.tool,
cmd_preview,
case.confidence * 100.0,
);
}
}
}
#[derive(Debug, Clone, Copy, Default)]
struct FalseApproveStats {
total: u32,
brain_approved: u32,
false_approved: u32,
}
#[derive(Debug, Clone)]
struct FalseApproveCase {
risk: RiskTier,
tool: String,
command: String,
confidence: f64,
}
pub fn dispatch(subcommand: &str) {
match subcommand {
"learning-curve" | "curve" => print_learning_curve(),
"accuracy" | "acc" => print_accuracy(),
"baseline" | "rules" => print_baseline(),
"false-approve" | "fa" => print_false_approve(),
"help" | "" => print_help(),
_ => {
eprintln!("Unknown brain-stats subcommand: '{subcommand}'");
eprintln!();
print_help();
}
}
}
fn print_help() {
println!("Brain Statistics & Metrics");
println!("==========================");
println!();
println!("Usage: claudectl --brain-stats <subcommand>");
println!();
println!("Subcommands:");
println!(" learning-curve Correction rate over time (is the brain learning?)");
println!(" accuracy Per-tool, per-risk, per-project accuracy breakdown");
println!(" baseline Compare brain vs. rules-only classifier");
println!(" false-approve False-approve rate on risky actions (safety metric)");
println!(" help Show this help");
println!();
println!("Aliases: curve, acc, rules, fa");
}
#[cfg(test)]
mod tests {
use super::super::decisions::DecisionType;
use super::*;
#[test]
fn classify_read_as_low() {
assert_eq!(
classify_risk(Some("Read"), Some("src/main.rs")),
RiskTier::Low
);
assert_eq!(classify_risk(Some("Glob"), Some("**/*.rs")), RiskTier::Low);
assert_eq!(classify_risk(Some("Grep"), Some("TODO")), RiskTier::Low);
}
#[test]
fn classify_edit_as_medium() {
assert_eq!(
classify_risk(Some("Edit"), Some("src/lib.rs")),
RiskTier::Medium
);
assert_eq!(
classify_risk(Some("Write"), Some("tests/test.rs")),
RiskTier::Medium
);
}
#[test]
fn classify_config_write_as_high() {
assert_eq!(
classify_risk(Some("Write"), Some("config.toml")),
RiskTier::High
);
assert_eq!(classify_risk(Some("Edit"), Some(".env")), RiskTier::High);
}
#[test]
fn classify_destructive_as_critical() {
assert_eq!(
classify_risk(Some("Bash"), Some("rm -rf /tmp")),
RiskTier::Critical
);
assert_eq!(
classify_risk(Some("Bash"), Some("git push --force origin main")),
RiskTier::Critical
);
assert_eq!(
classify_risk(Some("Bash"), Some("DROP TABLE users")),
RiskTier::Critical
);
}
#[test]
fn classify_safe_bash_as_low() {
assert_eq!(
classify_risk(Some("Bash"), Some("cargo test --release")),
RiskTier::Low
);
assert_eq!(
classify_risk(Some("Bash"), Some("git status")),
RiskTier::Low
);
assert_eq!(classify_risk(Some("Bash"), Some("ls -la")), RiskTier::Low);
}
#[test]
fn classify_risky_bash_as_high() {
assert_eq!(
classify_risk(Some("Bash"), Some("git push origin main")),
RiskTier::High
);
assert_eq!(
classify_risk(Some("Bash"), Some("npm publish")),
RiskTier::High
);
}
#[test]
fn classify_unknown_tool_as_medium() {
assert_eq!(
classify_risk(Some("CustomTool"), Some("anything")),
RiskTier::Medium
);
assert_eq!(classify_risk(None, None), RiskTier::Medium);
}
#[test]
fn rules_approves_reads() {
assert_eq!(
rules_baseline_classify(Some("Read"), Some("file.rs")),
"approve"
);
assert_eq!(
rules_baseline_classify(Some("Glob"), Some("**/*.ts")),
"approve"
);
assert_eq!(
rules_baseline_classify(Some("Grep"), Some("TODO")),
"approve"
);
}
#[test]
fn rules_denies_destructive() {
assert_eq!(
rules_baseline_classify(Some("Bash"), Some("rm -rf /tmp")),
"deny"
);
assert_eq!(
rules_baseline_classify(Some("Bash"), Some("git push --force")),
"deny"
);
}
#[test]
fn rules_approves_safe_bash() {
assert_eq!(
rules_baseline_classify(Some("Bash"), Some("cargo test")),
"approve"
);
assert_eq!(
rules_baseline_classify(Some("Bash"), Some("git status")),
"approve"
);
}
#[test]
fn rules_abstains_on_unknown() {
assert_eq!(
rules_baseline_classify(Some("Bash"), Some("python train.py")),
"abstain"
);
assert_eq!(
rules_baseline_classify(Some("Edit"), Some("src/main.rs")),
"abstain"
);
}
#[test]
fn rules_approves_test_file_edits() {
assert_eq!(
rules_baseline_classify(Some("Write"), Some("tests/unit_test.rs")),
"approve"
);
}
#[test]
fn rolling_window_empty() {
assert!(rolling_correction_rate(&[], 10).is_empty());
}
#[test]
fn rolling_window_too_small() {
let decisions: Vec<DecisionRecord> = (0..5).map(|_| make_decision("accept")).collect();
assert!(rolling_correction_rate(&decisions, 10).is_empty());
}
#[test]
fn rolling_window_all_correct() {
let decisions: Vec<DecisionRecord> = (0..20).map(|_| make_decision("accept")).collect();
let points = rolling_correction_rate(&decisions, 10);
assert!(!points.is_empty());
for p in &points {
assert!((p.correction_rate - 0.0).abs() < f64::EPSILON);
}
}
#[test]
fn rolling_window_all_rejected() {
let decisions: Vec<DecisionRecord> = (0..20).map(|_| make_decision("reject")).collect();
let points = rolling_correction_rate(&decisions, 10);
for p in &points {
assert!((p.correction_rate - 1.0).abs() < f64::EPSILON);
}
}
#[test]
fn rolling_window_decreasing() {
let mut decisions: Vec<DecisionRecord> = (0..10).map(|_| make_decision("reject")).collect();
decisions.extend((0..10).map(|_| make_decision("accept")));
let points = rolling_correction_rate(&decisions, 10);
let first = points.first().unwrap().correction_rate;
let last = points.last().unwrap().correction_rate;
assert!(
first > last,
"Expected decreasing curve: first={first}, last={last}"
);
}
#[test]
fn risk_tier_labels() {
assert_eq!(RiskTier::Low.label(), "low");
assert_eq!(RiskTier::Critical.label(), "critical");
assert_eq!(format!("{}", RiskTier::High), "high");
}
fn make_decision(user_action: &str) -> DecisionRecord {
DecisionRecord {
timestamp: "0".into(),
pid: 1,
project: "test".into(),
tool: Some("Bash".into()),
command: Some("cargo test".into()),
brain_action: "approve".into(),
brain_confidence: 0.9,
brain_reasoning: "test".into(),
user_action: user_action.into(),
context: None,
outcome: None,
decision_type: DecisionType::Session,
}
}
#[test]
fn dispatch_help_no_panic() {
print_help();
}
}