Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions docs/architecture/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,15 +176,18 @@ A Durable Object has one native alarm. `AlarmQueue` multiplexes it under the `al
`GameRoom` writes game records to Supabase through RPCs. Game start awaits `create_game_atomic`; everything after that goes through a queue.

- The bridge runs only when the Worker has the Supabase service-role secret. Without it, persistence is skipped and the reason is logged.
- `SupabaseRpcClient` (`packages/cloudflare-do/src/lib/persistence/supabase-rpc.ts`) posts to the PostgREST RPC endpoint for `create_game_atomic`, `complete_game_atomic`, `persist_domain_events`, `abandon_game_atomic` and `aggregate_game_stats`. Each result carries a retriable flag.
- The functions are SECURITY DEFINER plpgsql. They are defined from `supabase/migrations/20260105000002_rpc_create_game.sql` through `supabase/migrations/20260105000006_rpc_aggregate_stats.sql`, and `supabase/tests/rpc_functions.sql` tests them.
- `SupabaseRpcClient` (`packages/cloudflare-do/src/lib/persistence/supabase-rpc.ts`) posts to the PostgREST RPC endpoint for `create_game_atomic`, `complete_game_atomic`, `persist_domain_events`, `abandon_game_atomic` and `aggregate_game_stats`. Array parameters are JSON arrays of objects, which PostgREST converts to the composite types. Each result carries a retriable flag. No Edge Function is involved.
- The functions are SECURITY DEFINER plpgsql executable only by `service_role`. `supabase/migrations/20260914000001_player_stats_projection.sql` holds their current definitions, and `supabase/tests/rpc_functions.sql` and `supabase/tests/player_stats_projection.sql` test them.
- AI seats are `game_players` rows with `is_ai = true`, a NULL `user_id` and the AI profile in `ai_profile`. Seats are keyed by `(game_id, seat_number)`; completion matches human rankings by user id and AI rankings by seat number. An AI winner is stored as a NULL `winner_id`, and only events of human seats are persisted, because both columns reference profiles.
- `player_stats` is a projection that clients only read. `rebuild_player_stats(user_id)` recomputes a row from completed games and `TurnScored` events, so repeats and retries give the same row. `complete_game_atomic` refreshes the game's human seats in its transaction, and `aggregate_game_stats` refreshes them again after the domain events land.
- Projection rules: a game counts when it is `completed` and the seat has a final score. A win is `final_rank = 1` in a game with more than one seat, AI seats included. Decisions are `TurnScored` events with a boolean `was_optimal`.
- `PersistenceQueue` (`packages/cloudflare-do/src/lib/persistence/persistence-queue.ts`) stores tasks in the SQLite table `persistence_queue`. Task types are `PERSIST_GAME_COMPLETION`, `PERSIST_DOMAIN_EVENTS`, `TRIGGER_AGGREGATION` and `ABANDON_GAME`.
- At game end, `GameRoom` queues the completion, the domain events and, 500 ms later, the stats aggregation.
- The queue shares the native alarm. It moves the alarm only when its own task is due earlier.
- The domain-event sequence number survives hibernation as `event_sequence` in `game_metadata`. Completion rankings carry scorecards and AI flags.
- The domain-event sequence number survives hibernation as `event_sequence` in `game_metadata`. Completion rankings carry scorecards, seat numbers and AI flags.
- `packages/cloudflare-do/src/lib/persistence/schema-validation.ts` checks at compile time that persistence records are assignable to the generated Supabase insert types. Regenerating those types is an authenticated operator step, not part of the local gate.

Open defects: array RPC parameters, AI seats, abandonment scheduling and unchecked SQLite reads. They are listed under Supabase obligations in the [roadmap](../roadmap.md). Whether to repair the pipeline or freeze it is the owner's decision.
Open defects: abandonment scheduling and unchecked SQLite reads. They are listed under Supabase obligations in the [roadmap](../roadmap.md).

## Connection UX principles

Expand Down
8 changes: 4 additions & 4 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ Ordered next work. State, decisions and deadlines live in [status.md](status.md)
The backup, profile-role hardening/audit, two-script Worker URL restrictions and credential containment are recorded as complete in [status](status.md#latest-live-readbacks). Status action numbers are stable references; this is the execution order. Preserve durable facts (Supabase data, migrations, backups), not obsolete runtime topology: fix forward, delete classified legacy surfaces, and make the next production release the architecture we keep. Each production step retains its operator authority and stop points.

1. Read-only discovery (actions 5 and 8): `GameRoom` and `GlobalLobby` owners through the Durable Objects Deployments tab, SQLite and migration tags, production and preview `GAME_WORKER` targets, routes and domains on every Dicee Worker script and Pages project, last deployments of `dicee` and `dicee-production`, secret names on `dicee`, and whether `dicee.games` and `www` sit in a zone on this account and with which record types. Do not measure ephemeral Durable Object state — private readback, then a status decision naming the scripts and projects to delete.
2. Stats correctness (action 7): rebuild `player_stats` as a projection of completed `games`, `game_players` and `domain_events`, so repeats and retries give the same row; store AI seats; remove the Edge Function caller, the ratings/badge flags and unused web stats writers; rebuild existing rows once. The migration must apply alone before or after `000002` — pgTAP for repeats, retries, solo, multiplayer, AI seats, incomplete games and missing events.
2. Fix the self-referencing `game_players` SELECT policy: its subquery's unqualified `game_id` makes signed-in reads fail with infinite recursion, which breaks game history once games persist. Ship a migration that applies alone, like `20260914000001` — pgTAP as a seated player, a spectator and an outsider.
3. Establish the `main` ruleset, Production deployment protections and Dependabot controls (action 10) before the release. Require the exact check `Full repository validation`; inspect bypass behavior and the existing Wrangler/Miniflare ignore policy — authenticated control readbacks.
4. First release (action 3) from a clean checkout of the exact successful CI commit, at a quiet time, with operator commands rather than a CI dispatch: apply the stats migration alone, deploy `dicee-web`, detach `dicee.games` from Pages and attach it to `dicee-web` (short outage), then deploy `dicee` (its protocol gate closes the old Pages client's sockets), run the smoke checks, then delete the Pages project, the deployed Edge Function and the classified scripts (action 8) — sign-in, room/lobby, headers, transcription, non-admin refusal and deletion readbacks.
4. First release (action 3) from a clean checkout of the exact successful CI commit, at a quiet time, with operator commands rather than a CI dispatch: read back the completed `games` count, `player_stats` rows and summed `games_played` (the one-time rebuild resets stats that no completed game backs), apply `20260914000001` and the policy fix alone, deploy `dicee-web`, detach `dicee.games` from Pages and attach it to `dicee-web` (short outage), then deploy `dicee` (its protocol gate closes the old Pages client's sockets), run the smoke checks including one completed game, delete the deployed `aggregate-game-stats` Edge Function and rebuild all `player_stats` once, then delete the Pages project and the classified scripts (action 8) — sign-in, room/lobby, a persisted game with stats, headers, transcription, non-admin refusal and deletion readbacks.
5. Implement the profile visibility opt-in control, initially off for private profiles, writing `profiles.is_public`; explain that visibility is voluntary and cover it with tests. Merge through the new ruleset — successful full validation on the PR.
6. Deploy the opt-in code, verify the control and a test bug report, then take a fresh complete encrypted backup. Recheck the production link and history (`000001` remote, `000002` local-only), apply only `000002`, and verify the schema and two-account privacy behavior (action 4). Invite opt-ins only after verification; the migration clears earlier opt-ins. Fix forward — fresh backup evidence, migration readback and privacy tests.

Do not reapply or reverse `000001`, run a broad database push, release current `main` as it stands, or treat a successful dry run as namespace proof. Hosted multiplayer testing waits for an isolated backend (section 8).
Do not reapply or reverse `000001`, run a broad database push, or treat a successful dry run as namespace proof. Hosted multiplayer testing waits for an isolated backend (section 8).

## 2. Supabase obligations

Expand All @@ -25,7 +25,7 @@ Date-driven and independent of the organization move. Agents write and test the
- Asymmetric JWT signing: decide how to clear audit warning B8 ([open decision](cloudflare.md#open-decisions); default: remove HS256), then pin the JWKS verification algorithms and remove the HS256 fallback, `SUPABASE_JWT_SECRET` and the trailing `packages/cloudflare-do/wrangler.jsonc` comment that asks for it — auth tests; operator confirms the signing-key state first.
- Minimize Supabase after a row-count readback and a fresh dump: drop vestigial tables, RPCs and columns (gallery, `solo_leaderboard`, `rooms`, `analysis_events`, `feature_flags`, spectator policies, Glicko and badge columns) with their TypeScript; keep `log_admin_action` as the only admin audit writer; close the `bug_reports` delete-policy gap — pgTAP green; operator applies.
- Residual policy fixes for whatever minimization keeps: the open read policies on `admin_permissions` and `feature_flags`, `SET search_path` on the gallery security-definer functions, and spectator policies that match a `playing` status the `games` check never allows — pgTAP green; operator applies.
- Persistence pipeline repair after the section 1 stats fix: jsonb RPC parameters instead of hand-built array literals, scheduled abandonment, schema-validated outbox rows and surfaced permanent failures — tests.
- Persistence follow-ups: the Worker's `TurnScored` events carry `was_optimal` and `ev_difference`, so Decision Quality stops reading 0; scheduled abandonment, schema-validated outbox rows and surfaced permanent failures — tests.

## 3. Worker correctness and security

Expand Down
8 changes: 4 additions & 4 deletions docs/status.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Dicee status

**As of:** 2026-09-14T16:56:19Z
**As of:** 2026-09-14T17:14:24Z

**Current phase:** 2026-09 operator safety rollout; discovery (actions 5 and 8) next, then a first release that ships stats correctness and the dicee-web cutover (no deployment)

Next work: [roadmap.md](roadmap.md). Cloudflare: [cloudflare.md](cloudflare.md).
## Current state

- `main` carries the reviewed baseline, the database privacy work and current dependency updates (Vitest 5, jsdom 30). Its stats aggregation double-counts (action 7), so current `main` is not released as it stands.
- `main` carries the reviewed baseline, the database privacy work, current dependency updates (Vitest 5, jsdom 30), the `dicee-web` Worker, the protocol handshake and the stats fix (action 7); none of it is deployed.
- Migration `20260913000001` is applied to the hosted project and recorded in migration history. Authenticated clients cannot update `profiles.role`; their editable profile fields remain granted. Migration `20260913000002` remains local-only.
- The profile-role audit (action 2) is complete: 7 profiles comprise 5 users and 2 super admins, with no moderators or admins. The operator confirmed both elevated assignments as intentional after private record review; no role changes were needed. Audit-log absence cannot establish that the old privilege was never exploited.
- The database backup is encrypted and verified on off-machine storage; Storage contained 0 objects. The plaintext exports were removed after verification.
Expand Down Expand Up @@ -36,11 +36,11 @@ These are stable action identifiers, not execution order. [Roadmap section 1](ro

1. [x] **Backup.** The operator confirmed the Free plan. Five SQL dumps and the Storage inventory were verified in an AES-256 image on off-machine storage; Storage contained 0 objects. Plaintext exports were removed only after the copied image passed verification.
2. [x] **Profile-role audit.** Migration `20260913000001` is applied and recorded; do not reapply it. Role counts and both elevated records were retrieved privately. The operator confirmed both super-admin assignments as intentional; 0 unresolved accounts and 0 corrections.
3. [ ] **First production release.** After actions 5 and 10, with the action 7 fix merged, release the exact validated commit from a clean checkout at a quiet time: apply the stats migration alone, deploy `dicee-web`, move `dicee.games` off Pages, then deploy `dicee` (its protocol gate closes the old Pages client's sockets) and run the [post-deploy smoke checks](cloudflare.md#deploy-path). Do not release current `main` as it stands.
3. [ ] **First production release.** After actions 5 and 10, release the exact validated commit from a clean checkout at a quiet time: read back completed game and `player_stats` counts (the stats rebuild resets stats no completed game backs), apply `20260914000001` and the `game_players` policy fix alone, deploy `dicee-web`, move `dicee.games` off Pages, deploy `dicee` (its protocol gate closes the old Pages client's sockets), run the [post-deploy smoke checks](cloudflare.md#deploy-path), then delete the deployed Edge Function and rebuild all stats once.
4. [ ] **Apply `20260913000002`** only after the missing profile visibility opt-in control is implemented, tested, merged and verified in production. Current source omits the four bug-report fields this migration drops, but deployed compatibility is unverified. Take a fresh complete encrypted backup, recheck the production link and migration history, and apply only this migration. It resets all profiles to private; verify the schema and two-account privacy behavior before inviting opt-ins. Do not improvise a reverse migration; fix forward with the compatible opt-in build.
5. [ ] **Worker namespace and binding check.** Read back which scripts hold `GameRoom` and `GlobalLobby` (the Durable Objects Deployments tab shows backing Worker versions), whether both use SQLite, migration tags, and the production/preview `GAME_WORKER` targets. The result decides between a no-op lifecycle deploy to `dicee` and a cutover to it (decision 1); stop only on ambiguous ownership ([method](cloudflare.md#live-checks-still-needed)).
6. [x] **Worker subdomain URLs.** `workers.dev` and Preview URLs are disabled on `dicee` and `dicee-production`, verified by API. Namespace ownership and other ingress remain for the later reviews in actions 5 and 8.
7. [ ] **Stats aggregation.** Current `main` counts each completed game at least twice: the SQL `aggregate_game_stats` and the `aggregate-game-stats` Edge Function both add it, and a retry reruns both. Games with AI seats likely fail to persist. Replace it with a rebuildable `player_stats` projection and AI-seat storage (roadmap section 1); delete the deployed Edge Function after the release that stops calling it.
7. [ ] **Stats aggregation.** Fixed in source: `20260914000001` makes `player_stats` a rebuildable projection and stores AI seats, and the Worker now sends JSON arrays (its old array literals were rejected, so the deployed Worker has likely never persisted a game). Complete with action 3: delete the deployed `aggregate-game-stats` Edge Function, then rebuild all stats once.
8. [ ] **Delete obsolete surfaces.** After action 5, delete every other Dicee Worker script and Pages project (the `dicee` Pages project after the cutover), with their routes, domains and secrets. Live Durable Object state on them is not preserved.
9. [x] **Credential containment.** Both exposed tokens are replaced, revoked and verified dead by direct HTTP 401 readbacks. Cloudflare: the replacement is canonical in 1Password and GitHub Production, and the repository duplicate is removed. Supabase: a project-scoped token with only Database read-write access is the sole CLI credential; temporary copies, the environment override, the fallback token file and plaintext copies are absent.
10. [ ] **GitHub governance.** A `main` ruleset requiring the check **Full repository validation** (the job display name, not `validate`); a `Production` environment with required reviewers and a main-only deployment branch policy; Dependabot alerts and security updates.
Expand Down
Loading