Skip to content

feat(deep-forecast): Phase 2+3 scoring recalibration + autoresearch prompt self-improvement#2178

Merged
koala73 merged 19 commits into
mainfrom
fix/deep-forecast-acceptance-threshold
Mar 24, 2026
Merged

feat(deep-forecast): Phase 2+3 scoring recalibration + autoresearch prompt self-improvement#2178
koala73 merged 19 commits into
mainfrom
fix/deep-forecast-acceptance-threshold

Conversation

@koala73

@koala73 koala73 commented Mar 24, 2026

Copy link
Copy Markdown
Owner

Why this PR?

After Phase 1 (PR #2169) fixed structural LLM parsing bugs, two cascading problems remained:

  1. Gate 3 structurally unreachable: acceptanceScore >= 0.60 was impossible — reportableQualityScore (~0.30) is bounded by world-state simulation geometry, not hypothesis quality. Math: 0.65×0.55 + 0.30×0.20 + 0.55×0.15 = 0.50 pinned exactly at threshold. Every deep run returned completed_no_material_change.

  2. Generic chains with no commodity specificity: LLM produced route_disruption → inflation_pass_through for all candidates regardless of context. No LNG, gold, fertilizer appearing.

Changes

Gate 3 fix

  • Lower acceptance threshold 0.60 → 0.50 to match real score distribution
  • gateDetails.acceptanceThreshold updated to 0.50

Scoring recalibration (Phase 2)

  • second_order mapped floor: 0.66 → 0.58, multiplier: 0.85 → 0.88
  • pathScore threshold: 0.66 → 0.50
  • Evidence scoring: binary (0 if <2 refs, 1 if ≥2 refs) — enforces prompt rule deterministically
  • Parent-must-be-mapped invariant: second_order downgraded to trace_only if direct parent is not mapped
  • 6 new maritime chokepoints added to CHOKEPOINT_MARKET_REGIONS

Prompt v3

  • dependsOnKey exactness rule — MUST be exact variableKey of a direct in same response
  • 2-evidence minimum stated explicitly
  • Confidence calibration guidance (0.82-0.95 for strong direct, 0.72-0.85 for second_order)
  • direct+second_order pair structure enforced

Observability

  • scoringBreakdown in debug artifact: ALL hypotheses with validationScore, status, salience, specificity, evidence
  • gateDetails in debug artifact: self-documenting thresholds at run time

Autoresearch prompt self-improvement (Karpathy-style)

  • Locked scorer scoreImpactExpansionQuality: commodity rate (35%) + variable diversity (35%) + chain coverage (20%) + mapped rate (10%)
  • Mutable artifact: Redis key forecast:prompt:impact-expansion:learned (max 1600 chars, 30-day TTL)
  • LLM critic call: diagnoses failure mode, proposes addition to learned section, tests on same candidates
  • Rollback: revert Redis key if test score drops
  • Rate-limited: 1 refinement attempt per 30 minutes
  • buildImpactExpansionSystemPrompt(learnedSection) appends learned section after core rules
  • Cache busting: buildImpactExpansionCandidateHash includes learned fingerprint

Tests

  • T1–T8: Phase 2 scoring (all 8 pass)
  • T9: high commodity rate + chain coverage → composite > 0.70
  • T10: no commodity + no chain coverage → composite < 0.40
  • 113/113 tests pass

Verification

Run locally:

node scripts/seed-forecasts.mjs && node scripts/process-deep-forecast-tasks.mjs --once

Expect: [PromptRefinement] logs, at least one completed status, mappedCount >= 2 in debug artifact.

…l score distribution

computeDeepPathAcceptanceScore formula: pathScore×0.55 + quality×0.20 + coherence×0.15
With pathScore≈0.65, quality≈0.30, coherence≈0.55:
  0.358 + 0.060 + 0.083 = 0.50

The 0.60 threshold was calibrated before understanding that reportableQualityScore
is constrained by world-state simulation geometry (not hypothesis quality), and
coherence loses 0.15 for generic candidates without routeFacilityKey. The threshold
was structurally unreachable with typical expanded paths.

Verified end-to-end: deep worker now returns [DeepForecast] completed.

Also updates T6 gateDetails assertion and renames the rejection-floor test to
correctly describe the new behavior (strong inputs should be accepted).

111/111 tests pass.
@vercel

vercel Bot commented Mar 24, 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 Mar 24, 2026 2:31pm

Request Review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

…10 tests

- Add scoreImpactExpansionQuality() locked scorer: commodity rate (35%),
  variable diversity (35%), chain coverage (20%), mapped rate (10%)
- Add runImpactExpansionPromptRefinement(): rate-limited LLM critic loop
  (30min cooldown) that reads learned section from Redis, scores current
  run, generates critique if composite < 0.62, tests on same candidates,
  commits to forecast:prompt:impact-expansion:learned if score improves
- buildImpactExpansionSystemPrompt() now accepts learnedSection param,
  appends it after core rules with separator so model sees prior examples
- buildImpactExpansionCandidateHash() includes learnedFingerprint to
  bust cache when learned section changes
- processDeepForecastTask reads learnedSection from Redis before LLM
  call, runs refinement after both completed and no_material_change paths
- Export scoreImpactExpansionQuality + runImpactExpansionPromptRefinement
- T9: high commodity rate + chain coverage → composite > 0.70
- T10: no commodity + no chain coverage → composite < 0.40
- 113/113 tests pass
@koala73 koala73 changed the title fix(deep-forecast): lower acceptance threshold 0.60→0.50 to match real score distribution feat(deep-forecast): Phase 2+3 scoring recalibration + autoresearch prompt self-improvement Mar 24, 2026
koala73 and others added 11 commits March 24, 2026 09:35
… parse

- Threshold 0.62 was too low: commodity=1.00 + chain=1.00 compensated for
  diversity=0.50 (all same chains), keeping composite at 0.775 → no critique
- Raise to 0.80 so diversity<0.70 triggers critique even with good commodity/chain
- Fix JSON parser to extract first {…} block (handles Gemini code-fence wrapping)
- Add per-hypothesis log in refinement breakdown for observability
- Add refinementQualityThreshold to gateDetails for self-documenting artifacts
- Verified: critique fires on diversity=0.50 run, committed Hormuz/Baltic/Suez
  region-specific chain examples (score 0.592→0.650)
…xpansion

Previously: all candidates → one batch LLM call → LLM averages context →
identical route_disruption → inflation_pass_through chains for all candidates.

Now: each candidate → its own focused LLM call (parallel Promise.all) →
LLM reasons about specific stateKind/region/routeFacility for that candidate.

Results (3 candidates, 3 parallel calls):
- composite: 0.592 → 0.831 (+0.24)
- commodity: 0.17 → 1.00 (all mapped have specific commodity)
- diversity: 0.50 → 0.83 (energy_export_stress, importer_balance_stress
  appearing alongside route_disruption — genuinely different chains)
- Baseline updated: 0.831 (above 0.80 threshold → no critique needed)

Also threads learnedSection through extractSingleImpactExpansionCandidate
so the learned examples from autoresearch apply to each focused call.
Per-candidate cache keys (already existed) now serve as primary cache.
- Change stage mock from impact_expansion → impact_expansion_single
  (batch primary path removed, per-candidate is now primary)
- Assert parseMode === per_candidate instead of parseStage /^recovered_/
  (recovered_ prefix was only set by old batch_repair path)
- 2257/2257 tests pass
…kepoint map

Candidate packets had routeFacilityKey=none for Red Sea / Persian Gulf /
Baltic Sea signals because prediction titles say "Red Sea maritime disruption"
not "Bab el-Mandeb" or "Strait of Hormuz". CHOKEPOINT_MARKET_REGIONS only
had sub-facility names (Bab el-Mandeb, Suez Canal) as keys, not the sea
regions themselves.

Fix: add Red Sea, Persian Gulf, Arabian Sea, Black Sea, South China Sea,
Mediterranean Sea as direct keys so region-level candidate titles resolve.

Result: LLM user prompt now shows routeFacilityKey=Red Sea / Persian Gulf /
Baltic Sea per candidate — giving each focused call the geographic context
needed to generate route-specific chains.

- Autoresearch baseline updated 0.932→0.965 on this run
- T8 extended with Red Sea, Persian Gulf, South China Sea assertions
- 2257/2257 tests pass
…nstraint

- Bump IMPACT_EXPANSION_REGISTRY_VERSION to v4
- Add hypothesisKey, description, geography, affectedAssets, marketImpact, causalLink fields to normalizeImpactHypothesisDraft (keep legacy fields for backward compat)
- Rewrite buildImpactExpansionSystemPrompt: remove IMPACT_VARIABLE_REGISTRY constraint table, use free-form ImpactHypothesis schema with geographic/commodity specificity rules
- Rewrite evaluateImpactHypothesisRejection: use effective key (hypothesisKey || variableKey) for dedup; legacy registry check only for old cached responses without hypothesisKey
- Update validateImpactHypotheses scoring: add geographyScore, commodityScore, causalLinkScore, assetScore terms; channelCoherence/bucketCoherence only apply to legacy responses
- Update parent-must-be-mapped invariant to use hypothesisKey || variableKey as effective key
- Update mapImpactHypothesesToWorldSignals: use effective key for dedup and sourceKey; prefer description/geography over legacy fields
- Update buildImpactPathsForCandidate: match on hypothesisKey || variableKey for parent lookup
- Update buildImpactPathId: use hypothesisKey || variableKey for hash inputs
- Rewrite scoreImpactExpansionQuality: add geographyRate and assetRate metrics; update composite weights
- Update buildImpactPromptCritiqueSystemPrompt/UserPrompt: use hypothesisKey-based chain format in examples
- Add new fields to buildImpactExpansionBundleFromPaths push calls
- Update T7 test assertion: MUST be the exact hypothesisKey instead of variableKey string
…orer

- commodityDiversity = unique commodities / nCandidates (weight 0.35)
  Penalizes runs where all candidates default to same commodity.
  3 candidates all crude_oil → diversity=0.33 → composite ~0.76 → critique fires.
- Rebalanced composite weights: comDiversity 0.35, geo 0.20, keyDiversity 0.15, chain 0.10, commodityRate 0.10, asset 0.05, mappedRate 0.05
- Breakdown log now shows comDiversity + geo + keyDiversity
- Critique prompt updated: commodity_monoculture failure mode, diagnosis targets commodity homogeneity
- T9: added commodityDiversity=1.0 assertion (2 unique commodities across 2 candidates)
…ityDiversity + directGeoDiversity + candidateSpreadScore

Problem: measuring diversity on all mapped hypotheses misses the case where
one candidate generates 10 implications while others generate 0, or where
all candidates converge on the same commodity due to dominating signals.

Fix: score at the DIRECT hypothesis level (root causes only) and add
a candidate-spread metric:

- directCommodityDiversity: unique commodities among direct hypotheses /
  nCandidates. Measures breadth at the root-cause level. 3 candidates all
  crude_oil → 0.33 → composite ~0.77 → critique fires.

- directGeoDiversity: unique primary geographies among direct hypotheses /
  nCandidates. First segment of compound geography strings (e.g.
  'Red Sea, Suez Canal' → 'red sea') to avoid double-counting.

- candidateSpreadScore: normalized inverse-HHI. 1.0 = perfectly even
  distribution across candidates. One candidate with 10 implications and
  others with 0 → scores near 0 → critique fires.

Weight rationale: comDiversity 0.35, geoDiversity 0.20, spread 0.15,
chain 0.15, comRate 0.08, assetRate 0.04, mappedRate 0.03.

Verified: Run 2 Baltic/Hormuz/Brazil → freight/crude_oil/USD spread=0.98 ✓
Surface autoresearch loop outcome per run: converged (bool), finalComposite,
critiqueIterations (0 or 1), refinementCommitted, and perCandidateMappedCount
(candidateStateId → count). After 5+ runs the artifact alone answers whether
the pipeline is improving.

Architectural changes:
- runImpactExpansionPromptRefinement now returns { iterationCount, committed }
  at all exit paths instead of undefined
- Call hoisted before writeForecastTraceArtifacts so the result flows into the
  debug payload via dataForWrite.refinementResult
- buildImpactExpansionDebugPayload assembles convergence from validation +
  refinementResult; exported for direct testing
- Fix: stale diversityScore reference replaced with directCommodityDiversity

Tests: T-conv-1 (converged=true), T-conv-2 (converged=false + iterations=1),
T-conv-3 (perCandidateMappedCount grouping) — 116/116 pass
…rvability PR

P1-A: sanitize LLM-returned proposed_addition before Redis write (prompt injection
      guard via sanitizeProposedLlmAddition — strips directive-phrase lines)
P1-B: restore fire-and-forget for runImpactExpansionPromptRefinement; compute
      critiqueIterations from quality score (predicted) instead of awaiting result,
      eliminating 15-30s critical-path latency on poor-quality runs
P1-C: processDeepForecastTask now returns convergence object to callers; add
      convergence_quality_met warn check to evaluateForecastRunArtifacts
P1-D: cap concurrent LLM calls in extractImpactExpansionBundle to 3 (manual
      batching — no p-limit) to respect provider rate limits

P2-1: hash full learnedSection in buildImpactExpansionCandidateHash (was sliced
      to 80 chars, causing cache collisions on long learned sections)
P2-2: add exitReason field to all runImpactExpansionPromptRefinement return paths
P2-3: sanitizeForPrompt strips directive injection phrases; new
      sanitizeProposedLlmAddition applies line-level filtering before Redis write
P2-4: add comment explaining intentional bidirectional affectedAssets/assetsOrSectors
      coalescing in normalizeImpactHypothesisDraft
P2-5: extract makeConvTestData helper in T-conv tests; remove refinementCommitted
      assertions (field removed from convergence shape)
P2-6: convergence_quality_met check added to evaluateForecastRunArtifacts (warn)

🤖 Generated with Claude Sonnet 4.6 via Claude Code (https://claude.ai/claude-code) + Compound Engineering v2.49.0

Co-Authored-By: Claude Sonnet 4.6 (200K context) <[email protected]>
koala73 and others added 6 commits March 24, 2026 16:10
…rvability

P1-1: mapImpactHypothesesToWorldSignals used free-form marketImpact values
(price_spike, shortage, credit_stress, risk_off) verbatim as signal channel
types, producing unknown types that buildMarketTransmissionGraph cannot
consume. Add IMPACT_SIGNAL_CHANNELS set + resolveImpactChannel() to map
free-form strings to the nearest valid channel before signal materialization.

P1-2: sanitizeForPrompt had directive-phrase stripping added that was too
broad for a function called on headlines, evidence tables, case files, and
geopolitical summaries. Reverted to original safe sanitizer (newline/control
char removal only). Directive stripping remains in sanitizeProposedLlmAddition
where it is scoped to Redis-bound LLM-generated additions only.

P2: Renamed convergence.critiqueIterations to predictedCritiqueIterations to
make clear this is a prediction from the quality score, not a measured count
from actual refinement behavior (refinement is fire-and-forget after artifact
write). Updated T-conv-1/2 test assertions to match.
Wire inputs.newsInsights / inputs.newsDigest through the candidate
selection pipeline so buildImpactExpansionEvidenceTable receives up to
3 commodity-relevant live headlines as 'live_news' evidence entries.

Changes:
- IMPACT_COMMODITY_LEXICON: extend fertilizer pattern (fertiliser,
  nitrogen, phosphate, npk); add food_grains and shipping_freight entries
- filterNewsHeadlinesByState: new pure helper that scores headlines by
  alert status, LNG/energy/route/sanctions signal match, lexicon commodity
  match, and source corroboration count (min score 2 to include)
- buildImpactExpansionEvidenceTable: add newsItems param, inject
  live_news entries, raise cap 8→11
- buildImpactExpansionCandidate: add newsInsights/newsDigest params,
  compute newsItems via filterNewsHeadlinesByState
- selectImpactExpansionCandidates: add newsInsights/newsDigest to options
- Call site: pass inputs.newsInsights/newsDigest at seed time
- Export filterNewsHeadlinesByState, buildImpactExpansionEvidenceTable
- 9 new tests (T-news-1 through T-lex-3): all pass, 125 total pass

🤖 Generated with Claude Sonnet 4.6 (200K context) via Claude Code (https://claude.ai/claude-code) + Compound Engineering v2.49.0

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…adlinesByState

The LNG+2 score was commodity-specific and inconsistent with the
intent: headline scoring should be generic, not biased toward any
named commodity. The function already handles the state's detected
commodity dynamically via lexEntry.pattern (IMPACT_COMMODITY_LEXICON).
LNG headlines still score via CRITICAL_NEWS_ENERGY_RE (+1) and
CRITICAL_NEWS_ROUTE_RE (+1) when relevant to the state's region.
All 125 tests pass.


P1 fixes (block-merge):
- Lower third_order mapped floor 0.74→0.70 (max achievable via 0.72 multiplier was 0.72)
- Guard runImpactExpansionPromptRefinement against empty validation (no_mapped exit)
- Replace block-list sanitizeProposedLlmAddition with pattern-based allowlist (HTML/JS/directive takeover)
- Fix TOCTOU on PROMPT_LAST_ATTEMPT_KEY: claim slot before quality check, not after LLM call

P2 fixes:
- Fix learned section overflow: use slice(-MAX) to preserve tail, not discard all prior content
- Add safe_haven_bid and global_crude_spread_stress branches to resolveImpactChannel
- quality_met path now sets rate-limit key (prevents 3 Redis GETs per good run)
- Hoist extractNewsClusterItems outside stateUnit map in selectImpactExpansionCandidates
- Export PROMPT_LEARNED_KEY, PROMPT_BASELINE_KEY, PROMPT_LAST_ATTEMPT_KEY + read/clear helpers

All 125 tests pass.
@koala73
koala73 merged commit 226cebf into main Mar 24, 2026
8 checks passed
@koala73
koala73 deleted the fix/deep-forecast-acceptance-threshold branch March 24, 2026 14:53
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.

1 participant