use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Action {
pub id: String,
pub priority: u8,
pub effort: u8,
pub impact: u32,
pub quick_win: bool,
pub title: String,
pub evidence: String,
}
impl Action {
pub fn new(id: &str, priority: u8, effort: u8, title: &str, evidence: String) -> Self {
let impact = ((4 - priority.min(3)) as u32)
.saturating_mul(30)
.saturating_sub((effort.saturating_sub(1)) as u32 * 5);
Self {
id: id.to_string(),
priority,
effort,
impact,
quick_win: impact >= 55 && effort == 1,
title: title.to_string(),
evidence,
}
}
}
pub fn grade(score: u32) -> &'static str {
if score >= 90 {
"A"
} else if score >= 75 {
"B"
} else if score >= 60 {
"C"
} else if score >= 40 {
"D"
} else {
"F"
}
}
pub fn rank(mut actions: Vec<Action>) -> Vec<Action> {
actions.sort_by(|a, b| {
b.impact
.cmp(&a.impact)
.then(a.effort.cmp(&b.effort))
.then(a.id.cmp(&b.id))
});
actions
}
pub fn top(actions: &[Action], n: usize) -> &[Action] {
&actions[..actions.len().min(n)]
}
pub fn effort_label(e: u8) -> &'static str {
match e {
1 => "hours",
2 => "about a day",
3 => "several days",
_ => "a project",
}
}
pub fn to_csv(actions: &[Action]) -> String {
let mut s = String::from("id,priority,effort_band,impact,quick_win,gate,title,evidence\n");
for a in actions {
let cell = |v: &str| format!("\"{}\"", v.replace('"', "\"\""));
let gate = action_gate(a);
s.push_str(&format!(
"{},{},{},{},{},{},{},{}\n",
a.id,
a.priority,
effort_label(a.effort),
a.impact,
a.quick_win,
gate,
cell(&a.title),
cell(&a.evidence)
));
}
s
}
fn action_gate(action: &Action) -> &'static str {
let bare = action.id.strip_prefix("RULE-").unwrap_or(&action.id);
if crate::rules::rule(bare).is_none() {
return "advisory";
}
let scope = action.evidence.split(',').next().unwrap_or("").trim();
match crate::rules::effective_gate(bare, scope) {
crate::rules::Gate::Blocking => "blocking",
crate::rules::Gate::Advisory => "advisory",
}
}