openpay: a PEP match is recorded as a PEP match, not an OFAC sanctions hit #269

Merged
dcharlot merged 1 commit from fix/openpay-pep-not-ofac into main 2026-10-15 21:19:06 -04:00
Owner

What was wrong

op-connect's PEP index (build_pep_index_from_names) tagged every entry SanctionsList::OfacConsolidated, because the enum had nowhere to put a PEP list. The code comment said the operator surface "should not surface this".

But a PEP index is a Screener, and every screen appends source_list.label() to that screener's signed, hash-chained AuditLog. So every politically-exposed-person match was written into the tamper-evident compliance record as "OFAC Consolidated". The two findings call for different handling:

  • a sanctions match is a designation, which means block and report;
  • a PEP match is a flag for enhanced due diligence.

An adverse-media index had the same problem: it could only tag its entries with a sanctions authority.

The fix

  • SanctionsList becomes WatchList. That is the term ScreenerConfig::watchlist_filter already used.
  • Two new lists: Pep ("PEP list") and AdverseMedia ("Adverse media").
  • is_sanctions(), which is false for exactly those two.
  • Docs: the type's docs say why a match must never be recorded under a list it did not come from.
  • Compatibility: a deprecated SanctionsList alias keeps the old name compiling.
  • The PEP index now tags its entries WatchList::Pep.

Serialized forms are unchanged, because the variant names stay the same, so stored indexes still load. No published vector or doc carries the label.

Checked

Tests run on the build host:

  • op-screening and op-connect pass, including the three new tests:
    • a_pep_match_is_audited_as_a_pep_match_not_a_sanctions_match screens a PEP fixture, verifies the audit chain, and checks that the audit row names "PEP list";
    • only_the_authorities_are_sanctions_lists and every_list_has_its_own_label use an exhaustive match, so a new list fails to compile until someone classifies it.
  • Clippy reports nothing new. Its one warning, on Screener::screen (unused async), predates this change.

Each of these mutations fails a test:

  • tagging PEP entries OfacConsolidated again;
  • making every list a sanctions list;
  • labelling the PEP list "OFAC Consolidated".

🤖 Generated with Claude Code

## What was wrong op-connect's PEP index (`build_pep_index_from_names`) tagged every entry `SanctionsList::OfacConsolidated`, because the enum had nowhere to put a PEP list. The code comment said the operator surface "should not surface this". But a PEP index is a `Screener`, and every screen appends `source_list.label()` to that screener's signed, hash-chained `AuditLog`. So every politically-exposed-person match was written into the tamper-evident compliance record as **"OFAC Consolidated"**. The two findings call for different handling: - a sanctions match is a designation, which means block and report; - a PEP match is a flag for enhanced due diligence. An adverse-media index had the same problem: it could only tag its entries with a sanctions authority. ## The fix - **`SanctionsList` becomes `WatchList`.** That is the term `ScreenerConfig::watchlist_filter` already used. - **Two new lists:** `Pep` ("PEP list") and `AdverseMedia` ("Adverse media"). - **`is_sanctions()`**, which is false for exactly those two. - **Docs:** the type's docs say why a match must never be recorded under a list it did not come from. - **Compatibility:** a deprecated `SanctionsList` alias keeps the old name compiling. - **The PEP index** now tags its entries `WatchList::Pep`. Serialized forms are unchanged, because the variant names stay the same, so stored indexes still load. No published vector or doc carries the label. ## Checked Tests run on the build host: - `op-screening` and `op-connect` pass, including the three new tests: - `a_pep_match_is_audited_as_a_pep_match_not_a_sanctions_match` screens a PEP fixture, verifies the audit chain, and checks that the audit row names "PEP list"; - `only_the_authorities_are_sanctions_lists` and `every_list_has_its_own_label` use an exhaustive match, so a new list fails to compile until someone classifies it. - Clippy reports nothing new. Its one warning, on `Screener::screen` (`unused async`), predates this change. Each of these mutations fails a test: - tagging PEP entries `OfacConsolidated` again; - making every list a sanctions list; - labelling the PEP list "OFAC Consolidated". 🤖 Generated with [Claude Code](https://claude.com/claude-code)
openpay: a PEP match is recorded as a PEP match, not an OFAC sanctions hit
All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 10s
standards workspaces build + test / macOS energy backends (pull_request) Has been skipped
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m4s
standards workspaces build + test / openpay (openpay, openpay, linux-arm64) (pull_request) Successful in 6m13s
2d7de0396c
op-connect's PEP index tagged every entry `SanctionsList::OfacConsolidated`,
because the enum had no place for a PEP list. Its comment said the operator
surface "should not surface this". But a PEP index is a `Screener`, and every
screen appends `source_list.label()` to the screener's signed, hash-chained
audit log. So each PEP match was written into the tamper-evident compliance
record as "OFAC Consolidated": a sanctions designation (block, report) where
there was a due-diligence flag. An adverse-media index had the same problem:
it too could only tag its entries with a sanctions authority.

- `SanctionsList` is now `WatchList`, the term `ScreenerConfig::watchlist_filter`
  already used. It gains `Pep` ("PEP list") and `AdverseMedia` ("Adverse
  media"), and `is_sanctions()`, which is false for exactly those two. Its
  docs say why a match must never be recorded under a list it did not come
  from. A deprecated `SanctionsList` alias keeps the old name compiling.
- `build_pep_index_from_names` tags its entries `WatchList::Pep`.
- Serialized forms are unchanged: the enum's variant names stay the same, so
  stored indexes still load. No published vector or doc carries the label.

Tests:
- `a_pep_match_is_audited_as_a_pep_match_not_a_sanctions_match` screens a PEP
  fixture and checks that the audit chain verifies and that its row names
  "PEP list".
- `only_the_authorities_are_sanctions_lists` and `every_list_has_its_own_label`
  pin the kinds and labels. An exhaustive match makes a new list fail to
  compile until it is classified.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!269
No description provided.