use crate::proposals::ProposalStatus;
use crate::safety_gates::ApprovalRequirement;
#[derive(Debug, Clone)]
pub enum KanbanAction {
CreateItem {
id: String,
title: String,
status: String,
assignee: String,
priority: String,
tags: Vec<String>,
related: Vec<String>,
},
UpdateStatus { id: String, new_status: String },
Complete {
id: String,
outcome: String, },
}
pub fn proposal_to_kanban_id(proposal_id: &str) -> String {
format!("PROP-{proposal_id}")
}
pub fn proposal_status_to_kanban(status: &ProposalStatus) -> &'static str {
match status {
ProposalStatus::Draft => "backlog",
ProposalStatus::Open => "in_progress",
ProposalStatus::Reviewing => "in_progress",
ProposalStatus::Approved => "review",
ProposalStatus::Rejected => "done",
ProposalStatus::Revised => "in_progress",
ProposalStatus::Merged => "done",
ProposalStatus::Closed => "done",
}
}
pub fn safety_to_priority(requirement: &ApprovalRequirement) -> &'static str {
if requirement.requires_human {
"critical" } else if requirement.requires_shadow && requirement.auto_approve_threshold >= 0.05 {
"high" } else if requirement.requires_shadow {
"medium" } else {
"low" }
}
pub fn on_proposal_created(
proposal_id: &str,
title: &str,
author: &str,
proposal_type: &str,
requirement: &ApprovalRequirement,
) -> KanbanAction {
let kanban_id = proposal_to_kanban_id(proposal_id);
let priority = safety_to_priority(requirement);
let mut tags = vec!["self-modification".to_string(), proposal_type.to_string()];
if requirement.requires_human {
tags.push("human-gate".to_string());
}
KanbanAction::CreateItem {
id: kanban_id.clone(),
title: format!("Self-Mod Proposal: {title}"),
status: "backlog".to_string(),
assignee: author.to_string(),
priority: priority.to_string(),
tags,
related: vec![kanban_id],
}
}
pub fn on_proposal_transition(proposal_id: &str, new_status: &ProposalStatus) -> KanbanAction {
let kanban_id = proposal_to_kanban_id(proposal_id);
let kanban_status = proposal_status_to_kanban(new_status);
match new_status {
ProposalStatus::Merged => KanbanAction::Complete {
id: kanban_id,
outcome: "merged".to_string(),
},
ProposalStatus::Rejected => KanbanAction::Complete {
id: kanban_id,
outcome: "rejected".to_string(),
},
ProposalStatus::Closed => KanbanAction::Complete {
id: kanban_id,
outcome: "closed".to_string(),
},
_ => KanbanAction::UpdateStatus {
id: kanban_id,
new_status: kanban_status.to_string(),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::safety_gates::classify_change;
use crate::safety_gates::default_gates;
#[test]
fn test_proposal_to_kanban_id() {
assert_eq!(proposal_to_kanban_id("abc-123"), "PROP-abc-123");
}
#[test]
fn test_status_mapping() {
assert_eq!(proposal_status_to_kanban(&ProposalStatus::Draft), "backlog");
assert_eq!(
proposal_status_to_kanban(&ProposalStatus::Open),
"in_progress"
);
assert_eq!(
proposal_status_to_kanban(&ProposalStatus::Approved),
"review"
);
assert_eq!(proposal_status_to_kanban(&ProposalStatus::Merged), "done");
assert_eq!(proposal_status_to_kanban(&ProposalStatus::Rejected), "done");
assert_eq!(proposal_status_to_kanban(&ProposalStatus::Closed), "done");
}
#[test]
fn test_safety_priority_y6_critical() {
let gates = default_gates().unwrap();
let req = classify_change(&gates, 6, "general");
assert_eq!(safety_to_priority(&req), "critical");
}
#[test]
fn test_safety_priority_y2_high() {
let gates = default_gates().unwrap();
let req = classify_change(&gates, 2, "general");
assert_eq!(safety_to_priority(&req), "high");
}
#[test]
fn test_safety_priority_y5_medium() {
let gates = default_gates().unwrap();
let req = classify_change(&gates, 5, "engineering");
assert_eq!(safety_to_priority(&req), "medium");
}
#[test]
fn test_safety_priority_y0_low() {
let gates = default_gates().unwrap();
let req = classify_change(&gates, 0, "general");
assert_eq!(safety_to_priority(&req), "low");
}
#[test]
fn test_on_proposal_created() {
let gates = default_gates().unwrap();
let req = classify_change(&gates, 6, "general");
let action = on_proposal_created(
"001",
"Fix calibration",
"santiago",
"knowledge_change",
&req,
);
match action {
KanbanAction::CreateItem { priority, tags, .. } => {
assert_eq!(priority, "critical");
assert!(tags.contains(&"human-gate".to_string()));
assert!(tags.contains(&"self-modification".to_string()));
}
_ => panic!("Expected CreateItem"),
}
}
#[test]
fn test_on_proposal_merged() {
let action = on_proposal_transition("001", &ProposalStatus::Merged);
match action {
KanbanAction::Complete { outcome, .. } => assert_eq!(outcome, "merged"),
_ => panic!("Expected Complete"),
}
}
#[test]
fn test_on_proposal_rejected() {
let action = on_proposal_transition("001", &ProposalStatus::Rejected);
match action {
KanbanAction::Complete { outcome, .. } => assert_eq!(outcome, "rejected"),
_ => panic!("Expected Complete"),
}
}
#[test]
fn test_on_proposal_opened() {
let action = on_proposal_transition("001", &ProposalStatus::Open);
match action {
KanbanAction::UpdateStatus { new_status, .. } => assert_eq!(new_status, "in_progress"),
_ => panic!("Expected UpdateStatus"),
}
}
}