fix(resilience): correct WB IDS indicator + switch BIS LBS → CBS dataflow (financialSystemExposure activation)#3412
Conversation
…flow Two activation-time bugs caught when running the financialSystemExposure seeders against production Redis (PR #3407 follow-up). == Fix #1 — WB IDS short-term external debt indicator == Original code used `DT.DOD.DSTC.IR.ZS × DT.DOD.DECT.GN.ZS / 100`, intended to compose "short-term external debt as % of GNI." But `DT.DOD.DSTC.IR.ZS` is "% of total RESERVES" (NOT "% of total external debt"). Argentina (164%), Turkey (114%), and Sri Lanka all had intermediate ratios > 100% because their short-term debt exceeds reserves — caught by Redis audit on the first live seed run. Fix: divide absolute USD values directly. shortTermDebtPctGni = (DT.DOD.DSTC.CD / NY.GNP.MKTP.CD) × 100 Live verification: BR=4%, AR=7.77%, TR=13.26%, LK=6.03%, NG=9.05% — all in plausible 0-15% range matching real-world short-term debt ratios. == Fix #2 — BIS LBS → BIS CBS dataflow == Plan called for "BIS LBS by-parent" data, but `WS_LBS_D_PUB` only exposes counterparty as the aggregate `5J`. Per-counterparty breakdowns require `WS_CBS_PUB` (Consolidated Banking Statistics). CBS: 11 dimensions (LBS had 12), parent is L_REP_CTY, CBS_BASIS=F selects foreign-claims ultimate-risk basis. SDMX key: Q.S.<PARENT>.4B.F.C.A.A.TO1.A. Live verification: 201 records, all 16 parents fetched, BR=19.6%, CN=3.1%, AE=46.7%, SG=233%, CH=278% — values match real-world bank exposure including U-shape penalty for hub jurisdictions. Filename + Redis key (`economic:bis-lbs:v1`) retained for historical continuity; scorer-side contract unchanged. What's pending for full activation: FATF seeder still blocked by HTTP 403 on fatf-gafi.org. Separate PR. Quality gates: - typecheck:api / lint / lint:md: clean - npm run test:data: 7189/7189 pass 🤖 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]>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
Greptile SummaryThis PR corrects two post-activation bugs in the Confidence Score: 4/5Safe to merge — both formula and dataflow fixes are verified in production; only P2-level findings remain. All findings are P2 (style/quality). The null-as-zero issue in fetchWbIndicator is pre-existing across the codebase and partially mitigated by the mrv=5 window and the gni.value <= 0 guard. The backwards-compat alias and informational BIS_AGGREGATE_CODES are cosmetic. No logic errors introduced in this PR. scripts/seed-wb-external-debt.mjs — null WB API value handling in fetchWbIndicator; scripts/seed-bis-lbs.mjs — backwards-compat alias and unreachable aggregate-code set. Important Files Changed
Sequence DiagramsequenceDiagram
participant Seeder as seed-wb-external-debt.mjs
participant WB as World Bank API
participant Combine as combineExternalDebt()
participant Redis
Seeder->>WB: GET DT.DOD.DSTC.CD (short-term debt USD, mrv=5)
WB-->>Seeder: {iso2: {value, year}}
Seeder->>WB: GET NY.GNP.MKTP.CD (GNI USD, mrv=5)
WB-->>Seeder: {iso2: {value, year}}
Seeder->>Combine: {shortTermDebtUsd, gniUsd}
Combine-->>Seeder: countries {value=(debt/GNI)x100, yearMismatch}
Seeder->>Redis: SET economic:wb-external-debt:v1
participant BIS_Seeder as seed-bis-lbs.mjs
participant BIS as BIS WS_CBS_PUB API
participant GDP as World Bank GDP
participant CombineCBS as combineCbsByCounterparty()
loop 16 parents (concurrency=4)
BIS_Seeder->>BIS: GET Q.S.PARENT.4B.F.C.A.A.TO1.A.
BIS-->>BIS_Seeder: SDMX-JSON {byCounterparty, latestPeriod}
end
BIS_Seeder->>GDP: GET NY.GDP.MKTP.CD (all countries, mrv=3)
GDP-->>BIS_Seeder: {iso2: {value, year}}
BIS_Seeder->>CombineCBS: perParent + gdpByCountry
CombineCBS-->>BIS_Seeder: countries {totalXborderPctGdp, parentCount, parents}
BIS_Seeder->>Redis: SET economic:bis-lbs:v1
|
| const BIS_AGGREGATE_CODES = new Set([ | ||
| '5J', '5A', '5M', '1C', '4F', '4U', '5C', // common aggregates | ||
| 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8', 'A9', // grouped aggregates | ||
| '5R', '5T', '5W', '5Z', // EM/AE/world groupings | ||
| '5J', '5A', '5M', '5C', '5R', '5T', '5W', '5Z', | ||
| '1C', '1E', '1W', | ||
| '2Z', '3P', | ||
| '4F', '4U', | ||
| 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8', 'A9', | ||
| ]); |
There was a problem hiding this comment.
BIS_AGGREGATE_CODES set is unreachable by the guarding filter chain
The filter at line 136 is: cpCode.length !== 2 || !/^[A-Z]{2}$/.test(cpCode) || BIS_AGGREGATE_CODES.has(cpCode). Every entry in BIS_AGGREGATE_CODES contains at least one digit (5J, 1C, A2, 4F, etc.), so all of them already fail the /^[A-Z]{2}$/ test and are rejected before BIS_AGGREGATE_CODES.has() is ever evaluated. The comment says "informational only," which is accurate, but the set cannot catch a BIS aggregate that is two all-alpha letters (e.g., a regional aggregate like EU) — the set would need an entry AND those codes would have to be 2-letter purely alphabetic. Worth verifying during the next CBS codelist audit whether any 2-letter all-alpha aggregate codes exist.
| export { CANONICAL_KEY, CACHE_TTL, PARENT_COUNTRIES }; | ||
| // Backwards-compat alias for tests that imported the original LBS name. | ||
| export { combineCbsByCounterparty as combineLbsByCounterparty }; |
There was a problem hiding this comment.
Backwards-compat alias leaks LBS naming into the public export surface
export { combineCbsByCounterparty as combineLbsByCounterparty } makes the old name a first-class export of the module. If any production code imports combineLbsByCounterparty by name, it would silently receive CBS data while the name implies LBS semantics. The test file is the only currently known consumer, but the alias is visible to any future import. Consider documenting clearly in the export that this is a test-only alias to be removed once the test imports are updated.
Three fixes on top of the BIS LBS → CBS migration. == Greptile #1 — BIS_AGGREGATE_CODES Set was dead code == The filter chain was: cpCode.length !== 2 || !/^[A-Z]{2}$/.test(cpCode) || BIS_AGGREGATE_CODES.has(cpCode) Every entry in BIS_AGGREGATE_CODES had a digit (5J, 1C, A2, 4F, ...), so all of them already failed the `/^[A-Z]{2}$/` regex BEFORE the Set was consulted. The Set never caught anything in production. Real risk the Set was supposed to mitigate: BIS introduces a 2-letter ALL-ALPHA aggregate (e.g. `EU`) that the regex would pass. Replaced with `ALPHA_AGGREGATE_CODES` — empty by default, audited each CBS quarterly publish. Verified against the live `WS_CBS_PUB` L_CP_COUNTRY codelist (252 values as of 2026-04-25): no current 2-letter all-alpha aggregates exist. == Greptile #2 — combineLbsByCounterparty alias removed == `export { combineCbsByCounterparty as combineLbsByCounterparty }` made the old LBS name a first-class export of the module. Any future code importing `combineLbsByCounterparty` would silently get CBS data while the name implied LBS semantics. Only the test file consumed it. Removed the alias, renamed test imports to `combineCbsByCounterparty`. == Self-noted finding — hub-country self-counting in Component 4 == Caught during the 2026-04-25 production activation audit. For Singapore and Switzerland (both in PARENT_COUNTRIES AND on the counterparty list), the BIS CBS payload included their own domestic banking claims: SG-on-SG: $584B CH-on-CH: $2.2T These are domestic loan books, NOT foreign-bank cross-border exposure. Component 4 (`parentCount`) is supposed to measure "how many INDEPENDENT FOREIGN USD-clearing routes remain if a major counterparty pulls" — domestic banks aren't a foreign-fallback route. Filter: drop entries where `cp === parent` when building claimsByCpByParent. Live verification of the impact: Country OLD pct NEW pct OLD parentCount NEW parentCount SG 232.98% 126.11% 12 11 CH 277.89% 43.69% 10 9 US ... 32.2% (US-on-US excluded) GB ... 65.96% (GB-on-GB excluded) BR 19.62% 19.62% 4 4 (unchanged; BR not in PARENT_COUNTRIES) AE 46.69% 46.69% 10 10 (unchanged; AE not in PARENT_COUNTRIES) CH dropped from "Iceland-2008 territory" to "over-exposed" — the old number was a measurement artifact. The new numbers reflect actual foreign-bank exposure as the construct intends. Tests: new "excludes self-claims" regression guard pins the SG/CH behavior. Methodology doc gets a §"Self-exclusion rule" callout under Component 4. Quality gates: - typecheck:api / lint / lint:md: clean - npm run test:data: 7190/7190 pass (was 7189; +1 self-exclusion guard) - Live production seed run: 201 records, hub-self-exclusion verified 🤖 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]>
…1 + HIC=0) (#3427) * fix(resilience): repair externalDebtCoverage broken-data-source (mrv=1 trap + HIC=0 sentinel) PR #3425's post-merge cohort dry-run surfaced that `externalDebtCoverage` (0.25-weight recovery dimension) is awarding `debtToReservesRatio: 0` → score 100 to **72/164 countries (44%)** — including NO/CH/DK/SE/FI/IS/ KW/AE/SG/LU. A 25%-weighted dimension that scores nearly half the universe at 100 is not discriminating; it's structurally elevating wealthy countries on a metric that should differentiate them. Two-layered root cause: 1. **WB `mrv=1` coverage trap** (memory `feedback_wb_bulk_mrv1_null_coverage_trap`). `mrv=1` returns a SINGLE year across all countries with `value: null` for late-reporters; the script silently drops them. The mature pattern (already used by `seed-wb-external-debt.mjs` for the financialSystemExposure dim) is `mrv=5` + per-country pickLatest-non-null. This script was written before the memory was captured and never migrated. 2. **WB IDS dataset is LMIC-scoped.** `DT.DOD.DSTC.CD` (short-term external debt) returns the literal `0` (not `null`) for high-income countries that don't report into the IDS series — they're out-of-scope, not data-sparse. Under the prior code, `Number(0) → 0` passed `Number.isFinite()`, divided by reserves, and yielded `debtToReservesRatio: 0` → score 100. The construct semantically does not apply to HICs (which manage external debt through different channels), so dropping them is correct — they fall to the dim's existing IMPUTE fallback (`recoveryExternalDebt`: 50/0.3/'unmonitored'). **Same fix applied to `seed-recovery-reserve-adequacy.mjs`** which had the same `mrv=1` trap on `FI.RES.TOTL.MO` (no HIC=0 sentinel issue there because reserve-months is genuinely measured for HICs). **Propagation:** No cache-prefix bump. Score cache `resilience:score:v15:` has 12h TTL; the next bulk-warm tick after this seed lands will refresh all 222 country scores against the new debt seed, so the 72 false-100 countries naturally drop to IMPUTE 50 within 12h. Manual immediate warm: run `seed-recovery-external-debt.mjs` then `seed-resilience-scores.mjs`. **Expected v15 ranking impact:** countries previously inflated by false-perfect externalDebtCoverage will drop. The cohort dry-run should be re-run post-deploy to capture the empirical signature; PR #3425's `npm run dryrun:resilience` script does this. **Audit lineage:** Plan 2026-04-26-001 cohort dry-run (commit `ae7229e5e` post-merge) → user audit finding #7+#8 → this PR. Memory `feedback_wb_bulk_mrv1_null_coverage_trap` is the established recipe; this is the second seeder migrated to it (first was `seed-wb-external-debt.mjs` in PR #3412). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix(resilience): null-skip before Number() coercion + post-merge force-refresh script Two reviewer findings on PR #3427's initial commit: **P1: Number(null) === 0 defeats the latest-non-null picker.** The mrv=5 + pickLatest fix was structurally correct but `Number(null)` returns `0` (not `NaN`), passes `Number.isFinite()`, and lets a `value: null` record overwrite an older non-null record in the year-comparison branch. Net effect: the very late-reporters mrv=5 was supposed to capture (KW/QA/AE) get a final `value: 0` from a recent null record, then the country-level `debt.value <= 0` HIC filter drops them entirely — same end-state as before, fix defeated. Fix: explicit `if (record?.value == null) continue` BEFORE coercion in both seeders. Same pattern as the standard recipe documented in memory `feedback_wb_bulk_mrv1_null_coverage_trap`. Reviewer caught this within minutes of the initial push. **P2: Bundle-runner freshness gate would delay propagation by ~24d.** `scripts/_bundle-runner.mjs:240` skips bundle items where `elapsed < intervalMs * 0.8`. For the 30d recovery bundle, that means the new seeder code wouldn't RUN on Railway for ~24 days post-merge even though the code is in the container. Meanwhile `seed-resilience-scores` would keep recomputing scores against the old debt seed → fix appears not to land. Fix: `scripts/post-pr3427-force-refresh.mjs` — one-shot post-merge script that invokes both seeders directly (bypassing the bundle runner's freshness gate). Runs the two seeders in series, then prints the follow-up bulk-warm command. Documented in the script header and to be referenced in PR #3427's release checklist. Both fixes are surgical — same files, no logic re-architecture. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
…cher (review fixup on PR #3432) Reviewer found one more WB seeder I missed in the original PR #3432 sweep: **P1: `scripts/seed-wb-external-debt.mjs:69` had the exact same compound trap.** `Number(record?.value)` before any null check, then year-based latest-picker overwrites. Unlike the SWF imports picker (where `value <= 0` accidentally catches Number(null)=0 because imports must be positive), this script's downstream `combineExternalDebt` filter at `:98` only rejects negative debt — `debt.value < 0`, not `<= 0`. So a `value: null` record from a late-reporting LMIC (KW/QA/AE publish IDS data 1-2y behind G7) would coerce to `0`, win the year comparison, and propagate as a false `debtToReservesRatio: 0` → `0% short-term-debt-to-GNI` for the country. This feeds `economic:wb-external-debt:v1` consumed by the new `financialSystemExposure` dim (PR #3412 / #3407), so the false 0% would silently inflate the dim's score for affected LMICs. Fix: same recipe as PRs #3427 and #3432 — explicit `if (record?.value == null) continue;` BEFORE `Number()` coercion. **Defense-in-depth: `scripts/seed-bis-lbs.mjs:204` (the WB GDP fetcher inside the BIS LBS seeder) also gets the explicit null-skip.** This one was technically safe because GDP > 0 is a real-world invariant and the `value <= 0` filter catches Number(null)=0 by side effect. But per the lesson now codified in skill `wb-bulk-mrv1-null-coverage-trap`, the protection is fragile — future indicator-family changes that relax `value <= 0` would silently lose null protection. Adding the explicit null-skip makes the picker null-safe regardless of filter relaxation. **End state after this commit + PR #3427 + PR #3432 + this fixup:** ALL WB-bulk seeders in `scripts/seed-*.mjs` either (a) have an explicit `value == null` skip BEFORE coercion, or (b) use a non-WB endpoint shape (BIS SDMX, IMF SDMX, etc.) where the trap doesn't apply. Final sweep recipe documented in the commit; next reviewer can run it verbatim to audit any new seeder. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
…rs (#3432) * fix(resilience): apply mrv=5 + null-skip recipe to remaining 4 WB seeders PR #3427 fixed the mrv=1 + Number(null)=0 compound trap in 2 recovery seeders (external-debt + reserve-adequacy). A subsequent sweep (`grep -lE "mrv=1[^0-9]" scripts/*.mjs`) found 4 OTHER seeders with the same trap shape: - `seed-fossil-electricity-share.mjs` (EG.ELC.FOSL.ZS) - `seed-low-carbon-generation.mjs` (EG.ELC.{NUCL,RNEW,HYRO}.ZS) - `seed-power-reliability.mjs` (EG.ELC.LOSS.ZS) - `seed-sovereign-wealth.mjs` (NE.IMP.GNFS.CD via pickLatestPerCountry) All four migrated to the established recipe: 1. mrv=1 → mrv=5 + per-country pickLatest 2. per_page=500 → per_page=2000 (5x records per country = need bigger page) 3. Explicit `if (record?.value == null) continue` BEFORE Number() coercion 4. Year sanity check + per-country latest comparison **Why explicit null-skip matters more for the energy seeders:** The 3 energy seeders use "% of" indicators (EG.ELC.{FOSL,NUCL,RNEW, HYRO,LOSS}.ZS) where 0 IS a legitimate value (country has 0% nuclear, 0% fossil, perfect grid reliability, etc.). The accidental null-protection trick `if (value <= 0) continue` from the SWF picker WOULD WRONGLY DROP legitimate zeros for these. Must skip null explicitly so the `value === 0` case can flow through to scoring. **`seed-sovereign-wealth.mjs` was already mostly correct** (mrv=5 + pickLatest helper + accidental null protection via `value <= 0` since imports must be > 0). Added explicit null-skip as defense-in-depth so a future copy-paste of `pickLatestPerCountry` for a different indicator family doesn't silently lose null protection. **Lineage:** Plan 2026-04-26-001 cohort dry-run → user audit Priority Check #4 ("Replace mrv=1 with mrv=5 + latest non-null in ALL seeders") → this PR. Companion to PR #3427. **Memory `wb-bulk-mrv1-null-coverage-trap`** was updated post-PR-#3427 with the explicit `Number(null) === 0` warning + production case study — that memory is the recipe these 4 seeders now follow. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix(resilience): null-skip in seed-wb-external-debt + bis-lbs GDP fetcher (review fixup on PR #3432) Reviewer found one more WB seeder I missed in the original PR #3432 sweep: **P1: `scripts/seed-wb-external-debt.mjs:69` had the exact same compound trap.** `Number(record?.value)` before any null check, then year-based latest-picker overwrites. Unlike the SWF imports picker (where `value <= 0` accidentally catches Number(null)=0 because imports must be positive), this script's downstream `combineExternalDebt` filter at `:98` only rejects negative debt — `debt.value < 0`, not `<= 0`. So a `value: null` record from a late-reporting LMIC (KW/QA/AE publish IDS data 1-2y behind G7) would coerce to `0`, win the year comparison, and propagate as a false `debtToReservesRatio: 0` → `0% short-term-debt-to-GNI` for the country. This feeds `economic:wb-external-debt:v1` consumed by the new `financialSystemExposure` dim (PR #3412 / #3407), so the false 0% would silently inflate the dim's score for affected LMICs. Fix: same recipe as PRs #3427 and #3432 — explicit `if (record?.value == null) continue;` BEFORE `Number()` coercion. **Defense-in-depth: `scripts/seed-bis-lbs.mjs:204` (the WB GDP fetcher inside the BIS LBS seeder) also gets the explicit null-skip.** This one was technically safe because GDP > 0 is a real-world invariant and the `value <= 0` filter catches Number(null)=0 by side effect. But per the lesson now codified in skill `wb-bulk-mrv1-null-coverage-trap`, the protection is fragile — future indicator-family changes that relax `value <= 0` would silently lose null protection. Adding the explicit null-skip makes the picker null-safe regardless of filter relaxation. **End state after this commit + PR #3427 + PR #3432 + this fixup:** ALL WB-bulk seeders in `scripts/seed-*.mjs` either (a) have an explicit `value == null` skip BEFORE coercion, or (b) use a non-WB endpoint shape (BIS SDMX, IMF SDMX, etc.) where the trap doesn't apply. Final sweep recipe documented in the commit; next reviewer can run it verbatim to audit any new seeder. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * docs: fix stale 'mrv=1' top-comment in seed-low-carbon-generation.mjs Reviewer follow-up on PR #3432: the top-of-file rationale comment in `seed-low-carbon-generation.mjs:22` still claimed "We fetch the most-recent value (mrv=1)" even though the actual fetch was migrated to mrv=5 + null-skip in commit dd0be80. Non-blocking but worth cleaning up — this is the same doc-drift pattern memory `feedback_doc_drift_after_behavior_fix_needs_grep_sweep` warns about. Updated the comment to describe the current mrv=5 + per-country pickLatest behavior and reference the relevant skill + this PR. The other mrv=1 references in PR #3432's touched files are intentional — they appear inside rationale comments that explain the trap before documenting why the seeder uses mrv=5 instead. Those should NOT change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
Summary
Activation-time follow-up to PR #3407 (
financialSystemExposuredim). Running the 3 component seeders against production Redis surfaced two bugs in the Phase 2 design that the offline tests couldn't catch — fixing both here.Fix #1 — WB IDS short-term external debt indicator
PR #3407 used
DT.DOD.DSTC.IR.ZS × DT.DOD.DECT.GN.ZS / 100, intended to produce "short-term external debt as % of GNI." ButDT.DOD.DSTC.IR.ZSis "% of total reserves", not "% of total external debt." The composed ratio was mathematically meaningless.Smoking gun (production Redis audit, 2026-04-25):
shortTermPctOfTotalDebtFix: divide absolute USD values directly.
Where
DT.DOD.DSTC.CD= short-term external debt stocks (USD) andNY.GNP.MKTP.CD= GNI (USD).Post-fix live verification (after running the corrected seeder against production):
All values in the plausible 0-15% range matching real-world short-term-debt-to-GNI ratios.
Fix #2 — BIS LBS → BIS CBS dataflow
PR #3407 used
WS_LBS_D_PUB(Locational Banking Statistics) on the assumption it publishes by-parent / by-counterparty cross-border claims. It does not —WS_LBS_D_PUBonly exposes counterparty as the aggregate5J. Verified by direct API probe: an emptyL_CP_COUNTRYposition returns 200 OK with one series whose counterparty value is5J(single-aggregate breakdown), not a per-country expansion.The actual dataflow that publishes per-counterparty foreign claims by parent country is
WS_CBS_PUB(Consolidated Banking Statistics).CBS dimension shape (11 dims, discovered via probe)
QS(stocks)4B(consolidated)F(foreign claims, ultimate-risk basis)C(claims)AATO1ACBS uses
L_REP_CTYto mean parent country (the bank's HQ jurisdiction). LBS has a separateL_PARENT_CTYdim that also exists but is only published as aggregate. Different dataflow, different semantics; the original draft conflated them.Post-fix live verification (against production):
The U-shape correctly penalizes hub jurisdictions (SG, CH) at the over-exposure end while rewarding diversified parent-set coverage (high
parentCount). The Component 2 + Component 4 trade-off is working as designed.What's still pending for activation
The third component seeder,
seed-fatf-listing.mjs, returns HTTP 403 fromfatf-gafi.org(direct + Decodo proxy both rejected). Out of scope for this PR per user direction. Until FATF is fixed, the dim's preflight will throw on flag-flip even with WB + BIS healthy. Plan: separate follow-up PR for FATF using a different scrape strategy.Files changed
scripts/seed-wb-external-debt.mjs— new indicators +combineExternalDebt({ shortTermDebtUsd, gniUsd })direct USD ratioscripts/seed-bis-lbs.mjs— full rewrite for CBS dimension shape (filename + Redis key retained for scorer-side contract continuity)tests/seed-wb-external-debt.test.mjs— 8 fixtures rewritten around USD/GNI shape; new LK 2022-default ground-truth anchortests/seed-bis-lbs.test.mjs— SDMX-JSON fixtures updated to 11-dim CBS shape with CBS-specific dim valuesdocs/methodology/financial-system-exposure.md— Component 1 + 2 sections rewritten; "Common operational footguns" expanded with the LBS→CBS lesson and the WB DSTC.IR.ZS misnomer lessonTest plan
npm run typecheck:api— cleannpm run lint+lint:md— cleannpm run test:data— 7189/7189 tests passPost-Deploy Monitoring & Validation
seed-bundle-macroRailway service logs forWB-External-Debt,BIS-LBSlabels — both should reportstate: OKon every cron tick[bis-cbs]warnings (the seeder emits these only when 1-3 of 16 parent fetches fail; 4+ failures throws)redis-cli GET 'seed-meta:economic:wb-external-debt' | jq '{fetchedAt, recordCount}'— recordCount ≥ 80 (LMICs floor)redis-cli GET 'seed-meta:economic:bis-lbs' | jq '{fetchedAt, recordCount}'— recordCount ≥ 150 (CBS floor)redis-cli GET 'economic:wb-external-debt:v1' | jq '.data.countries.AR.value'— should be 0-15 range, NOT >100redis-cli GET 'economic:bis-lbs:v1' | jq '.data.countries.SG.totalXborderPctGdp'— should be ~200%+ (hub jurisdiction)>100%intermediate ratios in the WB outputtotalXborderPctGdp+ highparentCount(the U-shape design)state: FAILEDfor >24h → check WB API status; revert to PR feat(resilience): financialSystemExposure dim scaffold (Phase 2 Ship 2 — flag-gated dark) #3407 indicators if WB broke their endpointhttps://stats.bis.org/api/v1/dataflow/BISActivation runbook (after this merges)
The same runbook from PR #3407 applies, with the seeders now actually working:
RESILIENCE_FIN_SYS_EXPOSURE_ENABLED=truein VercelFLAG_GATED_DARK_DIMENSIONSallow-list entry intests/resilience-release-gate.test.mts🤖 Generated with Claude Opus 4.7 (1M context, extended thinking) via Claude Code + Compound Engineering v3.0.0