Every fork the model page waited on is ruled, so its cells now carry the rulings. GP1 confirms at runtime that the database server applies a put from a peer that carries no identity. F13-F17 record what the remote write authority cut stands on: peers have no identity, refusal codes are a coordinated cross-runtime enum, forwarding re-sends as a new client, the door has no consumer outside CultLib, and the Rust document server has the same gap. The cut sequence splits authority collapse into its remaining lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
24 KiB
CultMesh garden path: map
Campaign cultmesh-garden-path, stage 1 (server-authoritative garden path for the
orbs .io game). Target: docs/cultmesh-garden-path-target.md (typed as
cultmesh-garden-path:target:r2). Rulings, questions and follow-ups are documents
in the mind; this page holds the model page, body facts and the stage 1 cut lines.
Written by Imagination (imagination-gp-0b, session self-2026-10-27-eureka)
against CultLib d1bf21b3. Refreshed by imagination-gp-stage1 (session
self-2026-10-27c-eureka) against 51ca81a1: every fork the page waited on is
ruled, and the cells below carry the rulings.
Model page
One row per kind. "Store" says where it lives: a CultCache document in the room's store, or transient per-tick data that never enters a store. Proposed values are marked as such; they are the design this page offers for the cuts, not landed behaviour. A cell that waits on an operator question names it.
The governing split, from state-is-cultcache and the CultCache scope restraint:
every value that crosses a boundary is CultCache-encoded (one serialisation model,
no JSON), but only state that outlives a tick is a record in a store. Inputs, ticks
and snapshots are typed CultNet message payloads, not store records. The store
receives exactly one write set per tick (ruled tick-state-commit: tick-batch).
| Kind | Store | Identity (what names it) | Lifecycle (what happens to it) | Authority (who decides; forbidden writers) |
|---|---|---|---|---|
| Game definition (documents, systems, authority declaration, tick rate) | Proposed: one CultCache document per server process (gamecult.mesh.game_definition), read-only to everyone but the server's startup. |
Game id string from code ("orbs"), namespaced by Verse id. The id alone is not injective (two builds share it), so proposed identity is (game id, definition hash), the hash derived from the lowered definition: document schema ids and versions, system ids, order and marks ([Predicted], later [Adjudicable]), tick rate, authority declaration. Schema ids (orbs.player) are global strings; the game prefix is convention, not enforced (body fact F9). |
Created at server startup from code; never revised at runtime; a new build supersedes it with a new hash. Clients carry their own compiled hash (C# in wasm, ruling browser-sim-runtime) and are refused at join on mismatch. A declaration a mode cannot honour is refused at startup (modes-do-not-leak). Never replayed. |
The game's code, through the builder, at startup; the runtime validates. Forbidden: clients, any runtime mutation, Odin (Odin may read and index it, never write it). |
| Game document (Player, Pellet, Shot) | CultCache documents in the room store. | (schema id, record key). Today the key is a free string the caller picks; [CultIndex] Id in the illustration is an index, not the key. Proposed: the runtime mints keys. Player key derives from the session principal (one Player per principal per room). Spawned documents (Shot, Pellet) key on (spawner key, tick, spawn ordinal within that tick), so a client's predicted spawn and the server's authoritative spawn derive the same key and reconcile instead of duplicating. That is injective as long as spawn order within a system step is deterministic for the predicting client, which the predicted system's own code gives under one runtime (C# both sides). The developer's Id field becomes redundant; whether to keep it as a mirror of the key is an API call for the cut, not an operator fork. |
Created by a system during a tick, or by the join system (Player). Revised at most once per tick, in that tick's commit. Deleted by a system (pellet eaten) or by the game's leave system after a session ends. Across reconnect within the grace window the Player record is unchanged; only the session's connection binding moves. Across a server restart: gone (ruled room-durability: ephemeral; a restart is a new room epoch at tick 0). Replayed only on the client, during reconciliation; the server never replays in stage 1. |
The server tick runner on the room's primary is the one writer of committed state, and systems write only through the tick's working set. Forbidden writers: client document puts through CultNetDatabaseServer.HandlePutAsync (today accepted from any peer, F1); PutPredictedAsync and CultMeshGameSession.PredictAsync on any networked store; game code calling database APIs; the simulation-fact committer (F7). |
| Player input | Transient. A CultCache-encoded payload on a CultNet message; never a store record in stage 1. | (session principal, input sequence), the sequence monotonic per session; each input also carries the tick it targets. Namespace: the room. Injective given the principal. The input type is a schema'd CultCache type, so the schema catalog publishes it like any document. |
Sampled each client tick (devices read by the TS shell, handed to wasm); sent with the last few unacknowledged inputs repeated, so one loss is absorbed; buffered by the server per session against its target tick; consumed exactly once by the matching tick (proposed policy for late inputs: applied to the next tick and counted); acknowledged in the snapshot as "last processed sequence"; kept by the client until acknowledged, for replay; then discarded. Stage 4 changes this: inputs become signed, hash-chained and retained for a dispute window. | The client's principal authors the content; the tick runner decides which tick consumes it. Forbidden: server code altering input content; other clients; any input that arrives without a bound session. |
| Tick | Transient counter; proposed: the last committed tick is written into the room record in each tick's batch, so a snapshot or checkpoint knows it. | (room id, room epoch, tick number), the number starting at 0 when the room epoch starts. Injective with the epoch. |
Advanced only by the server tick runner at the one declared rate (ruled tick-model: one-fixed-rate); never revised. The client estimates the server tick from snapshots and runs its predicted tick ahead by its round trip plus a buffer; on reconnect it re-syncs from the next full snapshot. |
Server tick runner. Forbidden: client clocks, transport arrival time, wall-clock reads inside systems (systems receive the tick, not the time). Today no tick scheduler exists anywhere in the C# body (F6). |
| Predicted state and reconciliation | Client-side only. In the browser they live in the C# wasm CultCache store; the TS renderer reads a per-frame view (ruled browser-client-store: wasm-owns-store). |
Same (schema id, record key) as the authoritative document, plus the client tick it was predicted for. |
Each client tick: predicted systems run over owned documents and the client's own spawns. On a snapshot for tick T acknowledging input sequence S: predicted documents are replaced by the authoritative ones at T, then inputs after S are replayed through the predicted systems up to the current client tick; predicted spawns are matched by derived key, and an unmatched one is removed once the acknowledgement passes its tick. Today Reconciled means "the authoritative put replaced the predicted copy" with no replay (F3); the replay loop does not exist. |
Predicted state owns nothing outside the client. It is derived from the last authoritative snapshot plus unacknowledged inputs plus the predicted systems. The snapshot owns truth. Forbidden: sending predicted documents to the server; CultNetClientAuthorityScope in server mode (F4); the renderer writing. |
| Session and the player's identity | Proposed: session membership as a small CultCache document in the room store (gamecult.mesh.game_session, low churn, readable by systems and present in snapshots). The connection handle is transient process state. |
Principal: the fingerprint of a browser-held P-256 key, proved by signing a join challenge (ruled player-identity: browser-keypair). Connection: a server-minted id per transport connection (ephemeral). Logical session: (principal, room), bound across reconnects. Today the client's identity is a self-asserted runtimeId string (browser client options; SourceRuntimeId on operation envelopes); CultMesh's session identity proves the server to the client and not the reverse (F5). The body still has no authenticated client principal (F5, F13); the session cut builds it. |
Connect, then join (definition hash check, principal proof), then the session record is created and the join system spawns the Player. Disconnect suspends the session for a grace window; resume binds a new connection to the same session; expiry ends the session, systems see the leave, and the game's leave system removes the Player. On server restart sessions end (ruled room-durability). |
The server's join and leave handler on the room primary decides membership; the principal proves itself by signing the join challenge. Forbidden: a client-asserted runtimeId as identity; transport peer address; peer catalogs. |
| Authority declaration and the lease it lowers to | Declaration: inside the game definition. Lease: proposed as a CultCache document (gamecult.mesh.authority_lease); today a plain class in an in-memory dictionary (F2). |
Declaration: per document type, (writer, checker, arbiter). Lease: LeaseId, carrying Verse, peer, roles, shard ids, issuer, validity window, epoch and signature (CultMeshAuthorityLease). Namespace: the Verse. |
Declaration fixed per build. At startup Authority.Server lowers to: the server holds a ShardPrimary lease on the room shard, and clients hold no write scope at all. Renewed before expiry; revocation or expiry stops the tick runner from committing; a new primary takes a new epoch. Proposed issuer in stage 1: the server's own deployment key, which clients pin through the browser trust policy, so an indie deployment needs no Odin. Odin-issued leases stay possible. |
The game definition decides the declaration; the issuer issues leases; CultMeshAuthorityResolver decides whether a peer holds a role on a shard. Today the database's write gate (CultNetShardDescriptor.IsPrimary, CultNetClientAuthorityScope, both set from options) never consults the resolver: two authority structures (F2, F4). Proposed: the write gate is derived from lease resolution. First step (cut remote-write-authority): a peer's put or delete reaches the database only when CultNetDatabaseServerOptions.RemoteWriteAuthority grants it; none configured refuses every remote write (F1, GP1). The lease-backed grant needs an authenticated principal bound to the connection, so it follows the session cut. CultNetClientAuthorityScope is no longer an owner; it is derived from the declaration, and in stage 1 it is empty. |
| Shard / room (stage 1: one) | CultNetShardDescriptor (configuration); the room's state is its CultCache store. |
Room id (Verse, game id, room name), for example orbs/main, carried as the shard id plus the shard epoch. |
Created at server start (epoch 1; on restart a new epoch, ruled room-durability); never moved in stage 1; ended at shutdown. Join means "join the one room"; matchmaking is out of scope. Interest management is stage 5 per the target, so in stage 1 every client receives the whole room. |
The runtime holding the ShardPrimary lease. Forbidden: a second primary; clients. |
| Snapshot sent to clients | Transient. A CultNet message whose payload is CultCache-encoded documents; never stored. The server keeps per-client baselines in memory for deltas. | (room, epoch, tick, recipient session, baseline tick). |
Derived after a tick commit at the send rate; a delta against the recipient's last acknowledged snapshot, or a full snapshot on join and resume; carries the last processed input sequence for that session. Discarded once sent; a baseline is dropped when a newer one is acknowledged. Today the nearest things are per-record change pushes from CultNetDatabaseSubscriptionServer (no tick, no acknowledgement, no input sequence) and shard catch-up snapshots in CultMeshSnapshots.cs (F8). |
The snapshot builder decides nothing; it is derived from the committed tick. Forbidden: the per-record subscription stream acting as a second game replication path. That stream stays for non-game observers. |
| Simulation fact / witness observation (found) | CultMeshSimulationFact is a CultCache document; observations are hub memory. |
(shard, epoch, frame, subject, kind). |
Stage 1: not used. The target replaces it at stage 4. | Today CultMeshGameSession serves observations by default (ServeSimulationObservations = true) and commits quorum facts through the primary (F7). In server mode there is no checker, so the observation server and the committer must not be wired. Forbidden writer in stage 1. |
| Shard mutation log entry (found) | CultNetDatabase in-memory SortedList per shard, plus an optional durable store. |
(shard id, sequence). |
Today every primary write appends one entry, and the in-memory list is never trimmed (F10). Per-record writes at tick rate would grow it without bound. In stage 1 the tick's write set either bypasses the per-record log or lands as one bounded entry per tick. Ruled: the tick's write set lands as one batch per tick (tick-state-commit), in memory (room-durability); the bounded per-tick entry is the tick-runner cut's to specify. |
CultNetDatabase on the primary. Forbidden: per-system-write appends from the tick runner. |
Cells filled by rulings (2026-10-27)
- Session principal:
player-identity(browser-keypair). Not yet in the body (F5, F13). - Browser documents and the renderer's read:
browser-client-store(wasm-owns-store). - Room state across restarts:
room-durability(ephemeral). - Tick rate model:
tick-model(one-fixed-rate). - How a tick reaches the store:
tick-state-commit(tick-batch; the tick-runner cut timesCultCacheBatchat tick rate first). - Stage 1 transport:
browser-transport(WebSocket behind a port carrying only inputs and snapshots; a WebTransport server in parallel,webtransport-server-layer: managed C# over the MsQuic bridge). - Still unprobed:
CultCacheBatchcommit cost at tick rate for about 50 players plus pellets (wasm measuring probe and tick-runner cut).
Body facts
All are source reads at CultLib d1bf21b3 on 2026-10-27 by imagination-gp-0b. No
runtime probe was run in this pass. Each fact names the read that would falsify it.
-
F1. The primary applies any peer's document put.
CultNetDatabaseServer.HandlePutAsync(message, peer)(src/GameCult.Networking/CultNetDatabaseServer.cs:307) callsApplyPutAsync(message)(CultNetDatabase.cs:1022), which checks only that this runtime is primary for the shard and the epoch matches (EnsurePrimary,:2003).peeris used only to send errors. Any connected client can overwrite any game document on the server. -
F2. Leases are not typed state and do not gate writes.
CultMeshAuthorityLeaseis a plain class (no[CultDocument]);CultMeshAuthorityLeaseCatalogis aDictionary(CultMeshAuthority.cs:242). The only[CultDocument]types inGameCult.Meshare body publication, CDN chunk and manifest, content transfer state, discovery state, entity prefab package and simulation fact.GameCult.Networking(the database) references no lease type; the one Networking file namingLeaseisCultNetSchemaMessages.cs. -
F3. Reconciliation is replacement, not replay.
CultNetDatabase.cs:1247-1252: a predicted put records a prediction key; the next non-predicting write to that key turns the change intoReconciled. No code re-applies inputs after an authoritative state. -
F4. Client write scope is a runtime-id string match from options.
CultNetClientAuthorityScope.MatchescomparesOwnerRuntimeIdwith the database's own_runtimeId(CultNetDatabase.cs:111-153,EnsureClientAuthority:1934). It gates local prediction only and has no relation to leases. -
F5. No authenticated client principal.
CultMeshSessionIdentity.csverifies transports for(verse, authorityRuntimeId, protocol, routeGeneration), which proves the server to the client. The browser client'sruntimeId(packages/cultmesh-browser/src/index.ts:182) and the operation envelope'sSourceRuntimeId(CultNetOperationServer.cs:39) are self-asserted. -
F6. No tick scheduler. A grep of
GameCult.NetworkingandGameCult.Meshfor tick or timestep names finds none.Frameexists only as a witness-asserted number inCultNetSimulationConsensusandCultMeshSimulationFact. -
F7. The game session serves the witness path by default.
CultMeshGameSessionconstructsCultNetSimulationObservationServerwhenServeSimulationObservations(defaulttrue) and always constructs aCultMeshSimulationFactCommitter(CultMeshGameSession.cs:70-104).PredictAsyncisPutPredictedAsync(:161). -
F8. The browser can watch and invoke, not put. The
cultmesh-browserREADME says it "does not expose direct document writes". The client offersleaseRawDocument(index.ts:349) andinvokeof typed operations (:370), over one WebSocket. On C#:CultNetDatabaseSubscriptionServerandCultNetOperationServertakeICultNetSchemaServer, so they run overCultNetWebSocketSchemaServer;CultNetDatabaseServer(puts) takes the concrete UDPServer(CultNetDatabaseServer.cs:36). Operations are request and response with receipts and idempotency keys, which is a poor fit for an input stream at tick rate. -
F9. Schema-id namespacing is convention.
[CultDocument("orbs.player", ...)]ids are global strings; nothing ties a schema id to a game id. -
F10. The in-memory mutation log is never trimmed.
RecordMutationLogEntry(CultNetDatabase.cs:1610) inserts into a per-shardSortedList; noRemoveor trim on that list exists in the file. Compaction applies to the durable store and to burned sequences only. -
F11. CultCache has a write-set batch; CultNetDatabase does not expose one.
CultCacheBatchwithCommitandTryCommit(GameCult.Caching/CultCache.cs:1796,:2212) is the 2.0.0 write set (variants:ruling:write-set-content-version-declared-ownership).CultNetDatabasehas no batch method: every write is a per-record door. -
F12. A reflection site on the mesh put path.
CultMesh.cs:3502PutDatabaseDocumentAsyncpicksPutAsyncorPutPredictedAsyncbyGetMethodsandMakeGenericMethod, which matters to the AOT prerequisite (cultlib-gaps:follow_up:cs-aot-safe-registration; probe inresearch/cultmesh-garden-path/cultcache-aot-probe-2026-10-27.md). -
GP1 (runtime probe, 2026-10-28,
imagination-gp-stage1). Scratch NUnit test at51ca81a1, run on Yggdrasil (ygg-verify.sh,mcr.microsoft.com/dotnet/sdk:10.0, filterImagGpProbeTests, never pushed to origin). The server writesgp:player= "server-owned" throughCultNetDatabase.PutAsyncon a primary shard.CultNetDatabaseServer._putHandler, invoked with an uninitializedCultNetServerPeer(no field set: no endpoint, no transport, no identity) and a put of "client-overwrote", leaves the record "client-overwrote", andServer.CanProcessBeforeVerification(put)returnstrue. Output:GP1 after=client-overwrote canProcessBeforeVerification=True, Passed 1. This confirms F1 at runtime: nothing about the peer is consulted, and puts and deletes pass before the legacy login verification (Server.cs:562-575). -
F13. A server peer carries no identity.
CultNetServerPeer(Server.cs:829) holds the LiteNetLibNetPeer, its endpoint and transport stats. The only principal onServeris the legacy username and password login (PlayerData.PlayerId,IsVerified:697), which the database door never reads.ServerSecurityOptionsis one sharedConnectionKey: transport admission, not authority. -
F14. A new refusal code is a coordinated change. The
cultnet.error.v0codeis a closed enum (contracts/cultnet/cultnet.error.schema.json:28-31, with a byte-identical copy underpackages/cultnet-rs/contracts/). RustCultNetErrorCode(packages/cultnet-rs/src/contracts.rs:242-273) refuses unknown codes,packages/cultnet-rs/tests/error_message.rsdecodes captured C# bytes per code, and TypeScript imports the root schema (packages/cultnet-ts/src/contracts.ts:10).unowned_schemawas added this way and is the pattern. -
F15. Forwarding re-sends as a new client.
TryForwardPutAsync(CultNetDatabaseServer.cs:654) forwards anot_primaryput throughICultNetShardWriteForwarder, andCultNetSchemaWriteForwarderopens its own client connection (CultNetShardWriteForwarding.cs:122-128). The primary therefore sees the forwarding runtime, never the original peer. -
F16. Consumers of the remote write door. No C# consumer outside CultLib constructs
CultNetDatabaseServer(grep of the Aetheria, Epiphany, Ghostlight, huginn and gamecult-ops*.csfiles finds only vendored CultLib copies). In CultLib, tests reach the put and delete handlers directly inCultNetJournalTests.cs:738,:935-936andSoulP4cProbeTests.cs:57.CultNetDatabaseServer.ApplyPutAsyncandApplyDeleteAsync(:73,:81) have no caller insrcortestsbesides the handlers. TypeScript puts (thecultnet-tsIdunn presence and signed health publishers) target Rust servers. -
F17. The Rust document server admits a put by size only. In
packages/cultmesh-rs/src/rudp_document_server.rs:682-720, aDocumentPutRawis refused only when its served response would not fit; no authority is asked. This is F1's gap in the Rust runtime (a follow-up, not stage 1).
Cut sequence (stage 1)
One line per cut not yet mapped. The model page has no empty cell as of 2026-10-28. Remote write authority (cultmesh-garden-path:cut_spec:cut-remote-write-authority.r1, mapped 2026-10-28, so no longer a line here) goes first: it waits on nothing, every later server cut stands on a closed write door, and GP1 shows the door open today. The wasm measuring probe is the sibling that may run beside it, since it touches no file this cut does; ruling browser-sim-runtime names it the first cut of the browser runtime track, not of stage 1 as a whole.
- Wasm runtime measuring probe. Download size, startup and 60 Hz tick cost of a stub orbs tick under Mono browser-wasm and NetWasm, including
CultCacheBatchcommit cost at tick rate. This is the first cut named by rulingbrowser-sim-runtime, which it can overturn. Its NetWasm and AOT variant waits oncultlib-gaps:follow_up:cs-aot-safe-registration. - Witness services off in server mode.
CultMeshGameSessionstops constructing the observation server and fact committer when the declaration has no checker (F7). Waits on nothing; can run beside remote write authority. - Leases as typed state and the lease-derived grant.
CultMeshAuthorityLeasebecomes a CultCache document, the lease catalog a store view, and the Mesh host'sRemoteWriteAuthorityresolves the connection's principal throughCultMeshAuthorityResolver. Waits on remote write authority and on session and principal. - Demote
CultNetClientAuthorityScope. It becomes derived from the authority declaration (empty in server mode). Waits on game definition. - Changelog entry for the remote write refusal.
unity/org.gamecult.cultlib/CHANGELOG.mdis also written byvariants:cut_spec:cut-write-ownership-cs.r1, so Self adds this cut's bullet at merge, after that cut lands, and the two specs share no file. - Game definition and server tick runner. Builder, document and system registration, definition document and hash, fixed tick, tick-boundary commit. Rests on rulings
tick-model,tick-state-commitandroom-durability; waits on nothing mapped. - Session and principal. Join, resume and leave; principal proof; session document. Rests on ruling
player-identity; waits on game definition (the join checks the definition hash). - Input and snapshot path. Input messages with redundancy, a server input buffer, acknowledgements, delta snapshots. Rests on ruling
browser-transport; waits on session and principal. - Browser client runtime. Wasm simulation host, TypeScript shell for transport and rendering, prediction with replay reconciliation. Waits on the measuring probe and
browser-client-store. - Orbs reference game and garden-path proof. The game written with no networking code (
garden-path-needs-no-networking), and a browser client that writes (browser-is-first-class). Waits on all of the above.