1use nulid::Nulid;
9use serde::{Deserialize, Serialize};
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum Severity {
14 Low,
15 Medium,
16 High,
17}
18#[derive(Debug, Clone)]
20pub struct CyclePattern {
21 pub id: &'static str,
22 pub name: &'static str,
23 pub severity: Severity,
24 pub edge_sequence: &'static [&'static str],
27}
28
29#[derive(Debug, Clone)]
31pub struct PathStep {
32 pub edge_type: &'static str,
33 pub target_label: Option<&'static str>,
35}
36
37#[derive(Debug, Clone)]
40pub struct PathPattern {
41 pub id: &'static str,
42 pub name: &'static str,
43 pub severity: Severity,
44 pub start_label: Option<&'static str>,
46 pub steps: &'static [PathStep],
47}
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ConflictPattern {
51 pub pattern_id: String,
52 pub pattern_name: String,
53 pub severity: Severity,
54 pub nodes: Vec<Nulid>,
56 pub edges: Vec<Nulid>,
58 pub path: Vec<Nulid>,
60 pub description: String,
62}
63#[must_use]
65pub fn cycle_patterns() -> Vec<CyclePattern> {
66 vec![
67 CyclePattern {
68 id: "COI-001",
69 name: "Donation-Appointment Cycle",
70 severity: Severity::High,
71 edge_sequence: &["DONATED_TO", "APPOINTED_BY"],
72 },
73 CyclePattern {
74 id: "COI-002",
75 name: "Contract Kickback Cycle",
76 severity: Severity::High,
77 edge_sequence: &["CONTRACTED_WITH", "DONATED_TO"],
78 },
79 CyclePattern {
80 id: "COI-003",
81 name: "Revolving Door",
82 severity: Severity::High,
83 edge_sequence: &["EMPLOYED_BY", "LOBBIED_FOR"],
84 },
85 ]
86}
87
88#[must_use]
90pub fn path_patterns() -> Vec<PathPattern> {
91 vec![
92 PathPattern {
93 id: "COI-004",
94 name: "Family Appointment",
95 severity: Severity::Medium,
96 start_label: Some("Actor"),
97 steps: &[
98 PathStep {
99 edge_type: "FAMILY_OF",
100 target_label: Some("Actor"),
101 },
102 PathStep {
103 edge_type: "APPOINTED_BY",
104 target_label: Some("Institution"),
105 },
106 ],
107 },
108 PathPattern {
109 id: "COI-005",
110 name: "Donor Influence Chain",
111 severity: Severity::Medium,
112 start_label: Some("Actor"),
113 steps: &[
114 PathStep {
115 edge_type: "DONATED_TO",
116 target_label: Some("Institution"),
117 },
118 PathStep {
119 edge_type: "CONTRACTED_WITH",
120 target_label: Some("Institution"),
121 },
122 ],
123 },
124 ]
125}
126
127pub const HUB_INFLUENCE_TYPES: &[&str] = &[
129 "DONATED_TO",
130 "CONTRACTED_WITH",
131 "APPOINTED_BY",
132 "FUNDED_BY",
133 "OWNS",
134 "LOBBIED_FOR",
135];
136
137pub const HUB_THRESHOLD: usize = 3;
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn cycle_catalog_has_expected_entries() {
146 let patterns = cycle_patterns();
147 assert_eq!(patterns.len(), 3);
148 assert_eq!(patterns[0].id, "COI-001");
149 assert_eq!(patterns[0].edge_sequence.len(), 2);
150 }
151
152 #[test]
153 fn path_catalog_has_expected_entries() {
154 let patterns = path_patterns();
155 assert_eq!(patterns.len(), 2);
156 assert_eq!(patterns[0].id, "COI-004");
157 assert_eq!(patterns[0].steps.len(), 2);
158 }
159
160 #[test]
161 fn severity_serializes_lowercase() {
162 let json = serde_json::to_string(&Severity::High).unwrap_or_default();
163 assert_eq!(json, "\"high\"");
164 }
165}