mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
* 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>
2.9 KiB
2.9 KiB
status, priority, issue_id, tags, dependencies
| status | priority | issue_id | tags | dependencies | ||||
|---|---|---|---|---|---|---|---|---|
| pending | p2 | 089 |
|
Extract keyActorRoles guardrail IIFE to named sanitizeKeyActorRoles function
Problem Statement
The keyActorRoles guardrail in mergedPaths.map() is implemented as an 8-line IIFE. The logic is non-trivial (sanitize → check allowlist via normalizeActorName → filter), runs once per path per theater, and is currently untestable in isolation. IIFEs of this complexity violate the project's "extractable at natural boundaries" principle. Additionally, allowed.map(normalizeActorName) is recomputed fresh for every path, even though theater.actorRoles is constant across all paths in a theater.
Findings
- Flagged by kieran-typescript-reviewer (HIGH) and code-simplicity-reviewer independently
- Performance:
allowedNormSet rebuilt per path (up to 5 paths × 12 roles = 60 redundant normalizeActorName calls per theater) - The IIFE has no dedicated test — the guardrail filter behavior is only exercised implicitly through T-K (which covers the stateSummary path)
- The sanitize-before-filter ordering could cause a latent mismatch:
sanitizeForPrompt(s).slice(0,80)is applied beforenormalizeActorName(s)comparison (normalizeActorName normalizes the truncated string, which is correct — but the intent is clearer with filter-first)
Proposed Solution
Extract to a named function near the other sim helpers:
/**
* @param {string[] | undefined} rawRoles - LLM-returned role strings from keyActorRoles
* @param {string[]} allowedRoles - theater.actorRoles allowlist (empty = no semantic filter)
* @returns {string[]}
*/
function sanitizeKeyActorRoles(rawRoles, allowedRoles) {
const sanitized = (Array.isArray(rawRoles) ? rawRoles : [])
.map((s) => sanitizeForPrompt(String(s)).slice(0, 80));
if (!Array.isArray(allowedRoles) || allowedRoles.length === 0) return sanitized.slice(0, 8);
const allowedNorm = new Set(allowedRoles.map(normalizeActorName));
return sanitized.filter((s) => allowedNorm.has(normalizeActorName(s))).slice(0, 8);
}
Then in mergedPaths.map():
keyActorRoles: sanitizeKeyActorRoles(p.keyActorRoles, theater.actorRoles),
And hoist (optional but clean):
const allowedRoles = Array.isArray(theater.actorRoles) ? theater.actorRoles : [];
// pass allowedRoles to sanitizeKeyActorRoles for all paths in this theater
Technical Details
- Files:
scripts/seed-forecasts.mjs - Effort: Small | Risk: Low
Acceptance Criteria
sanitizeKeyActorRolesis a named function (not IIFE)allowedNormSet not recomputed per path whenallowedis constant per theaternpm run test:datapasses- New unit test for
sanitizeKeyActorRoles(allowlist match, allowlist miss, empty allowed)
Work Log
- 2026-03-31: Identified by kieran-typescript-reviewer and code-simplicity-reviewer during PR #2582 review