# Phase 06.7: Network model — client-trust + server illegal-position fall trigger - Context

**Gathered:** 2026-05-17
**Status:** Ready for planning

<domain>
## Phase Boundary

Flip the **self-player movement trust model** from hybrid client-prediction + server-authoritative-reconciler to **client-authoritative position**. Client sends absolute `{x, y, vx, vy, facing, anim_state, seq, monotonic_at_ms}` at 30 Hz; server stores the client-reported state as authoritative and broadcasts to remote players. Reconciler becomes a **no-op for the self-player** (the remote-player path is unchanged — server-state IS authoritative for those).

Server retains full authority over **non-movement state**: chat origin, inventory, room transitions, persistence. (REBNO has no scores or combat in scope.)

Motivating bugs (operator UAT 2026-05-16, staging):
- ~10 px horizontal shift on diagonal stop → root cause is reconciler snap from client-vs-server position drift in `apps/client/src/prediction/reconciler.ts`.
- Occasional movement hitching that appears packet-loss correlated → current sync-strict reconciler assumes a perfect network; absolute-position wire + client-trust naturally absorbs single-packet drops.

**Anti-cheat (Phase B in the seed) is OUT OF SCOPE.** Server-side legality check + fall reset is deferred to backlog (candidate Phase 06.8) — to be triggered only when cheating becomes observable. Pre-launch staging is a trusted operator pool at 50 CCU target, so the abuse window is acceptable.

</domain>

<decisions>
## Implementation Decisions

### Scope

- **D-01:** Phase A only — flip self-player to client-trust + reconciler no-op. Phase B (fall reset + legality check) → backlog (candidate Phase 06.8).
- **D-02:** Motivating bugs explicitly in scope: (a) ~10 px diagonal-stop drift on staging; (b) dropped-packet hitching tolerance. Current model assumes perfect network — fix that.
- **D-03:** Inter-player collision arbitration is **out of scope** (not currently observed; theoretical risk only).
- **D-04:** D-57b and D-58b anti-revert regression tests are **explicitly retired** as an acceptance criterion of this phase. They guard reconciler behavior that is being removed for the self-player. They are replaced by a regression test asserting **the self-player sprite position is never written by `ReconcileEngine.onServerSnapshot`** (positive guard against future revert).

### Wire shape + cadence

- **D-05:** Position-update wire shape (client → server, every tick): `{x, y, vx, vy, facing, anim_state, seq, monotonic_at_ms}`. **Absolute position** (not deltas) — single dropped packet is recovered cleanly by the next.
- **D-06:** Cadence = **30 Hz steady**, regardless of motion. Idle player sends `{x, y, vx=0, vy=0, facing, anim_state=STAND_*, ...}` 30 times/sec. Matches Minecraft's `PlayerPosition` cadence model and keeps the server's authoritative store always-fresh for remote-player broadcasts.
- **D-07:** **Int16 quantization** on `x, y` — client snap-rounds to integer pixels before sending. Matches the BNO snap-round model already in `packages/game-logic/src/step.ts:117-122`. Removes float-comparison footguns. Halves packet size vs float.
- **D-08:** `anim_state` is a 1-byte enum carried in **every position_update** (NOT derivable from kinematics — moving platforms, conveyor platforms, and hexport all produce velocity-without-running states; future spectator/watching pose too). Enum covers 8 directions × {stand, run} + reserved slots for special poses. The existing `SpriteStateMachine` in `packages/game-logic/src/sprite-state-machine.ts` continues to drive client-side animation selection; its current output is what gets shipped.
- **D-09:** Special non-derivable sprite states (HexportIn / HexportOut / TeleIn / TeleOut / ncol overlays / jokershell / watching) ship via a separate **`set_sprite_override`** event-only intent. Rare event triggers — no place in the 30 Hz hot path. Mirrors the `set_facing` precedent shipped in 06.4 round-3 (D-58c).
- **D-10:** `facing` field stays in position_update (NOT folded into `anim_state`). Facing = "where the player is pointing" intent (D-58c set_facing semantics); `anim_state` = "what pose to render right now". Different semantics, both needed.

### Anti-cheat

- **D-11:** **Zero server-side anti-cheat in 06.7.** Server trusts the client-reported position fully. Rationale: 50 CCU target, pre-launch private staging, trusted operator pool. Speed-cap + walkable-grid sanity + fall reset → all deferred to future 06.8 (or whenever cheating becomes observable on real-user traffic).

### Migration + doctrine

- **D-12:** **Hard cut on staging.** No feature flag. Staging is the only deployment target pre-launch; operator UAT validates the new model in place. If staging regresses, rollback = `/gsd-undo` on phase commits.
- **D-13:** **CLAUDE.md Hard Rule 1 — narrow movement carve-out.** Add one paragraph: "Movement (position, velocity, facing, anim_state) is CLIENT-AUTHORITATIVE — server stores client-reported state. Non-movement state (chat origin, inventory, room transitions, persistence) remains server-authoritative." Drop scores/combat from the example list — REBNO has no plans for those. Restructuring Rule 1 into a tabular trust matrix is **out of scope** (broader rewrite for a future phase if doctrine grows more complex).
- **D-14:** **Rollback strategy = `/gsd-undo` phase commits.** No dead-code branches kept around.

### Research directive (for /gsd-plan-phase 06.7)

- **D-15:** **Minecraft Java Edition is the canonical reference model.** Researcher (gsd-phase-researcher) must produce a comparison table in RESEARCH.md scoring at minimum:
  - **Minecraft Java Edition** (`PlayerPosition` packet, server-side `Player#move` validation, force-teleport correction)
  - **Quake / Tribes lineage** (client-prediction + server-reconcile — what REBNO is moving AWAY from)
  - **Current REBNO model** (hybrid prediction + 22 px reconciler snap/lerp)
  
  ...against the constraints: 50 CCU target, predictable-movement (core REBNO value), dropped-packet tolerance, single Fly.io machine, Colyseus 0.17 transport.

- **D-16:** Dropped-packet tolerance is a first-class research dimension. Researcher should investigate: how often is a missed `position_update` survivable for the remote-player path? Do we need server-side extrapolation between packets, or does 30 Hz + absolute-position + Colyseus transport reliability make it irrelevant?

### Claude's Discretion

- Exact `anim_state` enum members + numeric byte values — pick during planning based on existing `SpriteStateMachine` states. Add a binary-format note to `packages/protocol` matching int16 quantization.
- Whether to keep the `seq` echo / `last_input_seq` round-trip on the position_update path or drop it (current `cInputSchema` uses seq for ordering — likely keep for monotonic ordering + idempotent retries).
- Server-side `step()` retention strategy — server's authoritative step still drives platforms + future Phase B fall checks. Whether to keep it running on a separate tick for self-player platform-effects (conveyor application) vs. treating client's reported velocity as the truth.
- Whether to send remote-player broadcast diffs at full 30 Hz or downsampled (server-side fan-out optimization — researcher input).

### Folded Todos
None — the 2026-05-16 split-playwright-smoke todo (score 0.9) keyword-matched on "staging/uat/operator" but is a CI workflow refactor, not a network-model concern. Reviewed-only; see Deferred Ideas.

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Network-model precedent (external — researcher to fetch via WebSearch / WebFetch)
- **Minecraft Java Edition `PlayerPosition` / `PlayerPositionAndLook` packet semantics** — official protocol docs (wiki.vg or equivalent). The canonical implementation pattern of client-trust + server force-correct. Researcher to produce a comparison table per D-15.
- **Minecraft `Player#move` server-side validation** — `maxMoveDistanceSquared`, terrain check, force-teleport response. Reference even though REBNO Phase B is deferred — informs the future 06.8 plan.

### Current REBNO model (read these to understand what is being changed)
- `apps/client/src/prediction/reconciler.ts` — `ReconcileEngine.onServerSnapshot` (the 22 px threshold + lerp/snap logic that becomes a no-op for self-player). 123 LOC.
- `apps/client/src/prediction/predictor.ts` — `PredictionEngine` (self-player local prediction + ack/replay; need to determine whether this stays for input-driven local responsiveness even with client-trust).
- `apps/client/src/prediction/input-dispatcher.ts` — current `c2s.input` axes wire path (D-09 keydown/keyup events + D-58c `set_facing` precedent for client-authoritative state).
- `apps/server/src/RebnoRoom.ts` §621-622 — server-side spawn position assignment + tick loop (1389 LOC; pay attention to player.x / player.y writes — those become assignments from the position_update message, not from `step()` output).
- `apps/server/src/onMessageHandlers.ts` — zod-validated wire intent handlers. New `position_update` + `set_sprite_override` validators belong here.
- `packages/game-logic/src/step.ts` — pure deterministic step. Becomes server-side platform-effects-only for self-player; remote-player broadcast pass-through. Animation derivation **NOT possible** from x/y/vx/vy per D-08.
- `packages/game-logic/src/sprite-state-machine.ts` — SpriteStateMachine that produces `anim_state`; ship its current output verbatim on the wire.
- `packages/game-logic/src/collision.ts` — collision primitives (still needed for client-side prediction-of-self for input responsiveness; server walkable-grid check is deferred to 06.8).
- `packages/protocol/src/intents.ts` — `cInputSchema` (the precedent shape for the new `position_update` schema).

### Project doctrine to update
- `CLAUDE.md` §"Hard Rules" Rule 1 — narrow movement carve-out per D-13. **This update is part of the 06.7 deliverable**, not a side effect.
- `.planning/REQUIREMENTS.md` — REQ-SRV-03 / REQ-SRV-14 / REQ-CLI-04 / REQ-CLI-08 references (no requirement change needed — the trust-model swap is a refactor of how those reqs are satisfied, not a redefinition).

### Prior-phase context to carry forward
- `.planning/phases/06.4-cycle-5-gap-closure-d-51b-d-55b-d-55c-d-57b-d-58b-d-60-d-61-/` — D-58c set_facing precedent (client-authoritative wire intent for a state field previously derived server-side). The new `position_update` follows the same playbook.
- `.planning/phases/06.6-uat-accounts-integer-viewport-scaling-chat-region-clamp-righ/06.6-CONTEXT.md` — prior phase context style + decision-numbering convention.

### Phase seed
- `.planning/phases/06.7-network-model-client-trust-fall-trigger/06.7-SEED.md` — promoted from the 2026-05-16 todo; full problem statement and Phase A / Phase B framing.

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- **`SpriteStateMachine` (`packages/game-logic/src/sprite-state-machine.ts`)** — already produces the animation pose enum client-side. Wire its output as `anim_state` on every position_update; no new state machine needed.
- **`set_facing` intent (06.4 D-58c)** — exact precedent for client-authoritative state field shipped on a wire intent. Copy the validator + handler + broadcast pattern.
- **`cInputSchema.strict()` zod validator (`packages/protocol/src/intents.ts`)** — the security pattern: zod validates everything on the wire before the server consumes it. Apply identically to `position_update` (sanity-validates int16 range, enum membership, monotonic seq) even though no anti-cheat envelope ships in 06.7.
- **`HEARTBEAT_INTERVAL_MS = 15_000` in `input-dispatcher.ts`** — heartbeat-keepalive pattern. Position_update at 30 Hz IS the heartbeat — no separate keepalive needed.

### Established Patterns
- **Deterministic `step()` in `packages/game-logic`** — pure function, no I/O, runs identically in browser and Node. Stays useful for client-side input-responsiveness prediction (self-player) and for platform/conveyor effect application. Remote-player position is no longer derived from `step()` — it's stored from the wire.
- **Threat-model invariant T-06.1.02-01 (step.ts:151-158 comment)** — server never trusts client-fabricated walkable_grid. Compatible with 06.7: server still doesn't consume client grid; it just stores client-reported position. Document the new trust line clearly in CLAUDE.md per D-13.
- **REQ-SRV-14 deterministic broadcast envelope** — the existing state-diff envelope from Phase 4 stays. `position_update` from a client mutates the authoritative `PlayerState`; broadcast envelope unchanged.
- **`globalThis.__rebno.lastReconcile*` diagnostic ring buffer** (`reconciler.ts:73-92`) — operator-debugging surface. Remove cleanly during 06.7; if a self-reconciler regression slips in, this is the canary that resurfaces.

### Integration Points
- **`onMessageHandlers.ts`** — add `position_update` handler (validate via zod, write into PlayerState, broadcast in next tick) + `set_sprite_override` handler.
- **`RebnoRoom.ts` tick loop** — server `step()` no longer computes self-player position from axes; instead it consumes the most-recent client-reported `{x, y, vx, vy, facing, anim_state}` and broadcasts. Platform/conveyor application TBD per Claude's Discretion above.
- **`apps/client/src/net/colyseus-client.ts`** — replace the axes-only dispatcher path with the position_update dispatcher. Existing `input-dispatcher.ts` keydown/keyup events still drive **local** SpriteStateMachine; the dispatcher swap is "what we send", not "how we read input".
- **`apps/client/src/prediction/reconciler.ts`** — self-player branch becomes no-op (return early when `snap.account_id === local.account_id`). Remote-player branch unchanged. Document the split clearly in the file header.
- **`apps/client/src/prediction/predictor.ts`** — likely stays for client-side local input-to-position prediction (instant input responsiveness without round-trip). Researcher to confirm during planning.

</code_context>

<specifics>
## Specific Ideas

- **Minecraft Java Edition is the explicit precedent.** User asked specifically how Minecraft implements its semi-server-authoritative movement and confirmed it as a research reference. The pattern (client-trust + server validation as separate concerns) is the structural blueprint for 06.7 (Phase A) + 06.8 (future Phase B).
- **Absolute position on every packet (not deltas).** User reasoned about packet-loss tolerance from first principles: the current model "effectively assumes a perfect network connection", which it shouldn't. Mirroring Minecraft's absolute-position wire makes single-packet drops invisible.
- **Animation cannot be derived from kinematics.** User caught and corrected the initial idea of server-deriving `anim_state` from `(x, y, vx, vy)`. Counter-examples: moving platforms (velocity-without-running), conveyor platforms (velocity-without-running), hexport (motion-with-special-pose). Ship `anim_state` on the wire explicitly.
- **No scores or combat in REBNO scope.** User clarified during Hard Rule 1 discussion. Rule 1 carve-out language must reflect the actual REBNO domain (chat, inventory, room transitions, persistence), not generic MMO doctrine.
- **Trust-fully on staging.** User explicitly rejected speed-cap-as-cheap-first-line and logging-only options for 06.7. Pre-launch + 50 CCU + private = no anti-cheat envelope justifies the implementation cost yet.

</specifics>

<deferred>
## Deferred Ideas

- **Phase B — server illegal-position detection + fall reset.** Tick-by-tick `isPositionLegal(x, y, walkable_grid)` check; if illegal > N ticks (~250-500 ms grace), trigger fall event = forced animation + position reset to last-known-legal-position or designated respawn. Anti-cheat backstop. **Candidate Phase 06.8** — trigger when cheating becomes observable on real-user traffic OR before public launch, whichever comes first. Needs BNO legacy fall-spec extraction (drift falls, void falls, respawn target) as research input.
- **Inter-player collision arbitration.** Two clients claiming overlapping positions — server picks one canonical (latest-update-wins / lower-account-id / etc.) OR client-side visual nudge offset. Defer until observed in operator UAT.
- **Speed-cap server-side validation** (`max_delta_per_tick = RUN_SPEED * 1.5`). Cheap first-line cheat detection. Backlog for 06.8 alongside fall reset.
- **Walkable-grid sanity check server-side.** Server verifies client position is on a walkable tile. Requires server to compute walkable_grid (currently client-only per T-06.1.02-01). Backlog for 06.8.
- **CLAUDE.md trust-doctrine table restructure.** Replace Rule 1 prose with a per-domain authority matrix. Defer until doctrine grows complex enough to justify the rewrite — Rule 1 narrow carve-out (D-13) is the right move now.
- **Server-side `step()` future role.** Whether server keeps running `step()` for platform/conveyor effect application (server applies platform vx/vy to player position) vs. trusting the client's reported position to already include platform-carry. Planner + researcher input needed.

### Reviewed Todos (not folded)
- `2026-05-16-split-playwright-smoke-into-3rd-workflow-job.md` (area: ci, score 0.9) — Operator-suggested 06.5 follow-up to split `deploy-staging.yml` `full-path` into separate deploy + smoke jobs so `gh run watch` can distinguish "deploy succeeded but smoke failed" from "deploy failed". **Out of scope for 06.7** — different domain (CI workflow refactor, not network model). Keyword-match was on "staging/uat/operator", not on substance. Stays in `.planning/todos/pending/` for a future CI-polish phase.

</deferred>

---

*Phase: 06.7-network-model-client-trust-fall-trigger*
*Context gathered: 2026-05-17*
