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)
| Detector | Pattern | Example |
|---|---|---|
| Entity Overlap | Same entity across multiple cases | Director appears in 5 shell companies |
| Structural Motif | Known typology patterns | Phoenix company, circular ownership |
| Temporal Cluster | Suspicious timing | 10 companies registered same week |
| Risk Trend | Risk score trajectories | Sector-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 globalMotifcatalog node (created-if-absent, never overwriting a curated seeded entry)MATCHED_BY→ each involvedCompany(by registration number only)DETECTED_IN→ theInvestigationwhose 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 thresholdsalert_service.py— CRUD, deduplication, lifecycle managementpattern_alert.py— Data model with audit fieldsintelligence.py— 6 API endpointsRelatedIntelligenceCard.tsx— Case detail integrationintelligence/page.tsx— Intelligence Dashboard
API Endpoints
The intelligence router (app/api/intelligence.py, prefix /intelligence) is mounted under /api:
| Method | Path | Description |
|---|---|---|
| GET | /api/intelligence/alerts | List 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/stats | Aggregate alert / reliability statistics |
| GET | /api/intelligence/trends | Risk trend statistics |
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