use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Review {
pub reviewer_id: String,
pub status: String,
pub timestamp: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
pub at_version: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApprovalPolicy {
pub required_count: u32,
pub require_unanimous: bool,
pub allowed_reviewer_ids: Vec<String>,
pub timeout_ms: f64,
#[serde(default)]
pub required_percentage: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApprovalResult {
pub approved: bool,
pub approved_by: Vec<String>,
pub rejected_by: Vec<String>,
pub pending_from: Vec<String>,
pub stale_from: Vec<String>,
pub reason: String,
}
pub fn evaluate_approvals_native(
reviews: &[Review],
policy: &ApprovalPolicy,
current_version: u32,
now: f64,
) -> ApprovalResult {
let mut approved_by = Vec::new();
let mut rejected_by = Vec::new();
let mut pending_from = Vec::new();
let mut stale_from = Vec::new();
let effective_reviewers: Vec<String> = if !policy.allowed_reviewer_ids.is_empty() {
policy.allowed_reviewer_ids.clone()
} else {
let mut seen = HashSet::new();
reviews
.iter()
.filter(|r| seen.insert(r.reviewer_id.clone()))
.map(|r| r.reviewer_id.clone())
.collect()
};
let mut review_map: HashMap<&str, &Review> = HashMap::new();
for review in reviews {
let dominated = match review_map.get(review.reviewer_id.as_str()) {
None => true,
Some(existing) => review.timestamp > existing.timestamp,
};
if dominated {
review_map.insert(&review.reviewer_id, review);
}
}
for reviewer_id in &effective_reviewers {
let Some(review) = review_map.get(reviewer_id.as_str()) else {
pending_from.push(reviewer_id.clone());
continue;
};
if review.at_version < current_version {
stale_from.push(reviewer_id.clone());
continue;
}
if policy.timeout_ms > 0.0 && (now - review.timestamp) > policy.timeout_ms {
stale_from.push(reviewer_id.clone());
continue;
}
match review.status.to_uppercase().as_str() {
"APPROVED" => approved_by.push(reviewer_id.clone()),
"REJECTED" => rejected_by.push(reviewer_id.clone()),
"STALE" => stale_from.push(reviewer_id.clone()),
_ => pending_from.push(reviewer_id.clone()),
}
}
let (approved, reason) = if !rejected_by.is_empty() {
(false, format!("Rejected by {}", rejected_by.join(", ")))
} else if policy.require_unanimous {
let all_approved = approved_by.len() == effective_reviewers.len()
&& pending_from.is_empty()
&& stale_from.is_empty();
let reason = if all_approved {
format!(
"Unanimous approval ({}/{})",
approved_by.len(),
effective_reviewers.len()
)
} else {
format!(
"Awaiting unanimous approval ({}/{})",
approved_by.len(),
effective_reviewers.len()
)
};
(all_approved, reason)
} else {
let threshold = if policy.required_percentage > 0 {
let pct = policy.required_percentage.min(100) as f64 / 100.0;
(pct * effective_reviewers.len() as f64).ceil() as u32
} else {
policy.required_count
};
let met = approved_by.len() as u32 >= threshold;
let threshold_label = if policy.required_percentage > 0 {
format!("{}% = {}", policy.required_percentage, threshold)
} else {
format!("{}", threshold)
};
let reason = if met {
format!(
"Approved ({}/{} required)",
approved_by.len(),
threshold_label
)
} else {
let remaining = threshold.saturating_sub(approved_by.len() as u32);
format!(
"Needs {} more approval(s) ({}/{} required)",
remaining,
approved_by.len(),
threshold_label
)
};
(met, reason)
};
ApprovalResult {
approved,
approved_by,
rejected_by,
pending_from,
stale_from,
reason,
}
}
pub fn mark_stale_reviews_native(reviews: &[Review], current_version: u32) -> Vec<Review> {
reviews
.iter()
.map(|r| {
if r.at_version < current_version && r.status.to_uppercase() != "STALE" {
Review {
status: "STALE".to_string(),
..r.clone()
}
} else {
r.clone()
}
})
.collect()
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub fn evaluate_approvals(
reviews_json: &str,
policy_json: &str,
current_version: u32,
now_ms: f64,
) -> Result<String, String> {
let reviews: Vec<Review> =
serde_json::from_str(reviews_json).map_err(|e| format!("Invalid reviews JSON: {e}"))?;
let policy: ApprovalPolicy =
serde_json::from_str(policy_json).map_err(|e| format!("Invalid policy JSON: {e}"))?;
let result = evaluate_approvals_native(&reviews, &policy, current_version, now_ms);
serde_json::to_string(&result).map_err(|e| format!("Serialization failed: {e}"))
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub fn mark_stale_reviews(reviews_json: &str, current_version: u32) -> Result<String, String> {
let reviews: Vec<Review> =
serde_json::from_str(reviews_json).map_err(|e| format!("Invalid reviews JSON: {e}"))?;
let result = mark_stale_reviews_native(&reviews, current_version);
serde_json::to_string(&result).map_err(|e| format!("Serialization failed: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
fn default_policy() -> ApprovalPolicy {
ApprovalPolicy {
required_count: 1,
require_unanimous: false,
allowed_reviewer_ids: vec![],
timeout_ms: 0.0,
required_percentage: 0,
}
}
fn review(id: &str, status: &str, version: u32) -> Review {
Review {
reviewer_id: id.to_string(),
status: status.to_string(),
timestamp: 1_000_000.0,
reason: None,
at_version: version,
}
}
#[test]
fn test_single_approval_meets_default_policy() {
let reviews = vec![review("agent-1", "APPROVED", 1)];
let result = evaluate_approvals_native(&reviews, &default_policy(), 1, 2_000_000.0);
assert!(result.approved);
assert_eq!(result.approved_by, vec!["agent-1"]);
}
#[test]
fn test_no_reviews_pending() {
let policy = ApprovalPolicy {
allowed_reviewer_ids: vec!["agent-1".to_string()],
..default_policy()
};
let result = evaluate_approvals_native(&[], &policy, 1, 2_000_000.0);
assert!(!result.approved);
assert_eq!(result.pending_from, vec!["agent-1"]);
}
#[test]
fn test_rejection_overrides_approval() {
let reviews = vec![
review("agent-1", "APPROVED", 1),
review("agent-2", "REJECTED", 1),
];
let policy = ApprovalPolicy {
required_count: 1,
..default_policy()
};
let result = evaluate_approvals_native(&reviews, &policy, 1, 2_000_000.0);
assert!(!result.approved);
assert!(result.reason.contains("Rejected"));
}
#[test]
fn test_stale_review_for_old_version() {
let reviews = vec![review("agent-1", "APPROVED", 1)];
let result = evaluate_approvals_native(&reviews, &default_policy(), 2, 2_000_000.0);
assert!(!result.approved);
assert_eq!(result.stale_from, vec!["agent-1"]);
}
#[test]
fn test_timed_out_review() {
let reviews = vec![Review {
reviewer_id: "agent-1".to_string(),
status: "APPROVED".to_string(),
timestamp: 1_000_000.0,
reason: None,
at_version: 1,
}];
let policy = ApprovalPolicy {
timeout_ms: 500_000.0,
..default_policy()
};
let result = evaluate_approvals_native(&reviews, &policy, 1, 2_000_000.0);
assert!(!result.approved);
assert_eq!(result.stale_from, vec!["agent-1"]);
}
#[test]
fn test_unanimous_policy() {
let policy = ApprovalPolicy {
required_count: 2,
require_unanimous: true,
allowed_reviewer_ids: vec!["agent-1".to_string(), "agent-2".to_string()],
timeout_ms: 0.0,
required_percentage: 0,
};
let reviews = vec![review("agent-1", "APPROVED", 1)];
let result = evaluate_approvals_native(&reviews, &policy, 1, 2_000_000.0);
assert!(!result.approved);
assert!(result.reason.contains("Awaiting unanimous"));
let reviews = vec![
review("agent-1", "APPROVED", 1),
review("agent-2", "APPROVED", 1),
];
let result = evaluate_approvals_native(&reviews, &policy, 1, 2_000_000.0);
assert!(result.approved);
assert!(result.reason.contains("Unanimous"));
}
#[test]
fn test_latest_review_wins() {
let reviews = vec![
Review {
reviewer_id: "agent-1".to_string(),
status: "REJECTED".to_string(),
timestamp: 1_000_000.0,
reason: None,
at_version: 1,
},
Review {
reviewer_id: "agent-1".to_string(),
status: "APPROVED".to_string(),
timestamp: 2_000_000.0,
reason: None,
at_version: 1,
},
];
let result = evaluate_approvals_native(&reviews, &default_policy(), 1, 3_000_000.0);
assert!(result.approved);
assert_eq!(result.approved_by, vec!["agent-1"]);
}
#[test]
fn test_mark_stale_reviews() {
let reviews = vec![
review("agent-1", "APPROVED", 1),
review("agent-2", "APPROVED", 2),
];
let result = mark_stale_reviews_native(&reviews, 2);
assert_eq!(result[0].status, "STALE");
assert_eq!(result[1].status, "APPROVED");
}
#[test]
fn test_wasm_evaluate_approvals() {
let reviews_json =
r#"[{"reviewerId":"agent-1","status":"APPROVED","timestamp":1000000,"atVersion":1}]"#;
let policy_json =
r#"{"requiredCount":1,"requireUnanimous":false,"allowedReviewerIds":[],"timeoutMs":0}"#;
let result_json = evaluate_approvals(reviews_json, policy_json, 1, 2_000_000.0).unwrap();
let result: ApprovalResult = serde_json::from_str(&result_json).unwrap();
assert!(result.approved);
}
#[test]
fn test_wasm_mark_stale() {
let reviews_json =
r#"[{"reviewerId":"agent-1","status":"APPROVED","timestamp":1000000,"atVersion":1}]"#;
let result_json = mark_stale_reviews(reviews_json, 2).unwrap();
let result: Vec<Review> = serde_json::from_str(&result_json).unwrap();
assert_eq!(result[0].status, "STALE");
}
#[test]
fn test_percentage_threshold_51_percent() {
let ids: Vec<String> = (1..=10).map(|i| format!("agent-{i}")).collect();
let policy = ApprovalPolicy {
required_count: 0,
require_unanimous: false,
allowed_reviewer_ids: ids,
timeout_ms: 0.0,
required_percentage: 51,
};
let reviews: Vec<Review> = (1..=5)
.map(|i| review(&format!("agent-{i}"), "APPROVED", 1))
.collect();
let result = evaluate_approvals_native(&reviews, &policy, 1, 2_000_000.0);
assert!(!result.approved, "5/10 should not meet 51%");
assert!(result.reason.contains("51%"));
let reviews: Vec<Review> = (1..=6)
.map(|i| review(&format!("agent-{i}"), "APPROVED", 1))
.collect();
let result = evaluate_approvals_native(&reviews, &policy, 1, 2_000_000.0);
assert!(result.approved, "6/10 should meet 51%");
}
#[test]
fn test_percentage_threshold_20_percent() {
let policy = ApprovalPolicy {
required_count: 0,
require_unanimous: false,
allowed_reviewer_ids: vec![
"a1".into(),
"a2".into(),
"a3".into(),
"a4".into(),
"a5".into(),
],
timeout_ms: 0.0,
required_percentage: 20,
};
let reviews = vec![review("a1", "APPROVED", 1)];
let result = evaluate_approvals_native(&reviews, &policy, 1, 2_000_000.0);
assert!(result.approved, "1/5 should meet 20%");
}
#[test]
fn test_percentage_overrides_count() {
let policy = ApprovalPolicy {
required_count: 10,
require_unanimous: false,
allowed_reviewer_ids: vec![
"a1".into(),
"a2".into(),
"a3".into(),
"a4".into(),
"a5".into(),
],
timeout_ms: 0.0,
required_percentage: 20,
};
let reviews = vec![review("a1", "APPROVED", 1)];
let result = evaluate_approvals_native(&reviews, &policy, 1, 2_000_000.0);
assert!(result.approved, "percentage should override count");
}
}