use std::cmp::Ordering;
use exo_core::{Did, Hash256, Signature, Timestamp};
use serde::{Deserialize, Serialize};
pub type TenantId = String;
pub const FINANCIAL_HUMAN_GATE_THRESHOLD_CENTS: u64 = 100_000;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct SemVer {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl SemVer {
#[must_use]
pub fn new(major: u32, minor: u32, patch: u32) -> Self {
Self {
major,
minor,
patch,
}
}
#[must_use]
pub fn is_compatible_with(&self, other: &SemVer) -> bool {
self.major == other.major
&& (self.minor > other.minor
|| (self.minor == other.minor && self.patch >= other.patch))
}
}
impl Ord for SemVer {
fn cmp(&self, other: &Self) -> Ordering {
self.major
.cmp(&other.major)
.then_with(|| self.minor.cmp(&other.minor))
.then_with(|| self.patch.cmp(&other.patch))
}
}
impl PartialOrd for SemVer {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl std::fmt::Display for SemVer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum DecisionClass {
Operational,
Strategic,
Constitutional,
Financial { threshold_cents: u64 },
Emergency,
Custom(String),
}
impl DecisionClass {
#[must_use]
pub fn requires_human_gate(&self) -> bool {
match self {
DecisionClass::Constitutional | DecisionClass::Strategic | DecisionClass::Emergency => {
true
}
DecisionClass::Financial { threshold_cents } => {
*threshold_cents >= FINANCIAL_HUMAN_GATE_THRESHOLD_CENTS
}
DecisionClass::Operational | DecisionClass::Custom(_) => false,
}
}
}
impl std::fmt::Display for DecisionClass {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DecisionClass::Operational => f.write_str("Operational"),
DecisionClass::Strategic => f.write_str("Strategic"),
DecisionClass::Constitutional => f.write_str("Constitutional"),
DecisionClass::Financial { threshold_cents } => {
write!(f, "Financial(threshold_cents={threshold_cents})")
}
DecisionClass::Emergency => f.write_str("Emergency"),
DecisionClass::Custom(name) => write!(f, "Custom({name})"),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum SignerType {
Human,
AiAgent {
delegation_id: Hash256,
expires_at: u64,
},
}
#[derive(Clone, Serialize, Deserialize)]
pub struct GovernanceSignature {
pub signer: Did,
pub signer_type: SignerType,
pub signature: Signature,
pub key_version: u64,
pub timestamp: Timestamp,
}
impl std::fmt::Debug for GovernanceSignature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GovernanceSignature")
.field("signer", &self.signer)
.field("signer_type", &self.signer_type)
.field("signature", &"[REDACTED]")
.field("key_version", &self.key_version)
.field("timestamp", &self.timestamp)
.finish()
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum AuthorizedAction {
CreateDecision,
AdvanceDecision,
CastVote,
GrantDelegation,
RevokeDelegation,
RaiseChallenge,
TakeEmergencyAction,
AmendConstitution,
DiscloseConflict,
Custom(String),
}
impl std::fmt::Display for AuthorizedAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AuthorizedAction::CreateDecision => f.write_str("CreateDecision"),
AuthorizedAction::AdvanceDecision => f.write_str("AdvanceDecision"),
AuthorizedAction::CastVote => f.write_str("CastVote"),
AuthorizedAction::GrantDelegation => f.write_str("GrantDelegation"),
AuthorizedAction::RevokeDelegation => f.write_str("RevokeDelegation"),
AuthorizedAction::RaiseChallenge => f.write_str("RaiseChallenge"),
AuthorizedAction::TakeEmergencyAction => f.write_str("TakeEmergencyAction"),
AuthorizedAction::AmendConstitution => f.write_str("AmendConstitution"),
AuthorizedAction::DiscloseConflict => f.write_str("DiscloseConflict"),
AuthorizedAction::Custom(name) => write!(f, "Custom({name})"),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct EvidenceRef {
pub id: String,
pub description: String,
pub content_hash: Hash256,
pub timestamp: Timestamp,
pub author: Did,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum FailureAction {
Block,
Warn,
Escalate { escalation_target: Did },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_semver_display() {
let v = SemVer::new(1, 2, 3);
assert_eq!(v.to_string(), "1.2.3");
}
#[test]
fn test_semver_compatibility() {
let v1 = SemVer::new(1, 2, 0);
let v2 = SemVer::new(1, 1, 0);
let v3 = SemVer::new(2, 0, 0);
assert!(v1.is_compatible_with(&v2));
assert!(!v2.is_compatible_with(&v1));
assert!(!v1.is_compatible_with(&v3));
}
#[test]
fn test_semver_ordering_is_explicit_precedence_not_compatibility() {
assert!(
SemVer::new(1, 10, 0) > SemVer::new(1, 2, 99),
"semantic precedence must compare numeric major, minor, then patch components"
);
assert!(
SemVer::new(2, 0, 0) > SemVer::new(1, u32::MAX, u32::MAX),
"major version precedence must dominate minor and patch components"
);
assert!(
!SemVer::new(2, 0, 0).is_compatible_with(&SemVer::new(1, u32::MAX, u32::MAX)),
"ordering precedence must not imply constitutional compatibility"
);
let source = include_str!("types.rs");
assert!(
!source.contains(concat!("Partial", "Ord, Ord")),
"SemVer ordering must be implemented explicitly instead of derived"
);
assert!(
source.contains(concat!("impl Ord", " for SemVer")),
"SemVer must document precedence through an explicit Ord implementation"
);
}
#[test]
fn test_decision_class_human_gate() {
assert!(DecisionClass::Constitutional.requires_human_gate());
assert!(DecisionClass::Strategic.requires_human_gate());
assert!(DecisionClass::Emergency.requires_human_gate());
assert!(!DecisionClass::Operational.requires_human_gate());
assert!(
!DecisionClass::Financial {
threshold_cents: 1000
}
.requires_human_gate()
);
}
#[test]
fn test_financial_decision_requires_human_gate_at_threshold() {
assert!(
!DecisionClass::Financial {
threshold_cents: 99_999
}
.requires_human_gate()
);
assert!(
DecisionClass::Financial {
threshold_cents: 100_000
}
.requires_human_gate()
);
assert!(
DecisionClass::Financial {
threshold_cents: u64::MAX
}
.requires_human_gate()
);
}
#[test]
fn test_governance_signature_debug_redacts_signature_material() {
let signature = GovernanceSignature {
signer: Did::new("did:exo:secretary").expect("valid"),
signer_type: SignerType::Human,
signature: Signature::Ed25519([7_u8; 64]),
key_version: 3,
timestamp: Timestamp::new(1000, 0),
};
let debug = format!("{signature:?}");
assert!(debug.contains("GovernanceSignature"));
assert!(debug.contains("signature: \"[REDACTED]\""));
assert!(!debug.contains("Ed25519"));
assert!(!debug.contains("7, 7"));
}
#[test]
fn test_custom_decision_class_no_human_gate() {
assert!(!DecisionClass::Custom("routine".to_string()).requires_human_gate());
}
#[test]
fn test_signer_type_variants() {
let human = SignerType::Human;
let ai = SignerType::AiAgent {
delegation_id: Hash256::ZERO,
expires_at: 9999,
};
assert_ne!(human, ai);
}
#[test]
fn test_authorized_action_equality() {
assert_eq!(AuthorizedAction::CastVote, AuthorizedAction::CastVote);
assert_ne!(AuthorizedAction::CastVote, AuthorizedAction::CreateDecision);
}
#[test]
fn stable_display_labels_for_class_and_action() {
assert_eq!(DecisionClass::Strategic.to_string(), "Strategic");
assert_eq!(
DecisionClass::Financial {
threshold_cents: 100_000
}
.to_string(),
"Financial(threshold_cents=100000)"
);
assert_eq!(
DecisionClass::Custom("tenant-local".to_string()).to_string(),
"Custom(tenant-local)"
);
assert_eq!(AuthorizedAction::CastVote.to_string(), "CastVote");
assert_eq!(
AuthorizedAction::Custom("tenant-action".to_string()).to_string(),
"Custom(tenant-action)"
);
}
#[test]
fn test_evidence_ref() {
let evidence = EvidenceRef {
id: "ev-001".to_string(),
description: "Board minutes".to_string(),
content_hash: Hash256::digest(b"board-minutes-2024"),
timestamp: Timestamp::new(1000, 0),
author: Did::new("did:exo:secretary").expect("valid"),
};
assert_eq!(evidence.id, "ev-001");
}
}