* feat(simulation): MiroFish Phase 2 — theater-limited simulation runner Adds the simulation execution layer that consumes simulation-package.json and produces simulation-outcome.json for maritime chokepoint + energy/logistics theaters, closing the WorldMonitor → MiroFish handoff loop. Changes: - scripts/seed-forecasts.mjs: 2-round LLM simulation runner (prompt builders, JSON extractor, runTheaterSimulation, writeSimulationOutcome, task queue with NX dedup lock, runSimulationWorker poll loop) - scripts/process-simulation-tasks.mjs: standalone worker entry point - proto: GetSimulationOutcome RPC + make generate - server/worldmonitor/forecast/v1/get-simulation-outcome.ts: RPC handler - server/gateway.ts: slow tier for get-simulation-outcome - api/health.js: simulationOutcomeLatest in STANDALONE + ON_DEMAND keys - tests: 14 new tests for simulation runner functions * fix(simulation): address P1/P2 code review findings from PR #2220 Security (P1 #018): - sanitizeForPrompt() applied to all entity/seed fields interpolated into Round 1 prompt (entityId, class, stance, seedId, type, timing) - sanitizeForPrompt() applied to actorId and entityIds in Round 2 prompt - sanitizeForPrompt() + length caps applied to all LLM array fields written to R2 (dominantReactions, stabilizers, invalidators, keyActors, timingMarkers) Validation (P1 #019): - Added validateRunId() regex guard - Applied in enqueueSimulationTask() and processNextSimulationTask() loop Type safety (P1 #020): - Added isOutcomePointer() and isPackagePointer() type guards in TS handlers - Replaced unsafe as-casts with runtime-validated guards in both handlers Correctness (P2 #022): - Log warning when pkgPointer.runId does not match task runId Architecture (P2 #024): - isMaritimeChokeEnergyCandidate() accepts both flat and nested topBucketId - Call site simplified to pass theater directly Performance (P2 #025): - SIMULATION_ROUND1_MAX_TOKENS raised 1800 to 2200 - Added max 3 initialReactions instruction to Round 1 prompt Maintainability (P2 #026): - Simulation pointer keys exported from server/_shared/cache-keys.ts - Both TS handlers import from shared location Documentation (P2 #027): - Strengthened runId no-op description in proto and OpenAPI spec * fix(todos): add blank lines around lists in markdown todo files * style(api): reformat openapi yaml to match linter output * test(simulation): add flat-shape filter test + getSimulationOutcome handler coverage Two tests identified as missing during PR #2220 review: 1. isMaritimeChokeEnergyCandidate flat-shape tests — covers the || candidate.topBucketId normalization added in the P1/P2 review pass. The existing tests only used the nested marketContext.topBucketId shape; this adds the flat root-field shape that arrives from the simulation-package.json JSON (selectedTheaters entries have topBucketId at root). 2. getSimulationOutcome handler structural tests — verifies the isOutcomePointer guard, found:false NOT_FOUND return, found:true success path, note population on runId mismatch, and redis_unavailable error string. Follows the readSrc static-analysis pattern used elsewhere in server-handlers.test.mjs (handler imports Redis so full integration test would require a test Redis instance).
3.0 KiB
status, priority, issue_id, tags
| status | priority | issue_id | tags | ||||
|---|---|---|---|---|---|---|---|
| complete | p2 | 026 |
|
Redis key strings duplicated between TS handler and MJS seed script
Problem Statement
SIMULATION_OUTCOME_LATEST_KEY = 'forecast:simulation-outcome:latest' is defined independently in both server/worldmonitor/forecast/v1/get-simulation-outcome.ts and scripts/seed-forecasts.mjs. The same duplication exists for SIMULATION_PACKAGE_LATEST_KEY. server/_shared/cache-keys.ts (referenced in the worldmonitor-bootstrap-registration pattern) exists for exactly this purpose: shared Redis key constants that TypeScript handlers and seed scripts need to agree on. A future rename in one file without the other produces a silent miss where the handler reads an empty key forever.
Findings
F-1:
// server/worldmonitor/forecast/v1/get-simulation-outcome.ts line 10
const SIMULATION_OUTCOME_LATEST_KEY = 'forecast:simulation-outcome:latest';
// scripts/seed-forecasts.mjs line 35
const SIMULATION_OUTCOME_LATEST_KEY = 'forecast:simulation-outcome:latest';
// Two independent definitions with no enforcement of consistency
F-2: Same pattern for SIMULATION_PACKAGE_LATEST_KEY between get-simulation-package.ts and seed-forecasts.mjs.
Proposed Solutions
Option A: Move keys to server/_shared/cache-keys.ts, import in handler (Recommended)
// server/_shared/cache-keys.ts — add:
export const SIMULATION_OUTCOME_LATEST_KEY = 'forecast:simulation-outcome:latest';
export const SIMULATION_PACKAGE_LATEST_KEY = 'forecast:simulation-package:latest';
// server/worldmonitor/forecast/v1/get-simulation-outcome.ts — replace local const:
import { SIMULATION_OUTCOME_LATEST_KEY } from '../../../_shared/cache-keys';
The seed script (scripts/seed-forecasts.mjs) keeps its own definition since it's a standalone MJS module that cannot import from TypeScript source. But the TypeScript handler becomes the downstream consumer of a canonical definition, making renames TypeScript-checked.
Effort: Small | Risk: Low
Option B: Add a comment cross-referencing both locations
Not a fix, but documents the relationship so a human renaming one knows to update the other. Use as a stopgap if Option A causes import complexity.
Acceptance Criteria
SIMULATION_OUTCOME_LATEST_KEYexported fromserver/_shared/cache-keys.tsget-simulation-outcome.tsimports fromcache-keys.tsinstead of local constSIMULATION_PACKAGE_LATEST_KEYmoved simultaneouslyget-simulation-package.tsupdated to import fromcache-keys.ts- TypeScript compilation clean after change
Technical Details
- Files:
server/worldmonitor/forecast/v1/get-simulation-outcome.ts:10,server/worldmonitor/forecast/v1/get-simulation-package.ts:~10,server/_shared/cache-keys.ts - Scripts keep their own definitions (they're standalone MJS — can't import from TS source)
Work Log
- 2026-03-24: Found by compound-engineering:review:kieran-typescript-reviewer in PR #2220 review