feat(resilience): rename tradeSanctions → tradePolicy + drop OFAC component (Ship 1)#3405
Conversation
…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]>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryRenames the Confidence Score: 4/5Safe 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
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:]
Reviews (1): Last reviewed commit: "feat(resilience): rename tradeSanctions ..." | Re-trigger Greptile |
| // `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. |
There was a problem hiding this comment.
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.
| // `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]>
…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]>
…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]>
Summary
Plan
2026-04-25-004Phase 1 (Ship 1). Renames thetradeSanctionsresilience dimension totradePolicyand drops the OFACsanctionCountcomponent (was weight 0.45). The remaining 3 components are reweighted to total 1.0: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
financialSystemExposuredim 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:v12→v13resilience: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)
_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)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 migratedsrc/components/resilience-widget-utils.tslabel maptests/resilience-sanctions-field-mapping.test.mtsdeleted; replaced by newtests/resilience-trade-policy-formula.test.mtspinning the 3-component formula contract. 9 other test files updated for the new identifiers + numerics.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 writesanctions: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 typecheckpasses (zero errors)npm run typecheck:apipasses (zero errors after deleting unusedRESILIENCE_SANCTIONS_KEYandnormalizeSanctionCount)npm run test:data— 7101/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 prefixtests/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 idtradePolicytests/resilience-dimension-scorers.test.mts— full set of scoreTradePolicy paths, including non-reporter IMPUTE blend and tariff-only modenode --test tests/edge-functions.test.mjs— 177/177 passesbuild --bundlefor everyapi/*.js) cleannpm run lint(biome) — exit 0npm run lint:md— exit 0npm run version:check— exit 0tradeSanctions/scoreTradeSanctionshits; zero non-commentresilience:(score|ranking):v12/resilience:history:v7literalsPost-Deploy Monitoring & Validation
[resilience]invocations after deploy — expect compute-on-miss for v13 score keys until the seeder warms them/api/healthresilienceRankingregistered keyresilience:ranking:v13redis-cli GET 'resilience:score:v13:US'returns a populated record afterseed-bundle-resiliencecron runsredis-cli GET 'resilience:ranking:v13'exists and has_formulatag matching production/api/healthreportsresilience:ranking:v13STALE > 12h after merge → Railway cron failing; checkseed-bundle-resiliencelogs and re-deploy seeder/api/resilience/rankingimmediately post-deploy → revert the PR; v12 reads still work since we didn't delete the seeder keyCohort-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:
resilience:score:v12:*for all 222 countriesresilience:score:v13:*tradePolicymay 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)
tradeSanctions54 →tradePolicylikely 70-80 range (the removed sanctions component was the drag)tradeSanctions82 →tradePolicylikely similar (Kuwait's trade-policy openness is genuinely close to UAE's)🤖 Generated with Claude Opus 4.7 (1M context, extended thinking) via Claude Code + Compound Engineering v3.0.0