# tokenling shared rules `core/` is one Rust crate, `tokenling-core`, holding every rule the browser and the optional Rust host both apply. The host links it natively (`server/`); `core/wasm/` compiles it to WebAssembly for the page and its brain worker (`web/core.ts`). A pet therefore behaves the same in local and remote mode, and a transfer moves the same save between two copies of the same engine. `train/world.py` stays an independent Python rendering of the prompt contract for training; parity checks compare it with both builds. The crate does no I/O. Callers pass the time, a random source and the deployment's weather seed. Storage, networking, Web Locks, rendering, game physics and inference stay in each runtime, which only decides **when** to call the engine. ## What lives here | Module | Rules | |---|---| | `src/world.rs` | The trained prompt contract: closed word lists, chat/diary headers, lookups (`[clock]`, `[look]`, `[recall:…]`, `[yesterday]`), event lines, the tag grammar, `NOT_CUTE`, text cleaning, stop rules (`stop`, `lookup_tail`) and context fitting (`fit`, given the brain's token counter). | | `src/types.rs` | Save version 2 and the prompt's world state, with save validation. Unknown fields survive a round trip. | | `src/save.rs` | New saves, `VERSION`, `MIGRATIONS` (v1 → v2) and reading a stored or imported save; refusals carry `newer` for the bad egg. | | `src/calendar.rs` | Local date and hour. Natively: the save's `hosting.timeZone` (chrono-tz). In WebAssembly: the device clock (`Date`), so a local pet follows its owner's current zone. Moving to a host records that zone first. | | `src/body.rs` | Needs, poop, grime and sickness, collapsing and waking, growth stages, weather, the one-week catch-up cap and the alert projection. | | `src/turn.rs` | Brain requests (`Generation`: header, lines, prefix, budgets, temperature 0.85, top-k 40, top-p 0.92), the chat line loop with up to three lookups and three filtered tries, and diary pages (doodles, paragraphs, dropping unsafe pages). | | `src/engine.rs` | A running pet: the save plus its turn queue, disturbances, effects and accepted game. Owner commands (the host's JSON), the brain's tag effects, offers, the day log, dreams, silent-line trimming, turn scheduling (react, bedtime diary, idle, missed pages), commits and the "came back" notice. | `Engine` is serialized flattened into the host's stored rows, so their layout did not change when the engine moved here. In the browser the engine lives inside the WebAssembly module; `web/app/engine.ts` keeps a read-only snapshot in `store.state`, refreshing only the body fields on a plain tick. Effects (`emote`, `walk`, `want`, `dream`, `eat`, `bath`, `pat`, `sparkle`, `gift`, `remember`, `grew`, `sick`, `diary`, `sleep`, `woke`) are for the LCD and notices only. The save has already changed. Local changes and host snapshots play them through the same `playEffect()`. ## Build and checks Use Rust 1.88+. The browser build needs the `wasm32-unknown-unknown` target and `wasm-bindgen-cli` at exactly the version `core/wasm/Cargo.lock` pins; `core/wasm/build.sh` checks both and fails with the fix. Concurrent builds of one checkout wait for each other where `flock` exists, and the output directory is replaced by a rename. ```sh rustup target add wasm32-unknown-unknown cargo install wasm-bindgen-cli --version 0.2.129 --locked core/wasm/build.sh # core/wasm/pkg: module, ES glue, .d.ts (out of Git) cargo fmt --check --manifest-path core/Cargo.toml cargo fmt --check --manifest-path core/wasm/Cargo.toml cargo test --locked --manifest-path core/Cargo.toml node test/parity.mjs # WebAssembly build vs train/world.py cargo build --locked --manifest-path server/Cargo.toml --bin tokenling-contract node server/tests/parity.mjs # native build vs Python, then native vs WebAssembly replay ``` `site/build.sh` runs `core/wasm/build.sh` and copies `tokenling_core_bg.wasm` into the content-hashed `app//` directory beside `main.js` and `brain.js`. Both bundles load it from there, it enters the app hash, and the page lists it for offline use. The page shows an error instead of the app if the module cannot load. `server/tests/parity.mjs` replays a 31-step scenario natively (through `tokenling-contract`) and in WebAssembly with the same random draws. It covers chat with lookups and a filtered retry, offers, catch, an absence, sleep, dreams, both diary paths and growth. It runs in UTC and in a zone with a daylight-saving change, and requires identical engine JSON after every step. ## Size and speed (2026-10-10) Measured on one desktop with Node.js 24; not phone or download measurements. The release module is 454,762 bytes (167,674 with gzip). Node instantiated it in about 2–3 ms. Reading a deliberately large state (240 chat lines, 60 memories, 365 diary pages) took about 1.0–1.4 ms; the body-only patch read per tick about 0.01 ms, and one body advance about 0.01 ms (averages of 50 runs, three runs). The build is reproducible across checkouts: `core/wasm/build.sh` remaps the checkout, Cargo registry and toolchain paths that panic locations embed, and the release profile strips symbols. Builds of one commit from two different checkout paths produced identical module, glue and declaration files.