Tera's four (now five) spatial environments already implement a Gym-style contract with checksums, train/dev splits and a replay gate — in TypeScript, unreachable from `uv run eval`. This bridges them rather than porting them: a resident NDJSON worker over stdio drives the vendored TS closure, and no reward arithmetic exists in Python anywhere in the package, including the denominator. Correctness is replay, not assertion: every scenario x seed must round-trip through Python and replay to an identical FNV-1a-64 checksum. 32/32 do, and 52 tamper attempts are rejected, 24 of them re-sealed with Tera's own checksum so it is replay() catching divergence rather than the seal. Lives in arena rather than tera because tera has no Python toolchain. That overturns the earlier plan's claim that this workstream shares no files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2.5 KiB
tera-spatial
Arena's bridge to Tera's four renderer-independent spatial environments.
The environments themselves live in the tera repository, in TypeScript, as
tera.arena/v1. They are not reimplemented here and they never will be. This
package vendors their import closure, runs it in a resident node worker, and
transports results. The rule the whole thing is built around:
Never recompute in Python a number that came out of TypeScript.
Rewards, per-step state checksums, scenario materialisation and the four baseline returns that every reward is normalised against are all computed once, inside Tera, and only ever carried across the pipe.
What is here
| Path | What it is |
|---|---|
tera_spatial/worker.mjs |
the resident NDJSON worker: reset, step, snapshot, restore, trace, replay, oracle, checksum |
tera_spatial/bridge.py |
TeraWorker / TeraEpisode — transport, and nothing else |
tera_spatial/vendor/tera/ |
the 23-file, 283 KB import closure of src/arena/index.ts |
tera_spatial/hashes.py |
SHA-256 of all 23, generated; the worker refuses to start if the tree has drifted |
tera_spatial/closure.py |
the import walk both the sync script and the gate use |
scripts/sync_tera.py |
reproduces the vendoring from a tera checkout |
tests/test_replay.py |
the replay gate |
The tasksets are not here yet. This is the bridge and its correctness proof.
The gate
uv run python -m unittest discover -s environments/tera_spatial/tests -v
32 episodes — 4 environments x 4 public scenarios x 2 seeds — each driven from
Python one JSON step at a time, then replayed in a fresh TypeScript environment.
All 32 must reach an identical FNV-1a-64 checksum. Then seven forgeries per
environment, each tried twice: raw, where the envelope checksum catches it, and
re-sealed with a checksum Tera itself recomputed, where only replay()
re-running the simulator can. All must be rejected.
node >= 22.18 is required — the vendored sources are raw .ts and are
type-stripped, not compiled. Set TERA_NODE to point at a specific binary.
Re-vendoring
uv run python scripts/sync_tera.py --tera ~/repos/gitea/tera # copy + regenerate hashes
uv run python scripts/sync_tera.py --check # CI: is the tree clean?
The closure is walked, not declared, so a new import in tera travels with it.
A bare specifier is a hard failure: the wheel has no node_modules and a
simulator that needs one is not a simulator that ships.