Skip to main content

Cross-Case Pattern Detection (Pillar 3)

Automatic detection of suspicious patterns across compliance cases — entity overlap, structural motifs, temporal clusters, and risk trends.

Business Value

Individual case review misses systemic risk. Cross-Case Pattern Detection surfaces connections between cases that no single officer would see, identifying shell company networks, money laundering typologies, and coordinated fraud.

Architecture

Detectors (All Deterministic)

DetectorPatternExample
Entity OverlapSame entity across multiple casesDirector appears in 5 shell companies
Structural MotifKnown typology patternsPhoenix company, circular ownership
Temporal ClusterSuspicious timing10 companies registered same week
Risk TrendRisk score trajectoriesSector-wide risk increase

MotifInstance Persistence (#390)

Structural motifs were originally recomputed live on every read and never persisted — GraphService.upsert_motif_instance had zero callers, so the ontology store showed MotifInstance count 0 while motif alerts were surfaced to officers (a shape-vs-state metric defect). PatternEngine. detect_structural_motifs is now the single writer: each detected motif is persisted as a MotifInstance node with a deterministic instance id (sha256(motif_type | sorted identity-bearing entity keys)[:16] — registration numbers preferred over names; volatile fields like dates/addresses excluded so re-detection MERGEs into the same node, never duplicates). Edges written:

  • DEFINED_BY → the global Motif catalog node (created-if-absent, never overwriting a curated seeded entry)
  • MATCHED_BY → each involved Company (by registration number only)
  • DETECTED_IN → the Investigation whose post-ETL sweep detected it (omitted on backfill — no detection context is fabricated)

The persisted instance_id is stamped into the pattern alert's evidence_snapshot.motif_instance_id only when the write succeeded (claim-vs-check), so alerts and ontology instances reconcile. GET /graph/motifs stays live-computed for freshness; persistence is the audit record. Node props include detector_version (MOTIF_DETECTOR_VERSION, bumped with detector changes), detected_at, confidence/confidence_score, description, and the full motif dict as JSON evidence. Persistence failure never suppresses the alert. One-time idempotent backfill: scripts/backfill_motif_instances.py.

Alert Lifecycle

ACTIVE → ACKNOWLEDGED → DISMISSED (with reason) → RESOLVED

  • EU AI Act Art. 12 compliant: Immutable evidence snapshots, mandatory dismiss reasons, full audit trail
  • Deduplication: Same pattern_type + overlapping entities/cases = merged (not duplicated)

Key Components

  • pattern_engine.py — 4 detectors with configurable thresholds
  • alert_service.py — CRUD, deduplication, lifecycle management
  • pattern_alert.py — Data model with audit fields
  • intelligence.py — 6 API endpoints
  • RelatedIntelligenceCard.tsx — Case detail integration
  • intelligence/page.tsx — Intelligence Dashboard

API Endpoints

The intelligence router (app/api/intelligence.py, prefix /intelligence) is mounted under /api:

MethodPathDescription
GET/api/intelligence/alertsList alerts (filterable)
GET/api/intelligence/alerts/for-case/{workflow_id}Alerts linked to a specific case
GET/api/intelligence/alerts/{id}Get alert details
PATCH/api/intelligence/alerts/{id}Lifecycle action — acknowledge or dismiss (dismiss requires a reason, EU AI Act Art. 14)
GET/api/intelligence/statsAggregate alert / reliability statistics
GET/api/intelligence/trendsRisk trend statistics
note

Acknowledge and dismiss are a single PATCH endpoint driven by an action field, not separate POST routes. There is no separate resolve endpoint — the RESOLVED state in the lifecycle below is a model-level status, not a dedicated route.

Configuration

  • pattern_detection_enabled — Feature flag (default: true)
  • Alembic migration: 008_pattern_alerts