feat(resilience): add score confidence intervals via batch Monte Carlo (#2877)

* feat(resilience): add score confidence intervals via batch Monte Carlo

Weekly cron perturbs domain weights ±10% across 100 draws per country,
stores p05/p95 in Redis. Score handler reads intervals and includes
them in the API response as ScoreInterval { p05, p95 }.

Proto field 14 (score_interval) added to GetResilienceScoreResponse.

* chore: regenerate proto types and OpenAPI docs for ScoreInterval

* fix(resilience): add seed-meta + lock + fix interval cache + percentile formula

1. Write seed-meta:resilience:intervals for health monitoring
2. Add distributed lock to prevent concurrent cron overlap
3. Move scoreInterval read outside 6h score cache boundary
4. Fix percentile index from floor to ceil-1 (nearest-rank)

* fix(health): add resilience:intervals to health + seed-health registries

* fix(seed): skip seed-meta on no-op runs + register intervals in health check
This commit is contained in:
Elie Habib
2026-04-09 22:06:54 +04:00
committed by GitHub
parent a4ded3f5da
commit 0a1b74a9b2
13 changed files with 382 additions and 3 deletions

View File

@@ -152,6 +152,7 @@ const STANDALONE_KEYS = {
portwatchChokepointsRef: 'portwatch:chokepoints:ref:v1',
chokepointFlows: 'energy:chokepoint-flows:v1',
emberElectricity: 'energy:ember:v1:_all',
resilienceIntervals: 'resilience:intervals:v1:US',
};
const SEED_META = {
@@ -270,6 +271,7 @@ const SEED_META = {
vpdTrackerHistorical: { key: 'seed-meta:health:vpd-tracker', maxStaleMin: 2880 }, // shares seed-meta key with vpdTrackerRealtime (same run)
resilienceStaticIndex: { key: 'seed-meta:resilience:static', maxStaleMin: 576000 }, // annual October snapshot; 400d threshold matches TTL and preserves prior-year data on source outages
resilienceRanking: { key: 'seed-meta:resilience:ranking', maxStaleMin: 720 }, // on-demand RPC cache (6h TTL); 12h threshold catches stale rankings without paging on cold start
resilienceIntervals: { key: 'seed-meta:resilience:intervals', maxStaleMin: 20160 }, // weekly cron; 20160min = 14d = 2x interval
energyExposure: { key: 'seed-meta:economic:owid-energy-mix', maxStaleMin: 50400 }, // monthly cron on 1st; 50400min = 35d = TTL matches cron cadence + 5d buffer
energyMixAll: { key: 'seed-meta:economic:owid-energy-mix', maxStaleMin: 50400 }, // same seed run as energyExposure; shares seed-meta key
regulatoryActions: { key: 'seed-meta:regulatory:actions', maxStaleMin: 360 }, // 2h cron; 360min = 3x interval

View File

@@ -65,6 +65,7 @@ const SEED_DOMAINS = {
'economic:grocery-basket': { key: 'seed-meta:economic:grocery-basket', intervalMin: 5040 }, // weekly seed; intervalMin = maxStaleMin / 2
'economic:bigmac': { key: 'seed-meta:economic:bigmac', intervalMin: 5040 }, // weekly seed; intervalMin = maxStaleMin / 2
'resilience:static': { key: 'seed-meta:resilience:static', intervalMin: 288000 }, // annual October snapshot; intervalMin = health.js maxStaleMin / 2 (400d alert threshold)
'resilience:intervals': { key: 'seed-meta:resilience:intervals', intervalMin: 10080 }, // weekly cron; intervalMin = health.js maxStaleMin / 2 (20160 / 2)
'regulatory:actions': { key: 'seed-meta:regulatory:actions', intervalMin: 120 }, // 2h cron; intervalMin = maxStaleMin / 3
'economic:owid-energy-mix': { key: 'seed-meta:economic:owid-energy-mix', intervalMin: 25200 }, // monthly cron on 1st; intervalMin = health.js maxStaleMin / 2 (50400 / 2)
'economic:fao-ffpi': { key: 'seed-meta:economic:fao-ffpi', intervalMin: 43200 }, // monthly seed; intervalMin = health.js maxStaleMin / 2 (86400 / 2)

View File

@@ -1 +1 @@
{"components":{"schemas":{"Error":{"description":"Error is returned when a handler encounters an error. It contains a simple error message that the developer can customize.","properties":{"message":{"description":"Error message (e.g., 'user not found', 'database connection failed')","type":"string"}},"type":"object"},"FieldViolation":{"description":"FieldViolation describes a single validation error for a specific field.","properties":{"description":{"description":"Human-readable description of the validation violation (e.g., 'must be a valid email address', 'required field missing')","type":"string"},"field":{"description":"The field path that failed validation (e.g., 'user.email' for nested fields). For header validation, this will be the header name (e.g., 'X-API-Key')","type":"string"}},"required":["field","description"],"type":"object"},"GetResilienceRankingRequest":{"type":"object"},"GetResilienceRankingResponse":{"properties":{"greyedOut":{"items":{"$ref":"#/components/schemas/ResilienceRankingItem"},"type":"array"},"items":{"items":{"$ref":"#/components/schemas/ResilienceRankingItem"},"type":"array"}},"type":"object"},"GetResilienceScoreRequest":{"properties":{"countryCode":{"type":"string"}},"type":"object"},"GetResilienceScoreResponse":{"properties":{"baselineScore":{"format":"double","type":"number"},"change30d":{"format":"double","type":"number"},"countryCode":{"type":"string"},"dataVersion":{"type":"string"},"domains":{"items":{"$ref":"#/components/schemas/ResilienceDomain"},"type":"array"},"imputationShare":{"format":"double","type":"number"},"level":{"type":"string"},"lowConfidence":{"type":"boolean"},"overallScore":{"format":"double","type":"number"},"stressFactor":{"format":"double","type":"number"},"stressScore":{"format":"double","type":"number"},"trend":{"type":"string"}},"type":"object"},"ResilienceDimension":{"properties":{"coverage":{"format":"double","type":"number"},"id":{"type":"string"},"imputedWeight":{"format":"double","type":"number"},"observedWeight":{"format":"double","type":"number"},"score":{"format":"double","type":"number"}},"type":"object"},"ResilienceDomain":{"properties":{"dimensions":{"items":{"$ref":"#/components/schemas/ResilienceDimension"},"type":"array"},"id":{"type":"string"},"score":{"format":"double","type":"number"},"weight":{"format":"double","type":"number"}},"type":"object"},"ResilienceRankingItem":{"properties":{"countryCode":{"type":"string"},"level":{"type":"string"},"lowConfidence":{"type":"boolean"},"overallCoverage":{"format":"double","type":"number"},"overallScore":{"format":"double","type":"number"}},"type":"object"},"ValidationError":{"description":"ValidationError is returned when request validation fails. It contains a list of field violations describing what went wrong.","properties":{"violations":{"description":"List of validation violations","items":{"$ref":"#/components/schemas/FieldViolation"},"type":"array"}},"required":["violations"],"type":"object"}}},"info":{"title":"ResilienceService API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/api/resilience/v1/get-resilience-ranking":{"get":{"operationId":"GetResilienceRanking","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetResilienceRankingResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetResilienceRanking","tags":["ResilienceService"]}},"/api/resilience/v1/get-resilience-score":{"get":{"operationId":"GetResilienceScore","parameters":[{"in":"query","name":"countryCode","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetResilienceScoreResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetResilienceScore","tags":["ResilienceService"]}}}}
{"components":{"schemas":{"Error":{"description":"Error is returned when a handler encounters an error. It contains a simple error message that the developer can customize.","properties":{"message":{"description":"Error message (e.g., 'user not found', 'database connection failed')","type":"string"}},"type":"object"},"FieldViolation":{"description":"FieldViolation describes a single validation error for a specific field.","properties":{"description":{"description":"Human-readable description of the validation violation (e.g., 'must be a valid email address', 'required field missing')","type":"string"},"field":{"description":"The field path that failed validation (e.g., 'user.email' for nested fields). For header validation, this will be the header name (e.g., 'X-API-Key')","type":"string"}},"required":["field","description"],"type":"object"},"GetResilienceRankingRequest":{"type":"object"},"GetResilienceRankingResponse":{"properties":{"greyedOut":{"items":{"$ref":"#/components/schemas/ResilienceRankingItem"},"type":"array"},"items":{"items":{"$ref":"#/components/schemas/ResilienceRankingItem"},"type":"array"}},"type":"object"},"GetResilienceScoreRequest":{"properties":{"countryCode":{"type":"string"}},"type":"object"},"GetResilienceScoreResponse":{"properties":{"baselineScore":{"format":"double","type":"number"},"change30d":{"format":"double","type":"number"},"countryCode":{"type":"string"},"dataVersion":{"type":"string"},"domains":{"items":{"$ref":"#/components/schemas/ResilienceDomain"},"type":"array"},"imputationShare":{"format":"double","type":"number"},"level":{"type":"string"},"lowConfidence":{"type":"boolean"},"overallScore":{"format":"double","type":"number"},"scoreInterval":{"$ref":"#/components/schemas/ScoreInterval"},"stressFactor":{"format":"double","type":"number"},"stressScore":{"format":"double","type":"number"},"trend":{"type":"string"}},"type":"object"},"ResilienceDimension":{"properties":{"coverage":{"format":"double","type":"number"},"id":{"type":"string"},"imputedWeight":{"format":"double","type":"number"},"observedWeight":{"format":"double","type":"number"},"score":{"format":"double","type":"number"}},"type":"object"},"ResilienceDomain":{"properties":{"dimensions":{"items":{"$ref":"#/components/schemas/ResilienceDimension"},"type":"array"},"id":{"type":"string"},"score":{"format":"double","type":"number"},"weight":{"format":"double","type":"number"}},"type":"object"},"ResilienceRankingItem":{"properties":{"countryCode":{"type":"string"},"level":{"type":"string"},"lowConfidence":{"type":"boolean"},"overallCoverage":{"format":"double","type":"number"},"overallScore":{"format":"double","type":"number"}},"type":"object"},"ScoreInterval":{"properties":{"p05":{"format":"double","type":"number"},"p95":{"format":"double","type":"number"}},"type":"object"},"ValidationError":{"description":"ValidationError is returned when request validation fails. It contains a list of field violations describing what went wrong.","properties":{"violations":{"description":"List of validation violations","items":{"$ref":"#/components/schemas/FieldViolation"},"type":"array"}},"required":["violations"],"type":"object"}}},"info":{"title":"ResilienceService API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/api/resilience/v1/get-resilience-ranking":{"get":{"operationId":"GetResilienceRanking","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetResilienceRankingResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetResilienceRanking","tags":["ResilienceService"]}},"/api/resilience/v1/get-resilience-score":{"get":{"operationId":"GetResilienceScore","parameters":[{"in":"query","name":"countryCode","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetResilienceScoreResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetResilienceScore","tags":["ResilienceService"]}}}}

View File

@@ -132,6 +132,8 @@ components:
format: double
dataVersion:
type: string
scoreInterval:
$ref: '#/components/schemas/ScoreInterval'
ResilienceDomain:
type: object
properties:
@@ -164,6 +166,15 @@ components:
imputedWeight:
type: number
format: double
ScoreInterval:
type: object
properties:
p05:
type: number
format: double
p95:
type: number
format: double
GetResilienceRankingRequest:
type: object
GetResilienceRankingResponse:

View File

@@ -9,6 +9,11 @@ message GetResilienceScoreRequest {
string country_code = 1 [(sebuf.http.query) = { name: "countryCode", required: true }];
}
message ScoreInterval {
double p05 = 1;
double p95 = 2;
}
message GetResilienceScoreResponse {
string country_code = 1;
double overall_score = 2;
@@ -23,4 +28,5 @@ message GetResilienceScoreResponse {
double stress_score = 11;
double stress_factor = 12;
string data_version = 13;
ScoreInterval score_interval = 14;
}

View File

@@ -0,0 +1,176 @@
#!/usr/bin/env node
import {
acquireLockSafely,
getRedisCredentials,
loadEnvFile,
logSeedResult,
releaseLock,
writeFreshnessMetadata,
} from './_seed-utils.mjs';
loadEnvFile(import.meta.url);
const API_BASE = process.env.API_BASE_URL || 'https://api.worldmonitor.app';
const WM_KEY = process.env.WORLDMONITOR_API_KEY || '';
const SEED_UA = 'Mozilla/5.0 (compatible; WorldMonitor-Seed/1.0)';
const INTERVAL_KEY_PREFIX = 'resilience:intervals:v1:';
const INTERVAL_TTL_SECONDS = 7 * 24 * 60 * 60;
const DRAWS = 100;
const DOMAIN_WEIGHTS = {
economic: 0.22,
infrastructure: 0.20,
energy: 0.15,
'social-governance': 0.25,
'health-food': 0.18,
};
const DOMAIN_ORDER = [
'economic',
'infrastructure',
'energy',
'social-governance',
'health-food',
];
export function computeIntervals(domainScores, domainWeights, draws = DRAWS) {
const samples = [];
for (let i = 0; i < draws; i++) {
const jittered = domainWeights.map((w) => w * (0.9 + Math.random() * 0.2));
const sum = jittered.reduce((s, w) => s + w, 0);
const normalized = jittered.map((w) => w / sum);
const score = domainScores.reduce((s, d, idx) => s + d * normalized[idx], 0);
samples.push(score);
}
samples.sort((a, b) => a - b);
return {
p05: Math.round(samples[Math.max(0, Math.ceil(draws * 0.05) - 1)] * 10) / 10,
p95: Math.round(samples[Math.min(draws - 1, Math.ceil(draws * 0.95) - 1)] * 10) / 10,
};
}
async function redisPipeline(url, token, commands) {
const resp = await fetch(`${url}/pipeline`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify(commands),
signal: AbortSignal.timeout(30_000),
});
if (!resp.ok) {
const text = await resp.text().catch(() => '');
throw new Error(`Redis pipeline HTTP ${resp.status}${text.slice(0, 200)}`);
}
return resp.json();
}
async function fetchRanking() {
const headers = { 'User-Agent': SEED_UA, Accept: 'application/json' };
if (WM_KEY) headers['X-WorldMonitor-Key'] = WM_KEY;
const resp = await fetch(`${API_BASE}/api/resilience/v1/get-resilience-ranking`, {
headers,
signal: AbortSignal.timeout(60_000),
});
if (!resp.ok) throw new Error(`Ranking endpoint returned HTTP ${resp.status}`);
return resp.json();
}
async function fetchScore(countryCode) {
const headers = { 'User-Agent': SEED_UA, Accept: 'application/json' };
if (WM_KEY) headers['X-WorldMonitor-Key'] = WM_KEY;
const url = `${API_BASE}/api/resilience/v1/get-resilience-score?countryCode=${countryCode}`;
const resp = await fetch(url, { headers, signal: AbortSignal.timeout(30_000) });
if (!resp.ok) throw new Error(`Score endpoint returned HTTP ${resp.status} for ${countryCode}`);
return resp.json();
}
async function seedResilienceIntervals() {
const { url, token } = getRedisCredentials();
const runId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const lockResult = await acquireLockSafely('resilience:intervals', runId, 600_000);
if (!lockResult.locked) return { skipped: true, reason: 'concurrent_run' };
try {
console.log('[resilience-intervals] Fetching ranking...');
const ranking = await fetchRanking();
const allItems = [...(ranking.items ?? []), ...(ranking.greyedOut ?? [])];
console.log(`[resilience-intervals] ${allItems.length} countries in ranking`);
if (allItems.length === 0) {
return { skipped: true, reason: 'empty_ranking' };
}
const BATCH = 10;
let computed = 0;
const commands = [];
for (let i = 0; i < allItems.length; i += BATCH) {
const batch = allItems.slice(i, i + BATCH);
const results = await Promise.allSettled(
batch.map((item) => fetchScore(item.countryCode)),
);
for (let j = 0; j < results.length; j++) {
const result = results[j];
if (result.status !== 'fulfilled') {
console.warn(`[resilience-intervals] Failed ${batch[j].countryCode}: ${result.reason?.message}`);
continue;
}
const scoreData = result.value;
if (!scoreData?.domains?.length) continue;
const domainScores = DOMAIN_ORDER.map((id) => {
const d = scoreData.domains.find((dom) => dom.id === id);
return d?.score ?? 0;
});
const weights = DOMAIN_ORDER.map((id) => DOMAIN_WEIGHTS[id]);
const interval = computeIntervals(domainScores, weights, DRAWS);
const payload = {
p05: interval.p05,
p95: interval.p95,
draws: DRAWS,
computedAt: new Date().toISOString(),
};
const key = `${INTERVAL_KEY_PREFIX}${scoreData.countryCode}`;
commands.push(['SET', key, JSON.stringify(payload), 'EX', INTERVAL_TTL_SECONDS]);
computed++;
}
}
if (commands.length > 0) {
const PIPE_BATCH = 50;
for (let i = 0; i < commands.length; i += PIPE_BATCH) {
await redisPipeline(url, token, commands.slice(i, i + PIPE_BATCH));
}
}
console.log(`[resilience-intervals] Wrote ${computed}/${allItems.length} intervals`);
return { skipped: false, recordCount: computed, total: allItems.length };
} finally {
await releaseLock('resilience:intervals', runId);
}
}
async function main() {
const startedAt = Date.now();
const result = await seedResilienceIntervals();
logSeedResult('resilience:intervals', result.recordCount ?? 0, Date.now() - startedAt, {
skipped: Boolean(result.skipped),
...(result.total != null && { total: result.total }),
...(result.reason != null && { reason: result.reason }),
});
if (!result.skipped) {
await writeFreshnessMetadata('resilience', 'intervals', result.recordCount ?? 0, '', 7 * 24 * 3600);
}
}
if (process.argv[1]?.endsWith('seed-resilience-intervals.mjs')) {
main().catch((err) => {
const message = err instanceof Error ? err.message : String(err);
console.error(`FATAL: ${message}`);
process.exit(1);
});
}

View File

@@ -3,6 +3,7 @@ import type {
ResilienceDimension,
ResilienceDomain,
ResilienceRankingItem,
ScoreInterval,
} from '../../../../src/generated/server/worldmonitor/resilience/v1/service_server';
import { cachedFetchJson, getCachedJson, runRedisPipeline } from '../../../_shared/redis';
@@ -26,6 +27,7 @@ export const RESILIENCE_SCORE_CACHE_PREFIX = 'resilience:score:v7:';
export const RESILIENCE_HISTORY_KEY_PREFIX = 'resilience:history:v4:';
export const RESILIENCE_RANKING_CACHE_KEY = 'resilience:ranking:v7';
export const RESILIENCE_STATIC_INDEX_KEY = 'resilience:static:index:v1';
export const RESILIENCE_INTERVAL_KEY_PREFIX = 'resilience:intervals:v1:';
const RESILIENCE_STATIC_META_KEY = 'seed-meta:resilience:static';
const LOW_CONFIDENCE_COVERAGE_THRESHOLD = 0.55;
@@ -54,6 +56,16 @@ function scoreCacheKey(countryCode: string): string {
return `${RESILIENCE_SCORE_CACHE_PREFIX}${countryCode}`;
}
function intervalCacheKey(countryCode: string): string {
return `${RESILIENCE_INTERVAL_KEY_PREFIX}${countryCode}`;
}
async function readScoreInterval(countryCode: string): Promise<ScoreInterval | undefined> {
const raw = await getCachedJson(intervalCacheKey(countryCode), true) as { p05?: number; p95?: number } | null;
if (!raw || typeof raw.p05 !== 'number' || typeof raw.p95 !== 'number') return undefined;
return { p05: raw.p05, p95: raw.p95 };
}
function historyKey(countryCode: string): string {
return `${RESILIENCE_HISTORY_KEY_PREFIX}${countryCode}`;
}
@@ -166,7 +178,7 @@ export async function ensureResilienceScoreCached(countryCode: string, reader?:
};
}
return await cachedFetchJson<GetResilienceScoreResponse>(
const cached = await cachedFetchJson<GetResilienceScoreResponse>(
scoreCacheKey(normalizedCountryCode),
RESILIENCE_SCORE_CACHE_TTL_SECONDS,
async () => {
@@ -234,6 +246,12 @@ export async function ensureResilienceScoreCached(countryCode: string, reader?:
imputationShare: 0,
dataVersion: '',
};
const scoreInterval = await readScoreInterval(normalizedCountryCode);
if (scoreInterval) {
return { ...cached, scoreInterval };
}
return cached;
}
export async function listScorableCountries(): Promise<string[]> {

View File

@@ -262,6 +262,12 @@ export class ResilienceWidget {
'div',
{ className: 'resilience-widget__overall-meta' },
h('span', { className: 'resilience-widget__overall-score' }, String(Math.round(clampScore(data.overallScore)))),
...(data.scoreInterval
? [h('span', {
className: 'resilience-widget__overall-interval',
title: `95% confidence interval: ${data.scoreInterval.p05} - ${data.scoreInterval.p95}`,
}, `[${Math.round(data.scoreInterval.p05)}\u2013${Math.round(data.scoreInterval.p95)}]`)]
: []),
h('span', { className: 'resilience-widget__overall-level', style: { color: levelColor } }, levelLabel),
h('span', { className: 'resilience-widget__overall-trend' }, `${getResilienceTrendArrow(data.trend)} ${data.trend}`),
),

View File

@@ -19,6 +19,7 @@ export interface GetResilienceScoreResponse {
stressScore: number;
stressFactor: number;
dataVersion: string;
scoreInterval?: ScoreInterval;
}
export interface ResilienceDomain {
@@ -36,6 +37,11 @@ export interface ResilienceDimension {
imputedWeight: number;
}
export interface ScoreInterval {
p05: number;
p95: number;
}
export interface GetResilienceRankingRequest {
}

View File

@@ -19,6 +19,7 @@ export interface GetResilienceScoreResponse {
stressScore: number;
stressFactor: number;
dataVersion: string;
scoreInterval?: ScoreInterval;
}
export interface ResilienceDomain {
@@ -36,6 +37,11 @@ export interface ResilienceDimension {
imputedWeight: number;
}
export interface ScoreInterval {
p05: number;
p95: number;
}
export interface GetResilienceRankingRequest {
}

View File

@@ -5,12 +5,13 @@ import {
type ResilienceDomain,
type ResilienceDimension,
type ResilienceRankingItem,
type ScoreInterval,
} from '@/generated/client/worldmonitor/resilience/v1/service_client';
import { getRpcBaseUrl } from '@/services/rpc-client';
export type ResilienceScoreResponse = GetResilienceScoreResponse;
export type ResilienceRankingResponse = GetResilienceRankingResponse;
export type { ResilienceDomain, ResilienceDimension, ResilienceRankingItem };
export type { ResilienceDomain, ResilienceDimension, ResilienceRankingItem, ScoreInterval };
let _client: ResilienceServiceClient | null = null;

View File

@@ -0,0 +1,67 @@
import assert from 'node:assert/strict';
import { afterEach, describe, it } from 'node:test';
import { getResilienceScore } from '../server/worldmonitor/resilience/v1/get-resilience-score.ts';
import { createRedisFetch } from './helpers/fake-upstash-redis.mts';
import { RESILIENCE_FIXTURES } from './helpers/resilience-fixtures.mts';
const originalFetch = globalThis.fetch;
const originalRedisUrl = process.env.UPSTASH_REDIS_REST_URL;
const originalRedisToken = process.env.UPSTASH_REDIS_REST_TOKEN;
const originalVercelEnv = process.env.VERCEL_ENV;
afterEach(() => {
globalThis.fetch = originalFetch;
if (originalRedisUrl == null) delete process.env.UPSTASH_REDIS_REST_URL;
else process.env.UPSTASH_REDIS_REST_URL = originalRedisUrl;
if (originalRedisToken == null) delete process.env.UPSTASH_REDIS_REST_TOKEN;
else process.env.UPSTASH_REDIS_REST_TOKEN = originalRedisToken;
if (originalVercelEnv == null) delete process.env.VERCEL_ENV;
else process.env.VERCEL_ENV = originalVercelEnv;
});
describe('resilience score interval integration', () => {
it('includes scoreInterval when Redis has interval data', async () => {
process.env.UPSTASH_REDIS_REST_URL = 'https://redis.example';
process.env.UPSTASH_REDIS_REST_TOKEN = 'token';
delete process.env.VERCEL_ENV;
const fixtures = {
...RESILIENCE_FIXTURES,
'resilience:intervals:v1:US': {
p05: 65.2,
p95: 72.8,
draws: 100,
computedAt: '2026-04-06T00:00:00.000Z',
},
};
const { fetchImpl } = createRedisFetch(fixtures);
globalThis.fetch = fetchImpl;
const response = await getResilienceScore(
{ request: new Request('https://example.com') } as never,
{ countryCode: 'US' },
);
assert.ok(response.scoreInterval, 'scoreInterval should be present');
assert.equal(response.scoreInterval.p05, 65.2);
assert.equal(response.scoreInterval.p95, 72.8);
});
it('omits scoreInterval when Redis has no interval data', async () => {
process.env.UPSTASH_REDIS_REST_URL = 'https://redis.example';
process.env.UPSTASH_REDIS_REST_TOKEN = 'token';
delete process.env.VERCEL_ENV;
const { fetchImpl } = createRedisFetch(RESILIENCE_FIXTURES);
globalThis.fetch = fetchImpl;
const response = await getResilienceScore(
{ request: new Request('https://example.com') } as never,
{ countryCode: 'US' },
);
assert.equal(response.scoreInterval, undefined, 'scoreInterval should be absent when no interval data exists');
});
});

View File

@@ -0,0 +1,79 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { computeIntervals } from '../scripts/seed-resilience-intervals.mjs';
describe('computeIntervals', () => {
it('returns p05 and p95 within expected bounds', () => {
const domainScores = [80, 70, 60, 75, 65];
const domainWeights = [0.22, 0.20, 0.15, 0.25, 0.18];
const result = computeIntervals(domainScores, domainWeights, 1000);
assert.equal(typeof result.p05, 'number');
assert.equal(typeof result.p95, 'number');
assert.ok(result.p05 < result.p95, `p05 (${result.p05}) should be less than p95 (${result.p95})`);
assert.ok(result.p05 > 0, 'p05 should be positive');
assert.ok(result.p95 <= 100, 'p95 should not exceed 100');
});
it('produces narrow interval for uniform domain scores', () => {
const domainScores = [70, 70, 70, 70, 70];
const domainWeights = [0.22, 0.20, 0.15, 0.25, 0.18];
const result = computeIntervals(domainScores, domainWeights, 1000);
assert.ok(result.p95 - result.p05 < 1, `Uniform scores should produce narrow interval, got ${result.p05}-${result.p95}`);
});
it('produces wider interval for divergent domain scores', () => {
const domainScores = [95, 20, 80, 10, 60];
const domainWeights = [0.22, 0.20, 0.15, 0.25, 0.18];
const result = computeIntervals(domainScores, domainWeights, 1000);
assert.ok(result.p95 - result.p05 > 1, `Divergent scores should produce wider interval, got ${result.p05}-${result.p95}`);
});
it('respects custom draw count', () => {
const domainScores = [60, 70, 80, 50, 65];
const domainWeights = [0.22, 0.20, 0.15, 0.25, 0.18];
const result = computeIntervals(domainScores, domainWeights, 50);
assert.equal(typeof result.p05, 'number');
assert.equal(typeof result.p95, 'number');
assert.ok(result.p05 < result.p95);
});
it('rounds to one decimal place', () => {
const domainScores = [72, 68, 55, 81, 44];
const domainWeights = [0.22, 0.20, 0.15, 0.25, 0.18];
const result = computeIntervals(domainScores, domainWeights, 100);
const p05Decimals = String(result.p05).split('.')[1]?.length ?? 0;
const p95Decimals = String(result.p95).split('.')[1]?.length ?? 0;
assert.ok(p05Decimals <= 1, `p05 should have at most 1 decimal, got ${result.p05}`);
assert.ok(p95Decimals <= 1, `p95 should have at most 1 decimal, got ${result.p95}`);
});
});
describe('seed script is self-contained .mjs', () => {
it('does not import from ../server/', async () => {
const { readFileSync } = await import('node:fs');
const { fileURLToPath } = await import('node:url');
const { dirname, join } = await import('node:path');
const dir = dirname(fileURLToPath(import.meta.url));
const src = readFileSync(join(dir, '..', 'scripts', 'seed-resilience-intervals.mjs'), 'utf8');
assert.equal(src.includes('../server/'), false, 'Must not import from ../server/');
assert.equal(src.includes('tsx/esm'), false, 'Must not reference tsx/esm');
});
it('all imports are local ./ relative paths', async () => {
const { readFileSync } = await import('node:fs');
const { fileURLToPath } = await import('node:url');
const { dirname, join } = await import('node:path');
const dir = dirname(fileURLToPath(import.meta.url));
const src = readFileSync(join(dir, '..', 'scripts', 'seed-resilience-intervals.mjs'), 'utf8');
const imports = [...src.matchAll(/from\s+['"]([^'"]+)['"]/g)].map((m) => m[1]);
for (const imp of imports) {
assert.ok(imp.startsWith('./'), `Import "${imp}" must be a local ./ relative path`);
}
});
});