Owner review 4, lane D: score entry with less on screen, matches in sequence, and matches that stop (2026-10-12)
Status: Draft · Date: 2026-10-12 · Lane D of owner review 4, points 3, 4 and 12. Inputs: library §10, flat-v2-kit-controls.jsx (FvkScoreEntry 4068, FvkScoreTable 4021, fvkMatchFact 3982, FvkMatchRow 4257), lane C of review 3 §3 (the S0 to S6 machine, T1 to T29, N1 to N18, R1 to R13), the shipped entry (packages/features/sessions/src/multi-set-score-entry.tsx, match-board-helpers.ts), the scoring and rating SQL (00084, 00147, 00590, 00642, 00682), the matchmaking types, and sources read on 2026-10-12 (§8). Audience: club members aged 40 to 60 at the court, with the host holding the phone that runs the session. Everything marked new is a proposal for veto.
Owner, review 4 (points for this lane). 3: "the presence of presets make the whole thing a bit more visually cluttered, is there a way to make things a little less cluttered?" 4: "the flow of score input should be subsequent … if you don't enter the scores for the previous game the next game won't be able to start … we must make that explicit and this way we can eliminate the possibility that we settle the dispute later rather than right now." 12: "A retirement or walkover should count. How do organizations take care of this? We need proper measures for matches that are halted, cancelled or otherwise terminated."
0. Evidence tags and limits
| Tag | Meaning |
|---|---|
| C | Code or SQL in this repo (path:line). |
| D | A rulebook, help page or regulation read on 2026-10-12 (URL in §8). |
| S | A search snippet or a secondary page. Flagged where used; no recommendation rests on one alone. |
Limits, stated once. (1) I could not read the ITF Rules of Tennis, the ATP Rulebook, the USTA national regulations or the KATA rulebook as primary text: the PDFs came back as binary or 404. Their content below comes from search snippets of those documents and from secondary pages, and is tagged S. (2) The WTN and Playtomic help pages do not say how a retirement is rated; I say so rather than infer. (3) No peer's current app was run. Tap counts for peers are counted from their documented steps and marked "documented"; our own counts are from the kit and the shipped code. (4) I found no Korean amateur app that records a score between two players the way ours does (§2.1), so there is no local practice to copy for point 3.
1. At a glance
| Point | Recommendation | Decided by |
|---|---|---|
| 3 | Winner first, then only the winner's scores. At open: the two team lines (each a button: "who won") and the CTA, 3 controls instead of 18. After the winner is picked: one grid of the 7 common scores plus 다른 점수, 11 controls at most. The cells appear only for 다른 점수, a tiebreak or an edit. The chips come from the match-format spec, not a hard-coded list. Cost: one more tap for 6-4 (3 against 2). | Evidence for the count; taste for the extra tap (Q1) |
| 4 | Gate per player, enforced on the server, derived not stored. A match cannot start while any of its players has an earlier match in this session that is not settled. Settled = 확정, 호스트 확정, a recorded stop outcome, or 기록 제외. Entered is not settled. The host has five ways out, each leaving a recorded outcome; "start anyway" does not exist. Minute-scale nudges replace the 12 h and 24 h timers only while a match is held; the long timers stay for last-round matches and anything recorded after the session. | Evidence (the code already needs it) plus the owner's ruling |
| 12 | One outcome model, two independent axes: does it count in the record, and is it rated. Add retired and defaulted, an explicit winner, and a partial score. Retirement counts in the record and is rated at a reduced weight once enough games were played; walkover counts in the record and is never rated; default follows retirement if play had started, else walkover; abandoned, cancelled and 기록 제외 count nowhere. Every number is one config object with a parity gate. ScoreEntry gets one quiet row, 끝까지 못 했나요?. | Evidence for the direction; taste for the weights (Q2, Q3) |
Found while tracing (changes the plan). (a) The database already has the outcome column and six values (matches.outcome, 00084), and the rating code already refuses to rate walkover, abandoned and non_competitive (00590:884) and rates incomplete at half weight (00590:238-242). Nothing in the client writes any value but decisive (use-round-preview.ts:301, use-manual-composer.ts:159). So point 12 is mostly a UI and policy job on an existing column, plus two new values and a winner. (b) The score-driven styles already need the previous round's results, yet an unscored court is read as a 0-0 draw (score-driven.ts:57-70): the missing-score hole point 4 closes is real today. (c) awaiting_result and match_score_calls exist only in the redesign, not in any migration (no match in supabase/migrations).
Questions for the owner (three, all in §6). Q1: is one extra tap acceptable for a much shorter screen? Q2: should a walkover in an ordinary friendly pickup count in the win-loss record? Q3: retirement rated at half weight (my recommendation) or full?
2. Point 3: score entry without the clutter
2.1 How peers do it
| Product | What the entry is | Evidence |
|---|---|---|
| UTR "Post a Score" | Boxes per set; a set score of 1-0 first opens the tiebreak boxes; match tiebreaks (to 7 or 10) replace the third set. Incomplete matches accepted, not all rated. No presets. | D: UTR posting guidelines |
| Match Tennis app (tournament desk) | A dropdown first: win, weather delay, retirement, default, walkover; then set boxes; for a third-set tiebreak a TB column. The outcome type is the first control, before any number. | D: matchtennisapp how-to |
| DUPR (pickleball) | Format first (for example 2 of 3 to 11), then the points of each game; the match stays pending until the opponents confirm. No presets. | S: pickleball Connect and Reclub help, not the app itself |
| Playtomic | Add results on the match, confirm positions, enter the result, choose who sees it; the other side accepts, else it validates itself in 24 h. Exact controls not documented. | D: Playtomic help (via search snippet; the page itself returned 403) |
| Tennis Australia Match Centre | A status above the players for a rubber: Incomplete, Retirement or Forfeit; then the score where play stopped. | S: search snippet of the TA guide |
| Korean tools (대진표 manager, DataTennis, 테니스 스코어 매니저) | Draw generators and point-by-point watch scorers. None records a result between two players with a confirmation. | D: App Store listings; no local practice found |
Reading. No peer offers presets; all use typed boxes, which is exactly the clutter-free but slow-for-the-common-case shape we moved away from. The one pattern worth taking is the Match Tennis app's: say what happened before the numbers ("win", "retirement"), which is also the cheapest way to make presets small, because half of them (the other side's) disappear. UTR's "enter 1-0, then the tiebreak boxes appear" is the same idea for the tiebreak: show a field only when the score demands it, which the kit already does.
2.2 What is on screen now
Kit, mode open, one set (flat-v2-kit-controls.jsx:4139-4185): two 48 × 48 cells (the FvkScoreTable), two choice sets of 7 chips (us, them), the CTA, and the sheet's close control: 18 interactive controls, of which 14 are chips. A best-of-three adds 2 cells per revealed set. Another ambiguity: in best3 the same 14 chips are still drawn, and the file's header says a pick "filled set 1", so for sets 2 and 3 it is undefined which set a chip fills.
| Score | Taps now (after the sheet is open) | Count |
|---|---|---|
| 6-4 (a win) | the chip, the CTA | 2 |
| 4-6 (a loss) | the chip in the second row, the CTA | 2 |
| 7-6(5) | the 7-6 chip, tap the tiebreak field, type 5, the CTA | 4 |
The shipped app has no chips at all: MultiSetScoreEntry shows only the revealed set rows as raw numeric inputs, and nextEntryState reveals the next set only when the previous one is complete (match-board-helpers.ts:121-161). So the presets are new in the redesign, and the clutter the owner sees is the cost of the quick path.
2.3 Recommendation: winner first, scores second
Three changes, each removing controls and none removing a function.
- The two team lines are the "who won" control. Instead of 14 chips under two legends, the table's two team lines are selectable (a radio pair: 우리 팀 and 상대 팀, 위 팀 and 아래 팀 for a host). Nothing else is shown until one is chosen. At open: 2 lines plus the CTA (disabled) = 3 controls.
- Then one grid of winner-first scores. After a pick:
6-0 6-1 6-2 6-3 6-4 7-5 7-6and 다른 점수, eight cells in the same 4-column grid, two rows. Chips read winner-first ("6-4" under either pick) and the table places the numbers on the right lines, so the label vocabulary is halved. Max on screen: 2 + 8 + 1 = 11. - Cells are hidden until needed. The 48 × 48 cells appear (a) when 다른 점수 is tapped, (b) when a chip is chosen, as a read-only numeral pair that a tap reopens for editing, and (c) for a tiebreak. The tiebreak field appears as now on 7-6 and takes focus at once (numeric keypad), so no extra tap to reach it.
Chips are f(spec): the grid is built from the match-format spec (MatchFormatSpec, scoring.rules.ts:82), so a 4-game short set shows 4-0 … 4-2, 4-3 (tb), an 8-game pro-set shows 8-0 … 8-6, 9-7, and a super-tiebreak final set shows no chips (cells only). The kit's hard-coded us and them arrays (flat-v2-kit-controls.jsx:4139-4140) would otherwise be a second copy of isSetComplete.
Several sets. Each set is a block: winner pick plus grid, which collapses to a one-line numeral summary ("1세트 6-4", tap to reopen) once done, then the next block appears (the same progressive reveal nextEntryState already does). On screen at once: the summaries (1 control each) plus one active block, so a best-of-three never exceeds 11 + 2. This also fixes the "which set does a chip fill" ambiguity.
2.4 Counts, side by side
| Measure | Now (kit) | Proposal (winner first) |
|---|---|---|
| Controls on screen at open | 18 | 3 |
| Controls at the busiest moment | 18 | 11 |
| Taps, 6-4 won | 2 | 3: team line, chip, CTA |
| Taps, a loss (4-6) | 2 | 3 |
| Taps, 7-6(5) | 4 | 4: team line, 7-6, digit 5 (field already focused), CTA |
| Taps, any other score (6-1 typed) | 1 + 2 digits + CTA = 4 | 1 + 다른 점수 + 2 digits + CTA = 5 |
The honest trade: the winner tap makes the common case one tap longer (2 to 3) and a loss no cheaper; the screen is 7 times emptier at open and never above 11. A default on the winner (preselect "we won") would make a win cost 2 again, but a default on a consequential, confirmable claim is the pattern the foundational pass banned (confirm-consequential-mutation), and a player's win rate is near one half so it would be wrong half the time. I do not recommend it. Q1 asks the owner to confirm the extra tap.
Rejected alternatives, briefly. Steppers in the cells: 10 taps for 6-4. Wheels: a drag control inside a sheet, a precision problem for the audience, and the "no scroll and drag on one axis" rule applies. Fewer chips (drop 6-0, 6-1): they do occur between mismatched club players, and eight cells fill exactly two rows of four, so there is no gain.
2.5 Components
FvkScoreEntry loses us/them and the two legends; gains winner state and a setBlocks list. FvkScoreTable gains selectable on its team lines (radio semantics, spoken "우리 팀이 이겼어요"). The chips stay the existing FvkChoiceSet (size sm, 4 columns, single select). No new primitive.
3. Point 4: matches in sequence
3.1 How it is done elsewhere
- Score-driven formats need it by construction. Mexicano ranks on points from earlier rounds and King of the Court moves players by the previous round's court results. Our own types say so:
requiresPriorResultsistrueforrank_adaptiveandladder(matchmaking/types.ts:195-207). But the code's answer to a missing score is to read it as 0-0: "A still-unscored court passes through as-is …resolveCourtMovementalready treats a null pair as a 0-0 draw" (score-driven.ts:66-69). So today a forgotten score silently becomes a draw and moves players. - Rotation formats do not need results (
requiresPriorResultsis false forround_robinandamericano), which is why scores drift today: nothing waits on them. - Peers. I found no peer that hard-gates the next match on the previous score (§7). The closest are a desk that "calls" the next match, and the sectional-league app where captains "should enter scores as soon as the match is complete" but the next match is not blocked (S: Tennis Director guide). A forum report about a match-play tool shows the opposite failure: a round was closed with a result missing and the result could not be entered afterwards (S, one source,
tiltforumsthread). Treat this as motivation, not as proof of a practice. - Korean 동호인 대회. KATO rules require the competitors to enter within 5 minutes of the call and start within 10 minutes or be defaulted (제3조 ②, 제10조 ①); finished results go to the association after the event (2-6). Only a team's representative may appeal to the 본부 (6-5, 제19조). I found no clause that holds the next call for a result report; the 본부석 flow is practice, not text (§7).
The gate is therefore our own design, justified by the owner's ruling and by the code's existing silent fallback, not by an industry norm. Said plainly so the owner can veto it.
3.2 The gate
Unit: the player. A match may start only when, for each of its players, every earlier match of that player in this session is settled. Earlier means a lower round_number in the same session.
Why not the other two.
| Unit | Behaviour | Verdict |
|---|---|---|
| Per court | The next match on court 2 waits for the last match on court 2. | Wrong: players change courts every round, so the court is not who is waiting. |
| Per round | Round N+1 starts only when every match of round N is settled. | One slow scorer holds seven other courts; but it is the right rule for drawing a score-driven round (it needs all courts). |
| Per player | A match waits only for the people in it. | Chosen: the blame is local, other courts run on, and it matches the owner's sentence ("the next game", "the previous game"). |
With our generated draws. A round is a set of matches; each player is in at most one match per round. An unsettled match M in round N therefore holds the matches of round N+1 that contain M's players: at most four, usually two or three. Everything else starts. The host's 라운드 시작 becomes "시작할 수 있는 경기 시작": it starts every ready match and lists the held ones with the reason. For a score-driven style the draw of round N+1 needs every court of round N, so generate_session_round gets the same predicate over the whole round: it refuses with previous_round_unsettled instead of reading nulls as 0-0 (score-driven.ts:66). That is not a second rule: it is the player gate applied to all players at once.
The gate is derived, never stored (the same discipline as deriveRecruitmentStatus, docs/canon/status-and-recruitment.md). A pure function deriveMatchStartGate(match, sessionMatches) returns { open: true } or { open: false, blockedBy: matchId[], players: userId[] }. The server enforces the same predicate in the transition S0 to S1 (private.match_start_blockers(match_id)), refusing with a typed previous_unsettled. A parity test pins the two; a canary fixture (a held match that must refuse) keeps the gate from passing blind.
What "settled" means.
| Counts as settled | Does not |
|---|---|
| S4 확정 (an opponent agreed) | S1 playing, S2 awaiting result |
| S4h 호스트 확정, and S4c (a contested host result stands while the contest runs) | S3 확인 대기 (entered, nobody answered) |
| S6 기록 제외 | S5c countered, S5 disputed |
| a stop outcome that is recorded and accepted (§4): retired, walkover, defaulted, abandoned, cancelled, suspended with a plan | a stop that is only claimed and not yet accepted |
Entered is not settled. The owner's sentence is that the dispute is settled "right now". At the court the opponents are a few metres away and answering is one tap; if they are not, the host's tap is the second one. So S3 holds the gate. This is the rule that changes lane C's behaviour the most, and it is why §3.3 matters.
The host's ways out. Each leaves a recorded outcome; there is no "start anyway".
| Way out | Result | Who |
|---|---|---|
| 대신 입력 (enter the score) | S4h, rated | host or official |
| 바로 확정 on a pending claim | S4h | host or official |
| 경기를 못 했어요 (a stop, §4) | retired, walkover, defaulted, abandoned, cancelled, or suspended with a resume plan | host or official; a player proposes, the host or the other side accepts |
| 기록 제외 (T19) | S6, with its reason | host or official |
| A decision on a dispute (T11 to T13) | S4h or S6 | host or official |
If the host plays in the held match, rule 5 (nobody judges their own match) applies. With another official, that official acts. With none, a stop outcome needs one player from each team to agree, so a missing official can never freeze a session. Lane C's R8 line stands for scores; this extends it to stops.
Timers. The long timers (12 h reminder, 24 h escalation, 48 h contest, 7 d entry window, 30 d void) were built for a world where nobody is waiting. While a successor is held they are the wrong scale, so:
| Timer | Status |
|---|---|
| T15 reminder (12 h), T16 escalation (24 h) | Replaced for a match that holds a successor by gateNudgeMinutes (opponents who have not answered, first push at 3) and gateEscalationMinutes (host task, 6). They stay for every other match. |
| T20 entry window (7 d), noResultVoidDays (30 d) | Unchanged. They govern matches recorded outside a running session and the last match of each player, which holds nothing. |
| T11 to T13, T21 (48 h contest), T26 (7 d correction) | Unchanged: they act after settlement. |
| 8 s undo | Unchanged; the gate opens when the result is accepted, not when the undo window ends, but the rating still waits for the window. |
Last-round and after-the-fact matches are the remaining gap: no successor exists to wait. Two small closures: the host's 세션 종료 shows a Dialog "점수가 정해지지 않은 경기가 N개 있어요" with the same ways out, and the auto_advance_session_statuses() completion (which must not be blocked: status-and-recruitment.md, 8 h grace) leaves them on the long timers. Numbers 3 and 6 minutes are my defaults, not evidence: they are config and tunable (§3.7).
3.3 Why the gate needs the stop outcomes
A gate with no way out turns one absent player into a stuck session, and the cheap escape (선수들이 알아서 기록 제외) would become the new way to "settle later". The ways out above are what make the gate safe, so point 4 cannot ship without point 12's outcomes or at least 기록 제외 with a mandatory reason.
3.4 The state machine, rewritten (changes from lane C §3 marked)
States S0 to S6, S5c, S4c and the overlay R are unchanged in name and in matches.status. New: one derived condition and one new state.
| ID | State | matches.status | Change |
|---|---|---|---|
| S0 | 예정 | scheduled | Now splits for display into ready and held (derived, not stored). |
| S0h | 대기, held (new, derived) | scheduled | deriveMatchStartGate is closed. Row shows "이전 경기 점수 확정 후 시작". |
| S2, S3, S5c, S5 | as lane C | awaiting_result | Each now also holds the gate for the match's players. |
| S4, S4h, S4c, S6 | as lane C | completed | Each now releases it. |
| S7 | 중단, Suspended (new) | in_progress, outcome suspended | A stop with a partial score and a resume plan (§4). Releases the gate for others while the pair waits to resume. |
| stop outcomes | 기권, 부전승, 실격, 미완료, 취소 | completed (cancelled: cancelled) | New values of outcome; accepted like a score (S3 to S4 path) or set by the host. |
Transitions that change or are added. All others keep lane C's numbers.
| # | Transition | Change |
|---|---|---|
| T0 | S0 to S1 (start) | New guard G1: deriveMatchStartGate is open for all four players, else refusal previous_unsettled with blockedBy. Host 라운드 시작 starts the ready ones only. No override. |
| T1 | S1 to S2 | Effect adds: N1 body gains "다음 경기가 기다려요" when a successor exists. |
| T2 | S2 to S3 (submit) | The sheet's helper line says what the press does to the gate: "확정되면 다음 경기를 시작할 수 있어요". Clocks: the gate nudges (above) replace the 12 h and 24 h ones when a successor is held. |
| T5, T8 | S3 or S5c to S4 | Effect adds: gate released, N19 to the host and to the players whose match was held. |
| T7, T10 | to S5 | Effect adds: N10 to the host at once (no 24 h wait) when a successor is held. |
| T11 to T13, T17, T18, T19 | to S4h or S6 | Effect adds: gate released, N19. |
| T14, T20 | expiry to S6 | Unchanged. A held successor never reaches expiry, because the host's nudge fires first. |
| T30 | S2 or S1 to a stop (S4/S4h family) | New. A player proposes (or the host sets) a stop outcome with winner and partial score; accepted like T5/T11 by an opposing player, or set by the host. Releases the gate. |
| T31 | S1 to S7 | New. Suspended: partial score stored, resume plan (date or "다음 모임"), suspendedResumeDays = 7. Releases the gate. |
| T32 | S7 to S1 | New. Resume from the stored score (ITF: the score and positions stand on resumption, S). |
| T33 | S7 to abandoned | New. suspendedResumeDays pass with no resume: outcome abandoned, unrated, N12. |
Pushes added. N19 "다음 경기를 시작할 수 있어요", body "3라운드, 2번 코트" (to the host and the waiting players). N20 "점수 확인이 필요해요", body "다음 경기가 기다려요 · 김민준 님이 6-4를 입력했어요" (to opponents who have not answered, replacing N4 for a held successor). N21 "점수를 기다리는 경기가 N개 있어요" (host, at gateEscalationMinutes; once per round, not per match). Players who are only waiting get an in-app line, not a push: they are not the ones who can unblock it.
Races added. R14 two hosts press 라운드 시작 at once: the first commit starts the ready set; the second gets the new state, never a double start. R15 the gate opens while a held player's phone is offline: the start is a host action, so it cannot be queued by the player; the player's row refreshes on reconnect. R16 a correction (overlay R) on a settled match after the successor started does not re-hold anything. R17 a substitute is swapped into a held match (live substitution, 00683): the gate is recomputed with the substitute's own history. R18 the host is a player in the blocked match with no other official: the two-teams-consent rule above. R19 a confirm and a start race: the start reads the settled state under the same match lock (lock_match_score_write), so a start can never see a half-written confirm.
3.5 What the screens say
- Match row, held (S0h). Header: court, then the fact. For the blocked player: "내 이전 경기 점수가 필요해요", with the row's one Button 점수 확정 (opens their previous match's own action, so the row is the way out). For another player: "김민준 님 경기 점수를 기다려요". For the host: "2경기 점수를 기다려요" and the control 대신 입력 on the previous match's row, not here. Tag: 대기 (neutral weak), one new word; no numerals.
- 대진표 round header. "시작할 수 있는 경기 3 · 대기 2" so the cause is visible from the board.
- The previous match's row grows a line while it holds a successor: "다음 경기가 기다려요".
- Who is told, once each. The unconfirmed opponents (N20, the people who can unblock it), the host at the escalation (N21), everyone released (N19). The waiting players are not pushed.
4. Point 12: matches that stop
4.1 What organisations do
| Source | Walkover (not played) | Retirement (played in part) | Default / disqualification | Suspension and weather |
|---|---|---|---|---|
| ATP, ITF (S: secondary summaries) | Before the first serve; the opponent advances, scored as if played for ranking points (ATP, S); ITF juniors 2026: a win by walkover or by retirement after the match began "will be equivalent to winning a match" (D, 2026 ITF WTTJ summary, via search) | Match has started; the score so far is kept, written with ret.; the opponent wins | Code of Conduct; the referee decides | The umpire may stop, the referee suspends; for darkness at the end of a set or after an even number of games; the score and positions stand on resumption (2024 ITF Rules, S) |
| UTR (D) | Excluded from the rating; shown on the profile | Counted if either player had won at least four games; below that, excluded | "the games completed before the default may still contribute"; the rest not counted | Incomplete matches used on the same four-game test |
| DUPR (D: pickleball.com) | Withdrawal before the first match: not counted | Counts; the score at that point is recorded and the opponent is given the rest of the game (for example 10-5 recorded as 12-10) | Forfeit counts at 7 or more total points; recorded as a decisive win | not documented |
| USTA league regulations (S) | A default counts toward advancement for some players (section rules differ); a default can be recorded as 6-0 6-0 (one section's rule) | "RETIRED matches shall count for all players" | see walkover | not found |
| ITF WTN (S) | not documented | not documented | one ITF junior event: retirement or default counts as a straight-sets loss | not documented |
| Playtomic level (D) | not documented | not documented | not documented | not documented |
| KATO 대회 규정 (D: kato.kr/katoRules) | 호명 후 5분 안에 입장, 10분이 지나면 실격 (제3조 ②, 제10조 ①); 정당한 사유 없이 불참하면 경고 이상의 징계 (제3조 ①, 하루 전 통보 제외) | 부상 치료 시간 5분은 지연에서 제외 (제10조 ② 마); 특별한 사유 없이 진행 중인 경기를 끝내지 않으면 실격 후 징계 (제9조) | 감독관과 레프리가 선언; 비신사적 행위·위험한 행동은 한 번에도 가능 (제2조, 제12조, 제14조); 경고, 게임 포인트, 실격 (제10조 ④) | 악천후 등 불가피하면 감독관과 사전 협의 후 쇼트게임·타이브레이크 방식으로 진행 (10-15); 일정 조정은 협회와 상의 (5-3); 시합 개시 후 우천 중단은 연기일에 이어서 (9-12, 9-13의 부 자격) |
| 대한테니스협회 용어 (S: a 동호회 page quoting the glossary) | 부전승 | 기권 (retire): 병, 부상 또는 용구 고장으로 스스로 그만둠; 스코어는 Navratilova 6-2 2-1(ret) Mandikova 로 씀 | 실격 (disqualify): 중대한 규칙 위반, 대회 당국이 선언 | 엄파이어가 중단할 수 있으나 실제 연기는 레퍼리만 |
Reading, with the owner's sentence in mind ("A retirement or walkover should count").
- Everyone separates "counts" from "is rated". UTR and DUPR rate a retirement and do not rate a walkover; USTA counts a retirement; ITF juniors count both as wins for points. That is two axes, and one boolean cannot hold it.
- A retirement is rated on what was played, never on the remainder. UTR drops the rest; DUPR is the outlier (it awards the rest of the game). I recommend the UTR way: no extrapolation.
- A minimum of play separates a retirement from a walkover. UTR: four games won by either player. DUPR forfeits: 7 points. Both are config in a policy.
- Who declares matters. A default and a disqualification are declared by an authority (referee, 감독관), never by the opponent. A retirement is the retiring side's own act and is simply recorded. A walkover is observed by the present side and confirmed by the authority.
- Korean amateur practice splits "can't continue" from "won't continue". 기권 is injury, illness or broken gear; stopping with no reason is 실격 (제9조); lateness past 10 minutes is 실격 (제3조 ②). A shortened format in bad weather is allowed with the 감독관's agreement (10-15), which is a way out that keeps a result.
- Suspension is not an outcome. It is a state with a stored score, and tennis resumes from that score. For a one-day club slot, resuming usually means another day, so it needs an expiry.
4.2 The outcome model (config, never hard-coded)
Current truth: matches.outcome TEXT CHECK (… 'decisive','draw','incomplete','abandoned','non_competitive','walkover') (00084:44-45), mirrored by MatchOutcome (match.entity.ts:21-22); match_status has cancelled (00001_enums.sql:8). The rating weights live in SQL literals: 1.0 decisive, 0.6/0.4 with weight 0.5 incomplete with a leader, 0.5/0.5 with weight 0.3 level (00590:229-244). There is no winner column: the winner is derived from the scores, which is wrong for a retirement (a player can lead 6-4 1-3 and retire from the second set... the winner is the one who did not retire, not the one ahead).
Add two values, retired and defaulted; one column winner_side (team1, team2, NULL) that is required for retired, walkover and defaulted and derived for decisive; stopped_reason (injury, illness, equipment, no_show, late, conduct, weather, time, other), a key into the i18n registry; and for suspension a resume_of link. The partial score stays in the existing set rows.
One policy object, MATCH_OUTCOME_POLICY, in packages/app/src/domain/rules/match-outcome.rules.ts, mirrored by a private table rating_outcome_policy and pinned by a parity test (the trust-tier pattern), replacing the SQL literals:
Outcome (outcome) | Winner | Partial score | Counts in record | Rating mode | Rating detail (config) | Gate |
|---|---|---|---|---|---|---|
decisive | derived from sets | complete | yes | full | weight 1.0 | settled |
draw | none | complete | yes (무) | full | actual 0.5, weight 1.0 (as today) | settled |
incomplete (time-up, decided on games) | derived from games | partial | yes | partial | existing 0.6/0.4 at 0.5, level 0.3 (as today), kept as the "시간이 다 됐어요" path | settled |
retired | explicit (the side that did not retire) | the score so far | yes | partial if max(games) >= 4, else none | actual 0.6/0.4, weight 0.5; no extrapolation; retiredMinGames = 4 | settled |
walkover | explicit | none | yes (configurable, Q2) | none | none | settled |
defaulted | explicit | the score so far, may be empty | yes | as retired if play started, else none | same detail as retired | settled |
abandoned | none | the score so far | no | none | none (as today, 00590:884) | settled |
non_competitive (기록 제외) | none | optional | no | none | none (as today) | settled |
suspended (a state, §4.1.6) | none yet | the score so far | no, until resumed | none | none; becomes decisive/retired/abandoned later | releases for others |
cancelled (status = cancelled) | none | none | no | none | none | settled |
The mapping to the owner's list: retirement is retired; walkover walkover; default or disqualification defaulted; "suspended for rain, light or injury" the state suspended; abandoned abandoned; cancelled status = cancelled; our 기록 제외 non_competitive.
Recorded how, by whom, confirmed by whom.
| Outcome | Recorded by | Confirmed by |
|---|---|---|
retired | any player of the match (usually the retiring side's own) or the host; names the retiring side | one player of the other side, as a score (S3 to S4); the host's entry is final (S4h). An entry from the winning side is confirmed by the retiring side. |
walkover | the side that is present, as a report; or the host | the host or official (the absent side cannot be asked); the absent side may contest within 48 h (T21) |
defaulted | the host or official only (the referee's role; the opponent cannot default a player) | none; contest within 48 h by the defaulted side |
suspended | host or official; or any player with one of the other side agreeing | the stored score is confirmed like a claim |
abandoned, cancelled | host or official; with no other official, one player from each team | as above |
non_competitive | host or official with a reason (T19) | none |
4.3 How ScoreEntry records it
One quiet row at the foot of the sheet: 경기를 끝까지 못 했나요? (a PickerRow, grammar already in the library), 1 control, shown in every mode. It opens a second step in the same sheet, no new surface:
- Reason list (PickerRow): 한 팀이 도중에 그만뒀어요 (기권) · 한 팀이 오지 않았어요 (부전승) · 비나 시간 때문에 멈췄어요 (중단) · 경기를 하지 않았어요 (취소). A host and officials also see 실격 and 기록 제외.
- Who (the same two-line selector as §2.3): "누가 그만뒀어요?" / "누가 오지 않았어요?"; skipped for 중단 and 취소.
- The score so far (the same table, filled by the same winner-first grid is not used: a partial score is typed into the cells; optional for 부전승 and 취소). A 중단 asks for the resume plan: 다음 모임에 이어서 / 끝낼게요 (becomes 미완료).
- CTA: 기록하기 for a player (the other side confirms), 저장 for a host (final), with the helper "상대 팀이 확인하면 정해져요" / "저장하면 바로 정해지고 선수들에게 알려요".
Controls added to the main screen: one. The ordinary score path is unchanged and still the first thing the sheet shows.
4.4 How the rows show it
Marker rule: the outcome is a word with the score, not a new colour, and it replaces nothing the owner approved (Tag vocabulary stays; one new tone is not introduced).
| Outcome | Korean (Tag / words) | English | Match row | ResultRow (history) | Score as written |
|---|---|---|---|---|---|
| retired | Tag 기권; winner line "기권승" | Ret. | Team lines with the partial numerals; Tag 기권 (neutral weak); winner emphasis as 확정 | Outcome Tag stays 승 or 패; the meta line reads "6-4 3-1 기권" | 6-4 3-1 기권 (en: 6-4 3-1 ret.) |
| walkover | Tag 부전승 | W/O | No numerals; the winner's line carries "부전승", the other "불참" | Tag 부전승 or 부전패 (패 form unverified in my sources, Q2 note) | 부전승 (en: W/O) |
| defaulted | Tag 실격 | Def. | As retired if play started, else as walkover | Tag 승 or 패, meta "실격" | 6-4 1-0 실격 / 실격 |
| suspended | Tag 중단 (warning), fact "6-4 3-1에서 멈췄어요" | Suspended | Numerals so far; no winner emphasis; the row keeps its place under 이어 칠 경기 | not in history until resolved | 6-4 3-1 중단 (en: suspended) |
| abandoned | Tag 미완료 | Unfinished | Partial numerals, no winner emphasis | not in the record; kept in the session log | 6-4 3-1 미완료 |
| cancelled | Tag 취소 | Cancelled | No numerals, secondary ink | not shown | none |
| 기록 제외 | Tag 기록 제외 (as ratified) | Not counted | as ratified (S6) | not shown | none |
Team lines: the retiring side's line gets no mark of its own; the Tag and the written score say it. The winner emphasis (names 600, numerals ink) applies to retired, walkover and defaulted exactly as to 확정, because they count. abandoned, suspended and cancelled draw no emphasis, because there is no winner. The words 기권승, 부전승 and 실격 are the standard Korean forms in the KATA glossary page and KATO text I read; 부전패 and 기권패 as loser-side Tags are my own derivation, which I ask the owner to check against how club members actually say it (§7).
4.5 The rating policy
Config, never hard-coded: the MATCH_OUTCOME_POLICY above is the only place a number lives. Recommended defaults, with their basis.
| Setting | Value | Basis |
|---|---|---|
retiredMinGames | 4 | UTR: "at least four games were won by either player" (D). Read as max(team1Games, team2Games) >= 4. |
| retired weight, actual | 0.5, 0.6/0.4 | The existing incomplete branch (00590:238-242); a taste value, not evidence (Q3). |
| remainder | not extrapolated | UTR (D). DUPR's remainder rule (S) is the alternative and needs sets and points we do not store. |
| walkover rated | never | UTR (D) and the existing code (00590:884). Nothing was played. |
| walkover in the record | yes | The owner's ruling; ITF juniors and USTA treat it as a win (S). A friendly pickup option is Q2. |
| default | as retired if started | UTR: games before the default may count, the rest not (D). |
suspendedResumeDays | 7 | A club slot rarely resumes the same day; matches the 7-day grain already used (disputeVoidDays, entryWindowDays). Taste. |
Gates against abuse, because W/O counts in the record: a walkover needs the host's acceptance (never two friends), the no-show strike path stays separate (attendance_records), and a player with repeated W/O wins is visible to the host on the roster digest. These are policy, not a ruling, and the record could exclude W/O (Q2).
5. Where this touches lane C's open items
- Lane C §3.7 "not drawn" list gains: held row
mr-held, stop sheetse-stop, partial-score tablese-stop-score, suspended rowmr-suspended, gate Dialog on 세션 종료dg-end-unsettled. - Lane C §3.2: the rating column "applied on S4 or S4h" extends to "on an accepted outcome whose policy mode is not none".
match_outcome_unratedsignal (00682:1100) already exists for the unrated stops; reuse its copy key family for walkover and abandoned, add the reason.
6. Decisions
| # | Decision | Basis | Recommendation |
|---|---|---|---|
| D1 | Winner first, scores second (§2.3) | Evidence | Adopt |
| D2 | Gate per player, server-enforced, derived (§3.2) | Owner's ruling plus code | Adopt |
| D3 | Entered is not settled (§3.2) | Owner's ruling | Adopt |
| D4 | Nudge minutes 3 and 6; the long timers stay for last-round matches (§3.2) | Taste | Adopt as config |
| D5 | retired, defaulted, winner_side, stopped_reason, suspended state (§4.2) | Evidence | Adopt |
| D6 | Counts and rated as two axes (§4.1.1) | Evidence | Adopt |
Q1. One more tap? Winning or losing a set takes 3 taps instead of 2, in return for 3 controls at open instead of 18. My recommendation: yes, because the owner's complaint is the screen, and a default winner would be wrong half the time. If the owner prefers 2 taps, the fallback is to keep both rows of chips but collapse the losing row behind a toggle (11 controls at open).
Q2. Should a walkover in an ordinary friendly pickup count in the win-loss record? Competitive, tournament and league matches should (ITF and USTA do). In a friendly rotation a player who did not show up does not give a "win" in any sense a member would recognise. Recommendation: count it in competitive, tournament and league, show it in friendly but not in the win-loss total. One flag in the policy per MatchNature. (The owner said "should count"; this asks only about friendlies.)
Q3. Retirement at half weight or full? UTR and DUPR rate it; neither says the weight. Half weight protects against rating inflation from a short, one-sided retirement; full weight is simpler to explain. Recommendation: half weight, with the owner able to change the number in config.
7. What I could not verify
- The ITF Rules of Tennis, ATP Rulebook, USTA national regulations and KATA rulebook as primary text (§0). Their content here is from snippets and secondary pages, tagged S.
- Any peer that hard-gates the next match on the previous result. I searched Americano and Mexicano apps, tournament software and 본부석 practice and found none; the gate is our design.
- Korean usage of 부전패 and 기권패 on a result list. The winner forms (부전승, 기권승) are standard; the loser forms are derived.
- How WTN and Playtomic treat retirements. Neither page states it.
- How DUPR's in-app entry looks; only help pages and third-party guides were available.
- The real frequency of 6-0 and 6-1 sets in 동호회 play. I kept them because eight cells fill two rows, not for a statistic.
8. Sources (read 2026-10-12)
- UTR, effect of walkovers, retirements, defaults and incomplete matches: https://support.universaltennis.com/en/support/solutions/articles/9000153699-what-is-the-effect-of-walkovers-retirements-withdrawals-defaults-and-incomplete-matches-to-my-utr-r
- UTR, posting scores guidelines: https://support.universaltennis.com/en/support/solutions/articles/9000222603-posting-scores-guidelines
- DUPR via pickleball.com, forfeits, withdrawals and retires: https://pickleball.com/blogs/decoding-forfeits-withdrawals-and-retires-and-what-each-do-to-your-rating-1
- Match Tennis app, submitting scores (outcome dropdown, weather delay, retirement): https://web.matchtennisapp.com/how-to-submit-scores-via-match-tennis-app/
- Playtomic, publishing results and validation (search snippet): https://playerhelp.playtomic.com/hc/en-gb/articles/19831755554449-How-to-publish-your-match-results-to-the-Playtomic-Community
- ITF 2026 WTT Juniors summary of rule changes (search snippet; PDF unreadable): https://www.itftennis.com/media/15527/2026-itf-wttj-summary-of-rule-changes.pdf
- USTA league regulations (search snippet): https://ustasocal.com/adult/leagues/regulations
- KATO 대회 규정: https://kato.kr/katoRules
- 대한테니스협회 경기규칙 용어 해설 (secondary, 동호회 page): https://m.cafe.daum.net/mptclub/JKWX/110
- Tennis Australia Match Centre results entry (search snippet): https://support.tennis.com.au/?p=2730
- Match-play tool forum report (one source, anecdote): https://tiltforums.com/t/match-play-events-the-next-generation/6921?page=29
Repo evidence cited inline: 00084_rating_system_v2.sql, 00147_match_rules_and_two_sided_tiebreak.sql:268, 00590_apply_match_ratings_decomposed.sql:229-244,884, 00642_match_rating_preview.sql:435, 00682_rating_serialization_and_history.sql:1100, packages/app/src/domain/entities/match.entity.ts:21, packages/app/src/domain/rules/matchmaking/types.ts:195, score-driven.ts:57-70, packages/features/sessions/src/multi-set-score-entry.tsx, match-board-helpers.ts:121.
Implementation
Kit (docs/twomore_design_system/wireframes/)
- [ ]
flat-v2-kit-controls.jsxFvkScoreEntry(4068): dropus/themand the two legends (4139-4185); addwinnerstate, per-setsetBlocks, a collapsed set summary, and the 끝까지 못 했나요? PickerRow; new modesstop-reason,stop-score. - [ ]
FvkScoreTable(4021): addselectableteam lines (radio, spoken label). - [ ]
fvkMatchFact(3982): add stateheld(per viewer: blocked player, other player, host) andsuspended,retired,walkover,defaulted,abandoned,cancelledfacts;FvkMatchRow(4257) states and Tag map (FvkMatchTag) gain 대기, 기권, 부전승, 실격, 중단, 미완료, 취소. - [ ] A
fvkFormatChips(spec)helper so the chip grid is built from the format, not an array.
Boards
- [ ]
flat-v2-lib-social.jsxscoring board: replace the picks frames with winner-first frames (se-open,se-winner-picked,se-other,se-tiebreak,se-best3), addse-stop-reason,se-stop-score,se-stop-suspend,mr-held,mr-suspended, the stop Match rows and ResultRows. - [ ]
flat-v2-detail.jsx(mb-*frames): the 대진표 round header "시작할 수 있는 경기 · 대기", held rows for the three viewers,dg-end-unsettled. - [ ]
flat-v2-flows.jsxFlow 7: add the gate path (S2 to S3 to released) and the stop path (S1 to S7 to S1). - [ ] Record the grammar in the decision log; re-run
REGROUND=1 yarn docs:wireframesper board only.
Library (docs/design/flat-v2-component-library-2026-10-09.md §10)
- [ ] ScoreEntry: replace the quick-picks text with the winner-first anatomy and the count (3 at open, 11 maximum); add the stop path; Match row: the new states and Tags; ResultRow: the marker rule (§4.4).
- [ ] State table: S0h, S7 and the stop outcomes; transitions T0, T30 to T33; pushes N19 to N21; races R14 to R19; timer table (§3.2).
App and schema (for the build, after the boards are marked)
- [ ] Migration: extend
matches.outcomeCHECK withretired,defaulted; addwinner_side,stopped_reason,resume_of;private.rating_outcome_policyreplacing the SQL literals inmatch_rating_outcome_actual(00590:218-247);private.match_start_blockers(match_id)and the guard on the S0 to S1 transition;generate_session_roundrefuses withprevious_round_unsettled. Entity, mapper, adapter, migration and types in one commit. - [ ]
packages/app/src/domain/rules/match-outcome.rules.ts(MATCH_OUTCOME_POLICY) andmatch-start-gate.rules.ts(deriveMatchStartGate); parity tests against the SQL; a canary fixture for the gate. - [ ]
score-driven.ts:66: stop treating a null pair as 0-0 once the gate is in. - [ ]
multi-set-score-entry.tsx/match-board-helpers.ts: chips fromMatchFormatSpec;match-card.tsxstop path. - [ ] i18n: Tag and push copy (ko, en) in the registry;
josaEuroRotables for the new pushes;signal-routes.tsfor N19 to N21. - [ ] Update
docs/architecture/screen-blueprint.mdanddocs/history/rebuild-log.md.