mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-26 01:24:59 +02:00
* feat(resilience): three-pillar schema + schemaVersion v2.0 feature flag (Phase 2 T2.1) Ships the Phase 2 T2.1 schema slice of the country-resilience reference grade upgrade plan. Adds the three-pillar response shape (StructuralReadiness, LiveShockExposure, RecoveryCapacity) as a new `pillars` field on GetResilienceScoreResponse alongside a `schemaVersion` string field, both gated behind the RESILIENCE_SCHEMA_V2_ENABLED env flag (default false). This PR is schema + plumbing only. Pillars ship with score=0, coverage=0; real aggregation lands in PR 4 (T2.3). No behavior change at the v1 default, which preserves widget / map / Country Brief compatibility for one release cycle per the plan. What this PR commits - Proto: new ResiliencePillar message (id, score, weight, coverage, domains). New `pillars` repeated field + `schema_version` string field on GetResilienceScoreResponse. No renumbering or mutation of existing fields. - Generated TS: regenerated service_server.ts, service_client.ts, and OpenAPI JSON/YAML via `make generate`. - New module server/worldmonitor/resilience/v1/_pillar-membership.ts: declarative PILLAR_DOMAINS map + PILLAR_WEIGHTS map + ordered iteration list. Single source of truth for the pillar structure that PR 4 will import. Note: pillar membership uses the runtime ResilienceDomainId values (kebab-case domain ids that already ship in v1), not the long-form pillar names from the plan example. - structural-readiness (0.40): economic, infrastructure, social-governance - live-shock-exposure (0.35): energy, health-food - recovery-capacity (0.25): empty until PR 3 adds the new dimensions - Response builder: new buildPillarList helper emits shaped-but-empty pillars when the v2 flag is on, empty array when off. Response literal fallback paths in _shared.ts and the LOCKED_PREVIEW fixture in resilience-widget-utils.ts updated to include pillars: [] and schemaVersion: '1.0' to satisfy the generated TS types. - Tests: 13 new pillar-schema unit cases (membership invariants, weight sum=1.0, disjoint sets, empty recovery pillar, buildPillarList flag-off / flag-on / shuffled-order / partial-domain-set) + 3 response-shape cases on the release-gate test pinning the v1 default shape and the new field presence on the wire. What is deliberately NOT in this PR - No aggregation logic: score/coverage on pillars stay 0 until PR 4. - No cache key bump: schema is additive with proto3 defaults. - No changes to overallScore/baselineScore/stressScore (parallel for one release cycle). - No new seeders or dimensions (PR 3 / T2.2b). - No tiering registry changes (PR 2 / T2.2a). - No widget rendering (Phase 3 T3.6). Verified - make generate clean, new ResiliencePillar interface in regenerated client + server + OpenAPI artifacts - typecheck + typecheck:api clean - tests/resilience-pillar-schema.test.mts: 13/13 passing - tests/resilience-release-gate.test.mts: 14/14 passing (3 new T2.1 cases + 11 prior) - full resilience suite: 283/283 passing - npm run test:data: 4539/4539 passing - npm run lint: exit 0 * fix(resilience): cache-flag decoupling + freshness error-status guard (#2977 P1+P2) Two Greptile review findings addressed: P1: RESILIENCE_SCHEMA_V2_ENABLED changed the cached response shape but the cache key did not encode the flag state. Flipping the flag on a warm cache served stale v1.0 payloads until the 6h TTL expired. Fix: always compute and cache the v2 superset (with pillars and schemaVersion='2.0'). Apply the flag as a response-time gate: when off, strip pillars to [] and downgrade schemaVersion to '1.0' before returning. This decouples the cache from the flag and makes flag flips take effect immediately without waiting for TTL expiry. P2: readFreshnessMap in _dimension-freshness.ts trusted fetchedAt without checking status. The resilience-static seeder writes fetchedAt: Date.now() on BOTH success and error paths (status: 'ok' vs 'error'), so a failed seed run that preserved old data via extendExistingTtl made the freshness badges show 'fresh' for what is actually stale data. Fix: skip seed-meta entries where status !== 'ok'. When the meta is skipped, the dimension has no freshness data and classifies as stale, matching api/health.js behavior. Added a test case that verifies error-status entries are excluded from the freshness map.
272 lines
11 KiB
YAML
272 lines
11 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'
|
|
pillars:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/ResiliencePillar'
|
|
schemaVersion:
|
|
type: string
|
|
description: |-
|
|
Phase 2 T2.1: "1.0" (default, preserves the current response shape)
|
|
or "2.0" (adds pillars; keeps overall_score / baseline_score / etc.
|
|
populated for one release cycle for backward compat). Controlled at
|
|
response build time by the RESILIENCE_SCHEMA_V2_ENABLED env flag.
|
|
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
|
|
ResiliencePillar:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
description: '"structural-readiness" | "live-shock-exposure" | "recovery-capacity".'
|
|
score:
|
|
type: number
|
|
format: double
|
|
description: Pillar score in [0, 100]. 0 when shipped empty (T2.1).
|
|
weight:
|
|
type: number
|
|
format: double
|
|
description: 'Pillar weight in the overall combine. Per the plan: 0.40 / 0.35 / 0.25.'
|
|
coverage:
|
|
type: number
|
|
format: double
|
|
description: Coverage in [0, 1]. 0 when shipped empty (T2.1).
|
|
domains:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/ResilienceDomain'
|
|
description: |-
|
|
Phase 2 T2.1 of the country-resilience reference-grade upgrade plan.
|
|
Three-pillar response shape that regroups the existing 5 domains into
|
|
long-run capacity, current shock pressure, and recovery capability.
|
|
Shipped as a shaped-but-empty payload in T2.1 (score=0, coverage=0);
|
|
real aggregation lands in T2.3 / PR 4 of the Phase 2 rebuild.
|
|
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
|