feat(resilience): PR 3A — net-imports denominator for sovereignFiscalBuffer#3380
Conversation
…Buffer
PR 3A of cohort-audit plan 2026-04-24-002. Construct correction for
re-export hubs: the SWF rawMonths denominator was gross imports, which
double-counted flow-through trade that never represents domestic
consumption. Net-imports fix:
rawMonths = aum / (grossImports × (1 − reexportShareOfImports)) × 12
applied to any country in the re-export share manifest. Countries NOT
in the manifest get gross imports unchanged (status-quo fallback).
Plan acceptance gates — verified synthetically in this PR:
Construct invariant. Two synthetic countries, same SWF, same gross
imports. A re-exports 60%; B re-exports 0%. Post-fix, A's rawMonths
is 2.5× B's (1/(1-0.6) = 2.5). Pinned in
tests/resilience-net-imports-denominator.test.mts.
SWF-heavy exporter invariant. Country with share ≤ 5%: rawMonths
lift < 5% vs baseline (negligible). Pinned.
What shipped
1. Re-export share manifest infrastructure.
- scripts/shared/reexport-share-manifest.yaml (new, empty) — schema
committed; entries populated in follow-up PRs with UNCTAD
Handbook citations.
- scripts/shared/reexport-share-loader.mjs (new) — loader + strict
validator, mirrors swf-manifest-loader.mjs.
- scripts/seed-recovery-reexport-share.mjs (new) — publishes
resilience:recovery:reexport-share:v1 from manifest. Empty
manifest = valid (no countries, no adjustment).
2. SWF seeder uses net-imports denominator.
- scripts/seed-sovereign-wealth.mjs exports computeNetImports(gross,
share) — pure helper, unit-tested.
- Per-country loop: reads manifest, computes denominatorImports,
applies to rawMonths math.
- Payload records annualImports (gross, audit), denominatorImports
(used in math), reexportShareOfImports (provenance).
- Summary log reports which countries had a net-imports adjustment
applied with source year.
3. Bundle wiring.
- Reexport-Share runs BEFORE Sovereign-Wealth in the recovery
bundle so the SWF seeder reads fresh re-export data in the same
cron tick.
- tests/seed-bundle-resilience-recovery.test.mjs expected-entries
updated (6 → 7) with ordering preservation.
4. Cache-prefix bump (per cache-prefix-bump-propagation-scope skill).
- RESILIENCE_SCORE_CACHE_PREFIX: v11 → v12
- RESILIENCE_RANKING_CACHE_KEY: v11 → v12
- RESILIENCE_HISTORY_KEY_PREFIX: v6 → v7 (history rotation prevents
30-day rolling window from mixing pre/post-fix scores and
manufacturing false "falling" trends on deploy day).
- Source of truth: server/worldmonitor/resilience/v1/_shared.ts
- Mirrored in: scripts/seed-resilience-scores.mjs,
scripts/validate-resilience-correlation.mjs,
scripts/backtest-resilience-outcomes.mjs,
scripts/validate-resilience-backtest.mjs,
scripts/benchmark-resilience-external.mjs, api/health.js
- Test literals bumped in 4 test files (26 line edits).
- EXTENDED tests/resilience-cache-keys-health-sync.test.mts with
a parity pass that reads every known mirror file and asserts
both (a) canonical prefix present AND (b) no stale v<older>
literals in non-comment code. Found one legacy log-line that
still referenced v9 (scripts/seed-resilience-scores.mjs:342)
and refactored it to use the RESILIENCE_RANKING_CACHE_KEY
constant so future bumps self-update.
Explicitly NOT in this PR
- liquidReserveAdequacy denominator fix. The plan's PR 3A wording
mentions both dims, but the RESERVES ratio (WB FI.RES.TOTL.MO) is a
PRE-COMPUTED WB series; applying a post-hoc net-imports adjustment
mixes WB's denominator year with our manifest-year, and the math
change belongs in PR 3B (unified liquidity) where the α calibration
is explicit. This PR stays scoped to sovereignFiscalBuffer.
- Live re-export share entries. The manifest ships EMPTY in this PR;
entries with UNCTAD citations are one-per-PR follow-ups so each
figure is individually auditable.
Verified
- tests/resilience-net-imports-denominator.test.mts — 9 pass (construct
contract: 2.5× ratio gate, monotonicity, boundary rejections,
backward-compat on missing manifest entry, cohort-proportionality,
SWF-heavy-exporter-unchanged)
- tests/reexport-share-loader.test.mts — 7 pass (committed-manifest
shape + 6 schema-violation rejections)
- tests/resilience-cache-keys-health-sync.test.mts — 5 pass (existing 3
+ 2 new parity checks across all mirror files)
- tests/seed-bundle-resilience-recovery.test.mjs — 17 pass (expected
entries bumped to 7)
- npm run test:data — 6714 pass / 0 fail
- npm run typecheck / typecheck:api — green
- npm run lint / lint:md — clean
Deployment notes
Score + ranking + history cache prefixes all bump in the same deploy.
Per established v10→v11 precedent (and the cache-prefix-bump-
propagation-scope skill):
- Score / ranking: 6h TTL — the new prefix populates via the Railway
resilience-scores cron within one tick.
- History: 30d ring — the v7 ring starts empty; the first 30 days
post-deploy lack baseline points, so trend / change30d will read as
"no change" until v7 accumulates a window.
- Legacy v11 keys can be deleted from Redis at any time post-deploy
(no reader references them). Leaving them in place costs storage
but does no harm.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Greptile SummaryThis PR introduces the net-imports denominator fix for the
Confidence Score: 4/5Safe to merge with the P1 loader bound fix applied — manifest is empty so no country is affected today, but the inconsistency must be closed before the first live entry lands One P1 finding: schema allows share=1.0 but the math function throws on it, so the first manifest entry with that value would crash the seeder in production. The remaining findings are P2 (stale test description strings, after() shim). Since the manifest ships empty this PR, the P1 cannot trigger on landing, but should be fixed before any follow-up entry PRs are merged. scripts/shared/reexport-share-loader.mjs — assertZeroToOne upper-bound needs to be tightened to >= 1 before live manifest entries are added Important Files Changed
Sequence DiagramsequenceDiagram
participant Cron as Railway Cron
participant RS as seed-recovery-reexport-share
participant YAML as reexport-share-manifest.yaml
participant Redis as Redis
participant SWF as seed-sovereign-wealth
participant WB as World Bank API
Cron->>RS: run Reexport-Share step
RS->>YAML: loadReexportShareManifest()
YAML-->>RS: manifest (empty on this PR)
RS->>Redis: SET resilience:recovery:reexport-share:v1
Cron->>SWF: run Sovereign-Wealth step
SWF->>YAML: loadReexportShareByCountry()
YAML-->>SWF: Map<ISO2, {share, year, sources}>
SWF->>WB: fetchAnnualImportsUsd()
WB-->>SWF: grossImports per country
loop per country
SWF->>SWF: computeNetImports(gross, share ?? 0)
SWF->>SWF: rawMonths = aum / denominatorImports x 12
end
SWF->>Redis: SET resilience:recovery:sovereign-wealth:v1
Note over SWF,Redis: payload includes annualImports (gross), denominatorImports (net), reexportShareOfImports
Reviews (1): Last reviewed commit: "feat(resilience): PR 3A — net-imports de..." | Re-trigger Greptile |
| fail(`${path}: expected ISO-3166-1 alpha-2 country code, got ${JSON.stringify(value)}`); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Schema allows
share = 1.0 but computeNetImports rejects it
assertZeroToOne accepts values in [0, 1] (the upper-bound check is value > 1, so 1.0 passes), but computeNetImports guards share >= 1 and throws. A manifest entry written with reexportShareOfImports: 1.0 would be validated and loaded silently, then crash the SWF seeder mid-run for that country. The fix is to tighten the upper bound here to exclude 1:
| fail(`${path}: expected ISO-3166-1 alpha-2 country code, got ${JSON.stringify(value)}`); | |
| } | |
| } | |
| function assertZeroToOne(value, path) { | |
| if (typeof value !== 'number' || Number.isNaN(value) || value < 0 || value >= 1) { | |
| fail(`${path}: expected number in [0, 1), got ${JSON.stringify(value)}`); | |
| } | |
| } |
| it('RESILIENCE_RANKING_CACHE_KEY matches server-side key (v11)', () => { | ||
| assert.equal(RESILIENCE_RANKING_CACHE_KEY, 'resilience:ranking:v11'); | ||
| assert.equal(RESILIENCE_RANKING_CACHE_KEY, 'resilience:ranking:v12'); | ||
| }); | ||
|
|
||
| it('RESILIENCE_SCORE_CACHE_PREFIX matches server-side prefix (v11)', () => { | ||
| assert.equal(RESILIENCE_SCORE_CACHE_PREFIX, 'resilience:score:v11:'); | ||
| assert.equal(RESILIENCE_SCORE_CACHE_PREFIX, 'resilience:score:v12:'); |
There was a problem hiding this comment.
Stale version labels in test descriptions
Both test descriptions still reference (v11) while asserting v12 values. The resilience-pillar-aggregation.test.mts test similarly still reads 'RESILIENCE_SCORE_CACHE_PREFIX is v10' after two cache bumps. These don't affect correctness but make the test output misleading when a bump is missed.
| it('RESILIENCE_RANKING_CACHE_KEY matches server-side key (v11)', () => { | |
| assert.equal(RESILIENCE_RANKING_CACHE_KEY, 'resilience:ranking:v11'); | |
| assert.equal(RESILIENCE_RANKING_CACHE_KEY, 'resilience:ranking:v12'); | |
| }); | |
| it('RESILIENCE_SCORE_CACHE_PREFIX matches server-side prefix (v11)', () => { | |
| assert.equal(RESILIENCE_SCORE_CACHE_PREFIX, 'resilience:score:v11:'); | |
| assert.equal(RESILIENCE_SCORE_CACHE_PREFIX, 'resilience:score:v12:'); | |
| it('RESILIENCE_RANKING_CACHE_KEY matches server-side key (v12)', () => { | |
| assert.equal(RESILIENCE_RANKING_CACHE_KEY, 'resilience:ranking:v12'); | |
| }); | |
| it('RESILIENCE_SCORE_CACHE_PREFIX matches server-side prefix (v12)', () => { | |
| assert.equal(RESILIENCE_SCORE_CACHE_PREFIX, 'resilience:score:v12:'); | |
| }); |
| `); | ||
| await assert.rejects(loadManifestFromPath(path), /last_reviewed: expected YYYY-MM-DD/); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Custom
after() bypasses the node:test lifecycle
The local after() function routes cleanup via process.on('exit', fn) rather than the test runner's own after hook. process.on('exit') callbacks run synchronously during process teardown, which means they execute after the test reporter has already finished — temp files will appear to accumulate between test runs if the process exits uncleanly. Import after from node:test and remove the local shim:
import { describe, it, after } from 'node:test';Then drop the bottom-of-file shim entirely.
…s + flag-count Addresses P1 + 2 P2 Greptile findings on #3376 (draft). 1. **P1 — GIC tier contradiction.** GIC was listed as a canonical 0.7 ("Explicit stabilization with rule") precedent AND rated 0.6 in the alignment table with an "arguably 0.7" note. That inconsistency makes the rubric unusable as-is for PR 4b review. Removed GIC from the 0.7 precedent list and explicitly marked it as a 0.7 *candidate* (pending PR 4b evaluation), not a 0.7 *precedent*. KIA General Reserve Fund stays as the canonical 0.7 example; Norway GPFG remains the borderline case for fiscal-rule caps. 2. **P2 — ADIA liquidity midpoint inconsistency.** Methodology text said the rubric uses "midpoint" for ranged disclosures and cited ADIA 55-70% → 0.7 tier. But midpoint(55-70) = 62.5%, which sits in the 0.5 tier band (50-65%). Fixed the methodology to state the rubric uses the **upper-bound** of a disclosed range (fund's own statement of maximum public-market allocation), which keeps ADIA at 0.7 tier (70% upper bound fits 65-85% band). Added forward- compatibility note: if future ADIA disclosures tighten the range so the upper bound drops below 65%, the rubric directs the rating to 0.5. 3. **P2 — Flag-count header.** "(6 of 24 coefficients)" was wrong; the enumeration below lists 8 coefficients across 5 funds. Corrected to "8 coefficients across 5 funds" with the fund-by-fund count inline so the header math is self-verifying. Verified - `npm run lint:md` — clean - `npm run typecheck` — green (pure docs PR, no behaviour change) This PR remains in draft pending #3380 (PR 3A — net-imports denominator) merge per the plan's PR 4 → after PR 3A sequencing.
…ts, no manifest changes) (#3376) * docs(resilience): PR 4a — SWF classification rubric (tiers + precedents, no manifest changes) PR 4a of cohort-audit plan 2026-04-24-002. First half of the plan's PR 4 (full-manifest re-rate) split into: - PR 4a (this): pure documentation — central rubric defining tiers + concrete precedents per axis. No manifest changes. - PR 4b (deferred): apply the rubric to revise specific coefficients in `scripts/shared/swf-classification-manifest.yaml`. Behaviour- changing; belongs in a separate PR with cohort snapshots and methodology review. This split addresses the plan's concern that PR 4 "may not be outcome- predetermined" by separating the evaluative framework from its application. PR 4a makes every current manifest value evaluable against a benchmark; PR 4b applies the benchmark. Shipped - `docs/methodology/swf-classification-rubric.md` — new doc. Sections: 1. Introduction + scope (rubric vs manifest boundary) 2. Access axis: 5 named tiers (0.1, 0.3, 0.5, 0.7, 0.9) w/ concrete precedents per tier, plus edge cases for fiscal-rule caps (Norway GPFG) and state holding companies (Temasek) 3. Liquidity axis: 6 tiers (0.1, 0.3, 0.5, 0.7, 0.9, 1.0) w/ precedents + listed-vs-directly-owned real-estate edge case 4. Transparency axis: 6 tiers grounded in LM Transparency Index + IFSWF membership + annual-report granularity, plus edge cases for LM=10 w/o holdings-level disclosure and sealed filings (KIA) 5. Current manifest × rubric alignment — 24 coefficients reviewed; 6 flagged as "arguably higher/lower under the rubric" with directional-impact analysis marked INFORMATIONAL, not motivation for revision 6. How-to-use playbook for manifest-edit PRs (add/revise/rubric- revise workflows) Findings (informational only — no PR changes) Six ratings flagged as potentially under-/over-stated against the rubric. Per the plan's anti-pattern note (rank-targeted acceptance criteria), the flags are INFORMATIONAL: a future manifest-edit PR should revise only when the rubric + cited evidence support the change, not to hit a target ranking. Flagged (with directional impact if revised upward): - Mubadala access 0.4 → arguably 0.5; transparency 0.6 → 0.7 (haircut 0.12 → 0.175, +46% access × transparency product) - PIF access 0.4 → arguably 0.5; liquidity 0.4 → arguably 0.3 (net small effect — opposite directions partially cancel) - KIA transparency 0.4 → arguably 0.5 (haircut +25%) - QIA access 0.4 → arguably 0.5; transparency 0.4 → arguably 0.5 (haircut +56%) - GIC access 0.6 → arguably 0.7 (haircut +17%) Not flagged: GPFG, ADIA, Temasek (all 9 coefficients align with their rubric tiers). Verified - `npm run test:data` — 6694 pass / 0 fail (unchanged — pure docs PR) - `npm run typecheck` / `typecheck:api` — green - `npm run lint:md` — clean Not in this PR - Manifest coefficient changes (PR 4b) - Cohort-sanity snapshot before/after (PR 4b) - Live-data audit of IFSWF engagement + LM index current values (requires web fetch — not in scope for a doc PR) * fix(resilience): PR 4a review — resolve GIC/ADIA rubric contradictions + flag-count Addresses P1 + 2 P2 Greptile findings on #3376 (draft). 1. **P1 — GIC tier contradiction.** GIC was listed as a canonical 0.7 ("Explicit stabilization with rule") precedent AND rated 0.6 in the alignment table with an "arguably 0.7" note. That inconsistency makes the rubric unusable as-is for PR 4b review. Removed GIC from the 0.7 precedent list and explicitly marked it as a 0.7 *candidate* (pending PR 4b evaluation), not a 0.7 *precedent*. KIA General Reserve Fund stays as the canonical 0.7 example; Norway GPFG remains the borderline case for fiscal-rule caps. 2. **P2 — ADIA liquidity midpoint inconsistency.** Methodology text said the rubric uses "midpoint" for ranged disclosures and cited ADIA 55-70% → 0.7 tier. But midpoint(55-70) = 62.5%, which sits in the 0.5 tier band (50-65%). Fixed the methodology to state the rubric uses the **upper-bound** of a disclosed range (fund's own statement of maximum public-market allocation), which keeps ADIA at 0.7 tier (70% upper bound fits 65-85% band). Added forward- compatibility note: if future ADIA disclosures tighten the range so the upper bound drops below 65%, the rubric directs the rating to 0.5. 3. **P2 — Flag-count header.** "(6 of 24 coefficients)" was wrong; the enumeration below lists 8 coefficients across 5 funds. Corrected to "8 coefficients across 5 funds" with the fund-by-fund count inline so the header math is self-verifying. Verified - `npm run lint:md` — clean - `npm run typecheck` — green (pure docs PR, no behaviour change) This PR remains in draft pending #3380 (PR 3A — net-imports denominator) merge per the plan's PR 4 → after PR 3A sequencing.
…+ net-imports parity (v17.1) (#3482) * feat(resilience): plan 002 §U8 — methodology rewrite, doc-parity test, widget polish + net-imports parity for liquidReserveAdequacy (v17.1) Closes the plan 2026-04-26-002 sequence. Five things land together: 1. **§U8.1 — net-imports denominator parity for liquidReserveAdequacy.** PR #3380 shipped `computeNetImports(grossImports, reexportShareOfImports)` for `sovereignFiscalBuffer` via the SWF seeder, sourced from `resilience:recovery:reexport-share:v1` (Comtrade-backed via PR #3385). The same correction was structurally needed on the sibling `liquidReserveAdequacy` dimension — re-export hubs (AE 35.5%, PA similar) consume WB `FI.RES.TOTL.MO` which is computed at WB source against gross imports, double-counting goods that flow through without settling as domestic consumption. v17.1 multiplies WB's pre-computed months by `1/(1−reexportShare)` for hub countries (algebraic inverse of dividing the denominator) — yields the same adjusted-months a custom `reserves/(net-imports/12)` calc would, without re-fetching raw `FI.RES.TOTL.CD` + `BM.GSR.GNFS.CD` series. Non-hub countries unchanged (status quo). Expected impact at next ranking refresh: AE liquidReserveAdequacy 38 → ~64 (a +26-point dim swing); PA similar magnitude. 2. **§U8 — methodology doc comprehensive v17 changelog.** Single coherent narrative covering the universe + coverage rebuild: source-comprehensiveness flag, coverage penalty multiplier, per-capita normalization with 0.5M tiny-state floor, headline- eligible gate, symmetric cache-hit gate filtering. Anchored to the live `resilience:ranking:v17` cohort numbers captured 2026-04-28 post-#3477 merge: median(Nordics)=78.52 vs median(GCC)=70.53 (+7.98pt gap), min(G7)=64.31 vs max(LIC)=53.73 (+10.58pt gap), 1 microstate (MO at #4) in top-20. Documents 3 known open construct gaps honestly: economic-complexity / industrial-base indicator, importConcentration coverage gap on AE, cyberDigital transient zeros. 3. **§U8 — new doc-parity test (`tests/resilience-doc-parity.test.mts`).** Asserts the doc's prose claims match `_shared.ts` (cache prefix constants), `RESILIENCE_DOMAIN_ORDER` (6 domains), and `RESILIENCE_DIMENSION_ORDER` − `RESILIENCE_RETIRED_DIMENSIONS` (20 active dimensions). Caught real drift: Redis keys table named v11 prefixes despite live v17, and the headline "19 dimensions" claim was off-by-one after PR 2 §3.4 added liquidReserveAdequacy + sovereignFiscalBuffer. Fixed both as part of this commit. 4. **§U8 — widget polish.** `formatResilienceConfidence` now renders "Outside headline ranking" when `headlineEligible: false`, distinct from "Low confidence — sparse data" when `lowConfidence: true`. The two reasons are different and analysts should see them as different. Order matters: lowConfidence is more specific so it wins when both flags fire. New test pins the precedence. 5. **MDX-unsafe `<digit/letter` sweep + `lint:md`.** Both clean. 89 dimension-scorer tests + 36 widget tests + 5 doc-parity tests + 41 ranking + headline-gate tests all green (171 total, 0 fail). * fix(resilience): demote v17.1 changelog H4 to bold-paragraph (methodology-lint compat) CI methodology-lint test (T1.8) requires every H4 in docs/methodology/country-resilience-index.mdx to map to a known scorer dimension via HEADING_TO_DIMENSION — that's how the doc-vs- registry parity gate works. The v17.1 changelog sub-section I added (`#### v17.1 — Net-imports denominator parity for liquidReserveAdequacy (U8.1)`) is a non-dimension subsection and tripped the linter. Demoted to a bold lead-in paragraph (`**v17.1 — …**`). Same content, same prose, just no H4. The lint comment explicitly anticipates this case ("if a future edit adds a legitimate non-dimension H4, either upgrade it to H3 or add an explicit allowlist") — bold-paragraph is the cleanest of the three options and matches how Mintlify renders sub-changelog notes elsewhere in the doc. Local re-run: methodology-lint + doc-parity 10/10 pass; full test:data suite 7606/7606 pass, 0 fail. * fix(resilience): plan 002 §U8 review fixes (P1 cache bump v17→v18, P2 doc TTL/dim drift, P2 stale-count tightening) Three review findings + a Greptile P2 perf note. All addressed locally. P1 — cache invalidation missing for the §U8.1 scorer-formula change. liquidReserveAdequacy now adjusts reserveMonths by re-export share for AE/PA, but the prior commit kept score/ranking at v17 and history at v12. The `_formula` tag is binary 'd6'|'pc' and does NOT detect intra- 'd6' scorer changes — without this bump, cached v17 AE/PA scores (gross-imports-denominated) would continue to serve until TTL expiry, defeating the construct fix this PR delivers. Bumped: - RESILIENCE_SCORE_CACHE_PREFIX v17 → v18 (_shared.ts:164) - RESILIENCE_RANKING_CACHE_KEY v17 → v18 (_shared.ts:235) - RESILIENCE_HISTORY_KEY_PREFIX v12 → v13 (_shared.ts:217) - script mirrors: seed-resilience-scores.mjs, backtest, validate, benchmark - api/health.js: resilienceRanking key History bumps in lockstep so the rolling 30-day window doesn't mix pre/post-fix points and manufacture a false "improving" trend on day one. Same pattern as PR 3A's v11→v12 lockstep for the SWF-side fix. P2 — Redis keys table TTL + write-gate prose drift. Doc claimed: resilience:ranking:v17 | 6 hours | "only when all countries are scored" Actual code: 12h TTL (RESILIENCE_RANKING_CACHE_TTL_SECONDS) and publishes when coverage >= RANKING_CACHE_MIN_COVERAGE = 0.75. Updated the table to reflect both. Same correction caught by the parity test that flagged the v11/v17 drift in the prior commit. P2 — stale "19 dimensions" claim still in the doc. The v2.0 scorecard Data row at line 750 still said "19 dimensions across 6 domains" because the parity test only required ONE correct mention. Tightened the test to fail on any "<N> dimensions" mention in the plausible-current-total band [15, 25] that doesn't equal activeCount or totalCount — historical sub-counts (e.g. v1.0's "13 dimensions", recovery pillar's "6 dimensions") are outside the band and stay untouched. Updated the stale line to "20 active dimensions across 6 domains (plus 2 structurally-retired dimensions)". Bonus — addressed a load of test-side cache-key literal duplications (plan 002 §U8 follow-up): - tests/resilience-ranking.test.mts: 27 hardcoded 'resilience:score:v17:XX' / 'resilience:history:v12:XX' literals swapped to ${RESILIENCE_SCORE_CACHE_PREFIX}/${RESILIENCE_HISTORY_KEY_PREFIX} template-literal references. - tests/resilience-handlers.test.mts: same. - tests/resilience-pillar-aggregation.test.mts: pinned-to-version assertion replaced with structural-shape regex. - tests/resilience-scores-seed.test.mjs: same. This was caught by PR #3477 review for the ranking key; the score and history prefixes had the same drift trap that resurfaced now. The proper fix per the project skill (proto-required-field-add-needs-cache- backfill Trap 5b) is to import the constants — never hardcode literals. Greptile P2 (per-country Redis read for reexport-share map) — verified benign: createMemoizedSeedReader caches by key, and _shared.ts:992's `sharedReader` is shared across all 196 countries in the warm path (getOrComputeResilienceScores), so reexport-share is fetched ONCE per ranking compute, not 196 times. Single-country handler creates a fresh memoized reader per request → 1 read per request. No fix needed. Verification: 214/214 resilience tests pass, 7606/7606 npm test:data pass, typecheck clean, lint:md clean.
…+ net-imports parity (v17.1) (koala73#3482) * feat(resilience): plan 002 §U8 — methodology rewrite, doc-parity test, widget polish + net-imports parity for liquidReserveAdequacy (v17.1) Closes the plan 2026-04-26-002 sequence. Five things land together: 1. **§U8.1 — net-imports denominator parity for liquidReserveAdequacy.** PR koala73#3380 shipped `computeNetImports(grossImports, reexportShareOfImports)` for `sovereignFiscalBuffer` via the SWF seeder, sourced from `resilience:recovery:reexport-share:v1` (Comtrade-backed via PR koala73#3385). The same correction was structurally needed on the sibling `liquidReserveAdequacy` dimension — re-export hubs (AE 35.5%, PA similar) consume WB `FI.RES.TOTL.MO` which is computed at WB source against gross imports, double-counting goods that flow through without settling as domestic consumption. v17.1 multiplies WB's pre-computed months by `1/(1−reexportShare)` for hub countries (algebraic inverse of dividing the denominator) — yields the same adjusted-months a custom `reserves/(net-imports/12)` calc would, without re-fetching raw `FI.RES.TOTL.CD` + `BM.GSR.GNFS.CD` series. Non-hub countries unchanged (status quo). Expected impact at next ranking refresh: AE liquidReserveAdequacy 38 → ~64 (a +26-point dim swing); PA similar magnitude. 2. **§U8 — methodology doc comprehensive v17 changelog.** Single coherent narrative covering the universe + coverage rebuild: source-comprehensiveness flag, coverage penalty multiplier, per-capita normalization with 0.5M tiny-state floor, headline- eligible gate, symmetric cache-hit gate filtering. Anchored to the live `resilience:ranking:v17` cohort numbers captured 2026-04-28 post-koala73#3477 merge: median(Nordics)=78.52 vs median(GCC)=70.53 (+7.98pt gap), min(G7)=64.31 vs max(LIC)=53.73 (+10.58pt gap), 1 microstate (MO at #4) in top-20. Documents 3 known open construct gaps honestly: economic-complexity / industrial-base indicator, importConcentration coverage gap on AE, cyberDigital transient zeros. 3. **§U8 — new doc-parity test (`tests/resilience-doc-parity.test.mts`).** Asserts the doc's prose claims match `_shared.ts` (cache prefix constants), `RESILIENCE_DOMAIN_ORDER` (6 domains), and `RESILIENCE_DIMENSION_ORDER` − `RESILIENCE_RETIRED_DIMENSIONS` (20 active dimensions). Caught real drift: Redis keys table named v11 prefixes despite live v17, and the headline "19 dimensions" claim was off-by-one after PR 2 §3.4 added liquidReserveAdequacy + sovereignFiscalBuffer. Fixed both as part of this commit. 4. **§U8 — widget polish.** `formatResilienceConfidence` now renders "Outside headline ranking" when `headlineEligible: false`, distinct from "Low confidence — sparse data" when `lowConfidence: true`. The two reasons are different and analysts should see them as different. Order matters: lowConfidence is more specific so it wins when both flags fire. New test pins the precedence. 5. **MDX-unsafe `<digit/letter` sweep + `lint:md`.** Both clean. 89 dimension-scorer tests + 36 widget tests + 5 doc-parity tests + 41 ranking + headline-gate tests all green (171 total, 0 fail). * fix(resilience): demote v17.1 changelog H4 to bold-paragraph (methodology-lint compat) CI methodology-lint test (T1.8) requires every H4 in docs/methodology/country-resilience-index.mdx to map to a known scorer dimension via HEADING_TO_DIMENSION — that's how the doc-vs- registry parity gate works. The v17.1 changelog sub-section I added (`#### v17.1 — Net-imports denominator parity for liquidReserveAdequacy (U8.1)`) is a non-dimension subsection and tripped the linter. Demoted to a bold lead-in paragraph (`**v17.1 — …**`). Same content, same prose, just no H4. The lint comment explicitly anticipates this case ("if a future edit adds a legitimate non-dimension H4, either upgrade it to H3 or add an explicit allowlist") — bold-paragraph is the cleanest of the three options and matches how Mintlify renders sub-changelog notes elsewhere in the doc. Local re-run: methodology-lint + doc-parity 10/10 pass; full test:data suite 7606/7606 pass, 0 fail. * fix(resilience): plan 002 §U8 review fixes (P1 cache bump v17→v18, P2 doc TTL/dim drift, P2 stale-count tightening) Three review findings + a Greptile P2 perf note. All addressed locally. P1 — cache invalidation missing for the §U8.1 scorer-formula change. liquidReserveAdequacy now adjusts reserveMonths by re-export share for AE/PA, but the prior commit kept score/ranking at v17 and history at v12. The `_formula` tag is binary 'd6'|'pc' and does NOT detect intra- 'd6' scorer changes — without this bump, cached v17 AE/PA scores (gross-imports-denominated) would continue to serve until TTL expiry, defeating the construct fix this PR delivers. Bumped: - RESILIENCE_SCORE_CACHE_PREFIX v17 → v18 (_shared.ts:164) - RESILIENCE_RANKING_CACHE_KEY v17 → v18 (_shared.ts:235) - RESILIENCE_HISTORY_KEY_PREFIX v12 → v13 (_shared.ts:217) - script mirrors: seed-resilience-scores.mjs, backtest, validate, benchmark - api/health.js: resilienceRanking key History bumps in lockstep so the rolling 30-day window doesn't mix pre/post-fix points and manufacture a false "improving" trend on day one. Same pattern as PR 3A's v11→v12 lockstep for the SWF-side fix. P2 — Redis keys table TTL + write-gate prose drift. Doc claimed: resilience:ranking:v17 | 6 hours | "only when all countries are scored" Actual code: 12h TTL (RESILIENCE_RANKING_CACHE_TTL_SECONDS) and publishes when coverage >= RANKING_CACHE_MIN_COVERAGE = 0.75. Updated the table to reflect both. Same correction caught by the parity test that flagged the v11/v17 drift in the prior commit. P2 — stale "19 dimensions" claim still in the doc. The v2.0 scorecard Data row at line 750 still said "19 dimensions across 6 domains" because the parity test only required ONE correct mention. Tightened the test to fail on any "<N> dimensions" mention in the plausible-current-total band [15, 25] that doesn't equal activeCount or totalCount — historical sub-counts (e.g. v1.0's "13 dimensions", recovery pillar's "6 dimensions") are outside the band and stay untouched. Updated the stale line to "20 active dimensions across 6 domains (plus 2 structurally-retired dimensions)". Bonus — addressed a load of test-side cache-key literal duplications (plan 002 §U8 follow-up): - tests/resilience-ranking.test.mts: 27 hardcoded 'resilience:score:v17:XX' / 'resilience:history:v12:XX' literals swapped to ${RESILIENCE_SCORE_CACHE_PREFIX}/${RESILIENCE_HISTORY_KEY_PREFIX} template-literal references. - tests/resilience-handlers.test.mts: same. - tests/resilience-pillar-aggregation.test.mts: pinned-to-version assertion replaced with structural-shape regex. - tests/resilience-scores-seed.test.mjs: same. This was caught by PR koala73#3477 review for the ranking key; the score and history prefixes had the same drift trap that resurfaced now. The proper fix per the project skill (proto-required-field-add-needs-cache- backfill Trap 5b) is to import the constants — never hardcode literals. Greptile P2 (per-country Redis read for reexport-share map) — verified benign: createMemoizedSeedReader caches by key, and _shared.ts:992's `sharedReader` is shared across all 196 countries in the warm path (getOrComputeResilienceScores), so reexport-share is fetched ONCE per ranking compute, not 196 times. Single-country handler creates a fresh memoized reader per request → 1 read per request. No fix needed. Verification: 214/214 resilience tests pass, 7606/7606 npm test:data pass, typecheck clean, lint:md clean.
Summary
PR 3A of the cohort-audit plan (
docs/plans/2026-04-24-002-fix-resilience-cohort-ranking-structural-audit-plan.md). The construct correction: the SWFrawMonthsdenominator was gross imports, which double-counted flow-through trade that never represents domestic consumption for re-export hub economies.Countries NOT in the re-export manifest get gross imports unchanged (status-quo fallback).
Cut from
origin/main— not stacked. Required by PR 4 (which is currently drafted as #3376).Plan acceptance gates — verified synthetically
What shipped
1. Re-export share manifest infrastructure
scripts/shared/reexport-share-manifest.yaml(new, empty) — schema committed. Candidates documented inline (HK, SG, NL, BE, PA, AE, MY, LT). Entries populated one-by-one in follow-up PRs with UNCTAD Handbook citations — one figure per PR so each is individually auditable.scripts/shared/reexport-share-loader.mjs(new) — loader + strict validator mirroringswf-manifest-loader.mjspattern. Fail-closed on any schema violation.scripts/seed-recovery-reexport-share.mjs(new) — publishesresilience:recovery:reexport-share:v1from the manifest. Empty manifest = valid (no countries = no adjustments).2. SWF seeder uses net-imports denominator
scripts/seed-sovereign-wealth.mjsexportscomputeNetImports(gross, share)— pure helper, unit-tested.denominatorImports, applies torawMonths. Payload now records all three values (annualImportsgross for audit,denominatorImportsused in math,reexportShareOfImportsfor provenance).3. Bundle wiring
Reexport-Shareruns BEFORESovereign-Wealthin the recovery bundle so the SWF seeder reads fresh re-export data in the same cron tick.tests/seed-bundle-resilience-recovery.test.mjsexpected-entries bumped 6 → 7.4. Cache-prefix bump (per
cache-prefix-bump-propagation-scopeskill)RESILIENCE_SCORE_CACHE_PREFIXresilience:score:v11:resilience:score:v12:RESILIENCE_RANKING_CACHE_KEYresilience:ranking:v11resilience:ranking:v12RESILIENCE_HISTORY_KEY_PREFIXresilience:history:v6:resilience:history:v7:Sites bumped (source + 6 mirrors + 4 test files = 11 files):
server/worldmonitor/resilience/v1/_shared.tsscripts/seed-resilience-scores.mjs,scripts/validate-resilience-correlation.mjs,scripts/backtest-resilience-outcomes.mjs,scripts/validate-resilience-backtest.mjs,scripts/benchmark-resilience-external.mjs,api/health.jstests/resilience-ranking.test.mts(29 edits),tests/resilience-handlers.test.mts(4),tests/resilience-scores-seed.test.mjs(2),tests/resilience-pillar-aggregation.test.mts(1)New parity test in
tests/resilience-cache-keys-health-sync.test.mts— reads every declared mirror file, asserts (a) canonical prefix present AND (b) no stale v literals in non-comment code. Caught and fixed a legacy log-line inscripts/seed-resilience-scores.mjsthat still referencedv9; refactored to use theRESILIENCE_RANKING_CACHE_KEYconstant so future bumps self-update.History v6→v7 is critical: per the skill's "history-trap" note, a 30-day rolling window that mixes v11 (gross-imports) and v12 (net-imports) scores manufactures false "falling" trends on deploy day. Rotation forces a clean window.
What this PR explicitly does NOT do
liquidReserveAdequacydenominator fix. The plan's PR 3A wording mentions both dims, but WB'sFI.RES.TOTL.MOis a PRE-COMPUTED ratio; applying a post-hoc net-imports adjustment mixes WB's denominator year with our manifest year, and the math change belongs in PR 3B (unified liquidity) where α calibration is explicit.Testing
tests/resilience-net-imports-denominator.test.mts— 9 pass (construct contract + cohort invariants)tests/reexport-share-loader.test.mts— 7 pass (committed-manifest shape + 6 schema violations)tests/resilience-cache-keys-health-sync.test.mts— 5 pass (existing 3 + 2 new mirror parity checks)tests/seed-bundle-resilience-recovery.test.mjs— 17 pass (bumped 6 → 7)npm run test:data— 6714 pass / 0 failnpm run typecheck/typecheck:api— greennpm run lint/lint:md— cleanPost-Deploy Monitoring & Validation
What to monitor/search
seed-bundle-resilience-recovery— specifically the newReexport-Sharesection + the SWF seeder's "re-export manifest is empty" / "re-export adjustment applied to N country/countries" line.resilience:recovery:reexport-share:v1populated,resilience:recovery:sovereign-wealth:v1containsdenominatorImports+reexportShareOfImportsfields per country,resilience:score:v12:*populating from cold,resilience:ranking:v12present./api/healthmust remain HEALTHY;resilienceRankinghealth check now watchesv12.Validation checks (queries/commands)
Expected healthy behavior
"re-export manifest is empty; all countries use gross imports as the rawMonths denominator (status-quo behaviour)"(because no entries shipped in this PR)./api/healthshowsresilienceRanking→ HEALTHY onv12within 1h of deploy.Failure signal(s) / rollback trigger
v11(not rebuilding onv12): investigate seed-resilience-scores cron + ranking cache./api/healthCRIT onresilience:recovery:reexport-share: first-run gate; give it one cron cycle, then investigate.Validation window & owner
Related
docs/plans/2026-04-24-002-fix-resilience-cohort-ranking-structural-audit-plan.md§PR 3Acache-prefix-bump-propagation-scope,worldmonitor-scripts-package-json-install-scope(yaml dep already present),production-logic-mirror-silent-divergence(new parity test covers this)🤖 Generated with Claude Opus 4.7 (1M context) via Claude Code + Compound Engineering v2.49.0
Co-Authored-By: Claude Opus 4.7 (1M context) [email protected]