Files
worldmonitor/scripts/seed-forecasts.mjs
Elie Habib 8278c8e34e fix(forecasts): unwrap seed-contract envelope in canonical-key sim patcher (#3348)
* fix(forecasts): unwrap seed-contract envelope in canonical-key sim patcher

Production bug observed 2026-04-23 across both forecast worker services
(seed-forecasts-simulation + seed-forecasts-deep): every successful run
logs `[SimulationDecorations] Cannot patch canonical key — predictions
missing or not an array` and silently fails to write simulation
adjustments back to forecast:predictions:v2.

Root cause: PR #3097 (seed-contract envelope dual-write) wraps canonical
seed writes in `{_seed: {...}, data: {predictions: [...]}}` via runSeed.
The Lua patcher (_SIM_PATCH_LUA) and its JS test-path mirror both read
`payload.predictions` directly with no envelope unwrap, so they always
return 'MISSING' against the new shape — meeting the documented pattern
in the project's worldmonitor-seed-envelope-consumer-drift learning
(91 producers enveloped, private-helper consumers not migrated).

User-visible impact: ForecastPanel renders simulation-adjusted scores
only when a fast-path seed has touched a forecast since the bug landed;
deep-forecast and simulation re-scores never reach the canonical feed.

Fix:
  - _SIM_PATCH_LUA detects envelope shape (`type(payload._seed) == 'table'
    and type(payload.data) == 'table'`), reads `inner.predictions`, and
    re-encodes preserving the wrapper so envelope shape persists across
    patches. Legacy bare values still pass through unchanged.
  - JS test path mirrors the same unwrap/rewrap.
  - New test WD-20b locks the regression: enveloped store fixture, asserts
    `_seed` wrapper preserved on write + inner predictions patched.

Also resolves the per-run `[seed-contract] forecast:predictions missing
fields: sourceVersion — required in PR 3` warning by passing
`sourceVersion: 'detectors+llm-pipeline'` to runSeed (PR 3 of the
seed-contract migration will start enforcing this; cheap to fix now).

Verified: typecheck (both tsconfigs) clean; lint 0 errors; test:data
6631/6631 green (forecast suite 309/309 incl new WD-20b); edge-functions
176/176 green; markdown + version-check clean.

* fix(forecasts): tighten JS envelope guard to match Lua's strict table check

PR #3348 review (P2):

JS test path used `!!published._seed` (any truthy value) while the Lua
script requires `type(payload._seed) == 'table'` (strict object check).
Asymmetry: a fixture with `_seed: true`, `_seed: 1`, or `_seed: 'string'`
would be treated as enveloped by JS and bare by Lua — meaning the JS
test mirror could silently miss real Lua regressions that bisect on
fixture shape, defeating the purpose of having a parity test path.

Tighten JS to require both `_seed` and `data` be plain objects (rejecting
truthy non-objects + arrays), matching Lua's `type() == 'table'` semantics
exactly.

New test WD-20c locks the parity: fixture with non-table `_seed` (string)
+ bare-shape `predictions` → must succeed via bare path, identical to
what Lua would do.

Verified: 6632/6632 tests pass; new WD-20c green.
2026-04-23 20:38:11 +04:00

774 KiB