feat(resilience): PR 2 dimension wiring — split reserveAdequacy + add sovereignFiscalBuffer (#3324)

* feat(resilience): PR 2 dimension wiring — split reserveAdequacy + add sovereignFiscalBuffer

Plan §3.4 follow-up to #3305 + #3319. Lands the scorer + dimension
registration so the SWF seed from the Railway cron feeds a real score
once the bake-in window closes. No weight rebalance yet (separate
commit with Spearman sensitivity check), no health.js graduation yet
(7-day ON_DEMAND window per feedback_health_required_key_needs_
railway_cron_first.md), no bootstrap wiring yet (follow-up PR).

Shape of the change

Retirement:
- reserveAdequacy joins fuelStockDays in RESILIENCE_RETIRED_DIMENSIONS.
  The legacy scorer now mirrors scoreFuelStockDays: returns
  coverage=0 / imputationClass=null so the dimension is filtered out
  of the confidence / coverage averages via the registry filter in
  computeLowConfidence, computeOverallCoverage, and the widget's
  formatResilienceConfidence. Kept in RESILIENCE_DIMENSION_ORDER for
  structural continuity (tests, cached payload shape, registry
  membership). Indicator registry tier demoted to 'experimental'.

Two new active dimensions:
- liquidReserveAdequacy (replaces the liquid-reserves half of the
  retired reserveAdequacy). Same source (WB FI.RES.TOTL.MO, total
  reserves in months of imports) but re-anchored 1..12 months
  instead of 1..18. Twelve months ≈ IMF "full reserve adequacy"
  benchmark for a diversified emerging-market importer — the tighter
  ceiling prevents wealthy commodity-exporters from claiming outsized
  credit for on-paper reserve stocks that are not the relevant
  shock-absorption buffer.
- sovereignFiscalBuffer. Reads resilience:recovery:sovereign-wealth:v1
  (populated by scripts/seed-sovereign-wealth.mjs, landed in #3305 +
  wired into Railway cron in #3319). Computes the saturating
  transform:
    effectiveMonths = Σ [ aum/annualImports × 12 × access × liquidity × transparency ]
    score           = 100 × (1 − exp(−effectiveMonths / 12))
  Exponential saturation prevents Norway-type outliers (effective
  months in the 100s) from dominating the recovery pillar.

Three code paths in scoreSovereignFiscalBuffer:
1. Seed key absent entirely → IMPUTE.recoverySovereignFiscalBuffer
   (score 50 / coverage 0.3 / unmonitored). Covers the Railway-cron
   bake-in window before the first successful tick.
2. Seed present, country NOT in manifest → score=0 with FULL coverage.
   Substantive absence, NOT imputation — per plan §3.4 "What happens
   to no-SWF countries." 0 × weight = 0 in the numerator, so the
   country correctly scores lower than SWF-holding peers on this dim.
3. Seed present, country in payload → saturating score, coverage
   derated by the partial-seed completeness signal (so a Mubadala or
   Temasek scrape drift on a multi-fund country shows up as lower
   confidence rather than a silently-understated total).

Indicator registry:
- Demoted recoveryReserveMonths (tied to retired reserveAdequacy) to
  tier='experimental'.
- Added recoveryLiquidReserveMonths: WB FI.RES.TOTL.MO, anchors 1..12,
  tier='core', coverage=188.
- Added recoverySovereignWealthEffectiveMonths: the new SWF signal,
  tier='experimental' for now because the manifest only has 8 funds
  (below the 180-core / 137-§3.6-gate threshold). Graduating to 'core'
  requires expanding the manifest past ~137 entries — a later PR.

Tests updated

- resilience-release-gate: 19→21 dim count; RETIRED_DIMENSIONS allow-
  list now includes reserveAdequacy alongside fuelStockDays.
- resilience-dimension-scorers: scoreReserveAdequacy monotonicity +
  "high reserves score well" tests migrated to scoreLiquidReserve-
  Adequacy (same source, new 1..12 anchor). New retirement-shape test
  for scoreReserveAdequacy mirroring the PR 3 fuelStockDays retirement
  test. Four new scorer tests pin the three code paths of
  scoreSovereignFiscalBuffer (absent seed / no-SWF country / SWF
  country / partial-completeness derate).
- resilience-scorers fixture: baseline 60.12→60.35, recovery-domain
  flat mean 47.33→48.75, overall 63.27→63.6. Each number commented
  with the driver (split adds liquidReserveAdequacy 18@1.0 + sovereign
  FiscalBuffer 50@0.3 at IMPUTE; retired reserveAdequacy drops out).
- resilience-dimension-monotonicity: target scoreLiquidReserveAdequacy
  instead of scoreReserveAdequacy.
- resilience-handlers: response-shape dim count 19→21.
- resilience-indicator-registry: coverage 19→21 dimensions.
- resilience-dimension-freshness: allowlisted the new sovereign-wealth
  seed-meta key in KNOWN_SEEDS_NOT_IN_HEALTH for the ON_DEMAND window.
- resilience-methodology-lint HEADING_TO_DIMENSION: added the two new
  heading mappings. Methodology doc gets H4 sections for Liquid
  Reserve Adequacy and Sovereign Fiscal Buffer; Reserve Adequacy
  section is annotated as retired.
- resilience-retired-dimensions-parity: client-side
  RESILIENCE_RETIRED_DIMENSION_IDS gets reserveAdequacy. Parser
  upgraded to strip inline `// …` comments from the array body so a
  future reviewer can drop a rationale next to an entry without
  breaking parity.
- resilience-confidence-averaging: fixture updated to include both
  retired dims (reserveAdequacy + fuelStockDays) — confirms the
  registry filter correctly excludes BOTH from the visible coverage
  reading.

Extraction harness (scripts/compare-resilience-current-vs-proposed.mjs):
- recoveryLiquidReserveMonths: reads the same reserve-adequacy seed
  field as recoveryReserveMonths.
- recoverySovereignWealthEffectiveMonths: reads the new SWF seed key
  on field totalEffectiveMonths. Absent-payload → 0 for correlation
  math (matches the substantive-no-SWF scorer branch).

Out of scope for this commit (follow-ups)

- Recovery-domain weight rebalance + Spearman sensitivity rerun
  against the PR 0 baseline.
- health.js graduation (SEED_META entry + ON_DEMAND_KEYS removal) once
  Railway cron has ~7 days of clean runs.
- api/bootstrap.js wiring once an RPC consumer needs the SWF data.
- Manifest expansion past 137 countries so sovereignFiscalBuffer can
  graduate from tier='experimental' to tier='core'.

Tests: 6573/6573 data-tier tests pass. Typecheck clean on both
tsconfig configs. Biome clean on all touched files.

* fix(resilience): PR 2 review — add widget labels for new dimensions

P2 review finding on PR #3324. DIMENSION_LABELS in src/components/
resilience-widget-utils.ts covered only the old 19 dimension IDs, so
the two new active dims (liquidReserveAdequacy, sovereignFiscalBuffer)
would render with their raw internal IDs in the confidence grid for
every country once the scorer started emitting them. The widget test
at getResilienceDimensionLabel also asserted only the 19-label set,
so the gap would have shipped silently.

Fix: add user-facing short labels for both new dims. "Reserves" is
already claimed by the retired reserveAdequacy, so the replacement
disambiguates with "Liquid Reserves"; sovereignFiscalBuffer →
"Sovereign Wealth" per the methodology doc H4 heading.

Also added a regression guard — new test asserts EVERY id in
RESILIENCE_DIMENSION_ORDER resolves to a non-id label. Any future
dimension that ships without a matching DIMENSION_LABELS entry now
fails CI loudly instead of leaking the ID into the UI.

Tests: 502/502 resilience tests pass (+1 new coverage check).
Typecheck clean on both configs.

* fix(resilience): PR 2 review — remove dead IMPUTE.recoveryReserveAdequacy entry

Greptile P2: the retired scoreReserveAdequacy stub no longer reads
from IMPUTE (it hardcodes coverage=0 / imputationClass=null per the
retirement pattern), making IMPUTE.recoveryReserveAdequacy dead code.
Removed the entry + added a breadcrumb comment pointing at the
replacement IMPUTE.recoveryLiquidReserveAdequacy.

The second P2 (bootstrap.js not wired) is a deliberate non-goal — the
reviewer explicitly flags "for visibility" since it's tracked in the
PR body. No action this commit; bootstrap wiring lands alongside the
SEED_META graduation after the ~7-day Railway-cron bake-in.

Tests: 502/502 resilience tests still pass. Typecheck clean.
This commit is contained in:
Elie Habib
2026-04-23 09:01:30 +04:00
committed by GitHub
parent 29306008e4
commit c48ceea463
16 changed files with 496 additions and 59 deletions

View File

@@ -237,9 +237,34 @@ This domain forms the recovery-capacity pillar. It measures a country's ability
#### Reserve Adequacy
PR 2 §3.4 retired `reserveAdequacy` from the core overall score. The dimension remains registered for schema continuity but pins at `coverage=0`, `score=50`, `imputationClass=null` for every country (same shape as the PR 3 `fuelStockDays` retirement — the `null` tag avoids a false "Source down" label in the widget for a deliberate construct retirement). The construct split into two dimensions that separate the liquid-reserves signal from the sovereign-wealth signal: `liquidReserveAdequacy` (below) and `sovereignFiscalBuffer` (below). See the v2.3 changelog entry for the rationale.
| Indicator | Description | Direction | Goalposts (worst-best) | Weight | Source | Cadence |
|---|---|---|---|---|---|---|
| recoveryReserveMonths | Total reserves in months of imports (World Bank FI.RES.TOTL.MO) | Higher is better | 1 - 18 | 1.00 | World Bank | Annual |
| recoveryReserveMonths | Total reserves in months of imports (World Bank FI.RES.TOTL.MO) — **experimental tier, not part of core score** | Higher is better | 1 - 18 | 1.00 | World Bank | Annual |
#### Liquid Reserve Adequacy
PR 2 §3.4 replacement for the liquid-reserves half of the retired `reserveAdequacy`. Same upstream source (World Bank `FI.RES.TOTL.MO`, total reserves in months of imports) but re-anchored `1..12` months instead of `1..18`. Twelve months is the ballpark IMF "full reserve adequacy" benchmark for a diversified emerging-market importer; the tighter ceiling prevents wealthy commodity-exporters from claiming outsized credit for on-paper reserve stocks that are not the relevant shock-absorption buffer. The sovereign-wealth half of the split lives in `sovereignFiscalBuffer` below.
| Indicator | Description | Direction | Goalposts (worst-best) | Weight | Source | Cadence |
|---|---|---|---|---|---|---|
| recoveryLiquidReserveMonths | Total reserves in months of imports (World Bank FI.RES.TOTL.MO), re-anchored 1..12 | Higher is better | 1 - 12 | 1.00 | World Bank | Annual |
#### Sovereign Fiscal Buffer
PR 2 §3.4 new dimension. Measures the per-country deployable fiscal buffer from sovereign wealth fund assets, discounted by a three-component haircut (access × liquidity × transparency) per published fund governance. The composite is:
```
effectiveMonths = Σ [ (aum / annualImports × 12) × access × liquidity × transparency ]
score = 100 × (1 exp(effectiveMonths / 12))
```
The exponential saturation prevents Norway-type outliers (effective months in the 100s) from dominating the recovery pillar out of proportion to their marginal resilience benefit. Countries not in the SWF manifest (`docs/methodology/swf-classification-manifest.yaml`) score `0` with **full coverage** — this is substantive absence, not imputation: a country without a deployable sovereign-wealth buffer legitimately scores lower on this dim than a SWF-holding peer.
| Indicator | Description | Direction | Goalposts (worst-best) | Weight | Source | Cadence |
|---|---|---|---|---|---|---|
| recoverySovereignWealthEffectiveMonths | Haircut-weighted sovereign-wealth assets in months of imports, saturating | Higher is better | 0 - 60 | 1.00 | Wikipedia SWF list + per-fund articles (CC-BY-SA), haircut by swf-classification-manifest.yaml | Quarterly |
#### External Debt Coverage

View File

@@ -343,9 +343,18 @@ const EXTRACTION_RULES = {
recoveryFiscalBalance: { type: 'recovery-country-field', key: 'resilience:recovery:fiscal-space:v1', field: 'fiscalBalancePct' },
recoveryDebtToGdp: { type: 'recovery-country-field', key: 'resilience:recovery:fiscal-space:v1', field: 'debtToGdpPct' },
recoveryReserveMonths: { type: 'recovery-country-field', key: 'resilience:recovery:reserve-adequacy:v1', field: 'reserveMonths' },
// PR 2 §3.4: replacement for recoveryReserveMonths at the tighter 1..12
// anchor. Same seed key + field; the harness extracts the same value
// and the scorer applies the new goalpost.
recoveryLiquidReserveMonths: { type: 'recovery-country-field', key: 'resilience:recovery:reserve-adequacy:v1', field: 'reserveMonths' },
recoveryDebtToReserves: { type: 'recovery-country-field', key: 'resilience:recovery:external-debt:v1', field: 'debtToReservesRatio' },
recoveryImportHhi: { type: 'recovery-country-field', key: 'resilience:recovery:import-hhi:v1', field: 'hhi' },
recoveryFuelStockDays: { type: 'recovery-country-field', key: 'resilience:recovery:fuel-stocks:v1', field: 'stockDays' },
// PR 2 §3.4: SWF seed. Field is totalEffectiveMonths (pre-haircut sum
// across a country's manifest funds). Countries without a manifest
// entry score 0 via the substantive-no-SWF branch in the scorer;
// the harness treats "absent from payload" as 0 for correlation math.
recoverySovereignWealthEffectiveMonths: { type: 'recovery-country-field', key: 'resilience:recovery:sovereign-wealth:v1', field: 'totalEffectiveMonths' },
// ── stateContinuity derived signals ─────────────────────────────────
recoveryWgiContinuity: { type: 'static-wgi-mean' },

View File

@@ -21,11 +21,15 @@ export type ResilienceDimensionId =
| 'healthPublicService'
| 'foodWater'
| 'fiscalSpace'
| 'reserveAdequacy'
| 'reserveAdequacy' // RETIRED in PR 2 §3.4: replaced by
// liquidReserveAdequacy + sovereignFiscalBuffer
// (see RESILIENCE_RETIRED_DIMENSIONS below).
| 'externalDebtCoverage'
| 'importConcentration'
| 'stateContinuity'
| 'fuelStockDays';
| 'fuelStockDays'
| 'liquidReserveAdequacy' // PR 2 §3.4: WB FI.RES.TOTL.MO, anchors 1..12 months
| 'sovereignFiscalBuffer'; // PR 2 §3.4: SWF haircut with saturating transform
export type ResilienceDomainId =
| 'economic'
@@ -134,11 +138,27 @@ export const IMPUTE = {
bisCredit: IMPUTATION.curated_list_absent,
unhcrDisplacement: { score: 85, certaintyCoverage: 0.6, imputationClass: 'stable-absence' }, // crisis_monitoring_absent, displacement-specific
recoveryFiscalSpace: { score: 50, certaintyCoverage: 0.3, imputationClass: 'unmonitored' },
recoveryReserveAdequacy: { score: 50, certaintyCoverage: 0.3, imputationClass: 'unmonitored' },
// recoveryReserveAdequacy removed in PR 2 §3.4 — the retired
// scoreReserveAdequacy stub no longer reads from IMPUTE (it hardcodes
// coverage=0 / imputationClass=null per the retirement pattern). The
// replacement dimension's IMPUTE entry lives at
// `recoveryLiquidReserveAdequacy` below.
recoveryExternalDebt: { score: 50, certaintyCoverage: 0.3, imputationClass: 'unmonitored' },
recoveryImportHhi: { score: 50, certaintyCoverage: 0.3, imputationClass: 'unmonitored' },
recoveryStateContinuity: { score: 50, certaintyCoverage: 0.3, imputationClass: 'unmonitored' },
recoveryFuelStocks: { score: 50, certaintyCoverage: 0.3, imputationClass: 'unmonitored' },
// PR 2 §3.4 — same source as the retired reserveAdequacy
// (WB FI.RES.TOTL.MO) but the new dim re-anchors 1..12 months instead
// of 1..18. Fallback coverage identical because the upstream source
// has not changed.
recoveryLiquidReserveAdequacy: { score: 50, certaintyCoverage: 0.3, imputationClass: 'unmonitored' },
// PR 2 §3.4 — used when the sovereign-wealth seed key is absent
// entirely (Railway cron has not fired yet on a fresh deploy).
// Countries NOT in the manifest but payload present are handled
// separately by the scorer as "no SWF → score 0, full coverage"
// (substantive absence, not imputation — see plan §3.4 "What happens
// to no-SWF countries").
recoverySovereignFiscalBuffer: { score: 50, certaintyCoverage: 0.3, imputationClass: 'unmonitored' },
} as const satisfies Record<string, ImputationEntry>;
interface StaticIndicatorValue {
@@ -258,6 +278,13 @@ const RESILIENCE_RECOVERY_FISCAL_SPACE_KEY = 'resilience:recovery:fiscal-space:v
const RESILIENCE_RECOVERY_RESERVE_ADEQUACY_KEY = 'resilience:recovery:reserve-adequacy:v1';
const RESILIENCE_RECOVERY_EXTERNAL_DEBT_KEY = 'resilience:recovery:external-debt:v1';
const RESILIENCE_RECOVERY_IMPORT_HHI_KEY = 'resilience:recovery:import-hhi:v1';
// PR 2 §3.4 — new SWF seed populated by scripts/seed-sovereign-wealth.mjs
// (landed in #3305, wired into the resilience-recovery Railway bundle in
// #3319). Per-country shape: { funds: [...], totalEffectiveMonths,
// annualImports, expectedFunds, matchedFunds, completeness }. Countries
// not in the manifest are absent from the payload (substantive "no SWF"
// signal, distinct from the IMPUTE fallback below).
const RESILIENCE_RECOVERY_SOVEREIGN_WEALTH_KEY = 'resilience:recovery:sovereign-wealth:v1';
// RESILIENCE_RECOVERY_FUEL_STOCKS_KEY removed in PR 3: scoreFuelStockDays
// no longer reads any source key. If a new globally-comparable
// recovery-fuel concept lands in a future PR, add a new key with an
@@ -352,6 +379,8 @@ export const RESILIENCE_DIMENSION_DOMAINS: Record<ResilienceDimensionId, Resilie
importConcentration: 'recovery',
stateContinuity: 'recovery',
fuelStockDays: 'recovery',
liquidReserveAdequacy: 'recovery',
sovereignFiscalBuffer: 'recovery',
};
export const RESILIENCE_DIMENSION_ORDER: ResilienceDimensionId[] = [
@@ -369,11 +398,13 @@ export const RESILIENCE_DIMENSION_ORDER: ResilienceDimensionId[] = [
'healthPublicService',
'foodWater',
'fiscalSpace',
'reserveAdequacy',
'reserveAdequacy', // retired in PR 2 §3.4 — kept in order for structural continuity
'externalDebtCoverage',
'importConcentration',
'stateContinuity',
'fuelStockDays',
'fuelStockDays', // retired in PR 3 §3.5
'liquidReserveAdequacy', // new in PR 2 §3.4 — replaces reserveAdequacy
'sovereignFiscalBuffer', // new in PR 2 §3.4 — SWF haircut dimension
];
export const RESILIENCE_DOMAIN_ORDER: ResilienceDomainId[] = [
@@ -407,6 +438,8 @@ export const RESILIENCE_DIMENSION_TYPES: Record<ResilienceDimensionId, Resilienc
importConcentration: 'baseline',
stateContinuity: 'baseline',
fuelStockDays: 'mixed',
liquidReserveAdequacy: 'baseline',
sovereignFiscalBuffer: 'baseline',
};
function clamp(value: number, min: number, max: number): number {
@@ -1442,7 +1475,41 @@ export async function scoreFiscalSpace(
]);
}
// RETIRED in PR 2 §3.4. Superseded by `scoreLiquidReserveAdequacy` +
// `scoreSovereignFiscalBuffer`. The split was the only honest treatment
// of the construct: the previous dimension blended "central-bank reserves
// in months of imports" with an implicit assumption that sovereign wealth
// funds weren't state-deployable buffers, which systematically under-ranked
// Norway / Gulf oil states / Singapore. The new two-dimension shape
// separates the liquid-reserve signal from the SWF haircut signal.
//
// Shape mirrors scoreFuelStockDays (PR 3 §3.5 retirement):
// coverage=0 + imputationClass=null so the confidence/coverage averages
// filter it out via RESILIENCE_RETIRED_DIMENSIONS. Kept in the scorer
// map for structural continuity; a future PR can remove the dimension
// entirely once the cached response shape has bumped.
export async function scoreReserveAdequacy(
_countryCode: string,
_reader: ResilienceSeedReader = defaultSeedReader,
): Promise<ResilienceDimensionScore> {
return {
score: 50,
coverage: 0,
observedWeight: 0,
imputedWeight: 0,
imputationClass: null,
freshness: { lastObservedAtMs: 0, staleness: '' },
};
}
// PR 2 §3.4 — new dimension replacing the liquid-reserves half of the
// retired `reserveAdequacy`. Same source (World Bank `FI.RES.TOTL.MO`
// total reserves in months of imports) but re-anchored to 1..12 months
// instead of 1..18. The tighter ceiling is per the plan: "Anchors 112
// months." A country at 12+ months clamps at 100; a country at 1 month
// clamps at 0. Twelve months = ballpark IMF "full reserve adequacy"
// benchmark for a diversified emerging-market importer.
export async function scoreLiquidReserveAdequacy(
countryCode: string,
reader: ResilienceSeedReader = defaultSeedReader,
): Promise<ResilienceDimensionScore> {
@@ -1450,16 +1517,95 @@ export async function scoreReserveAdequacy(
const entry = getRecoveryCountryEntry<RecoveryReserveAdequacyCountry>(raw, countryCode);
if (!entry || entry.reserveMonths == null) {
return {
score: IMPUTE.recoveryReserveAdequacy.score,
coverage: IMPUTE.recoveryReserveAdequacy.certaintyCoverage,
score: IMPUTE.recoveryLiquidReserveAdequacy.score,
coverage: IMPUTE.recoveryLiquidReserveAdequacy.certaintyCoverage,
observedWeight: 0,
imputedWeight: 1,
imputationClass: IMPUTE.recoveryReserveAdequacy.imputationClass,
imputationClass: IMPUTE.recoveryLiquidReserveAdequacy.imputationClass,
freshness: { lastObservedAtMs: 0, staleness: '' },
};
}
return weightedBlend([
{ score: normalizeHigherBetter(Math.min(entry.reserveMonths, 18), 1, 18), weight: 1.0 },
{ score: normalizeHigherBetter(Math.min(entry.reserveMonths, 12), 1, 12), weight: 1.0 },
]);
}
// PR 2 §3.4 — new SWF haircut dimension. Reads per-country SWF records
// from `resilience:recovery:sovereign-wealth:v1` (produced by
// scripts/seed-sovereign-wealth.mjs). Composite:
// effectiveMonths = rawSwfMonths × access × liquidity × transparency
// pre-computed in the seed payload as `totalEffectiveMonths` (sum
// across a country's manifest funds). Score:
// score = 100 × (1 exp(effectiveMonths / 12))
// The exponential saturation prevents Norway-type outliers (effective
// months in the 100s) from dominating the recovery pillar out of
// proportion to their marginal resilience benefit.
//
// Three code paths:
// 1. Seed key absent entirely (Railway cron hasn't fired on fresh
// deploy) → IMPUTE fallback, score 50 / coverage 0.3 / unmonitored.
// 2. Seed key present, country in payload → saturating score. Coverage
// is derated by `completeness` so a partial-scrape on a multi-fund
// country (AE = ADIA + Mubadala, SG = GIC + Temasek) shows up
// as lower confidence rather than a silently-understated total.
// 3. Seed key present, country NOT in payload → the country has no
// sovereign wealth fund in the manifest. Per plan §3.4 "What
// happens to no-SWF countries": score 0 with FULL coverage (this
// is substantive absence, not imputation). The country stays in
// the recovery-pillar denominator with weight; 0 × weight = 0 in
// the numerator, so it correctly lowers relative recovery score
// vs SWF-holding peers.
interface RecoverySovereignWealthCountry {
totalEffectiveMonths?: number | null;
completeness?: number | null;
annualImports?: number | null;
}
interface RecoverySovereignWealthPayload {
countries?: Record<string, RecoverySovereignWealthCountry>;
}
export async function scoreSovereignFiscalBuffer(
countryCode: string,
reader: ResilienceSeedReader = defaultSeedReader,
): Promise<ResilienceDimensionScore> {
const raw = await reader(RESILIENCE_RECOVERY_SOVEREIGN_WEALTH_KEY);
const payload = raw as RecoverySovereignWealthPayload | null | undefined;
// Path 1 — seed key absent entirely. IMPUTE.
if (!payload || typeof payload !== 'object' || !payload.countries || typeof payload.countries !== 'object') {
return {
score: IMPUTE.recoverySovereignFiscalBuffer.score,
coverage: IMPUTE.recoverySovereignFiscalBuffer.certaintyCoverage,
observedWeight: 0,
imputedWeight: 1,
imputationClass: IMPUTE.recoverySovereignFiscalBuffer.imputationClass,
freshness: { lastObservedAtMs: 0, staleness: '' },
};
}
const entry = payload.countries[countryCode.toUpperCase()] ?? null;
// Path 3 — seed present, country not in manifest → no SWF.
if (!entry) {
return {
score: 0,
coverage: 1.0,
observedWeight: 1,
imputedWeight: 0,
imputationClass: null,
freshness: { lastObservedAtMs: 0, staleness: '' },
};
}
// Path 2 — country has SWF(s). Saturating transform on totalEffectiveMonths.
const em = typeof entry.totalEffectiveMonths === 'number' && Number.isFinite(entry.totalEffectiveMonths)
? Math.max(0, entry.totalEffectiveMonths)
: 0;
const score = 100 * (1 - Math.exp(-em / 12));
const completeness = typeof entry.completeness === 'number' && Number.isFinite(entry.completeness)
? Math.max(0, Math.min(1, entry.completeness))
: 1.0;
return weightedBlend([
// certaintyCoverage = completeness so partial-scrapes derate confidence
// without zeroing the observed weight. The country is still a real
// observation — just with fewer of its manifest funds resolved.
{ score, weight: 1.0, certaintyCoverage: completeness },
]);
}
@@ -1598,6 +1744,14 @@ export async function scoreStateContinuity(
// `tests/resilience-retired-dimensions-parity.test.mts`.
export const RESILIENCE_RETIRED_DIMENSIONS: ReadonlySet<ResilienceDimensionId> = new Set([
'fuelStockDays',
// PR 2 §3.4 — reserveAdequacy is retired; replaced by the split
// { liquidReserveAdequacy, sovereignFiscalBuffer }. The legacy
// scorer returns coverage=0 / imputationClass=null (same shape as
// scoreFuelStockDays post-retirement) so it's filtered from the
// confidence/coverage averages via this registry. Kept in
// RESILIENCE_DIMENSION_ORDER for structural continuity (tests,
// cached payload shape, registry membership).
'reserveAdequacy',
]);
export async function scoreFuelStockDays(
@@ -1648,6 +1802,8 @@ ResilienceDimensionId,
importConcentration: scoreImportConcentration,
stateContinuity: scoreStateContinuity,
fuelStockDays: scoreFuelStockDays,
liquidReserveAdequacy: scoreLiquidReserveAdequacy,
sovereignFiscalBuffer: scoreSovereignFiscalBuffer,
};
export async function scoreAllDimensions(

View File

@@ -905,22 +905,83 @@ export const INDICATOR_REGISTRY: IndicatorSpec[] = [
license: 'open-data',
},
// ── reserveAdequacy (1 sub-metric) ───────────────────────────────────────
// ── reserveAdequacy (RETIRED in PR 2 §3.4) ───────────────────────────────
// Replaced by liquidReserveAdequacy + sovereignFiscalBuffer. The legacy
// indicator is kept in the registry at tier='experimental' so drill-
// down views that consult the registry by dimension still see
// something structural; it does not contribute to the core score.
{
id: 'recoveryReserveMonths',
dimension: 'reserveAdequacy',
description: 'Total reserves in months of imports (World Bank FI.RES.TOTL.MO); recovery buffer against external shocks',
description: 'RETIRED in PR 2 §3.4. Legacy total-reserves-in-months-of-imports (WB FI.RES.TOTL.MO) at the 1..18 anchor. Does not contribute to the score — scoreReserveAdequacy returns coverage=0 + imputationClass=null. Superseded by recoveryLiquidReserveMonths (same source, re-anchored 1..12) + the new sovereign-wealth indicator.',
direction: 'higherBetter',
goalposts: { worst: 1, best: 18 },
weight: 1.0,
sourceKey: 'resilience:recovery:reserve-adequacy:v1',
scope: 'global',
cadence: 'annual',
tier: 'experimental',
coverage: 188,
license: 'open-data',
},
// ── liquidReserveAdequacy (1 sub-metric) ─────────────────────────────────
// PR 2 §3.4 replacement for the liquid-reserves half of the retired
// reserveAdequacy. Same source (WB FI.RES.TOTL.MO) but re-anchored
// 1..12 months instead of 1..18. Twelve months ≈ IMF "full reserve
// adequacy" ballpark for a diversified emerging-market importer.
{
id: 'recoveryLiquidReserveMonths',
dimension: 'liquidReserveAdequacy',
description: 'Total reserves in months of imports (World Bank FI.RES.TOTL.MO), re-anchored 1..12 per plan §3.4. Immediate-liquidity buffer against short external shocks, measured at central-bank reserves only — sovereign-wealth assets are scored separately in sovereignFiscalBuffer.',
direction: 'higherBetter',
goalposts: { worst: 1, best: 12 },
weight: 1.0,
sourceKey: 'resilience:recovery:reserve-adequacy:v1',
scope: 'global',
cadence: 'annual',
tier: 'core',
coverage: 188,
license: 'open-data',
},
// ── sovereignFiscalBuffer (1 sub-metric) ─────────────────────────────────
// PR 2 §3.4 — scored on the SWF haircut manifest. Payload produced by
// scripts/seed-sovereign-wealth.mjs (landed in #3305, wired into
// Railway cron in #3319). Per-country totalEffectiveMonths is the sum
// across a country's manifest funds of (aum / annualImports × 12) ×
// (access × liquidity × transparency). Scorer applies a saturating
// transform: score = 100 × (1 exp(effectiveMonths / 12)) to prevent
// Norway-type outliers from dominating the recovery pillar.
//
// Coverage for the registry entry is the current manifest size (8
// funds across NO / AE / SA / KW / QA / SG). Countries NOT in the
// manifest score 0 with full coverage (substantive "no SWF" signal,
// not imputation) — this is by design per plan §3.4 "What happens to
// no-SWF countries."
{
id: 'recoverySovereignWealthEffectiveMonths',
dimension: 'sovereignFiscalBuffer',
description: 'Sovereign-wealth fiscal-buffer signal per plan §3.4. Seeded from Wikipedia SWF list + per-fund article infoboxes (CC-BY-SA), haircut by the classification manifest (docs/methodology/swf-classification-manifest.yaml): effectiveMonths = rawSwfMonths × access × liquidity × transparency, summed across a country\'s manifest funds. Scorer applies a saturating transform score = 100 × (1 exp(effectiveMonths / 12)).',
direction: 'higherBetter',
goalposts: { worst: 0, best: 60 },
weight: 1.0,
sourceKey: 'resilience:recovery:sovereign-wealth:v1',
scope: 'global',
cadence: 'quarterly',
// tier='experimental' because the manifest ships with 8 funds (< the
// 180-country core-tier threshold / 137-country §3.6 gate). Non-SWF
// countries are scored meaningfully (0 with full coverage — a
// substantive absence, not imputation — per plan §3.4), but the
// §3.6 coverage-and-influence gate counts upstream-data coverage,
// which is 8. Graduating to 'core' requires expanding the manifest
// past 137 entries, which is a follow-up PR after external data
// partners are identified.
tier: 'experimental',
coverage: 8,
license: 'open-data',
},
// ── externalDebtCoverage (1 sub-metric) ──────────────────────────────────
{
id: 'recoveryDebtToReserves',

View File

@@ -8,8 +8,19 @@ import type { ResilienceScoreResponse } from '@/services/resilience';
// dimensions are filtered out of the displayed coverage percentage so
// a deliberate construct retirement does not silently drag the user-
// facing confidence reading down for every country.
//
// Retirement index:
// - fuelStockDays (PR 3 §3.5) — IEA days-of-stock incomparable across
// net importers vs net exporters.
// - reserveAdequacy (PR 2 §3.4) — superseded by the
// liquidReserveAdequacy +
// sovereignFiscalBuffer split.
//
// The parity test parses this Set literally, so keep the array contents
// as string literals only — do not interleave comments between entries.
const RESILIENCE_RETIRED_DIMENSION_IDS: ReadonlySet<string> = new Set([
'fuelStockDays',
'reserveAdequacy',
]);
// Gated locked-preview fixture rendered when the resilience widget is
@@ -248,6 +259,12 @@ const DIMENSION_LABELS: Record<string, string> = {
importConcentration: 'Imports',
stateContinuity: 'Continuity',
fuelStockDays: 'Fuel',
// PR 2 §3.4 — new active dimensions. Labels chosen to stay short
// enough for the 19/21-cell confidence grid without leaking the
// internal ID. "Reserves" is already taken by the retired
// reserveAdequacy so the replacement disambiguates with "Liquid".
liquidReserveAdequacy: 'Liquid Reserves',
sovereignFiscalBuffer: 'Sovereign Wealth',
};
export function getResilienceDimensionLabel(dimensionId: string): string {

View File

@@ -50,16 +50,20 @@ describe('computeOverallCoverage: retired-dim exclusion', () => {
id: 'recovery',
dimensions: [
dim('fiscalSpace', 0.9),
dim('reserveAdequacy', 0.8),
// Retired: must not pull the average down.
dim('fuelStockDays', 0),
dim('liquidReserveAdequacy', 0.8), // active replacement for reserveAdequacy
// Retired dims contribute coverage=0 in real payloads; both
// must be filtered out so the visible coverage reading
// tracks only the active dims.
dim('reserveAdequacy', 0), // retired in PR 2 §3.4
dim('fuelStockDays', 0), // retired in PR 3 §3.5
],
},
],
} as unknown as GetResilienceScoreResponse;
// (0.9 + 0.8) / 2 = 0.85. With retired included the flat mean
// would be (0.9 + 0.8 + 0) / 3 ≈ 0.5667 — the regression shape.
// (0.9 + 0.8) / 2 = 0.85 — only the two active dims count.
// With retired included the flat mean would be
// (0.9 + 0.8 + 0 + 0) / 4 = 0.425 — the regression shape.
assert.equal(computeOverallCoverage(response).toFixed(4), '0.8500');
});

View File

@@ -410,6 +410,14 @@ describe('INDICATOR_REGISTRY seed-meta coverage (T1.5 P1 regression lock)', () =
// writes this. The registry sourceKey economic:energy:v1:all does
// not strip to this shape, so SOURCE_KEY_META_OVERRIDES maps it.
'seed-meta:economic:energy-prices',
// PR 2 §3.4: seed-sovereign-wealth.mjs writes this via runSeed. Not
// yet registered in api/health.js SEED_META — per project memory
// feedback_health_required_key_needs_railway_cron_first.md, new
// seed keys go through ON_DEMAND_KEYS for ~7 days of clean Railway
// cron runs before promotion to SEED_META. A follow-up PR wires
// this once the cron has baked in; until then, allowlist it so
// the registry consistency check passes.
'seed-meta:resilience:recovery:sovereign-wealth',
]);
function extractSeedMetaKeys(filePath: string): Set<string> {

View File

@@ -27,7 +27,7 @@ import { describe, it } from 'node:test';
import {
scoreEnergy,
scoreReserveAdequacy,
scoreLiquidReserveAdequacy,
scoreFiscalSpace,
scoreExternalDebtCoverage,
scoreImportConcentration,
@@ -50,12 +50,14 @@ function makeRecoveryReader(keyValueMap: Record<string, unknown>): ResilienceSee
return async (key: string) => keyValueMap[key] ?? null;
}
describe('resilience dimension monotonicity — scoreReserveAdequacy', () => {
// PR 2 §3.4: scoreReserveAdequacy is retired. The monotonicity contract
// moves to scoreLiquidReserveAdequacy — same source but 1..12 anchor.
describe('resilience dimension monotonicity — scoreLiquidReserveAdequacy', () => {
it('higher reserveMonths → higher score', async () => {
const low = await scoreReserveAdequacy(TEST_ISO2, makeRecoveryReader({
const low = await scoreLiquidReserveAdequacy(TEST_ISO2, makeRecoveryReader({
'resilience:recovery:reserve-adequacy:v1': { countries: { [TEST_ISO2]: { reserveMonths: 2 } } },
}));
const high = await scoreReserveAdequacy(TEST_ISO2, makeRecoveryReader({
const high = await scoreLiquidReserveAdequacy(TEST_ISO2, makeRecoveryReader({
'resilience:recovery:reserve-adequacy:v1': { countries: { [TEST_ISO2]: { reserveMonths: 12 } } },
}));
assert.ok(high.score > low.score, `reserveMonths 2→12 should raise score; got ${low.score}${high.score}`);

View File

@@ -23,7 +23,9 @@ import {
scoreFuelStockDays,
scoreImportConcentration,
scoreMacroFiscal,
scoreLiquidReserveAdequacy,
scoreReserveAdequacy,
scoreSovereignFiscalBuffer,
scoreSocialCohesion,
scoreStateContinuity,
scoreTradeSanctions,
@@ -1100,13 +1102,16 @@ describe('resilience source-failure aggregation (T1.7)', () => {
it('produce plausible country ordering for the recovery-capacity dimensions', async () => {
const fiscal = await scoreTriple(scoreFiscalSpace);
const reserves = await scoreTriple(scoreReserveAdequacy);
// PR 2 §3.4: reserveAdequacy retired → test scoreLiquidReserveAdequacy
// (the replacement). Same source (WB FI.RES.TOTL.MO) but 1..12 anchor.
// Country ordering still holds: NO (14mo) > US (1mo) > YE (imputed).
const reserves = await scoreTriple(scoreLiquidReserveAdequacy);
const extDebt = await scoreTriple(scoreExternalDebtCoverage);
const importHhi = await scoreTriple(scoreImportConcentration);
const continuity = await scoreTriple(scoreStateContinuity);
assertOrdered('fiscalSpace', fiscal.no.score, fiscal.us.score, fiscal.ye.score);
assertOrdered('reserveAdequacy', reserves.no.score, reserves.us.score, reserves.ye.score);
assertOrdered('liquidReserveAdequacy', reserves.no.score, reserves.us.score, reserves.ye.score);
assertOrdered('externalDebtCoverage', extDebt.no.score, extDebt.us.score, extDebt.ye.score);
assertOrdered('importConcentration', importHhi.no.score, importHhi.us.score, importHhi.ye.score);
assertOrdered('stateContinuity', continuity.no.score, continuity.us.score, continuity.ye.score);
@@ -1127,9 +1132,100 @@ describe('resilience source-failure aggregation (T1.7)', () => {
assert.equal(score.imputedWeight, 1);
});
it('scoreReserveAdequacy: high reserves score well', async () => {
// PR 2 §3.4 — scoreReserveAdequacy is retired (coverage=0 /
// imputationClass=null regardless of seed). The "high reserves score
// well" contract moves to scoreLiquidReserveAdequacy with the new
// 1..12 anchor. NO's 14 months clamps to the top of the range → 100.
it('scoreLiquidReserveAdequacy: high reserves score at the anchor ceiling', async () => {
const no = await scoreLiquidReserveAdequacy('NO', fixtureReader);
assert.ok(no.score >= 99, `NO with 14 months reserves clamped to 12 should score >=99 on the 1..12 anchor, got ${no.score}`);
assert.ok(no.coverage >= 0.99, 'observed-data path must report full coverage');
assert.equal(no.imputationClass, null, 'observed-data path must not carry imputation class');
});
it('scoreLiquidReserveAdequacy: missing data returns unmonitored imputation', async () => {
const emptyReader = async (_key: string): Promise<unknown | null> => null;
const score = await scoreLiquidReserveAdequacy('XX', emptyReader);
assert.equal(score.imputationClass, 'unmonitored');
assert.equal(score.observedWeight, 0);
assert.equal(score.imputedWeight, 1);
});
// PR 2 §3.4 — retired scoreReserveAdequacy shape. Mirrors the
// fuelStockDays retirement test (PR 3 §3.5) — coverage=0 /
// imputationClass=null regardless of seed so the confidence /
// coverage averages filter it out via RESILIENCE_RETIRED_DIMENSIONS.
it('scoreReserveAdequacy: retired — coverage=0 / null imputationClass for every country', async () => {
const no = await scoreReserveAdequacy('NO', fixtureReader);
assert.ok(no.score > 70, `NO with 14 months reserves should score >70, got ${no.score}`);
const ye = await scoreReserveAdequacy('YE', fixtureReader);
for (const [label, result] of [['NO', no], ['YE', ye]] as const) {
assert.equal(result.coverage, 0, `${label}: retired dimension must have coverage=0`);
assert.equal(result.observedWeight, 0, `${label}: retired dimension must have observedWeight=0`);
assert.equal(result.imputedWeight, 0, `${label}: retired dimension must have imputedWeight=0`);
assert.equal(result.imputationClass, null, `${label}: retired dimension must not tag source-failure (intentional retirement, not a runtime outage)`);
}
});
// PR 2 §3.4 — scoreSovereignFiscalBuffer has three code paths per
// plan §3.4: (1) seed absent → IMPUTE, (2) seed present but country
// not in manifest → substantive "no SWF" (score=0, coverage=1.0),
// (3) country in payload → saturating transform on
// totalEffectiveMonths.
describe('scoreSovereignFiscalBuffer — three code paths', () => {
it('path 1: seed key absent → IMPUTE fallback', async () => {
const emptyReader = async (_key: string): Promise<unknown | null> => null;
const score = await scoreSovereignFiscalBuffer('US', emptyReader);
assert.equal(score.imputationClass, 'unmonitored');
assert.equal(score.observedWeight, 0);
assert.equal(score.imputedWeight, 1);
assert.equal(score.score, 50);
});
it('path 3: country not in manifest → score=0, coverage=1.0 (substantive absence)', async () => {
// Payload present but country missing → no SWF per plan §3.4
// "What happens to no-SWF countries." Must NOT fall through to
// IMPUTE.
const reader = async (_key: string) => ({ countries: { NO: { totalEffectiveMonths: 60, completeness: 1.0 } } });
const score = await scoreSovereignFiscalBuffer('US', reader);
assert.equal(score.score, 0, 'no-SWF country must score 0');
assert.equal(score.coverage, 1.0, 'no-SWF country must report FULL coverage (substantive, not imputed)');
assert.equal(score.observedWeight, 1);
assert.equal(score.imputedWeight, 0);
assert.equal(score.imputationClass, null);
});
it('path 2: country with SWF → saturating transform on totalEffectiveMonths', async () => {
// 60 effective months → 100 × (1 exp(60/12)) = 100 × (1 e^-5) ≈ 99.33
const reader = async (_key: string) => ({ countries: { NO: { totalEffectiveMonths: 60, completeness: 1.0 } } });
const score = await scoreSovereignFiscalBuffer('NO', reader);
assert.ok(score.score > 98 && score.score <= 100, `60 effective months should saturate near 100, got ${score.score}`);
assert.ok(score.coverage >= 0.99, 'full completeness should map to full coverage');
assert.equal(score.observedWeight, 1);
});
it('path 2: partial-scrape country derates coverage by completeness', async () => {
// AE = ADIA + Mubadala. If Mubadala's scrape drifts, completeness = 0.5.
// The score itself is still the saturating transform on whatever
// totalEffectiveMonths we got, but coverage reflects the partial-seed.
// Note: `coverage` (certaintyCoverage) is independent of `observedWeight`
// in weightedBlend — coverage degrades with completeness, observedWeight
// tracks the metric's nominal weight (still 1.0 for a single real-data
// metric). The two fields carry different semantics downstream.
const reader = async (_key: string) => ({ countries: { AE: { totalEffectiveMonths: 12, completeness: 0.5 } } });
const score = await scoreSovereignFiscalBuffer('AE', reader);
assert.ok(score.coverage > 0.49 && score.coverage < 0.51,
`partial-scrape (completeness=0.5) must derate coverage to ~0.5, got ${score.coverage}`);
assert.equal(score.observedWeight, 1, 'observedWeight tracks metric weight (real-data), not completeness');
assert.equal(score.imputedWeight, 0);
});
it('path 2: zero effective months → score 0 with observed coverage (fund exists but classification-haircut zeros it out)', async () => {
const reader = async (_key: string) => ({ countries: { XX: { totalEffectiveMonths: 0, completeness: 1.0 } } });
const score = await scoreSovereignFiscalBuffer('XX', reader);
assert.equal(score.score, 0);
assert.equal(score.coverage, 1.0);
assert.equal(score.observedWeight, 1);
});
});
it('scoreExternalDebtCoverage: low debt-to-reserves ratio scores well', async () => {

View File

@@ -40,7 +40,10 @@ describe('resilience handlers', () => {
assert.equal(response.countryCode, 'US');
assert.equal(response.domains.length, 6);
assert.equal(response.domains.flatMap((domain) => domain.dimensions).length, 19);
// 19 active + 2 retired (fuelStockDays, reserveAdequacy) = 21. Retired
// dims stay in the response for structural continuity; they're
// filtered out of confidence averages via RESILIENCE_RETIRED_DIMENSIONS.
assert.equal(response.domains.flatMap((domain) => domain.dimensions).length, 21);
assert.ok(response.overallScore > 0 && response.overallScore <= 100);
assert.equal(response.level, response.overallScore >= 70 ? 'high' : response.overallScore >= 40 ? 'medium' : 'low');
assert.equal(response.trend, 'rising');

View File

@@ -6,12 +6,12 @@ import { INDICATOR_REGISTRY } from '../server/worldmonitor/resilience/v1/_indica
import type { IndicatorSpec } from '../server/worldmonitor/resilience/v1/_indicator-registry.ts';
describe('indicator registry', () => {
it('covers all 19 dimensions', () => {
it('covers all 21 dimensions (19 active + 2 retired)', () => {
const coveredDimensions = new Set(INDICATOR_REGISTRY.map((i) => i.dimension));
for (const dimId of RESILIENCE_DIMENSION_ORDER) {
assert.ok(coveredDimensions.has(dimId), `${dimId} has no indicators in registry`);
}
assert.equal(coveredDimensions.size, 19);
assert.equal(coveredDimensions.size, 21);
});
it('has no duplicate indicator ids', () => {

View File

@@ -52,6 +52,8 @@ const HEADING_TO_DIMENSION: Readonly<Record<string, ResilienceDimensionId>> = {
'Import Concentration': 'importConcentration',
'State Continuity': 'stateContinuity',
'Fuel Stock Days': 'fuelStockDays',
'Liquid Reserve Adequacy': 'liquidReserveAdequacy',
'Sovereign Fiscal Buffer': 'sovereignFiscalBuffer',
};
function findMethodologyFile(): string {

View File

@@ -48,22 +48,19 @@ function installRedisFixtures() {
}
describe('resilience release gate', () => {
it('keeps all 19 dimension scorers non-placeholder for the required countries', async () => {
// PR 3 §3.5: fuelStockDays is retired — scoreFuelStockDays emits
// coverage=0 + imputationClass=null for every country. The retirement
// is intentional (construct incomparable across net importers / net
// exporters). Allow-list it so the zero-coverage placeholder check
// still catches unintended regressions in the OTHER 18 dimensions.
//
// imputationClass=null (not 'source-failure') because the widget maps
// 'source-failure' to a "Source down: upstream seeder failed" label
// with a `!` icon — surfacing that for every country on a deliberate
// retirement would manufacture a false outage signal.
const RETIRED_DIMENSIONS = new Set(['fuelStockDays']);
it('keeps all 21 dimension scorers non-placeholder for the required countries', async () => {
// PR 3 §3.5 retired fuelStockDays; PR 2 §3.4 retired reserveAdequacy
// (superseded by the liquidReserveAdequacy + sovereignFiscalBuffer
// split). Both scorers emit coverage=0 + imputationClass=null — the
// widget maps 'source-failure' to a "Source down" label, which would
// manufacture a false outage signal on every country for a deliberate
// construct retirement. Allow-list keeps the zero-coverage placeholder
// check enforcing on the OTHER 19 dimensions.
const RETIRED_DIMENSIONS = new Set(['fuelStockDays', 'reserveAdequacy']);
for (const countryCode of REQUIRED_DIMENSION_COUNTRIES) {
const scores = await scoreAllDimensions(countryCode, fixtureReader);
const entries = Object.entries(scores);
assert.equal(entries.length, 19, `${countryCode} should have all resilience dimensions`);
assert.equal(entries.length, 21, `${countryCode} should have all 21 resilience dimensions (19 active + 2 retired kept for structural continuity)`);
for (const [dimensionId, score] of entries) {
assert.ok(Number.isFinite(score.score), `${countryCode} ${dimensionId} should produce a numeric score`);
if (RETIRED_DIMENSIONS.has(dimensionId)) {
@@ -254,7 +251,7 @@ describe('resilience release gate', () => {
);
const allDimensions = response.domains.flatMap((domain) => domain.dimensions);
assert.equal(allDimensions.length, 19, 'US response should carry all 19 dimensions');
assert.equal(allDimensions.length, 21, 'US response should carry all 21 dimensions (19 active + 2 retired)');
for (const dimension of allDimensions) {
assert.equal(
typeof dimension.imputationClass,
@@ -282,7 +279,7 @@ describe('resilience release gate', () => {
);
const allDimensions = response.domains.flatMap((domain) => domain.dimensions);
assert.equal(allDimensions.length, 19, 'US response should carry all 19 dimensions');
assert.equal(allDimensions.length, 21, 'US response should carry all 21 dimensions (19 active + 2 retired)');
const validLevels = ['', 'fresh', 'aging', 'stale'];
for (const dimension of allDimensions) {
assert.ok(dimension.freshness != null, `dimension ${dimension.id} must carry a freshness payload`);

View File

@@ -31,7 +31,11 @@ function parseClientRetiredIds(): Set<string> {
'If the constant was renamed or reformatted, update this parser to match.',
);
}
const ids = match[1]!
// Strip line comments (// …) from the array body so a reviewer can
// drop an inline rationale without breaking parity. Block comments
// inside a const array are unusual enough we don't handle them.
const body = match[1]!.replace(/\/\/[^\n]*/g, '');
const ids = body
.split(',')
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0)

View File

@@ -60,17 +60,25 @@ describe('resilience scorer contracts', () => {
// source-failure when the adapter is in seed-meta failedDatasets. This is the
// single source of truth for "no currency data"; null-imputationClass paths
// on non-real-data return branches are no longer permitted.
// PR 3 §3.5: fuelStockDays removed from this set — scoreFuelStockDays
// now returns coverage=0 + imputationClass=null for every country
// (retired), so it passes the default coverage=0 assertion below
// instead of the T1.7 fall-through assertion. The `null` tag (rather
// than 'source-failure') reflects the intentional retirement — see
// the widget `formatDimensionConfidence` absent-path which would
// otherwise surface a false "Source down" label on every country.
// PR 3 §3.5: fuelStockDays retired (coverage=0 + imputationClass=null).
// PR 2 §3.4: reserveAdequacy retired (same shape). Both pass the
// default coverage=0 assertion below instead of the T1.7 fall-through
// assertion.
//
// liquidReserveAdequacy (PR 2 §3.4) is NEW and falls through to
// IMPUTE.recoveryLiquidReserveAdequacy (imputationClass=unmonitored)
// when its seed is missing — same taxonomy as the other recovery
// dims in this set.
//
// sovereignFiscalBuffer (PR 2 §3.4) falls through to
// IMPUTE.recoverySovereignFiscalBuffer when the SWF seed key is
// absent entirely. Added here alongside the other recovery
// fall-throughs.
const coverageZeroExempt = new Set([
'currencyExternal',
'fiscalSpace', 'reserveAdequacy', 'externalDebtCoverage',
'fiscalSpace', 'externalDebtCoverage',
'importConcentration', 'stateContinuity',
'liquidReserveAdequacy', 'sovereignFiscalBuffer',
]);
for (const [dimensionId, scorer] of Object.entries(RESILIENCE_DIMENSION_SCORERS)) {
const result = await scorer('US');
@@ -103,13 +111,24 @@ describe('resilience scorer contracts', () => {
// Recovery 54.83 → 47.33 after externalDebtCoverage goalpost was
// tightened from (0..5) to (0..2) per §3.5 point 3 (US ratio=1.5
// now scores 25 instead of 70).
//
// PR 2 §3.4: recovery 47.33 → 48.75 after the split. The flat mean
// now covers 8 dims for US: fiscalSpace=44, reserveAdequacy=50
// (retired, coverage=0 but still in the flat mean), externalDebt=25,
// importConcentration=88, stateContinuity=65, fuelStockDays=50
// (retired, same shape), liquidReserveAdequacy=18 (US has ~1 month
// of reserves via WB FI.RES.TOTL.MO normalized 1..12 → 18), and
// sovereignFiscalBuffer=50 (IMPUTE fallback until Railway cron
// populates the SWF seed; US has no manifest entry). Sum 390 / 8
// = 48.75. Coverage-weighted domain aggregation (used by the real
// scoring pipeline) is separately verified below.
assert.deepEqual(domainAverages, {
economic: 66.33,
infrastructure: 79,
energy: 80,
'social-governance': 61.75,
'health-food': 60.5,
recovery: 47.33,
recovery: 48.75,
});
function round(v: number, d = 2) { return Number(v.toFixed(d)); }
@@ -138,9 +157,15 @@ describe('resilience scorer contracts', () => {
const stressFactor = round(Math.max(0, Math.min(1 - stressScore / 100, 0.5)), 4);
// PR 3 §3.5: 62.64 → 63.63 (fuelStockDays retirement) → 60.12
// (externalDebtCoverage goalpost tightened; US score drops from 70
// to 25, pulling the coverage-weighted baseline mean down).
assert.equal(baselineScore, 60.12);
// (externalDebtCoverage goalpost tightened).
// PR 2 §3.4: 60.12 → 60.35 — split adds liquidReserveAdequacy
// (US ≈ 1 month WB reserves → score 18 at cov=1.0) and
// sovereignFiscalBuffer (IMPUTE at 50 / cov=0.3) into the baseline
// coverage-weighted mean. Net effect is a small upward shift
// because the retired reserveAdequacy's 50-at-coverage-weighted-1
// is replaced by the same total weight split across the two new
// dims with different coverage profiles.
assert.equal(baselineScore, 60.35);
// PR 3 §3.5: 65.84 → 67.85 (fuelStockDays retirement) → 67.21
// (currencyExternal rebuilt on IMF inflation + WB reserves, coverage
// shifts and US stress score moves). stressFactor updates in lockstep:
@@ -161,7 +186,12 @@ describe('resilience scorer contracts', () => {
// PR 3 §3.5: 65.57 → 65.82 (fuelStockDays retirement) → 65.52
// (currencyExternal rebuild) → 63.27 (externalDebtCoverage goalpost
// tightened 0..5 → 0..2; US recovery-domain contribution drops).
assert.equal(overallScore, 63.27);
// PR 2 §3.4: 63.27 → 63.6 after the reserveAdequacy split. The new
// liquidReserveAdequacy at score=18 / coverage=1.0 + sovereign-
// FiscalBuffer at score=50 / coverage=0.3 shifts the recovery-
// domain coverage-weighted mean upward (retired reserveAdequacy
// dropped out with coverage=0), lifting the overall by ~0.33.
assert.equal(overallScore, 63.6);
});
it('baselineScore is computed from baseline + mixed dimensions only', async () => {
@@ -234,7 +264,9 @@ describe('resilience scorer contracts', () => {
assert.ok(expected > 0, 'overall should be positive');
// PR 3 §3.5: 65.82 → 65.52 (currencyExternal rebuild) → 63.27 after
// externalDebtCoverage goalpost tightened from (0..5) to (0..2).
assert.equal(expected, 63.27, 'overallScore should match sum(domainScore * domainWeight); 65.52 → 63.27 after PR 3 §3.5 externalDebtCoverage re-goalpost');
// PR 2 §3.4: 63.27 → 63.6 after reserveAdequacy retirement + the
// liquidReserveAdequacy / sovereignFiscalBuffer split.
assert.equal(expected, 63.6, 'overallScore should match sum(domainScore * domainWeight); 63.27 → 63.6 after PR 2 §3.4 reserveAdequacy split');
});
it('stressFactor is still computed (informational) and clamped to [0, 0.5]', () => {

View File

@@ -187,7 +187,7 @@ test('baseResponse includes dataVersion (regression for T1.4 wiring)', () => {
// scorer dimension must have a stable display label and a consistent
// status classification.
test('getResilienceDimensionLabel returns short stable labels for all 19 dimensions', () => {
test('getResilienceDimensionLabel returns short stable labels for all 21 dimensions', () => {
assert.equal(getResilienceDimensionLabel('macroFiscal'), 'Macro');
assert.equal(getResilienceDimensionLabel('currencyExternal'), 'Currency');
assert.equal(getResilienceDimensionLabel('tradeSanctions'), 'Trade');
@@ -207,11 +207,32 @@ test('getResilienceDimensionLabel returns short stable labels for all 19 dimensi
assert.equal(getResilienceDimensionLabel('importConcentration'), 'Imports');
assert.equal(getResilienceDimensionLabel('stateContinuity'), 'Continuity');
assert.equal(getResilienceDimensionLabel('fuelStockDays'), 'Fuel');
// PR 2 §3.4 — new active dimensions. Retired reserveAdequacy's
// label stays ('Reserves'), and the live-data replacement
// disambiguates with 'Liquid Reserves'.
assert.equal(getResilienceDimensionLabel('liquidReserveAdequacy'), 'Liquid Reserves');
assert.equal(getResilienceDimensionLabel('sovereignFiscalBuffer'), 'Sovereign Wealth');
// Unknown dimension IDs fall through to the raw ID so the render
// never silently drops a row.
assert.equal(getResilienceDimensionLabel('unknownDim'), 'unknownDim');
});
// Every ID in RESILIENCE_DIMENSION_ORDER must have a display label —
// without this coverage the widget silently leaks raw internal IDs
// into the confidence grid for any new dimension that ships without
// a matching DIMENSION_LABELS entry (PR #3324 review-catch).
test('getResilienceDimensionLabel covers every dimension in RESILIENCE_DIMENSION_ORDER', async () => {
const { RESILIENCE_DIMENSION_ORDER } = await import('../server/worldmonitor/resilience/v1/_dimension-scorers.ts');
const leaks: string[] = [];
for (const id of RESILIENCE_DIMENSION_ORDER) {
const label = getResilienceDimensionLabel(id);
if (label === id) leaks.push(id);
}
assert.deepEqual(leaks, [],
`DIMENSION_LABELS missing entries for: ${leaks.join(', ')}. ` +
`Every new dimension must land its user-facing short label in src/components/resilience-widget-utils.ts.`);
});
test('formatDimensionConfidence classifies observed-heavy dimensions as observed', () => {
const result = formatDimensionConfidence({
id: 'macroFiscal',