brackt/docs/agents/brackt-sport-implementation-plan.md
Chris Parsons 5b03b22267
Add Brackt as a league-private meta-sport (#200) (#377)
Commissioners can add Brackt to any league. Each team drafts one manager
from their own league; at season end, that manager's final overall fantasy
ranking earns placement points. Resolution fires automatically when all
other sports finalize.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 20:31:44 -07:00

105 lines
5.6 KiB
Markdown

# 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.