Files
worldmonitor/todos/088-complete-p1-keyactorroles-missing-from-uitheaters-redis-projection.md
Elie Habib 2fddee6b05 feat(simulation): add keyActorRoles to fix actor overlap bonus vocabulary mismatch (#2582)
* feat(simulation): add keyActorRoles field to fix actor overlap bonus vocabulary mismatch

The +0.04 actor overlap bonus never reliably fired in production because
stateSummary.actors uses role-category strings ('Commodity traders',
'Policy officials') while simulation keyActors uses named geo-political
entities ('Iran', 'Houthi'). 53 production runs audited showed the bonus
fired once out of 53.

Fix: add keyActorRoles?: string[] to SimulationTopPath. The Round 2 prompt
now includes a CANDIDATE ACTOR ROLES section with theater-local role vocab
seeded from candidatePacket.stateSummary.actors. The LLM copies matching
roles into keyActorRoles. applySimulationMerge scores overlap against
keyActorRoles when actorSource=stateSummary, preserving the existing
keyActors entity-overlap path for the affectedAssets fallback.

- buildSimulationPackageFromDeepSnapshot: add actorRoles[] to each theater
  from candidate.stateSummary.actors (theater-scoped, no cross-theater noise)
- buildSimulationRound2SystemPrompt: inject CANDIDATE ACTOR ROLES section
  with exact-copy instruction and keyActorRoles in JSON template
- tryParseSimulationRoundPayload: extract keyActorRoles from round 2 output
- mergedPaths.map(): filter keyActorRoles against theater.actorRoles guardrail
- computeSimulationAdjustment: dual-path overlap — roleOverlapCount for
  stateSummary, keyActorsOverlapCount for affectedAssets (backwards compat)
- summarizeImpactPathScore: project roleOverlapCount + keyActorsOverlapCount
  into path-scorecards.json simDetail

New fields: roleOverlapCount, keyActorsOverlapCount in SimulationAdjustmentDetail
and ScorecardSimDetail. actorOverlapCount preserved as backwards-compat alias.

Tests: 308 pass (was 301 before). New tests T-P1/T-P2/T-P3 (prompt/parser),
T-RO1/T-RO2/T-RO3 (role overlap logic), T-PKG1 (pkg builder actorRoles),
plus fixture updates for T2/T-F/T-G/T-J/T-K/T-N2/T-SC-4.

🤖 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) <noreply@anthropic.com>

* fix(simulation): address CE review findings from PR #2582

- Add SimulationPackageTheater interface to seed-forecasts.types.d.ts
  (actorRoles was untyped under @ts-check)
- Add keyActorRoles to uiTheaters Redis projection in writeSimulationOutcome
  (field was stripped from Redis snapshot; only visible in R2 artifact)
- Extract keyActorRoles IIFE to named sanitizeKeyActorRoles() function;
  hoist allowedRoles Set computation out of per-path loop
- Harden bonusOverlap ternary: explicit branch for actorSource='none'
  prevents silent fallthrough if new actorSource values are added
- Eliminate roleOverlap intermediate array in computeSimulationAdjustment
- Add U+2028/U+2029 Unicode line-separator stripping to sanitizeForPrompt
- Apply sanitizeForPrompt at tryParseSimulationRoundPayload parse boundary;
  add JSDoc to newly-exported function

All 308 tests pass, typecheck + typecheck:api clean.

* fix(sim): restore const sanitized in sanitizeKeyActorRoles after early-return guard

Prior edit added `if (!allowedRoles.length) return []` but accidentally removed
the `const sanitized = ...` line, leaving the filter on line below referencing an
undefined variable. Restores the full function body:

  if (!allowedRoles.length) return [];
  const sanitized = (Array.isArray(rawRoles) ? rawRoles : [])
    .map((s) => sanitizeForPrompt(String(s)).slice(0, 80));
  const allowedNorm = new Set(allowedRoles.map(normalizeActorName));
  return sanitized.filter((s) => allowedNorm.has(normalizeActorName(s))).slice(0, 8);

308/308 tests pass.

---------

Co-authored-by: Claude Sonnet 4.6 (200K context) <noreply@anthropic.com>
2026-04-01 08:53:13 +04:00

2.3 KiB

status, priority, issue_id, tags, dependencies
status priority issue_id tags dependencies
pending p1 088
code-review
simulation
agent-native
observability
redis

keyActorRoles missing from uiTheaters Redis projection in writeSimulationOutcome

Problem Statement

writeSimulationOutcome builds a uiTheaters array written to Redis (SIMULATION_OUTCOME_LATEST_KEY) and returned by GetSimulationOutcomeResponse.theaterSummariesJson. The path map explicitly projects only pathId, label, summary, confidence, and keyActors — the new keyActorRoles field from PR #2582 is omitted. Even if the get-simulation-outcome RPC is eventually unblocked for agent access, keyActorRoles will always be undefined on every path because it is stripped at the Redis write stage.

Findings

  • Found by agent-native-reviewer during PR #2582 review
  • Location: scripts/seed-forecasts.mjs line ~16823 in writeSimulationOutcome
  • The uiTheaters path projection:
    .map((p) => ({
      pathId: p.pathId || '',
      label: p.label,
      summary: p.summary,
      confidence: p.confidence,
      keyActors: (p.keyActors || []).slice(0, 4),
      // keyActorRoles is NOT here
    }))
    
  • keyActorRoles exists in the full R2 artifact (simulation-outcome.json) but never reaches the Redis snapshot
  • The proto comment for theater_summaries_json in get_simulation_outcome.proto line 31 also needs updating

Proposed Solution

Add keyActorRoles to the uiTheaters path projection:

.map((p) => ({
  pathId: p.pathId || '',
  label: p.label,
  summary: p.summary,
  confidence: p.confidence,
  keyActors: (p.keyActors || []).slice(0, 4),
  keyActorRoles: (p.keyActorRoles || []).slice(0, 8),
}))

Update get_simulation_outcome.proto comment for theater_summaries_json to include keyActorRoles in the documented shape.

Technical Details

  • Files: scripts/seed-forecasts.mjs (uiTheaters map), proto/worldmonitor/forecast/v1/get_simulation_outcome.proto
  • Effort: Small | Risk: Low

Acceptance Criteria

  • keyActorRoles appears in uiTheaters path projection
  • redis-cli get forecast:simulation-outcome:latest | jq '.uiTheaters[0].topPaths[0].keyActorRoles' returns a non-null value after next sim run
  • Proto comment updated

Work Log

  • 2026-03-31: Identified by agent-native-reviewer during PR #2582 review