Files
arena/environments/tera_spatial/README.md
T
kartiandClaude Opus 5 162d67a83c tera-spatial: an NDJSON replay bridge to the TypeScript spatial environments
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>
2026-08-21 15:55:12 -07:00

56 lines
2.5 KiB
Markdown

# 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
```bash
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
```bash
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.