Skip to content

Fix forecast hard resolution coverage#5072

Merged
koala73 merged 4 commits into
mainfrom
codex/issue-5066-forecast-resolvability
Jul 9, 2026
Merged

Fix forecast hard resolution coverage#5072
koala73 merged 4 commits into
mainfrom
codex/issue-5066-forecast-resolvability

Conversation

@koala73

@koala73 koala73 commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • route conflict/UCDP hard count specs to fresh ACLED data instead of the lagged UCDP feed
  • add hard resolvability for political unrest-event and cyber-volume forecasts, including ISO/name country matching for cyber feeds
  • boost hard-resolvable forecasts during publication selection to help the canonical set meet the >=80% resolvability KPI

Fixes #5066

Tests

  • node --test tests/forecast-resolution.test.mjs tests/forecast-resolution-eval.test.mjs tests/forecast-resolutions-seeder.test.mjs tests/forecast-history.test.mjs tests/forecast-detectors.test.mjs
  • node --test tests/forecast-resolution.test.mjs tests/forecast-resolution-eval.test.mjs tests/forecast-detectors.test.mjs
  • npm run typecheck:api
  • git diff --check

@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment Jul 9, 2026 6:07am

Request Review

@greptile-apps

greptile-apps Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR improves forecast hard-resolution coverage by routing conflict/UCDP count specs to a fresher ACLED feed (2-day lag instead of 14 days), adding hard-resolvability for political unrest-event and cyber-volume forecasts with ISO/name country alias matching, and boosting hard-resolvable forecasts during publication scoring to help hit the ≥80% resolvability KPI.

  • Feed routing: conflict and ucdp_zone families now resolve against conflict:acled:v1:all:0:0 with a 2-day settlement lag; unrest and cyber families get dedicated source feeds with zero lag (live feeds).
  • Country matching: a bidirectional ISO↔name alias map loaded from shared/country-names.json enables matching cyber feed ISO-2 codes against display-name forecast regions; fieldAliases for country now includes countryCode and iso2 as record-key aliases.
  • Selection scoring: a flat RESOLVABLE_HARD_SELECTION_LIFT = 0.25 is added to computePublishSelectionScore for any forecast with resolution.kind === 'hard'.

Confidence Score: 4/5

Safe to merge; the core resolution logic and feed routing are well-tested and the new ISO/name alias matching is correct.

The lastSeenAt fallback in extractRecordTime can over-count persistent cyber threats whose observation window straddles the forecast boundary, and the 0.25 selection lift is large enough to effectively override probability and readiness signals for hard-resolvable forecasts. Both are bounded concerns — the first only affects records missing firstSeenAt, and the second is explicitly tied to a KPI target — but they warrant a second look before the lift value is treated as permanent.

scripts/_forecast-resolution-eval.mjs (lastSeenAt ordering) and scripts/seed-forecasts.mjs (lift magnitude).

Important Files Changed

Filename Overview
scripts/_forecast-resolution-eval.mjs Adds countSettlementLagMs (2-day ACLED, 0 for unrest/cyber), ISO/name country alias matching via country-names.json, and new record time fields (firstSeenAt, lastSeenAt, occurredAt, event_date); lastSeenAt ordering could misattribute persistent threats.
scripts/_forecast-resolution.mjs Adds unrest and cyber hard families with correct feed mappings, removes political/cyber from JUDGED_DOMAINS, exports new feed-key constants, and updates comments; logic is consistent with the new DOMAIN_TO_HARD_FAMILIES constraints.
scripts/seed-forecasts.mjs Adds RESOLVABLE_HARD_SELECTION_LIFT = 0.25 to the publish selection score for hard-resolvable forecasts; the magnitude is intentional but dominates analytical quality signals like probability and readiness.
tests/forecast-resolution.test.mjs Adds coverage for political/unrest and cyber hard-spec paths, updates JUDGED_DOMAINS assertions to the new military-only set, and replaces hardcoded feed-key strings with exported constants.
tests/forecast-resolution-eval.test.mjs New tests for ACLED 2-day lag, event_date timestamp parsing, and EE↔Estonia ISO/name matching confirm the eval-layer changes correctly; all fixtures are well-bounded.
tests/forecast-detectors.test.mjs Adds a publish-selection boost test confirming hard beats judged by the 0.25 lift; test is clear and correctly exercises the new scoring path.
tests/forecast-history.test.mjs Updates the sourceFeed assertion from the hardcoded UCDP key to the exported CONFLICT_COUNT_SOURCE_FEED constant; straightforward one-line change.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[buildResolutionSpec] --> B{domain in JUDGED_DOMAINS?}
    B -- military --> J[judged spec]
    B -- no --> C{prediction_market signal?}
    C -- yes --> D[hard: prediction_market family]
    C -- no --> E{domain in DOMAIN_TO_HARD_FAMILIES?}
    E -- political → unrest family --> F{unrest_events tally finite?}
    F -- yes --> G[hard: unrest:events:v1 lag=0ms]
    F -- no --> J
    E -- cyber → cyber family --> H{cyber tally finite?}
    H -- yes --> I[hard: cyber:threats-bootstrap:v2 lag=0ms ISO↔name matching]
    H -- no --> J
    E -- conflict/ucdp_zone family --> K{ucdp/conflict_events tally finite?}
    K -- yes --> L[hard: conflict:acled:v1:all:0:0 lag=2 days]
    K -- no --> J
    E -- other families --> M[hard or judged per signal type]
    L --> N[computePublishSelectionScore +0.25 resolvabilityLift]
    G --> N
    I --> N
    D --> N
    J --> O[score: no lift]
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"}}}%%
flowchart TD
    A[buildResolutionSpec] --> B{domain in JUDGED_DOMAINS?}
    B -- military --> J[judged spec]
    B -- no --> C{prediction_market signal?}
    C -- yes --> D[hard: prediction_market family]
    C -- no --> E{domain in DOMAIN_TO_HARD_FAMILIES?}
    E -- political → unrest family --> F{unrest_events tally finite?}
    F -- yes --> G[hard: unrest:events:v1 lag=0ms]
    F -- no --> J
    E -- cyber → cyber family --> H{cyber tally finite?}
    H -- yes --> I[hard: cyber:threats-bootstrap:v2 lag=0ms ISO↔name matching]
    H -- no --> J
    E -- conflict/ucdp_zone family --> K{ucdp/conflict_events tally finite?}
    K -- yes --> L[hard: conflict:acled:v1:all:0:0 lag=2 days]
    K -- no --> J
    E -- other families --> M[hard or judged per signal type]
    L --> N[computePublishSelectionScore +0.25 resolvabilityLift]
    G --> N
    I --> N
    D --> N
    J --> O[score: no lift]
Loading

Comments Outside Diff (1)

  1. scripts/seed-forecasts.mjs, line 13308 (link)

    P2 RESOLVABLE_HARD_SELECTION_LIFT = 0.25 dominates all other quality signals

    The flat 0.25 lift is larger than the maximum readiness * 0.2 contribution and roughly equal to the combined ceiling of probability (0.15) and confidence (0.07) terms. Any hard-resolvable forecast with resolution.kind === 'hard' will outscore a judged forecast unless the judged one has a significantly higher analysisPriority. The test demonstrates this: the judged forecast with probability 0.82 loses to the hard forecast with probability 0.50. This is documented as intentional for the ≥80% KPI, but once the KPI is met the magnitude may warrant re-tuning to avoid systematically surfacing low-probability but technically-measurable claims over higher-confidence judged ones.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "fix(forecast): improve hard resolution c..." | Re-trigger Greptile

record.timestamp,
record.generatedAt,
record.firstSeenAt,
record.lastSeenAt,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 lastSeenAt can misattribute persistent threats to the wrong window

extractRecordTime tries firstSeenAt before lastSeenAt, so for records that carry both fields the correct start time wins. But for records that carry only lastSeenAt — a threat active across the forecast boundary — the window check uses the observation end time, not the start time. A long-running threat first seen before generatedAt would be counted in the [generatedAt, deadline] window if its lastSeenAt falls within it, inflating the event count.

koala73 added 2 commits July 9, 2026 09:35
Resolve the tests/forecast-resolution-eval.test.mjs conflict (both sides
added count tests) and reconcile #5063's source-coverage gate with this
PR's new live feeds:

- Union this PR's ACLED-lag + cyber-ISO tests with #5063's UCDP
  source-lag + void/truncated-count tests.
- Scope #5063's summarizeRecordCoverage gate to lagged dated-snapshot
  feeds (settlementLagMs > 0). The naive merge ran it against the new
  live cyber feed, whose max firstSeenAt trails a future deadline,
  stranding cyber counts perpetually pending.

Review fixes found while resolving:

- P1: bridge UCDP<->ACLED country naming. Conflict regions are UCDP-named
  ("DR Congo (Zaire)") but now resolve against the ACLED feed
  ("Democratic Republic of Congo"); country-names.json bridged neither,
  scoring a false NO on active zones. Canonicalize both toward a shared
  token (drop parenthetical alternate + "the"), verified collision-free.
- P2: give unrest:events:v1 the 2-day ACLED settlement lag. It is ACLED
  "Protests" data dated by event_date, so lag 0 sealed before ingestion
  and scored false NOs. Cyber stays live (firstSeenAt is real-time).
- Correct a stale "No publish-selection change" comment.

Adds a UCDP-parenthetical-region regression test. Full forecast suite 335/335.

Claude-Session: https://claude.ai/code/session_01LDPvW28NvqVyyYx13a2QE8
…LED test

- Reduce RESOLVABLE_HARD_SELECTION_LIFT 0.25 -> 0.12. The flat 0.25 was the
  second-largest term in computePublishSelectionScore, dwarfing readiness
  (x0.2), probability (x0.15) and the memory lift (~0.17) and flatly
  overriding large quality gaps. 0.12 keeps a strong preference for
  measurable forecasts (the >=80% resolvability KPI) without dominating the
  primary quality signals; tunable upward if the KPI is missed.
- Reshape the ACLED conflict-count test to the production feed shape:
  seed-conflict-intel.mjs emits numeric occurredAt, not event_date strings.
  Keeps one event_date record so that fallback branch stays covered.

Full forecast suite 335/335.

Claude-Session: https://claude.ai/code/session_01LDPvW28NvqVyyYx13a2QE8
Main advanced with #5071 (which independently landed the same eval-layer
count-resolution coverage: ACLED settlement lag, ISO<->name country
aliases, extractRecordTime fields) and #5073 (resolution-ledger pruning).

Resolve the overlap in scripts/_forecast-resolution-eval.mjs and its test
by taking this branch's version: it is a strict superset of #5071's
eval-layer code plus the fixes from review —
- unrest:events:v1 gets the 2-day ACLED settlement lag (not 0),
- #5063's source-coverage gate is scoped to lagged feeds (settlementLagMs>0)
  so the new live cyber feed is not stranded pending,
- country matching canonicalizes UCDP parenthetical / ACLED article-dropped
  names so a UCDP-named region resolves against the ACLED feed,
- the ACLED test uses the production occurredAt shape,
- plus the UCDP-parenthetical-region regression test.
Confirmed no unique #5071 test/code is dropped (its titles are a subset of
this branch's, modulo one renamed test).

This PR's remaining unique surface is the spec-building side
(_forecast-resolution.mjs: unrest/cyber hard families, JUDGED_DOMAINS ->
{military}) and the tuned publish-selection lift. Full forecast suite 343/343.

Claude-Session: https://claude.ai/code/session_01LDPvW28NvqVyyYx13a2QE8
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.

fix(forecast): resolvability coverage — 43% hard vs ≥80% KPI + UCDP count specs unresolvable off lagged feed (#4930)

1 participant