use nulid::Nulid;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Low,
Medium,
High,
}
#[derive(Debug, Clone)]
pub struct CyclePattern {
pub id: &'static str,
pub name: &'static str,
pub severity: Severity,
pub edge_sequence: &'static [&'static str],
}
#[derive(Debug, Clone)]
pub struct PathStep {
pub edge_type: &'static str,
pub target_label: Option<&'static str>,
}
#[derive(Debug, Clone)]
pub struct PathPattern {
pub id: &'static str,
pub name: &'static str,
pub severity: Severity,
pub start_label: Option<&'static str>,
pub steps: &'static [PathStep],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConflictPattern {
pub pattern_id: String,
pub pattern_name: String,
pub severity: Severity,
pub nodes: Vec<Nulid>,
pub edges: Vec<Nulid>,
pub path: Vec<Nulid>,
pub description: String,
}
#[must_use]
pub fn cycle_patterns() -> Vec<CyclePattern> {
vec![
CyclePattern {
id: "COI-001",
name: "Payment-Appointment Cycle",
severity: Severity::High,
edge_sequence: &["PAID_TO", "APPOINTED_BY"],
},
CyclePattern {
id: "COI-002",
name: "Contract-Payment Cycle",
severity: Severity::High,
edge_sequence: &["AWARDED_CONTRACT", "PAID_TO"],
},
CyclePattern {
id: "COI-003",
name: "Revolving Door",
severity: Severity::High,
edge_sequence: &["EMPLOYED_BY", "LOBBIED"],
},
]
}
#[must_use]
pub fn path_patterns() -> Vec<PathPattern> {
vec![
PathPattern {
id: "COI-004",
name: "Family Appointment",
severity: Severity::Medium,
start_label: Some("Person"),
steps: &[
PathStep {
edge_type: "FAMILY_OF",
target_label: Some("Person"),
},
PathStep {
edge_type: "APPOINTED_BY",
target_label: Some("Organization"),
},
],
},
PathPattern {
id: "COI-005",
name: "Payment Influence Chain",
severity: Severity::Medium,
start_label: Some("Person"),
steps: &[
PathStep {
edge_type: "PAID_TO",
target_label: Some("Organization"),
},
PathStep {
edge_type: "AWARDED_CONTRACT",
target_label: Some("Organization"),
},
],
},
]
}
pub const HUB_INFLUENCE_TYPES: &[&str] = &[
"PAID_TO",
"AWARDED_CONTRACT",
"APPOINTED_BY",
"FUNDED_BY",
"OWNS",
"LOBBIED",
];
pub const HUB_THRESHOLD: usize = 3;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cycle_catalog_has_expected_entries() {
let patterns = cycle_patterns();
assert_eq!(patterns.len(), 3);
assert_eq!(patterns[0].id, "COI-001");
assert_eq!(patterns[0].edge_sequence.len(), 2);
}
#[test]
fn path_catalog_has_expected_entries() {
let patterns = path_patterns();
assert_eq!(patterns.len(), 2);
assert_eq!(patterns[0].id, "COI-004");
assert_eq!(patterns[0].steps.len(), 2);
}
#[test]
fn severity_serializes_lowercase() {
let json = serde_json::to_string(&Severity::High).unwrap_or_default();
assert_eq!(json, "\"high\"");
}
}