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>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user