* fix(seeds): strict-floor validators must not poison seed-meta on empty
When `runSeed`'s validateFn rejected (empty/short data), seed-meta was
refreshed with `fetchedAt=now, recordCount=0`. Bundle runners read
`fetchedAt` to decide skip — so one transient empty fetch locked the
IMF-extended bundle (30-day cadence) out for a full month.
Adds opt-in `emptyDataIsFailure` flag that skips the meta refresh on
validation failure, letting the bundle retry next cron fire and health
flip to STALE_SEED. Wires it on all four IMF/WEO seeders (floor 150-190
countries), which structurally can't have legitimate empty results.
Default behavior unchanged for quiet-period feeds (news, events) where
empty is normal.
Observed: Railway log 2026-04-13 18:58 — imf-external validation fail;
next fire 8h later skipped "483min ago / interval 43200min".
* test(seeds): regression coverage for emptyDataIsFailure branch
Static-analysis guard against the PR #3078 regression reintroducing itself:
- Asserts runSeed gates writeFreshnessMetadata on opts.emptyDataIsFailure
and that extendExistingTtl still runs in both branches (cache preserved).
- Asserts the four strict-floor IMF seeders (external/growth/labor/macro)
pass emptyDataIsFailure: true.
Prevents silent bundle-lockout if someone removes the gate or adds a new
strict-floor seeder without the flag.
* fix(seeds): strict-floor failure must exit(1) + behavioral test
P2 (surfacing upstream failures in bundle summary):
Strict-floor seeders with emptyDataIsFailure:true now process.exit(1)
after logging FAILURE. _bundle-runner's spawnSeed wraps execFile, so
non-zero exit rejects → failed++ increments → bundle itself exits 1.
Before: bundle logged 'Done' and ran++ on a poisoned upstream, hiding
30-day outages from Railway monitoring.
P3 (behavioral regression coverage, replacing static source-shape test):
Stubs globalThis.fetch (Upstash REST) + process.exit to drive runSeed
through both branches. Asserts on actual Redis commands:
- strict path: zero seed-meta SET, pipeline EXPIRE still called, exit(1)
- default path: exactly one seed-meta SET, exit(0)
Catches future regressions where writeFreshnessMetadata is reintroduced
indirectly, and is immune to cosmetic refactors of _seed-utils.mjs.
* test(seeds): regression for emptyDataIsFailure meta-refresh gate
Proves that validation failure with opts.emptyDataIsFailure:true does NOT
write seed-meta (strict-floor seeders) while the default behavior DOES
write count=0 meta (quiet-period feeds). Addresses PR #3078 review.