Starts after: Stage 6A complete.
What this is: The bracket logic for knockout tournaments. Each bracket match is a standard Room (from Stage 3 or 4). This stage wires Rooms into a bracket structure with automatic advancement.
Tasks
-
Bracket generation
POST /api/contests/:id/bracket/generate (admin action, triggered once when registration closes).
- Auto-round participant count to next power of 2.
- Insert byes for unfilled slots (auto-walkover wins).
- Seeding: by CF rating (default) or admin manual drag-and-drop.
- Standard bracket: highest seed vs lowest seed in round 1.
- Generate
ContestRound documents (QF, SF, F, etc.) and stub ContestRoom documents for all bracket positions.
- Populate
contest:<contestId>:rooms Set with all stub room IDs.
- Set
contest:<contestId>:meta Hash: { format: 'knockout', currentRound: 1, status: 'active' }.
-
Bracket advancement
- Winner of each bracket room auto-advances to the next round.
- When both feeder rooms in a bracket position complete: update the next round's
ContestRoom.teams with the two winners.
- Publish
contest.bracket_update to events:contest:<contestId> after each room completes: full bracket state snapshot.
ZADD contest:<contestId>:standings <wins> <teamId> after each room.
-
Round completion detection (race-safe)
- After every bracket room completes: check if all rooms in the current
ContestRound are done.
- If yes: increment
contest:<contestId>:meta.currentRound, publish contest.round_complete to events:contest:<contestId>: { roundNumber, advancingTeams: teamId[] }.
- This check must be race-safe — two rooms finishing simultaneously must not trigger double-advancement. Implement with a Redis Lua script or a
SETNX-based sentinel.
-
Manual walkover
- Admin can manually advance a participant:
POST /api/contests/rooms/:id/walkover with a required note.
- Note stored in the audit log. The walkover triggers the same advancement path as a completed room.
-
Third-place playoff
- Optional, off by default. Admin toggles at contest setup in Stage 6A.
- If enabled: two semifinal losers are seeded into a standard bracket room.
ContestRoom.bracketPosition = '3PO'.
-
Bracket state data model
- Define the bracket snapshot structure carried in
contest.bracket_update:
type BracketNode = {
roomId: string;
teams: [teamId: string, teamId: string] | null;
status: 'pending' | 'active' | 'completed';
winner: string | null;
bracketPosition: string;
}
- Commit this type definition and document the full snapshot shape in
SSE_EVENTS.md.
Testing Gate
Handoff Contract
Starts after: Stage 6A complete.
What this is: The bracket logic for knockout tournaments. Each bracket match is a standard Room (from Stage 3 or 4). This stage wires Rooms into a bracket structure with automatic advancement.
Tasks
Bracket generation
POST /api/contests/:id/bracket/generate(admin action, triggered once when registration closes).ContestRounddocuments (QF, SF, F, etc.) and stubContestRoomdocuments for all bracket positions.contest:<contestId>:roomsSet with all stub room IDs.contest:<contestId>:metaHash:{ format: 'knockout', currentRound: 1, status: 'active' }.Bracket advancement
ContestRoom.teamswith the two winners.contest.bracket_updatetoevents:contest:<contestId>after each room completes: full bracket state snapshot.ZADD contest:<contestId>:standings <wins> <teamId>after each room.Round completion detection (race-safe)
ContestRoundare done.contest:<contestId>:meta.currentRound, publishcontest.round_completetoevents:contest:<contestId>:{ roundNumber, advancingTeams: teamId[] }.SETNX-based sentinel.Manual walkover
POST /api/contests/rooms/:id/walkoverwith a required note.Third-place playoff
ContestRoom.bracketPosition = '3PO'.Bracket state data model
contest.bracket_update:SSE_EVENTS.md.Testing Gate
Contest.status = 'completed',Contest.winnerpopulated.contest.bracket_updatecarries correct updated bracket state. Verified at each round.contest.round_completefires exactly once.ContestRoom.bracketPosition = '3PO'in MongoDB.Handoff Contract
contest.bracket_updateandBracketNodetype are committed toSSE_EVENTS.md.