How to detect mule rings from the identity-link graph¶
When you need this: Your rules fire on individual customers, but a mule ring is a community — a cluster of accounts that share a phone / email / device / address / tax id / wallet and coordinate to move funds.
aml detect-mule-rings(#498) runs deterministic union-find + density community detection over the customer identity-link graph and writesmule_rings.jsonof candidate rings for an investigator to confirm.Prereqs: A finished run directory under
.artifacts/(so<run-dir>/manifest.jsonexists — it supplies the defaultas_offor a byte-deterministic re-run).engine/mule_ring.py::detect_mule_ringsis the canonical builder; the edge list comes from the sharedresolved_entity_linktable. The command is offline — it never runs in the engine path, fires an alert, or touches the audit ledger.Time: ~5 min, then as long as the ring review takes — the review is the point.
Governance up front: this command surfaces a community lens, it does not escalate. A detected ring is an investigative starting point, NOT an auto-decision — nothing here mutates a spec, fires an alert, or is hashed into the audit ledger. The detection is pure stdlib (deterministic union-find + density, no networkx / sklearn), so it is a triage surface, not a model in the regulated run path. An investigator confirms each ring before any action — that confirmation is a hard stop.
Steps¶
1 · Run the engine¶
You need a real run directory first — the command anchors its as_of to the run's own manifest.json so a re-run is deterministic.
2 · Detect mule rings over the identity graph¶
The tool loads the spec's data, builds an in-memory DuckDB warehouse the same way the engine does, lets the shared entity-resolution layer derive resolved_entity_link (customers sharing a linking attribute), then clusters dense communities. It writes mule_rings.json into the run dir (override with --output).
Tune what counts as a ring:
aml detect-mule-rings aml.yaml .artifacts/run-2026-06-05T10-15-30Z \
--min-ring-size 4 --min-density 0.6
--min-ring-size(default 3) — smallest community that can count as a ring.--min-density(default 0.5) — minimum internal-edge density (internal_edges / (size·(size-1)/2), 0–1) for a community to qualify as a ring.
Other knobs: --data-source / --data-dir (point at CSV/Parquet instead of the synthetic source), --seed, --as-of, --output / -o.
3 · Read mule_rings.json¶
The report is aggregate counts plus one entry per ring, sorted by size desc:
{
"rings": [
{
"ring_id": "MR-be9aa9c755",
"members": ["C0031", "C0032", "C0033"],
"size": 3,
"internal_edges": 3,
"density": 1.0,
"label": "3-account ring · 3 links · density 1.00"
}
],
"n_entities": 12,
"n_rings": 1
}
size is the member count; internal_edges / density tell you how tightly the community is wired (a clique has density 1.0); members are the customer ids to pull into the case.
4 · Investigate the largest / densest rings¶
The Network Explorer dashboard page (page 10) has a "Detected mule rings" section that reads this same mule_rings.json — it shows ring count, largest ring size, and a per-ring table (ring_id, size, internal_edges, density, members). Start with the largest and densest rings; each member already appears in Customer 360 and the rest of the dashboard, so you can drill straight into the accounts. Advisory: an investigator confirms the ring before action — it never auto-escalates.
Verify it worked¶
Three checks:
mule_rings.jsonexists at--output(default<run-dir>/mule_rings.json) withringssorted bysizedesc andn_rings == len(rings). The file is a proposal artifact — it is NOT one of the manifest-hashed run artifacts and is NOT on the audit chain (offline output, not run evidence).- Determinism — re-running
aml detect-mule-ringson the same spec + run dir + flags produces a byte-identical file. The clustering is pure stdlib union-find with min-id roots and content-hash ring ids, so there is nothing stochastic to drift. - No spec mutation / no alerts — the command writes only the report; it never edits the spec, fires an alert, or appends to
decisions.jsonl.
Common problems¶
| Symptom | Cause | Fix |
|---|---|---|
No identity-link edges |
The spec declares no linking attributes, or no customers share them | Add linking attributes to the customer contract / entity-resolution config; confirm the graph is non-empty |
No mule rings detected |
The communities are smaller / sparser than the thresholds | Lower --min-ring-size and/or --min-density |
| No file written | The run produced zero qualifying rings | Expected — the command writes nothing when n_rings == 0; loosen the thresholds if you want to inspect smaller communities |
| Section missing in the dashboard | You haven't run the CLI for this run dir | Run aml detect-mule-rings <spec> <run-dir>; the Network Explorer section reads <run-dir>/mule_rings.json |
Next steps¶
- How to discover candidate typologies from a run — the sibling offline lens over a run's unexplained anomalies; turns surfaced shapes into reviewable rule stubs.
- How to walk the lineage chain for a case — once you confirm a ring and open cases, trace each member's alert back to source rows.
- Dashboard: the Network Explorer page (page 10) renders the same rings plus the temporal-correlation graph, so you can see the identity community and the flow community side by side.