# Brackt as a Draftable Sport Implementation Plan

This plan is the implementation source of truth for making Brackt a league-private meta-sport. The design reference is [Brackt sport](brackt-sport.md).

## Summary

Implement Brackt as a league-private meta-sport with automatic final resolution. When any non-Brackt sports season is marked completed, the app checks linked fantasy seasons. If every drafted non-Brackt participant is finalized, it resolves that league's private Brackt season automatically. Commissioners still get a manual fallback action for repair and idempotent retry.

## Key Changes

- Add schema support:
  - Add `sportsSeasons.fantasySeasonId`.
  - Add `brackt` to `simulatorTypeEnum`.
  - Add `brackt_resolved` to `auditActionEnum`.
  - Generate migration with `npm run db:generate`.
- Add an admin seed/repair UI action:
  - Idempotently creates or repairs the global `Brackt` sport and global template sports season.
  - Template has no participants and a long draft window.
- Update draftable sports selection:
  - Hide per-league Brackt copies from all sport pickers with `fantasySeasonId IS NULL`.
  - Allow the global Brackt template through league creation/settings even with zero participants.
- Add Brackt lifecycle helpers:
  - On league creation or pre-draft settings update, replace the Brackt template link with a private sports season copy.
  - Insert manager participants directly into `seasonParticipants` with `externalId = team.id`.
  - Auto-sync private Brackt participants when pre-draft teams are added, removed, or renamed.
  - Remove the private Brackt season if Brackt is removed before draft start.

## EV And Draft Behavior

- Add `BracktSimulator`:
  - Supports all current league sizes, 6-16 teams.
  - Uses Harville probabilities for `N` teams and maps only available scoring tiers.
  - If all projected weights are zero, returns uniform placement probabilities.
  - Projects each team from drafted non-Brackt EV plus average available EV for each unpicked non-Brackt sport.
- Add `runBracktHarvilleForFantasySeason(fantasySeasonId, db)`:
  - Requires an explicit DB instance.
  - Runs the simulator, batch-upserts EVs, syncs VORP, rereads stored VORP, and returns `{ participantId, expectedValue, vorpValue }`.
- Refactor only the needed EV path to accept an optional provided DB:
  - `batchUpsertParticipantEVs(inputs, db?)`
  - `syncVorpForSeason(sportsSeasonId, db?)`
- Run Brackt Harville:
  - Await before autopick participant selection.
  - Fire-and-forget after manual picks commit.
  - Await after private Brackt season creation/sync.
- Emit `brackt-evs-updated` to the draft room with updated `expectedValue` and `vorpValue`.
- Draft room client:
  - Patches Brackt VORPs from socket updates.
  - Sorts by effective server VORP.
  - Never computes Brackt VORP client-side.

## Automatic Resolution

- Add a centralized service:
  - `maybeResolveCompletedBracktForSportsSeason(completedSportsSeasonId, db)`
  - Called after any non-Brackt sports season status transitions to `completed`.
- Hook all completion paths through this helper:
  - Admin sports-season edit when status becomes `completed`.
  - `finalize-standings`.
  - Bracket/qualifying finalization paths that mark or imply sports-season completion.
  - Any future model helper for status updates should call this from one shared status-transition function.
- Automatic resolution flow:
  - Find fantasy seasons linked to the completed non-Brackt sports season.
  - For each fantasy season, find its private Brackt season.
  - Skip leagues without Brackt or already resolved Brackt.
  - Require every drafted non-Brackt participant to have a finalized, non-partial result.
  - If ready, resolve Brackt from `teamStandings.currentRank`, mapping team ID to Brackt participant via `externalId`.
  - Preserve tied ranks as tied `finalPosition` values.
  - Mark the private Brackt sports season `completed`.
  - Recalculate standings and create a daily snapshot.
  - Write audit log `brackt_resolved` with `{ trigger: "auto", completedSportsSeasonId }`.
- Manual fallback:
  - Keep a commissioner-only league settings action to retry resolution.
  - Re-run is idempotent only: allow if placements match existing Brackt results; block if they would differ.

## Test Plan

- Unit tests:
  - Brackt Harville zero-weight uniform probabilities.
  - Brackt projection from drafted EV plus average available EV.
  - Private Brackt creation and team participant sync.
  - Picker hides private copies and allows the zero-participant global template.
  - EV/VORP path works with provided DB.
  - Automatic resolver skips incomplete leagues and resolves ready leagues.
  - Automatic resolver ignores leagues without Brackt and ignores Brackt-triggered recursion.
  - Tied-rank resolution and idempotent re-run behavior.
- Route/action tests:
  - League creation with Brackt creates private season and participants.
  - League settings add/remove/sync Brackt pre-draft.
  - Admin seed/repair action is idempotent.
  - Sports-season completion triggers automatic Brackt check.
  - Manual commissioner retry works only when idempotent or unresolved.
- Draft tests:
  - Autopick updates Brackt EV before top-VORP selection.
  - Manual pick emits Brackt EV update after commit.
  - Client socket update patches Brackt VORP sorting.
- Verification:
  - Run `npm run typecheck`, `npm run lint`, and targeted `npm run test:run`.

## Assumptions

- Brackt supports 6-16 team leagues.
- Brackt template remains a normal sport picker option with a Brackt-specific zero-participant exception.
- Pre-draft team edits auto-sync Brackt participants.
- Final tied standings remain tied for Brackt scoring.
- Automatic resolution is primary; commissioner action is fallback/repair.
