Files
worldmonitor/.husky
Elie Habib 12ea629a63 feat(supply-chain): Sprint C — scenario engine (templates, job API, Railway worker, map activation) (#2890)
* feat(supply-chain): Sprint C — scenario engine templates, job API, Railway worker, map activation

Adds the async scenario engine for supply chain disruption modelling:

- src/config/scenario-templates.ts: 6 pre-built ScenarioTemplate definitions
  (Taiwan Strait closure, Suez+BaB simultaneous, Panama drought, Hormuz blockade,
  Russia Baltic grain suspension, US electronics tariff shock) with costShockMultiplier
  and optional HS2 sector scoping. Exports ScenarioVisualState + ScenarioResult
  types (no UI imports, avoids MapContainer <-> DeckGLMap circular dep).

- api/scenario/v1/run.ts: PRO-gated edge function — validates scenarioId against
  template registry and iso2 format, enqueues job to Redis scenario-queue:pending
  via RPUSH. Returns {jobId, status:'pending'} HTTP 202.

- api/scenario/v1/status.ts: Edge function — validates jobId via regex to prevent
  Redis key injection, reads scenario-result:{jobId}. Returns {status:'pending'}
  when unprocessed, or full worker result when done.

- scripts/scenario-worker.mjs: Always-on Railway worker using BLMOVE LEFT RIGHT for
  atomic FIFO dequeue+claim. Idempotency check before compute. Writes result with
  24h TTL; writes {status:'failed'} on error; always cleans processing list in finally.

- DeckGLMap.ts: scenarioState field + setScenarioState(). createTradeRoutesLayer()
  overrides arc color to orange for segments whose route waypoints intersect scenario
  disruptedChokepointIds. Null state restores normal colors.

- MapContainer.ts: activateScenario(id, result) and deactivateScenario() broadcast
  ScenarioVisualState to DeckGLMap. Globe/SVG deferred to Sprint D (best-effort).

🤖 Generated with Claude Sonnet 4.6 via Claude Code (https://claude.com/claude-code) + Compound Engineering v2.49.0

Co-Authored-By: Claude Sonnet 4.6 (200K context) <noreply@anthropic.com>

* fix(supply-chain): move scenario-templates to server/ to satisfy arch boundary

api/ edge functions may not import from src/ app code. Move the authoritative
scenario-templates.ts to server/worldmonitor/supply-chain/v1/ and replace
src/config/scenario-templates.ts with a type-only re-export for src/ consumers.

* fix(supply-chain): guard scenario-worker runWorker() behind isMain check

Without the isMain guard, the pre-push hook picks up scenario-worker.mjs
as a seed test candidate (non-matching lines pass through sed unchanged)
and starts the long-running worker process, causing push failures.

* fix(pre-push): filter non-matching lines from seed test selector

The sed transform passes non-matching lines (e.g. scenario-worker.mjs)
through unchanged. Adding grep "^tests/" ensures only successfully
transformed test paths are passed to the test runner.

* fix(supply-chain): address PR #2890 review findings — worker data shapes + status PRO gate

Three bugs found in PR #2890 code review:

1. [High] scenario-worker.mjs read wrong cache shape for exposure data.
   supply-chain:exposure:{iso2}:{hs2}:v1 caches GetCountryChokepointIndexResponse
   ({ iso2, hs2, exposures: [{chokepointId, exposureScore}], ... }), not a
   chokepointId-keyed object. Worker now iterates data.exposures[], filters by
   template.affectedChokepointIds, and ranks by exposureScore (importValue does
   not exist on ChokepointExposureEntry). adjustedImpact = exposureScore x
   (disruptionPct/100) x costShockMultiplier.

2. [Medium] api/scenario/v1/status.ts was not PRO-gated, allowing anyone with
   a valid jobId to retrieve full premium scenario results. Added isCallerPremium()
   check; returns HTTP 403 for non-PRO callers, matching run.ts behavior.

3. [Low] Worker parsed chokepoint status cache as Array but actual shape is
   { chokepoints: [], fetchedAt, upstreamUnavailable }. Fixed to access
   cpData.chokepoints array.

* fix(scenario): per-country impactPct + O(1) route lookup in arc layer

- impactPct now reflects each country's relative share of the worst-hit
  country (0-100) instead of the flat template.disruptionPct for all
- Pre-build routeId→waypoints Map in createTradeRoutesLayer() so
  getColor() is O(1) per segment instead of O(n) per frame

* fix(scenario): rate limit, pipeline GETs, error sanitization, processing state, orphan drain

- Add per-user rate limit (10 jobs/min) + queue depth cap to run.ts
- Replace 594 sequential Redis GETs with single Upstash pipeline call
- Sanitize worker err.message to 'computation_error' in failed results
- Remove dead validateApiKey() calls (isCallerPremium covers this)
- Write processing state before computeScenario() starts
- Add SIGTERM handler + startup orphan drain to worker loop
- Validate dequeued job payload fields before use as Redis key fragments
- Fix maxImpact divide-by-zero with Math.max(..., 1)
- Hoist routeWaypoints Map to module level in DeckGLMap
- Add GET /api/scenario/v1/templates discovery endpoint
- Fix template sync comment to reference correct authoritative file

* docs(plan): mark Sprint C complete, record deferrals to Sprint D

- Sprint status table added: Sprints 0-2 merged, C ready to merge (#2890), A/B/D not started
- Sprint C checklist: 4 ACs checked off, panel UI + tariff-shock visual deferred
- Sprint D section updated to carry over Sprint C visual deferrals
- PR #2890 added to Related PRs

---------

Co-authored-by: Claude Sonnet 4.6 (200K context) <noreply@anthropic.com>
2026-04-10 14:44:14 +04:00
..