weave-graph 0.2.37

Graph conflict detection and pattern matching for OSINT knowledge graphs
Documentation
//! 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\"");
    }
}