Skip to content

feat(resilience): plan 002 §U8 — methodology rewrite + widget polish + net-imports parity (v17.1)#3482

Merged
koala73 merged 3 commits into
mainfrom
feat/resilience-u8-methodology-widget-v17
Apr 28, 2026
Merged

feat(resilience): plan 002 §U8 — methodology rewrite + widget polish + net-imports parity (v17.1)#3482
koala73 merged 3 commits into
mainfrom
feat/resilience-u8-methodology-widget-v17

Conversation

@koala73

@koala73 koala73 commented Apr 28, 2026

Copy link
Copy Markdown
Owner

Summary

Closes the plan 2026-04-26-002 sequence (PRs #3425#3477) with U8.

  • §U8.1 — net-imports denominator parity for liquidReserveAdequacy. PR feat(resilience): PR 3A — net-imports denominator for sovereignFiscalBuffer #3380 / feat(resilience): Comtrade-backed re-export-share seeder + SWF Redis read #3385 shipped re-export-adjusted denominators for sovereignFiscalBuffer via the SWF seeder. The same correction was structurally needed on the sibling reserves-in-months dimension, which consumes WB FI.RES.TOTL.MO (computed at WB source against gross imports) and over-penalizes hubs. v17.1 multiplies WB's pre-computed months by 1 / (1 − reexportShare) for hub countries (today: AE 35.5%, PA similar) — algebraic inverse of dividing the denominator. Non-hubs unchanged. Expected impact at next ranking refresh: AE liquidReserveAdequacy ≈38 → ≈64 (a +26-point dim swing); PA similar.
  • §U8 — methodology doc comprehensive v17 changelog. Universe + coverage rebuild narrated as a single coherent story (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: median(Nordics)=78.52 vs median(GCC)=70.53 (+7.98pt), min(G7)=64.31 vs max(LIC)=53.73 (+10.58pt), 1 microstate (MO Add related-asset cards to clusters and map highlighting for infrastructure #4) in top-20. Documents 3 known open construct gaps honestly.
  • §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 active-dim count (ORDER − RETIRED = 20). 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. Both fixed in this PR.
  • §U8 — widget polish. formatResilienceConfidence now renders "Outside headline ranking" when headlineEligible: false, distinct from "Low confidence — sparse data" when lowConfidence: true. Order matters: lowConfidence is more specific so it wins when both flags fire.

Test plan

All checks green from the worktree:

  • npm run typecheck — clean
  • Convex tsconfig typecheck — clean
  • npm run lint:md — 0 errors
  • npm run lint:boundaries — no violations
  • npm run lint:rate-limit-policies — 7 policies clean
  • npm run lint:premium-fetch — parity clean
  • npm run version:check — 2.8.0 in sync
  • tests/resilience-dimension-scorers.test.mts — 89 tests pass (4 new for U8.1 net-imports adjustment)
  • tests/resilience-widget.test.mts — 36 tests pass (3 new for the eligibility-aware confidence label)
  • tests/resilience-doc-parity.test.mts — 5 tests pass (new file)
  • tests/resilience-headline-eligible-gate.test.mts + tests/resilience-ranking.test.mts — full plan-002 regression suite green
  • tests/handlers.test.mts + tests/server-handlers.test.mjs — 102 tests pass
  • tests/edge-functions.test.mjs — clean
  • MDX-unsafe <digit/letter sweep — clean

Post-merge ops note

No cache-prefix bump (the v17 prefix was just bumped 24h ago in #3469; another bump would force needless rebuild). The §U8.1 formula change for AE + PA flows through on the next ranking refresh (within the 6h cache TTL). Trend metric will show a one-time step at deploy time for these two countries; this is the corrected baseline going forward.

Honest construct gaps documented in the doc

  • Economic-complexity / industrial-base indicator (Atlas/ECI) — flagged as a candidate construct expansion, not a v17 correction.
  • importConcentration coverage gap on AE (UN Comtrade falls through to unmonitored impute despite data being available) — likely a seeder gap, follow-up.
  • cyberDigital transient zeros — high dimension volatility on a 24-72h window. Smoothing-refinement is plan-deferred.

…, 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).
@mintlify

mintlify Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

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

Project Status Preview Updated (UTC)
WorldMonitor 🟢 Ready View Preview Apr 28, 2026, 5:43 AM

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

@vercel

vercel Bot commented Apr 28, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment Apr 28, 2026 6:12am

Request Review

@greptile-apps

greptile-apps Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR closes plan 2026-04-26-002 §U8 with three coordinated changes: a re-export-share denominator correction for liquidReserveAdequacy (mirroring the existing fix on sovereignFiscalBuffer), a distinct "Outside headline ranking" widget badge when headlineEligible=false, and a new doc-parity test that caught and fixed stale v11 cache-key references and an off-by-one dimension count. The validation logic in readReexportShareForCountry is solid and the test suite covers all edge cases well.

Confidence Score: 4/5

Safe to merge; all findings are P2 style/performance suggestions with no correctness impact.

No P0 or P1 issues. Three P2 findings: the per-country Redis fetch pattern in readReexportShareForCountry (may be redundant reads for non-hub countries), a merged comment block that makes a helper's docstring look like it's part of the parent function's description, and a stale inline comment in the new parity test referencing the old "19 dimensions" count.

server/worldmonitor/resilience/v1/_dimension-scorers.ts — confirm whether defaultSeedReader deduplicates within a scoring run; if not, the extra per-country reexport-share fetch is worth addressing before scaling to more hub countries.

Important Files Changed

Filename Overview
server/worldmonitor/resilience/v1/_dimension-scorers.ts Adds readReexportShareForCountry helper and applies net-imports re-export adjustment in scoreLiquidReserveAdequacy; validation and clamping logic are correct, but the helper fetches the full reexport-share map from Redis on every country scored (P2).
src/components/resilience-widget-utils.ts Adds headlineEligible=false branch to formatResilienceConfidence; ordering (lowConfidence wins when both fire) is correct and well-tested.
tests/resilience-dimension-scorers.test.mts Four new tests for the re-export adjustment path (no-hub passthrough, hub adjustment, ceiling clamp, malformed-share rejection) — coverage is thorough and edge cases are well chosen.
tests/resilience-doc-parity.test.mts New parity test asserting doc prose matches live constants; a stale "19 dimensions" inline comment needs updating to reflect the post-fix count of 20 (P2).
tests/resilience-widget.test.mts Three new widget tests for the headlineEligible label path; all cases (false, true, both-flags) are covered and assertions are correct.
docs/methodology/country-resilience-index.mdx Doc updated from "19 dimensions" to "20 active dimensions", cache key references corrected from v11→v17/v12, and a comprehensive v17 + v17.1 changelog section added with empirical anchors.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[scoreLiquidReserveAdequacy\ncountryCode, reader] --> B[Fetch reserve-adequacy key]
    B --> C{entry found &\nreserveMonths != null?}
    C -- No --> D[Return IMPUTE defaults\ncoverage=0, imputedWeight=1]
    C -- Yes --> E[readReexportShareForCountry\ncountryCode, reader]
    E --> F[Fetch reexport-share:v1 map]
    F --> G{share valid?\ntypeof number &&\nfinite && in 0..1}
    G -- No --> H[share = null]
    G -- Yes --> I[share = reexportShareOfImports]
    H --> J[adjustedMonths = reserveMonths]
    I --> K[adjustedMonths = reserveMonths / 1-share]
    J --> L[normalizeHigherBetter\nmin adjustedMonths 12, 1, 12]
    K --> L
    L --> M[weightedBlend → ResilienceDimensionScore]
Loading

Comments Outside Diff (2)

  1. server/worldmonitor/resilience/v1/_dimension-scorers.ts, line 2064-2083 (link)

    P2 Two function descriptions merged into one block without a separator

    The existing // PR 2 §3.4 — new dimension replacing... paragraph describes scoreLiquidReserveAdequacy, but the new "Re-export adjustment…" paragraph that follows it (no blank line) describes the helper readReexportShareForCountry. As written, both look like they're annotating the same function, making the block harder to parse at a glance.

  2. tests/resilience-doc-parity.test.mts, line 379-392 (link)

    P2 Stale inline comment references the old count ("19 dimensions")

    The comment says // The doc says "19 dimensions", but this PR is the one correcting the doc from 19 → 20. After the fix the doc says "20 active dimensions", so the comment's historical framing is immediately misleading to the next reader. Worth updating to reflect the current state.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "feat(resilience): plan 002 §U8 — methodo..." | Re-trigger Greptile

Comment on lines +2084 to +2094
async function readReexportShareForCountry(
countryCode: string,
reader: ResilienceSeedReader,
): Promise<number | null> {
const raw = await reader(RESILIENCE_RECOVERY_REEXPORT_SHARE_KEY);
const payload = raw as { countries?: Record<string, { reexportShareOfImports?: number | null } | undefined> } | null | undefined;
const share = payload?.countries?.[countryCode]?.reexportShareOfImports;
if (typeof share !== 'number' || !Number.isFinite(share)) return null;
if (share < 0 || share >= 1) return null;
return share;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Extra Redis read per country on every scoring cycle

readReexportShareForCountry fetches RESILIENCE_RECOVERY_REEXPORT_SHARE_KEY once per call to scoreLiquidReserveAdequacy, meaning the full reexport-share map is pulled from Redis for every one of the ~220 countries scored, even though only 2 entries (AE + PA) will ever yield a non-null result. If defaultSeedReader doesn't deduplicate/cache within a single scoring run, that's ~220 redundant network round-trips per 6-hour cycle.

A lightweight fix would be to accept the already-fetched map as an optional parameter (mirroring how scoreSovereignFiscalBuffer receives pre-fetched SWF data), or to let the caller fetch the map once and pass it down. Alternatively, if the reader already memoises by key within a run, this is a no-op cost — worth confirming.

…logy-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.
… 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.
koala73 added a commit that referenced this pull request Apr 28, 2026
…ence (#3484)

* fix(health): resilienceIntervals maxStaleMin matches real 6h cron cadence

The prior comment claimed `intervalMs=2h with hourly Railway fires +
96min skip-window → effective ~2h cadence` and set `maxStaleMin: 360`
(= 1× claimed cadence). The deployed Railway cron is actually
`0 */6 * * *` (every 6h on the hour, UTC), confirmed by the user
2026-04-28 and corroborated by Railway logs: only 2 successful runs
in the seed-bundle-resilience service log over 6h2min, both clean,
both writing 196 interval keys.

With true cadence=6h and maxStaleMin=360min, ANY normal cron jitter
or a slightly-late tick tips resilienceIntervals into STALE_SEED.
Reproduced 2026-04-28: seedAgeMin=367 vs maxStale=360 → false alarm
despite the bundle running cleanly on schedule.

Bumped maxStaleMin 360 → 720 (12h staleness = 2 missed cron ticks).
Matches the resilienceRanking entry directly above, written by the
SAME cron (refreshRankingAggregate runs in the same Resilience-Scores
section). Same pattern as the project's `health-maxstalemin-write-
cadence` skill: maxStaleMin must be sized to actual write cadence +
grace, not the documented one.

Updated the comment to:
  - Replace the incorrect "intervalMs=2h with hourly fires" claim with
    the empirically-verified `0 */6 * * *` schedule.
  - Document the prior values and what each was tuned for, so the next
    operator who hits this can see why 360 was wrong without re-doing
    the log-archaeology.
  - Note the 2026-04-28 incident timestamp so the audit trail is clear.

Verification: prefix bump v17→v18 in PR #3482 is in a DIFFERENT
namespace (resilience:score:, resilience:ranking:, resilience:history:);
this fix is independent and addresses resilience:intervals:v2:* which
was unaffected by that PR.

* test(health): align cache-keys health-sync test with real 6h cron cadence

Review-fix for PR #3484. The existing health-sync test asserted three
things that all assumed the wrong (~2h) writer cadence and rejected
the new 720 contract:

  1. line 216 — exact value 360 → 720
  2. line 220 — floor 180 (1.5× 2h) → 540 (1.5× 6h)
  3. line 229 — ceiling 480 (4× 2h) → 1080 (3× 6h)

Updated all three plus the describe-block label and the rationale
comment block at the top. The block now:

- Documents both the 2026-04-27 (false-OK at 14d) and 2026-04-28
  (false-positive at 360) incidents with root causes for each.
- Replaces the in-bundle `intervalMs=2h` cadence claim with the
  empirically-verified Railway cron `0 */6 * * *` schedule.
- Records the full audit trail of prior values (20160 / 1080 / 360
  / 720) and what each was tuned for.

The `Resilience-Scores section gate is 2h` test stays — that
assertion checks the IN-BUNDLE intervalMs in
scripts/seed-bundle-resilience.mjs, which is still 2h in the
source. Updated only its rationale comment to clarify that the
section's in-bundle 2h gate is independent from the OUTER
Railway-cron cadence (which is 6h).

13/13 tests in this file pass. Typecheck clean.
@koala73
koala73 merged commit 2009a22 into main Apr 28, 2026
11 checks passed
@koala73
koala73 deleted the feat/resilience-u8-methodology-widget-v17 branch April 28, 2026 06:47
fuleinist pushed a commit to fuleinist/worldmonitor that referenced this pull request May 9, 2026
…ence (koala73#3484)

* fix(health): resilienceIntervals maxStaleMin matches real 6h cron cadence

The prior comment claimed `intervalMs=2h with hourly Railway fires +
96min skip-window → effective ~2h cadence` and set `maxStaleMin: 360`
(= 1× claimed cadence). The deployed Railway cron is actually
`0 */6 * * *` (every 6h on the hour, UTC), confirmed by the user
2026-04-28 and corroborated by Railway logs: only 2 successful runs
in the seed-bundle-resilience service log over 6h2min, both clean,
both writing 196 interval keys.

With true cadence=6h and maxStaleMin=360min, ANY normal cron jitter
or a slightly-late tick tips resilienceIntervals into STALE_SEED.
Reproduced 2026-04-28: seedAgeMin=367 vs maxStale=360 → false alarm
despite the bundle running cleanly on schedule.

Bumped maxStaleMin 360 → 720 (12h staleness = 2 missed cron ticks).
Matches the resilienceRanking entry directly above, written by the
SAME cron (refreshRankingAggregate runs in the same Resilience-Scores
section). Same pattern as the project's `health-maxstalemin-write-
cadence` skill: maxStaleMin must be sized to actual write cadence +
grace, not the documented one.

Updated the comment to:
  - Replace the incorrect "intervalMs=2h with hourly fires" claim with
    the empirically-verified `0 */6 * * *` schedule.
  - Document the prior values and what each was tuned for, so the next
    operator who hits this can see why 360 was wrong without re-doing
    the log-archaeology.
  - Note the 2026-04-28 incident timestamp so the audit trail is clear.

Verification: prefix bump v17→v18 in PR koala73#3482 is in a DIFFERENT
namespace (resilience:score:, resilience:ranking:, resilience:history:);
this fix is independent and addresses resilience:intervals:v2:* which
was unaffected by that PR.

* test(health): align cache-keys health-sync test with real 6h cron cadence

Review-fix for PR koala73#3484. The existing health-sync test asserted three
things that all assumed the wrong (~2h) writer cadence and rejected
the new 720 contract:

  1. line 216 — exact value 360 → 720
  2. line 220 — floor 180 (1.5× 2h) → 540 (1.5× 6h)
  3. line 229 — ceiling 480 (4× 2h) → 1080 (3× 6h)

Updated all three plus the describe-block label and the rationale
comment block at the top. The block now:

- Documents both the 2026-04-27 (false-OK at 14d) and 2026-04-28
  (false-positive at 360) incidents with root causes for each.
- Replaces the in-bundle `intervalMs=2h` cadence claim with the
  empirically-verified Railway cron `0 */6 * * *` schedule.
- Records the full audit trail of prior values (20160 / 1080 / 360
  / 720) and what each was tuned for.

The `Resilience-Scores section gate is 2h` test stays — that
assertion checks the IN-BUNDLE intervalMs in
scripts/seed-bundle-resilience.mjs, which is still 2h in the
source. Updated only its rationale comment to clarify that the
section's in-bundle 2h gate is independent from the OUTER
Railway-cron cadence (which is 6h).

13/13 tests in this file pass. Typecheck clean.
fuleinist pushed a commit to fuleinist/worldmonitor that referenced this pull request May 9, 2026
…+ 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant