---
phase: 06.4-cycle-5-gap-closure
plan: 05
subsystem: telemetry
tags: [d58c, direction-state, sprite-state-machine, server-broadcast-facing, pino, window-rebno]

# Dependency graph
requires:
  - phase: 06.4-cycle-5-gap-closure
    provides: PATTERNS (window.__rebno unconditional publish, server-side pino structured-event log)
  - phase: 06.3
    provides: firstRemoteNameplateHistory ring buffer + reconciler convergence (D-58 cycle-3 position fix)
provides:
  - "client telemetry: window.__rebno.localDirection + firstRemoteDirection + firstRemoteAxes (unconditional, per-sim-tick)"
  - "server telemetry: pino d58c_player_axes events per-tick for operator-targeted accounts (uat_a, uat_b)"
  - "in-code hypothesis comment in SpriteStateMachine.deriveFacing pointing 06.4-10 fix at server-broadcast facing"
affects: [06.4-10 (D-58c fix), 06.4-08 (W2→W3 staging UAT + smoke), Phase 7 PAR-* (server-authoritative facing if codified)]

# Tech tracking
tech-stack:
  added: []
  patterns:
    - "Unconditional window.__rebno publish from per-sim-tick render hot path (no DEV/MODE gate — RC1 carry-in)"
    - "Operator-targeted pino event guard (account_id allowlist) to bound log volume to UAT accounts"
    - "In-code hypothesis comment as forward-reference to fix-plan executor (06.4-10)"

key-files:
  created: []
  modified:
    - apps/client/src/render/PlayerRenderer.ts
    - apps/client/src/render/SpriteStateMachine.ts
    - apps/server/src/RebnoRoom.ts

key-decisions:
  - "Telemetry-only spike — NO behavior change in deriveFacing or in any sim-tick path. The fix lands in 06.4-10."
  - "Three call sites for publishDirectionTelemetry (spawn-delay branch + normal local branch + remote tick) so the staging UAT captures the divergence even during the 30-tick spawn-in hold."
  - "Server pino guard by account_id (uat_a, uat_b) — operator-driven UAT accounts — to avoid log flooding while still capturing every state-mutation tick."
  - "Hypothesis comment embeds the fix-path recommendation (server-broadcast facing per RESEARCH §Pattern + REQ-SRV-03) so 06.4-10 executor has a single canonical pointer."

patterns-established:
  - "Spike telemetry surfaces a single ts field (directionTelemetryTs) distinct from other __rebno timestamps to avoid race readers."
  - "Server pino events keyed by `event: 'd58c_*'` follow the cycle-marker convention from 06.4-04 D-51c (event family + cycle marker)."

requirements-completed: [REQ-CLI-04, REQ-SRV-03, REQ-CLI-08]

# Metrics
duration: 35min
completed: 2026-05-15
---

# Phase 06.4 Plan 05: D-58c Direction Telemetry Spike Summary

**D-58c spike — client + server telemetry instrumenting the direction-state propagation path to confirm the lastFacing-divergence hypothesis on staging UAT before Plan 06.4-10 ships the server-broadcast-facing fix.**

## Performance

- **Duration:** ~35 min (incl. workspace pnpm install + package build in fresh worktree)
- **Started:** 2026-05-15T05:00:00Z
- **Completed:** 2026-05-15T05:10:00Z
- **Tasks:** 3 (executed as a single atomic commit per plan §success_criteria #6)
- **Files modified:** 3

## Accomplishments

- Client publishes `window.__rebno.localDirection`, `firstRemoteDirection`, `firstRemoteAxes` unconditionally on every sim-tick (both local sim-tick branches + remote sim-tick). Operator can read both literals on staging UAT without env-gate bypass dance.
- Server emits `d58c_player_axes` pino structured-event on every state mutation for `uat_a` / `uat_b` accounts — captures `account_id, axis_x_held, axis_y_held, x, y, vx, vy, ts` per tick.
- `SpriteStateMachine.deriveFacing` carries a multi-line hypothesis comment above the zero-velocity branch that records the lastFacing-divergence finding and points the next fix-plan executor (Plan 06.4-10) at the server-broadcast-facing fix path with explicit REQ-SRV-03 alignment.
- **NO behavior change** — no `deriveFacing` logic edit, no lerp factor change, no tick-loop edit, no schema change. Pure instrumentation.

## Task Commits

Three sub-tasks coalesced into one atomic commit per the plan's single-commit success criterion:

1. **Task 1: Client publishDirectionTelemetry()** — `21941a9` (feat)
2. **Task 2: Server d58c_player_axes pino** — `21941a9` (feat)
3. **Task 3: SpriteStateMachine hypothesis comment** — `21941a9` (feat)

**Plan metadata commit:** added in the SUMMARY commit immediately after this file.

## Files Created/Modified

- `apps/client/src/render/PlayerRenderer.ts` — added `private publishDirectionTelemetry()` method (lines ~419-441 in modified file); called from `onSimulationTickLocal` (spawn-delay branch + normal branch, 2 call sites) and `onSimulationTickRemote` (1 call site).
- `apps/client/src/render/SpriteStateMachine.ts` — added multi-line D-58c hypothesis comment above the zero-velocity branch in `deriveFacing` (lines ~117-127). No logic change.
- `apps/server/src/RebnoRoom.ts` — added guarded `log.info({event: 'd58c_player_axes', ...})` inside `applyToColyseusState` per-tick state-mutation loop, after `p.axis_x_held` / `p.axis_y_held` writes. Guard: `p.account_id === 'uat_a' || p.account_id === 'uat_b'`.

## Decisions Made

- **Single atomic commit** for all three sub-tasks. Plan §success_criteria #6 specifies one commit message; PlayerRenderer + SpriteStateMachine + RebnoRoom edits all serve the same spike goal and stand or fall together.
- **Three call sites for `publishDirectionTelemetry`** (not two as the plan §action suggested). The local sim-tick path has two branches (spawn-delay and normal), each of which updates `this.local.facing`. Publishing in both ensures the operator captures direction state even during the legacy 30-tick spawn-in hold (D-53 carry-in). The plan's "after the local facing recomputation" reads correctly to mean "every place we set facing"; both branches qualify.
- **`firstRemoteAxes` reads `lastSeenAxisX/Y` via `as any`** per RESEARCH §D-58c spike telemetry example (lines 488-491). These fields are not yet on `RemoteEntry`; the `as any` is intentional so the spike compiles cleanly today and the eventual fix-plan can add the field to `RemoteEntry` without churning this spike. The operator reads `undefined` until that field is populated — confirms the data path is missing.
- **`directionTelemetryTs` (not `ts`)** as the timestamp field so the publish does not race other `__rebno` timestamp consumers.

## Spike Output Classification (operator-captured telemetry, staging UAT)

> **Operator: populate after staging UAT walk-and-stop capture.** Plan 06.4-10 reads this table to lock the fix path.

| Field | Expected at the divergence | Captured value (tab A — local mover) | Captured value (tab B — remote viewer) |
|---|---|---|---|
| `window.__rebno.localDirection` | divergence point: tab A is the mover; localDirection reflects tab A's last sim-tick `deriveFacing` output | `<populate>` (e.g. `R`) | `<populate>` (e.g. `D` — stale lastFacing) |
| `window.__rebno.firstRemoteDirection` | tab A sees tab B's broadcast facing (or `undefined` if no remote yet); tab B sees tab A's broadcast facing | `<populate>` | `<populate>` |
| `window.__rebno.firstRemoteAxes` | the last-seen axis-held inputs for the first remote — if `undefined` confirms `RemoteEntry.lastSeenAxisX/Y` is not wired (expected; fix-plan adds it) | `<populate>` | `<populate>` |
| server pino `d58c_player_axes` for `account_id: 'uat_a'` | per-tick axis_x_held + axis_y_held values during the walk → at stop, both = 0 | `<paste pino tail or note: matches expected zero-on-stop>` | n/a (server log) |
| server pino `d58c_player_axes` for `account_id: 'uat_b'` | mirror of above for tab B | `<paste pino tail>` | n/a |

**Classification verdict (operator to fill):**
- [ ] **PASS — divergence confirmed at client-side `lastFacing`:** localDirection and firstRemoteDirection differ on tab A while server's `axis_x_held` / `axis_y_held` are consistent (both zero on stop). → Fix path locked: **server-broadcast facing** (Plan 06.4-10).
- [ ] **PARTIAL — divergence also visible at server axis values:** server `axis_x_held` differs between accounts when both are stopped. → Investigate held-input map (`heldInputs` in RebnoRoom.ts) and revisit hypothesis (d) → (c) → (b) → (a) priority before 06.4-10.
- [ ] **NO REPRO — directions match in spite of expected divergence:** the spike did not reproduce on staging. → Re-run with different walk-and-stop sequence (R-then-stop vs UR-then-stop); if still no repro, escalate D-58c as not-reproducible-in-cycle-5.

## Deviations from Plan

None — plan executed exactly as written. Three telemetry-only edits, one atomic commit, traceability tags present.

The plan §action for Task 1 reads "Call `this.publishDirectionTelemetry();` from BOTH `onSimulationTickLocal` (after the local facing recomputation) AND `onSimulationTickRemote`"; this was implemented as 3 call sites (not 2) because the local-tick path has two branches each of which updates facing — see "Decisions Made" above. This is faithful execution of the action ("after the local facing recomputation"), not a deviation.

## Issues Encountered

- **Worktree had no `node_modules`** — fresh worktree checkout. Ran `pnpm install --frozen-lockfile` + `pnpm -r --filter "./packages/**" build` to make workspace packages resolvable. This is normal worktree bootstrap, not a code issue.
- **Pre-existing test failures in `apps/server/test/admin-stubs.test.ts` (2/4 fail)** — CRLF vs LF mismatch on Windows worktree checkout. Already documented in `.planning/phases/06.4-cycle-5-gap-closure-d-51b-d-55b-d-55c-d-57b-d-58b-d-60-d-61-/deferred-items.md` §item 2. Verified pre-existing by stashing my changes and re-running — same 2 failures. Out of scope per executor SCOPE BOUNDARY rule. No action taken.

## Verification

| Check | Expected | Actual |
|---|---|---|
| `grep -c "publishDirectionTelemetry" apps/client/src/render/PlayerRenderer.ts` | ≥ 3 (definition + 2 call sites; plan minimum) | **4** (definition + 3 call sites — spawn-delay branch, normal local branch, remote tick) |
| `grep -c "localDirection" apps/client/src/render/PlayerRenderer.ts` | ≥ 1 | **3** |
| `grep -c "d58c_player_axes" apps/server/src/RebnoRoom.ts` | 1 | **1** |
| `grep -c "D-58c" apps/client/src/render/SpriteStateMachine.ts` | ≥ 1 | **1** |
| `cd apps/client && pnpm typecheck` | 0 exit | **0 exit** |
| `cd apps/server && pnpm typecheck` | 0 exit | **0 exit** |
| `cd apps/client && pnpm test --run` | all pass | **202/202 passed, 4 todo** |
| `cd apps/server && pnpm test --run` | all pass | 59/61 passed (2 fail = pre-existing CRLF admin-stubs, deferred-items §2 — NOT D-58c related) |

## User Setup Required

None — telemetry-only spike. No env vars, no external service config. Operator captures the spike output by visiting staging in two tabs (one per `uat_*` account), walking + stopping each, reading `window.__rebno.localDirection` / `firstRemoteDirection` in the devtools console, and tailing the server pino log for `d58c_player_axes` events.

## Next Phase Readiness

- Spike instrumentation is live. Operator UAT capture window opens at the next staging deploy (Plan 06.4-08 smoke per the phase wave plan).
- Plan 06.4-10 (D-58c fix) reads this SUMMARY's "Spike Output Classification" table to lock the fix path. The default recommendation (server-broadcast facing per RESEARCH §Pattern + REQ-SRV-03) is the expected outcome.
- No blockers introduced by this plan.

## Self-Check

- [x] `apps/client/src/render/PlayerRenderer.ts` — `publishDirectionTelemetry` method present (4 matches: 1 definition + 3 call sites).
- [x] `apps/client/src/render/SpriteStateMachine.ts` — `D-58c` hypothesis comment present (1 match).
- [x] `apps/server/src/RebnoRoom.ts` — `d58c_player_axes` pino event present (1 match).
- [x] Commit `21941a9` exists on this branch (`git log --oneline | grep 21941a9` confirmed).
- [x] No file deletions in the commit (`git diff --diff-filter=D HEAD~1 HEAD` → empty).
- [x] Both `pnpm typecheck` invocations (client + server) returned 0 exit.
- [x] Client tests 202/202 pass. Server tests 59/61 pass (2 pre-existing CRLF failures documented in `deferred-items.md` §item 2).

## Self-Check: PASSED

---
*Phase: 06.4-cycle-5-gap-closure*
*Completed: 2026-05-15*
