Flat grammar v2 — migration plan (2026-10-09)
Status: Active · Date: 2026-10-09 · Build order: 2026-11-01 · Owner ruling: "Make sure we properly migrate to the new grammar so that all foundations are based on the new grammar. Design all of the components first (including all their interactive behaviours) so that we have shared components used throughout the app. Then design the layouts for all of the pages based on the new components library. Then we move on." Accepted inputs: the grammar (flat-mobile-grammar-v2-2026-10-09.md incl. §4b seams), scheme S2 Ball & Ink with S1 as backup (colour-scheme-proposals-2026-10-09.md), the colour ledger (colour-system-research-2026-10-09.md), the list-surfaces spec (list-surfaces-spec-2026-10-09.md). Board: /wireframes/flat-v2.
The rule of the migration
Nothing is adapted. Every foundation, component and page is re-derived from the grammar; the old grammar (cards on list surfaces, the segmented pill, the mint tints, the 21-glyph vocabulary, the tinted ground) is retired screen by screen, never mixed on one screen (the half-flattened state the canvas law already forbids). Each phase ends with a board the owner marks; code starts only after the component board is marked.
Phases
| Phase | Deliverable | Gate |
|---|---|---|
| A — Foundations | Tokens re-derived from the grammar and S2: palette roles (primary ball, ink block, pure-grey neutrals, two hairline tiers, band, semantic set from the ledger, level accent, content hues), type scale 12/13/15/17/20/24/40 with roles, spacing ladder with roles (4…40), radii 6/8/12/16/full, icon sizes 24/20/16, seam values (§4b), motion tokens; dark counterparts. check:color-contrast extended to the ledger's nine assertions + CVD separation, with canaries that fail on today's theme. | Token spec reviewed on the board's system sheet; gate green |
| B — Component library (design) | Every shared component designed in the grammar at 1:1 with all states and behaviours: rest / pressed (settle + dim, values) / hover (web) / focus (ring) / disabled / loading / selected / error / empty; sizes; content rules; a11y (targets ≥ 44, roles, labels); motion (durations, easing); what each replaces (one-for-one map from the inventory). Families: chrome (header, tabs, tab bar, FAB, CTA band, sticky edges), surfaces (block, band, stat strip, hero, chart), rows (media, person, cell, day header, section head, roster), labels (Tag, count), controls (Button, Choice, Switch, Input, TextArea, Picker, Slider, Search, Stepper), overlays (ModalPanel, menu, toast), states (empty, skeleton, error, offline), forms (wizard field grammar). Board /wireframes/flat-v2-components. | Owner marks the component board |
| C — Page layouts (design) | Every screen (76 blueprints in docs/architecture/screen-blueprint.md + the page QA blueprints) laid out from the library only, grouped by template: hub (홈, 기록, 프로필, club 홈), list (경기, 클럽, members, dues, sessions, notifications, messages), detail (session, club, venue, post, record, player), form/wizard (create session, create club, settings, dues edit), overlay flows (filters, pickers, menus), auth/onboarding. One board per template with every screen of that template at 1:1 (or a ruler listing the stack). Web adaptation follows the mobile grammar afterwards (/wireframes/web-app-2 reworked). | Owner marks each template board |
| D — Build | S1 tokens + semantic palette + CVD gate → S2 Tag/Choice + lint + migration → S3 primitives (rows, block, tabs, header, tab bar, FAB, CTA band, seams in FlatColumn/RowList) → S4 type ladder sweep → S5 glyph vocabulary + hero facts → S6 controls to ink → S7 screens by template, one template per OTA → S8 web. Each stage: yarn check, screenshot rung against the board, device QA list, preview OTA. | Per stage: gate green, board parity screenshot, QA list |
What is already decided (do not reopen)
Pure white canvas; rows 80/64 without lines; section = whitespace + title at 32; one 12pt band per screen for a change of kind; quiet cell hairline; sticky-edge hairline only under scrolled content; no shadows; one coloured action per screen + one block per hub; top-down type ladder; one passive Tag + Choice control; facts inline; hero-tier glyphs only (4 concrete); S2 (ball actions with ink text, ink block, ball numeral, green as mark and court tile); semantic set teal/amber/red per the ledger; D1–D10 as logged.
Open for the owner (carried)
D6 bookmark off the row · D7 live ribbon → trailing Tag · 홈 scope row dropped · the S2 brand-weak tint (#EFF9B7) · cell hairline tiers for S1/S3/S4 · the figure + Tag cluster in rank/result rows.
Next
Phase B starts with the component inventory (every @twomore/ui export and shared presentation component: states in code, geometry, consumer counts, v2 disposition), then the component spec and its board.
What the plan was missing (added 2026-10-09)
Owner: "Is this everything we need to change our UI/UX?" No. Phases A–B cover the visual system and the component library. A full UI/UX change also needs the layers below; each is placed where it has to land so nothing is designed twice.
| Layer | Gap (evidence) | Where it lands |
|---|---|---|
| Text scaling | the app sets no allowFontScaling / maxFontSizeMultiplier / font-scale handling anywhere (grep 2026-10-09); the grammar's fixed heights (rows 80/64, cell 48, tag 24) clip at large OS text sizes, which 40–50s users commonly set | B6 (library addendum): supported range, min-height growth, stacking at large sizes, display-numeral caps; a 100 / 130 / 200% strip on the boards |
| Motion & transitions | press physics exist; screen transitions are native defaults with no spec; list insert/remove, state changes, number changes, the scroll-linked header title and skeleton → content are unspecified; eight motion tokens are unused | B7 |
| Accessibility semantics | per-component roles exist; page-level reading order, row-level composed labels, headings, toast live regions and focus return after sheets are not specified | B8 |
| Platform | edge-to-edge plugin on; Android predictive back disabled (predictiveBackGestureEnabled: false); status-bar/nav-bar colours, keyboard avoidance (keyboard-controller installed, unused) | B9 |
| Information architecture & navigation | open IA items: 홈 scope row, 경기 explore entry point, session-detail order, the club 관리 entry (usability U-05), information-flow audit waves 3–4; 94 routes with no current map of entries, back targets, push targets and share links | C0 (new, before page layouts): navigation map + IA rulings |
| Task flows | the usability audit's task table is the baseline: join a club 3–6 taps, RSVP + pay 5 taps + a banking-app trip + a wait, create a session 7 screens / ~13 taps, create a club 9 screens / ~17 taps, dues for 20 members ~45 taps with no bulk action and no confirm; a visual redesign changes none of these | C0: each core flow redrawn as a screen sequence with a target tap count, on the new components |
| Every page in every state | layouts drawn only "full" hide the problems | C requirement: each page board shows ideal · empty / first-run · loading · error / offline · overflow (long names, many items, 99+) · English (ko/en ship) · 200% text |
| Content | components impose new length limits (Tag 1–2 words, block eyebrow, FAB labels, one-line toasts); empty-state and error copy per screen | C, with the terminology guide and the i18n gates |
| Brand touchpoints | adaptive icon background #006633, notification colour #006633, splash, app icon (native — need an EAS build, not OTA); web themeColor #006D37, icon.svg, favicon, OG image, share pages, Kakao share template images | A2: the S2 mark decision, then web/OTA parts with S1 and native parts with the next build |
| Canon, lint and tests | canon still says "no FABs", the Divider law, group-spread over tabs; no-hand-hairline knows one hairline token; the wireframe-kit parity test locks the old seams | D0 (first build step): rewrite canon + AGENTS rows + lint rules + parity tests to v2 so the gates enforce the new grammar instead of fighting it |
| Component gallery | the boards are drawings; nothing renders the real components in every state on device | D1: a dev-panel gallery of every v2 component × state (the live mirror of the boards), captured by the web screenshot rung and diffed against the boards |
| Migration mechanics | swapping 73 Badge files and 56 SelectionChip files by hand invites drift | D: v2 components land beside v1 (strangler), codemods for the mechanical swaps, v1 deleted when unused (knip); S2 colours flip globally only when the last template moves, so no screen shows ball buttons on old cards |
| Validation | nothing tests the new design with the people it is for | after the first migrated template on preview: re-measure the task table, a 5-person moderated test (총무 and members, 3 tasks; script prepared here, people recruited by the owner), telemetry baselines for RSVP, pay and dues taken now |
| Web | the desktop board predates the grammar | D-last: web grammar extension (breakpoints, sidebar, pointer density) after mobile |
Status (2026-10-10)
| Phase | State |
|---|---|
| A foundations, A2 brand | specified (grammar, colour ledger, S2, brand touchpoints); tokens land in D/S1 |
| B library, B6–B9 | specified in the library §0–§22 and the behaviour addendum; boards flat-v2-components, -controls, -patterns, -additions |
| C0 IA and flows | done (flat-v2-ia-and-flows-2026-10-09.md, rulings §6, measured targets §3) |
| C page templates | drawn: flat-v2-hub, -list-a, -list-b, -detail, -forms, -auth, -flows, every one at 0 contract failures |
| Owner marking | pending; code starts only after the owner marks the library and page boards |
| D0 onward | not started |
Revised order: A foundations (+ A2 brand) → B library (+ B6–B9 behaviours) → C0 IA and flows → C page templates in all states → D0 canon/lint/tests → D1 gallery → D build by template → validation → web.
Build waves (owner review 44, 2026-10-16)
The owner closed the open decisions and said "C1 - C6: let's all proceed as recommended". Code research (2026-10-16) found the v2 interface absent from code (no S2 tokens, no v2 primitives) and the data items partly built. Database migrations run one at a time (one local Supabase stack), each validated by npx supabase migration up and its pgTAP file, then pushed. App-layer work runs beside them.
| Wave | Item | What | Evidence from the code |
|---|---|---|---|
| W0.1 | C1 head-to-head by format, partners | done (00692): player_matchups_by_format and player_partner_records, written by private.apply_match_ratings (the only writer of player_matchups), rebuilt by recompute_all_ratings, backfilled; usePlayerMatchupsByFormat, usePartnerRecords; computePartnerStats keyed by partnerId | player_matchups has no format (the engine is "discipline-agnostic"); matches.format exists; computePartnerStats groups by name, merging namesakes |
| W0.2 | C2 queries | done (00693): get_my_guest_open_sessions, get_session_result_summary(session), find_open_pickups_for_me ((play regions, else the profile's home region, or saved courts) and the level band, server-side); hooks useMyGuestOpenSessions, useSessionResultSummary, useOpenPickupsForMe; the home pickup card reads it | standing guests already read sessions through RLS but no client path exists; no per-session summary; the 번개 filter runs on the client after pagination |
| W0.3 | C5 bell and member 할 일 | done: the bell counts news only (deriveBellState; the attention tone and deriveBellCountInputs removed); 00694 get_home_signals gains unpaidSessionFeeCount (idle holds only: a submitted transfer waits on the host), unpaidSessionFeeDeadline and scoresToEnterCount; deriveHomeTasks is the one 할 일 model for members and officers (useHomeSignals, useHomeTasks) | obligations add to the bell today; member 할 일 has no hook; get_home_signals counts score verification, not entry, and no session fees |
| W0.4 | D2 club events | done (00695): status CHECKs; RSVP insert and update require active membership (WITH CHECK); withdrawal is own-row and the caller always sees their own row; capacity enforced (private.enforce_club_event_capacity, 23514 club_event_full); useClubEvents, useClubEventMutations | club_events is wired to the registry with no hook or screen; its RSVP policy has no membership check; max_attendees is not enforced |
| W0.5 | C4 recorded sessions close on the last score | done (00696): deriveSessionCloseGate (a completed recorded session with unsettled matches reads 점수 입력 대기); complete_session refuses a recorded session with unsettled matches (matches_unsettled) and can cancel the never-started ones on the host's explicit choice; auto_advance_session_statuses is unchanged (review 27: it keeps ending play and never voids a match); one shared end-session flow (end-session-flow.ts) on the detail menu and the match board; the badge label 점수 입력 대기 exists but no surface shows a completed status yet (the W4 pages do); then live mode in the UI waves | the cron completes in_progress sessions 8h after their end even with unscored matches; is_friendly already is the casual flag |
| W0.6 | C3 and D3 photos | done (00697): featured photos first, filled to five with the latest approved (selectMediaBandPhotos); a ThumbHash per photo and per banner, generated on the device at upload (expo-image generateThumbhashAsync, no edge function) and stored (club_media.thumbhash, clubs.banner_thumbhash); the top luminance derived from it (isBrightCover); seeded photos hashed by seed-media.mjs; RemoteImage takes the placeholder; covers resolve with their hash (resolveClubCover: album, banner, the default covers' generated hashes); the per-cover status bar is applied by the MediaBand primitive (W3) | upload goes client → Storage with no processing; no hash anywhere; the banner ignores kind |
| W0.7 | M14 알림 data | partly done (00699, 2026-10-21; the owner approved the drawing on 2026-10-20): 처리할 일 from signals_open_requests() (입금 확인, 가입 신청, 참가비 입금), dismiss and undismiss in batches (the Toast's 되돌리기) and unacknowledge, and the page in flat v2 (L3-13). Still to do: the feed and count RPCs read a 30-day window, not expires_at, and leave chat out; signals_emit records the actors and the latest one, so a repeat is one row ("외 N명"); the missing request signals (친구 요청 with its 수락 · 거절, 사유 인정 요청, 신고) with their open rules in signals_open_requests(); the emitters re-keyed so repeats group; club_members.notify_level (전체, 공지만, 끔); the six push families and 메시지 on 알림 설정, quiet hours with the payment exemption; send-push's ttl, interruptionLevel, threadId and badge (Time Sensitive needs an iOS entitlement, so with W5); in the client, the bell from the unread rows with no netting and 할 일 from deriveHomeTasks. Research: docs/design/research/owner-review-46-lane-e-2026-10-19.md § 5 | 37 of 101 declared kinds never fire; a row disappears when its signal expires (half the kinds within 48 hours); a 12-week series sends 12 rows; one severity decides push, band and bell |
| W0.8 | M6 신뢰 data (proposed) | waits for the owner's check of the 신뢰 drawing (record item 17): attendance_records for every club and pickup (the club setting decides enforcement only); rsvps.checked_in_at and the M26 window columns; member_reports.outcome with a 12-month expiry; manner praise counted by distinct people who shared a match; one windowed function (the last 20 records within 12 months) in place of the four attendance formulas, with its parity script; an append-only trust_events ledger feeding the tier, 내 신뢰 기록 and each push's cause; 사유 인정 요청 and the official's answer; the new tier gates in private.recalculate_trust_tier and trust-tier.rules.ts together; the privacy policy's criteria, retention and contact (with counsel, EXT-2). Research: docs/design/research/owner-review-46-lane-d-2026-10-19.md § 3 | attendance is recorded only where a club turned tracking on, while the tier is app-wide; handling a report erases its demotion; a strike never expires; four formulas compute attendance |
| W1 | C6 tokens (D/S1) | done, additive: the S2 roles into the default theme (canvas, ink, secondary, tertiary, success, danger, info, courts) and new keys for every other §1.7 role (ball, block, hairlines, on-photo, inverse, series…), in the theme (one scheme since D7, which deleted the other three); s2-contrast.test.ts holds §1.7's ratios. Interim: dark $success is S2's success-text value until W2/W3 split fills from text. No CVD simulator exists in the repo yet (owed) | tokens.ts is still forest green (brand600 #006D37); no #D6F04A or #15181A in code |
| W2 | C6 Tag and Choice (D/S2) | the v2 Tag and Choice, a lint rule, and the mechanical migration of Badge and SelectionChip consumers. Split by the owner on 2026-10-19: W2a (the components and their infrastructure, the lint rule, the codemods, the status wrappers and the 68 mechanical files) now; W2b (the 55 files that need a design call) with their pages; research in docs/design/research/w2-tag-and-choice-2026-10-19.md | 73 Badge files and 56 SelectionChip files |
| W3 | C6 primitives (D/S3) | Block (three rows), MediaBand and the photo counter, Feed item, PersonRow, MediaRow, Cell and Task Cell, Section head, Strip, Tabs, Header, Tab bar, FAB, CTA band | packages/ui holds card-era and flat-v1 versions only |
| W4 | C6 pages | 홈 (approved) first, then each page as the owner approves it. The money pages (participants with fees, 클럽 홈's 회비 pane, where 내 회비 folded on 2026-10-21) carry the departure refund ruled on 2026-10-19 (decision log): a member who has left needs a place to close the refund | page boards: hub, list, detail, forms, auth |
| W5 | C6 the native build | the build with the new mark becomes the OTA baseline; preview OTA resumes. It adds expo-clipboard 57.0.2 (SDK 57), the copy button beside a club's account number (owner review 46, M11) | OTA paused since 2026-10-14 |
Build order after the audits (owner, 2026-11-01: "Yes, go")
The three build-order audits (owner-review-52-audits-2026-11-01.md) order W2b, W3 and W4 above; the rows above keep their history, this table is the standing order. Three lanes run at once because their files are disjoint (kit, schema, boards), within the workstream rules: at most three agents, one writer per file, one migration at a time on the shared local database, every batch committed green, pushes from a clean tree, OTA paused until W5.
| Wave | Content | Ships as | Source |
|---|---|---|---|
| K1 | The v2 text roles (FvkT, the FVK_TYPE roles at the OS scale with their caps) and Button / IconButton (tones primary/tonal/ghost/danger × sizes sm 36 / md 44 / lg 52, loading label, expanded) | the W2a shape: the component in @twomore/ui, a parity test asserting the kit's literal values (wireframe-kit-parity.test.mjs), a lint rule that bans the legacy piece once replaced, a codemod sweep; additive first, then the sweep | kit audit § 1, waves 1 |
| K2 | Cell with CellGroup; from it Switch, PickerCell, CheckRow, TaskCell, the consent row | same | kit audit § 1, wave 2 |
| K3 | SectionHead (the feed variant extended: Door pill, bulk, note, pager, info; the command prop goes) and EmptyState | same | kit audit § 1, wave 3 |
| K4 | Header, then Sheet and from it Dialog, MenuSheet, ValueSheet, SheetFooter, Scrim | same | kit audit § 1, wave 4 |
| K5 | The rows: PersonRow, MediaRow, TextRow, FeedItem on Thumb, Avatar, Meta; DayHeader; ExpandToggle as the count button (ruled 2026-11-01) | same | kit audit § 1, wave 5 |
| K6 | Block, Strip, Notice, Skeleton, Toast, CtaBand, Tabs, Fab, and the form controls (Field, Input, TextArea, ChoiceSet, Stepper typed, Calendar) with their pages | same, with the first page that needs each | kit audit § 1, wave 6 |
| W0.9 | Schema batch 1, notes and 신뢰: match_notes (match_id, author_id, mood, body ≤ 500, own-row RLS) with useMatchNotes and the save; the trust-record read (신뢰 점수 = 50 + 12-month points held 0..100, none before 5 recorded sessions; per-record points, rule, venue/day, streak, praise) with useTrustRecord; the visibility-filtered reads of another person (match history, achievements, play facts) | one migration (00703+), SECURITY DEFINER functions with search_path = '', REVOKE + explicit GRANT, pgTAP cases (success, empty, refusal), generated types through prettier, port → adapter → hook → registry | data audit § 2, batch 1 |
| W0.10 | Schema batch 2, money: club_dues_plans, dues.plan_id/period_start/period_end, the club ledger with its aggregate read and CSV export, the reminder log and the reminder RPCs (dues, session fee) | same | data audit § 2, batch 2 |
| W0.11 | Schema batch 3, sessions: rsvps.leaves_at/available_until_round/checked_in_at/checked_out_at and the arrival/exit stamps, matches.stage/group/scheduled_start with tournament matches moved into matches, the saved generation preset and policy axes, submit_match_score expected-revision | same | data audit § 2, batch 3 |
| N | Three navigation fixes before any page: /venues/[id] takes the directory screen's navigation and /directory-venues/[id] redirects; the club 관리 gear sheet leaves /clubs/[clubId]/admin; NotFoundState takes the drawn door (홈으로 가기 + the context door) with 돌아가기 second | ordinary code batches, each with its screen test | navigation audit § 3 |
| B | The owed boards, before W4 reaches their pages: 정기 일정, 조, 앨범 관리, 행사, the club 테니스 여정 (basic/pro), the 노트 보기 Sheet, and the club 멤버 초대 Sheet on L2-01/L3-03/L3-11/L3-25 (replacing links 130, 182, 218, 317 to L4-10); the board-lint baseline cleared so the lint gates again | board sections grounded in the real components, page-levels and page-links entries, the heights table, the verifier green | navigation audit; rulings |
| W4 | Pages, in this order: forms and settings (L4-01 … L4-06, L4-11, L4-20, L4-21), the lists (L3-13, L3-14/15, L3-19 … L3-24), 경기 (L1-03) and 기록 (L1-04), 일정 (L2-02) after W0.11, the money pages after W0.10, the hubs (L1-01, L1-05, L2-01) after Block and the trust read; each page replaces its screen behind the same route | per page, the definition of done: its page-links rows built (the navigation table is the checklist), states/copy/accessibility audited (synthesis § 6: 3, 4, 8), the blueprint entry updated, a screen test per state, yarn check green | audit § 4 |
| R | Retire the nine screens of synthesis § 3 once their doors live on the pages that claim the routes; redirects kept for old links | route files removed, docs:page-inventory regenerated | synthesis § 3 |
| Release | Roles, deep links, signals, performance as release gates (synthesis § 6: 6, 7, 9, 10); then W5 |
The first batch (2026-11-01): K1, W0.9 and B in parallel.
Status, 2026-11-01 13:45. Landed on main: K1a (888622d69, 5e8ffa2ab, f44b8b739) and K1b part 1 (f66d5c980; the sweep runs next); W0.9 as 00703 (1f9ebec4e) and 00704 (6c038e2de) with their clients (3e6601a5e, ee3836ff5), both pushed to the hosted project; B: the invite Sheet (526149eb4), boards 1A (af11ae13c) and 1B (73adb75b2) — owed is empty; 1C (list-b, hub) and 1D (forms) remain. Next: the K1b sweep, boards 1C/1D, the page hooks on 00703/00704, then K2.
Status, 2026-11-01 17:10. Also on main: the K1b sweep (56e664962: every screen on the v2 roles and buttons, zero waivers), boards 1C and 1D (c05cdd8c4, 9e67a7c91: owed is empty), wave N fixes 3 and 2 (e801b86ff, b20b1e231; fix 1 moves into the L2-04 page build), K2a (d8860b2f2: Cell, CellGroup, Switch and the row atoms, additive). Next: K3a, K2b (the rows sweep), the page hooks on 00703/00704, schema batches 2 and 3.
Status, 2026-11-01 21:00. Also landed: K3a (74e8ca762: SectionHead, Door pill, EmptyState kind, FeedEnd doors, ExpandToggle label), K2b part 1 (ec094c433: no-legacy-rows at 51 files, five codemods) with batches 1 and 2 (8527c941f profile settings, e7fb95a1d clubs + onboarding: the ratchet at 22), K4a (b0b8a36e2, 82e2cee04: rail, Header, SheetPanel/SheetFooter/ButtonPair, $backdrop, Dialog behind ConfirmSheet, the guard hooks, MenuSheet, ValueSheet; RAIL_RESPONSIVE off until K4b). Next: K2b batch 3 (sessions + apps, the legacy rows retired), K3b, K4b, schema batch 2 (money) from its research; the 00703/00704 page hooks land inside their W4 pages.
Status, 2026-11-01 23:50. Also landed: K2b batch 3 (5e75ec481: sessions, messaging and the deep links on Cells; FactRow, EditFieldRow, TogglePill, PickerRow retired; no-legacy-rows at 1), K3b part 1 (376a94113: no-legacy-heads at 166, the four codemods) and part 2A (bf0f69203: home, activity, profile, records, clubs, packages/app and the apps on SectionHead and EmptyState kinds; the ratchet at 65), schema batch 2 (23f90dabc: 00705 dues plans and periods, 00706 the club ledger and payment reminders, client C1; rulings in canon/payments.md). Next: K3b part 2B (sessions, messaging, 알림; the legacy heads retired), money client C2–C4, K4b (tooling, the shells lane, the overlays lane, then the rail flip — k4b-brief.md), schema batch 3 (sessions) from its research; then W4.
Status, 2026-11-02 02:20. Also landed: money clients C2–C4 (9e39d7e07), K4b part 1 (ccccad391: no-legacy-chrome at 69 files, the frame-to-sheet and shell-props codemods, the chrome judgment table), schema batch 3 (1461d1c26: 00707 the attendance window and stamps with lateness as a trust ledger source, 00708 tournament matches as rows and the score revision; client C1; both pushed to the hosted project), K3b part 2B (2e3feda95: no-legacy-heads at 0; the legacy heads, EmptyState and ExpandToggle paths deleted). Next: the K4b shells lane and overlays lane with schema C2 in parallel (k4b-brief.md § File ownership, schema3-c2-brief.md), then the rail flip, then W4.
Status, 2026-11-02 04:30. Also landed: schema 3 client C2 (2611c3824: tournaments replayed from match rows, useSubmitScore with the revision, the saved preset and policy with parity scripts), K4b (bbc7fea05: Header and WizardHeader on every shell, Sheet for every panel, the kebab gone, no-legacy-chrome at 2 — the K6 quick panels) and the rail flip (0c4f6537c: RAIL_RESPONSIVE on). K1–K4 are complete. Next: K5 (rows and feed items) and K6 (blocks, strips, notice, skeleton, toast, CTA band, tabs, fab, the form controls, reactions, the search takeover) from their research briefs, then W4 pages in the table's order.
Status, 2026-11-02 08:20. Also landed: the rail flip (0c4f6537c), K5a (74b4384f4: the rows, Avatar, Thumb, Meta, DayHeader; the copy as domain), K5b part 1 (e1eae8bf4) and its clubs package (94a360ff5: the app PersonRow as a wrapper over the ui row; six hosts off the railed presets; ratchet 78 → 49), K6a-0 (246b046c0: the shared strings, Button fg, TextAction, Spinner, the chrome stack), K6 lane N (6302e3a53: Notice, SkeletonKind with deferred loading, the Toast restyle, errorSurface, no-error-toast at 122) and lane F (f0ae70cc8: Field, Input, TextArea, ChoiceSet, Stepper, SearchField, SearchTakeover, CtaBand). Next: K5b's remaining packages and retirements, K6 lane C (Tabs, Fab, Strip, Block, Calendar, Reactions), the K6b sweeps, then W4.
Status, 2026-11-02 12:30. K5 complete: K5b swept every people list and the media rows (fa1034885 … 63a622f5d) and retired ListRow, SelectionCheck, MetaSegments, AvatarBubble and the three media rows. K6 lane C landed (65144c726: Tabs, Fab, Strip, Block day/elo/dues/identity, Calendar, Reactions with the registry). The K6b tooling is in (435979036, 8d092c47e: no-legacy-forms 101, no-legacy-feedback 113, no-legacy-surfaces 29, thirteen codemods). Next: the three K6b sweeps by package ownership (k6b-sweeps-brief.md), the ui deletions when they are in, then W4 pages in the table's order.
Status, 2026-11-02 14:10. K6b swept: the three package sweeps landed (clubs/onboarding/activity/apps, sessions/messaging/home, profile/records/app/ui) and the ratchets read no-legacy-forms 18 files (from 101), no-legacy-feedback 6 (from 113), no-legacy-surfaces 11 (from 29); what is left is TODO(K6b) markers for the pages (captions, composer bands, bare inputs, the dues month selector, the venue correction Cards). The kit's own five ActivityIndicator sites draw Spinner. The ui legacy pieces are deleted when knip proves no caller (next batch). Next: schema batch 4, the K7 page pieces, then W4 pages in the table's order.
Status, 2026-11-02 15:10. Schema batch 4 landed (00709: the sixth post reaction; pgTAP 00709; the client's six) and the K6b push is on origin (3dd73ecdb, with the four suites the full gate caught). Next: the K7 page pieces from a research brief (Hero, TabBar, Band, Heatmap, Roster, PostRow, ResultRow, Shelf, the match and clubHero Blocks), then W4 pages in the table's order; the ui legacy deletions ride the W4 pages that clear the last TODO(K6b) markers.
Status, 2026-11-02 15:40. K7 lane 1 landed: Hero (detail) and Band in the kit with their parity tests, no-legacy-surfaces retiring every TypeHero and <Divider seam="section"> behind the ratchet. Next: K7 lanes 2 and 3 (TabBar, Shelf, Roster, PostRow, ResultRow, Heatmap, the match and clubHero Blocks), then W4 pages in the table's order, which convert the TypeHero and section-Divider sites as they go.
Status, 2026-11-02 17:40. K7 complete: lanes 2–4 landed (TabBar, Shelf, Roster, the cells grid; the Heatmap with the result scale, PostRow, ClubHero; Team lines, the Match shell, ResultRow, Block kind="match"), every number held to the kit; no-legacy-surfaces retires the six callers they replace (ratchet 31 files). Next: W4 pages in the table's order (forms and settings first), each converting its TypeHero / Divider / scoreboard / feed-row callers and clearing its TODO(K6b) markers; the ui legacy deletions ride them.
Status, 2026-11-02 20:00. W4 batch 1 landed: the Edit Sheet and PhotoTiles in the kit, and three pages on their boards with every board state tested — 프로필 수정 (L4-11), 글쓰기 (L4-05), 초대 코드로 가입 (L4-04) with the new 입금 계좌 route (L4-21). The W4 order is now the research's (w4-1-research.md § 3.2): batch 2 = L4-06 (a draft route over the wizard store), L4-20 with the relocation of 정기 일정 / 조 / 앨범 관리 behind routes (L3-48/49/50 join the order), ClubInviteSheet, CoverGrid, LevelBand; batch 3 = L4-02, then L4-03 with L2-07; L4-01 last (a review page with about fourteen Sheet editors, three lanes after a contract).
Status, 2026-11-02 22:30. W4 batch 2 landed: four kit pieces (BandSlider, LevelBand, CoverGrid, ClubInviteSheet), 클럽 설정 (L4-20) on its board with silent inline writes, 경기 규칙 (L4-06) as one body with a saved host and a draft route over the wizard store, and the relocation of 정기 일정 / 조 / 앨범 관리 behind their code-map routes (L3-48/49/50, a verbatim move on one shared host frame; the board pages come with the lists wave) with the guest roster on 멤버 관리. A visitor's share no longer carries the invite code; the column-level fix for clubs.invite_code (readable by any signed-in user on a discoverable club) is schema batch 2b. The house style rules the boards where they drifted (클럽 복구, 범위 설정, 공유, 로그인). Next: batch 3 = L4-02, then L4-03 with L2-07; L4-01 last.
Status, 2026-11-08. W4 batch 3 landed: six kit pieces (HueBand, CountCell, DateTimeSheet on BandSlider's point mode, MapFace, and the shell, media row, Field, picker Cell and Stepper extensions), 클럽 만들기 (L4-02) on its board with the explicit 활동 지역 and the quick path that skips it (B1), 행사 만들기 (L4-03) with the place search, and 행사 (L2-07) with its edit mode; migration 00710 (the cancel signal, the event limits) and the search function's places mode land with it. Next: schema batch 2b (the invite code column), then L4-01 일정 만들기 last of the forms (a review page with about fourteen Sheet editors, three lanes after a contract; it reuses DateTimeSheet with a range mode, the time-band model and CountCell); then the lists, 경기/기록, 일정, the money pages and the hubs.
Status, 2026-11-08 (2). Schema batch 2b landed: migration 00711 makes a club's invite code members-only (the column revoked from clients, get_club_invite_code for an active member, the club, home and session bundles naming their columns), and the client reads the code only where a member shares it or 클럽 설정 shows it. Next: L4-01 일정 만들기, last of the forms; then the lists, 경기/기록, 일정, the money pages and the hubs.
Status, 2026-11-08 (3). W4 batch 4 landed: 일정 만들기 (L4-01) on its board, the last of the forms. It opens on a review page of value Cells, each editing in its own Sheet, for a host with something to copy (an entry sheet offers the club's 정기 일정, its past sessions or 새로 만들기), and on seven steps for a first session; both render the same editor Bodies (create-session/editor-contract.ts). The kit gained DateTimeFields (range mode), form-layout.tsx, CountCell unsetText and the app PlaceMapFace. Next: the lists, 경기/기록, 일정, the money pages and the hubs (the hub's 만들기 opens this entry sheet).
Status, 2026-11-08 (4). W4 batch 5 landed: the lists (L3-13 알림, L3-14 메시지, L3-15 메시지 요청 on its own route, L3-19 친구 찾기, L3-20 차단한 사용자, L3-21 동의 이력, L3-22 공지사항 with a page per notice, L3-23 앨범, L3-24 복식 파트너 추천) on their boards, with the kit SwipeAction, PhotoGrid, CaptionRow, ScoreSheet, ExplainerSheet, DocPage and useFabListTail, and two migrations: 00712 (the bulk 최근 20회 pull for the partner scores) and 00713 (a block keeps two people out of each other's search and friend requests). Still open from W0.7: the grouped and request rows of 알림. Next: 경기 (L1-03) and 기록 (L1-04), 일정, the money pages and the hubs.
Status, 2026-11-08 (5). W4 batch 6 landed: 경기 (L1-03) with its 테니스장 pane (L1-03c) and 기록 (L1-04) on their boards, on the kit filter grammar (FilterHead, FilterSheet, the region and date Sheets), the tab-root chrome (MainTabShell chrome="tabs", the kit TabBar over the panes) and skeleton next pages; the venue finder is pick-only. 순위표 (L3-17) moves to its own batch with the leaderboard schema. Next: 순위표, 일정 (L2-02), the money pages and the hubs.
Status, 2026-11-09. W4 batch 7 landed: 순위표 (L3-17) on its board, four scopes (클럽, 친구, 지역, 전국) and three periods behind the Feed head's one Choice, on the server's new ranking reads (00714: one pool rule, tied ranks, the record behind the results setting; 00715 retires the old reads) and three kit pieces (Podium, a two-choice ValueSheet, FeedList's pinned head). Next: 일정 상세 (L2-02) with 매치 미리보기 (L2-03), then the host tools and money pages, the live pages, 대진 짜기 and the hubs.
Status, 2026-11-09 (2). W4 batch 8 landed: 일정 상세 (L2-02) and 매치 미리보기 (L2-03) on their boards, on two kit pieces (MediaBand with the shell's circles bar, TimeRangeSheet) and pure rules for the band, the one Notice and check-in; the 관리 sheet is a session doors registry; 도착했어요 stamps the time of the tap. Next: the host tools (L3-35/36/37) with schema 00716, then the money pages (L3-11, L4-14, L3-46, L3-16).
Status, 2026-11-09 (3). W4 batch 9 landed: the host tools on their boards — 참석자 관리 (L3-35, the session fees folded in, /payments retired), 참석 확인 (L3-36) and 호스트 관리 (L3-37) — on schema 00716 (fee undo, bulk arrival confirm, first timers) and 00717 (an arrival undo that keeps the stamp), three kit pieces (PersonSheet, DetailShell subhead, WindowBar) and one access rule. Next: the money pages (L3-11, L4-14, L3-46, L3-16).
Status, 2026-11-09 (4). W4 batch 10 landed: the money pages on their boards — 회비 관리 (L3-11), 회비 설정 (L4-14, its own route /dues/plan), 장부 (L3-46, /dues/ledger) and 참가비 내역 (L3-16, /profile/fees) — with the dues read fixed (the paid amount and currency it never selected), a fee-state badge shared with 일정 상세, the session-fee words moved to 미납 / 확인 대기 / 완납 / 환불 대기, a Strip that stacks from 1.5x text, and a silent option on the dues status write. No schema. Next: the live pages (L3-38, L3-39), 대진 짜기 (L3-47), then the hubs.
Status, 2026-11-09 (5). W4 batch 11 landed: 스코어카드 (L3-38) on its board — one page for every session kind (rotation, league, tournament, 코트승급전, a single match) with LIVE by court, 일정 by round or by group, 대진표, 순위 and 코트, the viewer's own match with its score entry, 경기 중단, 점수 검토 요청 and notes, and the recap with 결과 공유 — on schema 00718 (the score review store), seven kit pieces (ScoreEntry, ScoreTable, ScoreStop, MatchRow on an extended MatchShell, GroupCard, GroupTable, Bracket) and one kind rule for the page and its operator view. Next: 스코어카드 운영 (L3-39) as the operator layer over the same page, with the staff RPCs (start, review resolution, exclusion, end), and the retirement of the match board and the tournament board; then 대진 짜기 (L3-47) and the hubs.
Status, 2026-11-09 (6). W4 batch 12 landed: 스코어카드 운영 (L3-39) as the operator layer over the shared page — 할 일, the one step per phase in the band with its Dialogs, the ⋯ rows, 시작 / 검토 / 대신 입력 on the rows, the review Sheet (keep, change, 기록에서 제외), 대신 입력 in a run, 교체, 늦게 온 선수 and 팀 바꾸기 — on schema 00719 (the staff's lifecycle writes) and 00720 (review resolution, exclusion and reopening with a rating replay), the kit HostReviewSheet, and one rule for when a match owes a score; the old match board and tournament board retired. Next: 대진 짜기 (L3-47), then the hubs.
Status, 2026-11-09 (7). W4 batch 13 landed: 대진 짜기 (L3-47) on its own route for every draw — the first draw, the next round (a 대회's later rounds included) and the draw again before round 1 — with 대상, 대진 방식 with each style's needs, 고급 설정 (짜는 방법), the Notices, the preview and one save that writes the round and its 방식 together (00721); 일정 상세 and 스코어카드 운영 open it, and the chooser Sheet, the add-round Sheet and the tournament modal retired. The live pages are done. Next: the hubs (L1-01, L1-05, L2-01 with the 회비 pane rebuild, L2-04, L2-06), the W0.7 remainder, the R retirements and the whole-app audit.
Status, 2026-11-10. W4 batch 14 landed: 클럽 홈 (L2-01) on its board for members, officials and visitors — five panes (홈 with the club feed, 채팅, 기록 with seasons computed on read, 멤버, 회비 rebuilt on the 회비 관리 pieces), the join flow on the page itself (the preview screen retired), the shared feed model, and migrations 00722 (the bulk mark-paid dropped with ClubDuesBody) and 00723 (the club's 결과 read, the bundle trimmed). Next: 프로필 (L1-05) and 공개 프로필 (L2-06), then 홈 (L1-01) with its schema, then 장소 상세 (L2-04).
Status, 2026-11-10 (2). W4 batch 15 landed: 신뢰 기록 (L3-01, new route /profile/trust), 프로필 (L1-05) and 공개 프로필 (L2-06) on their boards — one identity Block model for both profiles, the friends' AvatarRow, the achievement shelf, the RSVP-cap Notice, another person's results through their own reads, the friendship band and ⋯ by friendship, the link share, and migration 00724 (구력 public, the session count for every viewer, the RSVP cap as a policy row); the tier and trust-tier info Sheets, the hero, menu, friends and attendance cards, the manner tags and the friendship band retired. Next: 홈 (L1-01) with its schema, then 장소 상세 (L2-04).
Status, 2026-11-10 (3). W4 batch 16 landed: 홈 (L1-01) on its board — the Lead (your match live or just ended, your next confirmed game with its fee, empty, first run), the 할 일 lane with each task's subject, 소식 from one server read with its filter, hide and mute with undo, the FAB by role with the club picker, the coachmark, and the stale / error / offline states; migration 00725 (get_home_news, the hidden and muted rows, task subjects in get_home_signals, club roles and the live match in get_home_snapshot); kit: FeedItem long press, the Shelf cap, OfflineStrip, Coachmark over Scrim, one match Block binding. The 일/주/월 panes, recap cards, the disputed banner and the old coachmark tour retired. Next: 장소 상세 (L2-04, brief w4-17), then the W0.7 remainder, the R retirements and the whole-app audit.
Status, 2026-11-10 (4). W4 batch 17 landed: 장소 상세 (L2-04, absorbing L2-05) on its board — one page for a venue whether you come from a club or the directory: the map band with 길찾기, the Hero with 인증됨, the Notice slot, the live Block, three Shelves from their own reads, the 정보 group, the band, ⋯ with the official's commands; migration 00726 (get_venue_live_matches, get_venue_public_pickups, get_venue_home_clubs); one id rule with /directory-venues/[id] redirecting; the club venue list's rows became doors; the two old venue screens, their sections and parts retired. The hubs are done. Next: the W0.7 remainder, the R retirements and the whole-app audit.
Status, 2026-11-10 (5). W4 batch 18 landed: the W0.7 remainder and the R retirements. 알림's data — one visibility rule (30 days, not deleted, not chat), grouped repeats with the people behind them, friend-request and report rows, the club push level, the master switch enforced, the push claim with its group key and unread count (migrations 00727–00731) — and its client: the 알림 page with 외 N명 titles, request answers and folds, the bell without netting, 알림 설정 on its board, send-push with ttl, interruption level, thread and badge. Two server defects closed on the way (an officials-only post alerted every member; any client could emit an alert). R: /records/history and old payment links redirect, the share receivers are claimed, the history screen retired. Left: 사유 인정 요청 (W0.8), match and rating grouping, the whole-app audit, then the release gates and W5.
Status, 2026-11-10 (6). W4 batch 19 landed (the whole-app audit's first batch): 멤버 관리 with the member Sheet, 가입 심사, 게시글, the DM thread, 단톡방 만들기 and 멤버 초대 on their boards, every frame a screen-test state named by its frame id; the old member modals, kebab menus and the nominate-guest modal retired (ROLE_ORDER kept in one pure module). The audit (research-audit.md in the build scratchpad) counts 48 of 88 pages rebuilt after this batch. Next: batch 20 (profile and settings forms, the profile lists on DocPage, the door fixes), then records detail and club-ops lists, the 정기 일정 / 조 / 앨범 관리 lists, the new pages, the entry pages; then the release gates.
Status, 2026-11-10 (7). W4 batch 20 landed, the first batch built by the new account's session: the four account forms (비밀번호 변경, 회원 탈퇴, 공개 범위, 동의 관리) and 설정 checked frame by frame, 친구 목록 and 업적 on their boards, 이용약관 and 개인정보 처리방침 on the kit DocPage and 문의하기 as topic heads with one mail door; trust notices land on 신뢰 기록; the club-ops doors of 성장 분석, 출석 현황 and 클럽 대항전 open; the frame cases owed for 일정 상세 and 신뢰 기록 are named by frame id. 58 of 88 pages are rebuilt after this batch. Next: batch 21 (records detail, the club-ops lists, the venue forms, the person doors this batch could not open), then the 정기 일정 / 조 / 앨범 관리 lists, the new pages, the entry pages; then the release gates.
Status, 2026-11-10 (8). W4 batch 21a landed (batch 21 runs in three parts): 기간 기록, 하루 기록 and 선수별 전적 on their boards, each over one shared row model; a record page can name another person (the 00704 read decides what the viewer sees) and 선수별 전적 can name who it is with; 공개 프로필 opens 전체 보기, 자세히 보기 and the club; the kit gained FormStrip and the identity Block's club link; the v1 record hero, ring, timeline, scope selector and activity calendar retired. 61 of 88 pages are rebuilt. Next: 21b club operations (일정 관리, 신고 관리 with the outcome write, 테니스장 관리, 출석 현황, 클럽 대항전 with a server-side answer, 성장 분석, the officials' record), 21c venue suggestions and ownership, then batches 22–24 and the release gates.
Status, 2026-11-10 (9). W4 batch 21b landed: the six club-operations pages on their boards (일정 관리, 신고 관리, 테니스장 관리, 출석 현황, 클럽 대항전, 성장 분석) and the officials' view of a member's 신뢰 기록, reached from 출석 현황, 회비 관리 and 멤버 관리 with the club in the route. Schema 00733 (a report resolved with its outcome; the policy window in the trust record read) and 00734 (a challenge answered by the invited club through one function; direct updates closed). 67 of 88 pages are rebuilt. Next: 21c venue suggestions and ownership (six pages), then batches 22–24 and the release gates.
Status, 2026-11-10 (10). W4 batch 21c landed, closing batch 21: the venue suggestion and ownership pages on their boards (the two lists on one Feed, the two receipts, the suggestion form with its five kinds, the ownership form), over a shared contract (two status wrappers, the receipt's venue row and wall, a past-only day Sheet, the place-search takeover in the app package, one evidence group). The v1 correction modal and its fields retired, and no TODO(K6b) marker is left in these folders. 73 of 88 pages are rebuilt. Next: batch 22 (정기 일정 with its editor, 조, 앨범 관리), 23 (테니스 여정, 클럽 리포트, 연결된 계정), 24 (the entry pages), then the release gates; two schema batches wait on the owner's rulings (사유 인정 요청; 대항전 results settled from matches).
Status, 2026-11-11 (11). W4 batch 22 landed: 정기 일정 and its new edit page (정기 일정 수정), 조 and 앨범 관리 on their boards, the last of the club-management lists. Migration 00735 closes waiting and refused album photos to ordinary members and makes the photo order one write. The v1 section editors under settings/ that these pages mounted are retired. 77 of 88 pages are rebuilt. The owner decided the nineteen open questions on 2026-11-10 (the decision log); they add six follow-up batches after the page batches. Next: batch 23 (테니스 여정, 클럽 테니스 여정), 24 (the entry pages; provider sign-in and 연결된 계정 stay with W5), the follow-up batches, then the release gates.
Status, 2026-11-11 (12). W4 batch 23 landed: 테니스 여정 and 클럽 테니스 여정, two new pages, over migration 00737 (a note carries its session, the notes read pages by whole days, a club's photo days) and a new club-plan entitlement. Migration 00736 closes who filed a club report to the club's officials. 79 of 88 pages are rebuilt. Next: batch 24 (the entry pages: 공유 링크, 온보딩, 이메일 로그인; provider sign-in, the mailed-link sign-up, 본인인증 and 연결된 계정 stay with W5), the six follow-up batches from the owner's decisions, then the release gates.
Status, 2026-11-11 (13). W4 batch 24 landed: the entry pages (공유 링크 for /s, /m, /p and /lb; 온보딩 as the board's three steps with an interim typed step 1; 이메일 로그인; the update gate's words), over migrations 00738 and 00739 (the share preview's added keys) and an auth gate that left the route file (a pure decision, shared public paths, a door to the legal documents for a signed-out person). 84 of 88 pages are rebuilt (the earlier statuses ran one low). The four left: the web share pages (batch 25, next) and three that wait for W5 (시작, 이메일 가입, 연결된 계정). After batch 25: the follow-up batches from the owner's nineteen decisions (order and rulings: the hand-over's follow-up plan), then the release gates.
Status, 2026-11-11 (14). W4 batch 25 landed: the web share pages (/s, /m, /p, /lb as server pages with a client island over the same models the app's receivers use; the ink link card from one shared function; migration 00740). 85 of 88 pages are rebuilt; the three left wait for W5 by the owner's word (시작, 이메일 가입, 연결된 계정). The first follow-up batch from the owner's nineteen decisions landed with it (F1a: the club row on 공개 프로필, 클럽 알림 in 클럽 홈). Next: the remaining follow-up batches in the order of the hand-over's follow-up plan (the scorer's 10-minute correction; the level-up celebration; offline; reports and social; draws; money; 사유 인정 요청; the 대항전 loop), then the release gates.
Status, 2026-11-11 (15). Three follow-up batches landed together: F3a (the scorer's 10-minute correction, migration 00741, with the replay made silent and, in 00743, made to keep self-assessed starts), F1b (the level-up celebration, 00742) and F1c (offline: one root strip, warm pages show, the kit disables writes, every write in one of two offline classes, staff reads hidden). Four of the nine follow-up batches are done (F1a, F3a, F1b, F1c); 85 of 88 pages stay rebuilt. Next, in order: F3c (the windowed rating replay, a ruling of this batch), F4 (reports reach officials, a block ends a friendship, the manage counts), F3b (draws), F2 (money), E (사유 인정 요청), C (the 대항전 loop), then the release gates.
Status, 2026-11-11 (16). Three more follow-up batches landed together: F3c (a correction recalculates ratings from the corrected match forward; migration 00744), F4 (reports reach a club's officials, a block ends a friendship, the manage counts; 00745 to 00747) and F3b (진출 인원 1 or 2, choosing who plays; 00748 and 00749). Seven of the follow-up batches are done (F1a, F3a, F1b, F1c, F3c, F4, F3b); 85 of 88 pages stay rebuilt. Next, in order: F2 (money, two cuts: 회비 한 번 and the 장부; the self-cancel refund and a 매치's own account), E (사유 인정 요청), C (the 대항전 loop), the owed list, then the release gates.
Status, 2026-11-11 (17). The money batch's first cut landed: 회비 한 번 (migration 00750: a joiner is charged while the due date is ahead; a one-off with charges is locked) and the 장부's date, opening balance, receipt and download (00751: the balance counts from the opening's date). Seven follow-up batches and half of the eighth are done; 85 of 88 pages stay rebuilt. Next, in order: the money batch's second cut (the refund on a self-cancel; a 매치's own account), E (사유 인정 요청), C (the 대항전 loop), the release list, then the release gates.
Status, 2026-11-11 (18). The money batch's second cut landed: the refund on a self-cancel (migration 00752) and a 매치's own account (00753). Eight follow-up batches are done (F1a, F3a, F1b, F1c, F3c, F4, F3b, F2); 85 of 88 pages stay rebuilt. Next, in order: E (사유 인정 요청), C (the 대항전 loop, which also closes the insert hole on challenges), the release list, then the release gates.
Status, 2026-11-12 (19). Follow-up batch E landed: 사유 인정 요청 (migration 00754: the request, the one authority rule, the answer with a 10-minute undo, the delayed notice). Nine follow-up batches are done (F1a, F3a, F1b, F1c, F3c, F4, F3b, F2, E); 85 of 88 pages stay rebuilt. Next, in order: C (the 대항전 loop, which also closes the insert hole on challenges), the release list, then the release gates.
Status, 2026-11-12 (20). Follow-up batch C landed: the 클럽 대항전 loop (migrations 00755 to 00757: propose, answer and cancel as functions, the session tag, the other club's members joining, results settled from matches; the insert hole on challenges is closed). Every follow-up batch is done (F1a, F3a, F1b, F1c, F3c, F4, F3b, F2, E, C); 85 of 88 pages stay rebuilt and three wait for W5 by the owner's word. Next: the release list's items that need no device, then the release gates.
Status, 2026-11-12 (21). Release-list batch R landed (migrations 00758 to 00760): the board lint is in the gate; the share preview's fields; an invite code's signed-out preview, its throttles and the lookup-before-join rule; three open privileged functions closed; the group-stage hold; the kit's offline tests. Of the release list, S10, S12, S14, S15, S16 and S17 are closed; what remains needs a device, the build or the owner. Next: the engineering pass (two read-only audits of 2026-11-12, six behaviour-preserving waves; its first tests are in), a review with the owner, then the owner pushes the build.
Status, 2026-11-12 (22). The engineering pass's first wave landed (migration 00761): characterization tests for seven untested modules on risky paths; one redaction in front of every telemetry sink; the server's excess grants revoked, anonymous writes bounded, one developer gate, three invariant tests; the on-device query cache as an allow-list. Next: wave 2 (a network failure never signs a person out; the confirm Sheet stops swallowing a failure; the server's swallowed-error sink and watchdog; the withdrawal scrub), then waves 3 to 5, a review with the owner, and the owner's build.
Status, 2026-11-12 (23). The engineering pass's second wave landed (migration 00762), with the third wave's rule-layer lane: a network failure never signs a person out; the confirm Sheet stops swallowing a failure; one key helper on a secure random source; the server records swallowed errors, its watchdog counts failed runs, and a withdrawn person's sign-in record is scrubbed at the 30-day anonymization; rules take time and randomness as parameters and no rule function is nested deeper than three. Next: server data validated at the boundary, one membership rule in the policies, limits on enumerable lookups, then every write naming where its failure shows.
Status, 2026-11-12 (24). The engineering pass's third and fourth waves landed as one cut (migration 00763): the bundle results validated at the boundary; adapters that no longer hide a failed read; one membership rule and per-query identity in the policies; limits on enumerable lookups; one reader for a failed write's words, every mutation hook naming where its failure shows, and the clubs, sessions and profile families moved from error toasts to the page. Next: the last wave (precise invalidation and bulk reads, rules out of screens, misleading names, index hygiene, a schema snapshot), then the UI/UX pass, a review with the owner, and the owner's build.
Status, 2026-11-13 (25). The engineering pass's last wave before the build landed (migration 00764) together with the closure of a security review (migration 00765: membership only by the person's own act, key columns immutable to clients, entitlements and scores written only by functions, an anonymous column allow-list): exact cache refreshes and bulk reads; rules in the domain, one date module, a use case for the match history; route ids validated at the door; the adapter registry split; eighteen unused write hooks deleted; duplicate indexes dropped, foreign-key indexes added, dead write grants revoked, money checks; a generated schema snapshot. From the pass's start: error toasts 119 to 19, lint ratchet waivers 123 to 46, files with recorded debt 523 to about 500. The sixth wave is deferred past the build. Next: the UI/UX pass (the clubs tab's top page rebuilt on its board first), then a readiness pass over the rest of the app, then a review with the owner and the owner's build.
Status, 2026-11-13 (26). The UI/UX pass began: a membership offer now has the person's side (a notice, the row in the 클럽 tab, 수락 or 거절 on the club's page; migration 00766) and the officials' side finished; the 클럽 tab's top page is rebuilt on its board, the last page on the old card design; one page-state helper serves sessions, records, the 경기 tab, profile, settings, messaging and home (the clubs family next); the web shows a loading frame, shares through the port and carries the brand link card; the kit lost its legacy sliders, badges, glyphs and unused parts. Next: the clubs family's states and the tab bars, wave C (words, dates, sheets, tables), then the readiness pass, one rendered check, the review with the owner and the owner's build.
Status, 2026-11-13 (27). The UI/UX pass is complete in code (the clubs pages on the one state helper, the tab re-tap, one date editor and the 무료 | 유료 fee form, one dismiss word, 매치 everywhere, the small rules, fifty-one club-home frames as tests) and the readiness pass's first wave landed with it: the network layer no longer re-sends writes or sends a lapsed session's requests as the anonymous role; native configuration is settled for the build; profile activity is private by default (migration 00767). Next: readiness waves 2 and 3 (colour tokens, the type ladder, state ink; test data, real-UI tests and phone flows, the QA documents), then the one rendered check of every page in light, dark and large text, the review with the owner, and the owner's build.
Status, 2026-11-14 (28). Batch 59 closes the readiness pass (waves 2 and 3): one colour system a gate vouches for (the library's medals, a dark success fill, one amber, state as ink, no raw colour or shadow in the kit), one type ladder with nothing under 12 px, the web's scheme set before first paint, test data for every feature built since the update pause with no account password in source, ten real-UI page tests, a gate against tests that read the real clock, fourteen rewritten phone flows (not run: they need the new build) and the QA documents reset to this build (List 53, the pre-build and post-build checklists). No migration. On 2026-11-14 the owner asked that best practice be adopted for every open decision: 92 answered (68 kept, 20 changed, 4 the owner's); the changes are built in batch 60, before the rendered check. Record: docs/history/rebuild-log.md "Flat v2 build, batch 59".
Status, 2026-11-14 (29). Batch 60 builds the answers adopted from the research: a block reaches profile details, the photo and offers; a health reason on an excusal request needs its own consent; the rating replay follows one total order (tied timestamps no longer force a full replay); ten retired functions are closed and the automatic score call is off; comment and chat boxes keep unsent text; 참석, 테니스장, 경기 방식 and 운영진 where they belong; the soft primary; the dues order; a lighter dark platinum; the Android icon's ink (the native fingerprint changed). Migrations 00768 and 00769. Next: the hosted seed and reads the adoption allows, then the one rendered check of every page (light, dark, 200%), the review with the owner, and the owner starts the W5 build. Record: docs/history/rebuild-log.md "Flat v2 build, batch 60".
Status, 2026-11-14 (30). Batch 61 fixes what the rendered check found: every page was photographed from the built web app with seeded data and compared with its board. Three pages that fell over on every load, the home news, wrong not-found walls, headings off the page margin, unreadable letters on coloured tiles in dark, the won sign and offline read as empty are fixed; a held seat is no longer shown as lapsed (the session bundles return every key their pages read, migrations 00770 and 00771); a gate holds that a read selects what its mapper reads; the QA world holds every state the pages draw and changes only its own rows; jest runs are bounded. Nothing ran on a device; the pictures are retaken from the rebuilt web app next, then the review with the owner, then the W5 build.
Status, 2026-11-14 (31). Batch 62 fixes what the second set of pictures found: 홈 for an account with a match in play no longer falls over (its match Block shows its loading form until the players' names load), a person's name is never blank (신규 멤버, and 탈퇴한 멤버 only for an anonymised profile), and in a release build a drawing rule that fails leaves out its own component and reports, instead of costing the page (the tab bar and a Notice keep their throw). Nothing ran on a device; the pictures are retaken next, then the review with the owner, then the W5 build.
Status, 2026-11-15 (32). Batch 63 builds what the lead's own review found (four methods over the build of batch 62 and the local stack with batch 63's migrations applied, each finding fact-checked and ruled; batch 63's client changes have not been seen in a browser, and the lead re-runs the measuring mode and the journeys after the build): migration 00772 closes six reads that were wider than the canon allows, the tabs sit in the reading column at desktop width, the chat and comment boxes share one visible field and fills are seen ($surfaceSecondary retired), a sent offer has its own heading, required consents come from the sign-up catalog, one way to write money, time, date and rating, the owner's table words, keyboard reach and 44 tall link rows on the web. The grounding of fifteen boards was re-accepted by name after their frames were read; seven older boards keep their warning. What is not held: the venue observations table, the region board's blocked person, 71 P0002 sites, and a device run (docs/release-evidence.md S48).
Status, 2026-11-15 (33). Batch 64 builds what the final run of the lead's review found (the built web app after batch 63, a third reading of every picture, each finding fact-checked and ruled): migration 00775 orders the chat list by its last message, friend lists refresh together, a person's region reads by name, 총 경기 counts the three rated tracks, the browser offers no push and rings only for the keyboard, one mark per corner of the club photo, a wizard saves a draft only after an answer changes, achievements draw their symbols, 참석 for "RSVP". The grounding of eighteen boards was re-accepted by name; the older drift stays. What is not held: batch 64's client changes in a browser, a device run, and R15 (one count under 총 경기, before launch) (docs/release-evidence.md S49).
Status, 2026-11-15 (34). Batch 65 builds what journey J17 found: a Toast no longer sits on a page's bottom button. Every bottom-pinned primitive (the band, a placed FAB, the two composers) reports its height through one seam, counted only while its screen is focused; the wizard footer hook and the cta-band-swap codemod are gone. What is not held: the stacking on a phone, and a Toast while the keyboard is up (NAV-10) (docs/release-evidence.md S50).
Status, 2026-11-15 (35). Batch 66 builds the owner's four marks (EYES-1, EYES-2, CALL-5, MEM-6): dark soft fills read 1.5:1 on the page and the card (gate and canary), an achievement tier is told by frame and star count with a locked tile as an outline, another person's 총 경기 and 승률 count 경쟁 matches only while one's own count every match with 경쟁 and 친선 separated in 기간 기록 (migrations 00776 to 00778), and a person the official blocked says so on the row while the other side gets a reason that never names a block. Not built, on purpose: telling an official that a member blocked them. The first W5 build failed on the splash drawable; the fix rides in the next build (docs/release-evidence.md S51).
K1b sweep: the judgment table
The lint rule no-legacy-text-and-button and the codemods text-role-v2, button-to-v2 and icon-button-to-v2 (scripts/codemods) do the mechanical part of the K1 sweep: pageTitle → hero, displaySm → title, cardBody → body, navTitle → headline, cardMeta → foot (one line) or footWrap, ActionButton action and Button variant → tone, xs → sm, and the three icon buttons → IconButton. Everything else carries a // TODO(k1b): judgment marker or a review line, and is settled by this table:
| Legacy form | v2 replacement |
|---|---|
cardTitle | headline when it titles a row or a card; title when it titles a section |
badge | the Tag component when it names a state; otherwise count |
micro | caption |
displayMd, displayLg, displayXl | display (one size: the kit has one) |
cardMeta without a literal numberOfLines | footWrap with an explicit color="$textSecondary" (the safe default: wrapping meta never truncates); foot when numberOfLines={1} |
ActionButton action="destructive" | Button tone="danger" |
shape="pill" | tone="tonal" with size="sm" |
shape="circle" | IconButton |
RemoveIconButton with confirm | IconButton tone danger, plus the existing ConfirmSheet at the call site |
K2b sweep: the rows
The lint rule no-legacy-rows and the codemods fact-list-to-cells, settings-row-to-cell, picker-row-to-cell, edit-field-row-to-cell and toggle-pill-split (scripts/codemods, on scripts/lib/cell-codemod-kit.mjs and cell-codemod-flow.mjs) do the mechanical part of the rows sweep. The Cells inside a group come out with a hairline between them, which supersedes the FactRow header's 2026-09-06 "no divider between peers" line (the library's rule; the lead's ruling 2, 2026-11-01). A converted RowList becomes the CellGroup; other runs get their own. Everything the codemods list in their review is settled by this table:
| Legacy form | v2 replacement |
|---|---|
FactList / FactRow item: label, value | Cell (static); onPress → nav; editCue → edit (spoken "{label}, {cue}"); locked → locked; note or a string secondaryValue → line2; tabularNums → tabular |
FactRow with an emoji icon | the Cell icon is a Lucide glyph: drop the emoji, or pick the glyph; the list-card glyph stays on the card |
FactRow with a JSX value, valueAccessory, valueTone | a state is a Tag in a tag Cell (tag, nav); a swatch or logo thumbnail leaves the row to the section that owns it |
FactList with revealedItems / disclosure | a disclosure Cell (variant="disclosure") opening the rest of the group |
FactSegments as a value | one Cell value for the main fact and line2 for the supporting facts; segments are joined by the Cell, never by a middle dot |
FactList items={rows} (a built array) | the builder returns CellProps objects and the screen renders <CellGroup>{rows.map(...)}</CellGroup> |
PickerRow inside a LabeledField | PickerCell carrying the field's label, required, hint (caption) and error; the unset text is the Cell's "선택" |
EditFieldRow | Cell variant edit: the label moves from over the value to its left; a long value shares the row |
SettingsRow | Cell: danger → destructive, right → trailing (an action Cell does not press), route → nav with appRouter.push; the web open-page fill has no Cell twin |
AdminNavRow | TaskCell (already retired) |
ListRow with chevron, trailing, tone or selected and no leading | Cell (nav, destructive, tag, action); an entity row (with leading) waits for PersonRow (K3) |
TogglePill in a Save form (a yes or no) | CheckRow kind="check" (library §1.22 rule 1): create-club step 1, create-session step 4, match rules, club settings that wait for Save |
TogglePill that applies at once | Cell variant="toggle" with the Switch: notifications, signal preferences, developer mode |
SelectionCheck | CheckRow with lead (the friend picker); ConsentGroup for the consent screens (legal strings verbatim) |
SelectionCard with meaning copy | CheckRow kind="radio" lead; swatch and preset tiles stay SelectionCard |
EloFactRow, CellRow, RowList dividers on block peers | stay: a Block, the Strip's columns, and block peers |
K3b sweep: the heads and states
The lint rule no-legacy-heads and the codemods section-block-to-head, empty-state-to-kind, expand-toggle-to-label and info-hint-term (scripts/codemods, on scripts/lib/head-codemod-kit.mjs) do the mechanical part of the heads sweep; a --write run formats what it changes with prettier. What a codemod cannot decide it leaves in place with a // TODO(K3b): judge: <why> marker and lists in its review; the sweep agent settles each marker by this table and removes it. no-hand-rolled-disclosure is inverted by review 52: the kit draws the visible fold word, so the words come from the label form of ExpandToggle and a hand-written one is the error. bounded-variant-enums bounds EmptyState kind to the kit's seven.
| Legacy form | v2 replacement |
|---|---|
SectionBlock around rows | SectionHead over the rows, inside a YStack root the codemod keeps; the head owns the rail, so the rows under it must not pad it again (list-rows-own-rail, no-double-rail) |
SectionHeader (list, flat, block, feed) | SectionHead; flush, dense, variant, titleLines and tone="default" go; first stays (the page's first head) |
link={{ label, onPress }} | action (the door pill; spoken "{label}, {title}", so a link with its own aria-label is a judgment); meta → count (one string, or an array of facts) |
right= (12 sites) | a way on → action; a scope → choice; a verb for the group's rows → bulk (the one allowed verb); a control for the rows below belongs under the head, not in it |
leading=, tone="danger" | the head has neither: the danger shows in destructive Cells; a tier dot or glyph moves into the rows |
command (알림 모두 읽음) | leaves with the page's container change (L3-13): the same verb is in the header menu |
ActionPill | no consumer; deleted with the old heads |
lone ExpandToggle (chevron only) | the count form: label="4개 더 보기" (the codemod writes the old word; the person writes the count), openLabel only if not 접기; a summary becomes the count |
accordion ExpandToggle (children) | a Cell variant disclosure with the folded rows under it |
InfoHintButton aria-label | term: the source string of the copy key without " 안내" |
EmptyState (variant, icon, subtitle, actionLabel) | kind (read from the title key: error, offline, permission, filtered, else first; a conditional title takes a kind per branch), compact, glyph, body, action, secondary |
the action Button of an EmptyState | tonal; primary only where it is the screen's one action (every site the codemod lists) |
QueryErrorState, OfflineEmptyState, NotFoundState, UpdateRequiredScreen | converted by hand onto kind (the wrappers' definition files are skipped); the state copy is the kit's, so common.noConnection, common.genericError and offline.subtitle stay only where a toast or banner reads them |
FeedEnd line | leaves with 알림; doors for a feed whose end has a way on, else nothing |
K4b sweep: the chrome
The lint rule no-legacy-chrome and the codemods frame-to-sheet and shell-props (scripts/codemods; frame-to-sheet reads a Frame's Button tail through scripts/lib/sheet-footer-source.mjs) do the mechanical part of the chrome sweep; a --write run formats what it changes with prettier. What a codemod cannot decide it leaves in place with a // TODO(K4b): judge: <why> marker and lists in its review; the sweep agent settles each marker by this table and removes it. The rule bans, outside packages/ui, the old header pieces (AppHeader, HeaderTextAction, HeaderIconButton, HeaderActionsMenu, WebNavCovered, APP_HEADER_HEIGHT, resolveAppHeaderHeight), <ModalPanel.Frame> and <ModalPanel.Overlay>, and the shells' headerRight, headerLeft, titleLeading and subtitle; the files that still write them are waived per file in eslint.ratchet.config.mjs. The ModalPanel root stays (the Modal, the keyboard avoidance and the placement that Sheet and Dialog stand on). Interlocks changed with the rule: no-bottom-sheet-outside-dev-panel and no-bare-scrollview-in-sheet judge only Tamagui's Sheet (the 'tamagui' source), so the rename SheetPanel to Sheet can land; no-padding-top-in-features takes the ui Sheet and MainTabShell as anchors; no-card-on-canvas treats every MainTabShell as canvas (the four tinted tab roots move to it); heading-has-level and COMP-4 name the Header as the level-1 title.
| Legacy form | v2 replacement |
|---|---|
DetailShell / MainTabShell / WizardShell subtitle | sub (a string, or an array of facts); the codemod renames it |
titleLeading (the live PulseDot, 3 sites) | a live segment of sub: a dot and "진행 중" (the kit has no leading slot) |
headerRight as a registry glyph | actions, at most two of the eleven glyphs: MessageCircle and MessageSquare → message, UserPlus → plus, Share2 → share, bookmark → bookmark, the bell → bell, Settings → gear |
headerRight with Users (club members) | no people glyph exists: the 멤버 door moves into the page body (a door pill or a Cell) or the ⋯ menu |
headerRight with Pencil and Edit/Done (SessionDetailHeaderRight) | the Edit/Done mode pair; the bookmark, share and ⋯ stay as actions while not editing |
headerRight as an XStack of three controls | two actions and the menu: the third control moves into the menu or the body |
headerRight / HeaderActionsMenu (8 sites; the function form with solidChrome) | the menu of a more action: the Header draws the glyph and opens the MenuSheet (sheet under 600, anchored from 600); management first, the destructive items last in tone="destructive", each behind its confirm; the over-photo ink is the Header's |
headerLeft (select-mode ✕, session edit 취소) | mode.left: a tonal Button, with the right Button (Done, 삭제) beside it |
HeaderTextAction (Edit/Done, 관리, 초대 코드, 제출 내역) | a mode Button pair, or a Button in the body when the page is not editing; never a text link in the bar |
HeaderIconButton | inside packages/ui only: CollapsingTopBar ports onto IconButton and the component goes with the last caller |
WebNavCovered around a header node (home bell, profile message and settings) | coveredByWebNav on the header action; the Header hides it at $medium on web. The activity create button keeps its wrapper until its page wave |
navRoot (backCoveredByWebNav, 3 pages) | navRoot stays on the shell and the shell omits back |
AppHeader in venue-search-takeover | a Header with close; the in-body VenueFinder field stays until K6 (FvkSearchTakeover) |
AppHeader chrome="bare" in the three score sheets | the Sheet's own title row with back |
APP_HEADER_HEIGHT / resolveAppHeaderHeight (dm-thread keyboard offset) | headerHeight() of the Header: 56, or 64 with a sub line |
the MainTabShell title block (24/32 in a $4 block) and its ground="tinted" | the title moves into the bar (20/28 at a tab root); the tab root is canvas. no-card-on-canvas now asks a // figure: note of every Card in a tab-root file |
ModalPanel.Frame whose first child is a headline Text | Sheet title=; the Frame's $background, paddingHorizontal="$4", paddingRight="$8" and paddingBottom go, a centred title goes left, the × is the Sheet's. A block of the title and one more Text is title and sub |
| a Frame's Button tail | footer={<SheetFooter …/>}: one lg Button, or a pair with the tonal cancel left and the primary right (equal for two choices of one weight, as in consent); an error line is the footer helper |
a Frame whose body is one ModalPanel.ScrollView | the body itself: the Sheet scrolls its own body (the codemod unwraps it); a ScrollView with other props, or a Button left in the body, is a person's call |
a Frame with no headline Text first, or a conditional title | a person writes the title; a Sheet always has one |
menu Frames: club-manage-sheet, notification-menu-sheet, post-row ⋯, MemberActionModal, guest-action-modal | MenuSheet (a title, groups of commands, the destructive group last); notification-menu-sheet empties the last no-legacy-rows waiver |
hybrid Frames: gallery-section item actions, dues-action-modals | MenuSheet whose destructive row opens a Dialog, or a small form Sheet for the note |
edit-delete-action-modal (the two-row kebab: regular meets, club groups) | retires: the row press opens the editor Sheet, and 삭제 is the editor's destructive footer helper (meets did this in batch 2; groups follow) |
| value-picker Frames: roles, groups, scope, list controls, cover, format chooser, correction reports | ValueSheet (options or groups); a Sheet of CheckRows where a search field or a confirm step is drawn (the guest picker, transfer ownership) |
| form Frames with a footer (18) | Sheet + SheetFooter; a two-step sheet (add round, create tournament) keeps both steps in one Sheet with back |
back-header Frames: the three score sheets, match-profiles-sheet | Sheet with back |
| dialog-like Frames: challenge accept, push permission prompt, tier promotion | Dialog |
| info Frames: the tier info sheets, member stats, the privacy preview | Sheet with one lg Button or none |
quick reactions (post-row reactions, dm-thread reactions and message actions) | stay on ModalPanel until K6 (quick) |
| the Daum postcode panel (native and web) and the web datepicker shim | Sheet tall (the kit's 88% max height, no title row): no new primitive |
the empty placeholder Frame in club-attendance | removed |
CollapsingTopBar (4 DetailShell sites and club detail) | stays behind header="collapsing" until the MediaBand wave, ported onto IconButton; the owner's scrim and bare glyphs against the kit's 40 circles is on the record page |
WizardShell's own header row | WizardHeader (the kit's wizard header); its BottomCtaBand footer stays until K6 |
Shells lane, the sites as ruled (2026-11-02). The shells draw the Header inside their content column (DetailShell wraps its bar and body in a page-wide ContentColumn with cap; the bar sits in a column-wide box, so its fill is the canvas the shell paints edge to edge and its scrolled edge is the column's width). HeaderAction is a press (icon, onPress, optional selected for a toggle) or a menu (icon: 'more' | 'plus', menu), with badge, label, testID and coveredByWebNav; the Header opens the menu itself (HeaderMenu: the anchored Menu from 600, else the MenuSheet). The scroll edge: each shell provides ScrollEdgeProvider; FeedList and GroupedFeedList report their own vertical scroll; a page with a body of its own renders ShellScrollView.
| Site | Ruled as |
|---|---|
clubs tab (ClubJoinHeaderAction + the +) | a newcomer: 초대 코드 as the bar's one tonal mode.right Button; a member with clubs: a plus whose menu offers 클럽 만들기 and 초대 코드로 가입 (clubsHeaderBar) |
| home bell | bell with the unread count, coveredByWebNav (useBellAction; NotificationBell and its CornerCountBadge use leave) |
| profile tab (메시지 with the unread dot, 설정) | message (dot badge) and gear, both coveredByWebNav; the tab root is canvas |
| club members (Users) | message named 새 그룹 채팅: the chat the members would start (no people glyph exists) |
| 메시지 (new group), friends (add), records (share) | message, plus, share |
| public profile (message, share, ⋯) | share and the ⋯; 메시지 보내기 is the menu's first command (the Header holds two glyphs) |
| notification center | the ⋯ menu (선택 · 모두 읽음 · 알림 설정); selecting swaps the bar to mode.left 취소 (the footer holds 삭제) |
| session detail | browsing: the bookmark (useSessionSaveAction, selected) and the ⋯, whose first row is 수정 for a host; editing: mode 취소 · 완료. The live dot is a { live } sub fact (none on the collapsing bar) |
| public club profile, directory venue detail, club detail | the collapsing bar draws actions (bare white over the photo, ink once solid): share; the ⋯; the 관리 gear (badge dot) with the ⋯ |
| venue search takeover | Header with onClose; the in-body VenueFinder field stays until K6 |
| create-club, create-session, onboarding | WizardHeader through WizardShell (canvas); create-club's and create-session's step bodies render ShellScrollView |
the 8 sub sites | sub (a string); no page passes more than one fact yet |
Rail readers moved to useRail() with RAIL_RESPONSIVE still false: the Cell and its hairline, FlatColumn, RailBleed, the new RailPad (the SEAM.rail horizontal paddings of seven feature files), the Divider bleed, BottomCtaBand, EmptyState, ExpandToggle, ChoiceSet's room, the Section head, MediaHero, TypeHero, the skeleton page, the menu clamp, and the rail-bearing scroll presets through useScrollContent (the vertical-only presets stay constants; SCROLL_CONTENT.page, railedList and railedBanded stay only as deprecated constants for club-attendance-screen, an overlays-lane file). A literal paddingHorizontal="$4" (188 sites) is not the rail yet.
K5b sweep: the people rows
The lint rule no-legacy-people-rows and the codemods list-row-to-person-row, avatar-bubble-to-avatar, meta-segments-to-meta and person-row-app (scripts/codemods, on scripts/lib/row-codemod-kit.mjs and head-codemod-kit.mjs) do the mechanical part of the people-rows sweep; a --write run formats what it changes with prettier. What a codemod cannot decide it leaves in place with a // TODO(K5b): judge: <why> marker and lists in its review; the sweep settles each marker by this table and removes it. A converted sub carries a marker too, always: the screen passes a context and facts through personRowModel, never a line-2 string (library rule 1). The rule bans, outside packages/ui, AvatarBubble and MetaSegments (imports), an entity ListRow (a leading or size="person"), VenueRow / VenueSearchResultRow / SessionCompactRow (elements), the app PersonRow with badge / avatarSize / avatarTone, and an AvatarStack that draws a doubles line (max={2} or two names); no-hand-built-person-row now routes an Avatar beside a name to PersonRow; no-double-rail and list-rows-own-rail know that PersonRow, TextRow, MediaRow, FeedItem and DayHeader own the rail.
| Legacy form | v2 replacement |
|---|---|
ListRow with a face in leading | the ui PersonRow: avatar={{ name, photoUrl }}, l1, l2; chevron, hairline and titleLines={NAME_LINES} go (a row opens its object and wraps a name itself); edge → first / last (the section trim); dense, tone, selected judged |
the app PersonRow (25 sites) | stays by name as the wrapper over the ui row (userId → the profile route and the prefetch, onPress={null} a plain box); sub → l2, badge → tag, subLines → l2Lines; avatarSize, avatarTone, chevron and hairline go |
sub (any string or list) | l2 from personRowModel(context, data, copy): the 22 contexts hold the fact order, the dedupe and the trailing parts; a screen passes the context and the facts. A list of facts that is no context's is l2 facts as given, never a join |
badge (호스트 / 게스트 / 예비 / 차단됨) | tag (one Tag in the trailing cluster; tagLabel only sharpens the fit estimate); a state that is the row's whole point is the context's tag |
trailing button(s) | control (the row's second stop: an sm Button or the ⋯), answer for the 수락 / 거절 pair; one trailing element otherwise (tag, level, figure, meta) |
unread dot (marker, markerSlot) | marker="unread"; a read row keeps the slot by itself |
Select mode's checkbox (check) | select on or off; a picker row keeps select="radio" / selected |
ListRow with an icon or glyph in leading (links, courts, host-confirm …) | TextRow (a count or Tag trailing), a nav Cell when it only navigates, MediaRow when a thumbnail belongs; per site |
AvatarBubble | Avatar: imageUrl → photoUrl; XS 28 → 32, SM 32 → 32, MD 40 → 40, LG 44 → 44, HERO 56 → 56, XL 80 and 2XL 88 → 96; no size was SM (32); tone and recyclingKey go; a size off the ladder (16, 30, 36, 64), outlined and label are judged |
AvatarBubble for a club mark | Thumb kind="club" (the photo, else the flat monogram); ClubAvatar keeps the flag medallion at 56 and up until the club hub wave |
AvatarStack with two faces (a doubles line) | Avatar group fit="overlap" (at most two); a roster of more faces keeps AvatarStack |
MetaSegments | Meta: segments → items, numberOfLines → lines; tier primary → default, secondary → role="footWrap" color="$textTertiary", caption → role="footWrap", eyebrow → role="foot" weight="700" color="$textTertiary"; foot → role="foot" |
MetaSegments foot as a row's line 2 | the row's l2 facts (the row draws them with the same break); a Meta beside a row is a screen fact line |
VenueRow (6 sites), VenueSearchResultRow, SessionCompactRow, same-court | MediaRow through a small adapter per view model (kind venue, session, sameCourt; compact for the 홈 row); the cards SessionCardV2, ClubCard and DirectoryVenueCard stay for their W4 pages |
a row host on SCROLL_CONTENT.railedList / railedBanded | SCROLL_CONTENT.list (the rows own the rail); inside a railed FlatColumn the rows sit in RailBleed |
DmThreadRow's unread count / dot | the row's marker; the group face is Avatar group |
ListRow itself | deleted with its last caller (part 3), with list-row-person.tsx, list-row-visual.ts, SelectionCheck and the PERSON_ROW pitch |
The app PersonRow wrapper (clubs package, 2026-11-02). packages/app/src/presentation/components/person-row.tsx keeps the name and takes: userId (the profile route and its press-in prefetch), name and avatarUrl (line 1 and the face; avatar replaces the face with a club's mark), onPress (null = a plain box) and onPressIn, a context with its data (the model draws line 2, the quote, the unread marker with its spoken label, and the trailing rank, level, figure and meta), the screen's own tag, trailing, explicit control / answer (or onControl / onAnswer for the ones a context draws: a context that asks for a control or answers the screen gave no handler for is rejected, not drawn inert), and l2 as given where no context fits. Explicit props win over the model's. RowCommonProps gained onPressIn.
Host flips. A list host on useScrollContent('railedList') / 'railedBanded' moves to SCROLL_CONTENT.list / banded once its rows own the rail; every other block in its header, loadingState and footer then pads itself (RailPad, or a head that owns its rail), a SectionHead or Divider loses its RailBleed / bleed, and rows inside a FlatColumn, a Well, a Sheet body or a padded block sit in RailBleed. list-rows-own-rail and no-double-rail now resolve useScrollContent('railedList' | 'railedBanded') (called, or bound to a same-file const) as well as the deprecated SCROLL_CONTENT.railedList constants (rules/railed-preset.js).
Entry doors. A navigation ListRow with an icon bubble (EntryIconBubble: 게스트 지명, 복식 파트너 추천) is a nav Cell with icon and line2 in a CellGroup; the bubble is deleted.
Profile, records and messaging (2026-11-02). An icon-leading nav ListRow (the contact channels, the records footer links) is a nav Cell with icon in a CellGroup under RailBleed; an achievement or a callout with an icon is a TextRow whose facts carry what the icon said. A club's mark at 16 in a hero fact is Thumb kind="club" (clubHeroFact), a club row's face is avatar={{ club }}. DmThreadRow is the threads context (the requests context with the answer pair on the request tab): the unread dot is the row's marker, the time its meta, a group's face is Avatar group (the first two members), and the open thread beside the list is a $primarySubtle fill around the row (the row has no selected-fill twin). The member picker's check box is the row's select. A tail-only GroupedFeedList passes its own renderSectionHeader: the default head bleeds the rail of a railed host and would start 16 off the edge here (list-rows-own-rail enforces it; the club dues roster and the friends list were the two that needed it). The leaderboard rows (LeaderboardRow over PlayerIdentityChip: a 조 column, a tappable tier badge, ELO delta, the points ladder) and MatchHistoryRow stay: they are data rows with slots the ranking context does not hold, and the chips and the identity card now draw the Avatar (avatarSize is a pixel step of the ladder, avatarTone is gone: rows are told apart by the name's weight).
K6b sweep: the forms
The lint rule no-legacy-forms (plus no-tamagui-input-in-features and no-hand-cta-band repointed) and the codemods labeled-field-to-field, styled-input-to-input, stepper-to-v2, cta-band-swap and pill-nav-split (scripts/codemods, on scripts/lib/form-codemod-kit.mjs, head-codemod-kit.mjs and, for the Stepper, ko-leaves.mjs) do the mechanical part of the forms sweep; a --write run formats what it changes with prettier. What a codemod cannot decide it leaves in place with a // TODO(K6b): judge: <why> marker and lists in its review; the sweep settles each marker by this table and removes it. Run them in this order, since the second reads what the first leaves: labeled-field-to-field, styled-input-to-input, stepper-to-v2, cta-band-swap, pill-nav-split. The rule bans, outside packages/ui, LabeledField, StyledInput, SearchBar, PillNav, BottomCtaBand and CTA_PAIR_GAP (imports), WonAmountField and SuggestedCountField (imports), an AttachedPanel that holds only text, and a Stepper without a label; Tamagui's Input from 'tamagui' is the no-tamagui-input-in-features finding now, and the ui Input is the control. The wizard primaryHint is not a finding: it stays the CtaBand's helper until its page retires it (Q4).
Dry runs against the tree at the time of writing (each conversion is one JSX site):
| Codemod | Converts | Marked for the sweep |
|---|---|---|
labeled-field-to-field | 96 LabeledField → Field in 44 files; 3 "n.length / limit" counters moved onto the input | 14 captions (the marker carries the caption's source), 1 collapsible section, 1 counter over a different value, 4 labelRight that are not counters |
styled-input-to-input | 53 Input, 11 TextArea, 4 secureTextEntry → password; 33 maxLength → limit (each carries a marker) | 3 bare, 1 flex, 1 minWidth, 1 autoFocus on a TextArea |
stepper-to-v2 | 10 end pairs → one label; 12 fullWidth → fill; 2 editable → typed | 15 pairs: 6 whose leaf the page must add ("경기 수", "운영비", "사용 코트", "코트 수", "요금", "수"), 8 whose leaves say 감소 / 줄이기 alone, 1 passed down |
cta-band-swap | 27 BottomCtaBand → CtaBand | 11 whose content is more than Buttons and a helper line (a composer, a conditional, a Pressable), 1 with a Button icon |
pill-nav-split | 1 underline PillNav → Tabs | 5 pills outside a form (a list surface or a Sheet option group), 1 pill inside a form whose handlers are not plain names |
| Legacy form | v2 replacement |
|---|---|
LabeledField caption (14 sites) | nothing: a field says nothing until it has an error (library 1.16). Per marker: delete the sentence, make it the error of the case it explains, or lift a consequence to a Notice; a unit moves into the Number field's unit, a rule into the placeholder example |
labelRight counter | the control's limit (the codemod moves the exact "n.length / limit" form); an Info hint moves into the label's own term or the Field's neighbour head; a count of picks is a head count (SectionHead count), not a label suffix |
LabeledField collapsed (create-club step 2) | a disclosure Cell over the list; the Field has no collapsible form |
LabeledField size="lg" | goes: one label style. A wizard step's title is the step's WizardHeader, not the field label |
StyledInput maxLength ≤ 12 | stays maxLength (a fixed format: phone, birth year, a code, a 4-letter monogram) |
StyledInput maxLength > 12 (33 markers) | limit (soft: the counter from 80%, "-3" past it, the CTA soft-disabled past it); a value the database caps hard keeps its cap in the schema and a limit of the same size in the form. 6 are computed (*_MAX): the page confirms each is free text |
StyledInput bare (the composers' inputs) | stays until the composer's page; a bare input inside a bordered parent is the Number field's unit form or a typed Stepper |
StyledInput textAlign, flex, minWidth | no twin: a right-aligned amount is the Number field (tabular), a flexed input is the row's layout |
StyledInput invalid | goes inside a Field with an error; a lone Input keeps invalid and an aria-label |
WonAmountField (3 sites) | the Number field: Input unit="원" tabular inputMode="numeric" inside a Field, with the won format on blur |
SuggestedCountField (3 sites) | a typed Stepper (typed, the unit in its label), the suggestion as the Field's placeholder example |
Stepper decrementLabel / incrementLabel (22 leaves) | one label ("참가 인원"); the per-site leaves retire with their callers, and the check-i18n-dead-keys baseline shrinks. A pair that says 감소 / 증가 or 줄이기 alone gets a label leaf from the page |
Stepper fullWidth / editable | fill / typed (the earlier names) |
AttachedPanel holding only text | the sentence goes, or a Notice where it states a consequence; the panel stays for nested controls (a Stepper and its receipt) |
SearchBar (3 sites) | SearchField (onCancel for the takeover's tonal 취소); the expandable filter bar's field is the Search takeover's header row |
PillNav underline | Tabs (pane): badge becomes count, badgeTone goes (a count Tag carries its own tone) |
PillNav pill on a list surface (records, the 경기 filters) | Tabs variant="filter"; the records scope is a Feed head's Choice when it scopes one question (library 1.17) |
PillNav pill in a Sheet or a form | ChoiceSet fill label={…} of Choices (two to four options; the codemod builds it from an inline { value, label } list) |
BottomCtaBand with Buttons only | CtaBand primary={{ label, onPress, … }} / secondary; size, full, flex go; tone is written only when it differs from the slot's |
BottomCtaBand with a composer, a conditional or a Pressable | the page: a comment composer is its own pinned row, a conditional pair is two bands chosen by state, a Pressable is a Button |
WizardShell primaryHint | stays the CtaBand helper (Q4); each wizard page retires it with on-press validation and scroll-to-error |
BottomCtaBand itself, CTA_PAIR_GAP | deleted with the last caller (part 3), with bottom-cta-band.tsx and bottom-cta-band-visual.ts |
LabeledField, StyledInput, labeled-field-logic.ts | deleted with their last callers (part 3), then search-bar.tsx, pill-nav*.tsx, won-amount-field.tsx, suggested-count-field.tsx |
K6b sweep: the feedback
The lint rule no-legacy-feedback and the codemods banner-to-notice, skeleton-to-kind, activity-indicator-to-spinner and show-toast-variant (scripts/codemods, on scripts/lib/form-codemod-kit.mjs and head-codemod-kit.mjs) do the mechanical part of the feedback sweep; bounded-variant-enums gains the Notice tone (neutral, success, info, warning, live, danger), the Notice glyph (the six) and the SkeletonKind kind (the eleven), each tested against the kit and the ui unions. The rule bans, outside packages/ui, Banner, Callout, Well, Skeleton, SkeletonCard, SkeletonRow, SkeletonHero and SkeletonFlatPage (imports), NetworkBanner outside apps/mobile/app/_layout.tsx, ActivityIndicator from react-native (also in packages/ui/src, the Spinner excepted), and a showToast with a third argument; no-error-toast (lane N) owns the error, warning and info variants.
| Codemod | Converts | Marked for the sweep |
|---|---|---|
banner-to-notice | 4 Banner and 2 Well → Notice; 3 icons dropped (not in the allow-list) | 15 Banners and Callouts whose tone needs a title and has none (5 info, 5 warning, 5 danger), 20 Wells that hold content, 1 Callout, 1 Banner with sublines, 1 computed tone, 2 others |
skeleton-to-kind | 93 SkeletonCard → kind="media", 15 SkeletonHero → kind="hero", 77 SkeletonRow → kind="cell"; adjacent siblings merge into n={count} | 15 SkeletonFlatPage, 6 Skeleton bars, 2 keyed rows in a loop |
activity-indicator-to-spinner | 9 ActivityIndicator → Spinner in 7 files (color dropped on 9) | none; the 5 files in packages/ui/src (cell-parts, action-icon-button, feed-list/parts, venue-location-picker, daum-postcode-modal) are done by hand |
show-toast-variant | 7 showToast(msg, 'success') → showToast(msg); 6 durations dropped (2 with an action keep their place as undefined) | none; 'error', 'warning' and 'info' calls are the no-error-toast ratchet's |
| Legacy form | v2 replacement |
|---|---|
Banner / Callout without a title (15 sites) | a Notice needs the state word on every tone but neutral (≤ 12 syllables), then the consequence as the body; the old text is usually the body and the title is new copy. A Banner tone="error" that reports a failed action is the page's error surface (helper, Notice or Dialog, Q1) |
Banner icon | the severity glyph is the default on warning and danger; a specific glyph is wifiOff, lock, clock, card or pin; any other icon goes |
Banner actions of two | one action (the verb that names the result) and the other as a door elsewhere on the page; a Notice has one |
Banner sublines (children), a countdown | CountdownNotice (the payment hold) or the body |
Well holding an EmptyState (the empty lists, 20 sites) | the EmptyState kind alone on the canvas; a Well is a pocket, and the empty state draws its own space |
Well holding facts (analytics, operations, completeness) | a Strip or a CellGroup on the canvas, or a Block where the page leads with it; a message in it becomes a Notice |
tappable Well (onPress) | a nav Cell or a door; navigation is never a Notice |
NetworkBanner | mounted once by the app frame as the host of the offline Notice (Q9); a screen never writes it |
SkeletonCard → kind="media" by default | the page fixes the kind (Q13): a session or club card is a media row, a hub Block is kind="block", a stat strip kind="strip", a field form kind="field" |
SkeletonRow → kind="cell" by default | kind="person" for a people list, kind="media" for a thumbnail row; a keyed row in a .map is one SkeletonKind n={count} |
SkeletonFlatPage (15 sites), Skeleton bars (6) | the page's own kinds in the order its sections draw (hero then strip then media); a hand-drawn bar goes with the legacy file |
raw ActivityIndicator in a control | Spinner (20; 28 for a large one); the control sets the colour. A spinner standing alone on a list or a screen is a SkeletonKind |
showToast with a duration | the duration goes; the store decides 4 s for a result and 8 s for an undo |
showToast(…, 'error' | 'warning' | 'info') (the lint's) | the surface the page picks for that failure (a field's support line, the CTA helper, a danger Notice or a Dialog; createMutationHook errorSurface, Q1) |
Banner, Callout, Well, well-tone.ts, Skeleton* | deleted with their last callers (part 3), with attached-panel.tsx once its text-only panels are gone |
K6b sweep: the surfaces
The lint rule no-legacy-surfaces and the codemods stat-strip-to-strip, segmented-tabs-chrome, hero-to-block and calendar-pieces (scripts/codemods, on scripts/lib/form-codemod-kit.mjs and surface-codemod-kit.mjs) do the mechanical part of the surfaces sweep; bounded-variant-enums gains the Strip face (tiles, cells, action) and the Block kind (day, elo, dues, identity), each tested against the kit and the ui unions. The rule bans, outside packages/ui, StatStrip, CellRow, CellRowGrid, StatTile, StatGrid, ActionIconRow, SummaryCard, DayDotsRow and PeriodScrubberShell (imports from the ui barrel), TypeHero in profile-screen.tsx and public-profile-screen.tsx only (the other eight TypeHero sites are the K7 Hero), EloFactRow, ActivityMetricCalendar, HeroCard, DayCalendarPicker, WeekMonthPicker and the dues MonthSelector (by name and module), and a SegmentedTabs without chrome="tabs" (chrome="none" stays: the host draws its own Tabs). PillNav is no-legacy-forms' finding and is not reported twice. A converted element's markers read // TODO(K6b): judge: <why>, as in the forms and the feedback.
Dry runs against the tree at the time of writing (each conversion is one JSX site):
| Codemod | Converts | Marked for the sweep |
|---|---|---|
stat-strip-to-strip | 12 StatStrip → Strip in 9 files (5 MetricValue slots became figure + delta; 7 tone dropped; 25 figures wrapped in String()) | 5 whose cells are built elsewhere (a variable or a .filter(): the two venue pages and the member-stats modal, StatStrip, StatGrid and both ActionIconRows), 1 single-cell strip, 2 lone StatTiles (the pickup host card); the builders' StatStripCell, StatGridCell and ActionIconRowItem types are the rule's findings too, and the codemod leaves them |
segmented-tabs-chrome | 5 SegmentedTabs → chrome="tabs" (clubs sessions, dm inbox, records leaderboard, spectator scorecard, the dev panel) | all 5 carry the "check badge / badgeTone against the Tabs count" marker, because their items are built elsewhere; chrome="none" (club detail) is left alone |
hero-to-block | 1 HeroCard → Block kind="elo" skeleton (records), 2 profile TypeHero → Block kind="identity" (profile, public profile) | 2 EloFactRow (the identity Block carries no rating); the Block skeletons are marked with every word the page adds |
calendar-pieces | 1 DayCalendarPicker and 1 WeekMonthPicker → Calendar (the two handlers renamed; the data props stay, named in the marker) | 1 ActivityMetricCalendar, 1 DayDotsRow, 4 PeriodScrubberShell (the three home pickers' own files and the dues MonthSelector) |
| Legacy surface | v2 replacement |
|---|---|
StatStrip / StatGrid cells built elsewhere (a variable) | map the builder to StripItems (label, figure as a string, delta as the formatted string); a .filter() that dropped a cell must keep the count in 2 to 4, because Strip throws outside it |
StatStrip with one cell | not a Strip: the one line (FvkLeftLine, a count restated under a header) or a Cell |
StatStrip tone, tabularNums | gone: a Strip draws one ink and always tabular figures; a state the colour carried goes into the label or a Tag |
valueSlot holding a node (not a MetricValue) | a figure string and a delta string; a badge or an icon in the figure has no twin |
MetricValue (club analytics) | the helper is deleted once its last valueSlot becomes figure + delta |
StatGrid delta (a signed number) | delta as text the page formats ("▲3"); the member-stats modal's three tiles are a tiles-face Strip |
StatTile pair (the pickup host card) | two facts beside an identity row are a Meta line or a Strip of two; the page picks, and no lone tile survives |
CellRow, CellRowGrid | deleted with StatStrip and ActionIconRow (their only callers are inside packages/ui); a cells-face Strip is face="cells" |
ActionIconRow of 5, or with disabled / active | a Strip holds 2 to 4 action cells: cut to four or split into a CellGroup; a disabled cell has no twin (hide it, or open a Dialog on press); the per-item aria-label goes (a Strip speaks the label), and figure: '' is added because the item type asks for one |
SegmentedTabs badge / badgeTone | count / countTone: a badge was a dot on a number and the Tabs draw the number; badgeTone warning and live have no twin (countTone is danger, neutral or ink; danger says the tab needs action), the page picks |
SegmentedTabs chrome="none" | stays while the host draws its own Tabs (the club place header); the pane engine is unchanged |
HeroCard (records) | Block kind="elo": the page writes label, eyebrow, meta, door and spark; matchesPlayed is meta, and the three activity rings have no Block twin (the ring is the dues Block's) |
profile TypeHero | Block kind="identity": titleBadge → trust (the 신뢰 door), facts → facts and level, leading → photoUrl; label is the spoken name. The join-review TypeHero stays until K7 even though the kit lists 가입 심사 among the identity pages (open: owner) |
EloFactRow (profile, public profile) | no home on an identity Block: a Strip cell linked to the tier sheet, or the 기록 page's elo Block; the page decides |
DayCalendarPicker, WeekMonthPicker | Calendar month (the week picker with the week as range): the page computes year, 0-based month, title, weekdays, today, selected and dots, and onSelect(day) replaces the "YYYY-MM-DD" handlers |
DayDotsRow (public club activity card) | a weekly pattern is words ("월·수·금" as a Meta) or a Calendar week when dates matter; the page picks |
ActivityMetricCalendar, ActivityMetricSelector | the 기록 page's Heatmap (K7); until it lands the calendar stays under its ratchet entry |
PeriodScrubberShell, the dues MonthSelector, MonthYearPicker | the dues month is the dues Block's period door (onDoorPress) opening a ValueSheet of months; the home pickers retire with the Calendar; the shell is deleted with its last caller |
StatStrip, StatGrid, StatTile, CellRow*, ActionIconRow, SummaryCard, DayDotsRow, PeriodScrubberShell, pill-nav*.tsx | deleted with their last callers (part 3) |
Status, 2026-11-15 (36). Batch 67 fixes the fault the second W5 build showed on the emulator: the phone build compiles with the React Compiler, which skipped a hook that batch 65's seam called through an unnamed expression, so every page with a bottom button threw on its second render. The hook is now bound to a use name, a test compiles the real file and renders it twice, and the lead's QA web build can turn the compiler on (TWOMORE_WEB_REACT_COMPILER=1; the public web app stays uncompiled, decision log S5). The compiled run then found a second fault of the same kind in the sign-in form (two refs created inside a literal); with both fixed it renders all 342 page states and passes the nineteen journeys, and check:compiled-hooks holds both forms in the gate. Preview builds enter through the developer account on the owner's instruction (decision log S6). What is not held: the third build and its boot-smoke, recorded when made (docs/release-evidence.md S52).