Files
worldmonitor/docs/api/ForecastService.openapi.yaml
Elie Habib f87c8c71c4 feat(forecast): Phase 2 simulation package read path (#2219)
* feat(forecast): Phase 2 simulation package read path (getSimulationPackage RPC + Redis existence key)

- writeSimulationPackage now writes forecast:simulation-package:latest to Redis after
  successful R2 write, containing { runId, pkgKey, schemaVersion, theaterCount, generatedAt }
  with TTL matching TRACE_REDIS_TTL_SECONDS (60 days)
- New getSimulationPackage RPC handler reads Redis key, returns pointer metadata without
  requiring an R2 fetch (zero R2 cost for existence check)
- Wired into ForecastServiceHandler and server/gateway.ts cache tier (medium)
- Proto: GetSimulationPackage RPC + get_simulation_package.proto message definitions
- api/health.js: simulationPackageLatest added to STANDALONE_KEYS + ON_DEMAND_KEYS
- Tests: SIMULATION_PACKAGE_LATEST_KEY constant + writeSimulationPackage null-guard test

Closes todo #017 (Phase 2 prerequisites for MiroFish integration)

* chore(generated): regenerate proto types for GetSimulationPackage RPC

* fix(simulation-rpc): distinguish Redis failure from not-found; signal runId mismatch

- Add `error` field to GetSimulationPackageResponse: populated with
  "redis_unavailable" on Redis errors so callers can distinguish a
  healthy not-found (found=false, error="") from a Redis failure
  (found=false, error="redis_unavailable"). Adds console.warn on error.
- Add `note` field: populated when req.runId is supplied but does not
  match the latest package's runId, signalling that per-run filtering
  is not yet active (Phase 3).
- Add proto comment on run_id: "Currently ignored; reserved for Phase 3"
- Add milliseconds annotation to generated_at description.
- Simplify handler: extract NOT_FOUND constant, remove SimulationPackagePointer
  interface, remove || '' / || 0 guards on guaranteed-present fields.
- Regenerate all buf-generated files.

Fixes todos #018 (runId silently ignored) and #019 (error indistinguishable
from not-found). Also resolves todos #022 (simplifications) and #023
(OpenAPI required fields / generatedAt unit annotation).

* fix(simulation-rpc): change cache tier from medium to slow (aligns with deep-run update frequency)

* fix(simulation-rpc): fix key prefixing, make Redis errors reachable, no-cache not-found

Three P1 regressions caught in external review:

1. Key prefix bug: getCachedJson() applies preview:<sha>: prefix in non-production
   environments, but writeSimulationPackage writes the raw key via a direct Redis
   command. In preview/dev the RPC always returned found:false even when the package
   existed. Fix: new getRawJson() in redis.ts always uses the unprefixed key AND throws
   on failure instead of swallowing errors.

2. redis_unavailable unreachable: getCachedJson swallows fetch failures and missing-
   credentials by returning null, so the catch block for redis_unavailable was dead
   code. getRawJson() throws on HTTP errors and missing credentials, making the
   error: "redis_unavailable" contract actually reachable.

3. Negative-cache stampede: slow tier caches every 200 GET. A request before any deep
   run wrote a package returned { found:false } which the CDN cached for up to 1h,
   breaking post-run discovery. Fix: markNoCacheResponse() on both not-found and
   error paths so they are served fresh on every request.
2026-03-24 22:45:22 +04:00

396 lines
14 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'
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.