mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-26 01:24:59 +02:00
ce797da3a42e4653bbbf551dbc24e10fae731ff7
11 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
502bd4472c |
docs(resilience): sync methodology/proto/widget to 6-domain + 3-pillar reality (#3264)
Brings every user-facing surface into alignment with the live resilience scorer. Zero behavior change: overall_score is still the 6-domain weighted aggregate, schemaVersion is still 2.0 default, and every existing test continues to pass. Surfaces touched: - proto + OpenAPI: rewrote the ResiliencePillar + schema_version descriptions. 2.0 is correctly documented as default; shaped-but-empty language removed. - Widget: added missing recovery: 'Recovery' label (was rendering literal lowercase recovery before), retitled footer data-version chip from Data to Seed date so it is clear the value reflects the static seed bundle not every live input, rewrote help tooltip for 6 domains and 3 pillars and called out the 0.25 recovery weight. - Methodology doc: domains-and-weights table now carries all 6 rows with actual code weights (0.17/0.15/0.11/0.19/0.13/0.25), Recovery section header weight corrected from 1.0 to 0.25, new Pillar-combined score activation (pending) section with the measured Spearman 0.9935, top-5 movers, and the activation checklist. - documentation.mdx + features.mdx: product blurbs updated from 5 domains and 13 dimensions to 6 domains and 19 dimensions grouped into 3 pillars. - Tests: recovery-label regression pin, Seed date label pin, clarified pillar-schema degenerate-input semantics. New scaffolding for defensibility: - docs/snapshots/resilience-ranking-2026-04-21.json frozen published tables artifact with methodology metadata and commit SHA. - docs/snapshots/resilience-pillar-sensitivity-2026-04-21.json live Redis capture (52-country sample) combining sensitivity stability with the current-vs-proposed Spearman comparison. - scripts/freeze-resilience-ranking.mjs refresh script. - scripts/compare-resilience-current-vs-proposed.mjs comparison script. - tests/resilience-ranking-snapshot.test.mts 13 assertions auto discovered from any resilience-ranking-YYYY-MM-DD.json in snapshots. Verification: npm run typecheck:all clean, 390/390 resilience tests pass. Follow-up: pillar-combined score activation. The sensitivity artifact shows rank-preservation Spearman 0.9935 and no ceiling effects, which clears the methodological bar. Blocker is messaging because every country drops ~13 points under the penalty, so activation PR ships with re-anchored release-gate bands, refreshed frozen ranking, and a v2.0 methodology note. |
||
|
|
39d5199ae0 |
feat(resilience): three-pillar schema + schemaVersion v2.0 feature flag (Phase 2 T2.1) (#2977)
* 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. |
||
|
|
75b3b0026b |
feat(resilience): dimension freshness propagation (T1.5 propagation pass) (#2961)
* 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. |
||
|
|
dca2e1ca3c |
feat(resilience): expose imputationClass on ResilienceDimension (T1.7 schema pass) (#2959)
* feat(resilience): expose imputationClass on ResilienceDimension (T1.7 schema pass) Ships the Phase 1 T1.7 schema pass of the country-resilience reference grade upgrade plan. PR #2944 shipped the classifier table foundation (ImputationClass type, ImputationEntry interface, IMPUTATION/IMPUTE tagged with four semantic classes) and explicitly deferred the schema propagation. This PR lands that propagation so downstream consumers can distinguish "country is stable" from "country is unmonitored" from "upstream is down" from "structurally not applicable" on a per-dimension basis. What this PR commits - Proto: new imputation_class string field on ResilienceDimension (empty string = dimension has any observed data; otherwise one of stable-absence, unmonitored, source-failure, not-applicable). - Generated TS types: regenerated service_server.ts and service_client.ts via make generate. - Scorer: ResilienceDimensionScore carries ImputationClass | null. WeightedMetric carries an optional imputationClass that imputation paths populate. weightedBlend aggregates the dominant class by weight when the dimension is fully imputed, returns null otherwise. - All IMPUTE.* early-return paths propagate the class from the table (IMPUTE.bisEer, IMPUTE.wtoData, IMPUTE.ipcFood, IMPUTE.unhcrDisplacement). - Response builder: _shared.ts buildDimensionList passes the class through to the ResilienceDimension proto field. - Tests: weightedBlend aggregation semantics (5 cases), dimension-level propagation from IMPUTE tables, serialized response includes the field. What is deliberately NOT in this PR - No widget icon rendering (T1.6 full grid, PR 3 of 5) - No source-failure seed-meta consultation (PR 4 of 5) - No freshness field (T1.5 propagation, PR 2 of 5) - No cache key bump: the new field is empty-string default, existing cached responses continue to deserialize cleanly Verified - make generate clean - npm run typecheck + typecheck:api clean - tests/resilience-dimension-scorers.test.mts all passing (existing + new) - tests/resilience-*.test.mts + test:data suite passing (4361 tests) - npm run lint exits 0 * fix(resilience): normalize cached score responses on read (#2959 P2) Greptile P2 finding on PR #2959: cachedFetchJson and getCachedResilienceScores return pre-change payloads verbatim, so a resilience:score:v7 entry written before this PR lands lacks the imputationClass field. Downstream consumers that read dim.imputationClass get undefined for up to 6 hours until the cache TTL expires. Fix: add normalizeResilienceScoreResponse helper that defaults missing optional fields in place and apply it at both read sites. Defaults imputationClass to empty string, matching the proto3 default for the new imputation_class field. - ensureResilienceScoreCached applies the normalizer after cachedFetchJson returns. - getCachedResilienceScores applies it after each successful JSON.parse on the pipeline result. - Two new test cases: stale payload without imputationClass gets defaulted, present values are preserved. - Not bumping the cache key: stale-read defaults are safe, the key bump would invalidate every cached score for a 6-hour cold-start cycle. The normalizer is extensible when PR #2961 adds freshness to the same payload. P3 finding (broken docs reference) verified invalid: the proto comment points to docs/methodology/country-resilience-index.mdx, which IS the current file. The .md predecessor was renamed in PR #2945 (T1.3 methodology doc promotion to CII parity). No change needed to the comment. * fix(resilience): bump score cache key v7 to v8, drop normalizer (#2959 P2) Second fixup for the Greptile P2 finding on #2959. The previous fixup ( |
||
|
|
ce30a48664 |
feat(resilience): add rankStable flag to ranking items (#2879)
* feat(resilience): add rankStable flag to ranking items Countries with score interval width <= 8 (p95-p05) are flagged as rankStable=true, indicating robust ranking under weight perturbation. Read from batch-computed intervals in Redis. * fix(resilience): guard inverted intervals + scope fetch to scored countries 1. isRankStable rejects negative width (malformed p05 > p95) 2. fetchIntervals scoped to cachedScores.keys() instead of all countries * fix(resilience): raw key read for intervals + bump ranking cache to v8 * fix(resilience): remove duplicate ScoreInterval interface after rebase ScoreInterval is now generated in service_server.ts (from PR #2877). Remove the local duplicate and re-export the generated type. |
||
|
|
0a1b74a9b2 |
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
|
||
|
|
75e9c22dd3 |
feat(resilience): populate dataVersion field from seed-meta timestamp (#2865)
* feat(resilience): populate dataVersion field from seed-meta timestamp Sets dataVersion to the ISO date of the most recent static bundle seed, making the data vintage visible to API consumers. * fix(resilience): bump score cache to v7 for dataVersion field addition |
||
|
|
f53c05599a |
feat(resilience): baseline vs stress scoring engine (#2821)
* feat(resilience): baseline vs stress scoring engine Splits the resilience index into structural capacity (baselineScore) and active disruption (stressScore) using the dimension type tags from RESILIENCE_DIMENSION_TYPES (baseline/stress/mixed). overallScore = baselineScore * (1 - stressFactor) where stressFactor is clamped to [0, 0.5]. Mixed dimensions contribute to both scores. Proto fields 10-12 added (baseline_score, stress_score, stress_factor). Widget updated to display baseline/stress breakdown. Cache keys bumped v4 -> v5 for atomic rollout. * fix(resilience): bump history key to v2 for baseline/stress formula change The overallScore formula changed from domain-weighted-sum to baselineScore * (1 - stressFactor). Old history entries are incomparable, causing fake change30d drops of -20 to -30 points. Versioned history key starts a clean series. |
||
|
|
2edcdeee06 |
refactor(resilience): remove Cronbach alpha, add imputationShare confidence (#2787)
* refactor(resilience): remove Cronbach alpha, add imputationShare confidence Remove cronbach_alpha from proto (field 5 reserved) and all response builders. Replace with imputationShare (field 9): the fraction of weighted score from imputed (not observed) data. lowConfidence now triggers on averageCoverage < 0.55 or imputationShare > 0.40, replacing the unstable Cronbach-based gate. weightedBlend() return type extended with observedWeight/imputedWeight for provenance tracking through the scoring pipeline. * fix(resilience): version cache key + fix IMF proxy imputation classification 1. Bump resilience score cache key to v2 to avoid serving stale cached responses missing imputationShare after deploy. 2. Add explicit `imputed` flag to WeightedMetric so proxy data (real IMF inflation with lower certaintyCoverage) is classified as observed, not imputed. Only synthetic absence-based scores count toward imputationShare. |
||
|
|
3e9556c37f |
feat(resilience): Phase 1 §3.2+§3.3 — full-country sanctions counts + grey-out ranking (#2763)
* feat(resilience): Phase 1 §3.2+§3.3 — full-country sanctions counts + grey-out ranking §3.2 — Switch sanctions from top-12 to full country counts - RESILIENCE_SANCTIONS_KEY: 'sanctions:pressure:v1' → 'sanctions:country-counts:v1' - New key is a plain ISO2→entryCount map covering ALL countries (no top-12 truncation) - Replaces compound pressure formula with normalizeSanctionCount() piecewise scale: 0=100, 1-10=90-75, 11-50=75-50, 51-200=50-25, 201+=25→0 - IMPUTE.ofacSanctions removed (country-counts covers all countries; no absent-country imputation needed for sanctions) §3.3 — Grey-out criteria for ranking - Proto: ResilienceRankingItem.overall_coverage (field 5) + GetResilienceRankingResponse.greyed_out - GREY_OUT_COVERAGE_THRESHOLD = 0.40: countries below this are excluded from ranking but still appear on choropleth in "insufficient data" style - buildRankingItem() now computes overallCoverage from domain/dimension data - getResilienceRanking() splits items into ranked (≥0.40) + greyedOut (<0.40) Tests updated for new sanctions format; overall score anchor updated (67.56). * fix(resilience): fix ranking cache guard for all-greyed-out + stale shape cases Two cache bugs: 1. Empty-items guard: `cached?.items?.length` fails when every country falls below GREY_OUT_COVERAGE_THRESHOLD (items=[], greyedOut=[…]). The cache was written correctly but never served, causing unnecessary rewarming on every request for sparse-data deployments. Fix: `cached != null && (items.length > 0 || greyedOut.length > 0)` 2. Stale-shape test: agent-written cache test stored a payload without `greyedOut` or `overallCoverage`, locking in pre-PR shape. Updated to the correct post-deploy shape so the test reflects actual cached content. Cache key was already bumped to resilience:ranking:v2 (forces fresh compute on first post-deploy request, avoiding old-shape responses in production). * fix(resilience): consume greyedOut on choropleth; version ranking cache key - Add 'insufficient_data' level to ResilienceChoroplethLevel and RESILIENCE_CHOROPLETH_COLORS - Extend buildResilienceChoroplethMap to accept optional greyedOut array - Thread greyedOut through DeckGLMap.setResilienceRanking, MapContainer.setResilienceRanking (with replay), and data-loader.ts - Add 'Insufficient data' tooltip guard for greyed-out countries in DeckGLMap - Bump RESILIENCE_RANKING_CACHE_KEY to resilience:ranking:v2 to invalidate stale schema-mismatched cache entries - Update api/health.js probe key to match * fix(resilience): include greyedOut in seed-meta count to avoid false health alert seed-meta:resilience:ranking was written with count=response.items.length, which excludes greyedOut countries. In an all-greyed-out deployment, count=0 causes api/health.js to report the ranking as EMPTY_DATA/critical even though the cached payload is valid (items:[], greyedOut:[…]). Fix: count = items.length + greyedOut.length — total scoured countries regardless of ranking eligibility. * test(resilience): pin all-greyed-out cache-hit regression Adds the missing test case: cached payload with items=[] and greyedOut=[…] must be served from cache without triggering score rewarming. Previously, `cached?.items?.length` was falsy for this shape, making the guard ineffective. The fix (items.length > 0 || greyedOut.length > 0) was correct but unpinned — this test locks it in. |
||
|
|
4b67012260 |
feat(resilience): add service proto and stub handlers (#2657)
* feat(resilience): add service proto and stub handlers Add the worldmonitor.resilience.v1 proto package, generated client/server artifacts, edge routing, and zero-state handler stubs so the domain is deployable before the seed and scoring layers land. Validation: - PATH="/Users/lucaspassos/go/bin:/Users/lucaspassos/.codex/tmp/arg0/codex-arg06nbVvG:/Users/lucaspassos/.antigravity/antigravity/bin:/Users/lucaspassos/.local/bin:/Users/lucaspassos/.codeium/windsurf/bin:/Users/lucaspassos/Library/Python/3.12/bin:/Library/Frameworks/Python.framework/Versions/3.12/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/opt/pkg/env/active/bin:/opt/pmk/env/global/bin:/Library/Apple/usr/bin:/opt/homebrew/bin:/Applications/Codex.app/Contents/Resources" make generate - PATH="/Users/lucaspassos/go/bin:/Users/lucaspassos/.codex/tmp/arg0/codex-arg06nbVvG:/Users/lucaspassos/.antigravity/antigravity/bin:/Users/lucaspassos/.local/bin:/Users/lucaspassos/.codeium/windsurf/bin:/Users/lucaspassos/Library/Python/3.12/bin:/Library/Frameworks/Python.framework/Versions/3.12/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/opt/pkg/env/active/bin:/opt/pmk/env/global/bin:/Library/Apple/usr/bin:/opt/homebrew/bin:/Applications/Codex.app/Contents/Resources" npx tsx --test tests/route-cache-tier.test.mjs tests/edge-functions.test.mjs - npm run typecheck (fails on upstream Dodo/Clerk baseline) - npm run typecheck:api (fails on upstream vitest baseline) - npm run test:data (fails on upstream dodopayments-checkout baseline via tests/runtime-config-panel-visibility.test.mjs) * fix(resilience): add countryCode validation to get-resilience-score Throw ValidationError when countryCode is missing instead of silently returning a zero-state response with an empty string country code. * fix(resilience): validate countryCode format and mark required in spec - Trim whitespace and reject non-ISO-3166-1 alpha-2 codes to prevent cache pollution from malformed aliases (e.g. 'USA', ' us ', 'foobar') - Add required: true to proto QueryConfig so generated OpenAPI spec matches runtime validation behavior - Regenerated OpenAPI artifacts via make generate --------- Co-authored-by: Elie Habib <elie.habib@gmail.com> |