use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationPlanV1 {
pub plan_id: String,
pub claim_id: String,
pub risk_class: RiskClass,
pub required_cheap_checks: Vec<String>,
pub required_replay_checks: Vec<String>,
pub required_falsification_checks: Vec<String>,
pub treatment_refs: Vec<String>,
pub deadline: Option<String>,
pub disposition: VerificationDisposition,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RiskClass {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum VerificationDisposition {
Pending,
InProgress,
Verified,
Failed,
Quarantined,
Expired,
}
impl VerificationPlanV1 {
pub fn new(claim_id: &str, risk_class: RiskClass) -> Self {
let plan_id = format!("vp:{claim_id}:{}", chrono::Utc::now().timestamp());
Self {
plan_id,
claim_id: claim_id.to_string(),
risk_class,
required_cheap_checks: vec!["schema_validation".into(), "digest_check".into()],
required_replay_checks: if risk_class >= RiskClass::Medium {
vec!["receipt_replay".into()]
} else {
vec![]
},
required_falsification_checks: if risk_class >= RiskClass::High {
vec!["counterexample_search".into()]
} else {
vec![]
},
treatment_refs: vec![],
deadline: None,
disposition: VerificationDisposition::Pending,
}
}
pub fn can_promote(&self) -> bool {
self.disposition == VerificationDisposition::Verified
}
pub fn is_blocked(&self) -> bool {
matches!(
self.disposition,
VerificationDisposition::Failed | VerificationDisposition::Quarantined
)
}
}
impl PartialOrd for RiskClass {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.ordinal().cmp(&other.ordinal()))
}
}
impl RiskClass {
fn ordinal(&self) -> u8 {
match self {
RiskClass::Low => 0,
RiskClass::Medium => 1,
RiskClass::High => 2,
RiskClass::Critical => 3,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaimEvidenceBundleV1 {
pub bundle_id: String,
pub claim_id: String,
pub treatment: String,
pub outcome: String,
pub confounders: Vec<String>,
pub evidence_refs: Vec<String>,
pub refutation_refs: Vec<String>,
pub supports: bool,
}
impl ClaimEvidenceBundleV1 {
pub fn new(claim_id: &str, treatment: &str, outcome: &str) -> Self {
Self {
bundle_id: format!("ceb:{claim_id}:{}", chrono::Utc::now().timestamp()),
claim_id: claim_id.to_string(),
treatment: treatment.to_string(),
outcome: outcome.to_string(),
confounders: vec![],
evidence_refs: vec![],
refutation_refs: vec![],
supports: outcome == "pass",
}
}
pub fn with_evidence(mut self, ref_id: &str) -> Self {
self.evidence_refs.push(ref_id.to_string());
self
}
pub fn with_confounder(mut self, confounder: &str) -> Self {
self.confounders.push(confounder.to_string());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GovernanceDecisionV1 {
pub decision_id: String,
pub claim_id: String,
pub verification_plan_id: String,
pub decision: GovernanceAction,
pub rationale: String,
pub timestamp: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum GovernanceAction {
Promote,
Reject,
Quarantine,
Defer,
}
pub fn evaluate_promotion(
plan: &VerificationPlanV1,
evidence: &[ClaimEvidenceBundleV1],
) -> GovernanceDecisionV1 {
let decision = if plan.is_blocked() {
if plan.disposition == VerificationDisposition::Quarantined {
GovernanceAction::Quarantine
} else {
GovernanceAction::Reject
}
} else if plan.can_promote() {
let has_refutation = evidence.iter().any(|e| !e.supports);
if has_refutation {
GovernanceAction::Quarantine
} else {
GovernanceAction::Promote
}
} else {
GovernanceAction::Defer
};
let rationale = match decision {
GovernanceAction::Promote => "All checks passed, no refutations found".to_string(),
GovernanceAction::Reject => "Verification failed".to_string(),
GovernanceAction::Quarantine => "Contradiction detected or refutation found".to_string(),
GovernanceAction::Defer => "Verification pending".to_string(),
};
GovernanceDecisionV1 {
decision_id: format!("gd:{}:{}", plan.claim_id, chrono::Utc::now().timestamp()),
claim_id: plan.claim_id.clone(),
verification_plan_id: plan.plan_id.clone(),
decision,
rationale,
timestamp: chrono::Utc::now().to_rfc3339(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verification_plan_created_pending() {
let plan = VerificationPlanV1::new("claim-01", RiskClass::High);
assert_eq!(plan.disposition, VerificationDisposition::Pending);
assert!(!plan.can_promote());
assert!(!plan.is_blocked());
}
#[test]
fn high_risk_requires_falsification() {
let plan = VerificationPlanV1::new("claim-01", RiskClass::High);
assert!(!plan.required_falsification_checks.is_empty());
}
#[test]
fn low_risk_skips_falsification() {
let plan = VerificationPlanV1::new("claim-01", RiskClass::Low);
assert!(plan.required_falsification_checks.is_empty());
assert!(plan.required_replay_checks.is_empty());
}
#[test]
fn verified_plan_can_promote() {
let mut plan = VerificationPlanV1::new("claim-01", RiskClass::Low);
plan.disposition = VerificationDisposition::Verified;
assert!(plan.can_promote());
}
#[test]
fn failed_plan_is_blocked() {
let mut plan = VerificationPlanV1::new("claim-01", RiskClass::Low);
plan.disposition = VerificationDisposition::Failed;
assert!(plan.is_blocked());
assert!(!plan.can_promote());
}
#[test]
fn claim_evidence_bundle_marks_support() {
let bundle = ClaimEvidenceBundleV1::new("claim-01", "experiment", "pass");
assert!(bundle.supports);
}
#[test]
fn claim_evidence_bundle_marks_refutation() {
let bundle = ClaimEvidenceBundleV1::new("claim-01", "experiment", "fail");
assert!(!bundle.supports);
}
#[test]
fn evaluate_promotion_promotes_verified() {
let mut plan = VerificationPlanV1::new("claim-01", RiskClass::Low);
plan.disposition = VerificationDisposition::Verified;
let evidence = vec![ClaimEvidenceBundleV1::new("claim-01", "test", "pass")];
let decision = evaluate_promotion(&plan, &evidence);
assert_eq!(decision.decision, GovernanceAction::Promote);
}
#[test]
fn evaluate_promotion_quarantines_with_refutation() {
let mut plan = VerificationPlanV1::new("claim-01", RiskClass::Low);
plan.disposition = VerificationDisposition::Verified;
let evidence = vec![
ClaimEvidenceBundleV1::new("claim-01", "test", "pass"),
ClaimEvidenceBundleV1::new("claim-01", "counterexample", "fail"),
];
let decision = evaluate_promotion(&plan, &evidence);
assert_eq!(decision.decision, GovernanceAction::Quarantine);
}
#[test]
fn evaluate_promotion_defers_pending() {
let plan = VerificationPlanV1::new("claim-01", RiskClass::Low);
let evidence = vec![];
let decision = evaluate_promotion(&plan, &evidence);
assert_eq!(decision.decision, GovernanceAction::Defer);
}
#[test]
fn evaluate_promotion_rejects_failed() {
let mut plan = VerificationPlanV1::new("claim-01", RiskClass::Low);
plan.disposition = VerificationDisposition::Failed;
let decision = evaluate_promotion(&plan, &[]);
assert_eq!(decision.decision, GovernanceAction::Reject);
}
#[test]
fn risk_class_ordering() {
assert!(RiskClass::Critical > RiskClass::High);
assert!(RiskClass::High > RiskClass::Medium);
assert!(RiskClass::Medium > RiskClass::Low);
}
}