This implements part of roadmap milestone 1. The current player uses the scheduler, declarative Transform schema and bounded event stream. These APIs are development contracts; the complete SDK and network schema negotiation remain later gates.
#Storage, identities and mutation
EntityStore keeps entity generations and dense/sparse membership in Uint32Array storage. Component rows are compacted by moving the last row into a removed row and repairing its sparse index. Queries start with the smallest required component set, intersect the other sets, and emit stable code-unit ID order. Dense row order is an internal detail and never changes canonical iteration or snapshots.
Fixed required schemas (including nested tuples, quaternions and closed objects) compile into structure-of-arrays columns: Float64Array preserves JavaScript numeric precision and Uint8Array stores booleans. Layouts have at most 256 flattened leaves. Strings, dynamic arrays and optional subrecords can occupy isolated reference columns alongside numeric fields. Entirely nonnumeric, optional-root, oversized-layout and legacy schemas retain dense structured rows. All values enter through the validated write API; reads, snapshots and reader batches return isolated values. Mutable arrays are not exposed as a second mutation path. The player's Transform v2 uses ten numeric columns; CharacterMotor uses five numeric/boolean columns and a platform-reference column. The component catalog specifies the explicit Transform migration; the player save now upgrades to format 2 as specified by the time contract.
new EntityStore({maxEntities, retention}) bounds runtime slots (1–65,536, default 65,536) and retained journal entries (1–65,536, default 256). The old numeric constructor argument still specifies retention. Arrays grow geometrically and retain their high-water capacity across ordinary removal/reuse; a successful restore replaces them with newly prepared stores. storageStats() reports live/allocated/retired slots, typed bytes and per-component rows/capacities. Typed byte counts exclude JavaScript objects, strings and journal payloads; these are not whole-process memory measurements or a global byte quota.
Destroyed slots advance their uint32 generation. A slot at generation 4,294,967,295 is permanently retired instead of wrapping. Retired slots count toward the lifetime slot budget; exhausting capacity rejects creation before identity or cursor mutation. remove(handle, component) emits a component tombstone while keeping the entity alive. Repeated removal of an absent component is inert. Entity destruction emits an $entity tombstone and releases all component rows. Creation, destruction, component insertion/removal, registration and restore cannot occur inside active queries; updates to existing component values remain allowed.
The independent-reader journal is a fixed-capacity ring with exact uint64 sequences. Writes preflight sequence exhaustion before changing canonical state. Eviction advances the earliest readable cursor; a slow, negative or future cursor receives a complete current baseline. Restoring a validated baseline publishes one new sequence and invalidates earlier reader cursors, rather than exposing partial destroy/create traffic. Consumers resume from that reset cursor. History at the uint64 maximum cannot accept another write or reset; it never wraps into older sequence identities.
Snapshot installation prepares replacement identities, generations and component columns before swapping live state. Validation/capacity failures leave existing handles, cursors and deferred commands unchanged. A successful restore invalidates old handles and discards old deferred commands. This is an ECS baseline operation; simulation/controller checkpoints use the separate current runtime checkpoint profile, while durable world transactions remain later work. Restore retains the store's current commit tick; historical simulation time is installed in a new runtime through the full checkpoint API.
The persistent entity identity contract defines UUID allocation at trusted command ingress, recorded replay identities and the installed final-state identity invariant. Spawned IDs survive complete simulation checkpoints without allocator state. Authored compiler mapping and durable request/tombstone retention remain separate work.
#Atomic mutation batches
transact(operations, {tick, transaction}) stages up to 4,096 operations and 4,096 resulting journal rows. Operations are ordered create, set, remove or destroy records; they reject unknown fields. Creation can include components, ordered by component name. Entity targets use a stable ID or a generation-checked handle. An ID can target a creation earlier in the same batch; an old handle cannot target a replacement created after deletion. Inputs are copied, and there is no mutable draft exposed to callers.
A transaction clones identity tables and copies only touched component stores. It validates each component and all installed final-state invariants through either isolated snapshots or scoped candidate views, prepares the bounded journal, then swaps live state synchronously without callbacks. Any validation, capacity, sequence or invariant failure leaves values, allocations, generations, cursor and queued commands unchanged. Successful updates preserve unaffected handles. Intermediate states such as a child's place changing before its parent's are private to the commit.
tick is a nondecreasing uint64 bigint; several transactions can share a tick. transaction is a nonempty label of at most 256 code units, not an idempotency key or authorization credential. Journal rows expose commit: {first, last, tick, transaction}, with all uint64 fields serialized as decimal strings. A read returns complete batches only. A cursor inside a batch receives a current baseline; eviction removes entire batches. A batch larger than retention commits successfully but invalidates older cursors, requiring a baseline. Sequence capacity is preflighted for the whole batch. Empty/no-effect transactions can advance the accepted tick but create no journal rows.
Direct writes publish single-row batches with the last accepted tick (initially zero) and an ecs.<sequence> label. Once an invariant is installed, they also use staged validation. Runtime systems should use an explicit named transaction for related writes. The player publishes Transform, CharacterMotor and any changed PlaceMembership together under player.state.<tick>.
addInvariant(id, callback) retains the complete isolated-snapshot interface. addViewInvariant(id, callback) gives trusted synchronous validators a scoped read-only candidate view (ids(...components), has(id, component?), get(id, component)) and frozen changed-key metadata. Both kinds share the same ID namespace, budget and stable callback order. Callbacks cannot mutate the live store during validation. Returned values are independent copies; retaining a view does not retain usable access to the candidate. Every view expires and releases its store reference when its callback returns or throws. Async functions and returned thenables are rejected.
Changed keys include entity lifecycle markers and component tombstones, in mutation order, without payloads or writable handles. Reads always observe the final candidate, not intermediate operations. null keys require complete validation when a view is installed or a baseline is restored; an empty array means an ordinary no-effect commit. A dependency-aware validator is responsible for checking every relationship affected by the keys. Snapshot validators still run over complete isolated copies; the snapshot is materialized lazily only if such a validator is installed. No accepted validation cache or post-commit callback is needed.
The installed identity validator checks changed identities/IDs. The spatial validator performs full forest validation on creation, destruction, place/parent changes, Transform removal and restores. Local Transform edits validate all affected descendants and their complete ancestor chains using the same float64 resolver, including depth/composed bounds. It scans the sparse Parent column to find descendants, so cost still scales with the number of parent edges and the affected subtree. Unrelated roots are not materialized. Nonspatial writes leave an already validated forest intact. The player installs both validators at startup. Generic stores without them enforce component shapes only.
deferTransaction copies a batch for a structural barrier. Each queued transaction commits independently; a later failure does not undo earlier accepted transactions. Structural transactions are rejected during queries; batches containing only updates to already present components remain allowed. Legacy defer callbacks remain trusted commands, not an automatically atomic group. Whole-tick rollback, physics/resource rollback, external effects, durable ACKs and hostile script isolation are outside this ECS transaction guarantee.
Staging still copies identity arrays and touched component columns. Topology and baseline edits validate the full spatial forest; local pose edits validate the affected closure. Scratch allocations are bounded by store limits but occur per transaction; retained typed-byte statistics do not prove allocation-free commits. The canonical mutation benchmark measures the flat-scene path, not general hierarchy or multiplayer capacity. The standalone setParent/setLocalTransform helpers retain their full preflight so they also guard stores without installed invariants.
#Systems and barriers
SystemScheduler<Context> compiles a list of systems once. Each system has a stable ID, a phase, resource reads/writes, optional before/after dependencies and an optional integer everyTicks cadence. Phases execute in this order:
ingress → transactions → input → behavior → physics → interaction → finalize → publish
Phase order is part of the dependency graph. Missing dependencies, duplicate IDs, cycles, phase inversions and unordered accesses involving a writer fail before any system runs. Independent systems use code-unit ID ordering, independent of registration order or locale. Cadence uses the exact bigint tick modulo; missed ticks do not create an unbounded lower-frequency backlog.
Structural commands apply at the initial barrier, phase boundaries and final barrier. A dependency within a phase orders execution but does not insert a structural barrier. Put entity creation in the transactions phase when later systems need those entities. Queries release their iteration guard on completion or early exit.
Frames have a strictly increasing uint64 bigint tick and finite positive dt no larger than one second. The scheduler freezes its copy of the frame. Application code supplies the fixed rate; the existing player remains at 30 Hz. Optional injected timing records per-system wall time separately from simulation state.
Systems are synchronous trusted code. Async functions and returned promises are rejected. Access declarations establish ordering; they do not isolate hostile code. A system/barrier fault discards queued structural commands and parks that scheduler permanently. Earlier direct writes and physics steps are not rolled back. The caller must stop publication and restore a committed checkpoint in a new runtime. This is a fault boundary, not the transactional creator-script runtime promised by milestone 13.
#Component metadata and versioned snapshots
defineComponent<T> accepts a declarative descriptor with:
| Field | Contract |
|---|---|
name, version |
Stable component name and integer version, 1–1024 |
persistence |
authored, persistent, session, or ephemeral |
visibility |
public, owner, server, or local |
authority |
player, place, trusted, or local |
predicted |
Whether the component belongs in prediction |
schema |
Finite number, bounded string/array, boolean, string enum, fixed tuple, unit quaternion, canonical uint64 string or closed object |
The registry clones and freezes descriptors and generates validators. Objects reject undeclared fields; optional fields may be absent but cannot contain undefined. Integer fields require safe JavaScript integers. Arbitrary uint64 values must use explicit string/binary sequence contracts, not numeric fields. Privacy/authority metadata is available to future replication/authorization consumers; declaring it alone does not enforce network privacy.
schemaManifest() returns an isolated, name-sorted dictionary. schemaFingerprint() hashes canonical JSON of {format: 1, components: [...]} using SHA-256. Golden identities are checked in tests/components.test.ts. Canonical serialization sorts object keys by code units and rejects non-finite numbers, sparse/undefined entries, cycles, non-JSON objects, excessive depth and oversized traversal. It does not round runtime values.
exportSnapshot() emits {format: 1, schemas, entities} with all registered component contracts and current ECS values. This is an ECS baseline, not a complete physics/controller/room checkpoint. It is deliberately separate from the local player save and relay room checkpoint formats. Legacy custom validators can still support local stores, but cannot export a public versioned snapshot without a declarative schema.
Migrations name the complete old descriptor and advance exactly one version. Supplied paths must reach the current version. The old payload and each intermediate result are validated. Unknown versions, contract mismatches (including metadata), invalid values or an exception reject the entire baseline before live entities, cursors or handles change. Migration functions operate on copies and are trusted synchronous code. They must not perform external effects.
Successful restore invalidates old handles, discards commands staged against the old baseline, and installs validated entities. Missing migrations never silently coerce old data. tests/fixtures/components-v1.json and components-v2.json are the first golden migration corpus.
#Exact time and compatibility
Simulation state and input sequences are canonical decimal uint64 strings at serialization boundaries, with bigint arithmetic and comparisons inside runtime code. The wall-clock accumulator retains floating-point seconds and a bounded catch-up count; its diagnostic step count is bigint. Exhaustion never wraps. Local save format 1 upgrades losslessly to format 2, while full profile-1 checkpoints require explicit migration and independent native validation. The time contract records format changes, input ordering, retained legacy browser saves and the supported replay evidence.
#Ephemeral events
TickEventStream<T> has an injected payload validator, bounded retained event count and maximum lifetime. A producer advances the monotonic simulation tick, then emits an isolated payload. Events expose decimal uint64 sequence, tick and expiry. They remain visible to independent readers until currentTick >= expiresAt; a read does not consume another reader's copy. Readers advance their own cursors. Expired events are never replayed as new activity.
Overflow rejects the new event without evicting existing live events or consuming a sequence number. A producer may call retireThrough after every intended reader has acknowledged a cursor. The player has a single presentation consumer: it retains at most 256 events, validates text/dialogue sizes, gives them a 30-tick lifetime, and retires them after draining. Pausing simulation also pauses tick-based expiry, but already consumed messages cannot accumulate during a long menu session. Durable rewards and ownership changes must use the durable transaction path, never this stream.
#Package dependency checks
npm run check combines TypeScript with tools/cli/check-boundaries.ts. It examines imports, re-exports and dynamic imports in engine packages. Packages cannot import games/, apps/, services/ or undeclared external dependencies. Simulation cannot import the renderer or browser socket client; the current pure protocol data types are an explicit type-only exception. New packages require an explicit dependency entry.
This is a development architecture check, not a hostile-code sandbox. Headless behavior tests also execute the real simulation without DOM shims. The later isolation and numerical compatibility gates remain open.
#Verification and remaining work
Run npm run check, npm test, npm run validate, npm run build and npm run test:browser. npm run bench -- OUTPUT.json records per-system percentiles and the source Git identity in addition to total simulation cost. This workload is one player, not the roadmap's 16-player/128-body capacity fixture.
npm run bench:storage -- OUTPUT.json measures an isolated 8,192-entity fixture: 128-row validated numeric updates, a full canonical query, a 128-row atomic commit and 12,800 destroy/recreate operations. It asserts that typed allocation reaches a stable high-water mark, records source/machine/runtime metadata and hashes the final canonical snapshot. This does not establish complete world capacity. tests/storage.test.ts also checks sparse growth/swap removal, precise numeric values, component tombstones, bounded slot reuse, atomic restore, uint64 exhaustion and seeded reference-model equivalence.
The initial component catalog, local transform migration, hierarchy validation and a shared Node/browser transform fixture now have dedicated contracts and golden evidence. Atomic batches and installed spatial invariants are covered by rejection/restore tests and a 600-step fast/slow-reader reference workload in tests/transactions.test.ts. The current simulation now has complete profile checkpoints and a shared Node/browser replay fixture. Spawned-ID allocation and its shared Node/browser fixture now have a recorded identity contract. Exact uint64 runtime time and legacy migrations now have a time contract. Milestone 1 still needs broader replay/mutation coverage, generated protocol integration and the complete identity acceptance audit. Milestone 0's physical reference-device measurements remain open; the render-worker probe has separate evidence. See the full program tracker; no release gate is reduced to this foundation increment.