mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-05-01 20:07:04 +02:00
* feat(resilience): dimension freshness propagation (T1.5 propagation pass) Ships the Phase 1 T1.5 propagation pass of the country-resilience reference-grade upgrade plan. PR #2947 shipped the staleness classifier foundation (classifyStaleness, cadence taxonomy, three staleness levels) and explicitly deferred the dimension-level propagation. This PR consumes the classifier and surfaces per dimension freshness on the ResilienceDimension response. What this PR commits - Proto: new DimensionFreshness message + `freshness` field on ResilienceDimension (last_observed_at_ms, staleness string). - New module server/worldmonitor/resilience/v1/_dimension-freshness.ts that reads seed-meta values for every sourceKey in INDICATOR_REGISTRY and aggregates the worst staleness + oldest fetchedAt across the constituent indicators of each dimension. - scoreAllDimensions decorates each dimension score with its freshness result before returning. The 13 dimension scorer function bodies are untouched: aggregation is a decoration pass at the caller level so this PR stays mechanical. - Response builder: _shared.ts buildDimensionList propagates the freshness field to the proto output. - Tests: 10 classifyDimensionFreshness + readFreshnessMap cases in a new test file + response-shape case on the release-gate test. Aggregation rules - last_observed_at_ms: MIN fetchedAt across the dimension's indicators (oldest signal = most conservative bound). 0 when no signal has ever been observed. - staleness: MAX staleness level across the dimension's indicators (stale > aging > fresh). Empty string when the dimension has no indicators in the registry (defensive path). What is deliberately NOT in this PR - No changes to the 13 individual dimension scorer function bodies. Per-signal freshness inside scorers is a future enhancement. - No widget rendering of the freshness badge (T1.6 full grid, PR 3). - No cache key bump: additive int64/string fields with zero defaults. Verified - make generate clean, new interface in regenerated types - typecheck + typecheck:api clean - tests/resilience-dimension-freshness.test.mts all new cases pass - tests/resilience-*.test.mts full suite pass - test:data clean - lint exits 0 on touched files * fix(resilience): resolve templated sourceKeys to real seed-meta (#2961 P1) Greptile P1 finding on PR #2961: readFreshnessMap() assumed every INDICATOR_REGISTRY sourceKey could be fetched as seed-meta:<sourceKey>, but most entries use placeholder templates like resilience:static:{ISO2}, energy:mix:v1:{ISO2}, and displacement:summary:v1:{year}. Those produce literal lookups like seed-meta:resilience:static:{ISO2} which don't exist in Redis, so the freshness map missed every templated entry and classifyDimensionFreshness marked the affected dimensions stale even with healthy seeds. Most Phase 1 T1.5 freshness badges were broken on arrival. Fix: two-layer resolution in _dimension-freshness.ts. Layer 1 stripTemplateTokens: drop :{placeholder} and :* segments. 'resilience:static:{ISO2}' -> 'resilience:static' 'resilience:static:*' -> 'resilience:static' 'energy:mix:v1:{ISO2}' -> 'energy:mix:v1' 'displacement:summary:v1:{year}' -> 'displacement:summary:v1' Layer 2 stripTrailingVersion: strip trailing :v\d+, mirroring writeExtraKeyWithMeta + runSeed() in scripts/_seed-utils.mjs which never persist the trailing version in seed-meta keys. Handles cyber:threats:v2, infra:outages:v1, unrest:events:v1, conflict:ucdp-events:v1, sanctions:country-counts:v1, and the displacement v1 case above. Layer 3 SOURCE_KEY_META_OVERRIDES: explicit table for drift cases where the two strips still do not match the real seed-meta key. Verified against api/seed-health.js, api/health.js, and scripts/seed-*. Drift cases covered: economic:imf:macro -> economic:imf-macro economic:bis:eer -> economic:bis economic:energy:v1:all -> economic:energy-prices energy:mix -> economic:owid-energy-mix energy:gas-storage -> energy:gas-storage-countries news:threat:summary -> news:threat-summary intelligence:social:reddit -> intelligence:social-reddit readFreshnessMap now deduplicates reads by resolved meta key (so the 15+ resilience:static indicators share one Redis read) and projects per-meta-key results back onto per-sourceKey map entries so classifyDimensionFreshness can keep its existing interface. Regression coverage: - stripTemplateTokens cases for {ISO2}, {year}, and *. - stripTrailingVersion cases for :v1 / :v2 suffixes. - Embedded :v1 carve-out (trade:restrictions:v1:tariff-overview:50 stays unchanged because :v1 is not trailing). - Override cases for the seven drift entries. - Integration test that proves every resilience:static:* / {ISO2} registry entry resolves to the same seed-meta and is marked fresh when that one key has a recent fetchedAt. - healthPublicService end-to-end test: classifies fresh when seed-meta:resilience:static is recent (was stale before the fix). - Registry-coverage assertion: every INDICATOR_REGISTRY sourceKey must resolve to a seed-meta key that either lives in api/seed-health.js, api/health.js, or the test's KNOWN_SEEDS_NOT_IN_HEALTH allowlist (which covers the four seeds written by writeExtraKeyWithMeta / runSeed that no health monitor tracks yet: trade:restrictions, trade:barriers, sanctions:country-counts, economic:energy-prices). Fails loudly if a future registry entry introduces an unknown sourceKey. Note on P1 #2 (scoreCurrencyExternal absence-branch delete): that is PR #2964's scope (T1.7 source-failure wiring), not #2961 (T1.5 propagation pass). #2961 never claimed to delete the fallback branch; no test in this branch expects the new IMPUTE.bisEer fallback. The reviewer conflated the two stacked PRs. #2964 owns the delete.
233 lines
8.6 KiB
YAML
233 lines
8.6 KiB
YAML
openapi: 3.1.0
|
|
info:
|
|
title: ResilienceService API
|
|
version: 1.0.0
|
|
paths:
|
|
/api/resilience/v1/get-resilience-score:
|
|
get:
|
|
tags:
|
|
- ResilienceService
|
|
summary: GetResilienceScore
|
|
operationId: GetResilienceScore
|
|
parameters:
|
|
- name: countryCode
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: Successful response
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/GetResilienceScoreResponse'
|
|
"400":
|
|
description: Validation error
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ValidationError'
|
|
default:
|
|
description: Error response
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
/api/resilience/v1/get-resilience-ranking:
|
|
get:
|
|
tags:
|
|
- ResilienceService
|
|
summary: GetResilienceRanking
|
|
operationId: GetResilienceRanking
|
|
responses:
|
|
"200":
|
|
description: Successful response
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/GetResilienceRankingResponse'
|
|
"400":
|
|
description: Validation error
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ValidationError'
|
|
default:
|
|
description: Error response
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Error'
|
|
components:
|
|
schemas:
|
|
Error:
|
|
type: object
|
|
properties:
|
|
message:
|
|
type: string
|
|
description: Error message (e.g., 'user not found', 'database connection failed')
|
|
description: Error is returned when a handler encounters an error. It contains a simple error message that the developer can customize.
|
|
FieldViolation:
|
|
type: object
|
|
properties:
|
|
field:
|
|
type: string
|
|
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')
|
|
description:
|
|
type: string
|
|
description: Human-readable description of the validation violation (e.g., 'must be a valid email address', 'required field missing')
|
|
required:
|
|
- field
|
|
- description
|
|
description: FieldViolation describes a single validation error for a specific field.
|
|
ValidationError:
|
|
type: object
|
|
properties:
|
|
violations:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/FieldViolation'
|
|
description: List of validation violations
|
|
required:
|
|
- violations
|
|
description: ValidationError is returned when request validation fails. It contains a list of field violations describing what went wrong.
|
|
GetResilienceScoreRequest:
|
|
type: object
|
|
properties:
|
|
countryCode:
|
|
type: string
|
|
GetResilienceScoreResponse:
|
|
type: object
|
|
properties:
|
|
countryCode:
|
|
type: string
|
|
overallScore:
|
|
type: number
|
|
format: double
|
|
level:
|
|
type: string
|
|
domains:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/ResilienceDomain'
|
|
trend:
|
|
type: string
|
|
change30d:
|
|
type: number
|
|
format: double
|
|
lowConfidence:
|
|
type: boolean
|
|
imputationShare:
|
|
type: number
|
|
format: double
|
|
baselineScore:
|
|
type: number
|
|
format: double
|
|
stressScore:
|
|
type: number
|
|
format: double
|
|
stressFactor:
|
|
type: number
|
|
format: double
|
|
dataVersion:
|
|
type: string
|
|
scoreInterval:
|
|
$ref: '#/components/schemas/ScoreInterval'
|
|
ResilienceDomain:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
score:
|
|
type: number
|
|
format: double
|
|
weight:
|
|
type: number
|
|
format: double
|
|
dimensions:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/ResilienceDimension'
|
|
ResilienceDimension:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
score:
|
|
type: number
|
|
format: double
|
|
coverage:
|
|
type: number
|
|
format: double
|
|
observedWeight:
|
|
type: number
|
|
format: double
|
|
imputedWeight:
|
|
type: number
|
|
format: double
|
|
imputationClass:
|
|
type: string
|
|
description: |-
|
|
Four-class imputation taxonomy (Phase 1 T1.7). Empty string when the
|
|
dimension has any observed data. One of: "stable-absence", "unmonitored",
|
|
"source-failure", "not-applicable". See docs/methodology/country-resilience-index.mdx.
|
|
freshness:
|
|
$ref: '#/components/schemas/DimensionFreshness'
|
|
DimensionFreshness:
|
|
type: object
|
|
properties:
|
|
lastObservedAtMs:
|
|
type: string
|
|
format: int64
|
|
description: |-
|
|
Unix milliseconds when the oldest constituent signal in this
|
|
dimension was last observed (min fetchedAt across INDICATOR_REGISTRY
|
|
entries for this dimension). 0 when no signal has ever been
|
|
observed.
|
|
staleness:
|
|
type: string
|
|
description: |-
|
|
Worst staleness level across the dimension's constituent signals,
|
|
classified by classifyStaleness against each signal's cadence.
|
|
One of: "fresh", "aging", "stale". Empty string when no signals.
|
|
ScoreInterval:
|
|
type: object
|
|
properties:
|
|
p05:
|
|
type: number
|
|
format: double
|
|
p95:
|
|
type: number
|
|
format: double
|
|
GetResilienceRankingRequest:
|
|
type: object
|
|
GetResilienceRankingResponse:
|
|
type: object
|
|
properties:
|
|
items:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/ResilienceRankingItem'
|
|
greyedOut:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/ResilienceRankingItem'
|
|
ResilienceRankingItem:
|
|
type: object
|
|
properties:
|
|
countryCode:
|
|
type: string
|
|
overallScore:
|
|
type: number
|
|
format: double
|
|
level:
|
|
type: string
|
|
lowConfidence:
|
|
type: boolean
|
|
overallCoverage:
|
|
type: number
|
|
format: double
|
|
rankStable:
|
|
type: boolean
|