Skip to content

fix(forecast): surface silent forecast input feed gaps#5109

Merged
koala73 merged 5 commits into
mainfrom
codex/issue-5101-signal-observability
Jul 9, 2026
Merged

fix(forecast): surface silent forecast input feed gaps#5109
koala73 merged 5 commits into
mainfrom
codex/issue-5101-signal-observability

Conversation

@koala73

@koala73 koala73 commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Summary

Forecast runs now call out silent input-feed failures instead of letting missing or empty Redis inputs collapse into empty detector arrays with no operator signal. The run logs emit one [ForecastInputs] warning per zero-record feed, including the Redis key and records=0, so ACLED, temporal anomalies, FRED series, and market-side inputs are visible when they no-fire.

Forecast input coverage is also wired into health: temporal anomalies now require a strict data/meta signal, the forecast-specific FRED keys are registered with seed-meta expectations, and a successfully computed zero-anomaly temporal snapshot stays healthy rather than alarming as empty data.

Temporal anomalies are now kept warm through the relay warm-ping path and seed-infra, and the producer writes seed-meta:temporal:anomalies when it refreshes the forecast input snapshot.

Validation

  • ./node_modules/.bin/tsx --test tests/forecast-input-observability.test.mjs tests/forecast-health-registration.test.mjs tests/relay-warm-ping-auth.test.mts tests/seed-warm-ping-origin.test.mjs tests/forecast-trace-export.test.mjs (339 pass)
  • npm run typecheck:api
  • git diff --check origin/main...HEAD
  • Pre-push gate: npm run typecheck, npm run typecheck:api, Convex typecheck, CJS syntax, Unicode safety, boundary/safe-html/Sentry/rate-limit/premium-fetch guards, edge bundle/tests, changed tests, version sync

Fixes #5101


Compound Engineering
GPT-5

@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
worldmonitor Ignored Ignored Preview Jul 9, 2026 3:22pm

Request Review

@koala73
koala73 force-pushed the codex/issue-5101-signal-observability branch from 843dd6f to 9642fdc Compare July 9, 2026 14:02
@greptile-apps

greptile-apps Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR surfaces silent forecast input feed failures by adding per-feed record-count logging, wiring temporal anomalies into the strict health-check path, and keeping the feed warm via relay and seed-infra pings. When any feed returns zero records, a [ForecastInputs] warning is emitted at run time, and the new seed-meta:temporal:anomalies key lets the health endpoint detect staleness rather than silently passing.

  • Observability layer (seed-forecasts.mjs): adds buildForecastInputFeedDefinitions, buildForecastInputPresenceRows, and warnOnMissingForecastInputs to emit one warning per zero-record feed during readInputKeys, covering ACLED, temporal anomalies, FRED series, and market-side inputs.
  • Health tightening (api/health.js): removes temporalAnomalies from ON_DEMAND_KEYS (EMPTY is now CRIT instead of WARN), adds 9 forecast-specific FRED series to STANDALONE_KEYS + SEED_META, and registers the new seed-meta:temporal:anomalies key with a 45-minute staleness budget.
  • Warm-ping integration (server/gateway.ts, scripts/seed-infra.mjs, list-temporal-anomalies.ts): the producer writes seed-meta:temporal:anomalies after each fresh snapshot, and the path is added to both RELAY_WARM_PING_PATHS and the seed-infra warm-ping loop.

Confidence Score: 4/5

Safe to merge; the observability and health wiring is well-structured, tested, and consistent with existing patterns in the codebase.

The core logic is sound — setCachedJson returns a reliable boolean, the 45-minute staleness budget aligns with the 15-minute TTL and warm-ping cadence, and the ON_DEMAND removal is correctly paired with seed-meta registration. Two design-level concerns in seed-forecasts.mjs (silent feed-key gap in buildForecastInputPresenceRows, and Math.max masking in countObjectCollection) don't affect current behavior but could quietly reduce warning coverage if the feed list drifts or if a multi-array payload shape appears.

scripts/seed-forecasts.mjs — specifically the interaction between buildForecastInputFeedDefinitions and the fetch list in readInputKeys, and the countObjectCollection max-array heuristic.

Important Files Changed

Filename Overview
scripts/seed-forecasts.mjs Adds feed-presence counting and warning helpers; the filter in buildForecastInputPresenceRows silently skips any feed key not present in parsedByKey, creating a latent maintenance gap. countObjectCollection returns Math.max of all array lengths, which could suppress a warning when only the semantically important array is empty.
api/health.js Removes temporalAnomalies from ON_DEMAND_KEYS to enforce strict CRIT on absence; adds 9 forecast FRED series to STANDALONE_KEYS and SEED_META with correct 1500-minute daily-cron budget; adds seed-meta:temporal:anomalies with 45-minute budget aligned to the 15-minute TTL and warm-ping cadence.
server/worldmonitor/infrastructure/v1/list-temporal-anomalies.ts Writes seed-meta:temporal:anomalies only when the main data key write succeeds; the 604800-second Redis TTL for the meta key is consistent with other seed-meta entries, while freshness is controlled by the fetchedAt+maxStaleMin pair in health.js.
server/gateway.ts Adds list-temporal-anomalies to RELAY_WARM_PING_PATHS; straightforward and consistent with existing entries.
scripts/seed-infra.mjs Adds temporal-anomalies warm-ping and extends the count-detection chain with data.anomalies?.length; nullish-coalescing correctly treats 0 as a valid count (not falling through to cables).
tests/forecast-input-observability.test.mjs New test file covering zero-record detection and per-feed warning output; assertions are specific and cover null, empty-array, valid, and wrong-shape payloads.
tests/forecast-health-registration.test.mjs Extends existing health tests to verify SEED_META registration for temporal anomalies and 9 FRED forecast keys, plus classifyKey behavior for missing vs. fresh-zero-anomaly snapshots.
tests/relay-warm-ping-auth.test.mts Adds list-temporal-anomalies to the expected RELAY_WARM_PING_PATHS set; minimal, correct change.
tests/seed-warm-ping-origin.test.mjs Verifies seed-infra.mjs includes the temporal-anomalies path in its source text; simple regex assertion.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant SI as seed-infra (warm-ping)
    participant Relay as relay warm-ping
    participant LTA as list-temporal-anomalies
    participant Redis as Redis
    participant SF as seed-forecasts (readInputKeys)
    participant HC as /api/health

    SI->>LTA: POST /api/infrastructure/v1/list-temporal-anomalies
    Relay->>LTA: POST /api/infrastructure/v1/list-temporal-anomalies

    LTA->>Redis: GET temporal:anomalies:v1
    alt "cache hit (age < 900s)"
        Redis-->>LTA: cached snapshot
        LTA-->>SI: return cached (no seed-meta update)
    else cache miss or stale
        LTA->>LTA: compute fresh snapshot
        LTA->>Redis: "SET temporal:anomalies:v1 (TTL=900s)"
        Redis-->>LTA: "published=true"
        LTA->>Redis: "SET seed-meta:temporal:anomalies {fetchedAt, recordCount} (TTL=604800s)"
        LTA-->>SI: return snapshot
    end

    SF->>Redis: pipeline GET all forecast input keys
    Redis-->>SF: parsedByKey
    SF->>SF: buildForecastInputPresenceRows(parsedByKey)
    SF->>SF: warnOnMissingForecastInputs(rows)
    Note over SF: logs [ForecastInputs] warn per zero-record feed

    HC->>Redis: GET temporal:anomalies:v1 strlen
    HC->>Redis: GET seed-meta:temporal:anomalies
    alt "data present AND seed-meta.fetchedAt < 45min ago"
        HC-->>HC: "status=OK"
    else missing or stale
        HC-->>HC: "status=EMPTY or STALE_SEED (CRIT)"
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant SI as seed-infra (warm-ping)
    participant Relay as relay warm-ping
    participant LTA as list-temporal-anomalies
    participant Redis as Redis
    participant SF as seed-forecasts (readInputKeys)
    participant HC as /api/health

    SI->>LTA: POST /api/infrastructure/v1/list-temporal-anomalies
    Relay->>LTA: POST /api/infrastructure/v1/list-temporal-anomalies

    LTA->>Redis: GET temporal:anomalies:v1
    alt "cache hit (age < 900s)"
        Redis-->>LTA: cached snapshot
        LTA-->>SI: return cached (no seed-meta update)
    else cache miss or stale
        LTA->>LTA: compute fresh snapshot
        LTA->>Redis: "SET temporal:anomalies:v1 (TTL=900s)"
        Redis-->>LTA: "published=true"
        LTA->>Redis: "SET seed-meta:temporal:anomalies {fetchedAt, recordCount} (TTL=604800s)"
        LTA-->>SI: return snapshot
    end

    SF->>Redis: pipeline GET all forecast input keys
    Redis-->>SF: parsedByKey
    SF->>SF: buildForecastInputPresenceRows(parsedByKey)
    SF->>SF: warnOnMissingForecastInputs(rows)
    Note over SF: logs [ForecastInputs] warn per zero-record feed

    HC->>Redis: GET temporal:anomalies:v1 strlen
    HC->>Redis: GET seed-meta:temporal:anomalies
    alt "data present AND seed-meta.fetchedAt < 45min ago"
        HC-->>HC: "status=OK"
    else missing or stale
        HC-->>HC: "status=EMPTY or STALE_SEED (CRIT)"
    end
Loading

Reviews (1): Last reviewed commit: "fix(forecast): surface missing forecast ..." | Re-trigger Greptile

Comment thread scripts/seed-forecasts.mjs
Comment thread scripts/seed-forecasts.mjs
@koala73
koala73 merged commit a86a922 into main Jul 9, 2026
23 checks passed
@koala73
koala73 deleted the codex/issue-5101-signal-observability branch July 9, 2026 15:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(forecast): signal observability — kill silent feed no-fire (per-feed logging + health coverage + temporal keep-warm) (P2, #5097)

1 participant