1use std::collections::HashMap;
9
10use chrono::NaiveDate;
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum HypergraphLayer {
18 GovernanceControls,
20 ProcessEvents,
22 AccountingNetwork,
24}
25
26impl HypergraphLayer {
27 pub fn index(&self) -> u8 {
29 match self {
30 HypergraphLayer::GovernanceControls => 1,
31 HypergraphLayer::ProcessEvents => 2,
32 HypergraphLayer::AccountingNetwork => 3,
33 }
34 }
35
36 pub fn name(&self) -> &'static str {
38 match self {
39 HypergraphLayer::GovernanceControls => "Governance & Controls",
40 HypergraphLayer::ProcessEvents => "Process Events",
41 HypergraphLayer::AccountingNetwork => "Accounting Network",
42 }
43 }
44}
45
46#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub enum AggregationStrategy {
50 Truncate,
52 #[default]
54 PoolByCounterparty,
55 PoolByTimePeriod,
57 ImportanceSample,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct HyperedgeParticipant {
64 pub node_id: String,
66 pub role: String,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub weight: Option<f64>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct Hyperedge {
76 pub id: String,
78 pub hyperedge_type: String,
80 pub subtype: String,
82 pub participants: Vec<HyperedgeParticipant>,
84 pub layer: HypergraphLayer,
86 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
88 pub properties: HashMap<String, Value>,
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub timestamp: Option<NaiveDate>,
92 #[serde(default)]
94 pub is_anomaly: bool,
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub anomaly_type: Option<String>,
98 #[serde(default, skip_serializing_if = "Vec::is_empty")]
100 pub features: Vec<f64>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct HypergraphNode {
106 pub id: String,
108 pub entity_type: String,
110 pub entity_type_code: u32,
112 pub layer: HypergraphLayer,
114 pub external_id: String,
116 pub label: String,
118 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
120 pub properties: HashMap<String, Value>,
121 #[serde(default, skip_serializing_if = "Vec::is_empty")]
123 pub features: Vec<f64>,
124 #[serde(default)]
126 pub is_anomaly: bool,
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub anomaly_type: Option<String>,
130 #[serde(default)]
132 pub is_aggregate: bool,
133 #[serde(default)]
135 pub aggregate_count: usize,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct CrossLayerEdge {
141 pub source_id: String,
143 pub source_layer: HypergraphLayer,
145 pub target_id: String,
147 pub target_layer: HypergraphLayer,
149 pub edge_type: String,
151 pub edge_type_code: u32,
153 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
155 pub properties: HashMap<String, Value>,
156}
157
158const DEFAULT_L1_BUDGET_DIVISOR: usize = 5;
160const DEFAULT_L3_BUDGET_DIVISOR: usize = 10;
162
163#[derive(Debug, Clone, Default, Serialize, Deserialize)]
165pub struct NodeBudget {
166 pub layer1_max: usize,
168 pub layer2_max: usize,
170 pub layer3_max: usize,
172 pub layer1_count: usize,
174 pub layer2_count: usize,
176 pub layer3_count: usize,
178}
179
180impl NodeBudget {
181 pub fn new(max_nodes: usize) -> Self {
184 let l1 = max_nodes / DEFAULT_L1_BUDGET_DIVISOR;
185 let l3 = max_nodes / DEFAULT_L3_BUDGET_DIVISOR;
186 let l2 = max_nodes - l1 - l3; Self {
188 layer1_max: l1,
189 layer2_max: l2,
190 layer3_max: l3,
191 layer1_count: 0,
192 layer2_count: 0,
193 layer3_count: 0,
194 }
195 }
196
197 pub fn can_add(&self, layer: HypergraphLayer) -> bool {
199 match layer {
200 HypergraphLayer::GovernanceControls => self.layer1_count < self.layer1_max,
201 HypergraphLayer::ProcessEvents => self.layer2_count < self.layer2_max,
202 HypergraphLayer::AccountingNetwork => self.layer3_count < self.layer3_max,
203 }
204 }
205
206 pub fn record_add(&mut self, layer: HypergraphLayer) {
208 match layer {
209 HypergraphLayer::GovernanceControls => self.layer1_count += 1,
210 HypergraphLayer::ProcessEvents => self.layer2_count += 1,
211 HypergraphLayer::AccountingNetwork => self.layer3_count += 1,
212 }
213 }
214
215 pub fn total_count(&self) -> usize {
217 self.layer1_count + self.layer2_count + self.layer3_count
218 }
219
220 pub fn total_max(&self) -> usize {
222 self.layer1_max + self.layer2_max + self.layer3_max
223 }
224
225 pub fn rebalance(&mut self, l1_demand: usize, l2_demand: usize, l3_demand: usize) {
228 let total = self.total_max();
229
230 let l1_actual = l1_demand.min(self.layer1_max);
232 let l3_actual = l3_demand.min(self.layer3_max);
233
234 let surplus = (self.layer1_max - l1_actual) + (self.layer3_max - l3_actual);
236 let l2_actual = (self.layer2_max + surplus)
237 .min(l2_demand)
238 .min(total - l1_actual - l3_actual.min(total.saturating_sub(l1_actual)));
239
240 self.layer1_max = l1_actual;
241 self.layer3_max = total.saturating_sub(l1_actual).saturating_sub(l2_actual);
242 self.layer2_max = l2_actual;
243 }
244}
245
246#[derive(Debug, Clone, Default, Serialize, Deserialize)]
248pub struct NodeBudgetReport {
249 pub total_budget: usize,
251 pub total_used: usize,
253 pub layer1_budget: usize,
255 pub layer1_used: usize,
256 pub layer2_budget: usize,
258 pub layer2_used: usize,
259 pub layer3_budget: usize,
261 pub layer3_used: usize,
262 pub aggregate_nodes_created: usize,
264 pub aggregation_triggered: bool,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct HypergraphMetadata {
271 pub name: String,
273 pub num_nodes: usize,
275 pub num_edges: usize,
277 pub num_hyperedges: usize,
279 pub layer_node_counts: HashMap<String, usize>,
281 pub node_type_counts: HashMap<String, usize>,
283 pub edge_type_counts: HashMap<String, usize>,
285 pub hyperedge_type_counts: HashMap<String, usize>,
287 pub anomalous_nodes: usize,
289 pub anomalous_hyperedges: usize,
291 pub source: String,
293 pub generated_at: String,
295 pub budget_report: NodeBudgetReport,
297 pub files: Vec<String>,
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct Hypergraph {
304 pub nodes: Vec<HypergraphNode>,
306 pub edges: Vec<CrossLayerEdge>,
308 pub hyperedges: Vec<Hyperedge>,
310 pub metadata: HypergraphMetadata,
312 pub budget_report: NodeBudgetReport,
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
321 fn test_layer_index() {
322 assert_eq!(HypergraphLayer::GovernanceControls.index(), 1);
323 assert_eq!(HypergraphLayer::ProcessEvents.index(), 2);
324 assert_eq!(HypergraphLayer::AccountingNetwork.index(), 3);
325 }
326
327 #[test]
328 fn test_node_budget_new() {
329 let budget = NodeBudget::new(50_000);
330 assert_eq!(budget.layer1_max, 10_000); assert_eq!(budget.layer2_max, 35_000); assert_eq!(budget.layer3_max, 5_000); assert_eq!(budget.total_max(), 50_000);
334 }
335
336 #[test]
337 fn test_node_budget_can_add() {
338 let mut budget = NodeBudget::new(100);
339 assert!(budget.can_add(HypergraphLayer::GovernanceControls));
340
341 for _ in 0..20 {
343 budget.record_add(HypergraphLayer::GovernanceControls);
344 }
345 assert!(!budget.can_add(HypergraphLayer::GovernanceControls));
346 assert!(budget.can_add(HypergraphLayer::ProcessEvents));
347 }
348
349 #[test]
350 fn test_node_budget_total() {
351 let mut budget = NodeBudget::new(1000);
352 budget.record_add(HypergraphLayer::GovernanceControls);
353 budget.record_add(HypergraphLayer::ProcessEvents);
354 budget.record_add(HypergraphLayer::AccountingNetwork);
355 assert_eq!(budget.total_count(), 3);
356 }
357
358 #[test]
359 fn test_hypergraph_node_serialization() {
360 let node = HypergraphNode {
361 id: "node_1".to_string(),
362 entity_type: "Account".to_string(),
363 entity_type_code: 100,
364 layer: HypergraphLayer::AccountingNetwork,
365 external_id: "1000".to_string(),
366 label: "Cash".to_string(),
367 properties: HashMap::new(),
368 features: vec![1.0, 2.0],
369 is_anomaly: false,
370 anomaly_type: None,
371 is_aggregate: false,
372 aggregate_count: 0,
373 };
374
375 let json = serde_json::to_string(&node).unwrap();
376 let deserialized: HypergraphNode = serde_json::from_str(&json).unwrap();
377 assert_eq!(deserialized.id, "node_1");
378 assert_eq!(deserialized.entity_type_code, 100);
379 assert_eq!(deserialized.layer, HypergraphLayer::AccountingNetwork);
380 }
381
382 #[test]
383 fn test_hyperedge_serialization() {
384 let he = Hyperedge {
385 id: "he_1".to_string(),
386 hyperedge_type: "JournalEntry".to_string(),
387 subtype: "R2R".to_string(),
388 participants: vec![
389 HyperedgeParticipant {
390 node_id: "acct_1000".to_string(),
391 role: "debit".to_string(),
392 weight: Some(500.0),
393 },
394 HyperedgeParticipant {
395 node_id: "acct_2000".to_string(),
396 role: "credit".to_string(),
397 weight: Some(500.0),
398 },
399 ],
400 layer: HypergraphLayer::AccountingNetwork,
401 properties: HashMap::new(),
402 timestamp: Some(NaiveDate::from_ymd_opt(2024, 6, 15).unwrap()),
403 is_anomaly: true,
404 anomaly_type: Some("split_transaction".to_string()),
405 features: vec![6.2, 1.0],
406 };
407
408 let json = serde_json::to_string(&he).unwrap();
409 let deserialized: Hyperedge = serde_json::from_str(&json).unwrap();
410 assert_eq!(deserialized.participants.len(), 2);
411 assert!(deserialized.is_anomaly);
412 }
413
414 #[test]
415 fn test_cross_layer_edge_serialization() {
416 let edge = CrossLayerEdge {
417 source_id: "ctrl_C001".to_string(),
418 source_layer: HypergraphLayer::GovernanceControls,
419 target_id: "acct_1000".to_string(),
420 target_layer: HypergraphLayer::AccountingNetwork,
421 edge_type: "ImplementsControl".to_string(),
422 edge_type_code: 40,
423 properties: HashMap::new(),
424 };
425
426 let json = serde_json::to_string(&edge).unwrap();
427 let deserialized: CrossLayerEdge = serde_json::from_str(&json).unwrap();
428 assert_eq!(deserialized.edge_type, "ImplementsControl");
429 assert_eq!(
430 deserialized.source_layer,
431 HypergraphLayer::GovernanceControls
432 );
433 assert_eq!(
434 deserialized.target_layer,
435 HypergraphLayer::AccountingNetwork
436 );
437 }
438
439 #[test]
440 fn test_aggregation_strategy_default() {
441 assert_eq!(
442 AggregationStrategy::default(),
443 AggregationStrategy::PoolByCounterparty
444 );
445 }
446}