Skip to content

Owner review 3, lane C — scoring components, the score state machine, team lines, and the controls and conversation audit (2026-10-11) ​

Status: Draft · Date: 2026-10-11 · Lane C of owner review 3. Inputs: the library v2.1 (§4, §5, §7 to §10), flat-v2-kit-controls.jsx and flat-v2-kit-content.jsx as built at 18:47 (so with the main thread's one-chevron Block change), the boards flat-v2-lib-social, flat-v2-lib-surfaces, flat-v2-detail (sc-*, mb-*, sd-notice-*) and flat-v2-flows (Flow 7), the review-2 rulings and lane A, the shipped i18n and MatchScoreboard, and a peer study of nine products read 2026-10-11. Nothing outside this file was changed.

Owner, review 3. "As for the scoring board, make sure the new components are all aligned, spaced and styled in the canonical way. And we need to ensure the flow logic is all properly configured here because there can be a lot of back and forth here." "As for match row, 대 in place of vs is just wrong. And make sure we style different teams and teammates accordingly so that there can be no visual confusion about who's who." "And can you make sure all of the components in the library and foundations are in line with industry best practices?" (this lane: controls and fields §7, conversation §9)

Coordinator addition. "Let's minimize the use of custom implementations across all building blocks": §6 lists every place the scoring pieces redo what a shared primitive owns.

Evidence tags. D a documentation page or token file read (URL in §9) · F an App Store frame (iTunes lookup, country kr) · W a web page rendered in headless Chrome at 390 CSS px · P a pixel or DOM measurement of a built board (getBoundingClientRect over CDP on the 18:47 build, copies kept in the scratchpad) · C code or a document in this repo (path:line) · S a search snippet only (none of the claims below rests on S alone). "Mock" means a scratch HTML page outside the repo that checks a proposed value; it proves fit, not taste.

1. At a glance ​

#PointRecommendation (exact values, type roles, tokens)EvidenceKit functions and library entries to change
1Scoring components: aligned, spaced, styled canonically20 deviations measured, 8 of them major (§2.2). The ones that change what a reviewer sees: AnswerPair buttons are 44 tall with 0 gap under line 2 (spec: sm 36, 8 gap); the S2 Button is nested in the row's press area and centred on line 1 (centre 67; row centre 78; the 바로 확정 Button is centred on the row); the Sheet title row is 50 with a sub line (spec 48); the decision sheets draw radio rows with no unselected control; the same claim reads 6-4 in one surface and 4 / 6 in another; the Block shows both numerals in ball. Everything else passes: rail 20, Tag sm 24, set cells 48 × 48 r12, sheet footer 16 / 8 / 52 / 16 + 34, Dialog 350 × 192, row pitch 68, type roles, targets. The verifier passes all of it because it checks no gaps (§2.4).P, C, DFvkAnswerPair, FvkCRow, FvkMatchRow, FvkSheetBox, FvkCheckRow, FvkCBlockMatch, FvkConfirmSheet, FvkDisputeSheet, FvkDecideSheet; library §5 AnswerPair, §8 Sheet, §10
2Flow logic as a complete state machine10 states (S0 upcoming, S1 playing, S2 awaiting, S3 pending, S5c countered, S5 disputed, S4 confirmed, S4h host-set, S4c contested, S6 not counted) plus one revision overlay, 29 transitions, 13 races, 10 policy keys (three the library's prose uses are missing from its table). Rules added: a counter by the submitter's partner becomes a new claim, never a confirmation; the host is told only when the submitter declines the counter or 24 h pass; a contest is resolved by another official, or by the host if there is none, and unresolved at 7 days it stands; every mutation carries claim_version and an idempotency key, so the first commit wins and the second gets a typed refusal the sheet renders. The boards draw 9 of the 29 transitions fully, 6 partly and 12 not at all; 27 frames are missing (15 of them P0). Flow 7's marks all describe real paths; the 3-tap count holds only when the player's team won. Four paths to add (§3.8).C, D, Plibrary §10 (states, policy, Match row, request row, Dispute, Decide), flat-v2-lib-social, flat-v2-detail sc-* mb-*, flat-v2-flows Flow 7; IA §3 flow 7
3Match row: teams and teammatesOne shared FvkTeamLines primitive. One team per line; a doubles pair on its line as two names with the 1px break; the set scores as 32-wide tabular columns on each team's own line, so no "6-4" ever has to be read in an order; winner emphasis only on accepted results (names 600 ink vs 400 $textSecondary, numerals 700 ink vs 700 $textSecondary; on the Block, ball vs white α0.7); the viewer's team first, the viewer's name suffixed "(나)"; no separator word anywhere in rows; "vs" in running text (the shipped copy already says vs ${names}), "대" only where a score is read aloud ("6 대 4"). The Match row becomes a header line (court, one fact, then the one trailing element) over the two team lines: 104 with a Tag and no numerals, 108 with numerals, 116 with a Button at 390 (today 68). The same primitive draws the Block (zone B), the Confirm and Decide sheets, the ScoreEntry table, the request row and ResultRow.W, F, C, P, mocknew FvkTeamLines in flat-v2-kit-content.jsx; FvkMatchRow, FvkResultRequestRow, FvkResultRow, FvkCBlockMatch, FvkScoreTable, fvkCtlBoardRow (deleted); library §4 Match state block, §5 ResultRow and a new Team lines entry, §10
4Best-practice audit of §7 and §941 components audited (§7, §9, §10, plus the Match state block, ResultRow and AnswerPair): 26 none, 6 minor, 9 major. Majors outside the scoring pieces: single-choice rows with a footer CTA show no unselected control (M3 radio 20 + 40 state layer, Seed radiomark 20 / 24, HIG "small, circular button"); ProviderButton: iOS needs an equivalent login beside Kakao (App Review 4.8) at no smaller than the Kakao button (HIG). Minors: Button pair resolves to 1 : 1.64, not 1 : 2; the date picker spec describes an Android wheel that the shipped picker does not draw; dark bubbles are 1.10 : 1 apart; Reactions chips 28 (Seed 32, M3 32); ResultRow shows only the opponents; three policy keys.D, P, CFvkCheckRow, FvkButtonPair, FvkBubble, FvkReactions, FvkResultRow; library §7 ProviderButton, Checkbox and radio rows, Date and time picker, Button, §9
5Custom implementations20 places where a scoring piece redoes layout, typography, spacing or a control that a shared primitive owns (§6). The largest: seven renderings of "a team" (Block, Match row, ScoreTable, fvkCtlBoardRow, the request row's winners fact, ResultRow's opponents, the tournament Choices), four copies of the Input border logic (FvkInput, FvkTextArea, ScoreTable cell, tiebreak box), two copies of the Tag + score cluster.CFvkTeamLines (new), fvkCtlFieldBox (new), FvkChoiceSet, FvkCRow control, FvkSheet bleed

Top ten drifts, by severity.

  1. Teams are drawn seven different ways, and the Match row puts both teams on one line separated by 대 (English: a stranded "vs"): the owner's point 3 (§4.1, DV3).
  2. AnswerPair buttons are 44 tall with no gap under line 2 (spec 36 and 8): every request row (DV1).
  3. The S2 Button sits inside the row's press area and 11 points above the row's centre, while 바로 확정 is outside and centred (DV2).
  4. The state machine stops short: S5c and S4c do not exist, a contest has no resolver, a partner's counter would count as a confirmation; 12 of 29 transitions have no screen and 27 frames are missing (§3).
  5. Decision sheets draw radio rows with no unselected control, so nothing says the rows are choices (DV6).
  6. The same claim reads 6-4 in one surface and 4 over 6 in another, repaired by a "winners" fact (DV5).
  7. A loss costs a hidden second tap on a quick pick (§4.5).
  8. ProviderButton: no Sign in with Apple beside Kakao for an iOS build (App Review 4.8, §5.3).
  9. The Block and the escalated rows show numerals no team owns, and the Dispute sheet no longer shows the claim it disputes (DV4, DV7, DV8).
  10. Dark-mode bubbles are 1.10 : 1 apart; mine and theirs differ only by side (§5.2).

2. Point 1: are the scoring components aligned, spaced and styled canonically? ​

Question. Check every scoring frame on flat-v2-lib-social (ScoreEntry, Result request row, Dispute sheet, Host decision sheet, Match row states), flat-v2-lib-surfaces (Match state block) and flat-v2-detail (mb-*, sc-*, sd-notice-*) against the library's row, sheet, block, button and type rules, and list every deviation with the measured and the canonical value.

2.1 Method ​

Each frame (a data-fvk-frame phone or a data-fvk-stage) was opened in headless Chrome, and every [data-g] part and every text node was measured relative to its frame: x, y, width, height, font size, line height, weight, colour (P). The frames measured: 44 library frames (Match row 7, request row 6, ScoreEntry 10, Dispute 5, Decide 5, Match state block 11), eight detail frames dumped part by part (sc-ideal, sc-entry-player, sc-hostset, sc-results, sd-notice-confirm, sd-notice-host, mb-escalated, mb-o-confirm-all) and 22 detail frames rendered and read (sc-*, mb-*, sd-notice-*), plus a census of every data-g part on flat-v2-lib-controls and flat-v2-lib-social for §5. node scripts/verify-flat-v2-boards.mjs passes on flat-v2-lib-social (89 frames), -lib-surfaces (122), -lib-controls (174), -detail (277) and -flows (65) (rail 0, type 0, targets 0, shadow 0, dot 0, ball 0): none of the deviations below is something it checks.

2.2 Deviations, with the measured and the canonical value ​

Severity: major breaks a library rule the owner can see, or changes what a person understands; minor is off by a grid step, a doc slip, or a missing spec line.

#WhereMeasured (P unless C)Canonical (source)SevFix
DV1AnswerPair in the Result request row (resultrequest Ideal; sc-results)The two buttons are 145 × 44 and start at the bottom of line 2: line 2's box is y 82 to 100, the pair y 100 to 144, hit overlay 96 to 148. Gap line 2 to fill 0. At 200% the stacked buttons are 298 × 58, 8 apart (correct).Two sm (36) tonal Buttons, "8 under line 2", the row grows by 44 = 8 + 36, each with a 44 target (library §5 AnswerPair, §7 Button sm 36). Seed small is also 36 (D).majorFvkAnswerPair: the cell stops stretching (alignItems: flex-start), marginTop: 8, button natural 36 with fvkCtlHit(4); the row still grows by 44 (8 + 36), so it stays 112. Blast radius: every row with answer (friend and applicant requests too).
DV2S2 Button on a Match row (matchrow Host, Player; mb-ideal; sc-draws)점수 입력 and 대신 입력 are inside the row's press area (data-hit div contains the Button) and centred on line 1: Button y 49 to 85 (centre 67) in a row y 44 to 112 (centre 78), 11 above centre. The 바로 확정 Button is outside the press area and centred on the row (y 60 to 96, centre 78). Cause: FvkCRow puts every trailing in a box l1H high; the S2 Button is passed as trailing, 바로 확정 as control."A Button is its own stop after the row" (library §10 Match row, §5 Row trailing control); "a control taller than 24 centres on the row" (library §5 PersonRow, Trailing alignment).majorPass every Button as control; give FvkCRow the rule (trailing taller than 24 centres on the row) so no caller can get it wrong; in the header-first anatomy of §4.4 both Buttons centre on the header line.
DV3Match row line 1 (matchrow, all stages; sc-draws; mb-*)Two teams share line 1, told apart only by a comma between partners and the word 대 between teams: "김민준, 이서연 대 박지훈, 최유나" is 190.4 wide at 15/22·600. In English the line wraps with "vs" at the end of line 1 (first rect 193.2 wide, "Minjun Kim, Seoyeon Lee vs"), "Jihun Park, Yuna Choi" on line 2.Owner review 3: 대 is wrong, teams and teammates must not be confusable. The shipped copy writes vs ${names} (C: ko/home.ts:102, en/home.ts:103).major§4: one team per line.
DV4Match state block (matchstate all frames; sc-* live frames)Both team rows are onBlockMeta 15/22·600 and both numerals are ball rgb(214,240,74) 24/32·700: a confirmed or host-set result has no winner. The viewer's team is first but unmarked. Only the Tag says anything about state.The library draws both numerals in ball (§4 Match state block) and puts the viewer's team on top; the owner's review-3 ask is that teams read apart at a glance.majorFvkTeamLines tone="onBlock": winner numeral ball, loser white α0.7; the "(나)" suffix.
DV5Score order across surfaces (sc-results against sc-live-confirm and rq-sheet)The same claim is "6-4" in the Match row cluster and the request row title ("김민준 님이 6-4를 입력했어요"), "4 over 6" in the Block and the Confirm sheet (viewer's team on top), and the request row's second fact names the winners ("김민준, 이서연 승") only to repair the order.One reading of a score: library §10 "so the order of the numbers never has to say who won" (the repair is a symptom).majorScores live on their team's line (§4.3); the claim string "6-4" stays only in push and spoken text, in the submitter's order, stated with the winner.
DV6Dispute sheet and Host decision sheet (dp-*, dc-*)Single-choice rows are plain text with a hairline: no unselected control; the chosen row bolds and shows a 20 check at the end. The CTA (보내기, 확정하기) is soft-disabled until a row is chosen, and nothing on screen says the rows are choices.Radios show their unselected state: M3 radio icon 20, state layer 40 (D); Seed radiomark 20 / 24 (D); HIG "a small, circular button followed by a label" (D). Our own checkbox rows draw the empty 2px square.majorFvkCheckRow gets kind="radio" lead: a 24 ring (2px $textSecondary edge, ink 12 dot when chosen) in the checkbox slot (24 + 12), content at 56. Used wherever a footer CTA follows the choice; the trailing check stays for tap-to-select-and-close value pickers (S-3).
DV7Dispute sheet title blockTitle only; no match excerpt and no claim. Confirm and Decide carry the match as sub. The claim the person is disputing was on the sheet this one replaced ("a Sheet never sits over a Sheet").Library §10: Confirm and Decide "the match as its excerpt".majorsub = match name on every score sheet; the Dispute sheet shows the claim as a read-only FvkTeamLines above the reasons.
DV8Host board, escalated rows (mb-escalated)The claimed score is a plain fact in line 2 ("1라운드 1번 코트 | 6-4") and the row reads "김민준, 이서연 대 정우진, 한소희". The host presses 바로 확정 on a number that belongs to no named team.Library §10 Escalated: "the score moves to line 2".majorShow the TeamLines (scores on their lines); the control sits in the header line (§4.4); the score never becomes a fact.
DV9Sheet title row with a sub (Confirm, Decide, MenuSheet quick row)Title row is 50 tall: sheet y 246, × target y 267 (centred in 266 to 316), first row y 316. Cause: the title block carries padding: 4px 0, 4 + 24 + 18 + 4 = 50. Every row below sits 2 off the 4pt grid."Title row 48" (library §8 Sheet, §1.3 grid).minorFvkSheetBox: title block padding: 3px 0 (3 + 24 + 18 + 3 = 48).
DV10Hairline after an expanded radio row (dp-ideal, dc-enter)After 점수가 달라요 (or 직접 입력) opens its table the hairline to the next Cell is missing (no hair-cell between y 272 and 456) while every other pair of rows has one (last={i === 0 && reason === 0})."The cell hairline runs between the Cells of one group and none follows the last" (library §5 Cell).minorDraw the hairline after the expanded block; the expanded content belongs to its Cell.
DV11Numeral colour of one datumConfirm-sheet numerals are ink 24/32·700; the Block shows the same two numbers in ball.One treatment per datum (library "single source of truth").minorFalls out of FvkTeamLines (§4.3).
DV12sd-notice-confirm, sd-notice-host titles"내 경기 2개의 점수를 확인해 주세요" is 15 syllables, "점수 확인이 필요한 경기가 있어요" 14; the body "점수 입력 전 3경기, 확인 대기 2경기, 분쟁 1경기" is facts written as a sentence. The player Notice says 2 matches while sc-results shows 1 request.Notice title "a state in 12 syllables or fewer" (library §4 Notice, Content); facts are separate nodes.minorTitles "확인할 점수가 N개 있어요" (11) and "처리할 점수가 있어요" (9); facts as MetaSegments; make the sample data agree (2 and 2, or 1 and 1).
DV13Result policy (library §10)The table lists 7 keys; the paragraph under it uses three more as literals: "2 hours later", "30 days", "24 and 72 hours"."All numbers live in one policy object, never a literal in an RPC or a screen" (library §10 Result policy).minorAdd entryReminderHours = 2, noResultVoidDays = 30, hostDisputeReminderHours = [24, 72].
DV14Button pair widthFooter pair 126 : 212 (1 : 1.68) in sheets, 112.7 : 185.3 (1 : 1.64) in the Dialog. Cause: flex: 1 and flex: 2 share only the free width after each button's 40 of padding."tonal flex 1 + primary flex 2" (library §7 Button, §8 Dialog), which a reader takes as 1 : 2 (112.7 : 225.3 at 350 with the 12 gap; 99.3 : 198.7 in the 310 Dialog).minorSay "the primary takes two thirds of the line" and set width: 33.33% / 66.67%, or reword the spec to the measured ratio. HIG prefers equal size ("use style, not size", D): ratified departure, see §5.3.
DV15Board framing, request rowsThe ideal frame ends on the pair's bottom edge (stage 256 = pair bottom 256): pad={0} plus last trims the 12.A frame is a stage with air; other LibSocRound frames add 16 below.minorpaddingBottom: 16 in LibSocResultRequest frames.
DV16Spec text of the Match row (board against library)Board "김민준 이서연 대 박지훈 최유나" (no commas); library "김민준, 이서연 대 박지훈, 최유나"; kit joins with a comma and a no-break space.One text per rule.minorRewritten with §4 anyway.
DV17Button rule's list of pair uses"A tonal Button sits beside it only in a pair (Dialog, wizard confirm step, FilterSheet footer)": the Confirm sheet's 거절 · 동의 is a fourth, undocumented use.Library §7 Button.minorAdd it to the list.
DV18Tournament board winner pick (DtlTnMatch)Two fill Choices whose labels are fvkTeamName(...): teammates joined with a comma inside a pill, the only surface where a pair is a Choice label.One pair rendering (the 1px break).minorFvkNames inside the Choice label.
DV19ScoreEntry captionThe Caption row is pulled up with margin: -6px -20px 10px: caption row y 284 to 316 overlaps the title row (y 242 to 290) by 6, and its text starts 0 to 2 below the × target's bottom edge.A sheet body that bleeds uses FvkSheet bleed, not a negative margin.minorMove the line under the title as the sheet's description slot.
DV20Tiebreak pointsScoreEntry records "타이브레이크, 진 쪽 점수" in a 56 × 36 box, but no row, block or sheet displays it: 7-6 is all anyone sees. The shipped scoreboard draws a 9px superscript (match-scoreboard.tsx, SCORE_SUP_WIDTH = 10), below the library's 12 minimum.Library §1.4 minimums (12 for non-essential text).minor (spec gap)FvkTeamLines draws the loser's tiebreak points as a 12/18·600 superscript in $textSecondary, slot reserved per match (§4.3).

2.3 What was measured and passes ​

Rail 20 on every text node (verifier, and by hand at 390). Row pitch 68 for every Match row at 100% (12 + 22 + 4 + 18 + 12). Tag sm 24, centred on line 1 (Tag y 123 in a row at 112: 11 below the row top, line 1 centre 23). Score cluster right edge 370 (= 390 − rail), Tag to score gap 8. Request row: avatar 40 at x 20, text edge 72, pair width 298 and two 145 buttons 8 apart (spec (298 − 8) / 2). ScoreEntry sheet: height 478 including the 34 inset, × target 48 at the rail, set cells 48 × 48 r12, team rows 8 apart, header labels 13/18·500, quick picks Choice 36 with gap 8, helper 15/22 8 above the 52 button, 16 below it, then the inset (matches library "open 478"). Dialog 350 × 192, pair 52. Block 350 × 144 (170 with a claim: 20 + 18 + 8 + 72 + 8 + 24 + 20), zone gaps 8, numeral columns 32, the Caption row's text 6 under the block and 24 to the next head. Type roles on every scoring text node are in the scale. A one-line Notice is 350 × 48. A scan of every data-g box in the scoring frames (368 in the five lib-social sections, 20 in the Match state block, 956 in the detail frames) finds no box off the 4pt grid except text-driven stacks the library exempts (sheet heights 478, 430, 374; the set-table header 18 + 8; the claim Block 170; a two-line Notice 70), hairlines and the 6 dots; the one padding off the grid is the sheet title row (DV9).

2.4 Prevention: what the gate does not see ​

The verifier checks rail, type, targets, shadow, dot, ball, flow marks, ranges and scores, so a 44-tall button with a zero gap passes. Four checks to add, each with a canary (the rule in CLAUDE.md: every fix ships with prevention):

  1. gap: named gaps are measured against the spec table: AnswerPair top − line 2 bottom = 8, AnswerPair button height 36, sheet title row 48, Caption row text 6 under a block, control centre − row centre = 0. A canary frame with the DV1 values must fail.
  2. team: a visible text node matching (대|vs\.?)\s between two name nodes inside a frame fails; "6 대 4" (a score), "대신 입력", "대진표" and "대기" pass. Canaries: "김민준, 이서연 대 박지훈, 최유나", "Minjun Kim vs Jihun Park".
  3. names: inside [data-g="team-lines"] a comma between names fails (partners take the break); exactly two [data-team] children.
  4. radio: a radio group followed by a sheetfooter must draw an unselected control in every row.
  5. In code: a lint that bans ' & ', ' vs ' and ', ' joins of player names outside joinNames() (the shipped PLAYER_JOINER = ' & ' in match-history-display.ts:14 and myTeamLabel are the first hits).

3. Point 2: the flow logic as a complete state machine ​

Question. Make the score flow's back and forth fully specified: every state, event, guard, side effect (which push to whom, with copy; the ELO apply or revert), timer, and the surface and frame that draws it, then show what the boards already draw and what is missing.

Basis. The model is the review-2 ruling (library §10, rulings, lane A §3). What it did not settle, and this section does: the state between "declined with a counter" and "the host decides" (the owner's list has "countered"), who resolves a contest, what happens when the host is the only official, the partner's counter, races, and the corrections and reopening paths. Everything marked new is a proposal for veto; nothing here contradicts a ratified ruling (§3.9 lists the decisions).

3.1 Rules the machine must hold ​

  1. A result is a proposal until the other side, or the host, says yes. Silence never confirms (autoConfirmHours = null).
  2. ELO moves only on entry to S4 or S4h, is reverted only by a move to S6 or replayed by a revision, and nothing in S2, S3, S5c, S5 or S6 is rated.
  3. One writer per match. Every mutation takes lock_match_score_write(match), carries claim_version and an idempotency key, re-reads the state under the lock, and either commits or returns a typed refusal (claim_changed, already_settled, not_pending, window_closed, not_allowed). The first commit wins. The UI shows a refusal as a Notice inside the open sheet and refreshes it; never a toast, never a silent overwrite.
  4. Back and forth is bounded. An edit is possible only while nobody has answered. One counter per claim. The submitter's reaction to a counter is final; after it the host decides. A path that returns to S3 (an edit, a partner's counter) restarts both clocks, so each loop needs a person to act again and nothing waits forever.
  5. Nobody judges their own match. A host who plays is a player there. A contested host result goes to another official when one exists.
  6. Tell each person once per change, in their role. The host is told only when something needs them.
  7. Every state has a screen for every role that can be in it (the matrix in §3.7).

3.2 States ​

IDState (ko, en)matches.statusCall row (match_score_calls)RatingTagEntered by
S0예정, Upcomingschedulednonenone예정the draw
S1경기 중, Playingin_progressnonenoneLivethe host starts the round
S2점수 입력 전, Awaiting resultawaiting_resultnonenonenonethe host ends the round, the next round starts, or the rotation timer
S3확인 대기, Pendingawaiting_resultpending, claim v n, who, whennone확인 대기a player's claim
S5c분쟁, countered (new)awaiting_resultcountered: claim plus the counternone분쟁an opposing player or the submitter's partner declines with a score
S5분쟁, Disputedawaiting_resultdisputednone분쟁a decline with no score; the submitter declines the counter or 24 h pass on S5c
S4확정, Confirmedcompletedclosed, verified_by = both sidesapplied확정one opposing player agrees; the submitter takes an opponent's counter
S4h호스트 확정, Host-setcompleted, result_source = hostclosedapplied (no bonus)호스트 확정a host or official decides, 바로 확정 or 모두 확정, or 대신 입력; the contest window is contestHours
S4c이의 접수, Contested (new)completeda contest event, reviewer namedstands호스트 확정, line 이의가 접수됐어요a player contests S4h
S6기록 제외, Not countedcompleted, outcome = non_competitiveclosednone (reverted if it was applied)기록 제외a host or official; the 7-day expiry; 30 days with no result
overlay R수정 요청, Revision pendingunchangeda correct_score callstands until acceptedthe Tag it had, plus a linea player requests a correction within correctionDays

S5c and S4c add no Tag word: a counter is still a dispute (분쟁) and a contested host result is still 호스트 확정 with a different line, so the vocabulary the owner approved stays the same. 분쟁 means "it needs the host" only in S5; the Tag in S5c is the same word with the line "상대가 다른 점수를 말했어요", because the people in S5c can still settle it themselves.

The machine on one page (arrows are the transitions of §3.4; R is the revision overlay):

S0 → S1 → S2 ── submit ───────────────→ S3 ── one opposing player 동의 ─────────→ S4 ── 수정 요청 ─→ R ─→ S4
 │          │                            │ ↺ edit (S3) · withdraw (S2)
 │          ├ host 대신 입력 ─→ S4h      ├ decline + score ─→ S5c ── submitter takes an opponent's counter ─→ S4
 │          ├ host 기록 제외 ─→ S6       │                     ├ takes the partner's counter ─→ S3 (new claim)
 │          └ 7 d / 30 d ─→ S2 locked,   │                     └ declines, or 24 h ─→ S5
 │            then S6                    ├ decline, no score ─────────────────→ S5
 │                                       └ 24 h, no answer ─→ host task ─ 바로 확정 / 모두 확정 ─→ S4h
 S5 ── host: claim, counter, 직접 입력 ─→ S4h ── 이의 있어요 (48 h) ─→ S4c ── 유지 / 수정 ─→ S4h · 기록 제외 ─→ S6
 S5 or S5c ── 기록 제외, or 7 d ─→ S6          S4c ── 7 d ─→ S4h (the result stands)
 S6 ── 다시 열기 ─→ S2                         ELO is applied on entry to S4 or S4h only

3.3 Policy: every number, one object ​

KeyValueUsed by
autoConfirmHoursnullsilence never confirms
reminderHours12T15
hostEscalationHours24T16, and S5c to S5 (T10): no new key
entryWindowDays7T20
contestHours48T11, T12, T17, T18 open the window; T21 uses it
disputeVoidDays7T14, T25: no new key for contest resolution
correctionDays7T26
entryReminderHours2T1 (in the library's prose, missing from its table)
noResultVoidDays30T20 (same)
hostDisputeReminderHours[24, 72]S5 reminders to the host (same)

The rating for T18 is applied after the 8 s undo window of the Toast, so 되돌리기 is a plain withdrawal and never a rating replay (recompute_all_ratings takes an exclusive lock and replays every match, lane A §3.10).

3.4 Transitions ​

"UI" names the surface and frame id. ✓ the boards draw it; part some of its surfaces are drawn; ✗ id missing, id proposed in §3.7. Push copy is in §3.4b.

A. Recording and confirming

#TransitionEvent and guardEffectsTimerUIDrawn
T1S1 to S2The match ends: 라운드 종료 by a host or official, the next round starting, or the rotation timer.N1 to the four; ended_at stamped; N2 at entryReminderHours to those who have not entered.2 hBlock 점수 입력 sc-entry-player; Match row S2 (lib-social Host, Player, Spectator); push opens the scorecard at that match✓
T2S2 to S3, or S1 to S3 once the scheduled end has passedSubmit (확인 요청). A player of the match (a host who plays is one). Not past session date + entryWindowDays; sets valid for the match rules; idempotency key.Claim v1; N3 to the two opposing players and an in-app FYI to the partner; Toast "결과 확인 요청을 보냈어요" with 되돌리기 for 8 s (T4). No rating.clocks start: 12 h, 24 hsc-entry-player, sc-o-entry-player, sc-waiting; Match row S3. A playing match past its end offering 점수 입력: sc-live-overtime ✗✓
T3S3 to S3Edit (수정). The submitter only, and only while nobody has answered.Claim v+1; every earlier response is void; clocks restart; N5 to the opponents.restartthe submitter's claim sheet rq-submitter-sheet ✗, then ScoreEntry prefilled (se-open with sets)✗
T4S3, S5c or S5 to S2Withdraw (요청 취소, or 되돌리기 within 8 s). The submitter, until the host has decided.Call closed and kept in the log; the opponents' request rows vanish and, from S5, the host's task closes; N6 low to them; Toast "요청을 취소했어요".noneToast "Sent" (lib-social ScoreEntry) has 되돌리기; the 요청 취소 button rq-submitter-sheet ✗part
T5S3 to S4Confirm (동의). One player of the other team; not the submitter's partner; call still pending; claim_version equals the one shown.verified_by = submitter and confirmer (+10% bonus); ELO applied once; N7 to the other three players, then elo_changed; Toast "동의했어요" (tt-answers ✗).nonerequest row sc-results; Confirm sheet sc-consensus, rq-sheet; the push (1 tap); Block S4 (lib-surfaces "Confirmed")✓

B. Disagreement (the back and forth)

#TransitionEvent and guardEffectsTimerUIDrawn
T6S3 to S5cDecline with a counter-score (거절, 점수가 달라요, 내가 아는 점수, 보내기). Any of the other three players (an opponent, or the submitter's partner); S3; a valid counter.Claim kept, counter stored with its author; N8 to the submitter (FYI to the partner). The host is not told.12 h reminder to the submitter; T10 at 24 hDispute sheet dp-ideal; S5c row mr-countered ✗; Blocks sc-live-counter ✗ (submitter) and sc-live-disputed ✗ (the decliner's line); counter row in lib-social "Counter", page sc-counter ✗part
T7S3 to S5Decline without a counter (선수가 달라요; 경기를 하지 않았어요; 점수가 달라요 with nothing typed). As T6.N9 to the submitter and partner; N10 to the host and officials who do not play (high).host reminders 24 h, 72 hdp-other; host mb-dispute; Match row S5 (host, player); Block disputed (lib-surfaces); players' Block page sc-live-disputed ✗; Decide with no counter dc-nocounter ✗part
T8S5c to S4Take the counter (동의 on the counter row). The submitter, and the counter came from an opponent.Both sides now name one score: confirm with it; verified_by = submitter and counter-giver (bonus); ELO applied; N7 to the counter-giver and the partner.nonecounter row (lib-social "Counter"); page sc-counter ✗; sheet rq-counter-sheet ✗part
T9S5c to S3Take the partner's counter. As T8, but the counter came from the submitter's own partner.Not a confirmation: no opponent has agreed to the new score. It becomes a new claim v+1 with the counter's sets; clocks restart; N5 to the opponents.restartthe counter row names its author ("팀원이 6-3을 말했어요") rq-counter-sheet ✗✗
T10S5c to S5Decline the counter (거절 on the counter row), or 24 h pass. The submitter, or the timer.N10 to the host and officials (high); the players' line becomes 호스트가 확인하고 있어요.hostEscalationHourscounter row 거절 (sc-counter ✗); host mb-dispute✗
T11S5 to S4hThe host decides version A or B (확정하기). A host or official who does not play that match; the sheet names the version and claim_version.Host-set, final, no bonus; ELO applied (after the undo window if any); the contest window opens; N11 to the four (high).48 hmb-o-decide, dc-ideal✓
T12S5 to S4hThe host enters another score (직접 입력). As T11, sets valid.As T11 with the typed sets.48 hdc-enter✓
T13S5 or S5c to S6The host excludes (기록 제외). As T11.Outcome non_competitive; no rating; N12 to the four.nonedc-void; row S6 for the host (lib-social Host)✓
T14S5 or S5c to S6Expiry. disputeVoidDays unresolved.N18 to the four.7 dS6 Match row ✓ (one row for every viewer); the Block sc-void ✗part

C. Silence, and the host stepping in

#TransitionEvent and guardEffectsTimerUIDrawn
T15S3 (stays)Reminder.N4 to the opposing players who have not answered.12 hpush only(none)
T16S3 (stays)Escalation.The 확인 대기 N경기 section for the host and officials; a once-a-day digest N10b. Players' lines do not change.24 hmb-escalated, mb-o-confirm-all✓
T17S3 to S4h바로 확정 on one row, or 모두 확정 (a Dialog that names the count). A host or official who does not play; the rows of matches the host plays in are left out and the Dialog says how many.Host-set; ELO applied; N11 per match; no undo once rated.48 hmb-escalated, mb-o-confirm-all; result Toast tt-answers ✗✓
T18S2 to S4h대신 입력 (점수 저장). A host or official who does not play.Final at once; ELO applied after the 8 s window; N11; Toast "6-4 점수가 저장됐어요" with 되돌리기.48 hmb-ideal, mb-o-entry, mb-o-typing, mb-o-tiebreak, mb-o-error, mb-o-leave, mb-o-toast✓
T19S2 or S3 to S6Exclude a match that was not played (rain, no-show). A host or official who does not play; from S3 the pending claim is closed in the log.N12; no rating; Toast with 되돌리기.nonethe row's official menu, mb-o-exclude ✗✗
T20S2 stays, then S2 or S3 to S6Entry window. After entryWindowDays players can no longer enter (a host or official still can); noResultVoidDays after the session with no accepted result (a claim nobody answered counts as none): S6.Host digest "입력되지 않은 점수가 N경기 있어요"; N18 at expiry.7 d, 30 dthe expired row state mr-s2-expired ✗✗

D. After acceptance

#TransitionEvent and guardEffectsTimerUIDrawn
T21S4h to S4cContest (이의 있어요). A player of the match, once each, within contestHours, not the setter. Reason (the same three as the dispute) and an optional counter.N13 to the reviewer (another official when one exists, else the setter); ELO stands; Toast "이의를 전했어요" (tt-answers ✗).48 h windowthe Notice in sc-hostset; the contest sheet ct-ideal ✗part
T22S4c to S4hUphold (유지). The reviewer.Contest closed, no second window; N14 "그대로 유지됐어요" to the contester and the four.nonethe reviewer's Decide sheet dc-review ✗✗
T23S4c to S4hChange (a new host-set score). The reviewer.New revision; ELO replayed; N14 "…으로 바뀌었어요".nonedc-review ✗✗
T24S4c to S6Exclude. The reviewer.ELO reverted by replay; N12 and N14.nonedc-review ✗✗
T25S4c to S4hExpiry. disputeVoidDays with no decision: the result stands.N14 "그대로 유지됐어요".7 dpush only(none)
T26S4 or S4h to overlay RRequest a correction (수정 요청). A player of the match, within correctionDays of acceptance, no contest open.A correct_score call on the completed match, with the S3 rules (one player of the side opposite the requester answers); N15; ELO stands.12 h, 24 h as S3ScoreEntry prefilled cr-ideal ✗; request row kind correction rq-correction ✗✗
T27overlay R to S4Accepted. One opposing player agrees, or the requester takes a counter.Sets replaced, score_revision + 1; ELO replayed (exclusive lock, bounded by correctionDays); N16 and elo_changed.none✗✗
T28overlay R to a host decisionDeclined, or 24 h with no answer.A host task: keep the original, the requester's version, enter another score, or exclude.24 hdc-review family ✗✗
T29S6 to S2Reopen (다시 열기). A host or official who does not play.N17 to the four; no rating effect (an excluded match is unrated, or was reverted at exclusion).nonethe S6 row's control mb-o-reopen ✗✗

Tally: 9 transitions fully drawn, 6 partly, 12 not drawn, 2 push-only (29 in all; T14, T15, T16, T20 and T25 are pure timers, and T1, T10 and T28 can also be fired by one).

3.4b Push copy ​

Title and body, 해요체; the body names the round and court so a notification is readable alone. The push opens the scorecard at that match for players and the 대진표 for officials (signal-routes.ts routes by role). "N" is the copy id used in §3.4. English follows the same keys.

NWhen (to whom)TitleBody
N1T1, to the four점수를 입력해 주세요3라운드, 2번 코트, 경기가 끝났어요
N2T1 +2 h, to those who have not entered점수 입력이 아직 남았어요3라운드, 2번 코트
N3T2, to the two opponents (the partner: in-app, low, 팀 점수가 입력됐어요)점수 확인이 필요해요3라운드, 2번 코트, 김민준 님이 6-4를 입력했어요
N4T15, to opponents who have not answered점수 확인이 아직 남았어요3라운드, 2번 코트, 김민준 님이 6-4를 입력했어요
N5T3, T9, to the opponents점수가 바뀌었어요3라운드, 2번 코트, 김민준 님이 6-5로 고쳤어요. 다시 확인해 주세요
N6T4, to the opponents (low)점수 입력이 취소됐어요3라운드, 2번 코트, 다시 입력해 주세요
N7T5, T8, to the other three players점수가 확정됐어요3라운드, 2번 코트, 6-4
N8T6, to the submitter (the partner: FYI)상대가 다른 점수를 말했어요3라운드, 2번 코트, 박지훈 님이 6-3을 말했어요
N9T7, to the submitter and the partner점수에 이견이 있어요3라운드, 2번 코트, 호스트가 확인해요
N10T7, T10, to the host and officials who do not play (high); N10b the daily digest for T16점수 확정이 필요해요; 확인 대기 3경기가 있어요3라운드, 2번 코트, 두 팀의 점수가 달라요; 24시간 넘게 답이 없는 점수예요
N11T11, T12, T17, T18, to the four (high)호스트가 점수를 확정했어요3라운드, 2번 코트, 6-4
N12T13, T19, T24, to the four기록에서 제외됐어요3라운드, 2번 코트, 레이팅에 반영되지 않아요
N13T21, to the reviewer점수 이의가 접수됐어요3라운드, 2번 코트, 박지훈 님이 이의를 냈어요
N14T22 to T25, to the contester and the four점수 이의가 처리됐어요3라운드, 2번 코트, 그대로 유지됐어요 | 6-3으로 바뀌었어요 | 기록에서 제외됐어요
N15T26, to the opposing side of the requester점수 수정 요청이 왔어요3라운드, 2번 코트, 김민준 님이 6-3으로 고쳐 달라고 했어요
N16T27, to the four점수가 수정됐어요3라운드, 2번 코트, 6-3
N17T29, to the four점수 입력이 다시 열렸어요3라운드, 2번 코트
N18T14, T20, to the four기록에서 제외됐어요3라운드, 2번 코트, 7일 동안 정해지지 않았어요

Copy rules found on the way. (1) The object particle after a score follows the digit as read (6-3을, 6-4를; the library already says it). The (으)로 particle has a different table: 으로 only after 0, 3 and 6 (영, 삼, 육); 로 after 1, 7, 8 (ㄹ-final) and after the vowel digits 2, 4, 5, 9. "6-3으로", "6-4로", "6-1로". josaEulReul and a future josaEuroRo both need the digit tables. (2) Never "6-4" alone in a push to an opponent: the body names who entered it, because the order of the numbers is the submitter's. (3) N7 is today's match_completed re-worded, and the shipped title-less pushes ("대진 / 대진", lane A §1.3) are fixed by giving every key a title in send-push/notification-copy.ts and a reverse sync test.

3.5 Races and conflicts ​

Rule 3 resolves all of them: one lock per match, the first commit wins, a later write gets a typed refusal and the sheet re-renders.

#RaceWhat the server doesWhat each person seesFrame
R1Both teams submit at once, the same sets (6-4 in one order, 4-6 in the other)The first write is the claim (S3). The second is an opposing entry of the same result: it counts as that player's explicit 동의, so S3 to S4 (ELO once, bonus).The second submitter: a Toast "상대 팀도 같은 점수를 입력해서 확정됐어요". The first: the usual N7.se-conflict ✗
R2Both teams submit at once, different setsThe second entry becomes a decline with its sets as the counter: S3 to S5c.The second submitter's sheet stays open with a Notice "방금 상대 팀이 먼저 입력했어요" and their digits kept as 내가 아는 점수, one button 보내기. The first gets N8.se-conflict ✗
R3The submitter's partner enters tooSame sets: no-op ("이미 입력됐어요", shows the claim). Different sets: the partner's counter (T6, resolved by T9, a new claim, never a confirmation).The partner sees the claim and a Notice; the submitter sees the counter row naming the partner.rq-counter-sheet ✗
R4The submitter edits while an opponent is confirmingThe confirm carries claim_version. Edit first: the confirm gets claim_changed. Confirm first: the edit gets already_settled.An inline Notice in the Confirm sheet "점수가 바뀌었어요. 다시 확인해 주세요", the new claim shown, both buttons enabled again. The submitter, if late: "상대가 이미 동의했어요" and a path to 수정 요청 (T26).rq-stale ✗
R5Two opponents answer at once (one 동의, one 거절)Lock order decides. Confirm first: the decline gets already_settled. Decline first: the confirm gets not_pending.The loser's sheet closes into the new state with "상대 팀 다른 분이 방금 동의했어요" or "방금 이 점수에 이견이 접수됐어요".rq-stale family ✗
R6The submitter withdraws while an opponent confirmsFirst commit wins; a late confirm on a withdrawn claim gets not_pending."점수 입력이 취소됐어요" over the closed sheet.rq-stale family ✗
R7The host steps in while a player answers바로 확정 or 대신 입력 against a player's confirm: first wins; if the host confirmed the claim the late confirm is harmless (same sets). A host entering different sets over a pending claim is possible only from the row's official menu; it closes the claim in the log and sends N11 to the submitter.The late actor gets already_settled and the Block of the new state.covered by T16 to T18
R8The host plays in the matchThe host acts as a player there: the board shows 점수 입력, never 대신 입력; 바로 확정 and 모두 확정 skip the match (the Dialog says how many); the Decide sheet refuses it (not_allowed). A dispute in the host's own match goes to another official; with none, T14 applies."내가 뛴 경기는 다른 운영진이 정해요" as the row's line.mb-own-match ✗
R9Offline submit or confirmThe client queues the write with its idempotency key; on replay the server runs R1, R2 or R3 or refuses with window_closed. A refused or conflicting queued entry becomes the R2 Notice, never a silent drop.A Toast "연결되면 보낼게요"; the row shows the Tag at .6 and "보내는 중". An offline 동의: "연결되면 동의가 전해져요".se-queued ✗
R10The host acts early on S5c while the submitter takes the counterAllowed (a host or official who does not play may decide at any time). The late 동의 gets already_settled.The submitter's counter row leaves; the Block shows the new state.covered by T11 to T13
R11A contest while the host re-enters the scoreRe-entering inside the window closes the contest as handled.N14 goes to the contester.covered by T21, T23
R12A timer fires while a person actsThe 12 h and 24 h jobs re-read the call under the lock and skip if it is no longer S3; a push queued before the call resolved is withdrawn (signals already resolve on a deleted call, 00240:91-105).Nothing.none
R13The submitter's partner tries to confirm (a stale screen, a deep link)The server refuses with not_allowed; the call stays pending.A Notice in the sheet "같은 팀은 동의할 수 없어요. 상대 팀이 확인해야 해요"; the UI never draws 동의 for the partner, so this is only a stale or forged call.rq-partner-sheet ✗

Not covered, flagged: a player replaced or removed after a claim (the pending request must re-address to the current players; the claim stays) and a player with no account (a host-added name). Neither was found in the code read; see §8.

3.6 Who may do what ​

ViewerS2S3S5cS5S4S4hS4cS6
The submitter점수 입력수정, 요청 취소동의 (an opponent's counter), 거절, 요청 취소read, 요청 취소수정 요청이의 있어요readread
The submitter's partner점수 입력거절 onlyreadread수정 요청이의 있어요readread
An opposing player점수 입력동의, 거절read (the decliner waits)read수정 요청이의 있어요readread
Host or official, not playing대신 입력, 기록 제외바로 확정 (from 24 h), 모두 확정, the row menu (점수 고치기, 기록 제외)read, early decision from the menu확정하기수정 (proposes a correction)re-enter inside the windowthe reviewer (the setter only when no other official exists)다시 열기
Host who playsas a playeras a playeras a playernot allowed (another official, else T14)as a playeras a playeras a playernot allowed
Another official (reviewer)as aboveas aboveas aboveas aboveas aboveread유지, 수정, 기록 제외다시 열기
Anyone else점수 입력 전 (text)readreadreadreadreadreadread

3.7 Coverage: what the boards draw, and what is missing ​

The matrix shows, per state, which surface and which viewer has a frame. Library-board stages have no id; they are named by section and caption. ✗ cells name the proposed frame.

StateBlock (scorecard)Match rowRequest row and sheetsHost surfacesNotice and push
S0, S1court name sc-ideal; playing rows sc-spectator, sc-draws, mb-ideal; overtime ✗ sc-live-overtime✓n/a✓sd-live
S2점수 입력 sc-entry-player✓ player, host, spectatorScoreEntry ✓ (10 frames); host plays ✗ mb-own-match; expired ✗ mr-s2-expired✓ mb-ideal✓ push; Notice ✗ sd-notice-enter (the rulings name the 일정 상세 Notice as a door for 점수 입력)
S3opponent sc-live-confirm ✓; submitter sc-waiting ✓; partner ✗ sc-live-partner✓request row ✓; Confirm sheet ✓; submitter's sheet ✗ rq-submitter-sheet; partner's sheet ✗ rq-partner-sheet; stale ✗ rq-stale; edit: ScoreEntry prefilled (se-saving shows the filled sheet)escalated mb-escalated ✓, Dialog ✓sd-notice-confirm ✓
S5c✗ sc-live-counter (submitter); decliner: sc-live-disputed ✗ with its own line✗ mr-counteredcounter row ✓ (lib only); page ✗ sc-counter; sheet ✗ rq-counter-sheet; conflict ✗ se-conflictnone by design (host not told yet)the 확인할 점수 Notice counts it; no new frame
S5players: ✗ sc-live-disputed (lib-surfaces ✓)✓ host, playerDispute sheet ✓ (5 frames)mb-dispute ✓; Decide ✓ (5 frames); no counter ✗ dc-nocounter; N > 1 ✗ mb-dispute-nsd-notice-host ✓
S4Block (lib-surfaces ✓)✓correction entry ✗ cr-ideal✓elo_changed
S4hsc-hostset ✓✓contest ✗ ct-ideal; Toast ✗ tt-answersmb-o-toast ✓Notice ✓
S4c✗ mr-contested, line on the Block✗ mr-contestedcontest sheet ✗ ct-idealreviewer ✗ dc-reviewthe host Notice gains a fact "이의 1건"; no new frame
S6lib-surfaces: not drawn; ✗ sc-void✓ (one row for every viewer)n/aexclude from S2 ✗ mb-o-exclude; reopen ✗ mb-o-reopenpush only
overlay R✗✗ rq-correction✗ cr-ideal✗✗

Missing frames, 27 in all, 15 of them P0 (P0 = a loop the owner asked about, or a role with no screen). Ids follow the boards' conventions (rq-, se-, dc-, dp- on the library board, sc-, mb- on the detail board); each caption is one line as the review format asks.

PIdBoardCaptionCovers
P0rq-submitter-sheetlib-social, phoneThe submitter reads the claim and ends in 요청 취소 · 수정T3, T4
P0rq-counter-sheetlib-social, phoneThe counter as a claim: 동의 takes it, 거절 leaves it to the host; the author is namedT8, T9, T10, R3
P0rq-stalelib-social, phoneThe claim changed under the open sheet: a Notice and the new claimR4 to R6
P0se-conflictlib-social, phoneSomeone entered first: the typed sets stay as 내가 아는 점수R1, R2
P0mr-counteredlib-social, stageS5c rows: the submitter's 동의 · 거절, the decliner waits, the host's read-only rowT6, T10
P0mr-contestedlib-social, stageS4c: 호스트 확정 and the line 이의가 접수됐어요T21
P0ct-ideallib-social, phone이의 있어요: the Dispute sheet in contest mode, the reviewer namedT21
P0dc-reviewlib-social, phoneThe reviewer's Decide sheet: the host's score, the contester's, 유지 · 수정 · 기록 제외T22 to T24, T28
P0dc-nocounterlib-social, phoneA dispute with no counter: the claim, 직접 입력, 기록 제외T7
P0sc-counterdetail결과 tab for the submitter: the counter row, 동의 takes itT6, T8
P0sc-live-counterdetailThe submitter's Block after a counter: 점수 확인, Tag 분쟁T6, T10
P0sc-live-partnerdetailThe partner's Block: 확인 대기, the door opens a read-only claim with 거절T2, T6
P0sc-live-disputeddetailPlayers' Block in a dispute: 분쟁, with 호스트가 확인하고 있어요 or the decliner's 님의 답을 기다려요T6, T7, T10
P0mb-own-matchdetailThe host plays here: 점수 입력, never 대신 입력 or 바로 확정R8
P0sd-notice-enterdetailThe 일정 상세 Notice for S2: 확인할 점수 becomes 입력할 점수가 N개 있어요 and opens the scorecardT1, T2
P1rq-partner-sheetlib-social, phoneThe partner reads the claim; the body has one Cell 점수가 달라요 and no footer buttonT6
P1sc-live-overtimedetailA playing match past its end offers 점수 입력T2
P1se-winnerlib-social, phoneQuick picks in two rows by who won (§4.5)DV9
P1se-queuedlib-social, phoneOffline: 보내는 중 and 연결되면 보낼게요R9
P1cr-ideallib-social, phone수정 요청: ScoreEntry prefilled for a correctionT26
P1rq-correctionlib-social, stageThe request row for a correctionT26 to T28
P1sc-voiddetailS6 for players: 기록 제외 and whyT13, T14
P1mr-s2-expiredlib-social, stageS2 after the entry window: no button for players; 대신 입력 and 기록 제외 stay with the hostT20
P1mb-o-excludedetailExclude a match that was not played, with 되돌리기T19
P1mb-o-reopendetail다시 열기 on a 기록 제외 rowT29
P1mb-dispute-ndetailThe 분쟁 Task Cell with N > 1 opens a list, then the sheetFlow 7 count
P1tt-answerslib-social, stageThe result Toasts: 동의했어요, 거절했어요, 요청을 취소했어요, 이의를 전했어요, 3경기를 확정했어요T4 to T6, T17, T21

The Match state block section on flat-v2-lib-surfaces also gains four stages (S5c, S4c, S6 and a confirmed result with the winner emphasised); they restate the page frames at library level and are not counted above.

3.8 Flow 7 ​

Do the marks describe real paths? Yes: the verifier finds every mark, and each step is a path a person can take. Seven checks:

Flow 7 stepMarkReal?Caveat
sc-entry-player점수 입력 (the Block's eyebrow)yes: the Block is the one buttonnone
sc-o-entry-player6-4, then 확인 요청yes for a winA quick pick credits the top team first and "a second tap swaps the teams" (library §10); a loss costs a fourth tap on a gesture nothing on screen announces.
sc-waitingnone (Done)yesnone
sd-notice-confirm확인하기yes: the Notice opens the 결과 tabThe Notice says 2 matches; sc-results shows 1 request.
sc-results동의yesThe push lands on this row (1 tap): the library says players land on the scorecard "at that match" but not on which tab. Decide: the 결과 tab, request on top.
mb-dispute분쟁 (Task Cell)yes for N = 1With N > 1 the Cell must open a list first (mb-dispute-n ✗): the count of 3 holds only for one dispute.
mb-o-decide박지훈이 말한 6-3, 확정하기yesAssumes a counter exists (dc-nocounter ✗).

Taps, rechecked. Win: 점수 입력, a pick, 확인 요청 = 3. Loss with today's swap: 4. With two rows of picks by who won (§4.5): 3 for both. One opponent: 2 from the session, 1 from the push. A disagreement costs the decliner 3 taps and one entry (거절, 점수가 달라요, the counter's digits, 보내기), the submitter 1 from the push (2 from the session), the host 3 (분쟁, a version, 확정하기).

Paths to add (each with the phones that exist plus the new ones):

PathStepsTaps
The other team disagrees (the counter loop)sc-results (거절) to sc-o-dispute (점수가 달라요, digits, 보내기) to sc-counter ✗ (the submitter: 동의) to the Block S4decliner 3 and 1 entry; submitter 1 from the push, 2 from the session
Nobody answersmb-escalated (모두 확정) to mb-o-confirm-all (모두 확정)host 2
The host set it and I disagreesc-hostset (이의 있어요) to ct-ideal ✗ (a reason, 이의 보내기) to the Toast3
A dispute with no countermb-dispute (분쟁) to dc-nocounter ✗ (the claim or 직접 입력, 확정하기)host 3

3.9 Decisions for the owner ​

  1. The host hears of a counter late (recommended). On S5c only the submitter is told; the host gets the task when the submitter declines the counter or 24 h pass. The alternative (tell the host at once) fills the host's list with disputes that settle themselves in one tap and invites a race between the host's decision and the submitter's 동의.
  2. A partner's counter is a new claim (recommended). The submitter taking their own partner's counter produces a score no opponent has agreed to, so it must return to S3. Taking an opponent's counter confirms at once.
  3. Contest resolution. Recommended: another official (not playing, not the setter) decides; if the host is the only official the host may uphold or amend and nothing escalates, the contest is logged, and at 7 days the result stands. The alternative (unresolved contests void the match at 7 days, like disputes) keeps "unresolved means unrated" but lets any loser void a result by contesting when no one else can answer. A host who is also the club's owner has no one above: that is accepted.
  4. Winner emphasis only on accepted results. A pending claim shows both teams equal, so a proposal never looks like a result (§4.3).
  5. Rating applied after the undo window (recommended), so 되돌리기 on a host-set score never replays ratings.
  6. Correction consent. The shipped correction asks all four players (agree_to_correction). Recommended: the same rule as the original, one opposing player, with the host deciding on a decline; the shipped rule is heavier than the rule that created the result.
  7. Outcomes with no UI. The shipped host outcomes abandoned, incomplete and walkover (00682) have no control in the rebuilt flow; a match stopped at 3-2 by rain or injury has only 기록 제외 (T19). Say whether a retirement should count.
  8. Quick picks by who won (§4.5): two rows, our games first, labelled 우리 팀이 이겼어요 and 상대 팀이 이겼어요. Cost: the open sheet grows from 478 to about 626 (74% of 844, under the 88% cap); the keypad state is unchanged (the picks fold away).
  9. The partner's sheet has no footer button (recommended). The partner can only decline; a lone tonal 거절 would sit outside the Button rule (a tonal button only in a pair), so the sheet carries the claim and one Cell 점수가 달라요 › that opens the Dispute sheet.
  10. Viewer-first order in rows (recommended). Library §10 keeps slot order for everyone in rows and puts the viewer's team first only in ScoreEntry, the Block and the dispute table. With scores on their own lines the order no longer changes meaning, so the viewer's own match can be recognised by "(나)" on the first line everywhere; the change reverses that sentence of §10.

4. Point 3: the Match row, and how teams and teammates are told apart ​

Question. "대" between teams is wrong, and teams and teammates must never be confusable. How do scoreboards and draw apps tell teams from teammates, and what one anatomy should every surface that shows two teams share?

4.1 What is wrong now ​

The kit draws "a team" seven different ways (C, P):

SurfaceHow a team is drawn today
Match row (FvkMatchRow, fvkTeamName)One line shared by both teams: names joined by a comma, teams by 대 (English: vs), 15/22·600; 190.4 wide in Korean; English wraps with "vs" stranded at the end of line 1 (§2.2 DV3).
Match state block (FvkCBlockMatch)One team per line, partners with the 1px break, numerals 24/32·700 ball for both teams.
ScoreTable (FvkScoreTable)One team per row, partners stacked one name per line (22 pitch, 12 more between teams), 48 × 48 cells.
Confirm sheet (fvkCtlBoardRow)A PersonRow: group avatar 40, names with the break, numeral 24/32·700 ink.
Request row (FvkResultRequestRow)The score as "6-4" in line 1 and the winners as a fact with a comma ("김민준, 이서연 승").
ResultRow (FvkResultRow)The opponents only, with the break: the viewer's own team and partner are absent.
Tournament pick (DtlTnMatch)Two fill Choices whose labels join partners with a comma.

The shipped app already knows better and the library moved away from it: MatchScoreboard is "ATP-broadcast-style": one row per team, a doubles team's two players on that team's row, the scores in fixed columns on the right, winner 700 against 500, and its header comment says it replaced a horizontal "name ··· vs ··· name" mirror "the way ATP shows competitors" (C: packages/app/src/presentation/components/sessions/match-scoreboard.tsx:1-14, 371-500). The shipped Korean copy writes vs ${names} and ${me} & ${partner} (C: ko/home.ts:102-103 and en/home.ts:103; match-history-display.ts:14), never 대.

4.2 Peer evidence ​

Read 2026-10-11. Layout, how partners sit together, what separates the teams, and how a winner is shown.

ProductLayoutPartnersBetween teamsWinnerScores
Flashscore, web (W, 390 CSS px, tennis list)One block per team, stacked. Singles: two 20-pitch lines, match pitch 51. Doubles: match pitch 83.Stacked, one name per line, pitch 1622 pitch (6 more, 1.375 times the partner pitch); no wordNames weight 800 against 400; both scores 700One score per team, centred on the team's names block, right edge 39 from the screen edge; 13/16·700
ATP WTA Live (F, store frame 3)One player per line, flag, name, seed; header line (round, stadium, duration) aboven/a (singles shown)the lineA green check left of the winner's set scores; the loser's scores dim greySet columns at the right, tiebreak superscript; pitch about 5.1% of the screen width (20 at 390)
Naver Sports, mobile web (W; five schedule pages, 24 games: volleyball on three days, baseball and football on one each)Header (time 15/17·700, league 15/17·400 grey, 10px 종료 or 예정 tag), then two team lines with a crest at the left, pitch 37, names 15/20·500n/athe line; no "VS", "vs" or "대" on any of the 24 rows, finished or upcoming (a text search of each page)The loser's score at α0.5 (both 17/21·700) and a small ◂ at the winner'sA score column at the right, buttons 영상 and 기록 at the far edge
SwingVision (F, store frame 5)Two sides stacked; avatar, then the namesStacked, one per linethe sideCheck and bright numerals against greySet columns at the right
MyTennis (F)One team per line in a green card; −/+ steppers beside a big scoreComma, wrapping a pair across three or four linesthe card edgeA crownone big score per team
BKPLAY, the badminton federation's app (F)Horizontal: team, "24 : 25", team; the region above the namesSlash ("박윤상 / 박윤석")the centred scoreThe winner's names and score in blueOne score pair in the middle, colon
배프, badminton friends (F)Horizontal: two stacked player chips, "10 : 21", two chipsStacked chipsthe centred score; "VS" only inside the results grid ("20 VS 21")The winner's score in yellowCentre
쉬운 대진표 (F)Team rows in two colours (blue, red), a +1 포인트 button each; "VS" in headers ("그린팀 VS 오렌지팀"); court cards "name | 1-0 | name"single namesteam colourcolourbeside each team
Playtomic (F, store frame 4)Teams as pairs of avatars with first names underAdjacent, no linea vertical hairline between the pairsn/a (open match)n/a
In-repo MatchScoreboard (C)One row per team; scores in 16 + 10 fixed columnsA doubles team's players stacked on its rowthe rowName 700 against 500, avatar tone, score weightCentred on the team block; the loser's tiebreak as a 9px superscript

Not reachable: Sofascore (the web page returns an empty shell to headless Chrome, 24 characters of text; its store frames are football only), 스포넷, 땡큐볼 and 베이스라인 (their frames show no match rows).

What the evidence says.

  • Teams are lines, or face each other across the score. Seven of ten put each team on its own line or block, two put the score between the teams, and Playtomic puts the pairs side by side. None writes a separator word between teams in a list row: "VS" appears in headers and a results grid, "대" nowhere.
  • Teammates are told apart from teams by structure, not by punctuation: stacked (Flashscore, SwingVision, 배프, MatchScoreboard), adjacent faces (Playtomic), or a slash or comma where one line is unavoidable (BKPLAY, MyTennis). The one that failed on a phone is the comma: MyTennis wraps a pair across three or four lines.
  • The gap between teams is larger than the gap between partners (Flashscore 22 against 16).
  • Winner emphasis is weight or a dimmed loser (Flashscore 800/400, Naver α0.5, ATP grey); glyph markers (a check, a crown, ◂) are the minority and would read as "confirmed" here.
  • Scores always sit on their own team's line, in tabular columns: no peer asks the reader to decode an order.

4.3 Recommendation: one TeamLines primitive ​

Job. Two teams, each with its players and its set scores, in every place a match is shown: the Match row, the Match state block, the Confirm and Decide sheets, the ScoreEntry and dispute tables, the request row and ResultRow.

Anatomy.

PartRule
Team lineOne team per line, two lines 8 apart (the Block's existing gap). Min-height 22 (24 when it carries 17/24 numerals; 32 in the Block; 48 in a sheet row).
NamesEach name is its own node, 15/22·600, joined by the 1px break (12 tall, 8 each side, $edge; on ink α0.4), the owner's review-2 ruling kept. Partners share a line, so the break is the only partner separator and teams are separated by the line itself. A no-break space keeps "Jihun Park" whole; a break never starts or ends a visual line; names wrap whole and are never cut.
ViewerThe viewer's team is first wherever the viewer plays (rows included), and the viewer's name takes the suffix " (나)" ("(You)"), the same suffix as the roster rows (C: flat-v2-lib-rows.jsx:604). Everyone else sees slot order.
Set columns32 wide each, right-aligned at the rail, tabular 700, centred on their line (on the team block when names wrap): 17/24 in rows, 24/32 in the Block, sheets and cells. The loser's tiebreak points are a 12/18·600 superscript in $textSecondary with the slot reserved per match, so 7-6(4) is finally drawn (§2.2 DV20).
WinnerOnly on accepted results (S4, S4h, S4c) and in history: winner names 600 $text, loser names 400 $textSecondary; winner numerals 700 $text, loser numerals 700 $textSecondary (7.38:1 on white). On the Block: the winner's numeral ball, the loser's white α0.7 (about 9:1 on #15181A), names white against white α0.7. Never a hue, never a glyph.
Pending, countered, disputed, voidBoth lines equal: a proposal never looks like a result.
Separator wordNone.

Contexts.

ContextNamesNumeralsLineUsed by
row15/2217/24·70024Match row, request row, ResultRow
block (on ink)15/22 α0.85, winner α124/32·70032Match state block zone B
sheet15/22 beside a 40 face pair24/32·70048, lines 12 apartConfirm sheet, Decide sheet's versions
cells15/2224/32·700 in 48 × 48 FvkInput cells, 8 apart, header labels 13/18·50048ScoreEntry, Dispute counter, Decide 직접 입력

States. Equal · accepted (winner emphasis) · no numerals (names only, S2, S5, S6, upcoming) · with a tiebreak · long names (wrap) · 200%: numerals cap at 1.5 (32 columns hold), names wrap to three lines.

Content. In rows no word between the teams. In running text a matchup uses " vs " between teams and " & " between partners ("김민준 & 이서연 vs 박지훈 & 최유나"), as the shipped copy already does. "대" stays only where a score is read aloud ("6 대 4"). One spelling of vs, lower-case, in both languages; the capital VS in matchup.vs (C: ko/home.ts:189) joins it. The terminology guide gets one line.

Accessibility. One text group. Spoken, from the viewer's side: "내 팀 김민준, 이서연 6. 상대 팀 박지훈, 최유나 4." (a full stop between teams, commas between partners; sets as "1세트 6 대 4, 2세트 …"); a spectator hears "김민준, 이서연 6. 박지훈, 최유나 4."; English "Your team Minjun Kim, Seoyeon Lee 6. Opponents Jihun Park, Yuna Choi 4." A winner is spoken only on accepted results ("승", "won"). One builder, teamLinesLabel(model), sits beside the component and feeds every surface's accessible name.

Replaces in code. MatchScoreboard (packages/app/src/presentation/components/sessions/match-scoreboard.tsx: its TeamRow and ScoreCell are the starting point), PLAYER_JOINER = ' & ' (match-history-display.ts:14), recordDetail.myTeamLabel and opponentTeamLabel, LiveMatchRow, CompletedRow, MatchHistoryRow, and joinNames() as the one place names are joined.

4.4 The Match row on top of it ​

Anatomy (header first). The header carries the court and, only when it adds something, one fact, and ends in the one trailing element: a Tag (24), an sm Button (36) or secondary text. Under it the two team lines. Header-first is the ATP and Naver card, and it is the order the Block already uses (court or action, then the teams); it also puts the Tag and the Button on one line, which fixes DV2 by construction. The Block keeps its Tag in zone C (its own grammar); rows put it in the header, as the peer cards do. The alternative, teams first and the court and Tag on a last line, has the same heights; it loses because the court is how a player finds the match.

1번 코트 | 김민준 님이 입력했어요                      [확인 대기]   header: 13/18·500 secondary, the trailing element at the right
김민준 (나) | 이서연                                  6      4   team line 1
박지훈 | 최유나                                        4      6   team line 2
RowTodayProposedArithmetic
Tag, no numerals (S5, S6, upcoming)6810412 + 24 + 4 + 22 + 8 + 22 + 12
Tag and numerals (S3, S4, S4h)6810812 + 24 + 4 + 24 + 8 + 24 + 12
Button (S2, escalated)6811612 + 36 + 4 + 22 + 8 + 22 + 12
Text trailing (S2, spectator)689812 + 18 + 4 + 22 + 8 + 22 + 12
Request row (Match row plus AnswerPair)11214612 + 18 + 4 + 56 + 8 + 36 + 12
ResultRow (header = the date, Tag 승 or 패)68108as S4
Confirm sheet's two team rows13610848 + 12 + 48

(Measured on a scratch mock at 390, in the boards' own font: 104, 108, 116 and 146; 98 and the ResultRow follow by arithmetic; the Block stays 170 with a claim. The fit holds: three sets leave 242 beside 96 of columns, and "Minjun Kim (You) | Seoyeon Lee" is 223 wide. A longer pair wraps whole rather than cut.) Cost: list rows grow by 44 to 71%; the host's board shows one round at a time (2 to 4 matches), the scorecard's 결과 tab all rounds (32 matches: 3,456 against 2,176). Two things pay some of it back: the header facts that only repeated the Tag go ("상대가 동의했어요" and "호스트가 확정했어요" stay in the Block's Caption row and the spoken name; review 2 point 7: line 2 never repeats a fact the row shows), and a team line is a score line, so the "winners" fact and the L1 "6-4" go.

Facts kept. S2 host "선수가 입력하는 중이에요"; S3 "김민준 님이 입력했어요" ("내가 입력했어요" to the submitter); S5c "상대가 다른 점수를 말했어요" (submitter) and "{이름} 님의 답을 기다려요" (decliner); S5 "호스트가 확인하고 있어요" (players) and "두 팀의 점수가 달라요" (host); S4c "이의가 접수됐어요"; S6 "레이팅에 반영되지 않아요".

Request row is this row with the AnswerPair under the team lines: header "김민준 님이 입력했어요 | 3라운드 2번 코트", the claim as team lines from the viewer's side with the numerals on their lines, then 동의 · 거절 (sm 36, 8 under the lines). The leading avatar goes (the names carry identity), so the row's decision content is the score, not a face.

Escalated rows. The same row with the Button in the header; the numerals stay on their teams (DV8).

At 200% (mock, text capped as the library caps it): the S3, S4 and S2 rows grow to 196, 212 and 268; names wrap whole, the 32 columns hold, numerals cap at 1.5. From 1.5× the trailing Tag or Button drops under the team lines at the text edge, as in every other row (library §2.4); the facts wrap whole.

Block. FvkCBlockMatch zone B becomes FvkTeamLines tone="onBlock"; nothing else moves.

4.5 ScoreEntry: say who won, do not hide it in a second tap ​

The library says a quick pick "credits the top team first; a second tap swaps the teams". Nothing on screen announces the swap, and a loss costs a fourth tap. Recommend two rows of picks, our games first, labelled by outcome: 우리 팀이 이겼어요 (6-0 to 7-6) and 상대 팀이 이겼어요 (0-6 to 6-7), with the table above them already showing the viewer's team first and "(나)". Three taps for a win or a loss, no hidden gesture, and the picks are unambiguous because the first number is always the top line's. For the host's 대신 입력 (no "us") the labels are 위 팀이 이겼어요 / 아래 팀이 이겼어요. Cost: 148 more points when open (478 to about 626, 74% of 844, under the 88% cap); the keypad state is unchanged because the picks fold away. Frame: se-winner ✗ (§3.7).

4.6 What changes in the kit and the library ​

Kit: new FvkTeamLines and FvkNames (the partner join), constants FVK_SET_COL = 32; FvkMatchRow and FvkResultRequestRow become one row; FvkResultRow composes it; FvkCBlockMatch composes FvkTeamLines; FvkScoreTable is FvkTeamLines context="cells"; fvkCtlBoardRow, fvkTeamName and the board's cluster spans are deleted. Library: §4 Match state block (zone B, emphasis, "(나)", spoken label without 대), §5 a new Team lines entry and ResultRow (the viewer's team and partner appear), §10 Match row states (anatomy, heights), Result request row (anatomy, no avatar), ScoreEntry (picks by who won), Dispute and Decide (TeamLines for the claim and each version), the terminology guide (vs, &, 대 only for scores). The full ordered list is §7.

5. Point 4: is the library in line with industry practice? Controls and fields (§7), conversation (§9), scoring (§10) ​

Question. Audit every component in §7 and §9 (and the scoring pieces they sit beside) against current practice: spec, reference, drift (none, minor, major), fix.

5.1 Method and references ​

For each component: (1) the library value, (2) the same value measured on the built boards (P: a census of every data-g part on flat-v2-lib-controls, 174 frames, and flat-v2-lib-social, 89), (3) a reference. References read this session: Material 3 component tokens (material-web tokens/versions/latest/sass, the M3 Expressive set, D), Seed Design component specs (daangn/seed-design packages/rootage/components/*.yaml and the docs, D), Apple HIG (the JSON behind the buttons, toggles, steppers, sliders, text fields, search fields, pickers, sheets and Sign in with Apple pages, D), App Review Guidelines 4.8 (D), WCAG 2.2 2.5.8, 2.5.7, 1.4.11 (D). Toss TDS publishes sizes by name without pixels ("small, medium, large, xlarge", D) and its consumer guide asks for "Close" rather than "Cancel" in a dialog, which our Dialog rule already does (D); KRDS pages and the field and notice research are lane B's, review 1 (cited as "lane B"). The measured spec against the kit passes everywhere in §7 and §9: button 36 / 44 / 52, IconButton 48, Choice 36 and 44, Switch track 52 × 32 (the data-g box is 52 × 24 and the track draws 32), Stepper 136 × 36 with 44 ends, Input 48, TextArea 120 and 200, Field 70 and 92, Picker Cell 48 and 70, check rows 48 and 68, checkbox 24, ProviderButton 52, Composer field 40 and send 36, bubble 40, reaction circles 44, chips 28.

5.2 The audit ​

Drift: none, minor, major. "Lane B" means the reference was read in review 1 and the value was re-measured here.

ComponentSpec nowBest-practice referenceDriftFix
Buttonsm 36 r10, md 44 r12, lg 52 r12; ball, tonal, ghost, danger; soft-disabled; loading keeps its labelSeed ActionButton 32 / 36 / 40 / 52 (D); M3 small 40, medium 56 (D); HIG hit region 44 and "use style, not size" to mark the preferred choice (D); Seed "one brand or neutral solid per screen" (D)minor: the pair resolves to 1 : 1.64 (DV14); HIG would make both buttons equalSay two thirds and one third and set the widths; keep the larger commit (ratified, thumb side), logged as a departure
IconButton48 target, 24 or 20 glyphHIG 44 (D), WCAG 2.5.8 24 (D), M3 48nonenone
Choice36 (44 in forms) r full; sets of at most 5, wrappingM3 filter chip 32, 48 target (D); Seed chip 32 / 36 / 40 (D); HIG "no more than about five" radios (D)nonenone
Switch52 × 32, 24 thumb, off track 3:1M3 track 52 × 32, handle 16 off, 24 on, 28 pressed (D); Seed switchmark 52 × 32 (D); HIG: a switch only in a list row (D)nonenone
Sliderink thumb 24, track 4, live tag, Stepper alternativeWCAG 2.5.7 (D); HIG "supplement with a text field and stepper" (D); M3 handle 44 tall (D)nonenone
Stepper136 × 36 pill, 44 ends, value insideSeed quantity-picker 36 / 40 / 52 with the value inside (D); HIG keeps the value beside a stepper (D)nonenone
Search field40 in a 44 target, r12 gray100, 취소HIG placeholder names the scope, clear button, search as you type (D); M3 search bar 56 (D)nonenone
Checkbox and radio rows24 square, 2px edge; a single choice shows only a trailing check on the chosen rowM3 checkbox 18 with a 40 layer, radio 20 with a 40 layer (D); Seed checkmark and radiomark 20 or 24 (D); HIG a radio is "a small, circular button followed by a label" (D); WCAG 1.4.11 3:1 for "visual information required to identify user interface components and states" (D)major where a footer CTA follows the choice (Dispute, Decide; DV6): the unchosen state is drawn nowhereFvkCheckRow kind="radio" lead (a 24 ring, 2px $textSecondary, 12 ink dot) in the checkbox slot, content at 56; the trailing check stays for tap-to-pick-and-close pickers (S-3)
Date and time picker"UIDatePicker wheel on iOS, the Material spinner on Android", in a Sheet with 완료HIG compact, inline or wheels, and "avoid switching views to show a picker" (D); M3 modal date picker is a calendar (360 × 524, 40 × 40 days) and the time picker a 256 dial (D); the app uses react-native-modal-datetime-picker ^18 over @react-native-community/datetimepicker 9.1 (C: apps/mobile/package.json:16,57); its Android file renders the community picker with no display prop, so Android shows the system Material calendar and clock dialogs, and its iOS file defaults to "spinner" (C: node_modules/react-native-modal-datetime-picker/src/DateTimePickerModal.android.js, .ios.js:178); no call site in the app passes displayminor (spec accuracy; read from the code, not run on a device)Say iOS draws the wheel in the Sheet and Android hands over to the system dialog; for a date more than a week out draw the Calendar (§4: a day cell is 50 wide at 390) in the Sheet on both platforms, since a wheel is the slowest way to reach a distant date
Time range wheelTabs 시작 and 종료 with one wheelM3 dial and keyboard input (D)nonenone
Fieldlabel 13/18·600 above, 48 control r12, 1px $borderControl, focus 2px ink, error 2px danger, support line 18Seed text-input: 1px, focus 2px, invalid 2px (and with focus), min height 52 (D); M3 1px, focus 3px (D); HIG label plus placeholder, validate on moving to another field (D); lane Bnone (48 sits between HIG and KRDS 44 and Seed 52; the borders match Seed)none
Inputexample placeholder, clear × when filled and focused, 16/24HIG clear button at the trailing end, leading glyph names the purpose (D)nonenone
Number fieldInput with a numeric keypad, the unit a 15/22 secondary suffix inside the box and in the accessible nameHIG number formatter and the appropriate keyboard (D); GOV.UK inputmode, never type=number (lane B)nonenone
TextAreamin 120, grows to 200 then scrolls; counter on the support line, always shownGOV.UK character count: soft limit, message below, tested with low-digital-skill users (lane B); Seed counter at the right of the footer (lane B)nonenone
Picker CellCell 48: label, value or "선택", chevron; the line under itHIG "prefer a list of options to text entry" (D); lane Bnonenone
Choice setlegend, Choices 44, at most 5 (7 for a fixed series)HIG radios "no more than about five", segmented controls "about five on iPhone" (D)nonenone
Field hintnone by default; four allowed cases; 13/18lane B (hints only where a rule would cause an error; KRDS inline text)nonenone
Input counterfrom 80% of the limit; typing past it is allowed and shown in $errorTextGOV.UK soft limit; Material overflow in the error colour (lane B)nonenone
Field error and validation2px border, 16 glyph, $errorText 13/18·600; validate on leaving, live once wrong, first error focusedHIG "validate … when people switch to another field" (D); Seed invalid 2px (D); KRDS focusout and first-error focus (lane B)nonenone
Field noteone line in the support line, else a neutral Notice; never an ⓘ that hides itKRDS: essential information inline, ⓘ only for non-essential help (lane B)nonenone
Field skeleton70 per field, one shimmer, shown after 300 msthe library's skeleton timing (§2.8, review 1)nonenone
Required and optional markerasterisk in $errorText hidden from assistive technology, "필수" in the name, "(선택)" after the labelSeed Field carries an indicator icon and text for required (D); lane Bnonenone
Map face in a form350 × 140 r12 static map 8 under the 장소 Cell; a tap reopens the searcha composition of the Cell and the takeover; no peer rulenonenone
Composer photo tiles4-up 81.5 tiles, an add tile with n / limit, 앞으로 and 뒤로 in a menuWCAG 2.5.7 (D): reordering has a non-drag pathnonenone
Cover gridtwo columns of 16:9 tiles; chosen = 2px ink ring and a 24 checkWCAG 1.4.11 and 1.4.1: the chosen state is a ring and a glyph, not colour alone (D)nonenone
Consent group전체 동의, [필수] and [선택], 보기 ›, nothing pre-tickedlane B (KRDS, PIPA)nonenone
ProviderButton (Kakao)the lg Button geometry, #FEE500, radius 12, label "카카오 로그인"; an email ghost Button beneathKakao's guide (lane B). App Review 4.8: an app that uses a third-party or social login for the primary account "must also offer as an equivalent option another login service" that limits data to name and email and lets the user keep the email private, unless it "exclusively uses your company's own account setup and sign-in systems" (D). HIG: a Sign in with Apple button "no smaller than other sign-in buttons", without scrolling (D)major for the iOS build: only Kakao and an email link are specified; the app's own email sign-in does not hide the emailAdd the Sign in with Apple button under Kakao at the same 52 × full width (system button, white outlined or black, radius 12 to match); the email stays the ghost md. The Kakao button stays the screen's one coloured action: the Apple button is black or white
PostRowauthor line 48, body 16/24 with a 4-line clamp, photos 350 × 262 / two 171 / +N, action row 48measured as specified (P: photo 350 × 262, squares 171 with gap 8, action row 48); review 1 lane Cnonenone
Commentavatar 32 at the rail, text at 64, replies one level (avatar 24, text 96), 24 between, 12 to repliesmeasured (P: avatars 32 and 24); one level of threading is the norm in the feeds studied in review 1nonenone
MessageBubblepad 8 12, r18, 16/24, 2 within a run, mine ink, theirs gray100review 1 lane A measured KakaoTalk, iMessage and Signal; measured as specified (P: one-line bubble 40)minor: in dark, mine #2A302B and theirs #232824 are 1.10 : 1 apart (P), so only the side tells them apartLift dark "mine" one step (about #3A413C, 1.5 : 1) or give theirs a 1px $edge
Composermin 56, field r20 min 40, send 36 in 48, attach 48measured as specified (P)nonenone
Reactionsquick row of six 44 circles, gap 16; summary chips 28Seed reaction button 32 (xsmall) and 36 (small) (D); M3 chip 32 (D); measured chips 28 × 44 to 54.5 (P)minor: 28 is under both references; wrapped chips 8 apart cannot reach a 44 targetChips 32; a wrapped row 12 apart
ScoreEntrythe sheet of §10HIG one sheet at a time, swipe to dismiss with a guard for unsaved changes (D); the library already follows bothmajor: the hidden swap (§4.5); custom cells (§6)picks by who won; cells from FvkInput
Result request rowPersonRow plus AnswerPairsee DV1, DV5major§4.4
AnswerPairtwo sm buttons in equal columns, 8 under line 2Button sm 36 (Seed small 36, D)major (DV1)§2.2
Dispute sheet× in the title; three radio Cells; the optional counter table under the first; helper; 보내기, soft until a reason is chosenHIG "display only one sheet at a time" (D), which the library's swap rule already honours; radio affordance as abovemajor (DV6, DV7, DV10): no unselected control, the claim missing, hairlineradio lead, sub and the claim as TeamLines, the hairline (§2.2)
Host decision sheet× in the title; four radio Cells (the claim, the counter, 직접 입력, 기록 제외); helper; 확정하기, soft until chosenas abovemajor (DV6, DV9, DV10): no unselected control, versions drawn as text ("6-4", "6-3") with no team, title row 50radio lead; each version as a TeamLines row; no-counter and review variants (§3.7)
Match row statesa Text row, 68: both teams on line 1 joined by 대, one trailing element (Tag, cluster, Button or text)the peer study (§4.2): none of ten writes a word between teams in a list rowmajor (DV2, DV3, DV8): shared line, Button nested and off-centre, score detached from its teamthe header-first row on FvkTeamLines (§4.4)
Match state blockBlock 144 (170 with a claim): the viewer's next action, two team rows, the Tag, a state linethe peer study (§4.2); library §4 Block grammarmajor (DV4, DV5): both numerals ball, no winner, viewer unmarkedFvkTeamLines tone="onBlock" (§4.3)
ResultRowPersonRow: the opponents, the date, Tag and scorepeers show both sides with the viewer's marked (§4.2)minor: the viewer's team and partner are absentthe same row as the Match row (§4.4)
Result policy7 keysthe library's own "never a literal" ruleminor (DV13)three keys

Counts: 41 components, 26 none, 6 minor, 9 major. (The six minors are Button, Date and time picker, MessageBubble, Reactions, ResultRow and Result policy; the nine majors are Checkbox and radio rows, ProviderButton, ScoreEntry, Result request row, AnswerPair, Dispute sheet, Host decision sheet, Match row states and Match state block.)

5.3 Findings that need a decision ​

  1. Radio affordance in decision sheets. The library chose a trailing check for single choice because that is the iOS Settings list. It works when a tap picks and closes. It fails when the choice waits for a CTA: nothing shows that the rows are choices. Recommended: the leading ring when a CTA follows. M3, Seed and HIG all draw the empty control.
  2. Sign in with Apple. If the iOS build ships with Kakao and the app's own email sign-in only, review 4.8 is a rejection risk; this belongs to the sign-in board's lane, flagged here because the ProviderButton entry is in §7.
  3. Equal-size pairs. HIG asks for equal size and style to mark the preferred option; the library ratified a larger commit button in review 1. Evidence for keeping it is thin (thumb side); evidence against it is one guideline, not a measured cost. Keep, and record.
  4. Dates by wheel. Fast for a time, slow for a distant date; the Calendar already exists in §4.

6. Custom implementations in the scoring pieces ​

The coordinator's rule: minimise custom implementations across all building blocks. Below is every place in flat-v2-kit-controls.jsx, flat-v2-kit-content.jsx and the detail board where a scoring piece redoes layout, positioning, typography, spacing or a control that a shared primitive (FvkT, FvkMeta, FvkCRow, FvkTextRow, FvkPersonRow, FvkSheet, FvkButton, FvkTag, FvkInput, FvkChoiceSet) already owns. Line numbers are in the 18:47 build (C).

#WhereWhat it redoesConsolidate onto
C1FvkScoreTable team rows, kit-controls.jsx:4073-4092A flex row per team with one FvkT block per name: the third way of drawing a pair, with its own 22 pitch and 8 gap.FvkTeamLines (cells context): partners on one line with the break
C2FvkScoreTable set cell, :4024-4058A 48 × 48 box with border: (on or bad ? 2 : 1) + 'px solid ' + (bad ? danger : on ? ink : control): the Input border logic, copied.fvkCtlFieldBox(state) shared with FvkInput; the cell is FvkInput size="cell"
C3FvkScoreTable set header labels, :4061-4072A hand-built row of 48-wide 13/18·500 labels.the FvkTeamLines column header
C4FvkScoreEntry quick picks, :4135-4149A data-g="quickpicks" flex-wrap with gap: 8, marginTop: 16 around FvkChoices.FvkChoiceSet (sm), which owns wrap and gap (and the legend the two rows need)
C5FvkScoreEntry tiebreak row, :4150-4179A 44 flex row with a custom 56 × 36 bordered box and a headline numeral.FvkField plus FvkInput size="compact", the same field box
C6FvkScoreEntry caption, :4214-4218margin: '-6px -rail 10px' to pull a Caption row out of the sheet padding (DV19).a description slot on FvkSheet, or bleed
C7fvkTeamName and FvkMatchRow line 1, :4240-4248, 4339A string join with commas and a no-break space; the same function is imported by the tournament board.FvkNames inside FvkTeamLines
C8Tag + score cluster, FvkMatchRow :4271-4280 and FvkResultRow kit-content.jsx:1602-1611The same span of FvkTag + 17/24·700 score written twice.the score moves onto its team line; the Tag is the header's trailing element
C9S2 Button in FvkMatchRow, :4293-4303Passed as trailing (inside the press area, centred on line 1) although control exists for exactly this and 바로 확정 uses it (DV2).control, and the FvkCRow rule
C10FvkResultRequestRow, :4361-4392A "winners" fact (fvkTeamName(winners) + ' 승') to repair the score's order; its own fvkScoreEulReul digit table.FvkTeamLines; the particle tables move to the shared formatter beside josaEulReul
C11fvkCtlBoardRow, :4395-4413A function that builds a FvkPersonRow with a group avatar, l1={names} and a hero numeral: the fourth team rendering.FvkTeamLines context="sheet"
C12FvkConfirmSheet body, :4441-4442The viewer-first order and the numerals 4 and 6 are written into the component.props from the claim model (deriveMatchResultState)
C13FvkDisputeSheet :4486-4495 and FvkDecideSheet :4543-4550The same expand-below-a-radio block: a div padding: '16px rail 16px' and a last={...} hairline hack, twice.FvkCheckRow expand slot, which owns the padding and the hairline
C14FvkCBlockMatch, kit-content.jsx:2878-2925A private row(t): min-height 32, FvkMeta names, 32-wide numeral columns, a spoken-label string with " 대 ".FvkTeamLines tone="onBlock" and teamLinesLabel
C15FvkAnswerPair, kit-content.jsx:1638-1668Equal cells that stretch the buttons to 44 (DV1).a natural 36 FvkButton sm, marginTop: 8
C16FvkSheetBox title block, kit-controls.jsx:2130padding: '4px 0' around title and sub, so the row is 50 (DV9).the 48 rule: padding: 3px 0
C17FvkCRow trailing slot, kit-content.jsx:1089-1103Every trailing element centred in a box l1H high, whatever its height.one rule: taller than 24 centres on the row (library §5)
C18Field border in FvkInput :1186, FvkTextArea :1266, the ScoreTable cell :4041, the tiebreak box :4168Four copies of the 1 / 2px border and the 15 / 16 padding compensation.fvkCtlFieldBox(state)
C19Caret in FvkInput :1207-1219, FvkTextArea :1281-1294, ScoreTable :4054Three hand-drawn 2px carets (22, 22, 28 tall).FvkCaret
C20Tournament winner pick, flat-v2-detail.jsx:2822-2834Two FvkChoice pills whose labels are fvkTeamName (commas).FvkNames

What the shared pieces own afterwards. FvkTeamLines owns names, break, columns, emphasis, order, the "(나)" mark, tiebreak superscripts, spacing and the spoken label, in four contexts. fvkCtlFieldBox owns the field border and padding. FvkCheckRow owns the radio lead and the expand slot. FvkCRow owns trailing alignment. FvkSheet owns the title row (48, sub, description). After that the scoring components are compositions: the Match row is FvkCRow + header facts + FvkTag or FvkButton + FvkTeamLines (+ FvkAnswerPair); the sheets are FvkSheet + FvkTeamLines + FvkCheckRow + FvkSheetFooter.

7. Fix list, ordered for implementation ​

Order: owner decisions first (§3.9, §4.5), then the shared pieces other things stand on, then the compositions, then boards, library and gate.

0. Owner decisions (recommendations first): the host hears of a counter late; a partner's counter is a new claim; contest resolution; winner emphasis only on accepted results; rating after the undo window; correction consent; outcomes with no UI; picks by who won; the radio lead variant; "vs" spelling; Sign in with Apple (the sign-in board).

1. flat-v2-kit.jsx (core)

  • FvkNames({ names, me, tone }): partners joined by the 1px break (FvkMeta rules), no-break spaces, "(나)" or "(You)" on me; replaces fvkTeamName.

2. flat-v2-kit-content.jsx

  1. FVK_SET_COL = 32; FvkTeamLines({ teams, sets, tiebreaks, result, viewer, context, cells }) with the four contexts of §4.3, the emphasis rule (only result accepted), the viewer-first order, the tiebreak superscript slot, and fvkTeamLinesLabel(model, locale).
  2. FvkCRow: trailing taller than 24 centres on the row; keep control.
  3. FvkAnswerPair: natural 36 buttons, marginTop: 8, fvkCtlHit(4).
  4. fvkMatchFact(state, viewer, locale): the one function that returns the header fact (S2 host, S3, S5c, S5, S4c, S6) so no board types a second string.
  5. FvkResultRow: the Match row with a date header and a 승 or 패 Tag, viewer's team first.
  6. FvkCBlockMatch: FvkTeamLines tone="onBlock"; the spoken label from fvkTeamLinesLabel; the "(나)" mark; zone C unchanged.

3. flat-v2-kit-controls.jsx

  1. fvkCtlFieldBox(state) and FvkCaret; FvkInput, FvkTextArea use them; FvkInput size="cell" and size="compact".
  2. FvkCheckRow: kind="radio" lead, the expand slot, the hairline after an expansion.
  3. FvkSheetBox: title row 48 with sub; a description slot for the Caption under the title.
  4. FvkScoreTable becomes FvkTeamLines context="cells"; FvkScoreEntry: two FvkChoiceSets of picks by who won, tiebreak through FvkField, caption through description.
  5. FvkMatchRow becomes the one row: header (court, fvkMatchFact, trailing Tag, Button or text), FvkTeamLines, optional answer. FvkResultRequestRow is FvkMatchRow answer. Delete fvkTeamName, fvkCtlBoardRow and the cluster spans.
  6. FvkConfirmSheet variants: opponent, submitter (요청 취소 · 수정), partner, counter, stale-claim Notice; FvkDisputeSheet: sub, the claim as FvkTeamLines, mode="contest", radio lead; FvkDecideSheet: each version as FvkTeamLines in a radio row, no-counter, mode="review", N > 1 list.
  7. FvkButtonPair: set the two-thirds and one-third widths.
  8. FvkBubble: dark "mine" one step lighter; FvkReactions: chips 32.
  9. fvkScoreEulReul moves out and gains the (으)로 table.

4. Boards

  • flat-v2-lib-social.jsx: 16 new frames (the lib-social rows of §3.7), the Match row, request row, ScoreEntry, Dispute and Decide specs rewritten with no 대; spec text matches the library.
  • flat-v2-lib-surfaces.jsx: Match state block frames for S5c, S4c, S6, a confirmed result with the winner emphasised, the "(나)" mark.
  • flat-v2-detail.jsx: the 11 detail frames of §3.7, Notice titles (DV12), sample data that agree, DtlTnMatch on FvkNames.
  • flat-v2-flows.jsx: the four Flow 7 paths of §3.8; the taps recounted.

5. Library, IA, terminology, decision log

  • Library: §4 Match state block; §5 new Team lines, ResultRow, AnswerPair; §7 Button (pair), Checkbox and radio rows, Date and time picker, ProviderButton; §8 Sheet; §9 MessageBubble, Reactions; §10 every entry (states S5c and S4c, the 29 transitions in short, policy keys, pushes N1 to N18, races, the guard matrix).
  • Terminology guide: "vs" between teams in running text, "&" between partners, "대" only for a score read aloud, one spelling.
  • IA flow 7 and the decision log: the added decisions, logged for veto.

6. The gate: gap, team, names and radio checks with canaries (§2.4).

7. Code, when built (not this review): TeamLines in @twomore/ui from MatchScoreboard; deriveMatchResultState extended with countered and contested; match_score_calls.status gains countered; the policy object gains three keys; signal.matchup.* titles for N1 to N18 with the reverse sync test; josaEulReul and josaEuroRo digit tables; a lint on name joins; pgTAP for R1 to R13, the partner counter and the contest resolver.

8. What I could not verify ​

  • Sofascore. The web page returns an empty shell to headless Chrome (24 characters); its store frames show football only. No tennis or doubles row was read.
  • Playtomic's result card. The store frames show the open-match lobby (pairs with a vertical hairline), not a finished result; the result screen is behind the app.
  • ATP WTA Live doubles. The store frame shows singles; how it stacks a doubles team was not seen.
  • Pretendard. It is not installed in this browser. The boards' own fallback gives the library's Hangul advance (0.864 em: "김민준" 38.9 at 15 px), so Korean widths agree with the library; Latin widths come from the same fallback ("Minjun Kim (You) | Seoyeon Lee" 223). Real Pretendard Latin may differ by a few points.
  • Android pickers and the Sheet. Read from the dependency's source and the call sites, not run on a device.
  • The one-chevron Block change was made by the main thread while I measured. My Block numbers are on the 18:47 build, which already contains it (zone A has no chevron; the lane is 24 wide on the right).
  • Players with no account (a host-added name) and a player replaced after a claim: not found in the migrations read (guests apply with accounts, 00481, 00482); if either exists, a claim for such a match must route to the host at once.
  • Backend feasibility of S5c, S4c, claim_version and the contest reviewer: designed against lane A's RPC map, not against the live functions.
  • KRDS and Toss TDS pixel values. KRDS pages are lane B's; TDS publishes names, not pixels.
  • Apple guideline 4.8 is cited as written; whether Kakao Login for a Korean-market app is reviewed as a "third-party or social login service" is App Review's call.

9. Sources ​

Read 2026-10-11.

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