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>
359 lines
13 KiB
Markdown
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`.
|