docs/internal/country-resilience-upgrade-plan.md
Current Country Resilience is rated 7.4/10 by the internal review in
docs/internal/upgrading-country-resilience.md. The architecture, data breadth,
and engineering are already strong (8.0–8.5). What keeps it below reference
grade is methodology rigor (6.0), validation/backtesting (5.5), and
explainability (6.5). This plan executes the six improvements prescribed in
the origin document and targets a scorecard of ≥9.0 overall across all six
axes, with public methodology and independently verifiable numbers.
Concretely the upgrade rebuilds the top-level shape of the index into three
pillars (structural readiness, live shock exposure, recovery capacity), ships a
published methodology pack at parity with the Country Instability Index,
introduces a cross-index benchmark + backtest suite, replaces the binary
lowConfidence flag with a decomposed uncertainty surface, deepens the data
model on the dimensions where resilience actually breaks, and turns the widget
from a number into an analyst tool with waterfall explanations and peer
comparison.
Before any code, this plan commits to a one-page construct definition so the index cannot be re-argued dimension by dimension later. (Review gap: "the construct is still not frozen".)
What resilience means here. The ability of a country to (a) absorb a contemporary shock across economic, infrastructural, energy, social-governance, and health-food channels, (b) continue delivering core state functions under that shock, and (c) recover along a trajectory measurable within a 90-day to 24-month window. This is explicitly not climate resilience (ND-GAIN's territory) or humanitarian crisis risk (INFORM's territory). It is current system robustness under acute stress.
Horizon. Structural readiness is slow-moving (annual to 3-year). Live shock exposure is fast-moving (daily to weekly). Recovery capacity is medium-moving (quarterly). These horizons are enforced at seed time: each signal is tagged with its native cadence and cannot contribute to a pillar whose horizon it violates without explicit justification in the methodology.
For whom. Primary audience: intelligence analysts and operators making country-level decisions over a 1–12 month window. Secondary audience: policy researchers and external indices that may cite us. The widget is designed for the primary audience; the methodology page is designed for the secondary.
Polarity. Higher score = more resilient. Every dimension's native polarity
is declared in the indicator registry with direction (higher-better /
lower-better) and goalposts (min, max). Sign flips like the RSF inversion
bug (PR #2847) are prevented by a build-time lint that reads the registry and
fails the build if a scorer disagrees with the declared direction.
Aggregation philosophy, partly non-compensatory.
Overall = (w_s·S + w_l·L + w_r·R) · penaltyFactor
where penaltyFactor = 1 − α · max(0, (pillarMax − pillarMin) / 100) and
α is tuned on backtest. At α=0 this collapses to today's arithmetic mean.
At α=1 a country with a 50-point gap between its strongest and weakest
pillar loses half its overall score. INFORM uses multiplication;
WorldRiskIndex uses the geometric mean of exposure × vulnerability. Both
choices exist to prevent a country with severe exposure from washing that
away with one strong institutional score. We adopt the same philosophy with
a tunable α so the calibration team can pick the operating point from
backtest + sensitivity evidence.unmonitored class. Penalized weighted mean degrades gracefully
and stays interpretable on the widget.What this commits the plan to. Tasks T2.1 and T2.3 ship the penalized
weighted mean as the v2.0 aggregation. Phase 2 sensitivity (T2.6) includes
α in {0, 0.25, 0.5, 0.75, 1} as a perturbation axis and publishes the
curve; the chosen α is documented in the methodology changelog with the
backtest evidence that justified it.
The upgrade ships two products, not one, because citation-grade reproducibility and real-time operational monitoring pull in opposite directions. INFORM, ND-GAIN, WorldRiskIndex, and FSI all ship editioned releases for exactly this reason. (Review gap: "the plan tries to ship a citation-grade index and a live operational monitor as one object".)
Annual Reference Edition, e.g., "Country Resilience Index 2026".
docs/methodology/country-resilience-index/reference-edition/2026/recompute.mts
that regenerates the sampled published score-cache values from the manifest.Live Monitor, the continuation of today's product surface.
Phase mapping. Phase 1 ships the methodology page and fixes; Phase 2 ships the three-pillar rebuild + signal tiering + snapshot/reproducibility tooling; Phase 3 ships the explanatory product + the first cut of the 2026 Reference Edition. The Live Monitor is the continuation of today's product and never loses coverage during the transition.
Origin document: docs/internal/upgrading-country-resilience.md
Key decisions carried forward verbatim from the origin (see origin for full rationale):
lowConfidence label with
per-dimension coverage, imputation share, source freshness, interval width,
and rank stability; intervals from bootstrap or Monte Carlo perturbations on
weights and missing-data choices (see origin: "4. Replace binary confidence
with decomposed uncertainty").90-day cadence (from origin, adopted verbatim as Phases 1–3 below):
The current resilience product is strong engineering on an under-documented model. Three specific gaps keep it below reference grade:
Execute the origin document's six improvements as three sequential phases
over ~90 days, building on the existing v3/v4/v5 roadmap and on the already-
shipped formula/direction fixes (PRs #2821, #2847, #2858). Each phase lands
behind a feature flag, ships with acceptance tests, and produces a publishable
changelog entry in docs/methodology/country-resilience-index.mdx.
Phase 1, Transparency & Calibration Fixes (methodology pack + ceiling
bugs + dimension-level confidence). This phase is mostly already scoped by
existing plans (2026-04-07-002, 2026-04-09-001), this plan's job is to
finish it to origin-doc quality, not to redo it.
Phase 2, Structural Rebuild (three pillars incl. recovery capacity, cross-index benchmark, sensitivity suite, new indicators).
Phase 3, Explanatory Product (waterfall, peer comparison, bootstrapped intervals, change attribution, external expert review).
server/worldmonitor/resilience/v1/_dimension-scorers.ts,
server/worldmonitor/resilience/v1/_shared.ts:~200.GetResilienceScore(countryCode) and
GetResilienceRanking() under
server/worldmonitor/resilience/v1/{get-resilience-score.ts, get-resilience-ranking.ts, handler.ts}._shared.ts): resilience:score:v7:<cc> (6h),
resilience:ranking:v8 (6h, written only when all countries scored),
resilience:history:v4:<cc> (daily sorted set, 30-day retention),
resilience:intervals:v3:<cc> (formula-tagged score sensitivity band).warmMissingResilienceScores() in get-resilience-ranking.ts.scripts/seed-resilience-static.mjs (WGI,
infrastructure, GPI, RSF, WHO, FAO, Aquastat, IEA, TradeToGDP, FXReserves,
AppliedTariffRate) with 400-day TTL, version resilience-static-v7, lock
domain resilience:static.scripts/_resilience-intervals.mjs,
scripts/validate-resilience-backtest.mjs,
scripts/validate-resilience-correlation.mjs,
scripts/validate-resilience-sensitivity.mjs. Phase 2's benchmark suite
extends these, it does not replace them.server/_shared/resilience-stats.ts: minMaxNormalize, cronbachAlpha,
detectTrend, detectChangepoints (CUSUM), exponentialSmoothing,
nrcForecast.coverage, observedWeight,
imputedWeight per dimension; lowConfidence (bool), imputationShare
(0–1), optional scoreInterval {p05, p95} on the top-level response. The
widget collapses this to a single "Low confidence" label via
src/components/resilience-widget-utils.ts:formatResilienceConfidence.docs/methodology/resilience-index.md
(5 domains, 13 dimensions, ~46 sub-metrics with sources, direction,
goalposts, weights, cadence + missing-data imputation taxonomy). Phase 1
promotes this to .mdx at CII parity.docs/plans/2026-03-29-feat-country-resilience-score-plan.md, original
v1 implementation plan.docs/plans/2026-04-07-002-fix-resilience-v3-phase1-coverage-correctness-plan.md
, three calibration bugs: zero-event feeds awarding 100, WTO absence as
no-barriers, Cronbach alpha misused as formative-index confidence metric.docs/plans/2026-04-09-001-fix-resilience-overall-score-formula-plan.md
, revert multiplicative formula baseline * (1 - stressFactor) to
domain-weighted average; fix RSF press-freedom direction (0=free is good,
currently scored higher-better). Most of Phase 1 inherits this.data_version field, never
populated, Phase 1 wires this).proto/worldmonitor/<domain>/, add RPC with
(sebuf.http.config) annotation, make generate, create handler in
server/worldmonitor/<domain>/, wire in domain's handler.ts, use
cachedFetchJson() with request-varying params in the cache key.server/_shared/cache-keys.ts, api/bootstrap.js, api/health.js
(SEED_META registry, not SEED_DOMAINS), server/_shared/gateway.ts.api/*.js) are self-contained JS only; cannot import
from src/ or server/. Enforced by tests/edge-functions.test.mjs +
pre-push esbuild check.MAJOR_REPORTERS-style allowlists (repo memory: "world_coverage_never_subset").@ts-check for .mjs, strict TS for .ts,
.types.d.ts for shared shapes.The origin document states: "Norway and the US both hit 100 under current
fixtures, which broke the intended ordering and exposed a ceiling effect at
the top end of the ranking." Repo research did not find a hard 100 ceiling
in _dimension-scorers.ts, scores are roundScore(clamp(value, 0, 100)) and
border-security uses a UCDP conflict metric + UNHCR displacement imputation at
85, neither of which produces a forced 100. The closest matching symptom in
the existing plans is the formula bug in 2026-04-09-001 (which
over-penalized rather than over-saturated) and the zero-event bias in
2026-04-07-002 (which does cause untracked countries to appear perfect
but in the lower-impact cyber/outage dimensions). Phase 1 task T1.1 is:
reproduce the exact ceiling scenario the origin doc references and write a
regression test before fixing it. If it is the zero-event bias that was
misattributed to border security, update the origin doc's changelog entry to
reflect the true root cause.
Three top-level pillars replace today's single overallScore as the
primary reporting shape:
StructuralReadiness = f(baseline dimensions) # long-run capacity
LiveShockExposure = f(stress dimensions) # current pressure
RecoveryCapacity = f(new recovery dims) # ability to absorb + rebound
OverallResilience = weighted combine of the three pillars
The current 5 domains × 13 dimensions are preserved and regrouped under the three pillars. Recovery capacity is a new pillar composed of:
fuel-stock days)Dimensions that already exist move under pillars; only genuinely new sub- metrics are added as new signal ingests.
Add to GetResilienceScoreResponse (under a versioned shape to preserve
backward compat):
pillars:
structuralReadiness:
score: number
weight: number
coverage: number
interval: { p05, p50, p95 }
domains: ResilienceDomain[] # subset of the current 5
liveShockExposure:
score: number
# ... same shape
recoveryCapacity:
score: number
# ... same shape
freshness:
perDimension: { [dimId]: { lastObservedAt: string, staleness: "fresh" | "aging" | "stale" }}
rankStability:
current: number # current rank
p05: number # 5th percentile rank from perturbation
p95: number # 95th percentile rank from perturbation
bandWidth: number
imputationBreakdown:
stableAbsence: number
unmonitored: number
sourceFailure: number
notApplicable: number
changeAttribution:
windowDays: 7 | 30
contributors: Array<{ dimensionId: string, deltaPoints: number }>
Old top-level overallScore, baselineScore, stressScore, lowConfidence,
imputationShare, scoreInterval remain for one release cycle behind a
schemaVersion: "1.0" vs "2.0" switch so the widget and any external
consumers (CountryDeepDivePanel, map layer, Country Brief) can migrate.
Not every new signal belongs in the public ranking. Following WorldRiskIndex's precedent of excluding sparse-coverage indicators even when experts consider them highly relevant, every signal is tagged Core, Enrichment, or Experimental. (Review gap: "Several new signals are valuable, but not good core-ranking inputs yet.")
Tiering for new signals:
| Signal | Source | Tier | Rationale |
|---|---|---|---|
| Fiscal space (debt, primary balance, deficit) | IMF WEO, GGR_G01_GDP_PT | Core | ≥190 country coverage, stable definition. Avoid GGR_NGDP, returns empty (memory: imf_datamapper_indicators). |
| Reserve adequacy (months of imports) | IMF IFS / WB | Core | ≥180 country coverage |
| Short-term external debt coverage | World Bank IDS | Core | ≥170 coverage; SIDS gaps handled via unmonitored imputation class |
| Import concentration (HHI, HS2) | Comtrade | Core | Wide reporter coverage via bilateral expansion (memory: comtrade_reporters_actual, current seeded reporters US/CN/RU/IR/IN/TW, expand in T2.2) |
| Hospital beds / 10k | WHO GHO | Core | Already in health dimension, unchanged |
| State continuity composite | WGI + UCDP + ACLED + displacement velocity | Core | All sources already Core-tier |
| ICU surge capacity | OECD health data | Enrichment | OECD-only, fewer than 80 countries |
| Grid reliability (SAIDI/SAIFI) | WB Doing Business archive + ENTSO-E | Enrichment | EU + archived WB only; definitional variance across sources |
| Fuel-stock days | IEA strategic reserves, EIA weekly, EMSA | Enrichment | IEA members only (~35 countries); Experimental for non-IEA until an open global source lands |
| Telecom redundancy (submarine cables, IXP count) | TeleGeography, PCH | Enrichment | Counts differ by source; no standard denominator |
| Language-normalized information signal | RSF + social velocity re-weighted by language/source density | Enrichment → Core (T2.9) | Starts Enrichment; promoted to Core when lint-rule check on language-weighted normalization passes |
Existing 13 dimensions default to Core, with one exception:
informationCognitive is demoted to Enrichment until the language /
source-density normalization lands in T2.9, at which point it re-enters Core.
Seeder gold standard. Every new seeder (Core or Enrichment) follows the
Railway gold standard (memory: feedback_seeder_gold_standard):
TTL ≥ 3×interval, retry in 20 min on failure, upstashExpire on both failure
paths, clear retry timer on success, health maxStaleMin = 2×interval.
All interval and rank-stability computation lives in scheduled batch jobs, never on the read path. A 200ms cold target is incompatible with per-request bootstrap/Monte Carlo work. (Review gap: "do not keep lazy interval computation anywhere near the read path".)
scripts/seed-resilience-scores.mjs, Resilience-Scores
Railway cron every 6 hours):
{0, 0.25, 0.5, 0.75, 1} across the penalty factor.resilience:intervals:v3:<cc> as
{ computedAt, schemaVersion, pillarIntervals, overallInterval, rankBand }.buildResilienceScore): always reads the latest interval
payload. If computedAt is older than 48 hours the response sets
staleIntervals: true. If the key is missing (new country in the warmup
path), the response omits intervals entirely rather than computing them
inline, intervals arrive on the next batch tick.staleIntervals: true responses exceeds 5%.stable-absence (e.g., landlocked → no maritime risk), unmonitored (no
global source publishes it), source-failure (upstream API down at seed
time), not-applicable (e.g., no nuclear power → no nuclear exposure).
The scorer context reads the class instead of branching on value == null,
and each class has its own certainty weight.A single correlation target across all comparators would reward mimicry and punish useful originality. Each comparator measures something different, so the current public benchmark is designed around explicit overall-score hypotheses with expected sign and minimum strength. (Review gap: "benchmark design compares unlike things".)
Cross-index benchmark (scripts/benchmark-resilience-external.mjs):
| Scope | Comparator | Expected sign | Minimum | Rationale |
|---|---|---|---|---|
| Overall Resilience | INFORM Risk | negative | absolute Spearman ≥0.60 | Higher resilience should inversely track humanitarian/disaster risk. |
| Overall Resilience | UNDP HDI | positive | Spearman ≥0.65 | Higher resilience should broadly track human-development capacity without copying it. |
| Overall Resilience | WorldRiskIndex vulnerability component | negative | absolute Spearman ≥0.55 | Higher resilience should inversely track societal vulnerability to disaster risk. |
ND-GAIN is deferred until the validation image can unzip the 2026 archive. Fragile States Index is retired from public artifacts because fresh bulk data is no longer available and the licensing posture blocks public comparator values in this repo.
Interpretation rules:
docs/methodology/country-resilience-index/benchmark-outliers.md.Event backtest, per event family (scripts/backtest-resilience-outcomes.mjs):
A single AUC target across seven event families mixes different label regimes. Split validation into seven gates, each with its own lead window, baseline, and metrics. (Review gap: "one AUC threshold across seven event families mixes different label regimes".)
| Event family | Label source | Lead window | Naive baseline | Metrics | Release gate |
|---|---|---|---|---|---|
| FX stress | Frozen 2024-2025 reference set curated from IMF AREAER and IMF country surveillance devaluation notes | 30d | Prior-quarter current-account deficit rank | AUC, calibration, precision@10, lead-time uplift | AUC ≥ baseline + 0.05 |
| Sovereign stress | Frozen 2024-2025 reference set curated from IMF sovereign-debt restructuring/program material plus major rating-agency default/downgrade actions | 60d | Prior debt/GDP rank | same | AUC ≥ baseline + 0.05 |
| Power outages (prolonged) | Frozen 2024-2025 reference set curated from IEA electricity analysis, ReliefWeb/OCHA humanitarian updates, and national operator/government notices | 14d | Prior SAIDI rank | same | AUC ≥ baseline + 0.05 |
| Food-crisis escalation | Live Redis label set from resilience:static:fao (IPC/FAO food-crisis seeding path) | 90d | Prior IPC phase | same | AUC ≥ baseline + 0.05 |
| Refugee surges | Live Redis label set from displacement:summary:v1:<year> (UNHCR displacement summary seeding path) | 60d | Prior border-security rank | same | AUC ≥ baseline + 0.05 |
| Sanctions shocks | Frozen 2024-2025 reference set curated from OFAC sanctions-list targeting, EU Sanctions Map regimes, and UN/EU/US comprehensive sanctions program coverage | 7d | Prior sanctions count | same | AUC ≥ baseline + 0.05 |
| Conflict spillover | Live Redis label set from conflict:ucdp-events:v1 (UCDP event seeding path) | 30d | Prior conflict density | same | AUC ≥ baseline + 0.05 |
The current artifact deliberately distinguishes curated and live labels:
dataSource: "hardcoded" for FX stress, sovereign stress, power outages, and
sanctions shocks; dataSource: "live" for food-crisis escalation, refugee
surges, and conflict spillover. Each family carries labelSources in the
committed JSON so future source flips are visible in review.
Per-family release gates. A pillar rebuild that improves FX-stress prediction but regresses food-crisis can ship if the food-crisis regression is below the gate width (0.03 AUC) and documented in the changelog; otherwise it is blocked on that family. Release notes publish metrics for all seven families, even when only one family gated the release.
Sensitivity suite extends scripts/validate-resilience-sensitivity.mjs.
Perturbation axes: domain weights (±20%), goalposts (±10%),
α in {0, 0.25, 0.5, 0.75, 1} for the penalized weighted mean,
imputation-class defaults (shift one class up and one class down), and
goalpost normalization method (linear vs percentile-based). Publishes a
curve per axis. Blocks release if any single-axis perturbation moves a
top-50 country by more than 5 rank positions.
All three scripts run as Railway cron jobs (weekly, bundled if service count
is a concern, memory: railway_seed_bundle_pattern) and publish results to
docs/methodology/country-resilience-index/validation/*.json, committed and
version-controlled so history is auditable.
src/components/resilience/WaterfallChart.ts), horizontal
bar per dimension → domain → pillar → overall. Each bar labeled with its
contribution in points. Clickable → opens signal detail drawer.fresh | aging | stale pill driven by
the new freshness.perDimension schema field.resilience:history:v4:<cc> (already populated).structural | live-shock | recovery
radio switch, defaulting to overall.Goal: Publish methodology pack, fix ceiling/calibration bugs, ship dimension-level confidence. Exit criteria: external analyst can read the methodology page and reproduce any country score from the Redis cache keys.
Tasks:
tests/resilience-release-gate.test.mts that asserts
the expected ordering. Determine whether the root cause is the zero-event
bias from 2026-04-07-002 or something else. Blocks T1.2.2026-04-07-002 and 2026-04-09-001
if not already merged (check PR #2847, #2858 state with
gh pr view <n> --json state, repo memory feedback_check_pr_merged).
Bump cache keys to resilience:score:v8:<cc> and
resilience:ranking:v9 on any scorer change.docs/methodology/resilience-index.md to
docs/methodology/country-resilience-index.mdx at CII parity. Sections
required: Framework (3 pillars), Domains (5), Dimensions (13+new),
Sub-metrics (~46), Normalization (per-signal goalposts + direction),
Weighting (with rationale), Missing-data rules (4-class imputation
taxonomy), Confidence/Intervals (bootstrap + MC method), Ranking rules
(greyedOut threshold, rank-stability bands), Changelog (v1→v2),
Reproducibility appendix (Redis keys + formulas).data_version field end-to-end (was added in PR #2821
but never populated). seed-resilience-static.mjs writes the ISO date
into resilience:static:meta:v7.dataVersion; scorers propagate; widget
shows it next to each dimension.lastObservedAt to every signal read
in the scorer context. ResilienceDimension schema gains
freshness: { lastObservedAt, staleness }. Staleness thresholds come from
the cadence column of the methodology indicator registry (daily, weekly,
monthly, quarterly, annual).formatResilienceConfidence() single-label rendering in
src/components/resilience-widget-utils.ts with a per-dimension bar
(coverage %, imputation class icon, freshness badge). Preserve the old
label as a fallback for mobile.imputationClass to the signal payload written under each
resilience:static:signal:<id>:<cc> key. Scorer context reads this
instead of branching on value == null. Update
resilience-dimension-scorers.test.mts with cases for each class.
T1.7 schema pass shipped in PR #2959 (imputationClass on the
ResilienceDimension proto). T1.7 source-failure wiring shipped in
PR #2964 (consult seed-meta failedDatasets and re-tag affected
dimensions at the aggregation layer + delete the one remaining
absence-based branch in scoreCurrencyExternal).data_version round-trip, imputation-class plumbing.server/_shared/cache-keys.ts, api/bootstrap.js, api/health.js
SEED_META (memory: health_js_registry_names), and
server/_shared/gateway.ts per the 4-file checklist.
T1.9 shipped in PR #2965 as a cache-key / health-registry sync test
resilience:score:v7 / ranking:v8 /
history:v4 keys.Phase 1 acceptance:
.mdx published + linked from navbar + every dimension has
its own subsection. (T1.3 #2945)data_version non-null on every country score response. (T1.4 #2943)typecheck, typecheck:api, test:data, test:sidecar, and
resilience-specific suites all green. (Verified in every Phase 1
PR's pre-push hook; this PR also adds the cache-key drift test.)Goal: Three pillars (incl. recovery capacity), cross-index benchmark, sensitivity suite, new indicators live.
Tasks:
schemaVersion: "2.0" field and keep schemaVersion: "1.0" response shape
behind a feature flag for one release cycle.feedback_empty_data_ok_keys_bootstrap_blind_spot, the bootstrap loop
ignores EMPTY_DATA_OK_KEYS, must be added to both loops).scripts/benchmark-resilience-external.mjs. Targets: INFORM, UNDP HDI,
the WorldRiskIndex vulnerability component; ND-GAIN deferred and FSI
retired from public artifacts.
Output: Spearman + Pearson, outlier list, stored as
resilience:benchmark:external:v1 and committed JSON under
docs/methodology/country-resilience-index/validation/.scripts/backtest-resilience-outcomes.mjs. Define event set,
hold-out 2024-2025, compute AUC, target 0.75 with a 0.03 gate width.
Current labels use four frozen independently sourced reference sets and
three live Redis seed outputs; outputs stored next to cross-index benchmark.scripts/validate-resilience-sensitivity.mjs. Perturb all weights and
goalposts; flag dimensions where top-10 swings >3 positions; block release
gate if >20% of dimensions fail.scripts/ralph/ Railway seed-bundle pattern (memory:
railway_seed_bundle_pattern) if service count is a concern. Validation
scripts run non-strict in cron so cold-start skips after cache-key flips do
not page; release/CI regeneration uses --strict or
RESILIENCE_VALIDATION_STRICT=1 so skipped artifacts still fail the gate.Phase 2 acceptance:
schemaVersion: "2.0" response shape live with three pillars.
(T2.1 #2977, flag flip in closeout PR)resilience:ranking:v9
(non-greyed). CRITICAL, memory: feedback_world_coverage_never_subset.Goal: Waterfall + peer comparison + bootstrapped intervals + change attribution + external expert review. Exit criteria: an analyst can look at a country's card and answer "why did this score move?" without opening dev tools.
Tasks:
scripts/seed-resilience-scores.mjs and shared interval helpers to write pillar-level intervals,
joint overall intervals, and rank bands to resilience:intervals:v3:<cc>
every 6 hours. Zero lazy computation on the read path, missing key
means intervals are omitted from the response; the read path never blocks
on bootstrap or MC work. Add staleIntervals: true flag when payload is
48h old; Railway health alert when stale share >5%.
rankStability field populated from the same MC runs as T3.1
(rank band p05/p95 from re-ranking each sample).src/components/resilience/WaterfallChart.ts. Uses existing chart
primitives, no new deps. Unit-tested for: 13 dimensions rendering, 0-point
dimensions hidden by default, sum-to-total invariant, click-to-drill.GetResiliencePeers(countryCode) returns peer list with light
payload. Follows 4-file bootstrap checklist.GetResilienceChangeAttribution(countryCode, windowDays: 7|30) reads
resilience:history:v4:<cc> sorted set + current signal snapshot, returns
per-dimension delta contributions. Caches per
(countryCode, windowDays), include both params in cache key
(memory: feedback_global_rpc_cache_contract, request-varying params
must be in the key, or use a separate RPC).benchmark-outliers.md;
(e) all 7 per-event-family release gates are met (or the shortfall is
documented in the changelog and below the per-family 0.03 AUC gate
width).schemaVersion: "1.0" after one release
cycle.docs/methodology/country-resilience-index/reference-edition/2026/ with a
reproducibility script and country-sliced snapshot manifest.Phase 3 acceptance:
GetResilienceChangeAttribution live, cached with both
countryCode and windowDays in the cache key.GetResilienceScore → buildResilienceScore() in _shared.ts → calls all
13 dimension scorers in parallel → each scorer reads memoized global Redis
keys (UCDP, displacement, sanctions, WGI, RSF, etc.) → aggregates into
domains → aggregates into pillars (new) → overall. History sorted set
updated if current day not yet written. Interval cache read only: if
resilience:intervals:v3:<cc> is present and fresh, it is attached; if
the payload is >48h old the response sets staleIntervals: true; if the
key is missing (warmup path), intervals are omitted from the response.
The read path never computes intervals inline. All bootstrap and Monte
Carlo work lives in the Railway cron batch (T3.1), and the next tick
rewrites the key.
Downstream consumers that will see the new schema:
src/components/ResilienceWidget.ts (primary)src/components/CountryDeepDivePanel, resilience card integrationsrc/components/CountryBrief, brief sections use resilience scoresource-failure, scorer
treats as imputed with certainty = 0.3 (tunable), overall coverage drops,
lowConfidence fires if total imputation >0.4.{ dimensionId, countryCode } tags. Overall
score still computes.schemaVersion in the cache key (resilience:score:v8:2.0:<cc> vs
:1.0:<cc>), or bump the version prefix on every schema change.seed-meta:<key> with
count: 0 on skipped paths (memory: feedback_seed_meta_skipped_path)
to avoid STALE_SEED false positives.api/bootstrap.js
(memory: feedback_empty_data_ok_keys_bootstrap_blind_spot).agent-native-reviewer): new RPCs must be
callable via the MCP server. Add tools:
get_country_resilience_waterfall, get_country_resilience_peers,
get_country_resilience_change_attribution. Update
mcp__claude_ai_World_Monitor__* registry.docs/api/ResilienceService.openapi.yaml per
phase; add deprecation notices for v1.0 fields during Phase 2/3.stable-absence at seed
time flows through scorer → response → widget without being re-tagged as
source-failure.schemaVersion: "2.0"), v1.0 shape supported
during the deprecation window.GetResilienceScore response ≤200ms cold, ≤50ms warm.cachedFetchJson with request params in cache key.typecheck + typecheck:api clean.test:data + test:sidecar green.gh pr view run before every push (memory:
feedback_check_pr_merged_before_commit).--no-verify on own branches.Controllable metrics only, nothing that depends on external adoption, third-party behavior, or uncontrollable journalism/citation dynamics. (Review gap: "Replace out-of-control success metrics with controllable ones".)
feedback_world_coverage_never_subset, non-negotiable.staleIntervals: false. Target: ≥95%.gh pr view <n> --json state before Phase 1 kickoff.comtrade_reporters_actual); reporter set must
be expanded beyond US/CN/RU/IR/IN/TW to hit Core coverage bar.schemaVersion to cache keys doubles cache
footprint during the transition, acceptable at 6h TTL but monitor Redis.railway_seed_bundle_pattern).Treated as a standalone workstream, not a risk-section footnote, because publishing benchmark comparisons against third-party indices touches copyright, database rights, and commercial-use restrictions that vary per comparator. This must be resolved before any Phase 2 T2.4 artifact with third-party values lands in the public repo or product surface. (Review gap: "the licensing section is too loose".)
Per-comparator status (to re-verify with counsel before use):
| Comparator | License (2026) | Commercial use | Republication of values | Action |
|---|---|---|---|---|
| INFORM Risk | Open data, JRC | Permitted with attribution | Permitted with attribution | Attribute JRC INFORM + cite methodology URL on every artifact |
| ND-GAIN | CC BY 3.0 | Permitted with attribution | Permitted with attribution | Attribute University of Notre Dame + CC BY 3.0 license line |
| WorldRiskIndex | CC BY 4.0 | Permitted with attribution | Permitted with attribution | Attribute Bündnis Entwicklung Hilft + CC BY 4.0 license line |
| Fragile States Index | CC BY-NC-SA 4.0 (non-commercial only) | Commercial requires contacting The Fund for Peace | Non-commercial only; commercial on contact | BLOCKS public benchmark artifacts containing FSI values inside the commercial repo. Default carve-out: redact FSI values from public artifacts, keep internally for calibration behind a feature flag. |
Workstream deliverables (tracked as their own task set):
docs/methodology/country-resilience-index/validation/.LEGAL_FSI_USE_APPROVED=true
env var that is not set by default. If commercial terms are later agreed
with FFP, the flag flips and the public artifacts are regenerated.Gate: Phase 2 T2.4 cannot ship a public artifact until deliverables 1–4 are complete.
docs/methodology/country-resilience-index.mdx, the canonical methodology
page, promoted from current draft in Phase 1.docs/methodology/country-resilience-index/benchmark-outliers.md, outlier
commentary, updated per cross-index run.docs/methodology/country-resilience-index/validation/, JSON artifacts
from weekly Railway cron, committed so the history is auditable.docs/api/ResilienceService.openapi.yaml, schema v2.0, with v1.0 fields
marked deprecated during the transition window.CHANGELOG.md + docs/changelog.mdx, dual update (memory:
feedback_changelog_dual_update) for each phase release.scripts/ docstring + Railway service notes.docs/internal/upgrading-country-resilience.md
, scorecard, 6 improvements, 90-day plan. Key decisions carried forward:
(1) three-pillar rebuild incl. recovery capacity, (2) CII-parity methodology
pack, (3) cross-index benchmark + outcome backtest.server/worldmonitor/resilience/v1/_dimension-scorers.ts, 13 dimension
scorers, ~1073 lines.server/worldmonitor/resilience/v1/_shared.ts, aggregation, confidence,
cache keys (~line 200 onwards).server/worldmonitor/resilience/v1/get-resilience-score.tsserver/worldmonitor/resilience/v1/get-resilience-ranking.tsserver/worldmonitor/resilience/v1/handler.tsserver/_shared/resilience-stats.ts, statistical utilities (Cronbach,
CUSUM, forecast).scripts/seed-resilience-static.mjs, 11 slots, ~920 lines.scripts/_resilience-intervals.mjs, shared interval helpers.scripts/validate-resilience-backtest.mjs,
scripts/validate-resilience-correlation.mjs,
scripts/validate-resilience-sensitivity.mjs, existing validation.src/components/ResilienceWidget.ts + src/components/resilience-widget-utils.tsdocs/api/ResilienceService.openapi.yamldocs/methodology/resilience-index.md, current draft (to be promoted).docs/country-instability-index.mdx, CII reference template.docs/plans/2026-03-29-feat-country-resilience-score-plan.md, v1 plan.docs/plans/2026-04-07-002-fix-resilience-v3-phase1-coverage-correctness-plan.mddocs/plans/2026-04-09-001-fix-resilience-overall-score-formula-plan.mdfeedback_world_coverage_never_subset, NEVER ship features limited to
N countries (CRITICAL, repeated violation).worldmonitor-bootstrap-registration, 4-file checklist.feedback_health_js_registry_names, SEED_META, not SEED_DOMAINS.feedback_seeder_gold_standard, Railway seeder pattern.feedback_global_rpc_cache_contract, request-varying params in cache key
or separate RPC.feedback_empty_data_ok_keys_bootstrap_blind_spot, both loops in
api/bootstrap.js.feedback_seed_meta_skipped_path, write seed-meta with count: 0 on
skipped path.feedback_check_pr_merged_before_commit, gh pr view before push.feedback_type_safety_always, JSDoc + @ts-check for .mjs, strict TS
for .ts.feedback_changelog_dual_update, CHANGELOG.md + docs/changelog.mdx.feedback_worktree_absolute_path_trap, worktree-scoped paths only.feedback_pr_review_value_first, every PR description leads with
"Why this PR?".feedback_no_bulk_prs, each phase task → its own PR.