Files
worldmonitor/docs/api/ForecastService.openapi.yaml
Elie Habib 01f6057389 feat(simulation): MiroFish Phase 2 — theater-limited simulation runner (#2220)
* 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).
2026-03-25 13:55:59 +04:00

468 lines
17 KiB
YAML

openapi: 3.1.0
info:
title: ForecastService API
version: 1.0.0
paths:
/api/forecast/v1/get-forecasts:
get:
tags:
- ForecastService
summary: GetForecasts
operationId: GetForecasts
parameters:
- name: domain
in: query
required: false
schema:
type: string
- name: region
in: query
required: false
schema:
type: string
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetForecastsResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/forecast/v1/get-simulation-package:
get:
tags:
- ForecastService
summary: GetSimulationPackage
operationId: GetSimulationPackage
parameters:
- name: runId
in: query
description: Currently ignored; always returns the latest package. Reserved for Phase 3 per-run lookup.
required: false
schema:
type: string
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetSimulationPackageResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/forecast/v1/get-simulation-outcome:
get:
tags:
- ForecastService
summary: GetSimulationOutcome
operationId: GetSimulationOutcome
parameters:
- name: runId
in: query
description: |-
IMPORTANT: Currently a no-op. Always returns the latest available outcome regardless of runId.
Per-run lookup is reserved for Phase 3. Check the response 'note' field when runId is supplied
and you need to detect a mismatch between requested and returned run.
required: false
schema:
type: string
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetSimulationOutcomeResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Error:
type: object
properties:
message:
type: string
description: Error message (e.g., 'user not found', 'database connection failed')
description: Error is returned when a handler encounters an error. It contains a simple error message that the developer can customize.
FieldViolation:
type: object
properties:
field:
type: string
description: The field path that failed validation (e.g., 'user.email' for nested fields). For header validation, this will be the header name (e.g., 'X-API-Key')
description:
type: string
description: Human-readable description of the validation violation (e.g., 'must be a valid email address', 'required field missing')
required:
- field
- description
description: FieldViolation describes a single validation error for a specific field.
ValidationError:
type: object
properties:
violations:
type: array
items:
$ref: '#/components/schemas/FieldViolation'
description: List of validation violations
required:
- violations
description: ValidationError is returned when request validation fails. It contains a list of field violations describing what went wrong.
GetForecastsRequest:
type: object
properties:
domain:
type: string
region:
type: string
GetForecastsResponse:
type: object
properties:
forecasts:
type: array
items:
$ref: '#/components/schemas/Forecast'
generatedAt:
type: integer
format: int64
description: 'Warning: Values > 2^53 may lose precision in JavaScript'
Forecast:
type: object
properties:
id:
type: string
domain:
type: string
region:
type: string
title:
type: string
scenario:
type: string
feedSummary:
type: string
probability:
type: number
format: double
confidence:
type: number
format: double
timeHorizon:
type: string
signals:
type: array
items:
$ref: '#/components/schemas/ForecastSignal'
cascades:
type: array
items:
$ref: '#/components/schemas/CascadeEffect'
trend:
type: string
priorProbability:
type: number
format: double
calibration:
$ref: '#/components/schemas/CalibrationInfo'
createdAt:
type: integer
format: int64
description: 'Warning: Values > 2^53 may lose precision in JavaScript'
updatedAt:
type: integer
format: int64
description: 'Warning: Values > 2^53 may lose precision in JavaScript'
perspectives:
$ref: '#/components/schemas/Perspectives'
projections:
$ref: '#/components/schemas/Projections'
caseFile:
$ref: '#/components/schemas/ForecastCase'
ForecastSignal:
type: object
properties:
type:
type: string
value:
type: string
weight:
type: number
format: double
CascadeEffect:
type: object
properties:
domain:
type: string
effect:
type: string
probability:
type: number
format: double
CalibrationInfo:
type: object
properties:
marketTitle:
type: string
marketPrice:
type: number
format: double
drift:
type: number
format: double
source:
type: string
Perspectives:
type: object
properties:
strategic:
type: string
regional:
type: string
contrarian:
type: string
Projections:
type: object
properties:
h24:
type: number
format: double
d7:
type: number
format: double
d30:
type: number
format: double
ForecastCase:
type: object
properties:
supportingEvidence:
type: array
items:
$ref: '#/components/schemas/ForecastCaseEvidence'
counterEvidence:
type: array
items:
$ref: '#/components/schemas/ForecastCaseEvidence'
triggers:
type: array
items:
type: string
actorLenses:
type: array
items:
type: string
baseCase:
type: string
escalatoryCase:
type: string
contrarianCase:
type: string
changeSummary:
type: string
changeItems:
type: array
items:
type: string
actors:
type: array
items:
$ref: '#/components/schemas/ForecastActor'
worldState:
$ref: '#/components/schemas/ForecastWorldState'
branches:
type: array
items:
$ref: '#/components/schemas/ForecastBranch'
ForecastCaseEvidence:
type: object
properties:
type:
type: string
summary:
type: string
weight:
type: number
format: double
ForecastActor:
type: object
properties:
id:
type: string
name:
type: string
category:
type: string
role:
type: string
objectives:
type: array
items:
type: string
constraints:
type: array
items:
type: string
likelyActions:
type: array
items:
type: string
influenceScore:
type: number
format: double
ForecastWorldState:
type: object
properties:
summary:
type: string
activePressures:
type: array
items:
type: string
stabilizers:
type: array
items:
type: string
keyUnknowns:
type: array
items:
type: string
ForecastBranch:
type: object
properties:
kind:
type: string
title:
type: string
summary:
type: string
outcome:
type: string
projectedProbability:
type: number
format: double
rounds:
type: array
items:
$ref: '#/components/schemas/ForecastBranchRound'
ForecastBranchRound:
type: object
properties:
round:
type: integer
format: int32
focus:
type: string
developments:
type: array
items:
type: string
actorMoves:
type: array
items:
type: string
probabilityShift:
type: number
format: double
GetSimulationPackageRequest:
type: object
properties:
runId:
type: string
description: Currently ignored; always returns the latest package. Reserved for Phase 3 per-run lookup.
GetSimulationPackageResponse:
type: object
properties:
found:
type: boolean
runId:
type: string
pkgKey:
type: string
schemaVersion:
type: string
theaterCount:
type: integer
format: int32
generatedAt:
type: integer
format: int64
description: 'Unix timestamp in milliseconds (from Date.now()). Warning: Values > 2^53 may lose precision in JavaScript.. Warning: Values > 2^53 may lose precision in JavaScript'
note:
type: string
description: |-
Populated when req.runId was supplied but does not match the returned package's runId.
Indicates that per-run filtering is not yet active and the latest package was returned instead.
error:
type: string
description: |-
Populated when the Redis lookup failed. Distinguish from healthy not-found (found=false, error="").
Value: "redis_unavailable" on Redis errors.
GetSimulationOutcomeRequest:
type: object
properties:
runId:
type: string
description: |-
IMPORTANT: Currently a no-op. Always returns the latest available outcome regardless of runId.
Per-run lookup is reserved for Phase 3. Check the response 'note' field when runId is supplied
and you need to detect a mismatch between requested and returned run.
GetSimulationOutcomeResponse:
type: object
properties:
found:
type: boolean
runId:
type: string
outcomeKey:
type: string
schemaVersion:
type: string
theaterCount:
type: integer
format: int32
generatedAt:
type: integer
format: int64
description: 'Unix timestamp in milliseconds (from Date.now()). Warning: Values > 2^53 may lose precision in JavaScript.. Warning: Values > 2^53 may lose precision in JavaScript'
note:
type: string
description: |-
Populated when req.runId was supplied but does not match the returned outcome's runId.
Indicates that per-run filtering is not yet active and the latest outcome was returned instead.
error:
type: string
description: |-
Populated when the Redis lookup failed. Distinguish from healthy not-found (found=false, error="").
Value: "redis_unavailable" on Redis errors.