Skip to content

CourtsideDesk — Requirements Document

Owner: Alex (Business Analyst) Status: Session 2 complete — scoring formats, match result structure, round robin standings, and bracket topology resolved. Session 3 addendum (2026-04-10) — DrawFormat catalogue disambiguated for Sam's first packages/db migration (F4 numbering gap, F6 games-per-set, F6 tiebreak trigger). Last updated: 2026-04-10


1. Project Context

CourtsideDesk complements the SwissTennis platform, which is the system of record for Swiss tennis tournaments. SwissTennis handles: publishing tournaments, categories, draws, timetables, and results. It has no public API and provides no friendly UI.

CourtsideDesk does not replace SwissTennis. It provides: 1. A digital signage display for tournament venues 2. An organizer toolset for managing the on-site experience


2. Users / Personas

Persona Description Authentication
Tournament Organizer Creates and manages tournament data, assigns matches to courts, enters results Required
Player / Visitor Views live signage screens None (public)

Phase: Organizer is the primary persona for v1. Player features are secondary.


3. Data Ingestion — Staged Approach

Stage Method Scope
1 (v1 launch) Manual data entry via web forms All tournament data entered by organizer
2 (future) File import from SwissTennis exports (Excel .xls/.xlsx and PDF) Schedule, draws, player registrations, results
3 (future) Automated web scraping of SwissTennis portal Full automation, no manual trigger
3 (planned) Live online import from SwissTennis Advantage JSON servlets One-shot tournament import, per-event bracket import, schedule mirror, background sync. See specs/swisstennis-api-integration.md.

[ASSUMPTION] The data model must support all three ingestion methods from day one. No schema migration between stages. Every entity must support a source field indicating origin.


4. Screen Inventory

4.1 Digital Signage (public, unauthenticated)

SCR-001 — Schedule Screen

Purpose: Live court schedule displayed on venue screens.

Layout: - One column per court (dynamic) - Each column has one highlighted Main Row = current match on court - Above main row: upcoming matches, sorted by planned_start_time ASC (next match closest to main row) - Below main row: completed matches, sorted planned_start_time DESC (most recent closest to main row) - When a new match is assigned as current, previous current match shifts below

Match display fields:

Field Notes
Planned date & start time
Actual start time Set when match begins
Status Planned / On Court / Completed
Player 1 & 2 Format: ([SEED\|WC\|Q]) FirstName LastName (RANKING) — all parts optional
Category Draw name (e.g. "WS 30+ R7/R9")
Round label Optional free-text: "Final", "Semifinal", "QF"
Result Completed matches only — standard tennis score or "WO"
Winner highlight Winning player visually distinguished

Status transitions: - PlannedOn Court: organizer assigns match to court - Any → Completed: result entered OR organizer manually marks complete (either triggers Completed)


SCR-002 — Draw / Bracket View

Purpose: Display draws on venue screens with live on-court match highlighting.

Screen parameter: Draw ID — operators configure which draw a given screen shows.

Sub-View A — Single Elimination Bracket: - Traditional left-to-right tree - Rounds as vertical columns (R16, QF, SF, Final, etc.) - Per-match fields: same as SCR-001 - On-court matches visually highlighted within the bracket - Winner highlighted and shown advancing to next round

Sub-View B — Round Robin: - Left panel: Result grid — player vs. player matrix, score in each cell - Right panel: Standings table — ranked by tournament standing

Standings columns (left to right): POS, PLAYER, MP, MW, ML, SW, SL, GW, GL

Column Meaning
POS Current rank within group
PLAYER Player name
MP Matches played
MW Matches won
ML Matches lost
SW Sets won
SL Sets lost
GW Games won
GL Games lost

Tiebreak sequence (applied in order when two or more players are tied on MW). Canonical SwissTennis cascade per Rafael ruling (Session 4, 2026-04-10):

Step Rule Scope
1 H2H matches won — recursive mini-table (if 2 tied, direct match; if 3+ tied, sub-sort by wins within the mini-table) Among tied players only
2 Overall set ratio (sets won / sets lost) All group matches
3 Overall game ratio (games won / games lost) All group matches
4 Set ratio within H2H mini-table (second pass, restricted to still-tied players) Among tied players only
5 Referee draw — Losentscheid App flags for manual intervention — no auto-assignment

Implementation notes: - Percentages / ratios use integer cross-multiplication to avoid float drift - After each step, players that stand apart are finalised; remaining tied group recurses into the next step on the sub-pool only (not the full pool) - A residual tie after step 4 is surfaced to the caller as {status: "manual-draw-required", partialOrder, tiedGroups} — the standings calculator does not auto-break - Previous draft had 3 H2H steps before overall (H2H wins → H2H set% → H2H game% → overall set% → overall game% → manual). Corrected to SwissTennis canonical ordering per Rafael domain ruling.

Special result handling in standings calculation: - Walkover (WO) / Default (DEF): win awarded to the opponent; sets array is empty — do NOT impute 6-0 6-0 scores - Retirement (ret.): win awarded to the opponent; sets and games counted up to the retirement point only; last set flagged incomplete and its games are included in GW/GL - Super-tiebreak [10-7]: counts as 1 set won/lost for SW/SL; points do NOT count as games in GW/GL

Both sub-views: - Real-time refresh - is_loser_draw = true draws never rendered - No authentication required

[RESOLVED — Phase 1 ship, 2026-04-10] Bracket topology storage: FK columns on the matches row. Canonical shape is next_match_id self-FK + winner_slot enum (UPPER / LOWER) on every match. The bracket_edges alternative was considered and rejected — the FK-on-row model keeps traversal to a single column chain and fits the packages/scoring computeAdvancement intent-builder cleanly. See packages/db/drizzle/0000_initial_schema.sql for the shipped definition.

[RESOLVED — Rafael] Round robin standings columns and tiebreak sort order — see Sub-View B above.


4.2 Organizer View (authenticated)

SCR-003 — Contact Sheet

Purpose: Player directory for the current tournament.

Scope: Players registered for the currently open tournament only.

Display: - Table sorted alphabetically by last name (default) - Real-time name search/filter - Columns: Player Name (First Last), Home Phone, Work Phone, Mobile Phone, Email - Phone columns rendered via unified display formatter (see SCR-003.1)

Copy Emails button: - Copies emails of currently visible/filtered players only - Semicolon-delimited (;) — ready to paste into Outlook To: field - Players without email silently excluded

Acceptance criteria: - AC-001: Only players registered for the current tournament shown - AC-002: Default sort: alphabetical by last name A→Z - AC-003: Name filter narrows list in real time - AC-004: Copy Emails copies only filtered/visible players' emails, semicolon-delimited - AC-005: Players without email silently excluded from copy


SCR-003.1 — Unified Phone Number Display (Contact Sheet)

Purpose: Render the three phone columns (phone_home, phone_work, phone_mobile) on /contacts in a single, internationally recognised format regardless of how SwissTennis stored them, while leaving the stored values untouched.

Non-goal (explicit): The import pipeline must NOT rewrite, normalise, or canonicalise phone numbers. Storage remains exactly as delivered by SwissTennis (see §6 cols 13–15 and ENTITY-005 phone_home / phone_work / phone_mobile). Formatting is a display-only transformation, applied at render time in the organizer UI.

Display rules: - Each stored phone value is parsed and rendered in the library's INTERNATIONAL format (E.164-grouped national format with country prefix), e.g.: - CH: +41 79 123 45 67 - DE: +49 30 12345678 - FR: +33 6 12 34 56 78 - Default parse region: CH — always, no exceptions. Any stored phone value that does NOT begin with a leading country-code prefix (+XX or 00XX) MUST be parsed as a Swiss number. Examples — all of these are Swiss and format to +41 79 123 45 67: 079 123 45 67, 0791234567, 0041791234567, +41 79 123 45 67. There is no heuristic detection of other regions from bare digits; a non-Swiss number is only recognised when it arrives with an explicit +XX / 00XX prefix. Per-tournament / per-org default region is explicitly out of scope for this ticket and tracked as a follow-up. - Unparseable input fallback: if the library cannot parse the string (garbage, extensions, alphanumeric, etc.), the raw stored value is rendered unchanged. No error, no empty cell, no visual hint or affordance (PO decision: silent raw render — the organizer can't fix SwissTennis data from this screen anyway). - Null / empty values render as empty cells, same as today.

Toggle: - Single UI control at the top of the /contacts table, labelled "Formatted phone numbers" (final label / placement owned by Jamie). - Applies to all rows simultaneously. Per-row toggle is explicitly out of scope. - Default state: ON (formatted). - When OFF: every phone cell shows the raw stored value exactly as in the database.

Toggle persistence: - Persist via browser localStorage, scoped per user/browser (key suggestion: courtsidedesk.contacts.phoneFormat, values "formatted" | "raw"). - No server-side preference, no per-tournament setting. Morgan / Taylor to confirm key naming and SSR-safe access pattern (hydration: server renders formatted default; client effect swaps to persisted value on mount).

Technical note — library choice: - Use libphonenumber-js (the maintained port of Google's libphonenumber). Tree-shakeable, browser-safe, no hand-rolled regex. Call parsePhoneNumberFromString(value, 'CH') and render phoneNumber.formatInternational(). Catch parse errors and fall through to raw. - Do NOT introduce a server-side phone normalisation step. Formatting happens in the React component rendering the cell (or a small shared util consumed by it).

Scope boundary: - Only /contacts (SCR-003) is in scope for this ticket. - Other surfaces that may display phone numbers (none confirmed in v1 requirements) are out of scope and will be tracked as separate follow-up tickets if/when they appear.

Acceptance criteria: - AC-006: Phone columns on /contacts render in international format by default for parseable values. Any value without a +XX / 00XX prefix MUST be parsed as Swiss (e.g. 079 123 45 67+41 79 123 45 67; 0041791234567+41 79 123 45 67). - AC-007: Numbers already stored with a country code are re-grouped to the library's international format for the detected country (e.g. +4915112345678+49 151 12345678). - AC-008: Unparseable values render as the raw stored string, unchanged, without throwing. - AC-009: A toggle at the top of the table switches all rows between formatted and raw views; there is no per-row toggle. - AC-010: Default toggle state on first visit is ON (formatted). - AC-011: Toggle state persists across page reloads within the same browser via localStorage; is not sent to the server. - AC-012: Import pipeline writes to phone_home / phone_work / phone_mobile remain byte-for-byte identical to the SwissTennis source (regression test against existing fixtures). - AC-013: No change to the database schema, API contract, or stored values. Pure UI change.

[ASSUMPTION — confirmed by PO] Default parse region is hard-coded to CH for v1. Any number without a leading +XX / 00XX prefix is Swiss. If a non-Swiss organizer onboards later, a per-tournament default region setting is a separate ticket. [RESOLVED — PO decision] Unparsed values render silently (raw string, no tooltip, no icon, no styling hint). [RESOLVED] Shared util location and packages/ui vs. app-local placement are implementer's choice at build time (Morgan / Taylor decide during implementation; PO defers to experts).


SCR-004 — Tournament Setup

Purpose: Create and configure a tournament.

Fields: - Tournament name (free text) - Start date / End date (end ≥ start) - Courts: list of free-text names/numbers, each with a sort order - Draws: list of draws, each with a name (e.g. "WS 30+ R7/R9") and format (Single Elimination or Round Robin)

No registration settings (no deadline, no player cap).


SCR-005 — Court Assignment / Match Scheduling

Purpose: Operational hub for the organizer on a live tournament day.

Layout: - Unassigned Matches Panel: Scrollable list of all matches not yet assigned to a court. TBD-player matches freely shown — no blocking. - Court Columns: One column per court (ordered by sort_order), each with: - Current match (top, visually distinct) - Upcoming matches sorted by planned_start_time ASC (nulls last)

Interactions:

Action Behaviour
Drag from panel → court column Assigns match; removes from unassigned panel
Drag between court columns Moves match; re-sorts by planned time in new column
Drag match back to panel Clears court_id + planned_start_time; returns to unassigned
Inline time edit Optional; past times valid; saves → re-sort + real-time push to SCR-001
Result entry Inline on match card; saves → status = COMPLETED + real-time push to SCR-001 + SCR-002

Real-time write paths (first-class requirements): 1. Assignment / time change → SCR-001 updates live 2. Result entry → SCR-001 + SCR-002 (bracket advancement) update live

Acceptance criteria: - AC-001: Unassigned matches listed; TBD-player matches shown without blocking - AC-002: Drag to court assigns match; disappears from unassigned panel - AC-003: Drag between courts moves match; re-sorts by planned time - AC-004: Remove from court clears court_id + planned_start_time - AC-005: Inline time edit optional; past times accepted; triggers re-sort - AC-006: Result entry sets status = COMPLETED - AC-007: Assignment/time edit/result entry propagates live to SCR-001 (target: < 2s) - AC-008: Result entry additionally propagates live to SCR-002 - AC-009: Upcoming matches always sorted by planned_start_time ASC; nulls last - AC-010: Completed matches do not clutter the active court column view


5. Data Entities

ENTITY-001 — Tournament

Field Type Notes
id UUID
name String e.g. "Stadtmeisterschaft 2026"
start_date Date
end_date Date ≥ start_date

ENTITY-002 — Court

Field Type Notes
id UUID
tournament_id UUID FK Scoped per tournament — no global court registry
name String Free text: "1", "Centre Court", "Platz 3"
sort_order Integer Controls display order

ENTITY-003 — Draw

Field Type Notes
id UUID
tournament_id UUID FK
name String Opaque label — e.g. "WS 30+ R7/R9". No parsing.
format Enum SINGLE_ELIMINATION | ROUND_ROBIN
draw_format_id UUID FK References DrawFormat (ENTITY-010) — scoring format for all matches in this draw
is_loser_draw Boolean Default false. Stored but never rendered.

ENTITY-004 — Match

Field Type Notes
id UUID
tournament_id UUID FK
draw_id UUID FK / null Nullable — standalone matches supported
court_id UUID FK / null Null until assigned
planned_start_time DateTime / null Editable; past values valid
actual_start_time DateTime / null Set when match begins
status Enum planned | on_court | completed
player1_id UUID FK / null Nullable for TBD players
player2_id UUID FK / null Nullable for TBD players
player1_seed_or_id String / null Seed number or "WC", "Q", "LL"
player2_seed_or_id String / null
round_label String / null Free text: "Final", "Semifinal"
result MatchResult / null Structured set-score result object (see ENTITY-007). Null until completed.
winner Enum / null player1 | player2 | null
next_match_id UUID FK / null Elimination brackets — FK → Match; null for the final match in a draw
winner_slot Enum / null Elimination brackets — UPPER | LOWER; which slot the winner fills in the next match
source Enum manual | import | scrape

[RESOLVED] Bracket topology: FK fields on Match row (Option A). next_match_id and winner_slot added above. Double-elimination is explicitly out of scope.

[RESOLVED] Match result format — structured set-score entry confirmed. See ENTITY-007 (MatchResult), ENTITY-008 (SetScore), ENTITY-009 (TiebreakScore).


ENTITY-005 — Player

Field Type Notes
id UUID
license_number String / null SwissTennis Lizenznummer — upsert key on import
first_name String
last_name String
date_of_birth Date / null
address_street String / null
address_co String / null
address_postal_code String / null
address_city String / null
address_country String / null
phone_home String / null
phone_work String / null
phone_mobile String / null
email String / null
ranking_category String / null Swiss ranking: "R3", "R6", "R9" — this IS the displayed ranking
ranking_value Float / null Underlying numeric value e.g. 7.284
club_code String / null SwissTennis club ID
club_name String / null Denormalized — no Club entity for now
source Enum manual | import | scrape

[ASSUMPTION] license_number is the canonical identity key. On import: upsert if exists, create if not. [ASSUMPTION] Ranking fields = snapshot at time of last import. Ranking history is out of scope. [ASSUMPTION] Club denormalized for now; Club entity deferred.


ENTITY-006 — Registration

Links a Player to a Tournament + Draw. One record per player per draw entered.

Field Type Notes
id UUID
player_id UUID FK
tournament_id UUID FK
draw_id UUID FK References the draw (Konkurrenz)
registered_at DateTime / null From SwissTennis Anmeldedatum
is_seeded Boolean Seeding is per-category
qualifier_wildcard String / null "Q", "WC", "LL", or null
restrictions String / null Admin note from SwissTennis
comment String / null
is_confirmed Boolean
is_online_registration Boolean
is_paid Boolean

ENTITY-007 — MatchResult

Embedded object stored on Match. Replaces the former free-text result string.

Field Type Notes
outcome Enum COMPLETED | WALKOVER | DEFAULT | RETIREMENT
sets SetScore[] Ordered array (ENTITY-008). Empty for WALKOVER and DEFAULT.
matchTiebreak TiebreakScore / null Populated only for formats with a standalone match tiebreak (F3, F8)

ENTITY-008 — SetScore

One entry per set played.

Field Type Notes
setIndex Integer 0-based position in the match
setType Enum NORMAL | MATCH_TIEBREAK
player1Games Integer
player2Games Integer
tiebreak TiebreakScore / null Present when a set-level tiebreak was played (ENTITY-009)
isComplete Boolean false if the set was interrupted by retirement

ENTITY-009 — TiebreakScore

Used for both set-level tiebreaks and standalone match tiebreaks.

Field Type Notes
player1Points Integer
player2Points Integer Loser's score stored per tennis convention — e.g. 7-6(4) means tiebreak was 7-4
targetPoints Integer Winning target (7 for standard TB, 10 for match TB, 5 for Fast4)
winByTwo Boolean Whether the winner must win by 2 points

ENTITY-010 — DrawFormat

Scoring format configuration. One record per format; referenced by FK on Draw.

Supported formats:

formatId Name Best Of Games/Set Final Set Rule TB Target No-Ad
F1 Standard, Advantage Final Set 3 6 ADVANTAGE 7 (sets 1–2) No
F2 Standard, Tiebreak All Sets 3 6 TIEBREAK 7 No
F3 Match Tiebreak (Super TB) 2+MTB 6 MATCH_TIEBREAK 7 (sets 1–2) / 10 (MTB) No
F5 Fast4 Best of 3 3 4 TIEBREAK 5 (win by 1) Yes
F5b Fast4 Best of 5 5 4 TIEBREAK 5 (win by 1) Yes
F6 Pro Set 1 8 TIEBREAK 7 (at 7-7) No
F7 Short Set No-Ad 3 4 TIEBREAK 7 (win by 2) Yes
F8 Standalone Match Tiebreak 0+MTB 10

[RESOLVED — Session 3, 2026-04-10] DrawFormat catalogue has 8 rows. F4 is a reserved, intentionally unused id. The catalogue contains exactly 8 DrawFormat rows: F1, F2, F3, F5, F5b, F6, F7, F8. The id "F4" is deliberately reserved and not seeded — it corresponds to a legacy Swiss club format code that predates this catalogue. Reserving the id (rather than renumbering F5→F4) preserves compatibility with paper draw sheets, tournament bulletins, and future SwissTennis import mappings that may still reference "F4". F8 is a first-class format in the catalogue despite being a degenerate "no sets, match tiebreak only" rule set; the scoring package branches on finalSetRule = MATCH_TIEBREAK with gamesPerSet = 0 to drive MTB-only mode. Morgan's tech stack doc describes this as "7 DrawFormat variants" — that count refers to the seven full scoring rule sets (F1, F2, F3, F5, F5b, F6, F7) and excludes F8 as a degenerate case; the DB-level row count is 8. Morgan's doc needs a clarifying footnote; see Session 3 report.

[RESOLVED — Session 3, 2026-04-10] F6 Pro Set is canonical at 8 games per set. The "8 or 10" phrasing in earlier drafts has been retired. The 8-game Pro Set is the overwhelmingly dominant variant at Swiss club tournaments; the 10-game variant is rare enough that it does not justify a per-draw override column or a second catalogue row in v1. If the 10-game variant is needed later, a future F6b row can be added with no schema migration — draw_formats is a seed catalogue, not a schema change.

[RESOLVED — Session 3, 2026-04-10] F6 final-set tiebreak triggers at 7-7. The "7-7 or 9-9" phrasing in earlier drafts has been retired. Canonical F6 plays a 7-point tiebreak when the score reaches 7-7 (i.e., first to 8 games, or tiebreak at 7-7). This is the mainstream Swiss club convention for an 8-game Pro Set. The 9-9 variant belongs to a longer Pro Set format that is not in scope for v1. If it becomes required, it will ship as a separate catalogue row (e.g., F6c) and not as a per-draw override.

[ASSUMPTION] Best of 5 / Grand Slam format is reserved for a future release. Do not implement now.

[ASSUMPTION] Scoring format is scoped to the Draw, not the Tournament. Multiple draws within the same tournament may use different formats.

DrawFormat fields:

Field Type Notes
formatId UUID
name String Human-readable label
bestOf Integer 1, 3, or 5 (or conceptual value for MTB-only formats)
gamesPerSet Integer 4, 6, 8, or 10
noAd Boolean If true, deuce point decides immediately (no advantage)
finalSetRule Enum ADVANTAGE | TIEBREAK | MATCH_TIEBREAK
finalSetTiebreakAt Integer / null Game score at which final-set tiebreak is triggered; null for ADVANTAGE
standardTiebreakTarget Integer Points to win a set-level tiebreak
matchTiebreakTarget Integer Points to win a match tiebreak (used when finalSetRule = MATCH_TIEBREAK)
tiebreakWinByTwo Boolean Whether tiebreak winner must win by 2 points

Validation rules (application-layer, driven by DrawFormat config): 1. Game scores must not exceed gamesPerSet 2. "Win by 2" at game level is disabled when noAd = true 3. Tiebreak winning score must satisfy >= targetPoints and the applicable win-by rule 4. WALKOVER / DEFAULT: sets array must be empty 5. RETIREMENT: the last set in sets must have isComplete = false 6. Tiebreak loser's score is stored in parentheses in display (e.g. 7-6(4) means tiebreak was 7-4)


6. SwissTennis Export — Field Mapping

PlayerList.xls (confirmed format)

File structure: tournament header rows 0–2, column headers row 3, data from row 4. One row per registration.

Col SwissTennis field Translation Entity Field name
0 Konkurrenz Category / Competition Draw / Registration draw.name
1 Anmeldedatum Registration date Registration registered_at
2 Lizenznummer License number Player license_number
3 Klub Club code Player club_code
4 Klub Name Club name Player club_name
5 Name Last name Player last_name
6 Vorname First name Player first_name
7 Geburtsdatum Date of birth Player date_of_birth
8 Adresse Street address Player address_street
9 c/o c/o Player address_co
10 PLZ Postal code Player address_postal_code
11 Ort City Player address_city
12 Land Country Player address_country
13 Tel P Home phone Player phone_home
14 Tel G Work phone Player phone_work
15 Mobile Mobile phone Player phone_mobile
16 Email Email Player email
17 Klassierung Swiss ranking category Player ranking_category
18 Klass. Wert Ranking numeric value Player ranking_value
19 Gesetzte Seeded flag Registration is_seeded
20 Q/WC Qualifier / Wild Card Registration qualifier_wildcard
21 Einschränkungen Restrictions Registration restrictions
22 Kommentar Comment Registration comment
23 bestätigt Confirmed Registration is_confirmed
24 On-line Anmeldung Online registration Registration is_online_registration
25 bezahlt Paid Registration is_paid

Note: Date fields (cols 1, 7) are Excel date serials — must be converted to ISO dates on import.


7. Key Decisions

Decision Detail
Courts are per-tournament No global court registry
Draw names are opaque labels No parsing of e.g. "WS 30+ R7/R9"
Loser draws stored but never rendered is_loser_draw = true suppressed at display layer
Standalone matches supported draw_id is nullable on Match
TBD-player matches freely assignable No blocking on court assignment
Past start times valid Delayed matches accepted without error
Player is a persistent entity Persists across tournaments; upsert on license_number
Real-time write paths are first-class Assignment and result entry both push live to signage
No registration settings No deadline, no player cap
Contact Sheet is tournament-scoped Filters to current tournament registrations only
Copy Emails respects active filter Copies only visible/filtered players
Ranking displayed is Swiss system ranking_category (R3, R6 etc.) — not ATP/WTA points
Match result is structured, not free-text MatchResult object with SetScore[] and TiebreakScore; replaces score string
Scoring format is per-Draw, not per-Tournament draw_format_id FK on Draw; draws within the same tournament may differ
Best of 5 / Grand Slam format deferred Not in scope for v1
WO/DEFAULT sets array must be empty Do not impute 6-0 6-0; outcome enum carries the result type
Retirement: partial set is included Games up to retirement point counted; last set flagged isComplete = false
Super-tiebreak counts as 1 set Points do not feed into game totals (GW/GL)
RR tiebreak final step is manual (Losentscheid) App flags the tie for manual referee intervention — no automatic draw
Bracket topology: FK fields on Match row (Option A) next_match_id + winner_slot on Match; double-elimination explicitly out of scope
DrawFormat catalogue has 8 rows F1, F2, F3, F5, F5b, F6, F7, F8. "F4" id is reserved/unused for legacy compatibility. Morgan's "7 variants" count refers to full scoring rule sets only (F8 excluded as degenerate).
F6 Pro Set canonicalised at 8 games per set, TB at 7-7 "8 or 10" and "7-7 or 9-9" variants retired. 10-game / 9-9 variants, if ever needed, ship as separate catalogue rows, not per-draw overrides.

8. Open Items

Item Description Owner Status
Round robin standings columns Standard Swiss tennis RR standings columns + tiebreak logic Rafael RESOLVED — see SCR-002 Sub-View B and ENTITY-010
Bracket topology storage FK on Match row vs. bracket_edges table Morgan RESOLVED — Option A: next_match_id + winner_slot FK fields on Match row; double-elimination out of scope
Match result format Free-text assumed — structured set-score entry TBD PO / Morgan RESOLVED — structured MatchResult confirmed; see ENTITY-007–009
Unified phone display on /contacts Display-only formatter + toggle; storage untouched; libphonenumber-js recommended Taylor (impl) / Jamie (UX) / Morgan (arch) SPEC READY — see SCR-003.1