Skip to content

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 ​

TagMeaning
CCode or SQL in this repo (path:line).
DA rulebook, help page or regulation read on 2026-10-12 (URL in §8).
SA 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 ​

PointRecommendationDecided by
3Winner 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)
4Gate 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
12One 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 ​

ProductWhat the entry isEvidence
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
PlaytomicAdd 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 CentreA 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.

ScoreTaps now (after the sheet is open)Count
6-4 (a win)the chip, the CTA2
4-6 (a loss)the chip in the second row, the CTA2
7-6(5)the 7-6 chip, tap the tiebreak field, type 5, the CTA4

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.

  1. 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.
  2. Then one grid of winner-first scores. After a pick: 6-0 6-1 6-2 6-3 6-4 7-5 7-6 and 다른 점수, 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.
  3. 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 ​

MeasureNow (kit)Proposal (winner first)
Controls on screen at open183
Controls at the busiest moment1811
Taps, 6-4 won23: team line, chip, CTA
Taps, a loss (4-6)23
Taps, 7-6(5)44: team line, 7-6, digit 5 (field already focused), CTA
Taps, any other score (6-1 typed)1 + 2 digits + CTA = 41 + 다른 점수 + 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: requiresPriorResults is true for rank_adaptive and ladder (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 … resolveCourtMovement already 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 (requiresPriorResults is false for round_robin and americano), 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, tiltforums thread). 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.

UnitBehaviourVerdict
Per courtThe 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 roundRound 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 playerA 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 settledDoes 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 plana 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 outResultWho
대신 입력 (enter the score)S4h, ratedhost or official
바로 확정 on a pending claimS4hhost or official
경기를 못 했어요 (a stop, §4)retired, walkover, defaulted, abandoned, cancelled, or suspended with a resume planhost or official; a player proposes, the host or the other side accepts
기록 제외 (T19)S6, with its reasonhost or official
A decision on a dispute (T11 to T13)S4h or S6host 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:

TimerStatus
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 undoUnchanged; 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.

IDStatematches.statusChange
S0예정scheduledNow splits for display into ready and held (derived, not stored).
S0h대기, held (new, derived)scheduledderiveMatchStartGate is closed. Row shows "이전 경기 점수 확정 후 시작".
S2, S3, S5c, S5as lane Cawaiting_resultEach now also holds the gate for the match's players.
S4, S4h, S4c, S6as lane CcompletedEach now releases it.
S7중단, Suspended (new)in_progress, outcome suspendedA 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.

#TransitionChange
T0S0 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.
T1S1 to S2Effect adds: N1 body gains "다음 경기가 기다려요" when a successor exists.
T2S2 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, T8S3 or S5c to S4Effect adds: gate released, N19 to the host and to the players whose match was held.
T7, T10to S5Effect adds: N10 to the host at once (no 24 h wait) when a successor is held.
T11 to T13, T17, T18, T19to S4h or S6Effect adds: gate released, N19.
T14, T20expiry to S6Unchanged. A held successor never reaches expiry, because the host's nudge fires first.
T30S2 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.
T31S1 to S7New. Suspended: partial score stored, resume plan (date or "다음 모임"), suspendedResumeDays = 7. Releases the gate.
T32S7 to S1New. Resume from the stored score (ITF: the score and positions stand on resumption, S).
T33S7 to abandonedNew. 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 ​

SourceWalkover (not played)Retirement (played in part)Default / disqualificationSuspension 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 winsCode of Conduct; the referee decidesThe 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 profileCounted if either player had won at least four games; below that, excluded"the games completed before the default may still contribute"; the rest not countedIncomplete matches used on the same four-game test
DUPR (D: pickleball.com)Withdrawal before the first match: not countedCounts; 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 winnot 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 walkovernot found
ITF WTN (S)not documentednot documentedone ITF junior event: retirement or default counts as a straight-sets lossnot documented
Playtomic level (D)not documentednot documentednot documentednot 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").

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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)WinnerPartial scoreCounts in recordRating modeRating detail (config)Gate
decisivederived from setscompleteyesfullweight 1.0settled
drawnonecompleteyes (무)fullactual 0.5, weight 1.0 (as today)settled
incomplete (time-up, decided on games)derived from gamespartialyespartialexisting 0.6/0.4 at 0.5, level 0.3 (as today), kept as the "시간이 다 됐어요" pathsettled
retiredexplicit (the side that did not retire)the score so faryespartial if max(games) >= 4, else noneactual 0.6/0.4, weight 0.5; no extrapolation; retiredMinGames = 4settled
walkoverexplicitnoneyes (configurable, Q2)nonenonesettled
defaultedexplicitthe score so far, may be emptyyesas retired if play started, else nonesame detail as retiredsettled
abandonednonethe score so farnononenone (as today, 00590:884)settled
non_competitive (기록 제외)noneoptionalnononenone (as today)settled
suspended (a state, §4.1.6)none yetthe score so farno, until resumednonenone; becomes decisive/retired/abandoned laterreleases for others
cancelled (status = cancelled)nonenonenononenonesettled

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.

OutcomeRecorded byConfirmed by
retiredany player of the match (usually the retiring side's own) or the host; names the retiring sideone 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.
walkoverthe side that is present, as a report; or the hostthe host or official (the absent side cannot be asked); the absent side may contest within 48 h (T21)
defaultedthe host or official only (the referee's role; the opponent cannot default a player)none; contest within 48 h by the defaulted side
suspendedhost or official; or any player with one of the other side agreeingthe stored score is confirmed like a claim
abandoned, cancelledhost or official; with no other official, one player from each teamas above
non_competitivehost 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:

  1. Reason list (PickerRow): 한 팀이 도중에 그만뒀어요 (기권) · 한 팀이 오지 않았어요 (부전승) · 비나 시간 때문에 멈췄어요 (중단) · 경기를 하지 않았어요 (취소). A host and officials also see 실격 and 기록 제외.
  2. Who (the same two-line selector as §2.3): "누가 그만뒀어요?" / "누가 오지 않았어요?"; skipped for 중단 and 취소.
  3. 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 미완료).
  4. 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).

OutcomeKorean (Tag / words)EnglishMatch rowResultRow (history)Score as written
retiredTag 기권; 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.)
walkoverTag 부전승W/ONo numerals; the winner's line carries "부전승", the other "불참"Tag 부전승 or 부전패 (패 form unverified in my sources, Q2 note)부전승 (en: W/O)
defaultedTag 실격Def.As retired if play started, else as walkoverTag 승 or 패, meta "실격"6-4 1-0 실격 / 실격
suspendedTag 중단 (warning), fact "6-4 3-1에서 멈췄어요"SuspendedNumerals so far; no winner emphasis; the row keeps its place under 이어 칠 경기not in history until resolved6-4 3-1 중단 (en: suspended)
abandonedTag 미완료UnfinishedPartial numerals, no winner emphasisnot in the record; kept in the session log6-4 3-1 미완료
cancelledTag 취소CancelledNo numerals, secondary inknot shownnone
기록 제외Tag 기록 제외 (as ratified)Not countedas ratified (S6)not shownnone

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.

SettingValueBasis
retiredMinGames4UTR: "at least four games were won by either player" (D). Read as max(team1Games, team2Games) >= 4.
retired weight, actual0.5, 0.6/0.4The existing incomplete branch (00590:238-242); a taste value, not evidence (Q3).
remaindernot extrapolatedUTR (D). DUPR's remainder rule (S) is the alternative and needs sets and points we do not store.
walkover ratedneverUTR (D) and the existing code (00590:884). Nothing was played.
walkover in the recordyesThe owner's ruling; ITF juniors and USTA treat it as a win (S). A friendly pickup option is Q2.
defaultas retired if startedUTR: games before the default may count, the rest not (D).
suspendedResumeDays7A 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 sheet se-stop, partial-score table se-stop-score, suspended row mr-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_unrated signal (00682:1100) already exists for the unrated stops; reuse its copy key family for walkover and abandoned, add the reason.

6. Decisions ​

#DecisionBasisRecommendation
D1Winner first, scores second (§2.3)EvidenceAdopt
D2Gate per player, server-enforced, derived (§3.2)Owner's ruling plus codeAdopt
D3Entered is not settled (§3.2)Owner's rulingAdopt
D4Nudge minutes 3 and 6; the long timers stay for last-round matches (§3.2)TasteAdopt as config
D5retired, defaulted, winner_side, stopped_reason, suspended state (§4.2)EvidenceAdopt
D6Counts and rated as two axes (§4.1.1)EvidenceAdopt

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) ​

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.jsx FvkScoreEntry (4068): drop us/them and the two legends (4139-4185); add winner state, per-set setBlocks, a collapsed set summary, and the 끝까지 못 했나요? PickerRow; new modes stop-reason, stop-score.
  • [ ] FvkScoreTable (4021): add selectable team lines (radio, spoken label).
  • [ ] fvkMatchFact (3982): add state held (per viewer: blocked player, other player, host) and suspended, retired, walkover, defaulted, abandoned, cancelled facts; 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.jsx scoring board: replace the picks frames with winner-first frames (se-open, se-winner-picked, se-other, se-tiebreak, se-best3), add se-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.jsx Flow 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:wireframes per 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.outcome CHECK with retired, defaulted; add winner_side, stopped_reason, resume_of; private.rating_outcome_policy replacing the SQL literals in match_rating_outcome_actual (00590:218-247); private.match_start_blockers(match_id) and the guard on the S0 to S1 transition; generate_session_round refuses with previous_round_unsettled. Entity, mapper, adapter, migration and types in one commit.
  • [ ] packages/app/src/domain/rules/match-outcome.rules.ts (MATCH_OUTCOME_POLICY) and match-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 from MatchFormatSpec; match-card.tsx stop path.
  • [ ] i18n: Tag and push copy (ko, en) in the registry; josaEuroRo tables for the new pushes; signal-routes.ts for N19 to N21.
  • [ ] Update docs/architecture/screen-blueprint.md and docs/history/rebuild-log.md.

Markdown remains the source of truth. Run yarn docs:check before handoff.