Skip to content

docs(resilience): PR 5.1 — sanctions construct audit (designated-party domicile question)#3375

Merged
koala73 merged 2 commits into
mainfrom
fix/resilience-pr51-sanctions-audit
Apr 24, 2026
Merged

docs(resilience): PR 5.1 — sanctions construct audit (designated-party domicile question)#3375
koala73 merged 2 commits into
mainfrom
fix/resilience-pr51-sanctions-audit

Conversation

@koala73

@koala73 koala73 commented Apr 24, 2026

Copy link
Copy Markdown
Owner

Summary

PR 5.1 of the cohort-audit plan (docs/plans/2026-04-24-002-fix-resilience-cohort-ranking-structural-audit-plan.md). Read-only static audit of scoreTradeSanctions + sanctions:country-counts:v1, framed around the Codex-reformulated construct question: should designated-party domicile count penalize resilience?

⚠ Stacked on #3374 (PR 5.3), which is stacked on #3373 (PR 5.2). Merge order: #3373#3374 → this. Ships one methodology-doc extension + 10 regression-guard tests. No scorer behaviour changes.

Finding

The count in sanctions:country-counts:v1 is "OFAC-designated-party domicile locations", NOT "sanctions against this country." The seeder (scripts/seed-sanctions-pressure.mjs:85-93) parses OFAC Advanced XML SDN + Consolidated lists, extracts each designated party's Locations, and increments map[countryCode] by 1 for every location country on that party.

That count conflates three semantically distinct categories a resilience construct might want to handle differently:

Category Example Current penalty Construct question
(a) Country-level sanction NK SDN listings +1 per entity inside the sanctioned state Intended — resilience genuinely degraded by comprehensive sanctions
(b) Domiciled sanctioned entity RU bank HQ'd in Moscow (post-2022) +1 per listing Debatable — country hosts the actor
(c) Transit / shell entity UAE trading co designated under SDGT for Iran evasion; CY SPV for a Russian oligarch +1 per listing even when the country is NOT the target Country is a financial hub; debatable whether its resilience suffers

Observed cohort impact: AE=54 vs KW/QA=82 on tradeSanctions. The −28 gap is almost entirely driven by category (c) — AE is a financial hub where sanctioned parties incorporate shells.

Options documented (not decided here)

Option Rationale Cost
1. Keep flat count (status quo) Secondary-sanctions + correspondent-banking isolation + FATF grey-listing IS real resilience risk even for transit hubs Countries whose policy is NOT what earned the count carry the penalty
2. Program-weighted count (recommended) Programs encode designation nature: DPRK/IRAN/SYRIA = country-comprehensive; SDGT/SDNTK/CYBER/GLOMAG = entity-specific. Weight cat-(a) at 1.0, cat-(c)-ish at 0.3-0.5 via named mapping Needs a program→category manifest; program codes evolve. But the data is already collected — seeder captures programs per entry (line 95-108)
3. Transit-hub exclusion list Hard-excludes AE, SG, HK, CY, VG, KY from domicile-count signal Brittle + normative; who gets on the list decides who "wins" the scoring change

Recommendation: Option 2. Implementation deferred to a separate methodology-decision PR.

What shipped

  • docs/methodology/known-limitations.md — new section extending the PR 5.2/5.3 file. Full section covers: what the count represents, the three categories with examples, observed GCC impact, three options with trade-offs + recommendation, follow-up entity-sample audit (gated on API-key access), file references.
  • tests/resilience-sanctions-field-mapping.test.mts (new) — 10 regression-guard tests:
    • normalizeSanctionCount piecewise anchors pinned: count=0→100, 1→90, 10→75, 50→50, 200→25, 500→≤1
    • Monotonicity: strictly decreasing across the ramp
    • Country absent from map defaults to count=0 → score 100 (intentional)
    • Seed outage (raw=null) → null score slot, NOT imputed (protects against silent data-outage scoring)
    • Construct anchor: count=1 is exactly 10 points below count=0

Testing

  • npx tsx --test tests/resilience-sanctions-field-mapping.test.mts10 pass / 0 fail
  • npm run test:data6721 pass / 0 fail (across the full stacked branch)
  • npm run typecheck / typecheck:api — green
  • npm run lint / lint:md — clean

What this PR explicitly does NOT do

  • No construct decision. Which of Options 1/2/3 to ship is a methodology judgment belonging in a separate PR with cohort snapshots + rubric discussion. This PR only surfaces the question.
  • No scorer changes. Pure docs + tests. Zero live-ranking movement.

Post-Deploy Monitoring & Validation

No additional operational monitoring required: pure docs + tests. Zero behaviour change, no endpoints/crons/flags/Redis writes touched.

Follow-up (not in this PR)

  • Entity-sample audit: 10 OFAC entries per country for AE, HK, SG, CY, TR, RU, IR, US classified into categories (a)/(b)/(c). Produces calibration data for an Option-2 program-weight mapping. Requires API key / Redis read access.
  • Construct-decision PR: chooses one of Options 1/2/3, reports before/after cohort snapshots, updates the acceptance tests.

Related


🤖 Generated with Claude Opus 4.7 (1M context) via Claude Code + Compound Engineering v2.49.0

Co-Authored-By: Claude Opus 4.7 (1M context) [email protected]

@vercel

vercel Bot commented Apr 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment Apr 24, 2026 2:31pm

Request Review

@greptile-apps

greptile-apps Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This is a pure docs + regression-tests PR (no scorer behaviour changes) that documents the sanctions:country-counts:v1 construct ambiguity — specifically that the flat designated-party-domicile count conflates country-level sanctions targets, domiciled sanctioned entities, and transit/shell-entity listings — and pins the current normalizeSanctionCount piecewise scale with 10 guard tests.

Two minor issues in the test file: the count=500 assertion uses the loose bound <= 1 rather than === 0 (the calculated value is provably 0), and a helper-function comment incorrectly says other slots "contribute zero weight" when they contribute 0.55 to totalWeight (affecting coverage arithmetic).

Confidence Score: 5/5

Safe to merge — pure docs and regression-guard tests with no scorer behaviour changes and no live-ranking impact.

All findings are P2 style/comment issues. The test logic is correct, assertions match the scorer implementation, and the documentation accurately describes the current behaviour and design trade-offs. No P0 or P1 issues found.

No files require special attention beyond the two minor P2 comments on the test file.

Important Files Changed

Filename Overview
tests/resilience-sanctions-field-mapping.test.mts New 139-line regression-guard test file for scoreTradeSanctions; all 10 tests correctly exercise the piecewise normalizeSanctionCount anchors, field-mapping defaults, and outage semantics — one assertion bound is loose (<= 1 instead of === 0) and one helper comment slightly mischaracterises coverage arithmetic.
docs/methodology/known-limitations.md Adds a well-structured 148-line methodology section documenting the designated-party-domicile construct question for tradeSanctions, the three-category taxonomy, observed GCC cohort impact, three scoring options with trade-offs, and a recommendation; minor inconsistency where the regression-test anchors table lists count=500 → ≤ 0 while the test assertion uses <= 1.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[sanctionCount] --> B{count == 0?}
    B -- yes --> C[return 100]
    B -- no --> D{count <= 10?}
    D -- yes --> E["return round(90 − (count−1)×1.667)\nrange: 90→75"]
    D -- no --> F{count <= 50?}
    F -- yes --> G["return round(75 − (count−10)×0.625)\nrange: 75→50"]
    F -- no --> H{count <= 200?}
    H -- yes --> I["return round(50 − (count−50)×0.167)\nrange: 50→25"]
    H -- no --> J["return round(max(0, 25 − (count−200)×0.1))\nrange: 25→0 clamped"]

    style C fill:#2d6a4f,color:#fff
    style J fill:#d62828,color:#fff
Loading

Reviews (1): Last reviewed commit: "docs(resilience): PR 5.1 — sanctions con..." | Re-trigger Greptile

Comment on lines +94 to +99
});

describe('scoreTradeSanctions — field-mapping + outage semantics', () => {
it('country absent from sanctions map defaults to count=0 (score 100)', async () => {
// The map is ISO2 → count. A country NOT in the map is semantically
// "no designated parties located here" — NOT "data missing". The

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Loose assertion bound may allow silent scorer regression

The comment confirms the exact computed value is 0 (25 - 30 = -5 → clamped to 0), but the assertion uses <= 1. If a future scorer change produces 0.5 at count=500 (e.g., from a changed roundScore implementation or a boundary-condition tweak), this test still passes even though the behaviour has shifted. For a regression-guard test, pinning the exact expected value is more protective.

Suggested change
});
describe('scoreTradeSanctions — field-mapping + outage semantics', () => {
it('country absent from sanctions map defaults to count=0 (score 100)', async () => {
// The map is ISO2 → count. A country NOT in the map is semantically
// "no designated parties located here" — NOT "data missing". The
// At count=500: 25 - (500-200)*0.1 = 25 - 30 = -5 → clamped to 0.
assert.equal(result.score, 0,
`expected 0 at count=500 (heavily-sanctioned state, clamped); got ${result.score}`);

Comment on lines +45 to +48
// count=11-50 → 75..50 (linear)
// count=51-200 → 50..25 (linear)
// count=201+ → 25..0 (linear at 0.1/step, clamped 0)
//

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Misleading comment on "zero weight" slot behaviour

The comment says other slots "contribute zero weight", but they actually contribute their full declared weight (0.15 + 0.15 + 0.25 = 0.55) to totalWeight, which is the denominator for the coverage calculation in weightedBlend. Only the availableWeight (score-computation denominator) drops to 0.45. A reader following this comment might incorrectly conclude that coverage will equal 1.0 for the sanctions-only case, whereas it is actually 0.45. The phrasing "contribute zero weight to the blended score" would be more precise.

@koala73
koala73 force-pushed the fix/resilience-pr53-foodwater-audit branch from 7de0630 to 83d47f9 Compare April 24, 2026 13:58
koala73 added a commit that referenced this pull request Apr 24, 2026
… weightedBlend weights

Addresses 2 P2 Greptile findings on #3375:

1. Tighten count=500 assertion. Was `<= 1` with a comment stating the
   exact value is 0. That loose bound silently tolerates roundScore /
   boundary drift that would be the very signal this regression guard
   exists to catch. Changed to strict equality `=== 0`.

2. Clarify the "zero weight" comment on the sanctions-only harness.
   The other slots DO contribute their declared weights (0.15 +
   0.15 + 0.25 = 0.55) to weightedBlend's `totalWeight` denominator —
   only `availableWeight` (the score-computation denominator) drops
   to 0.45 because their score is null. The previous comment elided
   this distinction and could mislead a reader into thinking the null
   slots contributed nothing at all. Expanded to state exactly how
   `coverage` and `score` each behave.

Verified

- `npx tsx --test tests/resilience-sanctions-field-mapping.test.mts`
  — 10 pass / 0 fail (count=500 now pins the exact 0 floor)
@koala73
koala73 force-pushed the fix/resilience-pr51-sanctions-audit branch from c0ba53e to ecc5033 Compare April 24, 2026 14:00
@koala73
koala73 force-pushed the fix/resilience-pr53-foodwater-audit branch from 83d47f9 to fa08998 Compare April 24, 2026 14:17
koala73 added a commit that referenced this pull request Apr 24, 2026
… weightedBlend weights

Addresses 2 P2 Greptile findings on #3375:

1. Tighten count=500 assertion. Was `<= 1` with a comment stating the
   exact value is 0. That loose bound silently tolerates roundScore /
   boundary drift that would be the very signal this regression guard
   exists to catch. Changed to strict equality `=== 0`.

2. Clarify the "zero weight" comment on the sanctions-only harness.
   The other slots DO contribute their declared weights (0.15 +
   0.15 + 0.25 = 0.55) to weightedBlend's `totalWeight` denominator —
   only `availableWeight` (the score-computation denominator) drops
   to 0.45 because their score is null. The previous comment elided
   this distinction and could mislead a reader into thinking the null
   slots contributed nothing at all. Expanded to state exactly how
   `coverage` and `score` each behave.

Verified

- `npx tsx --test tests/resilience-sanctions-field-mapping.test.mts`
  — 10 pass / 0 fail (count=500 now pins the exact 0 floor)
@koala73
koala73 force-pushed the fix/resilience-pr51-sanctions-audit branch from ecc5033 to e9458b1 Compare April 24, 2026 14:18
Base automatically changed from fix/resilience-pr53-foodwater-audit to main April 24, 2026 14:26
koala73 added 2 commits April 24, 2026 18:26
…y domicile question)

PR 5.1 of cohort-audit plan 2026-04-24-002. Stacked on PR 5.3 (#3374)
so the known-limitations.md section append is additive. Read-only
static audit of scoreTradeSanctions + the sanctions:country-counts:v1
seed — framed around the Codex-reformulated construct question:
should designated-party domicile count penalize resilience?

Findings

1. The count is "OFAC-designated-party domicile locations," NOT
   "sanctions against this country." Seeder (`scripts/seed-sanctions-
   pressure.mjs:85-93`) parses OFAC Advanced XML SDN + Consolidated,
   extracts each designated party's Locations, and increments
   `map[countryCode]` by 1 for every location country on that party.

2. The count conflates three semantically distinct categories a
   resilience construct might treat differently:

   (a) Country-level sanction target (NK SDN listings) — correct penalty
   (b) Domiciled sanctioned entity (RU bank in Moscow, post-2022) —
       debatable, country hosts the actor
   (c) Transit / shell entity (UAE trading co listed under SDGT for
       Iran evasion; CY SPV for a Russian oligarch) — country is NOT
       the target, but takes the penalty

3. Observed GCC cohort impact: AE scores 54 vs KW/QA 82. The −28 gap
   is almost entirely driven by category (c) listings — AE is a
   financial hub where sanctioned parties incorporate shells.

4. Three options documented for the construct decision (NOT decided
   in this PR):
   - Option 1: Keep flat count (status quo, defensible via secondary-
     sanctions / FATF argument)
   - Option 2: Program-weighted count — weight DPRK/IRAN/SYRIA/etc.
     at 1.0, SDGT/SDNTK/CYBER/etc. at 0.3-0.5. Recommended; seeder
     already captures `programs` per entry — data is there, scorer
     just doesn't read it.
   - Option 3: Transit-hub exclusion list (AE, SG, HK, CY, VG, KY) —
     brittle + normative, not recommended

5. Recommendation documented: Option 2. Implementation deferred to
   a separate methodology-decision PR (outside auto-mode authority).

Shipped

- `docs/methodology/known-limitations.md` — new section extending
  the file: "tradeSanctions — designated-party domicile construct
  question." Covers what the count represents, the three categories
  with examples, observed GCC impact, three options w/ trade-offs,
  recommendation, follow-up audit list (entity-sample gated on
  API-key access), and file references.
- `tests/resilience-sanctions-field-mapping.test.mts` (new) —
  10 regression-guard tests pinning CURRENT behavior:
    1-6. normalizeSanctionCount piecewise anchors:
       count=0→100, 1→90, 10→75, 50→50, 200→25, 500→≤1
    7. Monotonicity: strictly decreasing across the ramp
    8. Country absent from map defaults to count=0 → score 100
       (intentional "no designated parties here" semantics)
    9. Seed outage (raw=null) → null score slot, NOT imputed
       (protects against silent data-outage scoring)
    10. Construct anchor: count=1 is exactly 10 points below count=0
        (pins the "first listing drops 10" design choice)

Verified

- `npx tsx --test tests/resilience-sanctions-field-mapping.test.mts` — 10 pass / 0 fail
- `npm run test:data` — 6721 pass / 0 fail
- `npm run typecheck` / `typecheck:api` — green
- `npm run lint` / `lint:md` — clean
… weightedBlend weights

Addresses 2 P2 Greptile findings on #3375:

1. Tighten count=500 assertion. Was `<= 1` with a comment stating the
   exact value is 0. That loose bound silently tolerates roundScore /
   boundary drift that would be the very signal this regression guard
   exists to catch. Changed to strict equality `=== 0`.

2. Clarify the "zero weight" comment on the sanctions-only harness.
   The other slots DO contribute their declared weights (0.15 +
   0.15 + 0.25 = 0.55) to weightedBlend's `totalWeight` denominator —
   only `availableWeight` (the score-computation denominator) drops
   to 0.45 because their score is null. The previous comment elided
   this distinction and could mislead a reader into thinking the null
   slots contributed nothing at all. Expanded to state exactly how
   `coverage` and `score` each behave.

Verified

- `npx tsx --test tests/resilience-sanctions-field-mapping.test.mts`
  — 10 pass / 0 fail (count=500 now pins the exact 0 floor)
@koala73
koala73 force-pushed the fix/resilience-pr51-sanctions-audit branch from e9458b1 to 435da34 Compare April 24, 2026 14:27
@koala73
koala73 merged commit b4198a5 into main Apr 24, 2026
10 of 11 checks passed
@koala73
koala73 deleted the fix/resilience-pr51-sanctions-audit branch April 24, 2026 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant