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.
1 line
6.7 KiB
JSON
1 line
6.7 KiB
JSON
{"components":{"schemas":{"DimensionFreshness":{"properties":{"lastObservedAtMs":{"description":"Unix milliseconds when the oldest constituent signal in this\n dimension was last observed (min fetchedAt across INDICATOR_REGISTRY\n entries for this dimension). 0 when no signal has ever been\n observed.","format":"int64","type":"string"},"staleness":{"description":"Worst staleness level across the dimension's constituent signals,\n classified by classifyStaleness against each signal's cadence.\n One of: \"fresh\", \"aging\", \"stale\". Empty string when no signals.","type":"string"}},"type":"object"},"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"},"pillars":{"items":{"$ref":"#/components/schemas/ResiliencePillar"},"type":"array"},"schemaVersion":{"description":"Phase 2 T2.1: \"1.0\" (default, preserves the current response shape)\n or \"2.0\" (adds pillars; keeps overall_score / baseline_score / etc.\n populated for one release cycle for backward compat). Controlled at\n response build time by the RESILIENCE_SCHEMA_V2_ENABLED env flag.","type":"string"},"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"},"freshness":{"$ref":"#/components/schemas/DimensionFreshness"},"id":{"type":"string"},"imputationClass":{"description":"Four-class imputation taxonomy (Phase 1 T1.7). Empty string when the\n dimension has any observed data. One of: \"stable-absence\", \"unmonitored\",\n \"source-failure\", \"not-applicable\". See docs/methodology/country-resilience-index.mdx.","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"},"ResiliencePillar":{"description":"Phase 2 T2.1 of the country-resilience reference-grade upgrade plan.\n Three-pillar response shape that regroups the existing 5 domains into\n long-run capacity, current shock pressure, and recovery capability.\n Shipped as a shaped-but-empty payload in T2.1 (score=0, coverage=0);\n real aggregation lands in T2.3 / PR 4 of the Phase 2 rebuild.","properties":{"coverage":{"description":"Coverage in [0, 1]. 0 when shipped empty (T2.1).","format":"double","type":"number"},"domains":{"items":{"$ref":"#/components/schemas/ResilienceDomain"},"type":"array"},"id":{"description":"\"structural-readiness\" | \"live-shock-exposure\" | \"recovery-capacity\".","type":"string"},"score":{"description":"Pillar score in [0, 100]. 0 when shipped empty (T2.1).","format":"double","type":"number"},"weight":{"description":"Pillar weight in the overall combine. Per the plan: 0.40 / 0.35 / 0.25.","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"},"rankStable":{"type":"boolean"}},"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"]}}}} |