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>
13 KiB
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:
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:
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
seasonParticipantSurfaceElospgTable(...)export. -
The
seasonParticipantSurfaceElosRelationsexport. -
Any references in other
*Relationsexports (e.g., inseasonParticipantsRelationsorsportsSeasonsRelationsthat includeseasonParticipantSurfaceElosviamany()). -
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:
SELECT tablename FROM pg_tables WHERE tablename = 'season_participant_surface_elos';
Expected: no row returned.
- Step 6: Commit
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:
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
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
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-resultson that route -
Step 1: Grep for usages of the wrapper
Run:
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-resultsintent 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:
<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
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:
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
intentprop default onBatchResultEntry
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
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:
## 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
# 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
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:
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
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_elostable does not exist in prod./admin/sports-seasons/:id/surface-eloroute 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.mddescribes the canonical/per-window split.- Operator guide
docs/operations/rolling-windows.mdexists. npm run test:allis green.- Baseline diff is empty.
- Tag
canonical-tournament-layer-completeexists 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:
git revertthe drop migration commit.- Re-generate the
season_participant_surface_elostable via Drizzle migration. - Re-seed it from
participant_surface_eloskeyed viaseason_participants.participantId.
Other Phase 4 removals (routes, UI, shims) are pure code and reversible via git revert.