Commit Graph

5 Commits

Author SHA1 Message Date
Elie Habib
6e401ad02f feat(supply-chain): Global Shipping Intelligence — Sprint 0 + Sprint 1 (#2870)
* feat(supply-chain): Sprint 0 — chokepoint registry, HS2 sectors, war_risk_tier

- src/config/chokepoint-registry.ts: single source of truth for all 13
  canonical chokepoints with displayName, relayName, portwatchName,
  corridorRiskName, baselineId, shockModelSupported, routeIds, lat/lon
- src/config/hs2-sectors.ts: static dictionary for all 99 HS2 chapters
  with category, shockModelSupported (true only for HS27), cargoType
- server/worldmonitor/supply-chain/v1/_chokepoint-ids.ts: migrated to
  derive CANONICAL_CHOKEPOINTS from chokepoint-registry; no data duplication
- src/config/geo.ts + src/types/index.ts: added chokepointId field to
  StrategicWaterway interface and all 13 STRATEGIC_WATERWAYS entries
- src/components/MapPopup.ts: switched chokepoint matching from fragile
  name.toLowerCase() to direct chokepointId === id comparison
- server/worldmonitor/intelligence/v1/_shock-compute.ts: migrated from old
  IDs (hormuz/malacca/babelm) to canonical IDs (hormuz_strait/malacca_strait/
  bab_el_mandeb); same for CHOKEPOINT_LNG_EXPOSURE
- proto/worldmonitor/supply_chain/v1/supply_chain_data.proto: added
  WarRiskTier enum + war_risk_tier field (field 16) on ChokepointInfo
- get-chokepoint-status.ts: populates warRiskTier from ChokepointConfig.threatLevel
  via new threatLevelToWarRiskTier() helper (FREE field, no PRO gate)

* feat(supply-chain): Sprint 1 — country chokepoint exposure index + sector ring

S1.1: scripts/shared/country-port-clusters.json
  ~130 country → {nearestRouteIds, coastSide} mappings derived from trade route
  waypoints; covers all 6 seeded Comtrade reporters plus major trading nations.

S1.2: scripts/seed-hs2-chokepoint-exposure.mjs
  Daily cron seeder. Pure computation — reads country-port-clusters.json,
  scores each country against CHOKEPOINT_REGISTRY route overlap, writes
  supply-chain:exposure:{iso2}:{hs2}:v1 keys + seed-meta (24h TTL).

S1.3: RPC get-country-chokepoint-index (PRO-gated, request-varying)
  - proto: GetCountryChokepointIndexRequest/Response + ChokepointExposureEntry
  - handler: isCallerPremium gate; cachedFetchJson 24h; on-demand for any iso2
  - cache-keys.ts: CHOKEPOINT_EXPOSURE_KEY(iso2, hs2) constant
  - health.js: chokepointExposure SEED_META entry (48h threshold)
  - gateway.ts: slow-browser cache tier
  - service client: fetchCountryChokepointIndex() exported

S1.4: Chokepoint popup HS2 sector ring chart (PRO-gated)
  Static trade-sector breakdown (IEA/UNCTAD estimates) per 9 major chokepoints.
  SVG donut ring + legend shown for PRO users; blurred lockout + gate-hit
  analytics for free users. Wired into renderWaterwayPopup().

🤖 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(tests): update energy-shock-v2 tests to use canonical chokepoint IDs

CHOKEPOINT_EXPOSURE and CHOKEPOINT_LNG_EXPOSURE keys were migrated from
short IDs (hormuz, malacca, babelm) to canonical registry IDs
(hormuz_strait, malacca_strait, bab_el_mandeb) in Sprint 0.
Test fixtures were not updated at the time; fix them now.

* fix(tests): update energy-shock-seed chokepoint ID to canonical form

VALID_CHOKEPOINTS changed to canonical IDs in Sprint 0; the seed test
that checks valid IDs was not updated alongside it.

* fix(cache-keys): reword JSDoc comment to avoid confusing bootstrap test regex

The comment "NOT in BOOTSTRAP_CACHE_KEYS" caused the bootstrap.test.mjs
regex to match the comment rather than the actual export declaration,
resulting in 0 entries found. Rephrase to "excluded from bootstrap".

* fix(supply-chain): address P1 review findings for chokepoint exposure index

- Add get-country-chokepoint-index to PREMIUM_RPC_PATHS (CDN bypass)
- Validate iso2/hs2 params before Redis key construction (cache injection)
- Fix seeder TTL to 172800s (2× interval) and extend TTL on skipped lock
- Fix CHOKEPOINT_EXPOSURE_SEED_META_KEY to match seeder write key
- Render placeholder sectors behind blur gate (DOM data leakage)
- Document get-country-chokepoint-index in widget agent system prompts

* fix(lint): resolve Biome CI failures

- Add biome.json overrides to silence noVar in HTML inline scripts,
  disable linting for public/ vendor/build artifacts and pro-test/
- Remove duplicate NG and MW keys from country-port-clusters.json
- Use import attributes (with) instead of deprecated assert syntax

* fix(build): drop JSON import attribute — esbuild rejects `with` syntax

---------

Co-authored-by: Claude Sonnet 4.6 (200K context) <noreply@anthropic.com>
2026-04-09 17:06:03 +04:00
Elie Habib
d2fa60ae0b feat(energy): per-product refinery yield model (V5-5) (#2828)
* feat(energy): per-product refinery yield coefficients replacing 0.8 heuristic (V5-5)

* fix(energy): yield scales crudeLossKbd not demand; worst deficit across all products

* fix(energy): update seed test to match worst-product assessment text
2026-04-08 18:28:05 +04:00
Elie Habib
c826003e5f feat(energy): LNG and gas shock path (V5-4) (#2822)
* feat(energy): LNG gas shock path with GIE storage buffer (V5-4)

* fix(energy): gas path zero-LNG handling, oil gate bypass, live flow scaling, coverage semantics

* fix(energy): gas-only coverage respects degraded; zero oil fields in gas mode
2026-04-08 17:10:11 +04:00
Elie Habib
f91382518a feat(energy): Ember spine integration + grid-tightness limitation (V5-6b) (#2820)
* feat(energy): Ember generation mix in spine + grid-tightness limitation (V5-6b)

* fix(energy): compose flowRatio with baseExposure; core-source guard; skip proxy low-dependence dismissal
2026-04-08 16:52:12 +04:00
Elie Habib
80b24d8686 feat(energy): shock model v2 — live flow ratios, coverage, limitations (V5-3) (#2810)
* feat(energy): shock model v2 — live flow ratios, coverage, limitations (V5-3)

Replace static CHOKEPOINT_EXPOSURE multipliers with live PortWatch flow
ratios from energy:chokepoint-flows:v1. Add proto fields 11-19: per-source
coverage flags (jodi_oil_coverage, comtrade_coverage, iea_stocks_coverage,
portwatch_coverage), coverage_level, limitations[], degraded,
chokepoint_confidence, live_flow_ratio. Expand ISO2_TO_COMTRADE from 6 to
150+ countries via _comtrade-reporters.ts. Partial-coverage path proxies
Gulf share at 40%. Unsupported countries return structured metadata instead
of opaque string. data_available (field 9) preserved for backward compat.

* fix(energy): correct chokepoint-flows key shape in shock handler (V5-3)

energy:chokepoint-flows:v1 is a flat object keyed by canonicalId
(hormuz_strait, bab_el_mandeb, suez, malacca_strait), not an object
with a chokepoints[] array. The wrong shape caused degraded=true and
liveFlowRatio=null for every request, silently falling back to static
CHOKEPOINT_EXPOSURE multipliers even when live PortWatch data was
available.

Fixes: ChokepointEntry interface, typecast, cpEntry lookup, degraded check.

* fix(energy): 4 review fixes for shock v2 handler (V5-3)

1. Cache key v1→v2: response shape changed (fields 11-19 added), old
   v1 cache entries would be served without new coverage fields.
2. IEA unknown vs zero: when ieaStocksCoverage=false, assessment now
   shows "IEA cover: unknown" instead of "0 days" to avoid conflating
   missing data with real zero stock.
3. liveFlowRatio 0.0 truthiness: changed `if (liveFlowRatio)` to
   `if (liveFlowRatio != null)` — a blocked chokepoint (ratio=0.0)
   is now shown, not hidden as "no live data".
4. Badge cleared on request start: coverageBadge is now reset before
   each shock compute request so a failed request doesn't leave the
   previous result's badge visible.

* fix(energy): 3 remaining review fixes for shock v2 (V5-3)

1. Flow column: gate on portwatchCoverage (bool) not liveFlowRatio!=null —
   proto double defaults to 0, so null-check was always true and degraded
   responses showed a misleading "Flow: 0%" column.
2. Degraded cache TTL: 5min when degraded=true, 1h when live data present —
   limits how long stale degraded state persists after PortWatch recovers.
3. IEA anomaly cover days: zero daysOfCover when ieaStocksCoverage=false
   so anomalous IEA data no longer contributes to effectiveCoverDays;
   panel IEA cover row also gated on ieaStocksCoverage.

* fix(energy): netExporter from anomalous IEA + zero-days panel display (V5-3)

1. netExporter: gated on ieaStocksCoverage — anomalous IEA rows no
   longer drive the net-exporter assessment branch when coverage=false.
2. Panel IEA cover: removed effectiveCoverDays>0 guard so a real zero
   (reserves exhausted under scenario) renders as "0 days" instead of
   being silently hidden as if there were no IEA data.

* fix(energy): handle net-exporter sentinel (-1) in IEA cover panel row

* fix(energy): NaN guard on flowRatio; optional live_flow_ratio; coverageLevel includes IEA+degraded

* fix(energy): narrow Gulf-share proxy to Comtrade-only; NaN guard computeGulfShare; EMPTY liveFlowRatio undefined

* fix(energy): tighten ieaStocksCoverage null guard; cache key varies by degraded state

* fix(energy): harden IEA/PortWatch input validation; reduce shock cache TTL

* fix(energy): add null narrowing for daysOfCover to satisfy strict TS
2026-04-08 12:26:21 +04:00