brackt/docs/superpowers/plans/2026-05-02-phase4-cleanup.md
Chris Parsons 81af8907cc
Add design + 5 phase plans: canonical tournament & participant layer
Spec and phased implementation plans for eliminating data duplication
across overlapping qualifying-points windows (golf, tennis, CS2) by
introducing canonical tournaments, participants, tournament_results,
and surface-Elo tables above the existing per-window layer.

- Phase 1a: rename existing per-window tables to season_* prefix
- Phase 1b: create canonical tables + nullable FKs
- Phase 2:  backfill canonical from existing in-flight windows
- Phase 3:  cutover - sync service, admin UI, simulator read switch
- Phase 4:  cleanup - drop deprecated tables and routes

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 05:20:34 +00:00

359 lines
13 KiB
Markdown

# Phase 4 — Cleanup Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Remove the per-window surface-Elo admin UI + backing table, remove the old batch-add-results wrapper route (keeping the canonical route as the only path), and strip compatibility shims. Leaves the canonical layer as the sole source of truth.
**Architecture:** Pure removal. No new behavior. Each removal is preceded by a verification step proving the removed code is no longer reachable.
**Tech Stack:** Drizzle migration, Vitest, Cypress.
**Prerequisite:** Phase 3 complete and green in production for at least 7 days with active draft activity. Tag `phase-3-complete` exists.
**Reference:** `docs/superpowers/specs/2026-05-01-canonical-tournament-layer-design.md` — Phase 4 section.
---
### Task 1: Verify `season_participant_surface_elos` is no longer read
**Why:** Before dropping the table, prove nothing reads from it.
- [ ] **Step 1: Grep for reads**
Run:
```bash
grep -rn "seasonParticipantSurfaceElos\|season_participant_surface_elos" app/ server/ scripts/ database/
```
Expected: only hits in `database/schema.ts` (the Drizzle export and relation). No app/service reads, no queries.
If any live read remains — stop. Phase 3 was incomplete; fix the read path to go canonical, redeploy, wait 24h, then return here.
- [ ] **Step 2: Production observability check**
Check production logs and/or `pg_stat_user_tables` for activity on `season_participant_surface_elos`:
```sql
SELECT seq_scan, seq_tup_read, idx_scan, idx_tup_fetch, n_tup_upd, n_tup_del
FROM pg_stat_user_tables
WHERE relname = 'season_participant_surface_elos';
```
Check `pg_stat_reset()` was not called recently. If the table has seen reads/writes in the past 7 days (compared against a baseline), investigate before dropping.
- [ ] **Step 3: Commit a note in the plan**
Paste the output of Steps 1 and 2 into a short PR description for the drop migration, documenting verification.
---
### Task 2: Drop `season_participant_surface_elos`
**Files:**
- Modify: `database/schema.ts` (remove export + relations)
- Create: `drizzle/XXXX_drop_season_participant_surface_elos.sql`
- [ ] **Step 1: Remove the table from schema**
In `database/schema.ts`, delete:
- The `seasonParticipantSurfaceElos` `pgTable(...)` export.
- The `seasonParticipantSurfaceElosRelations` export.
- Any references in other `*Relations` exports (e.g., in `seasonParticipantsRelations` or `sportsSeasonsRelations` that include `seasonParticipantSurfaceElos` via `many()`).
- [ ] **Step 2: Generate migration**
Run: `npm run db:generate -- --name=drop_season_participant_surface_elos`
Expected: Migration with a single `DROP TABLE "season_participant_surface_elos"` statement.
- [ ] **Step 3: Verify snapshot chain**
Run: `npm run db:generate`
Expected: "No schema changes".
- [ ] **Step 4: Apply migration on staging and re-run tests**
Run: `npm run db:migrate`
Run: `npm run test:all`
Expected: Green.
- [ ] **Step 5: Apply migration on production**
Follow your normal deploy flow. After deploy, re-verify:
```sql
SELECT tablename FROM pg_tables WHERE tablename = 'season_participant_surface_elos';
```
Expected: no row returned.
- [ ] **Step 6: Commit**
```bash
git add database/schema.ts drizzle/
git commit -m "migration: drop season_participant_surface_elos table
Canonical participant_surface_elos is the sole source of surface Elo
since Phase 3. No reads or writes observed in the past 7 days.
Part of Phase 4 canonical tournament layer cleanup."
```
---
### Task 3: Remove per-window surface-Elo admin UI
**Files:**
- Remove: `app/routes/admin.sports-seasons.$id.surface-elo.tsx`
- Modify: `app/routes.ts` (remove the route registration)
- Modify: any navigation links pointing at the removed page
- [ ] **Step 1: Find references to the removed route**
Run:
```bash
grep -rn "surface-elo" app/ | grep -v "^\.\/app/routes/admin\.sports-seasons\.\\\$id\.surface-elo\.tsx"
```
Expected: admin navigation links + possibly dashboard; update them to point at `/admin/participants/:id/surface-elo` or the canonical participant list.
- [ ] **Step 2: Delete the route file**
```bash
git rm app/routes/admin.sports-seasons.$id.surface-elo.tsx
```
- [ ] **Step 3: Remove route registration**
In `app/routes.ts`, delete the line registering `admin.sports-seasons.$id.surface-elo.tsx`.
- [ ] **Step 4: Update any links**
Edit each file found in Step 1 to redirect or remove the per-window link.
- [ ] **Step 5: Run full test suite**
Run: `npm run test:all`
Expected: Green. Any failing tests that specifically asserted on the old route should be updated or removed.
- [ ] **Step 6: Commit**
```bash
git add app/routes.ts app/
git commit -m "remove: per-window surface-elo admin UI
Canonical participant surface-elo edit page (/admin/participants/:id/surface-elo)
is now the only path. Part of Phase 4 cleanup."
```
---
### Task 4: Remove the batch-add-results wrapper from per-window route
**Goal:** The per-window scoring-event admin route still has a `batch-add-results` intent (added in Phase 3 as a wrapper that writes to canonical). Remove the intent so the route no longer accepts per-window result entry. Admins must use `/admin/tournaments/:id` instead.
**Files:**
- Modify: the per-window route handling qualifying result entry (the one edited in Phase 3 Task 11; find via `grep -l "batch-add-results" app/routes/`)
- Modify: any UI component that still posts to `batch-add-results` on that route
- [ ] **Step 1: Grep for usages of the wrapper**
Run:
```bash
grep -rn "batch-add-results" app/
```
Cross-reference with Phase 3 changes — confirm the only live callers are UI components on per-window pages (which should have been deprecated in Phase 3 release notes).
- [ ] **Step 2: Remove the `batch-add-results` intent branch**
In the per-window route action handler, delete the `if (intent === "batch-add-results") { ... }` branch. Other intents stay.
- [ ] **Step 3: Remove the Batch Result Entry UI from the per-window event page**
Find the component that rendered the "Enter Results" form on per-window pages. Replace with a banner:
```tsx
<div className="p-4 bg-blue-50 rounded">
<p>Result entry has moved.</p>
<Link to={`/admin/tournaments/${event.tournamentId}`} className="underline">
Enter results for {event.name} here.
</Link>
</div>
```
This assumes `event.tournamentId` is loaded in the route loader (it is, after Phase 1b).
- [ ] **Step 4: Remove related E2E tests that exercised the old form**
Search `cypress/e2e/` for tests that used the per-window batch form. Either update to use the new canonical flow or remove if covered by `cypress/e2e/enter-tournament-result.cy.ts`.
- [ ] **Step 5: Run full suite**
Run: `npm run test:all`
Expected: Green.
- [ ] **Step 6: Manual smoke**
Run dev server, log in as admin, navigate to `/admin/sports-seasons/<id>/events/<eventId>` → verify the banner links to the canonical tournament page.
- [ ] **Step 7: Commit**
```bash
git add app/
git commit -m "remove: per-window batch-add-results intent and UI
Result entry consolidated on /admin/tournaments/:id. Per-window event page
shows a deep link to the canonical flow. Part of Phase 4 cleanup."
```
---
### Task 5: Remove unused compatibility shims
**Goal:** Any helper functions, type aliases, or dead code paths added during Phase 3 to keep old callers working.
- [ ] **Step 1: Grep for known shim markers**
If Phase 3 left `@deprecated` JSDoc tags, find them:
```bash
grep -rn "@deprecated" app/ | grep -iE "canonical|tournament|participant"
```
For each, verify no live callers remain (`grep -rn "<function-name>" app/`), then delete.
- [ ] **Step 2: Check the `intent` prop default on `BatchResultEntry`**
If Phase 3 Task 8 added an `intent` prop with default `"batch-add-results"`, change the default to `"batch-upsert-results"` now (the canonical intent) since the old one is gone. Or, if the component is now only used from the canonical route, make the prop required and remove the default.
- [ ] **Step 3: Run full suite + typecheck**
Run: `npm run typecheck && npm run test:all`
Expected: Green.
- [ ] **Step 4: Commit**
```bash
git add app/
git commit -m "chore: remove deprecated shims after Phase 3 cutover"
```
---
### Task 6: Final documentation sweep
**Files:**
- Modify: `docs/agents/database.md` — document canonical vs per-window split
- Modify: `docs/agents/domain-models.md`
- Create: a short section in `docs/` describing how to create a new rolling window (user-facing operator guide)
- [ ] **Step 1: Update `docs/agents/database.md`**
Add a section:
````markdown
## Canonical vs. Per-Window Tables (Qualifying-Points Sports)
Golf, tennis, and CS2 use a split model:
- **Canonical** (real-world identity, shared across seasons):
- `tournaments` — real-world tournaments (Wimbledon 2026)
- `participants` — real-world players/teams (Djokovic, NAVI)
- `tournament_results` — final placements
- `participant_surface_elos` — tennis surface-specific Elo
- **Per-window** (specific to a `sports_seasons` row):
- `season_participants` — draftable roster for this window
- `season_participant_expected_values` — EV under this window's 4-event set
- `season_participant_qualifying_totals` — running QP sum for this window
- `season_participant_results` — final placement for fantasy scoring
- `event_results` — per-scoring-event placements (derived from `tournament_results`)
Enter tournament results at `/admin/tournaments/:id`. The `syncTournamentResults`
service fans out to every window linked via `scoring_events.tournament_id`.
````
- [ ] **Step 2: Update `docs/agents/domain-models.md`**
Replace references to the old per-window participant model with the split canonical/per-window description.
- [ ] **Step 3: Create operator guide**
```markdown
# docs/operations/rolling-windows.md
# Creating a Rolling Qualifying-Points Window
When a new tennis/golf/CS2 "season" should open (typically every 3 months), follow these steps:
1. Go to `/admin/tournaments` and ensure the 4 real-world tournaments exist.
If a tournament is missing, click "New Tournament" on its sport page.
2. Go to `/admin/sports-seasons/new` and pick the sport.
3. In the tournament picker, select the 4 tournaments that define this window.
4. In the roster picker, select the canonical participants you want in the draft pool.
5. Configure scoring (QP point values, roster size, etc.).
6. Submit. The new `sports_seasons` row is created with scoring_events linked to
the canonical tournaments and season_participants linked to canonical participants.
When a real-world tournament completes, enter its results once at
`/admin/tournaments/<id>` — they automatically propagate to every window that
contains it.
```
- [ ] **Step 4: Commit**
```bash
git add docs/
git commit -m "docs: canonical tournament layer — agent guides + operator docs"
```
---
### Task 7: Final regression + tag
- [ ] **Step 1: Full test suite**
Run: `npm run test:all`
Expected: Green.
- [ ] **Step 2: Baseline diff (sanity)**
Re-run baseline capture and diff against Phase 1 fixtures:
```bash
npx tsx scripts/capture-baseline.ts --out=/tmp/post-phase4
diff test-fixtures/baselines/qp-totals-pre-phase1.json /tmp/post-phase4/qp-totals-*.json
diff test-fixtures/baselines/event-results-pre-phase1.json /tmp/post-phase4/event-results-*.json
```
Expected: Empty (no QP math drift across the entire migration).
- [ ] **Step 3: Tag**
```bash
git tag phase-4-complete
git tag canonical-tournament-layer-complete
```
The second tag marks the full feature complete.
---
## Self-Review Checklist
- [ ] `season_participant_surface_elos` table does not exist in prod.
- [ ] `/admin/sports-seasons/:id/surface-elo` route returns 404.
- [ ] Per-window result entry form has been replaced with a link to the canonical route.
- [ ] `grep -rn "batch-add-results" app/` returns zero results.
- [ ] `docs/agents/database.md` describes the canonical/per-window split.
- [ ] Operator guide `docs/operations/rolling-windows.md` exists.
- [ ] `npm run test:all` is green.
- [ ] Baseline diff is empty.
- [ ] Tag `canonical-tournament-layer-complete` exists on main.
## Rollback
Dropping `season_participant_surface_elos` is reversible if needed (no row is dropped in Phase 4 — the data was never used for anything after Phase 3). To rollback:
1. `git revert` the drop migration commit.
2. Re-generate the `season_participant_surface_elos` table via Drizzle migration.
3. Re-seed it from `participant_surface_elos` keyed via `season_participants.participantId`.
Other Phase 4 removals (routes, UI, shims) are pure code and reversible via `git revert`.