# Handoff: Table-fidelity batch — deployed; manual test pass underway ## Session Metadata - Created: 2026-05-18 20:57:36 - Project: /home/claude/bin/blind_chess - Branch: main (all work committed and pushed to `git.sethpc.xyz/Seth/blind_chess`) - Session duration: ~one full session (brainstorm → spec → plan → 12-task execution) ## Handoff Chain - **Continues from**: [2026-04-29-060121-blind-casual-check-fix.md](./2026-04-29-060121-blind-casual-check-fix.md) — the prior session fixed blind Casual's premature resignation. - **Supersedes**: None. ## Recent Commits (for context) - `c01244c` fix: promotion dialog only fires for genuine pawn promotions — **deployed** - `5d995eb` docs: update handoff - `d10e581` fix(client): light outline on dark phantom glyphs for panel contrast — **deployed** - `0773300` docs: table-fidelity batch deployed to both instances - `2e80800` docs: record table-fidelity feature batch as code-complete - Feature implementation range: `be8ecd9..2e80800` (20 commits); two follow-up fixes since (`d10e581`, `c01244c`), both deployed. ## Current State Summary The "table-fidelity feature batch" (three features Andrew Freiberg — Seth's dad, a physical-blind-chess player — requested by email) is **fully implemented, reviewed, committed to `main`, and deployed** (2026-05-18) to both live instances — `chess.sethpc.xyz` (CT 690) and `chess.local` (VDJ-RIG). All 12 plan tasks were executed via subagent-driven development with two-stage review per task plus a final whole-batch review. Build, typecheck, and the 94-test suite all pass (32 shared + 62 server). A **manual browser test pass is underway** (Seth). It has surfaced two bugs so far — both fixed and deployed: 1. **Contrast** (`d10e581`): opponent (black) phantom pieces were near-invisible on the dark `--panel` background — black glyphs in the palette, Captures panel, and drag-ghost now get a light text-shadow outline. 2. **Spurious promotion dialog** (`c01244c`): the "Promote pawn" modal fired for any pawn "moved" toward the last rank because the commit paths checked the destination rank but not the pawn's *source* rank — easy to hit once the phantom layer filled ranks 7-8 with tappable phantoms. Fixed with a new shared `isPromotionMove(piece, from, to)` (pawn, from the rank adjacent to promotion, to the promotion rank, ≤1 file over), used by both client `onCommit` and server `isPromotionRequired`. Both fixes are live — the two instances and `main` are all at `c01244c`. ## Architecture Overview Three features, two increments, all shipped: 1. **Announce-all (F1):** every moderator `Announcement` is now `audience: 'both'`. Previously move events went only to the opponent and attempted-move errors only to the actor. The `audience` field is retained (uniformly `'both'`) as the egress-control hook in `ws.ts`/`ModeratorPanel`. The Casual bot's *intermediate* retry-rejection announcements are popped in `BotDriver.dispatch` so its blind-mode search does not broadcast as churn — only its final move is announced. 2. **Capture tally (F2):** a server-derived per-viewer `captures: CaptureTally` field on the `joined`/`update` messages, rendered by a new `CaptureTally.svelte` panel. Must be server-side — in blind mode the capturing client cannot see what it took. 3. **Phantom layer (F3):** a client-LOCAL overlay of guessed opponent pieces, blind mode only. Seeded once with the opponent's standard army, then fully manual: pointer-drag a phantom anywhere, off-board to remove, re-add from an unlimited palette. Persisted to `localStorage` (`bc:phantoms:`). **Never sent to the server** — it lives in its own store so the zero-leak property is auditable. The zero-leak core (`buildView`, `geometric.ts`) was deliberately untouched. ## Critical Files | File | Purpose | Relevance | |------|---------|-----------| | `docs/superpowers/specs/2026-05-18-table-fidelity-features-design.md` | The spec | Design rationale, info-leak analysis | | `docs/superpowers/plans/2026-05-18-table-fidelity-features.md` | The 12-task plan | Amended to match shipped code | | `packages/server/src/translator.ts`, `commit.ts` | F1 audience change | All announcements `'both'` | | `packages/server/src/bot/driver.ts` | F1 bot-churn suppression | `dispatch` pops intermediate rejections | | `packages/server/src/captures.ts` | F2 `captureTally` | Pure per-viewer derivation | | `packages/shared/src/phantoms.ts` | F3 pure model | `opponentStartPosition`, `deserializePhantoms` (tested) | | `packages/client/src/lib/stores/phantoms.svelte.ts` | F3 local store | Never read in any `send` path | | `packages/client/src/lib/stores/phantom-drag.svelte.ts` | F3 drag controller | Pointer events, tap-vs-drag, `pointercancel`-safe | | `packages/client/src/lib/Board.svelte`, `Game.svelte`, `PhantomPalette.svelte` | F3 UI | Phantom rendering + wiring | ## Key Patterns Discovered - **Build ordering:** `server`/`client` resolve `@blind-chess/shared` from its built `dist/`. After editing `shared`, run `pnpm --filter @blind-chess/shared build` before downstream typecheck/build. `pnpm -r build` handles order automatically. - **Client has no test harness** (by design). Pure logic worth testing goes to `packages/shared` (vitest); Svelte components are covered by `svelte-check` + manual. - **`ply`-parity actor derivation:** the four attempted-move enums carry no colour; the client derives White/Black from `ply % 2` (an attempt only happens on the actor's turn). ## Work Completed - Tasks 1–11 of the plan (all three features), each with implementer + spec-compliance review + code-quality review and fix loops. - Final whole-batch code review — verdict: ready to ship, no Critical/Important issues. - Checkpoint A and B verifications: `pnpm -r build && pnpm -r typecheck && pnpm -r test` all clean; 87 tests pass (25 shared + 62 server). - DECISIONS.md, CLAUDE.md, and the spec updated to reflect the shipped state. ## Files Modified See `git diff --stat be8ecd9..2e80800`. New files: `packages/server/src/captures.ts`, `packages/server/test/unit/captures.test.ts`, `packages/shared/src/phantoms.ts`, `packages/shared/test/phantoms.test.ts`, `packages/client/src/lib/{CaptureTally,PhantomPalette}.svelte`, `packages/client/src/lib/stores/{phantoms,phantom-drag}.svelte.ts`. Modified: `translator.ts`, `commit.ts`, `bot/driver.ts`, `ws.ts`, `shared/{types,protocol,index}.ts`, `client/lib/{Board,Game,ModeratorPanel}.svelte`, `client/lib/stores/game.svelte.ts`. ## Decisions Made All recorded in `DECISIONS.md` under "Table-fidelity features (2026-05-18)" and "Deferred / Rejected". Key ones: announcements widened to `'both'` (deliberate, authorised); manual phantom model (smart-tracker rejected); phantom layer client-local only; drag-and-drop for phantoms only (real moves stay click-to-move). ## Immediate Next Steps 1. **Continue the manual browser/phone test pass.** Open https://chess.sethpc.xyz, create a **blind game vs computer**, and check: the moderator panel shows White/Black-labelled attempt lines; the Captures panel updates on a capture; the phantom layer renders 16 seeded pieces; dragging a phantom moves it / off-board removes it; the palette places phantoms; a *tap* still makes real moves; genuine pawn promotion (pawn e7→e8) still pops the dialog; phantoms persist across reload; vanilla mode shows no phantom UI; phantoms hide on game end. The phantom drag is the main mobile-risk surface — test on a phone. 2. **Check the board phantom glyph contrast.** `.phantom-b` (dark phantoms on the board) render at `opacity: 0.4` on the board squares; they have a dashed frame so the square reads, but the piece *type* may be hard to tell on dark squares. Flagged to Seth to eyeball during the test pass — if too faint, give `.phantom-b` a subtle light outline without killing the intentional translucency. 3. (Optional) Fix the `install-local.sh` redeploy gap, and reconcile the stale VDJ-RIG "no Caddy" note — see Potential Gotchas. ## Blockers / Open Questions - **Board phantom glyph contrast — open question.** See Immediate Next Steps #2 — needs a human eye on a real board before deciding whether `.phantom-b` needs an outline. - **Manual browser test pass is in progress, not complete.** Two bugs found and fixed+deployed so far (contrast, spurious promotion dialog); the rest of the checklist in Next Step #1 is unverified. The phantom drag-and-drop in particular (pointer events, tap-vs-drag, hit-testing) is verified only by code review, not by clicking. ## Deferred Items - Phantom-layer `localStorage` cleanup for games abandoned mid-play (no `finished` transition) — tiny leak, add a stale-key sweep only if it matters. - Highlighting interacting with phantoms (rays stopping at phantom pieces) — safe but out of v1 scope. - An `ai-game-casual` WebSocket integration test for F1 — the `driver.test.ts` unit test was chosen instead (covers the same commit-path; spec updated to record this). ## Important Context - **Everything is on `main` and pushed.** Seth explicitly chose to work directly on `main` (no feature branch). There is nothing to merge. - **The feature batch plus both follow-up fixes are deployed and live** on both instances (`chess.sethpc.xyz` and `chess.local`) at commit `c01244c` — `main` and the live site match. Deployment is outward-facing and drops in-memory games — confirm timing with Seth. - **Two deploy instances now exist** (CLAUDE.md was updated mid-project): CT 690 / `chess.sethpc.xyz`, and `chess.local` on VDJ-RIG. Both need the update. - The final review confirmed the security invariant holds: no `phantom` token anywhere under `packages/server/src/`, `buildView`/`geometric.ts` byte-for-byte unchanged. ## Assumptions Made - The client a11y trade-off (phantom spans and palette pieces are pointer-only, with a documented `svelte-ignore a11y_no_static_element_interactions`) is acceptable — adding `tabindex` without a keyboard drag would be worse a11y. Real gameplay stays fully keyboard-operable via the square buttons. - 94 is the expected test count (32 shared + 62 server); the client contributes 0 (no harness). ## Potential Gotchas - `packages/server/tsconfig.tsbuildinfo` shows persistent `M` in `git status` — pre-existing drift (tracked before `*.tsbuildinfo` was gitignored), not this session's work. - The pre-commit hook is `detect-secrets-hook --baseline .secrets.baseline`. - Server restart on deploy drops all in-memory games. (CT 690 had 3 "active" games at deploy time — almost certainly stale abandoned games, since active games never auto-expire and uptime was 19 days; dropped per the pre-accepted MVP policy.) - `deploy/install-local.sh` (the `chess.local` installer) ends with `systemctl enable --now blind-chess.service`, which does NOT restart an already-running service — a redeploy via the script alone leaves the old code running. Deploys work around it with an explicit `sudo systemctl restart blind-chess` after the script. Proper fix: change the script's `enable --now` to `enable` then `restart`. - **VDJ-RIG port 80 has a Caddy** (host-routing): `curl http://chess.local/` serves the app correctly, but `curl http://localhost/` returns a Caddy `502`. CLAUDE.md's `chess.local` operations note says "no Caddy" — that note is stale or incomplete. Doesn't affect the instance; verify rig deploys via the `chess.local` hostname, not `localhost`. ## Environment State - **Tools/Services:** pnpm workspace; `gitea` CLI for push. Subagent-driven development for execution. - **Active Processes:** none. No dev servers left running. - **Environment Variables:** none added or changed. ## Related Resources - Spec: `docs/superpowers/specs/2026-05-18-table-fidelity-features-design.md` - Plan: `docs/superpowers/plans/2026-05-18-table-fidelity-features.md` - `DECISIONS.md` → "Table-fidelity features (2026-05-18)" - Live URL: https://chess.sethpc.xyz (deployed at `c01244c`) · Repo: https://git.sethpc.xyz/Seth/blind_chess (`main` at `c01244c`) --- **Security Reminder**: No credentials or secrets are included in this handoff.