Skip to content

feat(resilience): rename tradeSanctions → tradePolicy + drop OFAC component (Ship 1)#3405

Merged
koala73 merged 2 commits into
mainfrom
feat/trade-policy-rename
Apr 25, 2026
Merged

feat(resilience): rename tradeSanctions → tradePolicy + drop OFAC component (Ship 1)#3405
koala73 merged 2 commits into
mainfrom
feat/trade-policy-rename

Conversation

@koala73

@koala73 koala73 commented Apr 25, 2026

Copy link
Copy Markdown
Owner

Summary

Plan 2026-04-25-004 Phase 1 (Ship 1). Renames the tradeSanctions resilience dimension to tradePolicy and drops the OFAC sanctionCount component (was weight 0.45). The remaining 3 components are reweighted to total 1.0:

  • WTO restrictions count → 0.30 (was 0.15)
  • WTO barriers count → 0.30 (was 0.15)
  • Applied tariff rate → 0.40 (was 0.25)

Why drop the OFAC component? Counting designated-party domicile locations is a corporate-finance liability metric, not a country-resilience indicator. A transit-hub like UAE or Singapore hosts many shell-company entries without that reflecting on host-country structural resilience. UAE's −28-point gap vs Kuwait/Qatar in the 2026-04-24 cohort audit was almost entirely driven by Iran-evasion shell entities and Russian-asset SPVs.

Phase 2 (Ship 2 — separate PR) will add a new financialSystemExposure dim built from BIS Locational Banking Statistics by-parent cross-border claims + World Bank IDS short-term external debt (% GNI) + FATF AML/CFT listing status. This replaces the dropped sanctions signal with a structural-exposure construct that doesn't conflate transit-hub corporate domicile with host-country risk. See plan §Phase 2 for the full design.

What changes

Cache prefixes bumped in lockstep

  • resilience:score:v12:v13:
  • resilience:ranking:v12v13
  • resilience:history:v7:v8:

Pre-bump entries serve under the old (sanctions-included) formula for the full TTL window unless the prefix rotates; bumping forces a clean slate. The history bump is especially important — without it, pre/post-rename history points would mix inside the rolling 30-day window and produce false-negative "falling" trends across the deploy.

Files touched (31 files)

  • Server: _dimension-scorers.ts (union, weights, domains, order, types, scorer fn body, retired constant), _indicator-registry.ts (3 entries renamed + sanctionCount dropped), _shared.ts (3 cache prefix constants), _source-failure.ts (adapter→dim mapping)
  • Mirrors: scripts/seed-resilience-scores.mjs, scripts/{audit,backtest,benchmark,validate-correlation,validate-backtest,compare-current-vs-proposed}-resilience-*.mjs, api/health.js — every literal v12/v7 site migrated
  • Frontend: src/components/resilience-widget-utils.ts label map
  • Tests: tests/resilience-sanctions-field-mapping.test.mts deleted; replaced by new tests/resilience-trade-policy-formula.test.mts pinning the 3-component formula contract. 9 other test files updated for the new identifiers + numerics.
  • Docs: docs/methodology/country-resilience-index.mdx (heading "Trade & Sanctions" → "Trade Policy" + new weights table), indicator-sources.yaml (3 entries renamed + reweighted, sanctionCount entry removed), known-limitations.md (construct-question section replaced with resolution pointer to this plan).

The OFAC seeder (scripts/seed-sanctions-pressure.mjs) continues to write sanctions:country-counts:v1 — that key is still consumed by country brief generation and ad-hoc analysis. Only the resilience scorer's binding was removed.

Test plan

  • npm run typecheck passes (zero errors)
  • npm run typecheck:api passes (zero errors after deleting unused RESILIENCE_SANCTIONS_KEY and normalizeSanctionCount)
  • npm run test:data7101/7101 tests pass, including:
    • tests/resilience-cache-keys-health-sync.test.mts — parity test that pins every mirror site to the canonical v13 score / v13 ranking / v8 history prefix
    • tests/resilience-trade-policy-formula.test.mts — 5 new tests pinning the 3-component formula (sanctions key not read; weights total 1.0; full-data best-case 100; full-data worst-case 0; total seed outage coverage 0)
    • tests/resilience-methodology-lint.test.mts — heading "Trade Policy" → dim id tradePolicy
    • tests/resilience-dimension-scorers.test.mts — full set of scoreTradePolicy paths, including non-reporter IMPUTE blend and tariff-only mode
  • node --test tests/edge-functions.test.mjs — 177/177 pass
  • Edge-bundle check (esbuild --bundle for every api/*.js) clean
  • npm run lint (biome) — exit 0
  • npm run lint:md — exit 0
  • npm run version:check — exit 0
  • Repo-wide grep audit: zero non-comment tradeSanctions / scoreTradeSanctions hits; zero non-comment resilience:(score|ranking):v12 / resilience:history:v7 literals

Post-Deploy Monitoring & Validation

  • What to monitor/search
    • Logs (Vercel): [resilience] invocations after deploy — expect compute-on-miss for v13 score keys until the seeder warms them
    • Metrics: /api/health resilienceRanking registered key resilience:ranking:v13
  • Validation checks
    • redis-cli GET 'resilience:score:v13:US' returns a populated record after seed-bundle-resilience cron runs
    • redis-cli GET 'resilience:ranking:v13' exists and has _formula tag matching production
  • Expected healthy behavior
    • First 6h: edge handler computes-on-miss for v13 score keys (no warmed cache yet); per-country GET avg latency rises modestly until warm
    • After cron: ranking + 222 country score keys populated under v13 prefix; latency returns to baseline
  • Failure signal / rollback trigger
    • /api/health reports resilience:ranking:v13 STALE > 12h after merge → Railway cron failing; check seed-bundle-resilience logs and re-deploy seeder
    • Spike in 5xx on /api/resilience/ranking immediately post-deploy → revert the PR; v12 reads still work since we didn't delete the seeder key
  • Validation window & owner
    • Window: first 12h after merge
    • Owner: Elie

Cohort-audit rollout (per plan §Phase 1 Rollout)

After merge, perform the bounded-movement gate audit per the methodology-invariant pattern from plan 2026-04-25-001:

  1. Pre-merge: snapshot resilience:score:v12:* for all 222 countries
  2. Post-merge: flush v12 caches, run seed-bundle-resilience, snapshot resilience:score:v13:*
  3. Compute |Δ| per country
  4. Gate: at least 80% of countries should have |Δ| < 1.5 points; outliers with |Δ| ≥ 5 must be in the tradeSanctions-meaningfully-affected set (high OFAC count or tariff-data noise) and explained in the cohort-audit doc
  5. Sanity-check anchors: Russia / Iran / DPRK should remain low-resilience (their tradePolicy may rise modestly because the OFAC component is removed, but other low-resilience signals — currencyExternal, fiscalSpace, liquidReserveAdequacy — hold the bottom rank)

Snapshot will be committed to docs/snapshots/resilience-cohort-audit-post-tradepolicy-2026-04-25.md.

Informational (not gates)

  • AE: tradeSanctions 54 → tradePolicy likely 70-80 range (the removed sanctions component was the drag)
  • KW: tradeSanctions 82 → tradePolicy likely similar (Kuwait's trade-policy openness is genuinely close to UAE's)
  • AE-KW gap on the renamed dim: likely closes from −28 to ~0 to −10
  • Russia / Iran / DPRK overall-score Δ: likely +1 to +3 points

🤖 Generated with Claude Opus 4.7 (1M context, extended thinking) via Claude Code + Compound Engineering v3.0.0

Compound Engineering v3.0.0

…ponent (Ship 1)

Renames the `tradeSanctions` resilience dimension to `tradePolicy` and
drops the OFAC `sanctionCount` component (was weight 0.45). Counting
designated-party domicile locations is a corporate-finance liability
metric, not a country-resilience indicator — a transit-hub like UAE or
Singapore hosts many shell-company entries without that reflecting on
host-country structural resilience. The remaining 3 components are
reweighted to total 1.0 (WTO restrictions 0.30 + WTO barriers 0.30 +
applied tariff rate 0.40).

Cache prefixes bumped in lockstep:
  - resilience:score:v12: → v13:
  - resilience:ranking:v12 → v13
  - resilience:history:v7: → v8:

The OFAC seeder (scripts/seed-sanctions-pressure.mjs) continues to
write `sanctions:country-counts:v1` — that key is still consumed by
country brief generation and ad-hoc analysis. Only the resilience
scorer's binding was removed.

Plan 2026-04-25-004 Phase 1 (Ship 1). Phase 2 (Ship 2) will add a new
`financialSystemExposure` dim built from BIS Locational Banking
Statistics by-parent cross-border claims + WB IDS short-term external
debt (% GNI) + FATF AML/CFT listing status, replacing the dropped
sanctions signal with a structural-exposure construct that doesn't
conflate transit-hub corporate domicile with host-country risk.

🤖 Generated with Claude Opus 4.7 (1M context, extended thinking) via [Claude Code](https://claude.com/claude-code) + Compound Engineering v3.0.0

Co-Authored-By: Claude Opus 4.7 (1M context, extended thinking) <[email protected]>
@mintlify

mintlify Bot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
WorldMonitor 🟢 Ready View Preview Apr 25, 2026, 2:28 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@vercel

vercel Bot commented Apr 25, 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 25, 2026 2:40pm

Request Review

@greptile-apps

greptile-apps Bot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Renames the tradeSanctions resilience dimension to tradePolicy, drops the OFAC sanctionCount component (weight 0.45), and reweights the remaining 3 components (WTO restrictions 0.30, WTO barriers 0.30, applied tariff 0.40) to total 1.0. Cache prefixes are bumped in lockstep (score:v12→v13, ranking:v12→v13, history:v7→v8) across all 31 touched files, and the replacement formula test suite thoroughly pins the new contract.

Confidence Score: 4/5

Safe to merge; all cache versions bumped consistently, formula weights total 1.0, and test coverage is comprehensive — only a stale comment needs fixing.

Only P2 finding: an inaccurate comment in scoreTradePolicy that describes RESILIENCE_SANCTIONS_KEY and normalizeSanctionCount as 'retained' when both are deleted in this same PR. No logic, correctness, or data-safety concerns found.

server/worldmonitor/resilience/v1/_dimension-scorers.ts — stale comment at lines 1071-1074 only

Important Files Changed

Filename Overview
server/worldmonitor/resilience/v1/_dimension-scorers.ts Renames scoreTradeSanctions → scoreTradePolicy, drops OFAC component, reweights 3 remaining components to 1.0 (0.30/0.30/0.40); stale comment on lines 1071-1074 incorrectly describes removed constants as "retained"
server/worldmonitor/resilience/v1/_shared.ts Cache prefixes correctly bumped: score v12→v13, ranking v12→v13, history v7→v8, with well-documented rationale for each
server/worldmonitor/resilience/v1/_indicator-registry.ts Drops sanctionCount indicator entry (weight 0.45), renames 3 remaining indicators from tradeSanctions to tradePolicy dimension with corrected weights (0.30/0.30/0.40)
server/worldmonitor/resilience/v1/_source-failure.ts Correctly updates appliedTariffRate mapping from tradeSanctions → tradePolicy; no orphaned entries
tests/resilience-trade-policy-formula.test.mts New test file pinning the 3-component formula: confirms OFAC key not read, best-case=100, worst-case=0, weights sum to 1.0, and total outage produces coverage=0
tests/resilience-sanctions-field-mapping.test.mts Deleted — correctly retired alongside normalizeSanctionCount and the OFAC component; replaced by resilience-trade-policy-formula.test.mts
scripts/seed-resilience-scores.mjs Score prefix and ranking key bumped from v12 to v13 in lockstep with _shared.ts; seeder and server agree on cache keys
api/health.js Single-line update: resilienceRanking key bumped from v12 to v13, consistent with other changes
tests/resilience-scorers.test.mts Numeric anchors updated to reflect tradePolicy reweight (US economic domain 66.33→71, overallScore 64.39→65.24, stressScore 67.21→69.01); commented rationale included

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph OLD["tradeSanctions (v12)"]
        S1[OFAC sanctionCount\nweight 0.45]
        S2[WTO restrictions count\nweight 0.15]
        S3[WTO barriers count\nweight 0.15]
        S4[Applied tariff rate\nweight 0.25]
    end

    subgraph NEW["tradePolicy (v13)"]
        N1[WTO restrictions count\nweight 0.30]
        N2[WTO barriers count\nweight 0.30]
        N3[Applied tariff rate\nweight 0.40]
    end

    S1 -- DROPPED --> X([Removed - OFAC domicile count - not a resilience signal])
    S2 -- reweighted --> N1
    S3 -- reweighted --> N2
    S4 -- reweighted --> N3

    N1 & N2 & N3 --> BLEND[weightedBlend - total weight = 1.0]
    BLEND --> OUT[ResilienceDimensionScore - cached under resilience:score:v13:]
Loading

Reviews (1): Last reviewed commit: "feat(resilience): rename tradeSanctions ..." | Re-trigger Greptile

Comment on lines +1071 to +1074
// `RESILIENCE_SANCTIONS_KEY` and `normalizeSanctionCount` are retained
// (no longer read here) pending plan 2026-04-25-004 Phase 2, which adds the
// `financialSystemExposure` dim built from BIS LBS + WB IDS + FATF status —
// a structural-exposure construct that does not rely on the OFAC count.

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 Inaccurate comment: constants marked "retained" but actually removed

Lines 1071–1074 say RESILIENCE_SANCTIONS_KEY and normalizeSanctionCount "are retained (no longer read here) pending plan 2026-04-25-004 Phase 2", but both are deleted in this same PR. The constant's own retirement block (above, ~line 262) and the function's replacement comment both correctly describe the removal. A developer searching for RESILIENCE_SANCTIONS_KEY and landing here will think it exists somewhere in the file when it doesn't.

Suggested change
// `RESILIENCE_SANCTIONS_KEY` and `normalizeSanctionCount` are retained
// (no longer read here) pending plan 2026-04-25-004 Phase 2, which adds the
// `financialSystemExposure` dim built from BIS LBS + WB IDS + FATF status —
// a structural-exposure construct that does not rely on the OFAC count.
// `RESILIENCE_SANCTIONS_KEY` ('sanctions:country-counts:v1') and
// `normalizeSanctionCount` were REMOVED in plan 2026-04-25-004 Phase 1 to
// avoid TS6133 unused-local errors. If Phase 2's `financialSystemExposure`
// dim re-introduces a sanctions signal, re-add them there.

P2: scoreTradePolicy header comment claimed RESILIENCE_SANCTIONS_KEY
and normalizeSanctionCount were "retained" but both were actually
removed during the typecheck pass (TS6133). Replaced with an accurate
one-line pointer to the retire-tag locations at line ~263 (constant)
and line ~542 (helper).

P3: Added a positive-coverage assertion to the formula test —
"DOES read every expected component seed key" — to defend against
accidental component drops in the opposite direction. Symmetric to
the existing "does NOT read sanctions:country-counts:v1" guard.

P3: Cross-linked the methodology doc's "Trade Policy" section to the
resolved-construct paragraph in known-limitations.md so a reader
curious about the dropped sanctions component lands on the rationale
in one click.

🤖 Generated with Claude Opus 4.7 (1M context, extended thinking) via [Claude Code](https://claude.com/claude-code) + Compound Engineering v3.0.0

Co-Authored-By: Claude Opus 4.7 (1M context, extended thinking) <[email protected]>
@koala73
koala73 merged commit c354ee5 into main Apr 25, 2026
11 checks passed
@koala73
koala73 deleted the feat/trade-policy-rename branch April 25, 2026 16:15
koala73 added a commit that referenced this pull request Apr 25, 2026
…2 — flag-gated dark)

Adds the new `financialSystemExposure` resilience dimension introduced in
plan 2026-04-25-004 Phase 2, behind the
`RESILIENCE_FIN_SYS_EXPOSURE_ENABLED` env flag. The flag defaults OFF so
the dim ships dark — the scorer returns the empty-data shape (score=0,
coverage=0, imputationClass=null) until the 3 component seeders
(seed-bis-lbs, seed-fatf-listing, seed-wb-external-debt) are populating
in production. Rollout pattern matches energy v2 (plan
2026-04-24-001).

Components (weights total 1.0 inside the dim):
  short_term_external_debt_pct_gni    0.35  (WB IDS, lowerBetter, 15-0)
  bis_lbs_xborder_us_eu_uk_pct_gdp    0.30  (BIS LBS by-parent, U-shape)
  fatf_listing_status                  0.20  (FATF, discrete: black=0, gray=30, compliant=100)
  financial_center_redundancy          0.15  (BIS LBS by-parent count, higherBetter, 1-10)

When the flag flips ON, the scorer preflights all 3 required seed-meta
envelopes (the BIS LBS seed serves both Component 2 and Component 4 —
no separate Component 4 seeder). Missing envelopes throw
`ResilienceConfigurationError(message, missingKeys)` (two-arg form per
Codex R3 P1 #2) which `scoreAllDimensions` catches and routes to
`imputationClass='source-failure'`. Per-country data gaps inside an
otherwise-published envelope are distinct: per-component reads return
null, the slot drops out of the weighted blend.

Cache prefixes bumped in lockstep with the new dim:
  resilience:score:v13:    → v14:
  resilience:ranking:v13   → v14
  resilience:history:v8:   → v9:

The scaffold also reweights `tradePolicy` 1.0 → 0.5 in
RESILIENCE_DIMENSION_WEIGHTS (the new dim shares the economic-domain
weight). This shifts the headline overallScore by ~0.46 points in the
flag-off baseline because the half-weighted tradePolicy contributes
proportionally less to the coverage-weighted economic-domain mean.
When the flag flips on with seeders populated, the new dim's actual
signal will rebalance the headline score.

What this PR ships:
  - new `scoreFinancialSystemExposure` + `normalizeBandLowerBetter` U-shape helper
  - 4 new INDICATOR_REGISTRY entries (BIS-derived tagged non-commercial/enrichment per Codex R1 #8)
  - api/health.js + api/seed-health.js dual-registry entries for the 3 new seed keys
  - frontend label map + ResilienceWidget "20 dimensions" copy
  - methodology doc heading "Financial System Exposure" + indicator table
  - tests/resilience-financial-system-exposure.test.mts: 13 tests pinning the
    formula contract + fail-closed preflight + flag-off rollout posture
  - parity test extension for v14/v9 prefixes
  - 22-dim count update across release-gate + handlers + indicator-registry tests
  - flag-gated-dark exemption in release-gate's coverage check

What this PR does NOT ship (deliberately deferred):
  - The 3 component seeders themselves (seed-bis-lbs, seed-fatf-listing,
    seed-wb-external-debt). These need real-API integration with BIS SDMX,
    FATF HTML scraping, and WB IDS — best landed as a separate PR with
    fixture-recorded tests.
  - Methodology doc `docs/methodology/financial-system-exposure.md` with
    full alternatives + license attribution.
  - Bundle registration in scripts/seed-bundle-macro.mjs.
  - Cohort sanity-check anchor test (RU/IR/KP < 20 on
    financialSystemExposure) — needs real seeder data.

Once those land and seeders populate Redis, flip
`RESILIENCE_FIN_SYS_EXPOSURE_ENABLED=true` in Vercel + Railway env
config to activate the dim.

Stacked on PR #3405 (Phase 1 Ship 1).

🤖 Generated with Claude Opus 4.7 (1M context, extended thinking) via [Claude Code](https://claude.com/claude-code) + Compound Engineering v3.0.0

Co-Authored-By: Claude Opus 4.7 (1M context, extended thinking) <[email protected]>
koala73 added a commit that referenced this pull request Apr 25, 2026
…2 — flag-gated dark) (#3407)

* feat(resilience): financialSystemExposure dim scaffold (Phase 2 Ship 2 — flag-gated dark)

Adds the new `financialSystemExposure` resilience dimension introduced in
plan 2026-04-25-004 Phase 2, behind the
`RESILIENCE_FIN_SYS_EXPOSURE_ENABLED` env flag. The flag defaults OFF so
the dim ships dark — the scorer returns the empty-data shape (score=0,
coverage=0, imputationClass=null) until the 3 component seeders
(seed-bis-lbs, seed-fatf-listing, seed-wb-external-debt) are populating
in production. Rollout pattern matches energy v2 (plan
2026-04-24-001).

Components (weights total 1.0 inside the dim):
  short_term_external_debt_pct_gni    0.35  (WB IDS, lowerBetter, 15-0)
  bis_lbs_xborder_us_eu_uk_pct_gdp    0.30  (BIS LBS by-parent, U-shape)
  fatf_listing_status                  0.20  (FATF, discrete: black=0, gray=30, compliant=100)
  financial_center_redundancy          0.15  (BIS LBS by-parent count, higherBetter, 1-10)

When the flag flips ON, the scorer preflights all 3 required seed-meta
envelopes (the BIS LBS seed serves both Component 2 and Component 4 —
no separate Component 4 seeder). Missing envelopes throw
`ResilienceConfigurationError(message, missingKeys)` (two-arg form per
Codex R3 P1 #2) which `scoreAllDimensions` catches and routes to
`imputationClass='source-failure'`. Per-country data gaps inside an
otherwise-published envelope are distinct: per-component reads return
null, the slot drops out of the weighted blend.

Cache prefixes bumped in lockstep with the new dim:
  resilience:score:v13:    → v14:
  resilience:ranking:v13   → v14
  resilience:history:v8:   → v9:

The scaffold also reweights `tradePolicy` 1.0 → 0.5 in
RESILIENCE_DIMENSION_WEIGHTS (the new dim shares the economic-domain
weight). This shifts the headline overallScore by ~0.46 points in the
flag-off baseline because the half-weighted tradePolicy contributes
proportionally less to the coverage-weighted economic-domain mean.
When the flag flips on with seeders populated, the new dim's actual
signal will rebalance the headline score.

What this PR ships:
  - new `scoreFinancialSystemExposure` + `normalizeBandLowerBetter` U-shape helper
  - 4 new INDICATOR_REGISTRY entries (BIS-derived tagged non-commercial/enrichment per Codex R1 #8)
  - api/health.js + api/seed-health.js dual-registry entries for the 3 new seed keys
  - frontend label map + ResilienceWidget "20 dimensions" copy
  - methodology doc heading "Financial System Exposure" + indicator table
  - tests/resilience-financial-system-exposure.test.mts: 13 tests pinning the
    formula contract + fail-closed preflight + flag-off rollout posture
  - parity test extension for v14/v9 prefixes
  - 22-dim count update across release-gate + handlers + indicator-registry tests
  - flag-gated-dark exemption in release-gate's coverage check

What this PR does NOT ship (deliberately deferred):
  - The 3 component seeders themselves (seed-bis-lbs, seed-fatf-listing,
    seed-wb-external-debt). These need real-API integration with BIS SDMX,
    FATF HTML scraping, and WB IDS — best landed as a separate PR with
    fixture-recorded tests.
  - Methodology doc `docs/methodology/financial-system-exposure.md` with
    full alternatives + license attribution.
  - Bundle registration in scripts/seed-bundle-macro.mjs.
  - Cohort sanity-check anchor test (RU/IR/KP < 20 on
    financialSystemExposure) — needs real seeder data.

Once those land and seeders populate Redis, flip
`RESILIENCE_FIN_SYS_EXPOSURE_ENABLED=true` in Vercel + Railway env
config to activate the dim.

Stacked on PR #3405 (Phase 1 Ship 1).

🤖 Generated with Claude Opus 4.7 (1M context, extended thinking) via [Claude Code](https://claude.com/claude-code) + Compound Engineering v3.0.0

Co-Authored-By: Claude Opus 4.7 (1M context, extended thinking) <[email protected]>

* feat(resilience): financialSystemExposure component seeders + methodology doc

Completes Phase 2 of plan 2026-04-25-004 by shipping the 3 component
seeders, registering them in the macro bundle, adding 34 new fixture-
based tests, and writing the full methodology doc with license attribution.

Seeders (per plan §Files (Phase 2)):
  scripts/seed-wb-external-debt.mjs    — WB IDS short-term external debt as % GNI
                                         (DT.DOD.DSTC.IR.ZS × DT.DOD.DECT.GN.ZS,
                                          mrv=5 + pickLatestPerCountry per memory
                                          `feedback_wb_bulk_mrv1_null_coverage_trap`)
  scripts/seed-bis-lbs.mjs              — BIS LBS by-parent SDMX integration
                                         (12-dim key with L_POS_TYPE=N per Codex
                                          R3 P1 #1; 16 enumerated parent ISO2 codes
                                          per Codex R4 P1 #2 — no `4F` aggregate;
                                          ISO2 direct, no M49 mapping per CL_BIS_IF_REF_AREA)
  scripts/seed-fatf-listing.mjs         — FATF entry-page parser with dynamic
                                         publication-URL follow + sanity-check
                                         band gates + monthly cadence + 90d cache
                                         TTL fallback

Bundle registration:
  scripts/seed-bundle-macro.mjs — Option A per Codex R1 #5 (less operational
                                  overhead than provisioning a new bundle).

Tests (34 new):
  tests/seed-wb-external-debt.test.mjs  — combineExternalDebt formula pinning,
                                          IMF Article IV 15% GNI anchor,
                                          conservative-year selection, validate floor
  tests/seed-bis-lbs.test.mjs           — combineLbsByCounterparty Brazil 2024Q4
                                          ground-truth anchor, 1% GDP threshold
                                          for parentCount, BIS aggregate-code
                                          allow-list (5J/5A skipped), SDMX-JSON
                                          shape parsing, parser-regression guard
  tests/seed-fatf-listing.test.mjs      — findPublicationLink entry-page anchor
                                          extraction (case-insensitive), country-
                                          name lookup with apostrophe handling
                                          (`People's` → `peoples`), pub-date
                                          inference from URL slug + header,
                                          DPRK-on-call-for-action invariant

Methodology doc:
  docs/methodology/financial-system-exposure.md — full construct definition,
                                                   per-component formulas + score
                                                   shapes + coverage matrices,
                                                   fail-closed preflight,
                                                   methodology invariants,
                                                   sanctions-isolated-jurisdiction
                                                   sanity-check anchors (RU/IR/KP/...
                                                   < 20), bounded-movement gate
                                                   (60%+ |Δ|<3pt), data-source
                                                   licensing (BIS terms of use),
                                                   alternatives considered (5),
                                                   future considerations (Phase 3-5)

Quality:
  - npm run typecheck + typecheck:api: clean
  - npm run test:data: 7149/7149 pass (was 7115; +34 new seeder tests)
  - npm run lint + lint:md + version:check: clean
  - Edge function bundle check: clean

Activation runbook (when ready to flip the dim live):
  1. Merge this PR
  2. Run seed-wb-external-debt + seed-bis-lbs + seed-fatf-listing manually
     via `railway run --service seed-bundle-macro -- node scripts/<seeder>.mjs`
  3. Verify all 3 seed-meta envelopes published:
       redis-cli GET 'seed-meta:economic:wb-external-debt'
       redis-cli GET 'seed-meta:economic:bis-lbs'
       redis-cli GET 'seed-meta:economic:fatf-listing'
  4. Set RESILIENCE_FIN_SYS_EXPOSURE_ENABLED=true in Vercel + Railway env config
  5. Flush v14 caches: bulk DEL resilience:score:v14:* + DEL resilience:ranking:v14
  6. Run seed-resilience-scores.mjs to bulk-warm v14 with the new dim's signal
  7. Cohort audit: snapshot resilience:score:v14:* for all 222 countries;
     RU/IR/KP must score < 20 on financialSystemExposure (gate the construct
     before stable-rollout)
  8. Bounded-movement gate: 60% of countries |Δ|<3pt; outliers > 12pt must
     be in the explicitly-predicted RU/IR/KP/CU/VE/BY/LY/MM set
  9. Remove FLAG_GATED_DARK_DIMENSIONS allow-list entry in
     tests/resilience-release-gate.test.mts in the same commit that flips
     the flag

🤖 Generated with Claude Opus 4.7 (1M context, extended thinking) via [Claude Code](https://claude.com/claude-code) + Compound Engineering v3.0.0

Co-Authored-By: Claude Opus 4.7 (1M context, extended thinking) <[email protected]>

* chore(resilience): address self-review hardening (P1 + 4×P2 + 4×P3)

P1 — BIS LBS sequential-fetch timeout math:
  Sequential 16 parents × 60s timeout = 960s worst-case, exceeding the
  bundle's 600s timeoutMs. SIGTERM mid-flight would have leaked the
  child-lock + produced a covertly-degraded payload (per memory
  `bundle-runner-sigkill-leaks-child-lock`). Fix: parallelize parent
  fetches with concurrency=4 via a bounded-concurrency runner. Caps wall
  time at ~4 × 60s = 240s on the slow path while staying polite to BIS.

P2 — BIS LBS parent-success gate:
  Previous "all 16 failed" short-circuit was too permissive. If 15 of 16
  parent fetches failed, a single-parent payload would pass validate
  (>100 counterparty floor) and skew Component 4 (financialCenterRedundancy)
  low for every counterparty until the next successful run. Added
  MIN_SUCCESSFUL_PARENTS=12 gate; below that, throw → seed-meta unchanged
  → previous valid payload stays alive under cache TTL.

P2 — FATF findPublicationLink prefers highest-year anchor:
  Previous "first anchor matching label" was vulnerable to FATF page
  layouts where a sidebar links to historical publications using the
  same wording. Fix: collect all candidates, sort by year (URL slug or
  anchor text), return highest. Logs all candidates at WARN when more
  than one matches so ops can spot drift.

P2 — FATF unmatched country-name surfacing:
  Previous parser silently dropped country names not in
  shared/country-names.json. If FATF introduced a new spelling
  ("Mauretania", "Türkiye"), the country fell out of the listing and
  defaulted to "compliant" (score 100) — materially shifting its
  financialSystemExposure score under a fresh seed-meta. Fix:
  extractListedCountries now returns { listed, unmatchedCandidates }.
  Seeder logs unmatched at WARN; throws if > 2 candidates unmatched
  (indicates parser drift / new spellings the lookup needs to learn).

P2 — WB external-debt yearMismatch metadata:
  Composition can mix vintages of the two source indicators (different
  WB IDS lag patterns). Previous output silently used min(year) as the
  conservative anchor. Fix: emit `yearMismatch: boolean` +
  `shortTermPctOfTotalDebtYear` + `totalDebtPctOfGniYear` on each
  country record so the dashboard / scorer / audit can flag countries
  with cross-year composition. Pinning test asserts min(year) selection
  + correct flag.

P3 — BIS LBS upper-bound corruption guard:
  Added `latestVal > 1e8` skip in extractClaimsByCounterparty. 1e8
  millions = $100T (>half of global GDP), well above any plausible
  bilateral claim. Drops corrupt SDMX values silently rather than
  letting them inflate totalXborderPctGdp downstream.

P3 — BIS LBS validation floor 100 → 150:
  BIS LBS counterparty coverage is ~200+ jurisdictions. Floor of 100
  would have accepted a payload with massive coverage regression.
  Tightened to 150.

P3 — FATF grey-list floor 8 → 12 + band 8-40 → 12-40:
  Historical FATF grey-list size has been 15+ since 2020. Floor of 8
  was too lenient. Tightened to 12 with comment explaining the
  historical band.

P3 — FATF parse-failure WARN logging:
  All sanity-check throw paths in seed-fatf-listing now emit a
  console.warn explaining the failure and noting "previous valid
  payload remains under cache TTL" before throwing. Plan called for
  "warn loudly"; the implicit fall-back-to-cache pattern is now
  diagnostic-friendly.

Suggestion — BIS LBS droppedForMissingGdp provenance:
  Counterparties seen in BIS LBS but dropped because no WB GDP record
  was available are now collected into a `droppedForMissingGdp` array
  on the seed payload. Surfaces silent coverage gaps for ops triage
  without polluting the main `countries` map.

Suggestion — methodology doc operational footguns + smoke test:
  New §"Common operational footguns" section in
  docs/methodology/financial-system-exposure.md surfacing the BIS LBS
  4F-aggregate-rejection lesson + ISO 3166-1 (not M49) clarification +
  pre-flag-flip smoke test commands.

Quality gates:
  - npm run typecheck + typecheck:api: clean
  - npm run lint + lint:md + version:check: clean
  - npm run test:data: 7153/7153 pass (was 7149; +4 hardening tests)

🤖 Generated with Claude Opus 4.7 (1M context, extended thinking) via [Claude Code](https://claude.com/claude-code) + Compound Engineering v3.0.0

Co-Authored-By: Claude Opus 4.7 (1M context, extended thinking) <[email protected]>

* fix(resilience): scoreFinancialSystemExposure preflights UNVERSIONED seed-meta keys

P1 reviewer catch: the preflight in scoreFinancialSystemExposure was
reading `seed-meta:economic:<key>:v1` while runSeed
(scripts/_seed-utils.mjs) writes the freshness record at
`seed-meta:${dataKey.replace(/:v\d+$/, '')}` — i.e. with the trailing
:v\d+ stripped. Once `RESILIENCE_FIN_SYS_EXPOSURE_ENABLED=true` was
flipped, every /api/resilience/* request would have hit the missing-
seed-meta path indefinitely, throwing ResilienceConfigurationError and
stamping every country's financialSystemExposure as
imputationClass='source-failure' even with healthy seeders running.

The same unversioned shape is already used by api/health.js +
api/seed-health.js + every other in-tree scorer that walks
seed-meta keys via _dimension-freshness.ts.

Fix: route the preflight through `resolveSeedMetaKey` from
_dimension-freshness.ts. That helper already strips the trailing
:v\d+ AND applies the SOURCE_KEY_META_OVERRIDES table — the canonical
in-tree pattern for "given a registry data-key, return the seed-meta
key that runSeed actually writes." Inlining the regex would have
re-introduced the same writer/reader drift this helper exists to
prevent.

Tests:
  - All formula + preflight tests (which previously mocked the
    incorrect versioned form) updated to the unversioned key shape so
    they actually exercise the production read path.
  - New regression-guard test "preflight reads UNVERSIONED seed-meta
    keys (matches runSeed write-key shape)" pins the exact key shape
    the scorer probes. Asserts both presence of the unversioned form
    AND absence of the versioned form. A future refactor that
    accidentally re-versions the preflight will fail loudly here
    instead of silently routing every country to source-failure.

Quality gates:
  - npm run typecheck:api: clean
  - npm run test:data: 7154/7154 pass (was 7153; +1 contract guard)

🤖 Generated with Claude Opus 4.7 (1M context, extended thinking) via [Claude Code](https://claude.com/claude-code) + Compound Engineering v3.0.0

Co-Authored-By: Claude Opus 4.7 (1M context, extended thinking) <[email protected]>

* fix(resilience): address Greptile P1 + P2 catches on PR #3407 (4 issues)

P1 — 30-point scoring cliff at 25% boundary in normalizeBandLowerBetter:
  Original draft had piecewise-linear segments with mismatched endpoints:
    - At value=25:    sweet spot ended at 100, over-exposed started at 70
                      → 30-point cliff × 0.30 weight ≈ 9-pt headline swing
    - At value=5:     low-int ended at 70, sweet started at 75 → 5-pt jump
  Cliffs in piecewise-linear scorers cause ranking instability for
  countries near band edges (24.9% scores ~99.9, 25.1% scores ~70).
  Re-anchored adjacent segments to share endpoints — function is now
  piecewise-CONTINUOUS at 5%, 25%, and 60% transitions:
    0%-5%:    60 → 75   (slope +3/pct, was +2)
    5%-25%:   75 → 100  (unchanged)
    25%-60%:  100 → 30  (slope −2/pct, was 70 → 30 / slope −1.14)
    60%+:     30 → 0    (unchanged)
  New regression test pins continuity at all three boundaries (samples
  values immediately above/below each transition, asserts |Δ| ≤ 1pt).

P2 — readFatfStatus defaults empty listings dict to "compliant":
  An empty `listings: {}` payload that bypassed the seeder's validate()
  would silently score every country at 100 (compliant default), masking
  a parser regression. Added defense-in-depth guard: empty dict → null
  component score → slot drops out of weighted blend → coverage shrinks
  visibly rather than the dim looking healthy. Seeder validate already
  enforces ≥1 black + ≥12 grey so this can't reach production through
  the normal write path; the guard costs nothing and catches malformed
  payloads that bypass validation. New regression test pins.

P2 — bisLbsXborderPctGdp goalposts mismatch U-shape peak:
  Registry entry had `goalposts: { worst: 60, best: 15 }` implying a
  linear lowerBetter scale peaking at 15%. The U-shape function actually
  peaks at 25% (value=15% scores 87.5, not 100). Updated to
  `{ worst: 60, best: 25 }` (over-exposed branch, peak anchor) with an
  explicit comment that goalposts here are documentation-only — the
  actual scorer uses normalizeBandLowerBetter, not a generic linear
  normalizer. Tooling reading the registry should consult the scorer
  helper directly.

P2 (resolved differently than originally suggested) —
api/bootstrap.js missing 3 new data keys:
  Greptile flagged that AGENTS.md requires bootstrap hydration for new
  data sources. Verified the bootstrap-hydration-coverage test enforces
  this via a `getHydratedData` consumer requirement in src/.
  The 3 new keys (economic:wb-external-debt:v1, economic:bis-lbs:v1,
  economic:fatf-listing:v1) are SERVER-ONLY — they feed
  scoreFinancialSystemExposure inside /api/resilience/* handlers; no
  client panel consumes them directly. Adding them to bootstrap without
  a consumer would fail the parity tests. Documented in api/bootstrap.js
  as a deferred entry: "if a future PR adds a client panel that displays
  raw BIS LBS / FATF / WB external-debt data, register the keys here
  AND add the corresponding consumer + cache-keys.ts entries in the
  same PR."

Note on Greptile's P1 #1 (preflight `:v1` suffix mismatch): already
resolved in commit d904103 (uses `resolveSeedMetaKey` from
_dimension-freshness.ts). Greptile reviewed an earlier commit.

Quality gates:
  - npm run typecheck:api: clean
  - npm run lint + lint:md + version:check: clean
  - npm run test:data: 7156/7156 pass (was 7154; +2 regression guards)

🤖 Generated with Claude Opus 4.7 (1M context, extended thinking) via [Claude Code](https://claude.com/claude-code) + Compound Engineering v3.0.0

Co-Authored-By: Claude Opus 4.7 (1M context, extended thinking) <[email protected]>

---------

Co-authored-by: Claude Opus 4.7 (1M context, extended thinking) <[email protected]>
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