use std::io::{self, BufRead, Write};
use super::decisions::{DecisionRecord, mark_canonical, read_all_decisions};
use super::metrics::{compute_counterfactuals, compute_tier_stats};
use super::risk::{RiskTier, classify_risk};
#[derive(Debug, Clone)]
pub struct ReviewItem {
pub record: DecisionRecord,
pub reason: String,
pub score: i32,
}
pub fn build_queue(decisions: &[DecisionRecord]) -> Vec<ReviewItem> {
let mut items: Vec<ReviewItem> = Vec::new();
let cfs = compute_counterfactuals(decisions);
for cf in &cfs {
if cf.brain_was_right {
if let Some(record) = find_by_id(decisions, cf.decision_id.as_deref()) {
items.push(ReviewItem {
record: record.clone(),
reason: format!("Brain was right (counterfactual): {}", cf.outcome_summary),
score: 100,
});
}
}
}
for d in decisions {
if d.brain_action.is_empty() {
continue;
}
if d.canonical == Some(true) {
continue;
}
let tier = classify_risk(d.tool.as_deref(), d.command.as_deref());
if matches!(tier, RiskTier::Critical) && d.is_negative() && d.brain_action == "approve" {
items.push(ReviewItem {
record: d.clone(),
reason: "Critical-tier false-approve (safety review)".into(),
score: 90,
});
}
if d.is_negative() && d.brain_confidence >= 0.80 {
items.push(ReviewItem {
record: d.clone(),
reason: format!(
"High-confidence miss ({:.0}% confidence)",
d.brain_confidence * 100.0
),
score: 60 + ((d.brain_confidence - 0.80) * 100.0) as i32,
});
}
}
items.sort_by(|a, b| {
let a_id = a.record.decision_id.as_deref().unwrap_or("");
let b_id = b.record.decision_id.as_deref().unwrap_or("");
a_id.cmp(b_id).then_with(|| b.score.cmp(&a.score))
});
items.dedup_by(|a, b| {
a.record.decision_id.is_some() && a.record.decision_id == b.record.decision_id
});
items.sort_by_key(|x| std::cmp::Reverse(x.score));
items
}
fn find_by_id<'a>(decisions: &'a [DecisionRecord], id: Option<&str>) -> Option<&'a DecisionRecord> {
let id = id?;
decisions
.iter()
.find(|d| d.decision_id.as_deref() == Some(id))
}
pub fn run_interactive() -> usize {
let decisions = read_all_decisions();
let queue = build_queue(&decisions);
println!("Brain Review");
println!("============");
println!();
if queue.is_empty() {
println!("No review-worthy decisions in the queue. Either:");
println!(" - The brain has been right on every confident call (great).");
println!(" - Outcome attribution hasn't kicked in yet (try after more usage).");
println!();
println!("Run `claudectl --brain-stats scorecard` to see overall health.");
return 0;
}
println!(
"{} review candidates in queue, ordered by review value.",
queue.len()
);
println!();
println!("For each: [m]ark canonical · [n]ote + mark · [s]kip · [d]etails · [q]uit");
println!();
let stdin = io::stdin();
let mut reader = stdin.lock();
let mut marked = 0usize;
let total = queue.len();
for (i, item) in queue.iter().enumerate() {
println!("[{}/{}] reason: {}", i + 1, total, item.reason);
print_summary_line(&item.record);
println!();
loop {
print!(" > ");
let _ = io::stdout().flush();
let mut buf = String::new();
if reader.read_line(&mut buf).is_err() {
println!();
println!("Stopping review.");
return marked;
}
let cmd = buf.trim();
match cmd {
"m" | "mark" => {
if let Some(id) = item.record.decision_id.as_deref() {
match mark_canonical(id, None) {
Ok(()) => {
println!(" ✓ marked canonical");
marked += 1;
}
Err(e) => {
println!(" ! could not write: {e}");
}
}
} else {
println!(" ! no decision_id — older record, can't mark");
}
break;
}
"n" | "note" => {
print!(" note: ");
let _ = io::stdout().flush();
let mut note = String::new();
let _ = reader.read_line(&mut note);
if let Some(id) = item.record.decision_id.as_deref() {
match mark_canonical(id, Some(note.trim())) {
Ok(()) => {
println!(" ✓ marked canonical with note");
marked += 1;
}
Err(e) => {
println!(" ! could not write: {e}");
}
}
} else {
println!(" ! no decision_id — older record, can't mark");
}
break;
}
"d" | "details" => {
print_full_details(&item.record);
}
"s" | "skip" | "" => {
break;
}
"q" | "quit" | "exit" => {
println!();
println!("Reviewed {} item(s), marked {marked}.", i + 1);
return marked;
}
_ => {
println!(" unknown: '{}' — try m / n / s / d / q", cmd);
}
}
}
println!();
}
println!("Done. Marked {marked} of {total} canonical.");
marked
}
fn print_summary_line(d: &DecisionRecord) {
let tier = classify_risk(d.tool.as_deref(), d.command.as_deref());
println!(
" tier={} tool={} brain={} (conf {:.0}%) user={}",
tier,
d.tool.as_deref().unwrap_or("?"),
d.brain_action,
d.brain_confidence * 100.0,
d.user_action,
);
if let Some(cmd) = &d.command {
let short = if cmd.len() > 100 {
format!("{}…", &cmd[..100])
} else {
cmd.clone()
};
println!(" cmd: {}", short);
}
}
fn print_full_details(d: &DecisionRecord) {
println!(" --- details ---");
println!(
" decision_id: {}",
d.decision_id.as_deref().unwrap_or("(none)")
);
println!(" project: {}", d.project);
println!(
" tool: {}",
d.tool.as_deref().unwrap_or("(none)")
);
if let Some(cmd) = &d.command {
println!(" command: {cmd}");
}
println!(" brain_action: {}", d.brain_action);
println!(" brain_confidence: {:.2}", d.brain_confidence);
println!(" brain_reasoning: {}", d.brain_reasoning);
println!(" user_action: {}", d.user_action);
if let Some(reason) = &d.override_reason {
println!(" override_reason: {reason}");
}
if let Some(ms) = d.brain_decision_ms {
println!(" brain_latency: {ms} ms");
}
if let Some(hit) = d.cache_hit {
println!(" cache_hit: {hit}");
}
if let Some(ctx) = &d.context {
println!(" cost_usd: ${:.4}", ctx.cost_usd);
println!(" context_pct: {}%", ctx.context_pct);
println!(" model: {}", ctx.model);
}
println!();
}
pub fn mark_by_id(decision_id: &str, note: Option<&str>) -> Result<(), String> {
mark_canonical(decision_id, note)
}
pub fn print_queue() {
let decisions = read_all_decisions();
let queue = build_queue(&decisions);
let tier_stats = compute_tier_stats(&decisions);
println!("Review Queue ({} item(s))", queue.len());
println!(
"======================{}",
"=".repeat(queue.len().to_string().len())
);
println!();
println!("Per-tier sample sizes:");
for s in &tier_stats {
println!(" {:<10} n = {}", s.tier.label(), s.n);
}
println!();
for (i, item) in queue.iter().enumerate() {
println!("{}. [{}] {}", i + 1, item.score, item.reason);
print_summary_line(&item.record);
println!();
}
if queue.is_empty() {
println!("(empty)");
}
}