Files
worldmonitor/docs/api/ForecastService.openapi.json
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

1 line
8.0 KiB
JSON

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