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>
638 lines
26 KiB
Markdown
638 lines
26 KiB
Markdown
# Phase 1a — Rename Per-Window Tables 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:** Rename the existing per-window tables (`participants`, `participant_expected_values`, `participant_qualifying_totals`, `participant_results`, `participant_surface_elos`) to `season_`-prefixed names, and rename `event_results.participant_id` to `season_participant_id`. Rename `app/models/participant.ts` to `app/models/season-participant.ts`. Ship a fully green build with zero behavior changes.
|
||
|
||
**Architecture:** Pure mechanical rename across ~100 files. No new tables, no new columns, no logic changes. This phase must land cleanly before Phase 1b can create the new canonical `participants` table (which reuses the freed name).
|
||
|
||
**Tech Stack:** Drizzle ORM, PostgreSQL, React Router 7, TypeScript, Vitest.
|
||
|
||
**Reference:** `docs/superpowers/specs/2026-05-01-canonical-tournament-layer-design.md` — Phase 1a section.
|
||
|
||
**Rename mapping (authoritative):**
|
||
|
||
| Old (table / column / symbol) | New |
|
||
|---|---|
|
||
| table `participants` | `season_participants` |
|
||
| table `participant_expected_values` | `season_participant_expected_values` |
|
||
| table `participant_qualifying_totals` | `season_participant_qualifying_totals` |
|
||
| table `participant_results` | `season_participant_results` |
|
||
| table `participant_surface_elos` | `season_participant_surface_elos` |
|
||
| column `event_results.participant_id` | `event_results.season_participant_id` |
|
||
| Drizzle export `participants` | `seasonParticipants` |
|
||
| Drizzle export `participantExpectedValues` | `seasonParticipantExpectedValues` |
|
||
| Drizzle export `participantQualifyingTotals` | `seasonParticipantQualifyingTotals` |
|
||
| Drizzle export `participantResults` | `seasonParticipantResults` |
|
||
| Drizzle export `participantSurfaceElos` | `seasonParticipantSurfaceElos` |
|
||
| Drizzle export `participantsRelations` | `seasonParticipantsRelations` |
|
||
| Drizzle export `participantExpectedValuesRelations` | `seasonParticipantExpectedValuesRelations` |
|
||
| Drizzle export `participantQualifyingTotalsRelations` | `seasonParticipantQualifyingTotalsRelations` |
|
||
| Drizzle export `participantResultsRelations` | `seasonParticipantResultsRelations` |
|
||
| Drizzle export `participantSurfaceElosRelations` | `seasonParticipantSurfaceElosRelations` |
|
||
| File `app/models/participant.ts` | `app/models/season-participant.ts` |
|
||
| Relation field `eventResults.participant` | `eventResults.seasonParticipant` |
|
||
|
||
**Files affected (counts from pre-flight audit):**
|
||
- `database/schema.ts` (1 — critical)
|
||
- `app/models/index.ts` (1)
|
||
- `app/models/` — 11 model files + 2 test files
|
||
- `app/routes/` — ~20 route files
|
||
- `app/services/` — probability-updater + 19 simulator files
|
||
- `app/utils/` — `sports-data-sync.server.ts`
|
||
- `server/` — `socket.ts`, test files
|
||
- `cypress/` — verify none reference these symbols by name
|
||
|
||
**Note on granularity:** Because this is a mechanical rename, tasks are batched by scope rather than broken into micro-TDD steps. Each task ends with typecheck + test run as the verification step.
|
||
|
||
---
|
||
|
||
### Task 0: Create baseline snapshot (one-time, before any code changes)
|
||
|
||
**Why:** Phase 2 verification depends on proving QP totals didn't drift. Capture now.
|
||
|
||
**Files:**
|
||
- Create: `scripts/capture-baseline.ts`
|
||
|
||
- [ ] **Step 1: Write the capture script**
|
||
|
||
```typescript
|
||
// scripts/capture-baseline.ts
|
||
import { db } from "~/db/context";
|
||
import { sql } from "drizzle-orm";
|
||
import { writeFileSync, mkdirSync } from "fs";
|
||
import { join } from "path";
|
||
|
||
const OUT_DIR = join(process.cwd(), "test-fixtures", "baselines");
|
||
|
||
async function main() {
|
||
mkdirSync(OUT_DIR, { recursive: true });
|
||
|
||
const qpTotals = await db.execute(sql`
|
||
SELECT pqt.*, ss.id as sports_season_id, s.name as sport_name
|
||
FROM participant_qualifying_totals pqt
|
||
JOIN sports_seasons ss ON ss.id = pqt.sports_season_id
|
||
JOIN sports s ON s.id = ss.sport_id
|
||
WHERE ss.scoring_pattern = 'qualifying_points'
|
||
ORDER BY ss.id, pqt.participant_id
|
||
`);
|
||
|
||
const eventResults = await db.execute(sql`
|
||
SELECT er.*, se.sports_season_id
|
||
FROM event_results er
|
||
JOIN scoring_events se ON se.id = er.scoring_event_id
|
||
JOIN sports_seasons ss ON ss.id = se.sports_season_id
|
||
WHERE ss.scoring_pattern = 'qualifying_points'
|
||
AND er.qualifying_points_awarded IS NOT NULL
|
||
ORDER BY se.sports_season_id, er.scoring_event_id, er.participant_id
|
||
`);
|
||
|
||
const surfaceElos = await db.execute(sql`
|
||
SELECT pse.*, ss.sport_id
|
||
FROM participant_surface_elos pse
|
||
JOIN sports_seasons ss ON ss.id = pse.sports_season_id
|
||
ORDER BY ss.id, pse.participant_id
|
||
`);
|
||
|
||
writeFileSync(join(OUT_DIR, "qp-totals-pre-phase1.json"), JSON.stringify(qpTotals.rows, null, 2));
|
||
writeFileSync(join(OUT_DIR, "event-results-pre-phase1.json"), JSON.stringify(eventResults.rows, null, 2));
|
||
writeFileSync(join(OUT_DIR, "surface-elos-pre-phase1.json"), JSON.stringify(surfaceElos.rows, null, 2));
|
||
|
||
console.log(`Wrote baselines: ${qpTotals.rows.length} QP totals, ${eventResults.rows.length} event results, ${surfaceElos.rows.length} surface elo rows.`);
|
||
}
|
||
|
||
main().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1); });
|
||
```
|
||
|
||
- [ ] **Step 2: Run against production data read replica (or local dev DB loaded from `npm run db:sync-prod`)**
|
||
|
||
Run: `npx tsx scripts/capture-baseline.ts`
|
||
Expected: three JSON files created under `test-fixtures/baselines/`; non-zero row counts for QP totals and surface elos; Masters 2026 results visible in `event-results-pre-phase1.json`.
|
||
|
||
- [ ] **Step 3: Also capture Monte Carlo simulator output baselines**
|
||
|
||
Extend `scripts/capture-baseline.ts` to, for each in-flight qualifying-points `sports_seasons` row, run the matching simulator with a **fixed random seed** and save its output. This JSON is what Phase 3 Task 6's tennis-simulator regression test reads.
|
||
|
||
Pattern:
|
||
|
||
```typescript
|
||
// Pseudocode appended to scripts/capture-baseline.ts main():
|
||
import { getSimulator } from "~/services/simulations/registry";
|
||
// ... for each ss in qualifying-points seasons:
|
||
const sim = getSimulator(ss.simulatorType!);
|
||
const output = await sim.run(ss.id, { seed: 42, iterations: 10000 });
|
||
writeFileSync(join(OUT_DIR, `sim-output-${ss.simulatorType}-${ss.id}.json`), JSON.stringify(output, null, 2));
|
||
```
|
||
|
||
Check the existing `Simulator` interface in `app/services/simulations/types.ts` for the actual method signature; adapt accordingly. If a simulator does not currently accept a `seed`, add one as a non-breaking optional param in this task — it's needed for reproducible baselines.
|
||
|
||
- [ ] **Step 4: Commit the baseline fixtures**
|
||
|
||
```bash
|
||
git add scripts/capture-baseline.ts test-fixtures/baselines/
|
||
git commit -m "chore: capture pre-rename baselines for qualifying-points data and simulator output"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 1: Rename schema exports and table names in `database/schema.ts`
|
||
|
||
**Why first:** Schema is the source of truth; the whole codebase compiles against its exports. Easiest to do in one sweep.
|
||
|
||
**Files:**
|
||
- Modify: `database/schema.ts`
|
||
|
||
- [ ] **Step 1: Rename table definitions and string names**
|
||
|
||
Apply these replacements in `database/schema.ts`. The safest order: string names first (they're unambiguous), then export names (which must be done with care to avoid breaking references in relations).
|
||
|
||
Table rename block 1 — `participants` (~ lines 376-390):
|
||
```typescript
|
||
// OLD:
|
||
export const participants = pgTable("participants", {
|
||
// ...
|
||
});
|
||
// NEW:
|
||
export const seasonParticipants = pgTable("season_participants", {
|
||
// ...
|
||
});
|
||
```
|
||
|
||
Table rename block 2 — `participant_expected_values` (~ lines 642-672):
|
||
```typescript
|
||
// OLD:
|
||
export const participantExpectedValues = pgTable("participant_expected_values", { ... });
|
||
// NEW:
|
||
export const seasonParticipantExpectedValues = pgTable("season_participant_expected_values", { ... });
|
||
```
|
||
|
||
Table rename block 3 — `participant_qualifying_totals` (~ lines 537-550):
|
||
```typescript
|
||
// OLD:
|
||
export const participantQualifyingTotals = pgTable("participant_qualifying_totals", { ... });
|
||
// NEW:
|
||
export const seasonParticipantQualifyingTotals = pgTable("season_participant_qualifying_totals", { ... });
|
||
```
|
||
|
||
Table rename block 4 — `participant_results` (~ lines 414-428):
|
||
```typescript
|
||
// OLD:
|
||
export const participantResults = pgTable("participant_results", { ... });
|
||
// NEW:
|
||
export const seasonParticipantResults = pgTable("season_participant_results", { ... });
|
||
```
|
||
|
||
Table rename block 5 — `participant_surface_elos` (~ lines 1219-1235):
|
||
```typescript
|
||
// OLD:
|
||
export const participantSurfaceElos = pgTable("participant_surface_elos", { ... });
|
||
// NEW:
|
||
export const seasonParticipantSurfaceElos = pgTable("season_participant_surface_elos", { ... });
|
||
```
|
||
|
||
Column rename in `eventResults` table (~ lines 458-476):
|
||
```typescript
|
||
// OLD:
|
||
participantId: uuid("participant_id").notNull().references(() => participants.id, { onDelete: "cascade" }),
|
||
// NEW:
|
||
seasonParticipantId: uuid("season_participant_id").notNull().references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||
```
|
||
|
||
- [ ] **Step 2: Rename relations exports and their field references**
|
||
|
||
In `database/schema.ts`, update each `*Relations` export and every `one()/many()` call that referenced the renamed tables. Replacements needed:
|
||
- `participantsRelations` → `seasonParticipantsRelations`
|
||
- `participantExpectedValuesRelations` → `seasonParticipantExpectedValuesRelations`
|
||
- `participantQualifyingTotalsRelations` → `seasonParticipantQualifyingTotalsRelations`
|
||
- `participantResultsRelations` → `seasonParticipantResultsRelations`
|
||
- `participantSurfaceElosRelations` → `seasonParticipantSurfaceElosRelations`
|
||
|
||
Within `eventResultsRelations` (~ lines 952-961):
|
||
```typescript
|
||
// OLD:
|
||
participant: one(participants, {
|
||
fields: [eventResults.participantId],
|
||
references: [participants.id],
|
||
}),
|
||
// NEW:
|
||
seasonParticipant: one(seasonParticipants, {
|
||
fields: [eventResults.seasonParticipantId],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
```
|
||
|
||
Update every other relation `one()`/`many()` that referenced `participants`, `participantResults`, `participantQualifyingTotals`, `participantSurfaceElos`, or `participantExpectedValues` to use the new table export names.
|
||
|
||
- [ ] **Step 3: Verify schema parses**
|
||
|
||
Run: `npm run typecheck`
|
||
Expected: Will fail with many errors in model/route files that still import old names. That's fine for now — schema.ts itself compiling is the win. Confirm the errors are only in files outside `database/schema.ts`.
|
||
|
||
- [ ] **Step 4: Commit schema rename alone**
|
||
|
||
```bash
|
||
git add database/schema.ts
|
||
git commit -m "refactor(schema): rename per-window tables to season_* prefix
|
||
|
||
Renames participants, participant_expected_values, participant_qualifying_totals,
|
||
participant_results, participant_surface_elos to season_* prefixed names.
|
||
Renames event_results.participant_id to season_participant_id.
|
||
Phase 1a of canonical tournament layer migration."
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: Generate and hand-edit the Drizzle rename migration
|
||
|
||
**Why:** `drizzle-kit generate` treats renames as drop+create by default; this loses data. For safe renames, we generate with `--custom` and hand-edit, OR use `drizzle-kit generate` and answer its interactive rename prompts. Using custom is safer here because multiple renames in one migration can confuse the prompt sequence.
|
||
|
||
**Files:**
|
||
- Create: `drizzle/0087_rename_per_window_tables.sql` (filename assigned by drizzle-kit; adjust to whatever it generates)
|
||
- Create: `drizzle/meta/0087_snapshot.json` (generated)
|
||
|
||
- [ ] **Step 1: Generate a custom migration scaffold**
|
||
|
||
Run: `npm run db:generate -- --custom --name=rename_per_window_tables`
|
||
Expected: Creates a new empty `.sql` file in `drizzle/` and a paired snapshot in `drizzle/meta/`.
|
||
|
||
- [ ] **Step 2: Write the rename SQL manually**
|
||
|
||
Open the new `drizzle/XXXX_rename_per_window_tables.sql` and replace its contents with:
|
||
|
||
```sql
|
||
-- Rename per-window tables to season_* prefix
|
||
-- Part of canonical tournament layer migration (Phase 1a)
|
||
|
||
-- Drop the FK on event_results.participant_id before renaming target
|
||
ALTER TABLE "event_results" DROP CONSTRAINT IF EXISTS "event_results_participant_id_participants_id_fk";
|
||
|
||
-- Rename tables
|
||
ALTER TABLE "participants" RENAME TO "season_participants";
|
||
ALTER TABLE "participant_expected_values" RENAME TO "season_participant_expected_values";
|
||
ALTER TABLE "participant_qualifying_totals" RENAME TO "season_participant_qualifying_totals";
|
||
ALTER TABLE "participant_results" RENAME TO "season_participant_results";
|
||
ALTER TABLE "participant_surface_elos" RENAME TO "season_participant_surface_elos";
|
||
|
||
-- Rename column on event_results
|
||
ALTER TABLE "event_results" RENAME COLUMN "participant_id" TO "season_participant_id";
|
||
|
||
-- Re-add the FK against the renamed table
|
||
ALTER TABLE "event_results"
|
||
ADD CONSTRAINT "event_results_season_participant_id_season_participants_id_fk"
|
||
FOREIGN KEY ("season_participant_id") REFERENCES "season_participants"("id")
|
||
ON DELETE cascade ON UPDATE no action;
|
||
|
||
-- Rename any unique indexes that embed the old table names
|
||
ALTER INDEX IF EXISTS "participant_surface_elos_unique" RENAME TO "season_participant_surface_elos_unique";
|
||
-- (Add further ALTER INDEX ... RENAME TO for any other indexes whose names embed old table names.
|
||
-- Run `\d <new_table_name>` in psql against a fresh migrate to confirm nothing lingers.)
|
||
```
|
||
|
||
**Note:** FK and index constraint names are auto-generated based on table names. After the `ALTER TABLE ... RENAME TO`, Postgres does NOT auto-rename the FK constraint on `event_results` pointing at the old name — we drop and re-create explicitly above. Run the migration against a scratch DB and inspect `\d event_results` and `\d season_participants` to catch any leftover naming.
|
||
|
||
- [ ] **Step 3: Verify snapshot is consistent**
|
||
|
||
Run: `npm run db:generate`
|
||
Expected: "No schema changes" — confirms the snapshot chain is valid. If drizzle-kit wants to generate another migration, stop and diagnose; the snapshot is out of sync with the custom SQL.
|
||
|
||
- [ ] **Step 4: Run migration on local DB**
|
||
|
||
Run: `npm run db:migrate`
|
||
Expected: New migration applied; existing data intact (verify row counts match pre-rename via `psql ... -c "SELECT count(*) FROM season_participants;"` = prior `SELECT count(*) FROM participants;`).
|
||
|
||
- [ ] **Step 5: Commit migration**
|
||
|
||
```bash
|
||
git add drizzle/
|
||
git commit -m "migration: rename per-window tables to season_* prefix"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: Rename and update `app/models/participant.ts` → `app/models/season-participant.ts`
|
||
|
||
**Files:**
|
||
- Rename: `app/models/participant.ts` → `app/models/season-participant.ts`
|
||
- Modify: `app/models/index.ts`
|
||
|
||
- [ ] **Step 1: Git-rename the file**
|
||
|
||
```bash
|
||
git mv app/models/participant.ts app/models/season-participant.ts
|
||
```
|
||
|
||
- [ ] **Step 2: Update the file's internal symbol references**
|
||
|
||
Open `app/models/season-participant.ts`. Replace all references to renamed Drizzle exports:
|
||
- `participants` → `seasonParticipants`
|
||
- `participantResults` → `seasonParticipantResults` (if referenced)
|
||
- `participantQualifyingTotals` → `seasonParticipantQualifyingTotals` (if referenced)
|
||
|
||
Update any exported function names that embed "participant" that logically should now be "seasonParticipant" — but **only if they are window-scoped**. E.g., a function `findParticipantsForSeason` likely stays named as-is since callers in routes don't distinguish; a function `createParticipant(sportsSeasonId, data)` likely stays named. Rename only if the name would be genuinely misleading. **Default: keep exported function names to minimize route-layer churn.**
|
||
|
||
- [ ] **Step 3: Update `app/models/index.ts`**
|
||
|
||
Change:
|
||
```typescript
|
||
export * from "./participant";
|
||
```
|
||
To:
|
||
```typescript
|
||
export * from "./season-participant";
|
||
```
|
||
|
||
- [ ] **Step 4: Verify file compiles in isolation**
|
||
|
||
Run: `npx tsc --noEmit app/models/season-participant.ts` (may still reference other unrenamed files — acceptable as long as the error is about imports, not the rename itself.)
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add app/models/season-participant.ts app/models/index.ts
|
||
git commit -m "refactor: rename participant.ts model file to season-participant.ts"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: Update all model files referencing renamed schema exports
|
||
|
||
**Files (update each to use new export names):**
|
||
- `app/models/draft-pick.ts`
|
||
- `app/models/draft-utils.ts`
|
||
- `app/models/event-result.ts`
|
||
- `app/models/group-stage-match.ts`
|
||
- `app/models/participant-result.ts`
|
||
- `app/models/qualifying-points.ts`
|
||
- `app/models/scoring-calculator.ts`
|
||
- `app/models/scoring-event.ts`
|
||
- `app/models/sports-season.ts`
|
||
- `app/models/surface-elo.ts`
|
||
- `app/models/team-score-events.ts`
|
||
|
||
- [ ] **Step 1: In each file, replace imports and references**
|
||
|
||
Replacement map for imports and schema symbol references throughout the file body:
|
||
|
||
| Old symbol | New symbol |
|
||
|---|---|
|
||
| `participants` (Drizzle table) | `seasonParticipants` |
|
||
| `participantExpectedValues` | `seasonParticipantExpectedValues` |
|
||
| `participantQualifyingTotals` | `seasonParticipantQualifyingTotals` |
|
||
| `participantResults` | `seasonParticipantResults` |
|
||
| `participantSurfaceElos` | `seasonParticipantSurfaceElos` |
|
||
| `eventResults.participantId` | `eventResults.seasonParticipantId` |
|
||
| Relation access `.participant` on an `eventResults` row | `.seasonParticipant` |
|
||
|
||
**Keep unchanged:**
|
||
- Local variable names like `participant`, `participants`, `participantId` — these are runtime identifiers used by business logic; renaming them would cascade into hundreds of lines for zero benefit. Only the *schema symbol* and *Drizzle relation field* change.
|
||
- Function signatures and param names.
|
||
- JSDoc.
|
||
|
||
**Example `draft-pick.ts` update:**
|
||
|
||
```typescript
|
||
// BEFORE
|
||
import { participants, draftPicks } from "database/schema";
|
||
const [p] = await db.select().from(participants).where(eq(participants.id, participantId));
|
||
|
||
// AFTER
|
||
import { seasonParticipants, draftPicks } from "database/schema";
|
||
const [p] = await db.select().from(seasonParticipants).where(eq(seasonParticipants.id, participantId));
|
||
// ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^ schema symbol only
|
||
// `participantId` param name stays.
|
||
```
|
||
|
||
- [ ] **Step 2: Run typecheck after each batch of 3 files**
|
||
|
||
Run: `npm run typecheck 2>&1 | head -40`
|
||
Expected: Errors shrink with each file updated. Stop and diagnose if error count grows.
|
||
|
||
- [ ] **Step 3: Update model test files**
|
||
|
||
Files:
|
||
- `app/models/__tests__/sports-season.clone.test.ts`
|
||
- `app/models/__tests__/auto-pick.test.ts`
|
||
- `app/models/__tests__/executeAutoPick.timer.test.ts`
|
||
|
||
Same replacement rules. Run affected tests after update:
|
||
|
||
Run: `npm run test:run -- app/models/__tests__`
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add app/models/
|
||
git commit -m "refactor(models): update model layer to use renamed schema exports"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: Update all route files referencing renamed schema exports
|
||
|
||
**Files:**
|
||
- `app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts`
|
||
- `app/routes/admin.sports-seasons.$id.events.$eventId.server.ts`
|
||
- `app/routes/admin.sports-seasons.$id.events.$eventId.cs2-setup.tsx`
|
||
- `app/routes/admin.sports-seasons.$id.expected-values.server.ts`
|
||
- `app/routes/admin.sports-seasons.$id.surface-elo.tsx`
|
||
- `app/routes/admin.sports-seasons.$id.simulate.tsx`
|
||
- `app/routes/admin.sports-seasons.$id.standings.tsx`
|
||
- `app/routes/admin.sports-seasons.$id.elo-ratings.tsx`
|
||
- `app/routes/admin.sports-seasons.$id.tsx`
|
||
- `app/routes/admin.sports-seasons.$id.golf-skills.tsx`
|
||
- `app/routes/admin.sports-seasons.$id.regular-standings.tsx`
|
||
- `app/routes/admin.sports-seasons.$id.futures-odds.tsx`
|
||
- `app/routes/admin.data-sync.tsx`
|
||
- `app/routes/api/draft.force-manual-pick.ts`
|
||
- `app/routes/api/draft.make-pick.ts`
|
||
- `app/routes/api/draft.replace-pick.ts`
|
||
- `app/routes/api/seasons.$seasonId.draft.ts`
|
||
- `app/routes/leagues/$leagueId.draft-board.$seasonId.tsx`
|
||
- `app/routes/leagues/$leagueId.draft.$seasonId.tsx`
|
||
- `app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts`
|
||
|
||
- [ ] **Step 1: Apply replacement rules (same map as Task 4) to each file**
|
||
|
||
Use find-and-replace per-file. After every ~5 files, run:
|
||
|
||
Run: `npm run typecheck 2>&1 | wc -l`
|
||
(Track error count; should decrease monotonically.)
|
||
|
||
- [ ] **Step 2: Verify route tests still pass**
|
||
|
||
Run: `npm run test:run -- app/routes`
|
||
Expected: PASS (or same-as-before count; no new failures).
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add app/routes/
|
||
git commit -m "refactor(routes): update route layer to use renamed schema exports"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 6: Update simulators and services
|
||
|
||
**Files:**
|
||
- `app/services/probability-updater.ts`
|
||
- `app/services/standings-sync/index.ts`
|
||
- `app/services/simulations/registry.ts`
|
||
- `app/services/simulations/simulator-config.ts`
|
||
- `app/services/simulations/types.ts`
|
||
- `app/services/simulations/afl-simulator.ts`
|
||
- `app/services/simulations/auto-racing-simulator.ts`
|
||
- `app/services/simulations/bracket-simulator.ts`
|
||
- `app/services/simulations/cs-major-simulator.ts`
|
||
- `app/services/simulations/darts-simulator.ts`
|
||
- `app/services/simulations/golf-simulator.ts`
|
||
- `app/services/simulations/llws-simulator.ts`
|
||
- `app/services/simulations/mlb-simulator.ts`
|
||
- `app/services/simulations/nba-simulator.ts`
|
||
- `app/services/simulations/ncaa-football-simulator.ts`
|
||
- `app/services/simulations/ncaam-simulator.ts`
|
||
- `app/services/simulations/ncaaw-simulator.ts`
|
||
- `app/services/simulations/nfl-simulator.ts`
|
||
- `app/services/simulations/nhl-simulator.ts`
|
||
- `app/services/simulations/snooker-simulator.ts`
|
||
- `app/services/simulations/tennis-simulator.ts`
|
||
- `app/services/simulations/ucl-simulator.ts`
|
||
- `app/services/simulations/wnba-simulator.ts`
|
||
- `app/services/simulations/world-cup-simulator.ts`
|
||
- `app/utils/sports-data-sync.server.ts`
|
||
- `server/socket.ts`
|
||
- `server/__tests__/timer-autodraft.test.ts`
|
||
|
||
- [ ] **Step 1: Apply replacement rules**
|
||
|
||
Same map as Task 4. Simulators are largely symmetric — one simulator's pattern will reveal all; batch 5 at a time and verify with typecheck.
|
||
|
||
- [ ] **Step 2: Run simulator unit tests**
|
||
|
||
Run: `npm run test:run -- app/services/simulations/__tests__`
|
||
Expected: PASS. These tests consume Drizzle types extensively — a good regression canary.
|
||
|
||
- [ ] **Step 3: Run full typecheck**
|
||
|
||
Run: `npm run typecheck`
|
||
Expected: PASS (0 errors). Any remaining errors indicate a file missed in earlier tasks.
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add app/services/ app/utils/ server/
|
||
git commit -m "refactor(services): update simulators and services for renamed schema"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 7: Full test suite + Cypress verification
|
||
|
||
**Why:** Mechanical renames are easy to get 99% right and 1% broken in a way typecheck doesn't catch — especially relation-field renames (`.participant` → `.seasonParticipant`).
|
||
|
||
- [ ] **Step 1: Run full unit test suite**
|
||
|
||
Run: `npm run test:run`
|
||
Expected: PASS. If failures, inspect each: a common miss is a relation accessor like `result.participant.name` that needs to become `result.seasonParticipant.name`.
|
||
|
||
- [ ] **Step 2: Run a key E2E flow**
|
||
|
||
Run: `npx cypress run --spec "cypress/e2e/draft-room.cy.ts"` (or equivalent flagship flow)
|
||
Expected: PASS. E2E is the final guard — it exercises real relation loading.
|
||
|
||
- [ ] **Step 3: Manual smoke in dev**
|
||
|
||
Run: `npm run dev`
|
||
Manually:
|
||
1. Load the admin dashboard for an in-flight golf sports season — verify participant list renders.
|
||
2. Load a league's draft room — verify participant cards render.
|
||
3. Open admin surface-elo page for the tennis sports season — verify Elo values load.
|
||
|
||
Fix any broken UI before committing. (The rename may have missed a relation accessor somewhere.)
|
||
|
||
- [ ] **Step 4: Final commit if any fixups were needed**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "refactor: fix straggler relation accessors after rename"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 8: Verify baselines still match
|
||
|
||
**Why:** The entire point of Phase 1a is zero data change. Verify.
|
||
|
||
- [ ] **Step 1: Re-capture and diff**
|
||
|
||
Run:
|
||
```bash
|
||
npx tsx scripts/capture-baseline.ts --out=/tmp/post-phase1a
|
||
```
|
||
(Update the script to accept `--out` or copy the baseline JSON files from `test-fixtures/baselines/` and re-run capture manually into `/tmp/post-phase1a/`.)
|
||
|
||
Run:
|
||
```bash
|
||
diff test-fixtures/baselines/qp-totals-pre-phase1.json /tmp/post-phase1a/qp-totals-pre-phase1.json
|
||
diff test-fixtures/baselines/event-results-pre-phase1.json /tmp/post-phase1a/event-results-pre-phase1.json
|
||
diff test-fixtures/baselines/surface-elos-pre-phase1.json /tmp/post-phase1a/surface-elos-pre-phase1.json
|
||
```
|
||
Expected: All three diffs empty.
|
||
|
||
- [ ] **Step 2: Tag the branch**
|
||
|
||
```bash
|
||
git tag phase-1a-complete
|
||
```
|
||
|
||
---
|
||
|
||
### Task 9: Update documentation
|
||
|
||
**Files:**
|
||
- Modify: `docs/agents/database.md` — if it references the old table names
|
||
- Modify: `docs/agents/domain-models.md` — if it describes these tables
|
||
|
||
- [ ] **Step 1: Scan docs for old names**
|
||
|
||
Run: `grep -l "participant_qualifying_totals\|participant_expected_values\|participant_results\|participant_surface_elos\|event_results.participant_id" docs/`
|
||
Expected: list of docs to update.
|
||
|
||
- [ ] **Step 2: Update each doc to use new names**
|
||
|
||
For each file found, apply the rename map. Keep prose clear; where it helps readers, note "renamed from `X` in 2026-05".
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add docs/
|
||
git commit -m "docs: update table names in agent guides"
|
||
```
|
||
|
||
---
|
||
|
||
## Self-Review Checklist
|
||
|
||
Before marking this plan complete, verify:
|
||
|
||
- [ ] `git log --oneline phase-1a-complete` shows ~8 commits corresponding to Tasks 0–9.
|
||
- [ ] `grep -rn "from \"./participant\"" app/` returns zero results.
|
||
- [ ] `grep -rn "participantExpectedValues\|participantQualifyingTotals\|participantSurfaceElos" app/ database/ server/ scripts/` returns zero results (excluding string literals in UI labels, which are fine).
|
||
- [ ] `grep -rn "participantResults\b" app/ database/ server/` returns zero results.
|
||
- [ ] `npm run test:all` is green.
|
||
- [ ] `psql` confirms the renamed tables exist and the old names do not:
|
||
```
|
||
SELECT tablename FROM pg_tables WHERE tablename LIKE '%participant%' OR tablename LIKE '%season_participant%';
|
||
```
|
||
Expected: `season_participants`, `season_participant_expected_values`, `season_participant_qualifying_totals`, `season_participant_results`, `season_participant_surface_elos`. None without the `season_` prefix.
|
||
|
||
## Rollback
|
||
|
||
If Phase 1a must be undone:
|
||
|
||
1. `git revert` the commits from Tasks 1–9 (skip Task 0 baseline capture — keep fixtures).
|
||
2. Create a reverse migration that renames all five tables back and the column back, re-creates the old FK constraint.
|
||
3. Run `npm run db:migrate`.
|
||
|
||
Phase 1b must not begin until Phase 1a is shipped, green in production for at least 24 hours, and the tag `phase-1a-complete` is on `main`.
|