feat(energy): seed EIA chokepoint baseline volumes (#2735)

* feat(energy): seed EIA chokepoint baseline volumes

- Add scripts/seed-chokepoint-baselines.mjs with 7 hardcoded EIA 2023 chokepoints (Hormuz through Panama), 400-day TTL, no network calls
- Add tests/chokepoint-baselines-seed.test.mjs with 14 test cases covering payload shape, key constants, TTL, and validateFn
- Register seed-chokepoint-baselines in railway-set-watch-paths.mjs with annual cron (0 0 1 1 *)

* fix(energy): 3 review fixes for chokepoint-baselines PR

P1 — IEA seed: move per-country Redis writes from fetch phase to
afterPublish pipeline. fetchIeaOilStocks now returns pure data;
publishTransform builds the canonical index; writeCountryKeys sends all
32 country keys atomically via pipeline in the publish phase. A mid-run
Redis failure can no longer leave a partially-updated snapshot with a
stale index.

P2 — Wire chokepointBaselines into bootstrap: add to
BOOTSTRAP_CACHE_KEYS + SLOW_KEYS in api/bootstrap.js and
server/_shared/cache-keys.ts + BOOTSTRAP_TIERS.

P3 — Wire IEA seed operationally: add seed-iea-oil-stocks service to
railway-set-watch-paths.mjs (monthly cron 0 6 20 * *) and
ieaOilStocks health entry (40-day maxStaleMin) to api/health.js.

* fix(test): add chokepointBaselines to PENDING_CONSUMERS

Frontend consumer not yet implemented; consistent with chokepointTransits,
correlationCards, euGasStorage which are also wired to bootstrap ahead
of their UI panels.

* fix(energy): register country keys in extraKeys for TTL preservation

afterPublish runs in the publish phase but is NOT included in runSeed's
failure-path TTL extension. Replace afterPublish+writeCountryKeys with
COUNTRY_EXTRA_KEYS (one entry per COUNTRY_MAP iso2) declared as extraKeys:

- On fetch failure or validation skip: runSeed extends TTL for all 32
  country keys alongside the canonical index
- On successful publish: writeExtraKey writes each country key with a
  per-iso2 transform; no dangling index entries after failed refreshes

Also removes now-unused getRedisCredentials import.

* fix(energy): 3 follow-up review fixes

High — seed-meta TTL: writeFreshnessMetadata now accepts a ttlSeconds param
and uses max(7d, ttlSeconds). runSeed passes its data TTL so monthly/annual
seeds (IEA: 40d, chokepoint: 400d) no longer lose their seed-meta key on
day 8 before health maxStaleMin is reached.

Medium — Turkey name: IEA API returns "Turkiye" (no umlaut) while COUNTRY_MAP
keys "Türkiye". parseRecord now normalizes the alias before lookup; TR is no
longer silently dropped. Test added to cover the normalized form.

Medium — Bootstrap revert: remove chokepointBaselines from BOOTSTRAP_CACHE_KEYS,
SLOW_KEYS (bootstrap.js), BOOTSTRAP_TIERS (cache-keys.ts), and PENDING_CONSUMERS
(bootstrap test) until a src/ consumer exists. Static 7-entry payload should
not load on every bootstrap request for a feature with no frontend.

* fix(seed-utils): pass ttlSeconds to writeFreshnessMetadata on skip path

The validation-skip branch at runSeed:657 was still calling
writeFreshnessMetadata without ttlSeconds, reintroducing the 7-day meta
TTL for any monthly/annual seed that hits an empty-data run.

* fix(test): restore chokepointBaselines in PENDING_CONSUMERS

Rebase conflict resolution kept chokepointBaselines in BOOTSTRAP_CACHE_KEYS
but the follow-up fix commit's test change auto-merged and removed it from
PENDING_CONSUMERS. Re-add it so the consumer-coverage test passes while the
frontend consumer is still pending.

* fix(iea): align COUNTRY_MAP to ASCII Turkiye key (matches main + test)

main (PR #2733) uses 'Turkiye' (no umlaut) as the COUNTRY_MAP key directly.
Our branch had 'Türkiye' + parseRecord normalization. Align with main's
approach: single key, no normalization shim needed.
This commit is contained in:
Elie Habib
2026-04-05 21:47:00 +04:00
committed by GitHub
parent fc27ae8f4a
commit f3843aaaf1
9 changed files with 195 additions and 32 deletions

View File

@@ -253,7 +253,7 @@ describe('Bootstrap key hydration coverage', () => {
const allSrc = srcFiles.map(f => readFileSync(f, 'utf-8')).join('\n');
// Keys with planned but not-yet-wired consumers
const PENDING_CONSUMERS = new Set(['chokepointTransits', 'correlationCards', 'euGasStorage']);
const PENDING_CONSUMERS = new Set(['chokepointTransits', 'correlationCards', 'euGasStorage', 'chokepointBaselines']);
for (const key of keys) {
if (PENDING_CONSUMERS.has(key)) continue;
assert.ok(