fix(forecast): resolve judged forecast outcomes#5087
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Greptile SummaryThis PR adds LLM-judge-based resolution for forecasts that cannot be auto-resolved by metric feeds, routing due
Confidence Score: 3/5Safe to merge with the understanding that any story:track:v1 hash key expiring before its ZSET entry is pruned will silently block all judged resolutions for every subsequent run until the stale ZSET members age out. The HGETALL fail-closed behavior in readDigestAccumulatorArchive turns a missing Redis key into a blanket archive-unavailable signal that freezes every pending-judge entry indefinitely. The remaining findings are self-contained and do not affect production resolution correctness. scripts/seed-forecast-resolutions.mjs — specifically the HGETALL empty-result handling in readDigestAccumulatorArchive and whether story track key TTLs are guaranteed to be at least as long as the ZSET accumulator retention window. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Worker as Railway Worker
participant Redis as Redis (Upstash)
participant OR as OpenRouter Judge
participant Groq as Groq Judge
participant R2 as Cloudflare R2
Worker->>Redis: LRANGE forecast:predictions:history:v1
Redis-->>Worker: history snapshots
Worker->>Worker: ingestHistory / samplePendingEntries / resolveDueEntries
Worker->>Redis: ZRANGEBYSCORE digest:accumulator:v1:full:en [window]
Redis-->>Worker: story hashes
Worker->>Redis: HGETALL pipeline (story:track:v1:hash x N)
Redis-->>Worker: story track rows (throws on empty/error row)
loop Each due pending-judge entry
Worker->>Worker: selectJudgedArchiveItems
alt No relevant archive items
Worker->>Worker: archiveCoversEntryWindow check
else Relevant items found
Worker->>OR: callForecastLLM
OR-->>Worker: outcome + citations
Worker->>Groq: callForecastLLM
Groq-->>Worker: outcome + citations
Worker->>Worker: normalizeJudgment x 2
end
end
Worker->>Redis: SET forecast:resolutions:v1
Worker->>Redis: SET forecast:scorecard:v1
Worker->>R2: PUT receipt JSON
%%{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 Worker as Railway Worker
participant Redis as Redis (Upstash)
participant OR as OpenRouter Judge
participant Groq as Groq Judge
participant R2 as Cloudflare R2
Worker->>Redis: LRANGE forecast:predictions:history:v1
Redis-->>Worker: history snapshots
Worker->>Worker: ingestHistory / samplePendingEntries / resolveDueEntries
Worker->>Redis: ZRANGEBYSCORE digest:accumulator:v1:full:en [window]
Redis-->>Worker: story hashes
Worker->>Redis: HGETALL pipeline (story:track:v1:hash x N)
Redis-->>Worker: story track rows (throws on empty/error row)
loop Each due pending-judge entry
Worker->>Worker: selectJudgedArchiveItems
alt No relevant archive items
Worker->>Worker: archiveCoversEntryWindow check
else Relevant items found
Worker->>OR: callForecastLLM
OR-->>Worker: outcome + citations
Worker->>Groq: callForecastLLM
Groq-->>Worker: outcome + citations
Worker->>Worker: normalizeJudgment x 2
end
end
Worker->>Redis: SET forecast:resolutions:v1
Worker->>Redis: SET forecast:scorecard:v1
Worker->>R2: PUT receipt JSON
|
…ed-resolution # Conflicts: # scripts/seed-forecast-resolutions.mjs
Addressed in
Validation: pre-push hook passed, and the merged-tree focused forecast/script sweep passed. |
…5136) (#5138) * fix(forecast): reclassify conflict/ucdp_zone count specs to judged (#5136) Conflict/ucdp_zone hard-count specs resolve against CONFLICT_COUNT_SOURCE_FEED (conflict:acled-resolution:v1), which only populates with ACLED credentials we don't have -> EXISTS=0 in prod -> every conflict count spec is unresolvable (sits pending/VOID forever). Route these families to judged resolution (live via #5087) instead of emitting a dead hard spec. - deriveHardMetrics: conflict/ucdp_zone return null when the count feed is unavailable -> buildHardSpec falls back to buildJudgedSpec (LLM judge over the news archive). Gated by CONFLICT_COUNT_FEED_AVAILABLE (default false); flip to re-enable hard-count once a populated event-count feed exists. - buildQuestion: family-aware, escalation-framed question for conflict forecasts (resolves more reliably against the archive than the generic phrasing). - Threaded an optional `conflictCountFeedAvailable` override through buildResolutionSpec/attachResolutionSpec so the preserved #5010 horizon-commensurable threshold logic stays under test (the tests that lock it force the hard path); production emits judged by default. - GDELT article-volume is NOT a substitute: its scale is article count, not event count, so the thresholds would mis-resolve. Tests: conflict now judged with an escalation question; scoped-regression that unrest still emits hard; existing conflict-hard tests force the feed-available path (preserves #5010 coverage). 405 forecast tests green. Related: #5136, #5097, #5087 (judged resolver), #5091 (KPI reframe follow-up) Claude-Session: https://claude.ai/code/session_01StNurp4TGC3JLHbTtKJhbp * fix(forecast): migrate pending conflict counts to judged
* fix(forecast): reclassify unrest count specs to judged (#5091) The twin of #5136/#5138: unrest hard-count specs resolve against UNREST_COUNT_SOURCE_FEED (unrest:events-resolution:v1), which is EMPTY in prod — seed-unrest-events only writes it from an ACLED resolution fetch, and we have no ACLED credentials. So unrest counts were unresolvable, sitting pending/VOID forever, while inflating the nominal hard-ratio. - deriveHardMetrics: the `unrest` family returns null when the count feed is unavailable -> buildJudgedSpec (LLM judge, #5087). Gated by the new UNREST_COUNT_FEED_AVAILABLE flag (default false) + an `unrestCountFeedAvailable` override; the horizon-scaled threshold logic is preserved for re-enable. - buildQuestion: family-aware, instability-framed question for the political (unrest) domain, mirroring the conflict question. - Not GDELT-for-count: article volume != event count, so the thresholds would mis-resolve (same reason it was rejected for conflict). Tests: unrest now judged by default with an instability question; the scoped regression now uses cyber (a populated feed) as the still-hard exemplar; the unrest feed-mapping + R4 fixtures force the feed-available path. 404 forecast tests green. Related: #5091 (KPI reframe + empty-feed audit), #5136/#5138 (conflict template), #5087 (judged resolver), #4930 Claude-Session: https://claude.ai/code/session_01StNurp4TGC3JLHbTtKJhbp * test(forecast): address Greptile — assert horizon in unrest question + cover tally-null guard under override (#5142) - unrest judged test now asserts the horizon (7d) appears in the question, matching the conflict test's rigor (Greptile P2). - add a test for the unrest tally-null path under unrestCountFeedAvailable:true (a non-numeric count signal still falls back to judged) — the path the flag early-return shadowed by default (Greptile P2). Claude-Session: https://claude.ai/code/session_01StNurp4TGC3JLHbTtKJhbp * fix(forecast): migrate existing pending unrest count entries to judged (#5091) #5138 added a pending-ledger migration for conflict, but the unrest reclassification (this PR) only changed new-forecast emission — existing pending unrest count rows would stay stuck as unresolvable hard specs. Generalize the migration to cover both families. - migratePendingConflictCountEntryToJudged -> table-driven migratePendingCountEntryToJudged over UNAVAILABLE_COUNT_FEED_MIGRATIONS (conflict + unrest), each with its own flag gate + judged-question builder. - unrest question mirrors the generator-side political/instability phrasing. - Tests: new unrest-migration test; the two existing conflict+unrest ledger tests now assert unrest rows migrate to judged (they encoded the old unrest-resolves premise). 150 forecast tests green. Related: #5091, #5138 Claude-Session: https://claude.ai/code/session_01StNurp4TGC3JLHbTtKJhbp * test(forecast): lock unrest threshold scaling
🔧 Fix plan — judged forecasts perpetual-pend / forced-VOID (48h archive window) — subagent-readyNow urgent: conflict + unrest were reclassified to The bug (verified against
|
|
☝️ Tracked as a live work item in #5183 (this is a merged PR — the fix lives there for a subagent to own). |
Summary
Forecast resolution now handles due judged forecasts instead of leaving them permanently in
pending-judge. The resolver uses two LLM judges over digest-archive evidence, seals YES/NO only when both models agree with grounded citations, and writes the result through the existing ledger, receipt, and scorecard flow.The failure modes are deliberately conservative: model disagreement becomes VOID, missing/malformed judge output stays pending for retry, complete archive reads with no relevant evidence become VOID, and incomplete or truncated archive reads stay pending instead of creating permanent false VOID receipts.
Related: #5007
Validation
node --test tests/forecast-resolutions-seeder.test.mjs tests/forecast-scorecard.test.mjs tests/forecast-resolution-eval.test.mjs tests/scripts-railway-nixpacks-no-escape-import.test.mtsPost-Deploy Monitoring & Validation
forecast-resolutions,[LLM:forecast_resolution_judge_openrouter],[LLM:forecast_resolution_judge_groq],judged archive unavailable,Terminal receipts resolved this cycle, andR2 receipts archived./api/healthandseed-meta:forecast:scorecardfor fresh successful runs; inspectforecast:scorecard:v1for judged entries moving from pending to scored or VOID without a sudden VOID-rate spike.forecast-resolutionsreceipts and confirm judged evidence includesdual_model_agreement,judge_disagreement,all_judges_void, orno_archive_evidencewith citations only when excerpts match the archive item.judge_unavailable/archive_unavailableattempts are transient, and LLM telemetry shows successful calls from both judge providers.archive_unavailable, same provider failing every run, zero judged receipts after due forecasts exist, or a sharp increase inno_archive_evidence/VOID rate. Roll back by reverting this PR and redeploying the previous Railway worker image.