Packages
rbt_weave
0.2.57
0.2.67
0.2.66
0.2.65
0.2.64
0.2.63
0.2.62
0.2.61
0.2.60
0.2.59
0.2.58
0.2.57
0.2.56
0.2.55
0.2.54
0.2.53
0.2.52
0.2.51
0.2.50
0.2.49
0.2.48
0.2.47
0.2.46
0.2.45
0.2.44
0.2.43
0.2.42
0.2.41
0.2.40
0.2.39
0.2.38
0.2.37
0.2.36
0.2.35
0.2.34
0.2.33
0.2.32
0.2.31
0.2.30
0.2.29
0.2.28
0.2.27
0.2.26
0.2.25
0.2.24
0.2.23
0.2.22
0.2.21
0.2.20
0.2.19
0.2.18
0.2.17
0.2.16
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Graph conflict detection and image processing NIFs for OSINT knowledge graphs, powered by Rust.
Current section
Files
Jump to
Current section
Files
crates/weave-graph/src/patterns.rs
//! Conflict-of-interest pattern catalog.
//!
//! Patterns are defined as data structures, not hardcoded traversals.
//! New patterns can be added by appending to the catalog vectors
//! returned by [`cycle_patterns`], [`path_patterns`], and the hub
//! threshold constant.
use nulid::Nulid;
use serde::{Deserialize, Serialize};
/// How serious a detected conflict is considered.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Low,
Medium,
High,
}
/// A cycle pattern: a directed cycle whose edge types match a sequence.
#[derive(Debug, Clone)]
pub struct CyclePattern {
pub id: &'static str,
pub name: &'static str,
pub severity: Severity,
/// Edge types that must appear in order along the cycle.
/// The cycle length equals the length of this vector.
pub edge_sequence: &'static [&'static str],
}
/// A single step in a path pattern.
#[derive(Debug, Clone)]
pub struct PathStep {
pub edge_type: &'static str,
/// If `Some`, the target node must have this label.
pub target_label: Option<&'static str>,
}
/// A path pattern: a directed path whose edges and (optionally) node
/// labels match a template.
#[derive(Debug, Clone)]
pub struct PathPattern {
pub id: &'static str,
pub name: &'static str,
pub severity: Severity,
/// The label the starting node must have, or `None` for any.
pub start_label: Option<&'static str>,
pub steps: &'static [PathStep],
}
/// A single detected conflict pattern.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConflictPattern {
pub pattern_id: String,
pub pattern_name: String,
pub severity: Severity,
/// NULIDs of all involved nodes.
pub nodes: Vec<Nulid>,
/// NULIDs of all involved edges.
pub edges: Vec<Nulid>,
/// Ordered path: alternating node, edge, node, edge, ...
pub path: Vec<Nulid>,
/// Human-readable summary.
pub description: String,
}
/// Cycle-based conflict patterns.
#[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"],
},
]
}
/// Path-based conflict patterns.
#[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"),
},
],
},
]
}
/// Edge types considered "influence" edges for hub concentration.
pub const HUB_INFLUENCE_TYPES: &[&str] = &[
"PAID_TO",
"AWARDED_CONTRACT",
"APPOINTED_BY",
"FUNDED_BY",
"OWNS",
"LOBBIED",
];
/// Minimum distinct influence edge types for a hub to be flagged.
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\"");
}
}